> 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/swaps.md).

# Swaps

This page covers the full **buyer lifecycle**: opening a swap, moving it, closing it (three ways), retrieving escrowed settlement if a direct transfer ever fails, and a small permissionless queue-maintenance helper. Every function lives in `SwapCore.sol`.

## Lifecycle at a glance

```
        ┌─────────────┐
        │   buySwap   │
        └──────┬──────┘
               │
   (optionally transferSwapPosition)
               │
     ┌─────────┼─────────────────────┐
     │         │                     │
     ▼         ▼                     ▼
 makePayment  exitSwapEarly     liquidateSwap
 (expiry)     (before expiry,   (underwater,
               owner only)       anyone)
     │         │                     │
     └─────────┼─────────────────────┘
               │
       (if transfer failed)
               │
               ▼
         claimEscrow
```

Four things to keep in mind across all of these:

* **Collateral cap.** The net payment a swap can produce at settlement is capped at each side's posted collateral — the buyer can never lose more than `collateralBalance`, the pool can never lose more than `poolCollateralBacking`. `getSwapNetAmount` applies the same cap.
* **Accrued-only liquidation.** Liquidation triggers on the **accrued** P\&L, never on where the swap is trending. Buyer-side, the trigger is `netObligation ≥ collateralBalance` (at the boundary, not strictly above it). Pool-side, the trigger fires **early**, at `netObligation ≥ poolCollateralBacking − liquidatorReward` — the threshold is derived from the liquidator's payout so the liquidatable boundary and the reward stay consistent. See [`liquidateSwap`](#liquidateswap).
* **Failed transfers escrow.** If SwapCore can't deliver settlement to the buyer (blacklisted address, blocked receiver, etc.) the payout is held in `escrowedCollateral[swapId]` and emits `BuyerTransferFailed`. The buyer (or their authorized delegate) retrieves it later via [`claimEscrow`](#claimescrow). Funds always go back to the original `swap.userAddress` — there is no admin redirect path.
* **A dead oracle can't brick settlement.** If the reference oracle stops producing data, settlement refuses to extrapolate only for a bounded **grace window** after expiry. Past that, the swap becomes settleable at an extrapolated index and the market pair is permanently closed to new swaps — see [Dead-oracle settlement fallback](#dead-oracle-settlement-fallback).

***

## buySwap

```solidity
function buySwap(
    bytes32 marketId,
    uint256 notionalAmount,
    address onBehalfOf,
    int256  rateBound,
    uint256 maxMarkup,
    uint256 maxTotalIn
) external nonReentrant returns (bytes32 swapId);
```

**Who calls:** anyone. `buySwap` is beneficial to `onBehalfOf` (they get the position), so unlike most other `onBehalfOf` entry points it does **not** require `setAuthorization`. The caller pays the collateral and fees.

**What it does:**

