> For the complete documentation index, see [llms.txt](https://docs.kairosswap.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kairosswap.com/protocol/liquidity.md).

# 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.

### 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.

| Mark                   | Function                                       | Used for                                                                                                                                                                                                                                                                                              |
| ---------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Mint price**         | `getPoolMintAndBurnPrice(marketId)` (mint leg) | Minting shares in `supplyCollateral`. The accrued-only **upper** bound, no vesting deduction — a deposit pays full freight for pending settlement-vesting drips.                                                                                                                                      |
| **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 exact netted fair mark, also vesting-deducted. It normally sits between the burn (lower) and mint (upper) bounds — and collapses to equal them when no cap binds — but it is not computed as a midpoint. 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 NAV 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.**

Under the hood, all three marks come from a scan of the market's occupied **sub-buckets**, which cohort open swaps by time window *and* entry-rate band (a geometric ladder of rate bands; BUY\_FIXED swaps band by `baseRate`). Buckets where a buyer-side collateral cap binds — a liquidatable buyer whose liquidation would seize collateral into the pool — are priced with conservative mass-capped bounds rather than a point value, which is what opens the spread between the mint and burn marks. [`anyBucketCapBinds`](#anybucketcapbinds) exposes whether any such bucket exists right now.

**Mint flow (`supplyCollateral`):** shares minted = `collateralAmount × WAD / mintPrice`, rounded down, where `mintPrice` is the mint leg of [`getPoolMintAndBurnPrice`](#getpoolmintandburnprice) (computed internally via `VirtualShareLib.poolMintAndAnchorPrices`) — **not** `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 feeds the profit-vest `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. **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, market.swapTerm)`. 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`. 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 — unless the profit-vest cap is active (`E417`) or undripped settlement vesting remains (`E419`); see below.

**Profit-vest cap.** Every deposit records a profit-vest anchor on the LP's position — `entryPrice` (the burn price at deposit) and `vestEndTimestamp`. 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 is fully (re-)set only when the prior window has expired or the position was empty (a full exit clears its protection). On a mid-vest top-up of a non-zero position the anchor **never moves up**, and moves in one of two ways:

* **Top-up at or above the anchor:** the anchor holds steady. The new shares mint at the higher current price but burn at the lower anchor while the vest is active — exiting before vest expiry realizes a haircut on those new shares; holding past `vestEndTimestamp` restores it.
* **Top-up below the anchor** (e.g. averaging into a drawdown): the anchor **blends down** to the share-weighted average of the existing shares (at the old anchor) and the new shares (at today's burn price), floored in the pool's favor. This stops fresh capital from inheriting a stale-high anchor and withdrawing a below-anchor recovery uncapped.

`vestEndTimestamp` is a **forward-only ratchet**: each deposit sets it to `max(existing vestEndTimestamp, block.timestamp + admin.lpProfitVestSeconds())` (default 12h, bounded `[1h, 7d]`, timelocked). The latest deposit always gets a full window under the live policy, but a top-up can never *shorten* the deadline already protecting existing capital — after a governance decrease of `lpProfitVestSeconds`, the shorter window applies only to positions that open a new one. Off-chain surfaces should read `getLpVestState` and warn an LP before supplying or withdrawing while `capActive` is `true`.

### The settlement-vesting reserve

Separate from the per-LP profit-vest cap, each market carries a pool-level **settlement-vesting reserve**. When an early exit or a liquidation would produce an upward jump in pool NAV, the jump is not recognized instantly: it is booked into the market's `vestingReserve` and **drips linearly back into NAV** over the settled swap's canceled remaining term, reaching zero at `dripEnd`. When a new crystallization lands on a live reserve, the amounts blend and the combined balance drips over the later of the two horizons.

What this means mechanically:

* **No tokens move.** The withheld value stays inside `pool.totalCollateral`; outstanding shares keep their pro-rata claim on it. Only the *marks* change: every burn/NAV read — `getPoolBurnPrice`, `getPoolSharePrice`, `getVirtualSharePrice` — subtracts the still-outstanding amount (`outstanding × WAD / totalShares` per share). The **mint price deliberately does not** deduct it, so a share minted an instant before a settlement cannot capture the jump, and a fresh depositor pays full freight for pending drips.
* **Reading it:** `SwapCore.vestingOutstanding(marketId)` returns the market's still-withheld amount (token decimals), a pure function of `block.timestamp` — nothing needs to be poked for it to decay. `Views.lpVestingClaim(marketId, lpAddress)` returns one LP's pro-rata slice. An integrator valuing a *held* (not exited) position should add `shares × vestingOutstanding / totalShares` back to the deducted mark — the value is theirs, it just hasn't dripped in yet.
* **Capacity is not throttled.** [`totalLPAvailableCollateral`](#totallpavailablecollateral) does **not** subtract the withheld vesting. The exclusion lives in the share *price*, which is what defeats the JIT skim; since every withdrawal prices at the deducted mark, LPs in aggregate can never pull more than NAV − outstanding, and the withheld tokens stay productive rather than idle.
* **Last-LP interaction:** a full last-LP exit of an idle pool reverts with `E419` while undripped vesting remains — sweeping the pool would orphan value that is still decaying into NAV with no shares left to own it. Wait past `dripEnd`, or do a partial withdrawal that leaves shares outstanding.
* **Events:** bookings emit `EarlyExitValueVested` or `LiquidationValueVested`. The booked amount is capped at the collateral physically present and not already withheld (a solvency net for near-wiped pools); when that clamp under-books the jump, the shortfall is reported via `SettlementVestingClipped`.

***

## supplyCollateral

```solidity
function supplyCollateral(
    bytes32 marketId,
    uint256 collateralAmount,
    address onBehalfOf,
    uint256 minSharesOut
) external nonReentrant;
```

**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`, updates the profit-vest anchor (`entryPrice` — the **anchor/burn** leg of that same call: (re-)set fresh when the prior vest expired or the position was empty, held steady on an at-or-above-anchor top-up, blended down share-weighted on a below-anchor top-up), ratchets `vestEndTimestamp` forward, 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](#the-three-share-prices).

**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`](/protocol/authorization.md#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`
* `lpPositions[marketId][onBehalfOf].entryPrice` — (re)set to the **anchor/burn** price when the prior vest expired or the position was empty; held steady on an at-or-above-anchor mid-vest top-up; blended **down** to the share-weighted average of old anchor and current burn price (floored in the pool's favor) on a below-anchor mid-vest top-up
* `lpPositions[marketId][onBehalfOf].vestEndTimestamp = block.timestamp + admin.lpProfitVestSeconds()`, except on a mid-vest top-up of a non-zero position, where it is the forward-only ratchet `max(existing vestEndTimestamp, block.timestamp + admin.lpProfitVestSeconds())`
* `pool.totalShares += sharesToMint`
* `pool.totalCollateral += collateralAmount`
* `IERC20(swapToken).transferFrom(msg.sender, address(this), collateralAmount)`
* Emits `CollateralSupplied(marketId, onBehalfOf, caller, amount, sharesMinted, sharePrice, mintPrice, entryPrice, vestEndTimestamp)` — **nine** arguments; see [the event reference](/protocol/events.md#collateralsupplied) for the `sharePrice` vs. `mintPrice` distinction.

**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`.                                                                                                         |
| `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`](#withdrawcollateral), [`Views.previewSupply`](#views-preview-and-dashboard-reads), [`getPoolSharePrice`](#getpoolshareprice), [`setAuthorization`](/protocol/authorization.md#setauthorization).

***

## withdrawCollateral

```solidity
function withdrawCollateral(
    bytes32 marketId,
    uint256 amount,
    address onBehalfOf,
    address receiver,
    uint256 minCollateralOut,
    uint256 maxSharesToRedeem
) external nonReentrant returns (uint256);
```

**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 **only when the pool has active swaps** (`lockedCollateral != 0` — an idle pool prices oracle-free at `totalCollateral / totalShares`, so LP exits keep working through a reference-oracle outage), 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:**

| Parameter           | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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 -= sharesToRedeem`
* `pool.totalShares -= sharesToRedeem`
* `pool.totalCollateral -= withdrawalAmount`
* `IERC20(swapToken).safeTransfer(receiver, withdrawalAmount)`
* Emits `CollateralTokenWithdrawn(marketId, onBehalfOf, caller, receiver, amount, sharesRedeemed, sharePrice, capApplied)`.

**Reverts:**

| Code   | Reason                                                                                                                                                                                                                                                                               |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `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.                                                                                |
| `E419` | Full last-LP exit on an idle pool while undripped settlement vesting remains (`vestingOutstanding(marketId) > 0`) — sweeping the pool would orphan value still dripping back into NAV. Wait past the reserve's `dripEnd`, or do a partial withdrawal that leaves shares outstanding. |

**See also:** [`supplyCollateral`](#supplycollateral), [`Views.previewWithdraw`](#views-preview-and-dashboard-reads), [`totalLPAvailableCollateral`](#totallpavailablecollateral), [`getLpValue`](#getlpvalue).

***

## getLpPosition

```solidity
function getLpPosition(bytes32 marketId, address account)
    external view returns (Types.LpPosition memory);
```

**Who calls:** anyone — UIs, indexers, adapters.

**What it does:** returns the full `LpPosition` struct for `account`:

```solidity
struct LpPosition {
    uint256 shares;            // pool shares held
    uint256 entryPrice;        // profit-vest anchor (burn price at deposit), WAD
    uint64  lastDepositBlock;  // block of most recent supplyCollateral
    uint64  vestEndTimestamp;  // profit-vest window end (unix seconds)
}
```

**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`](#getpoolburnprice)), which is what makes `min(currentBurnPrice, entryPrice)` a meaningful cap.

Reverts with `E300` if the market doesn't exist.

***

## getLpShares

```solidity
function getLpShares(bytes32 marketId, address lpAddress)
    external view returns (uint256);
```

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

```solidity
function getLpValue(bytes32 marketId, address lpAddress)
    external view returns (uint256);
```

Returns the redeemable value of `lpAddress`'s shares, denominated in `swapToken` decimals:

```
value = shares × min(getPoolBurnPrice(marketId), entryPrice if vest active) / WAD
```

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 (or read `fairValue` from [`Views.viewLPDashboard`](#views-preview-and-dashboard-reads), which does exactly that), and label it as such. For a *held* position, you can also add back the LP's pro-rata slice of the still-withheld settlement vesting (`Views.lpVestingClaim`) — value that belongs to the shares but hasn't dripped into the marks yet.

***

## getPoolSharePrice

```solidity
function getPoolSharePrice(bytes32 marketId) public view returns (uint256);
```

Returns the **informational** netted-fair 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 delegates to `VirtualShareLib.poolSharePrice`:

* **Idle pools** (`lockedCollateral == 0`) price **oracle-free** at `totalCollateral / totalShares` — no bucket iteration, no oracle read, so NAV stays answerable during a reference-rate oracle outage.
* **Active pools** project a fresh oracle index without mutating state and compute the netted fair value over the market's occupied buckets (`BucketBoundsLib.computePoolSharePrice`).
* In both cases the still-outstanding **settlement vesting is subtracted**, so the fair mark stays continuous across an early exit or liquidation (see [The settlement-vesting reserve](#the-settlement-vesting-reserve)).

> **This price is non-transactable, and nothing in SwapCore trades at it.** It has **no internal callers**. `supplyCollateral` mints at the mint leg of [`getPoolMintAndBurnPrice`](#getpoolmintandburnprice), `withdrawCollateral` burns at [`getPoolBurnPrice`](#getpoolburnprice), and `getLpValue` values at `getPoolBurnPrice` too. Quoting an LP's exit from `getPoolSharePrice` **over-states the proceeds**, because the burn mark sits at the lower bound and this one normally sits above it (the three coincide only when no bucket cap binds). 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

```solidity
function getPoolBurnPrice(bytes32 marketId) public view returns (uint256);
```

**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`](#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.

***

## getPoolMintAndBurnPrice

```solidity
function getPoolMintAndBurnPrice(bytes32 marketId)
    external view returns (uint256 mintPrice, uint256 burnPrice);
```

Both **transactable** prices in one call: `mintPrice` is what `supplyCollateral` mints at (the accrued-only conservative upper bound, undeducted for vesting) and `burnPrice` is what `withdrawCollateral` burns at before the per-LP profit-vest cap — identical to [`getPoolBurnPrice`](#getpoolburnprice). Both are computed off a single shared bucket scan, so they are always mutually consistent.

Returns `(WAD, WAD)` for an empty pool, mirroring the deposit path. Note that it **returns values even in states where the transactions themselves would revert** (`E414` underwater floor, `E415` expired-unsettled) — gate on those conditions separately, or use [`Views.previewSupply` / `Views.previewWithdraw`](#views-preview-and-dashboard-reads), which replicate the live calls' revert surface.

This is the getter integrators should use for deposit/withdrawal quotes; the internal `VirtualShareLib.poolMintAndAnchorPrices` delegate it wraps is not part of the public surface. Reverts with `E300` if the market doesn't exist. Safe to call off-chain.

***

## getPoolSharePriceVirtual

```solidity
function getPoolSharePriceVirtual(bytes32 marketId) external view returns (uint256);
```

**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 `getPoolSharePrice` whenever a withdrawal is actually possible — the two diverge only *inside* the E415 window, during which LP operations revert anyway. It **is** vesting-deducted (the same settlement-vesting deduction as `getPoolSharePrice`), but it is not liquidation-threshold-aware and it applies no per-LP profit-vest cap. For what an LP would receive, use [`getLpValue`](#getlpvalue); for the pool-level burn mark, [`getPoolBurnPrice`](#getpoolburnprice).

The result is floored at `Utils.MIN_SHARE_PRICE`, same as `getPoolSharePrice`. Safe to call off-chain; does not mutate state.

***

## getVirtualSharePrice

```solidity
function getVirtualSharePrice(bytes32 marketId, Types.PriceMode mode)
    external view returns (uint256);
```

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`. All modes apply the settlement-vesting deduction.

| `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:

* `AccruedOnly` is **not** a `min` with `Fair` and **can exceed it**. `AccruedOnly > Fair` is 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.
* `Conservative` is what `KairosMarketAdapter._realAssets` reads, so a Morpho-style vault cannot redeem idle cash against an optimistic mark before a liquidation crystallizes the loss. Direct-LP withdrawals reach the same `min` via `getPoolBurnPrice` — without the virtual settlement, which the E415 gate rules out on that path.

***

## getPoolMetrics

```solidity
function getPoolMetrics(bytes32 marketId)
    external view returns (uint256 totalShares, uint256 totalCollateral, uint256 lockedCollateral);
```

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`](#totallpavailablecollateral) helper returns the same subtraction.

***

## vestingOutstanding

```solidity
function vestingOutstanding(bytes32 marketId) external view returns (uint256);
```

The market's still-withheld settlement vesting, in `swapToken` decimals — the amount currently excluded from every burn/NAV mark (see [The settlement-vesting reserve](#the-settlement-vesting-reserve)). Decays linearly to zero at the reserve's `dripEnd` as a pure function of `block.timestamp`; nothing needs to be poked. The decay is ceil-rounded, so the value stays `> 0` (at least one token atom) until `dripEnd` — which is exactly the condition the `E419` last-LP guard checks.

Per-LP slice: `Views.lpVestingClaim(marketId, lpAddress)` returns `shares / totalShares` of this figure, with rounding that mirrors the price deduction so `burn value + claim` never exceeds the undeducted book value. This is display/valuation data, **not** a separately claimable balance — the value drips back into the share price on its own.

Reverts with `E300` if the market doesn't exist.

***

## anyBucketCapBinds

```solidity
function anyBucketCapBinds(bytes32 marketId) external view returns (bool);
```

Returns `true` iff any occupied bucket currently has a **buyer-side collateral cap binding** — i.e. it contains a liquidatable buyer whose liquidation would seize collateral into the pool. While such a bucket exists, the netted point value is not trustworthy for that bucket, so share pricing switches it to conservative capped bounds — this is when the mint/burn spread is widest and the fair mark is most "informational."

It is a non-maskable pending-recovery signal: `KairosMarketAdapter` uses it to gate permissionless `forceDeallocate`, and `Views.viewMarketPoolState` surfaces it as `capBinds`. UIs can treat it as "a liquidation is executable in this market right now." Reverts with `E300` if the market doesn't exist.

***

## totalLPAvailableCollateral

```solidity
function totalLPAvailableCollateral(bytes32 marketId) public view returns (uint256);
```

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`).

Note: still-withheld settlement vesting is deliberately **not** subtracted here. The exclusion lives in the share *price* instead — every withdrawal prices at the vesting-deducted mark, so LPs in aggregate can never pull more than NAV − outstanding, and the withheld tokens stay available as swap-backing capacity rather than sitting idle.

***

## Views: preview and dashboard reads

The read-only `Views` contract carries the LP integration surface that doesn't fit on SwapCore. All of these are safe to call off-chain against the `Views` address.

### previewSupply

```solidity
function previewSupply(bytes32 marketId, uint256 collateralAmount)
    external view returns (uint256 sharesOut, uint256 mintPrice);
```

The exact shares `supplyCollateral` would mint for `collateralAmount` this block, replicating its rounding and **reverting with the same errors the live call would**: `E300`, `E400` (zero/dust), `E414` (underwater floor with active swaps), `E415` (expired-unsettled), `E728` (index at floor). Caller-shaped guards (`E206` authorization, `E201` whitelist) are deliberately not replicated — they depend on who sends the transaction, not on the amount math. A quote and a transaction landing in different blocks can diverge; that is what `minSharesOut` is for.

### previewWithdraw

```solidity
function previewWithdraw(bytes32 marketId, address lpAddress, uint256 amount)
    external view
    returns (uint256 sharesBurned, uint256 amountOut, bool capApplied, uint256 effectivePrice);
```

The exact outcome `withdrawCollateral(marketId, amount, lpAddress, ...)` would produce this block. Pass `type(uint256).max` (or any value ≥ the position's full value) for a full exit. Replicates the profit-vest cap (`capApplied`, `effectivePrice = min(getPoolBurnPrice, entryPrice)`), the full/partial rounding split, and **every last-LP branch**, reverting with the same errors as the live call: `E300`, `E400`, `E405`, `E407`, `E413`, `E415`, `E416`, `E417`, `E419`. Slippage params (`E412`/`E418`) and authorization/receiver checks (`E206`/`E103`) are not replicated — feed this preview's outputs into those params instead.

### viewLPDashboard

```solidity
function viewLPDashboard(address lpAddress, bytes32[] calldata marketIds)
    external view returns (LpDashboardEntry[] memory);
```

Batched per-market LP position rows: the `LpPosition` struct, `burnValue` (full-exit value with the per-LP cap applied, i.e. `getLpValue`), `fairValue` (informational, non-transactable), all three prices (`mintPrice`, `burnPrice`, `fairPrice`), `capActive`, the LP's `vestingClaim` and the pool-level `vestingOutstanding`, the full `Market` struct, and the oracle description. Skips non-existent markets and zero-share positions. This is the intended one-call backing for an LP portfolio page.

### viewMarketPoolState

```solidity
function viewMarketPoolState(bytes32 marketId) external view returns (MarketPoolState memory);
```

Single-call market-page payload: `mintPrice` / `burnPrice` / `fairPrice`, `vestingOutstanding`, the three pool aggregates, `availableCollateral` (vesting **not** subtracted), `utilizationWad`, `capBinds` ([`anyBucketCapBinds`](#anybucketcapbinds)), and `lpOpsBlocked` (`true` when supply/withdraw would revert `E415`). Prices are still returned in states where the transactions themselves revert — gate on `lpOpsBlocked` or use the previews.

### lpVestingClaim

```solidity
function lpVestingClaim(bytes32 marketId, address lpAddress) external view returns (uint256);
```

`lpAddress`'s pro-rata claim on the market's still-withheld settlement vesting, in `swapToken` decimals — see [`vestingOutstanding`](#vestingoutstanding). Returns `0` (rather than reverting) for unknown markets and share-less LPs.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kairosswap.com/protocol/liquidity.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
