This chapter is for anyone deciding how far to trust Levee with money. It lists every key that can do something the public cannot, the checks that stand between a pool price and your funds, how the contracts are tested, and what is still open.
Who holds a key
The deploy uses five roles, with one funder per hedged vault, and DeployRobinhood refuses to run if any of them is the deploying key, if a funder is the hedger or the keeper, if the owner is a funder or either hot key, or if the two hedged vaults share a funder. The hedger and the keeper may share a key; the API signs both bots with one. After the deploy the deploying key is an ordinary address.
| Key | Kept | Holds |
|---|---|---|
| Owner | cold | the venue's admin calls: ProtocolConfig, AssetRegistry, FeeCollector, BuybackV2, and through ProtocolConfig the DLMM factory and the two venue vault factories |
| Hedger | hot, on the API server | hedge and sleeve reports |
| Keeper | hot, on the API server (may be the hedger's key) | rebalancing the DLMM and stock vaults |
Funder, one per hedged vault (FUNDER_NVDA_HEDGED, FUNDER_SPY_HEDGED, default FUNDER) | cold, never a hot key | moving margin from its vault to Lighter, within caps; and, off chain, the vault's Lighter account itself, including withdrawing from it |
| Deployer | used once | nothing after the hand-over |
The venue owner
The four owned contracts use OpenZeppelin Ownable2Step: the deploy proposes the owner and the owner must call acceptOwnership. The owner can:
- Pause the venue (
ProtocolConfig.setPaused). This stops swaps and liquidity adds on DAMM and stock pools, DLMM swaps, deposits and new pairs, deposits and rebalances in the DLMM and stock vaults, and new DLMM limit orders. It never gates an exit: the hooks have no remove-liquidity callback, andDlmmPair.burn, the position NFT'sdecreaseandburn, the venue vaults'withdrawand the limit orders'cancelandclaimdo not read the flag. - Direct protocol revenue. Set the protocol share of every swap fee, at most 5 000 bps; replace the fee collector; set the
FeeCollector's conversion routes, its slippage bound (at most 20 %) and where it sends ETH; sweep tokens that have no conversion route; tuneBuybackV2's price guard and caller reward. - Curate stock pools. List or delist an asset, choose its Chainlink feed and heartbeat, its fees (at most 1 000 bps) and its band, and set the session hours and holidays.
- Curate the rest. Enable DLMM bin steps and their fee presets (new pairs only), create DLMM and stock vaults, name their keeper and rebalance them itself.
None of these calls can take a liquidity provider's tokens. Two of them still matter to you: a wrong feed for a stock moves that pool's band, and providers would trade at it; and delisting an asset stops swaps in its pools until it is listed again. The LP-management contracts read nothing the owner controls.
The hedger and the funder
A hedged vault cannot see its Lighter position, so it believes the hedger's reportHedge(notionalUsd, unrealizedPnlUsd, equityUsd, asOf) and adds equityUsd to totalAssets() through the Chainlink ETH/USD feed. An Income note does the same with reportSleeve; a Protected note refuses it.
fundHedge(assetAmount) belongs to the funder alone. It can only post USDG to the one Lighter deposit contract fixed at initialisation, credited to the funder's own Lighter account; it moves at most maxFundBpsPerCall (1 000 bps) of the vault's on-chain assets per call and maxFundBpsPerPeriod (2 500 bps) per rolling day, measured against the position and idle balance rather than the reported equity; it sells at a floor taken from the USDG pool's TWAP; and it is refused while the vault is paused. No bot calls it. The USDG it delivers is added to the stored equity in the same transaction, and returnHedge (the funder again) takes the USDG it brings home off it, so margin on its way to or from Lighter never drops out of the share price; the hedger reports what is still in flight (see Margin in transit).
As keeper, the hedger may rebalance DLMM and stock vaults only when the contracts allow it: a DLMM vault once the active bin has drifted more than half its half-width, a stock vault on a session change or a large oracle move and only while the pool sits near the oracle; at most every 5 minutes, and never with a swap.
Contracts with no admin
The LP-management contracts (the three vault kinds and VaultFactory, both zaps, RangeOrders, PositionKeeper, LaunchPipeline, PreMarketPerp, FeeRouter and ReferralRegistry) have no owner, no pause switch and no upgrade path. Every address they call (Uniswap, WETH, Permit2, feeds, the Lighter deposit) is a constructor immutable or, for the vault clones, written once in initialize. On the venue side the Router, the DLMM pairs, position NFT and limit orders, LaunchPools and LaunchVault have no owner of their own either; the pools among them follow the venue pause and protocol share described above.
The vaults are EIP-1167 clones of three implementations. Each implementation locks itself in its constructor, and VaultFactory clones and initialises in one transaction, so no stranger can configure a fresh clone. What that leaves:
- A vault pays out only to whoever redeems its shares. A vault's cap and policy are set once, at creation, and never change.
PositionKeepermints a rebalanced position to its owner and pays only the owner, the caller's bounty (capped by the owner, at mostMAX_BOUNTY_BPS, 300) and theFeeRouter. A position that changed hands since enrolment revertsOwnerChanged.FeeRouter.takebelieves a reported fee only if the router's balance covers it (FeeNotReceived), and referral links are write-once.- No contract can withdraw from Lighter, and neither can the signer's order keys. The one key that can is a hedged vault's funder, for that vault's margin (see above).
LaunchPoolslocks each launch's seed liquidity for good; only the right to its fees can change hands.
Outside the Router and the hooks, functions that move tokens carry OpenZeppelin ReentrancyGuard. The two report functions carry it too, though they make no external call, so a report can never land in the middle of a deposit, exit or funding and move the share price under it. The Router keeps nothing between calls, and the hooks run inside the PoolManager's lock. The v3 zap approves exact amounts and clears them, while the v4 zap keeps standing Permit2 approvals, which is safe only because it never holds a balance (see the invariants below).
Price checks
The threat is the same everywhere: move a pool, get Levee to act at that price, move it back. TwapGuard.check is the shared answer. It reads spot and the mean tick over TWAP_WINDOW (30 minutes), compares them as prices, and reverts PriceDeviation when they are more than 300 bps apart. A new pool's observation ring holds one slot, so its "average" is the last swap; the guard therefore refuses to answer (TwapWindowTooShort) until the ring reaches back 10 minutes. VaultFactory and LaunchPipeline.track grow each ring to 60 slots, and anyone can deepen one further. The cost is availability: on a fresh pool, deposits, withdrawals and rebalances wait.
The guard is waived three times, on purpose. A vault with no shares skips it, so the first depositor into a freshly graduated pool can get in. harvest skips it, since it only compounds fees under a slippage bound and blocking it would let a manipulator stop a vault being paid. A Protected note stops checking the SGOV pool once it holds no SGOV, which is what settle is for.
Inside the 300 bps band, more checks apply:
PositionKeeper.rebalancevalues what it unwound and what it minted at the TWAP, and revertsSlippageif the round trip lost more than the owner'smaxSlippageBps. This catches what the swap floor cannot see.- Swap floors: the vaults and the keeper quote at spot after the guard; a zap quotes at spot and also enforces the
minLiquiditythe app derives from the price you saw;fundHedgeand the SGOV leg quote at the TWAP. - Mint minimums: the vaults allow 500 bps between the minted and the balanced amounts, the zaps your slippage. A zap refuses a slippage above 5 000 bps (
MAX_SLIPPAGE_BPS,SlippageTooHigh), so no call can switch its floors off. - Chainlink reads (
ChainlinkGuard) need a positive, complete answer under 25 h old. A bad read values a hedge or sleeve at zero rather than freezing exits, and makes the perp's index unreadable. - Stock pools end every swap inside their oracle band or closer to the oracle, and DLMM deposits carry an active-bin and minimum-amount guard; see Levee pools.
- Graduation measures pool depth as a 30-minute average, so liquidity added in the graduating block counts for nothing.
The pre-market perp stays solvent
PreMarketPerp has no counterparty; its USDG balance is all a winner can be paid from. Leverage is capped at 3x and a market's open interest at 5 times its virtual depth, which is at least 10 000 USDG. A third of each 30 bps taker fee feeds an insurance fund that absorbs bad debt first; what it cannot absorb becomes a deficit that the next profitable closers pay out of their profit, never their collateral. A payout never touches another position's collateral or the fund: whatever the contract cannot pay now becomes a deferred claim, collected with claimDeferred. Claims already waiting are set aside before any later close is paid, so USDG a realised loss frees goes to them first; among themselves they are paid first come, first served.
The index, a 30-minute TWAP times Chainlink ETH/USD, drives funding, capped at 1 % per hour and at most 24 hours charged at once. A liquidation must hold at both the mark and the index, so with the index unreadable liquidations wait while close stays open. Details are in Graduation and pre-market perps.
Off-chain keys
Perp orders go through a signer service that holds a per-user Lighter order key, encrypted at rest. That key can trade and cannot withdraw. The signer fails closed without a current mark, refuses a signed price more than LIMIT_PRICE_BAND_BPS (1 000 bps by default) from it or an order above MAX_ORDER_NOTIONAL_USD ($250 000 by default), and gives a market order at least 100 bps of slippage while rejecting requests above 1 000 bps. Every perp write to the API carries an EIP-712 signature from your wallet, at most 300 s old and with a fresh nonce. See Perpetuals.
How it is tested
- Unit and fuzz tests. 45 Foundry test files and more than 450 test functions, against locally built Uniswap v3 (compiled with solc 0.7.6 so pools keep the canonical init code hash) and v4. Fuzz tests take 256 runs; each top-level call is its own transaction so transient storage clears.
- Invariants. Four suites run with
fail_on_revert = true: the v3 zap holds no ETH, WETH or USDC and keeps no allowances; the v4 zap holds no ETH or USDC; a harvest never lowerstotalAssets()beyond 8 wei of rounding, and a vault with no shares holds no position and almost no assets; the perp's balance covers collateral plus insurance fund less deficit. Each suite also asserts that enough calls actually landed. - Fork tests. Suites under
test/forkandtest/venuerun against Robinhood Chain whenROBINHOOD_RPC_URLis set. Without it the venue suites are marked skipped, while the seven undertest/forkpass without asserting anything and printSKIP:, so a green run means little without the RPC. One of them funds a hedged vault's margin into Lighter's real bridge, checks that the deposit opens the funder's Lighter account and not one for the vault, and brings margin back throughreturnHedge. - Shared fixtures.
contracts/fixtures/zap.jsonandpayoff.jsonhold the zap and note payoff maths bit for bit identical in Solidity and in@levee/core. - Drift guards. Tests fail when an API query names a field the indexer schema lacks, when a wire type differs between API and web, or when a page calls a function the generated ABI does not have.
Open issues and limits
- Hedge margin in an operator account.
fundHedgedeposits through the bridge's realdeposit(address, uint16, uint8, uint256)(USDG asset 3, perp route 0) to the vault's funder, because a contract-owned Lighter account could never bind an API key. Until Lighter supports accounts owned by a contract or a delegated owner, the margin on Lighter depends on the funder key. The Income note is not open yet: its long has no funding path on chain at all. - Withdrawal size. A hedged vault sizes exits against its on-chain holdings, so a withdrawal larger than them reverts
Shortfalluntil margin comes back from Lighter. - The first depositor into a vault with no shares prices themselves, with no TWAP check. Shares carry three more decimals than the asset (1 000 virtual shares per virtual asset unit), so donating to a near-empty vault to round the next depositor down costs about a thousand times what it can take.
- Deferred perp profits are paid after the deficit haircut and only from realised losses; claims waiting are paid before later closes, and among themselves first come, first served.
- A frozen feed that keeps a positive answer is believed for up to 25 h.
- Polish items from the security review: the decimals offset on the ERC-4626 vaults, the bound on the zaps'
maxSlippageBpsand the reentrancy guards on the report functions are in; a nonce on signer requests is still open.
Reference
| Name | Allowed | Default | Effect |
|---|---|---|---|
TWAP_WINDOW | constant | 30 min | Window TwapGuard averages over, when the ring allows. |
MIN_TWAP_WINDOW | constant | 10 min | Shortest history TwapGuard accepts. |
MAX_TWAP_DEVIATION_BPS | constant bps | 300 | Spot against the mean, compared as prices. |
MIN_OBS_CARDINALITY | constant | 60 | Ring size the factory and pipeline grow pools to. |
MAX_FEED_AGE | constant | 25 h | Oldest Chainlink answer ChainlinkGuard accepts. |
maxHedgeAge | per vault, fixed | 1 h | Report age that pauses a hedged vault or Income note. |
maxEquityJumpBps | per vault, fixed bps | 1 500 | Report move that pauses it. |
maxFundBpsPerCall / PerPeriod | per vault, fixed bps | 1 000 / 2 500 | fundHedge caps per call and per rolling day. |
cap | per vault, fixed | none | Deposit limit, set once at creation. The seed, hedged and graduated vaults and the Protected note have none; the Income note deploys at 0, not open yet. |
MAX_SLIPPAGE_BPS | constant bps | 5 000 | Highest maxSlippageBps a zap accepts. |
SHARE_DECIMALS_OFFSET | constant | 3 | Extra share decimals on the ERC-4626 vaults: 1 000 virtual shares per virtual asset unit. |
MAX_PROTOCOL_SHARE_BPS | constant bps | 5 000 | Highest protocol share of a swap fee the owner can set. |
MAX_BOUNTY_BPS | constant bps | 300 | Highest bounty a kept position can offer. |
contracts/src/libraries/TwapGuard.solthe price guard, its window and its verdictscontracts/src/libraries/ChainlinkGuard.solhow the vaults and the perp read a feedcontracts/src/HedgedLPVault.solthe reports, the funder caps and the pausecontracts/src/venue/core/ProtocolConfig.solthe venue owner, the pause and the protocol sharecontracts/src/venue/core/AssetRegistry.solstock feeds, fees, bands and sessionscontracts/src/venue/periphery/FeeCollector.solthe owner-set fee routescontracts/src/PreMarketPerp.solthe perp bounds, payouts and deferred claimscontracts/script/LeveeWiring.solroles, hand-over and every deploy value abovecontracts/test/invariantsthe four invariant suitescontracts/foundry.tomlruns, depth, fail_on_revert and isolationservices/signer/README.mdthe signer trust model and its bandsdocs/LAUNCH-TODO.mdthe open review items