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 underlying swapToken the pool holds (LP-supplied + retained swap payments, minus paid-out losses).

  • lockedCollateral — the portion of totalCollateral reserved against open swaps. totalCollateral − lockedCollateral is 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. 1-block deposit lock. supplyCollateral stamps lpPos.lastDepositBlock = block.number, and withdrawCollateral reverts with E413 if you try to withdraw in the same block. Plus, supplyCollateral requires auth for onBehalfOf even though deposits are nominally beneficial — this prevents an attacker from front-running your withdrawal with a dust deposit that would push your lastDepositBlock forward and DoS you for a block.

  2. Expired-unsettled gate. Both supplyCollateral and withdrawCollateral call _revertIfExpiredUnsettled(marketId, bucketInterval). If the market has any expired swaps that haven't been run through makePayment yet, LP operations revert with E415. The share price is unreliable until those swaps settle — call makePayment(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:

Parameter
Meaning

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 += sharesToMint

  • lpPositions[marketId][onBehalfOf].lastDepositBlock = block.number

  • pool.totalShares += sharesToMint

  • pool.totalCollateral += collateralAmount

  • IERC20(swapToken).transferFrom(msg.sender, address(this), collateralAmount)

  • Emits CollateralSupplied(marketId, swapToken, onBehalfOf, caller, amount, sharesMinted, sharePrice).

Reverts:

Code
Reason

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:

Parameter
Meaning

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 -= sharesToRedeem

  • pool.totalShares -= sharesToRedeem

  • pool.totalCollateral -= withdrawalAmount

  • IERC20(swapToken).safeTransfer(msg.sender, withdrawalAmount)

  • Emits CollateralTokenWithdrawn(marketId, swapToken, onBehalfOf, caller, amount, sharesRedeemed, sharePrice).

Reverts:

Code
Reason

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