Liquidity provision
This page covers everything an LP (or an integrator acting on an LP's behalf) touches on SwapCore. Deposits mint shares against the pool's mark-to-market value, withdrawals burn them at the current share price, and a handful of view functions expose the pool's state.
Source: SwapCore.sol
How the pool works
Each market has a single collateral pool (Types.Pool) with three numbers:
totalShares— total LP shares outstanding.totalCollateral— total underlyingswapTokenthe pool holds (LP-supplied + retained swap payments, minus paid-out losses).lockedCollateral— the portion oftotalCollateralreserved against open swaps.totalCollateral − lockedCollateralis what can actually be withdrawn or used to back new swaps.
The three share prices
There is no single "share price." SwapCore maintains three distinct marks, and using the wrong one is the most common integration error on this page. Deposits and withdrawals price at different marks by design — the spread between them is what defeats the deposit → oracle-update → withdraw NAV sandwich.
Mint price
VirtualShareLib.poolMintAndAnchorPrices (mint leg)
Minting shares in supplyCollateral. The accrued-only upper bound, no vesting deduction.
Burn price
getPoolBurnPrice(marketId)
Burning shares in withdrawCollateral, and valuing a position via getLpValue. min(fair, accrued) lower bounds, vesting-deducted, liquidation-threshold-aware. Also seeds the profit-vest entryPrice anchor.
Fair / netted mark
getPoolSharePrice(marketId)
Informational only — non-transactable. The netted-fair midpoint between the two. No transfer in SwapCore prices at this value.
Mint prices at the upper bound and burn at the lower, so a deposit-then-withdraw round trip cannot extract value from the spread between them. (This is complementary to the per-LP profit-vest cap below, which is what blocks the slower deposit → oracle-update → withdraw sandwich.) All three are floored at Utils.MIN_SHARE_PRICE — an underwater pool reports the floor rather than zero so the math stays well-defined.
If you want the number an LP would actually receive, use getPoolBurnPrice (or getLpValue, which applies the per-LP cap on top). getPoolSharePrice will over-state it.
Mint flow (supplyCollateral): shares minted = collateralAmount × WAD / mintPrice, rounded down, where mintPrice comes from VirtualShareLib.poolMintAndAnchorPrices — not from getPoolSharePrice. For the first deposit (empty pool) the price defaults to WAD and one token unit mints one share. Shares and lastDepositBlock are credited to onBehalfOf. The same call returns the anchor price (the burn mark) that is stored as entryPrice; both are reported on the CollateralSupplied event, as mintPrice and entryPrice respectively.
Burn flow (withdrawCollateral): on a partial withdrawal, shares burned = amount × WAD / burnPrice, rounded up in favor of the pool, where burnPrice is getPoolBurnPrice(marketId) further capped at entryPrice while the LP's profit-vest window is active; a full exit (amount ≥ the position's full value) burns the LP's entire share balance directly instead. Shares are burned from onBehalfOf's balance (caller must be onBehalfOf or authorized); tokens are delivered to receiver.
Two gates LPs should know about:
1-block deposit lock.
supplyCollateralstampslpPos.lastDepositBlock = block.number, andwithdrawCollateralreverts withE413if you try to withdraw in the same block. Plus,supplyCollateralrequires auth foronBehalfOfeven though deposits are nominally beneficial — this prevents an attacker from front-running your withdrawal with a dust deposit that would push yourlastDepositBlockforward and DoS you for a block.Expired-unsettled gate. Both
supplyCollateralandwithdrawCollateralcall_revertIfExpiredUnsettled(marketId, market.swapTerm). If the market has any expired swaps that haven't been run throughmakePaymentyet, LP operations revert withE415. The share price is unreliable until those swaps settle — callmakePayment(expiredSwapIds)first (anyone can).
MIN_SHARE_PRICE guard. If the pool is at the underwater floor and has active swaps (lockedCollateral > 0), deposits revert with E414. At the floor, shares are massively inflated (1M:1), so a fresh deposit would capture any eventual settlement recovery at the expense of existing LPs. If there are no active swaps, deposits are allowed — there's no pending inflow to capture.
Last-LP guard. withdrawCollateral will not let pool.totalShares drop to zero while lockedCollateral > 0. When a withdrawal would redeem every outstanding share, the guard retains one share so accounting stays alive through settlement — the payout is reduced by that one share's worth. It only reverts with E416 in the degenerate case where the pool holds exactly one share to begin with (there's nothing left to keep the pool alive). Once all swaps have settled (lockedCollateral == 0), a last LP can sweep the pool clean.
Profit-vest cap. Every deposit records a profit-vest anchor on the LP's position — entryPrice (the share price at deposit) and vestEndTimestamp (block.timestamp + admin.lpProfitVestSeconds(), default 12h, bounded [1h, 7d], timelocked). While the window is active, withdrawCollateral burns the LP's shares at min(currentSharePrice, entryPrice) instead of the live price, neutralizing the deposit → oracle-update → withdraw NAV-sandwich. The cap is one-directional (binds only above the anchor; below-anchor exits take the full loss) and uniform across the position (every share, including mid-vest top-ups, prices at the capped rate — there is no per-tranche accounting). The anchor holds steady on a mid-vest top-up of a non-zero position and is only (re-)set once the prior window expires or the position is fully exited; each deposit extends vestEndTimestamp. Off-chain surfaces should read getLpVestState and warn an LP before supplying or withdrawing while capActive is true.
supplyCollateral
Who calls: an LP, or an authorized delegate. Shares are credited to onBehalfOf, but tokens are pulled from msg.sender (so bundlers and routers work via Permit2-style flows).
What it does: refreshes the rate index, computes the mint and anchor prices in one call (VirtualShareLib.poolMintAndAnchorPrices), mints collateralAmount × WAD / mintPrice shares to onBehalfOf, stamps lastDepositBlock, sets the profit-vest anchor (entryPrice — the anchor/burn leg of that same call, held steady on a mid-vest top-up of a non-zero position; refreshed otherwise) and extends vestEndTimestamp, and pulls collateralAmount of market.swapToken from msg.sender into SwapCore.
Note that minting and anchoring use different prices from the one call: shares mint at the upper (accrued-only) bound while the anchor records the lower (burn) bound. See The three share prices.
Parameters:
marketId
Target market.
collateralAmount
Tokens to deposit, in swapToken decimals. Must be > 0.
onBehalfOf
The address that receives the shares. msg.sender must be authorized via setAuthorization if different from msg.sender.
minSharesOut
Slippage guard: if > 0 and fewer shares would be minted, revert with E411. Pass 0 to disable.
State changes / events:
lpPositions[marketId][onBehalfOf].shares += sharesToMintlpPositions[marketId][onBehalfOf].lastDepositBlock = block.numberlpPositions[marketId][onBehalfOf].entryPrice— (re)set to the anchor/burn price when the prior vest expired or the position was empty; held steady on a mid-vest top-uplpPositions[marketId][onBehalfOf].vestEndTimestamp = block.timestamp + admin.lpProfitVestSeconds()pool.totalShares += sharesToMintpool.totalCollateral += collateralAmountIERC20(swapToken).transferFrom(msg.sender, address(this), collateralAmount)Emits
CollateralSupplied(marketId, onBehalfOf, caller, amount, sharesMinted, sharePrice, mintPrice, entryPrice, vestEndTimestamp)— nine arguments; see the event reference for thesharePricevs.mintPricedistinction.
Reverts:
E206
msg.sender is not authorized to act for onBehalfOf.
E300
Market doesn't exist.
E415
Market has expired swaps that haven't been settled — call makePayment first.
E201
lpWhitelistEnabled and onBehalfOf is not on marketLpWhitelist[marketId].
E400
collateralAmount == 0, or share calculation rounds down to sharesToMint == 0 (deposit too small for the current share price).
E414
Pool is at MIN_SHARE_PRICE floor and has active swaps — deposits are blocked to prevent dilution capture of settlement recovery.
E411
Slippage — fewer shares minted than minSharesOut.
E728
oracleIndex <= SwapFormulas.MIN_INDEX.Refuses LP deposits when the oracle index is at the floor; only hits SpotRate/SpotCompoundRate markets in practice.
See also: withdrawCollateral, getPoolSharePrice, setAuthorization.
withdrawCollateral
Who calls: an LP, or an authorized delegate of onBehalfOf. Shares are burned from onBehalfOf's balance; the redeemed swapToken is delivered to receiver (any non-zero address — typically onBehalfOf, or a bundler/router that passes itself as receiver and forwards).
What it does: refreshes the rate index, burns enough of onBehalfOf's shares to cover the requested withdrawal, and sends the underlying swapToken out. While the position's profit-vest window is active, shares burn at min(currentSharePrice, entryPrice) rather than the live price (the CollateralTokenWithdrawn event's capApplied flag records whether the cap bound). Passing amount >= the LP's current position value (idiomatically type(uint256).max) triggers a full withdrawal. On an exact-amount partial withdrawal the token payout is pinned to amount, so the share-burn count — not the payout — absorbs any adverse share-price move; maxSharesToRedeem bounds it.
Parameters:
marketId
Market to withdraw from.
amount
Collateral to withdraw in swapToken decimals. Pass a value ≥ the position's full value (idiomatically type(uint256).max) to trigger a full withdrawal.
onBehalfOf
Owner of the shares. msg.sender must be authorized via setAuthorization unless they are onBehalfOf.
receiver
Address that receives the withdrawn swapToken. Must be non-zero.
minCollateralOut
Slippage guard: if > 0 and the computed withdrawalAmount is lower, revert with E412. Pass 0 to disable. Especially useful when passing type(uint256).max for amount, where you can't predict the exact payout upfront.
maxSharesToRedeem
Share-burn slippage guard — the max shares the call may burn. If > 0 and the burn would exceed it, revert with E418. Pass 0 to disable. This is the share-side dual of minCollateralOut: on an exact-amount partial withdrawal the payout is pinned to amount, so minCollateralOut can't catch an adverse share-price move that burns extra shares — maxSharesToRedeem can. On a full withdrawal it bounds the total shares burned.
Returns: withdrawalAmount — the actual amount of swapToken delivered.
State changes / events:
lpPositions[marketId][onBehalfOf].shares -= sharesToRedeempool.totalShares -= sharesToRedeempool.totalCollateral -= withdrawalAmountIERC20(swapToken).safeTransfer(receiver, withdrawalAmount)Emits
CollateralTokenWithdrawn(marketId, onBehalfOf, caller, receiver, amount, sharesRedeemed, sharePrice, capApplied).
Reverts:
E103
receiver == address(0)
E206
msg.sender not authorized for onBehalfOf.
E300
Market doesn't exist.
E400
amount == 0, or sharesToRedeem rounds to 0.
E413
Same-block deposit + withdraw from onBehalfOf (flash-loan guard).
E407
Pool has zero total shares.
E405
Computed withdrawalAmount exceeds totalLPAvailableCollateral(marketId) — the pool doesn't have enough unlocked collateral.
E412
Slippage — withdrawalAmount < minCollateralOut.
E415
Expired swaps haven't been settled yet — call makePayment first.
E416
You'd redeem the pool's only share (pool.totalShares == 1) while lockedCollateral > 0 — there's no share left to retain, so accounting couldn't stay alive through settlement. (When more than one share is redeemed, the guard silently keeps one back instead of reverting.)
E417
Cap is active and this would be a full last-LP exit on an idle pool — would bypass the vest cap. Wait for vest expiry or do a partial withdrawal.
E418
Share-burn slippage — maxSharesToRedeem > 0 and the burn would redeem more shares than that (adverse share-price move). Checked after the last-LP guard, so a guard-reduced burn is never rejected.
See also: supplyCollateral, totalLPAvailableCollateral, getLpValue.
getLpPosition
Who calls: anyone — UIs, indexers, adapters.
What it does: returns the full LpPosition struct for account:
Field order matters if you decode positionally. entryPrice is the second field and lastDepositBlock the third, and lastDepositBlock is a uint64 packed into one slot with vestEndTimestamp. Getting the order wrong returns a WAD share price where a block number is expected — a value large enough to look like a far-future block rather than to fail loudly. Named/ABI decoding is unaffected.
entryPrice anchors to the burn price at deposit (see getPoolBurnPrice), which is what makes min(currentBurnPrice, entryPrice) a meaningful cap.
Reverts with E300 if the market doesn't exist.
getLpShares
Thin accessor — returns just lpPositions[marketId][lpAddress].shares. Does not revert on a non-existent market; it will simply return 0. Use this when you only need the share count and want to avoid the getLpPosition tuple.
getLpValue
Returns the redeemable value of lpAddress's shares, denominated in swapToken decimals:
It prices through getPoolBurnPrice — the same mark withdrawCollateral burns at — not getPoolSharePrice. That is deliberate: valuation and settlement stay consistent, so the number a UI shows matches the number the LP receives.
It projects the oracle index forward and reflects unrealized swap P&L, floored at MIN_SHARE_PRICE (so an underwater pool still returns a positive number), and it also applies the LP's profit-vest cap when active. The result is therefore what the LP would actually receive on withdrawCollateral, not the uncapped pool NAV.
To show an uncapped mark-to-market figure instead — for analytics rather than for a withdrawal quote — compute shares × getPoolSharePrice(marketId) / WAD off-chain, and label it as such.
getPoolSharePrice
Returns the informational netted-fair share price in WAD (1e18 = 1.0) — the midpoint between the mint and burn marks. If the pool has totalShares == 0, returns WAD as a safe default (prevents manipulation of the first-deposit ratio). Otherwise it projects a fresh oracle index without mutating state and iterates over the market's buckets via BucketBoundsLib.calculateLpSharePrice to compute the fair value.
This price is non-transactable, and nothing in SwapCore trades at it. It has no internal callers.
supplyCollateralmints atVirtualShareLib.poolMintAndAnchorPrices,withdrawCollateralburns atgetPoolBurnPrice, andgetLpValuevalues atgetPoolBurnPricetoo. Quoting an LP's exit fromgetPoolSharePriceover-states the proceeds, because the burn mark sits at the lower bound and this one sits above it. Use it for analytics and NAV display, never for a withdrawal quote.
The result is floored at Utils.MIN_SHARE_PRICE. If the pool is deeply underwater, getPoolSharePrice will report the floor — consumers should treat a floor reading as "pool is distressed, deposits will be blocked if lockedCollateral > 0" rather than as an accurate valuation.
Safe to call off-chain.
getPoolBurnPrice
The price an immediate withdrawal actually burns at — the mark to use for any redemption quote.
Returns the fair mark floored at the accrued-only lower bound, vesting-deducted and liquidation-threshold-aware, in WAD. Delegates to VirtualShareLib.poolBurnPrice, which shares one body with the anchor leg of poolMintAndAnchorPrices so the burn price and the profit-vest anchor can never diverge.
This is a pool-level price. The per-LP profit-vest cap applies on top of it: an LP inside an active vest window burns at min(getPoolBurnPrice, entryPrice). For the true redeemable figure for a specific LP, use getLpValue, which applies both. Views.getLpVestState reports capActive against this same price.
Called internally by withdrawCollateral and getLpValue. Floored at Utils.MIN_SHARE_PRICE. Safe to call off-chain; does not mutate state.
getPoolSharePriceVirtual
Deployed on the Views helper contract, not SwapCore. Call it against the Views address. It is a thin wrapper over SwapCore.getVirtualSharePrice(marketId, Types.PriceMode.Fair).
Returns the LP share price in WAD (1e18 = 1.0) after virtually settling all expired-unsettled swaps at the queue head. Unlike getPoolSharePrice, which can read stale when expired swaps haven't been run through makePayment yet, this walks the expiryQueue from the current pointer forward, simulates the settlement of each expired-but-unsettled swap (rolling its P&L into the pool), and prices shares against the resulting projected pool value.
If there are no expired-unsettled swaps, it transparently falls back to getPoolSharePrice(marketId).
This is the value UIs should display as "current NAV per share" — an unrealized mark that stays meaningful during the E415 expired-unsettled window, when getPoolSharePrice cannot be trusted.
It is not a withdrawal quote. Being Fair mode, it equals the non-virtual
getPoolSharePricewhenever a withdrawal is actually possible — the two diverge only inside the E415 window, during which LP operations revert anyway. It is neither vesting-deducted nor liquidation-threshold-aware, and it applies no per-LP profit-vest cap. For what an LP would receive, usegetLpValue; for the pool-level burn mark,getPoolBurnPrice.
The result is floored at Utils.MIN_SHARE_PRICE, same as getPoolSharePrice. Safe to call off-chain; does not mutate state.
getVirtualSharePrice
On SwapCore. The full-fidelity form of the above — same virtual settlement of the expired queue head, but with the valuation basis selectable. getPoolSharePriceVirtual is exactly this with mode = Fair.
Types.PriceMode
Basis
Use for
Fair
Accrued + projected P&L per active bucket.
NAV display. Equals getPoolSharePrice whenever a withdrawal is possible.
AccruedOnly
Drops each active bucket's projected leg — the basis the liquidation trigger uses (Utils.isSwapLiquidatable).
Guard thresholds only.
Conservative
min(Fair, AccruedOnly) — projected losses count, projected gains don't.
Solvency-sensitive valuation; never exceeds what a live pool-side liquidation would leave in the pool.
Two traps worth stating plainly:
AccruedOnlyis not aminwithFairand can exceed it.AccruedOnly > Fairis a meaningful signal — it says a forced exit would burn below the immediately-executable value. It is bucket-averaged and reward-slack approximate, so it is safe as a guard threshold and never as a mint or burn price.Conservativeis whatKairosMarketAdapter._realAssetsreads, so a Morpho-style vault cannot redeem idle cash against an optimistic mark before a liquidation crystallizes the loss. Direct-LP withdrawals reach the sameminviagetPoolBurnPrice— without the virtual settlement, which the E415 gate rules out on that path.
getPoolMetrics
One-shot read of the three numbers that make up Types.Pool. totalCollateral − lockedCollateral is the unlocked balance available for new swaps or LP withdrawals; the dedicated totalLPAvailableCollateral helper returns the same subtraction.
totalLPAvailableCollateral
Returns pool.totalCollateral − pool.lockedCollateral — the amount of swapToken in the pool that is not currently backing an open swap. This is the number that both withdrawCollateral and buySwap check against before committing state. Pair it with Views.getCalculatedAvailableLiquidity(marketId), which lives on the read-only Views contract, to convert "unlocked collateral" into "maximum notional a new swap can use" (it combines this value with the live base rate, swap term, and leverage multiplier via SwapFormulas.calculateAvailableLiquidity).
Last updated