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.
Share price is computed by getPoolSharePrice(marketId) by projecting each bucket's P&L at the current oracle index and dividing the pool's fair value by totalShares. It is floored at Utils.MIN_SHARE_PRICE — an underwater pool reports the floor rather than zero so the math stays well-defined.
Mint flow (supplyCollateral): shares minted = collateralAmount × WAD / sharePrice, rounded down. For the first deposit (empty pool) sharePrice defaults to WAD and one token unit mints one share. Shares and lastDepositBlock are credited to onBehalfOf.
Burn flow (withdrawCollateral): shares burned = amount × WAD / sharePrice, rounded up in favor of the pool. Burning uses the caller's own LP balance (from onBehalfOf); tokens land on msg.sender so bundlers can forward.
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, bucketInterval). 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. The last share is retained so accounting stays alive through settlement. If you're the last LP, a full withdrawal leaves one share behind; a partial withdrawal that would otherwise burn the final share reverts with E416. Once all swaps have settled, a last LP can sweep the pool clean.
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 current share price, mints collateralAmount × WAD / sharePrice shares to onBehalfOf, stamps lastDepositBlock, and pulls collateralAmount of market.swapToken from msg.sender into SwapCore.
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.numberpool.totalShares += sharesToMintpool.totalCollateral += collateralAmountIERC20(swapToken).transferFrom(msg.sender, address(this), collateralAmount)Emits
CollateralSupplied(marketId, swapToken, onBehalfOf, caller, amount, sharesMinted, sharePrice).
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.
See also: withdrawCollateral, getPoolSharePrice, setAuthorization.
withdrawCollateral
Who calls: an LP, or an authorized delegate of onBehalfOf. Tokens are always delivered to msg.sender (so a bundler can receive them and forward).
What it does: refreshes the rate index, burns enough of onBehalfOf's shares to cover the requested withdrawal, and sends the underlying swapToken out. If fullAmount = true, amount is ignored and the LP's entire position is redeemed.
Parameters:
marketId
Market to withdraw from.
amount
Collateral to withdraw in swapToken decimals. Ignored when fullAmount = true.
fullAmount
If true, redeem the LP's entire share balance in one shot.
onBehalfOf
Owner of the shares. msg.sender must be authorized via setAuthorization unless they are onBehalfOf.
minCollateralOut
Slippage guard: if > 0 and the computed withdrawalAmount is lower, revert with E412. Pass 0 to disable. Especially useful with fullAmount = true, where you don't pass a target amount.
Returns: withdrawalAmount — the actual amount of swapToken delivered.
State changes / events:
lpPositions[marketId][onBehalfOf].shares -= sharesToRedeempool.totalShares -= sharesToRedeempool.totalCollateral -= withdrawalAmountIERC20(swapToken).safeTransfer(msg.sender, withdrawalAmount)Emits
CollateralTokenWithdrawn(marketId, swapToken, onBehalfOf, caller, amount, sharesRedeemed, sharePrice).
Reverts:
E206
msg.sender not authorized for onBehalfOf.
E300
Market doesn't exist.
E415
Expired swaps haven't been settled yet — call makePayment first.
E400
amount == 0 && !fullAmount, or sharesToRedeem rounds to 0.
E413
Same-block deposit + withdraw from onBehalfOf (flash-loan guard).
E407
Pool has zero total shares.
E406
Requested partial withdrawal needs more shares than onBehalfOf owns.
E405
Computed withdrawalAmount exceeds totalLPAvailableCollateral(marketId) — the pool doesn't have enough unlocked collateral.
E412
Slippage — withdrawalAmount < minCollateralOut.
E416
You're the last LP, you'd burn the final share, and lockedCollateral > 0 — a single share is retained so accounting stays alive through settlement.
See also: supplyCollateral, totalLPAvailableCollateral, getLpValue.
getLpPosition
Who calls: anyone — UIs, indexers, adapters.
What it does: returns the full LpPosition struct for account:
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 mark-to-market value of lpAddress's shares, denominated in swapToken decimals:
Useful for showing LPs their live NAV in a UI. Because it calls getPoolSharePrice, it will project the oracle index forward and reflect unrealized swap P&L. The share price is floored at MIN_SHARE_PRICE, so an underwater pool still returns a positive number.
isUserLpInMarket
Returns true if userAddress has any shares in marketId. Reverts with E300 if the market doesn't exist.
getPoolSharePrice
Returns the mark-to-market share price in WAD (1e18 = 1.0). 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 Utils.calculateLpSharePrice to compute the fair value.
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.
Called internally by supplyCollateral, withdrawCollateral, and getLpValue. Safe to call off-chain.
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 getCalculatedAvailableLiquidity to convert "unlocked collateral" into "maximum notional a new swap can use".
Last updated