1. Loads the market, checks it's live (`exists && !terminated`), and reads `baseRate` from `baseSwapRateOracle` (tenor-dependent). On a market whose reference oracle uses the **Cumulative** convention, a negative `baseRate` reverts `E512` — for **both** rate types, before the rate-type branch. A Cumulative index is monotone, so its floating leg is always ≥ 0: on BUY\_FIXED a negative fixed leg would be a one-sided claim on LP backing, and on BUY\_FLOATING it would open a position that every remaining-tenor consumer (share-price marks, early exit, NAV views) immediately rejects with the same `E512` — unpriceable and un-exitable until maturity. Spot-convention references can legitimately go negative and are not gated.
2. Computes `availableLiquidity` from the pool's unlocked collateral — `totalLPAvailableCollateral(marketId)` (i.e. `totalCollateral − lockedCollateral`) — via `SwapFormulas.calculateAvailableLiquidity(availableCollateral, baseRate, swapTerm, leverageMultiplier)`, and asserts `notionalAmount > 0 && ≤ availableLiquidity` (reverting `E509` / `E508` respectively).
3. Integrates the kinked utilization fee curve over `[uPre, uPost]` using `market.utilFeeSlopeWad`, `kinkUtilization`, and `maxKinkFeeWad`. The utilization is measured against seasoned (EMA) liquidity, not the spot pool balance — the capacity gate in step 2 uses spot, but the fee is priced on the smoothed average (see [Core concepts → Seasoned utilization fee](/protocol/core-concepts.md#seasoned-utilization-fee)). This is a JIT-deposit defense: a same-block LP deposit expands capacity but can't cheapen the fee.
4. Reads `riskPremium` from the market's risk premium oracle (returns `0` if the address is zero). A negative premium reverts `E612`; an invalid-flagged reading reverts `E605`.
5. Builds `swapRate`: for **BUY\_FIXED**, `swapRate = baseRate + utilFee + riskPremium`; for BUY\_FLOATING, `swapRate` is left at the sentinel value `0` and the floating leg is derived at settlement from the entry/expiry cumulative index. The buyer's pre-funded collateral still covers `utilFee + riskPremium` on top of `baseRate` — those accrue to LPs through the floating leg at settlement.
6. Applies slippage guards: `rateBound` and `maxMarkup` (see below), then — after sizing collateral, fee, and bounty — checks `requiredBuyerCollateral + protocolFee + liquidationBounty ≤ maxTotalIn`, reverting `E513` if exceeded.
7. Sizes `requiredBuyerCollateral` and `totalLpCollateralRequired`; rejects if either side is below `market.minCollateral` or if the pool lacks enough unlocked collateral. Buyer collateral is sized off the **larger-magnitude base quote of this market and its paired twin** (`correspondingMarketId`), fee-inclusive. The two sides of a pair are priced off separate base quotes; buyer collateral is the losing side's payout cap in a paired position, so sizing it off this side's own (possibly lower) quote would let an equal-notional BUY\_FIXED + BUY\_FLOATING hedge — whose floating legs cancel — pay out more on the leg it wins than it forfeits on the leg it loses. LP backing and the step-2 capacity gate stay on this side's own `baseRate` (base-only), so LP capital efficiency is unaffected.
8. Reads protocol fee config from Admin in a single `admin.getFeeConfig()` call — returning `feeRate`, `creatorFeeShare`, and `protocolMultisig` together — and computes `protocolFee = notionalAmount × feeRate × swapTerm / (SECONDS_IN_YEAR × WAD)`.
9. Computes `liquidationBounty = requiredBuyerCollateral × market.liquidationIncentive / WAD`. This is prefunded by the buyer at entry, held outside `collateralBalance`, and either paid to a liquidator on liquidation or returned to the buyer at normal expiry.
10. Snapshots the current cumulative index from `referenceRateOracle` via `rateIndex.update` (reverts `E728` if it's at the `MIN_INDEX` floor). The reference rate itself isn't materialized until settlement, when the entry/expiry index ratio is converted via the market's `rateConvention`.
11. Generates `swapId` via `Utils.generateSwapId`, writes the `SwapPosition` to storage (embedding the just-snapshotted index as `entryFloatingIndex`), assigns it to a bucket (`Utils.addSwapToBucket`), bumps `pool.lockedCollateral += totalLpCollateralRequired`, and appends the `swapId` to the market's `expiryQueue` so settlement/LP-gate logic can find it later.
12. Pulls `requiredBuyerCollateral + protocolFee + liquidationBounty` from `msg.sender`. If the token under-delivers (fee-on-transfer or other non-standard behavior), the protocol fee is **zeroed entirely** rather than over-crediting the multisig from swap collateral. The fee is then split with two graceful (low-level, non-reverting) transfers: if `creatorFeeShare > 0` and the market has an owner, the creator's share goes to `market.marketOwner` — on failure that share rolls back into the protocol share. The protocol share goes to the `protocolMultisig`; if the **multisig itself** can't receive the token, the protocol share is **refunded to the payer** (`msg.sender`) and `ProtocolFeeForegone` is emitted instead of `ProtocolFeeCollected`. A fee-delivery failure never reverts the swap.
13. Emits `SwapCreated`, plus `ProtocolFeeCollected` / `CreatorFeeCollected` / `ProtocolFeeForegone` as applicable. Note `ProtocolFeeCollected.feeAmount` is **net of any delivered creator share** — summing `ProtocolFeeCollected + CreatorFeeCollected` reconstructs the gross fee without double-counting.

**Parameters:**

| Parameter        | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `marketId`       | Which side of the pair to open on.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `notionalAmount` | Swap notional in `swapToken` decimals. Must be `> 0` and `≤ getCalculatedAvailableLiquidity(marketId)`.                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `onBehalfOf`     | The address that will own the new swap. Must not be `address(0)`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `rateBound`      | Slippage on the rate. For **BUY\_FIXED**, this is a **ceiling on `swapRate`** — revert if `swapRate > rateBound`. For **BUY\_FLOATING**, it's a **floor on `baseRate`** — revert if `baseRate < rateBound`. Pass `type(int256).max` to disable.                                                                                                                                                                                                                                                                                                                |
| `maxMarkup`      | Slippage on the fee markup: revert if `utilFee + riskPremium > maxMarkup`. Pass `0` to disable.                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `maxTotalIn`     | Ceiling on the total tokens pulled from **`msg.sender`** (`requiredBuyerCollateral + protocolFee + liquidationBounty`). Reverts `E513` if exceeded. Pass `type(uint256).max` to disable. Necessary because buyer collateral is sized off the absolute value of the larger-magnitude base quote across the market pair, so a *favorable* `rateBound` check (BUY\_FLOATING with a high base rate, or BUY\_FIXED with a negative base rate) — or a large twin-side quote — can still inflate the posted collateral. `rateBound` alone doesn't bound tokens spent. |

**Returns:** `swapId` — a unique `bytes32` produced by `Utils.generateSwapId(marketId, onBehalfOf, block.timestamp, ++globalNonce)`

**Payment from caller:** `requiredBuyerCollateral + protocolFee + liquidationBounty`. Approve this amount on `swapToken` before calling. `msg.sender` is the token source; `onBehalfOf` is the position owner. This decoupling lets bundlers and routers pay on behalf of the end user.

**State changes / events:**

* `swapPositions[swapId]` written with all fields from `Types.SwapPosition`.
* `buckets[marketId][bucketId]` updated with the new aggregates.
* `pool.lockedCollateral += totalLpCollateralRequired`.
* `expiryQueue[marketId].push(swapId)` — the swap ID is appended to the market's expiry queue. The separate `expiryQueuePointer[marketId]` (which tracks the earliest unsettled position) is advanced opportunistically by `Utils.tryAdvanceExpiryPointer` on later settlements — or explicitly via [`advanceExpiryPointer`](#advanceexpirypointer) — not on entry.
* `IERC20(swapToken).transferFrom(msg.sender, address(this), requiredBuyerCollateral + protocolFee + liquidationBounty)`.
* Graceful low-level token transfers to the market owner (creator share) and multisig (protocol share); refund to `msg.sender` if the multisig can't receive.
* Emits `SwapCreated`; `ProtocolFeeCollected` (net of delivered creator share) or `ProtocolFeeForegone` (multisig couldn't receive — share refunded to payer); `CreatorFeeCollected` if the creator split transfer succeeded.

**Reverts:**

| Code         | Reason                                                                                                                                                                               |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `E103`       | `onBehalfOf == address(0)`.                                                                                                                                                          |
| `E300`       | Market doesn't exist.                                                                                                                                                                |
| `E304`       | Market is terminated — no new swaps.                                                                                                                                                 |
| `E611`       | Base rate oracle returned an invalid reading.                                                                                                                                        |
| `E512`       | `baseRate < 0` on a Cumulative reference oracle — applies to **both** rate types (checked before the rate-type branch; the monotone index means the floating leg can't go negative). |
| `E509`       | `notionalAmount == 0`.                                                                                                                                                               |
| `E508`       | `notionalAmount` exceeds the pool's available liquidity.                                                                                                                             |
| `E612`       | Risk premium oracle returned a negative premium.                                                                                                                                     |
| `E605`       | Risk premium oracle returned an invalid-flagged reading.                                                                                                                             |
| `E510`       | Slippage — rate bound exceeded (BUY\_FIXED: `swapRate > rateBound`; BUY\_FLOATING: `baseRate < rateBound`).                                                                          |
| `E511`       | Slippage — `utilFee + riskPremium > maxMarkup`.                                                                                                                                      |
| `E503`       | `requiredBuyerCollateral < minCollateral` or `totalLpCollateralRequired < minCollateral`.                                                                                            |
| `E404`       | Pool has insufficient available collateral to back the LP side.                                                                                                                      |
| `E513`       | `requiredBuyerCollateral + protocolFee + liquidationBounty > maxTotalIn` — total-in slippage guard.                                                                                  |
| `E606`       | Reference rate oracle returned invalid or zero data during the index update.                                                                                                         |
| `E728`       | `oracleIndex <= SwapFormulas.MIN_INDEX` — reference index at the floor.                                                                                                              |
| ERC20 revert | `swapToken.transferFrom` failed (allowance or balance).                                                                                                                              |

**Choosing slippage bounds.**

* **BUY\_FIXED:** you're locking in what you pay, so the risk is rates move down between your quote and the tx landing. Use `rateBound = quotedSwapRate × (1 + tolerance)` to cap your worst case.
* **BUY\_FLOATING:** you're locking in what you receive, so the risk is the base rate drops. Use `rateBound = quotedBaseRate × (1 − tolerance)` as a floor.
* `maxMarkup` is useful in both directions — it caps the fee portion of the all-in rate so a utilization spike after quote time doesn't burn you. Set it to your quoted `utilFee + riskPremium` plus a small buffer.
* Set `maxTotalIn` to your expected total cost plus a small buffer to bound worst-case tokens pulled, especially for BUY\_FLOATING — and remember the collateral is sized off the larger quote across the market pair, so quote either side's collateral requirement from the contract, not from this side's rate alone.

**See also:** [`getCalculatedAvailableLiquidity`](/protocol/views.md#getcalculatedavailableliquidity), [`getSwapNetAmount`](/protocol/views.md#getswapnetamount), [`transferSwapPosition`](#transferswapposition).

***

## makePayment

```solidity
function makePayment(bytes32[] calldata swapIds)
    public nonReentrant returns (Types.SettlementResult[] memory results);
```

**Who calls:** anyone. Settlement is permissionless — keepers, bots, adapters, even the buyer or LP themselves. There's no keeper whitelist.

**What it does:** iterates the `swapIds` array and, for each one that's reached expiry (`block.timestamp ≥ swap.entryTimestamp + market.swapTerm`), runs the settlement pipeline:

1. Validates `swap.entryTimestamp != 0` and `!swap.settled`.
2. While still inside the settlement grace window (`block.timestamp < expiry + admin.settlementGracePeriodSec()`), makes a **best-effort** attempt to refresh the oracle index — a `try this.updateMarketRateIndex(marketId) {} catch {}` self-call, so a transiently unreadable oracle doesn't revert the batch. Past the grace window this uncapped attempt is deliberately **skipped** (a gas-burning oracle dependency could otherwise starve the fallback adjudication below at any transaction gas limit); the fixed-budget adjudication retry in step 3 becomes the first oracle attempt instead.
3. Resolves the historical oracle index at expiry via `Utils.resolveExpiryIndex`. If a snapshot exists at or after `entryTimestamp + swapTerm`, settlement reads `rateIndex.getIndexAt(oracle, expiryTimestamp)` — real data, no fallback. If none exists, behavior depends on the grace window — see [Dead-oracle settlement fallback](#dead-oracle-settlement-fallback) below.
4. Calls `Utils.settleSwap` to compute fixed and floating payments over the full term, net them, cap at each side's collateral, and determine `netRecipient` (`0` = LP receives, `1` = buyer receives).
5. Transfers the net to the winning side and releases the unused collateral back to its original owner (or to the pool). If the buyer-side transfer reverts, the payout is moved to `escrowedCollateral[swapId]` and `BuyerTransferFailed` is emitted.
6. Marks `swap.settled = true`, records `settlementPayouts[swapId] = buyerCollateralReleased`, and advances `expiryQueuePointer[marketId]` past now-settled queue entries via `Utils.tryAdvanceExpiryPointer` (the queue itself is append-only — settled entries are zeroed in place but the array does not shrink).
7. Emits `SwapClosed` with `closureType = 0` (the event's `liquidatorReward` field is `0` on this path — any prefunded bounty returns to the buyer) and, if pool collateral hit zero, `PoolCollateralZeroed`.

### Dead-oracle settlement fallback

Settlement needs an oracle snapshot at or after the swap's expiry. When none exists, the protocol distinguishes a *lagging* oracle from a *dead* one:

* **Inside the grace window** (`expiry + admin.settlementGracePeriodSec()`; default 14 days, admin-adjustable within \[7, 90] days behind a timelock), settlement simply reverts `E450` — it refuses to extrapolate past the last known data. Keepers should call `updateMarketRateIndex` post-maturity to advance the snapshot horizon and retry.
* **Past the grace window**, the failed update is *adjudicated* before any fallback engages: `resolveExpiryIndex` retries the real index update once with a guaranteed, fixed gas budget. If the calling transaction can't fund that budget, it reverts `E451` (retry with more gas) — gas starvation can never force the fallback. If the retry succeeds, a real snapshot now brackets maturity and settlement resolves on real data with no fallback effects.
* **Only when the update fails despite the guaranteed budget** — a genuinely dead, invalid, or garbage-value oracle — does settlement proceed at an **extrapolated index**: the last-known index is projected forward at the trailing average rate, with the slope anchored one day behind the final snapshot (deterministic no matter when settlement finally lands, since a failing oracle appends no further snapshots). `ExpiryIndexExtrapolated(swapId, oracle, expiryTime, lastUpdate, expiryIndex)` is emitted.
* Engaging the fallback **permanently terminates both halves of the market pair for new swaps** — the settling market *and* its `correspondingMarketId` twin (same effect and event as `terminateMarket`). The verdict is about the shared reference oracle, not one market: neither half may sell new positions priced against that oracle's history, even if the oracle later resumes. **Existing positions keep settling normally on both halves.**

This is what keeps a permanently dead oracle from bricking settlement — or holding the expired-unsettled LP gate open — forever: every stuck swap becomes fallback-settleable once the grace elapses, so the market winds down instead of freezing.

**Returns:** one `Types.SettlementResult` per input ID:

```solidity
struct SettlementResult {
    uint256 settlementAmount; // net obligation transferred
    uint8   netRecipient;     // 0 = LP receives, 1 = buyer receives
}
```

**Batching.** Pass an array because keepers typically sweep many swaps at once — the oracle index update per-swap is cheap, and batching amortizes the `rateIndex.update` call.

**Reverts (per swap, the whole batch aborts):**

| Code   | Reason                                                                                                                                                                                                                                             |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `E500` | `swap.entryTimestamp == 0` (swap doesn't exist).                                                                                                                                                                                                   |
| `E506` | `block.timestamp < expiry` — too early to settle this swap.                                                                                                                                                                                        |
| `E450` | No oracle snapshot at or after `swap.entryTimestamp + swapTerm` **and the settlement grace period hasn't elapsed** — settlement refuses to extrapolate. Keepers should call `updateMarketRateIndex` post-maturity to advance the snapshot horizon. |
| `E451` | Past the grace window, the transaction doesn't carry enough gas to fund the fallback adjudication retry — resend with more gas. Guarantees gas starvation can't force an extrapolated settlement.                                                  |

> Already-settled IDs in the batch are **skipped** (not reverted), so a partially pre-settled batch still succeeds for the rest.

**See also:** [`exitSwapEarly`](#exitswapearly), [`liquidateSwap`](#liquidateswap), [`claimEscrow`](#claimescrow), [`updateMarketRateIndex`](/protocol/views.md#updatemarketrateindex).

***

## exitSwapEarly

```solidity
function exitSwapEarly(
    Types.ExitRequest[] calldata requests,
    address onBehalfOf
) external nonReentrant returns (Types.SettlementResult[] memory results);
```

**Who calls:** the owner of every swap in `requests`, or an authorized delegate of that owner. All `requests[i].swapId` must belong to `onBehalfOf`.

**What it does:** marks each swap `isEarlyExit = true` and settles them using the same settlement pipeline as `makePayment` — but with the `earlyExit` branch taken, which (a) uses `block.timestamp` as the effective expiry, (b) re-reads the base rate for the remaining tenor to project the unrealized fixed leg, and (c) applies `market.earlyExitFee` to the buyer. After settlement, each request's `minExitAmount` is checked against the buyer's total entitlement (`settlementPayouts[swapId] + escrowedCollateral[swapId]` — both are summed so an escrowed payout still satisfies the slippage check).

Requires the market to have been created with `earlyExitAllowed = true`, and the swap must still be open and already have at least one second of elapsed time.

### Early-exit economics

Early exit is **not** a symmetric mark-to-market closeout — it settles *accrued* value plus any *projected losses*, and the buyer forfeits projected gains:

* **Accrued payments** (both legs, up to `block.timestamp`) always settle.
* **Projected remaining payments** are computed for the unelapsed term using a fresh base-rate read at the remaining tenor (this fresh read, rather than the entry rate, is what prevents manipulation). Then:
  * If the projected remaining payments **favor the buyer**, they are **forfeited** — only the accrued legs settle. You give up unrealized upside by exiting early.
  * If they **favor the pool**, the buyer is **charged for them** — the remaining legs are added to the obligation. You can't exit to dodge a projected loss.
* **The fee is proportional to the net closeout, not the notional:** `earlyExitFee = netObligation × market.earlyExitFee / WAD`. It is charged to the buyer in whichever direction the net flows — *added* to what the buyer owes, or *deducted* from what the buyer receives. A zero net closeout means a zero fee.
* **The retained fee can be less than the nominal fee.** Settlement caps the buyer's payout at the pool's `poolCollateralBacking` (and the buyer's payment at their `collateralBalance`). When a cap binds, part or all of the nominal fee is erased by the cap — the `earlyExitFee` field in `SwapClosed` reports what the pool **actually retained**, which can be less than the configured rate implies, down to `0`.

**LP-side note — settlement-jump vesting.** An early exit can realize pool value beyond the swap's conservative live mark (a forfeited buyer-favorable leg, the unaccrued remaining-term fee, or a pool-favorable remaining leg). That excess is not credited to the pool's NAV instantly: it is diverted into a per-market **vesting reserve** (withheld from NAV reads, still inside pool collateral) and streamed back over the canceled remaining term, keeping transactable share prices continuous across the exit (`SettlementVestingClipped` is emitted when the diversion is clipped). The **buyer's payout is completely unaffected** — only the timing of LP-side profit recognition shifts.

**Parameters:**

| Parameter    | Meaning                                                                                                                             |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `requests[]` | Array of `ExitRequest { bytes32 swapId; uint256 minExitAmount; }`. `minExitAmount == 0` disables the slippage check for that entry. |
| `onBehalfOf` | Swap owner. All `requests[i]` must reference swaps owned by this address.                                                           |

**Returns:** one `SettlementResult` per request, same shape as `makePayment`.

> **Duplicate IDs in a batch:** the same `swapId` appearing twice passes the up-front validation on each occurrence (validation runs before any settlement), then the **first** occurrence settles it and later ones hit the settled-skip — their `results` slots stay zeroed. The slippage check keys `settlementPayouts`/`escrowedCollateral` by `swapId`, so every occurrence compares its `minExitAmount` against the one real payout.

**State changes / events:**

* Each swap: `swap.isEarlyExit = true`, then full settlement (see `makePayment`).
* Vesting reserve credited when the exit realizes value beyond the live mark (see above); `SettlementVestingClipped` emitted if the diversion is clipped.
* Emits `SwapClosed` with `closureType = 1` per swap; its `earlyExitFee` field carries the fee actually retained after collateral caps.

**Reverts:**

| Code   | Reason                                                                                                                                                    |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `E206` | `msg.sender` not authorized for `onBehalfOf`.                                                                                                             |
| `E500` | Swap doesn't exist.                                                                                                                                       |
| `E203` | Swap is not owned by `onBehalfOf`.                                                                                                                        |
| `E501` | Swap already settled.                                                                                                                                     |
| `E506` | `swap.entryTimestamp >= block.timestamp` — can't exit in the same second you opened.                                                                      |
| `E550` | Market was created with `earlyExitAllowed = false`.                                                                                                       |
| `E502` | Swap is already past expiry — use `makePayment` instead.                                                                                                  |
| `E611` | Base rate oracle returned an invalid reading for the remaining tenor (fetched to project the unrealized fixed leg).                                       |
| `E512` | The projected base rate for the remaining tenor is negative on a Cumulative reference market — the remaining leg can't be priced (same gate as at entry). |
| `E606` | Reference rate oracle returned invalid or zero data during the index update.                                                                              |
| `E552` | Slippage — buyer payout below `minExitAmount` for some request.                                                                                           |

**See also:** [`buySwap`](#buyswap), [`makePayment`](#makepayment).

***

## liquidateSwap

```solidity
function liquidateSwap(bytes32 swapId) external nonReentrant;
```

**Who calls:** anyone. Liquidation is permissionless — `msg.sender` becomes the liquidator and is paid a reward whose source depends on which side is being liquidated. Buyer-side liquidations pay the **prefunded `liquidationBounty`** that was banked from the buyer at `buySwap` entry (held separately from `collateralBalance` and refunded to the buyer at normal expiry if liquidation never happens). Pool-side liquidations pay `market.liquidationIncentive × poolCollateralBacking / WAD`, drawn from the LP backing for this swap.

**What it does:** updates the market's oracle index, delegates to `Utils.liquidateSwap`, and emits `SwapLiquidated` + `SwapClosed` (with `closureType = 2`). `Utils.liquidateSwap` computes the accrued P\&L and decides whether the **buyer** is liquidatable, whether the **pool** is liquidatable, or neither.

Key properties:

* **Accrued, not projected.** Liquidation is based on payments that have already been earned against the oracle index up to `block.timestamp` — not on where the rate might go. The exact triggers:
  * **Buyer-side:** liquidatable when `netObligation ≥ collateralBalance` — the accrued amount the buyer owes has reached (or passed) their posted collateral. The prefunded bounty sits outside `collateralBalance`, so no carve-out is needed on this side.
  * **Pool-side:** liquidatable **early**, when `netObligation ≥ poolCollateralBacking − liquidatorReward`. The liquidator's reward is carved from the pool backing, so the trigger threshold is derived from the payout — reward and liquidatable boundary stay aligned by construction.
* **Liquidator incentive.** The payout source depends on which side is liquidated:
  * **Buyer-side**: the liquidator receives the prefunded `swap.liquidationBounty` — a fixed amount banked at swap entry as `requiredBuyerCollateral × market.liquidationIncentive / WAD`. It is held outside `collateralBalance` and is excluded from bucket aggregates; if the swap is *never* liquidated, the bounty is returned to the buyer at normal settlement.
  * **Pool-side**: the liquidator receives `swap.poolCollateralBacking × market.liquidationIncentive / WAD`, drawn from the LP backing reserved for this swap; the buyer receives the rest of the backing plus their own collateral and prefunded bounty back.
* **Single side.** At most one side is liquidated in a call; the other receives its normal settlement.
* **Escrow fallback.** If the post-liquidation transfer to the buyer reverts, the payout is escrowed for later [`claimEscrow`](#claimescrow).

**Parameters:** `swapId` — the position to liquidate.

**State changes / events:**

* `swap.settled = true`; `settlementPayouts[swapId]` recorded.
* `pool.lockedCollateral -= lpCollateralReleased`. On a **buyer-side** liquidation, `pool.totalCollateral` *increases* by the full seized buyer collateral (the bounty goes to the liquidator, everything else to the pool); on a **pool-side** liquidation it decreases by the backing paid out.
* **Settlement-jump vesting (buyer-side).** As with early exits, if the seized collateral exceeds the swap's live mark, the excess is diverted into the market's vesting reserve (withheld from NAV reads) and streamed back over the canceled remaining term, rather than crystallizing into the share price instantly. Because the trigger is accrued-only, both marks are typically already pinned at the collateral cap by the time liquidation fires, so the diverted amount is usually small. A pool-side liquidation is a NAV decrease, so no vesting applies.
* `expiryQueuePointer[marketId]` advanced past now-settled entries via `Utils.tryAdvanceExpiryPointer` (the queue itself is append-only — settled entries are zeroed in place but the array does not shrink).
* Emits `SwapLiquidated(marketId, swapId, liquidatedParty, buyerLiquidated, poolLiquidated, collateralTransferred, liquidator)`.
* Emits `SwapClosed` with `closureType = 2`; its final `liquidatorReward` field carries the amount paid to the liquidator (this field is `0` for expiry and early-exit closures).
* Emits `PoolCollateralZeroed` if a pool-side liquidation drained the pool's collateral to zero — the same signal the settlement path emits.

**Reverts:**

| Code   | Reason                                                                                                                                                       |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `E608` | `swapId` doesn't exist — its zero-address reference oracle is uninitialized, so the index update reverts before liquidation runs.                            |
| `E606` | Reference rate oracle returned invalid or zero data during the index update.                                                                                 |
| `E507` | Not liquidatable — neither buyer nor pool is underwater on accrued P\&L. Also covers already-settled and past-expiry swaps (settle those via `makePayment`). |

**See also:** [`getSwapNetAmount`](/protocol/views.md#getswapnetamount) to check a swap's P\&L before attempting liquidation, [`claimEscrow`](#claimescrow) for the escrow fallback.

***

## transferSwapPosition

```solidity
function transferSwapPosition(bytes32 swapId, address newOwner, address onBehalfOf) external;
```

**Who calls:** the current swap owner, or an authorized delegate of that owner.

**What it does:** flips `swap.userAddress` from `onBehalfOf` to `newOwner`. After this call, the new owner is the one who will receive any positive net settlement and who can call `exitSwapEarly` / `claimEscrow` on this swap. The swap's economic terms are unchanged.

**Parameters:**

| Parameter    | Meaning                                                                                                                                                                                          |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `swapId`     | The swap to transfer. Must exist and be unsettled.                                                                                                                                               |
| `newOwner`   | The new owner. Must not be `address(0)`. No authorization check is performed on `newOwner` — the transfer is one-sided, so only send it to an address you control or a contract that expects it. |
| `onBehalfOf` | The current owner. `msg.sender` must be authorized for this address unless they are `onBehalfOf`.                                                                                                |

**State changes / events:**

* `swap.userAddress = newOwner`.
* Emits `SwapTransferred(swapId, previousOwner, newOwner, caller)`.

**Reverts:**

| Code   | Reason                                        |
| ------ | --------------------------------------------- |
| `E206` | `msg.sender` not authorized for `onBehalfOf`. |
| `E500` | Swap doesn't exist.                           |
| `E203` | `swap.userAddress != onBehalfOf`.             |
| `E501` | Swap already settled.                         |
| `E103` | `newOwner == address(0)`.                     |

This is the hook that `SwapPositionWrapper` (the ERC-721 wrapper contract) uses under the hood — but note the wrapper is **custodial**: `wrap` calls `transferSwapPosition` to move the position *into* the wrapper contract, and `unwrap` calls it to move the position back out to the caller. Those are the only two places the wrapper calls it. A plain ERC-721 transfer of the wrapped token moves **only the token** — `swap.userAddress` stays the wrapper contract until someone unwraps.

**See also:** [`setAuthorization`](/protocol/authorization.md#setauthorization).

***

## claimEscrow

```solidity
function claimEscrow(bytes32 swapId) external nonReentrant returns (uint256 amount);
```

**Who calls:** the original swap owner (`swap.userAddress`) or an authorized delegate.

**What it does:** retrieves collateral that SwapCore couldn't deliver at settlement or liquidation. When the buyer-side transfer in `_settleSwaps` or `liquidateSwap` reverts (e.g., the recipient became blacklisted between `buySwap` and settlement), the payout is moved to `escrowedCollateral[swapId]` and `BuyerTransferFailed` is emitted. `claimEscrow` pulls that balance, zeroes the entry, and sends it to `swap.userAddress`.

**Crucially, funds are always delivered to the original `swap.userAddress`, never to `msg.sender`**. An authorized delegate can trigger the claim but cannot redirect the money. If the recipient is still blocked, `safeTransfer` will revert and the escrow stays in place — try again later.

**Parameters:** `swapId`.

**Returns:** `amount` — the amount delivered.

**State changes / events:**

* `escrowedCollateral[swapId] = 0` (before the transfer, CEI pattern).
* `IERC20(swapToken).safeTransfer(swap.userAddress, amount)`.
* Emits `EscrowClaimed(swapId, recipient, caller, amount)`.

**Reverts:**

| Code         | Reason                                                |
| ------------ | ----------------------------------------------------- |
| `E206`       | `msg.sender` not authorized for `swap.userAddress`.   |
| `E400`       | `escrowedCollateral[swapId] == 0` — nothing to claim. |
| ERC20 revert | Target is still blocked from receiving the token.     |

**See also:** [`makePayment`](#makepayment), [`exitSwapEarly`](#exitswapearly), [`liquidateSwap`](#liquidateswap), [`setAuthorization`](/protocol/authorization.md#setauthorization).

***

## advanceExpiryPointer

```solidity
function advanceExpiryPointer(bytes32 marketId) external;
```

**Who calls:** anyone — permissionless queue maintenance.

**What it does:** advances `expiryQueuePointer[marketId]` past already-settled entries in the market's expiry queue. The pointer normally advances opportunistically on every settlement and liquidation, but its scan is bounded per call, so it can lag behind after large settled runs. Since the pointer is what LP entry/exit gating and expired-unsettled checks read, this helper lets anyone catch it up without settling anything.

**State changes / events:** `expiryQueuePointer[marketId]` advanced (settled queue entries are zeroed in place; the array does not shrink). No events; never reverts on an up-to-date queue — it's simply a no-op.

**See also:** [`makePayment`](#makepayment).


---

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