# Swaps

This page covers the full **buyer lifecycle**: opening a swap, moving it, closing it (three ways), and retrieving escrowed settlement if a direct transfer ever fails. 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
```

Three 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 when the **accrued** P\&L exceeds a side's posted collateral. There is no "projected" liquidation that closes a swap based on where it's trending. This keeps the rule simple and predictable.
* **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.

***

## buySwap

```solidity
function buySwap(
    bytes32 marketId,
    uint256 notionalAmount,
    address onBehalfOf,
    int256  rateBound,
    uint256 maxMarkup
) 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 and checks it's live (`exists && !terminated`).
2. Reads `baseRate` from `baseSwapRateOracle` (tenor-dependent) and `referenceRate` from `referenceRateOracle`.
3. Computes `availableLiquidity` from the pool's unlocked collateral via `SwapFormulas.calculateAvailableLiquidity(totalLpC, baseRate, swapTerm, leverageMultiplier)` and asserts `notionalAmount > 0 && <= availableLiquidity`.
4. Integrates the kinked utilization fee curve over `[uPre, uPost]` using `market.utilFeeSlopeWad`, `kinkUtilization`, and `maxKinkFeeWad`.
5. Reads `riskPremium` from the market's risk premium oracle (returns `0` if the address is zero).
6. Builds `swapRate`: for **BUY\_FIXED**, `swapRate = baseRate + utilFee + riskPremium`; for **BUY\_FLOATING**, the fee leg is carried separately and the reference rate drives the fixed leg — see the source at `SwapCore.sol:605-609`.
7. Applies slippage guards (see `rateBound` / `maxMarkup` below).
8. Sizes `requiredBuyerCollateral` and `totalLpCollateralRequired` using the fee-inclusive and base-only formulas respectively; rejects if either side is below `market.minCollateral` or if the pool lacks enough unlocked collateral.
9. Reads protocol fee config from `Admin` once and computes `protocolFee = notionalAmount × feeRate × swapTerm / (SECONDS_IN_YEAR × WAD)`.
10. Generates `swapId`, writes the `SwapPosition` to storage, assigns it to a bucket (`Utils.addSwapToBucket`), bumps `pool.lockedCollateral += totalLpCollateralRequired`, and increments the expiry-epoch counter.
11. Pulls `requiredBuyerCollateral + protocolFee` from `msg.sender`. The protocol fee is split: if `creatorFeeShare > 0` and the market has an owner, the creator's share is sent to `market.marketOwner` via a graceful low-level call (on failure the full fee goes to the multisig); the remainder goes to `admin.getProtocolMultisig()`.
12. Emits `SwapCreated`, plus `ProtocolFeeCollected` and `CreatorFeeCollected` when applicable.

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

**Returns:** `swapId`, a unique `bytes32` derived from `keccak256(marketId, onBehalfOf, block.timestamp, ++globalNonce)`.

**Payment from caller:** `requiredBuyerCollateral + protocolFee`. 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`.
* `unsettledExpiryCount[marketId][expiryEpoch]++` and `earliestUnsettledEpoch` maintained.
* `IERC20(swapToken).transferFrom(msg.sender, address(this), requiredBuyerCollateral + protocolFee)`.
* `IERC20(swapToken).transfer` (or low-level call for creator split) to the multisig and/or market owner.
* Emits `SwapCreated`, `ProtocolFeeCollected` (if `protocolFee > 0`), `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.                                                                        |
| `E400` / swap-params revert | `notionalAmount == 0`.                                                                                      |
| `E404`                      | Pool has insufficient available collateral to back the LP side.                                             |
| `E503`                      | `requiredBuyerCollateral < minCollateral` or `totalLpCollateralRequired < minCollateral`.                   |
| `E510`                      | Slippage — rate bound exceeded (BUY\_FIXED: `swapRate > rateBound`; BUY\_FLOATING: `baseRate < rateBound`). |
| `E511`                      | Slippage — `utilFee + riskPremium > maxMarkup`.                                                             |
| `E606`                      | Reference or base rate oracle returned invalid / unhealthy.                                                 |
| 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.

**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. Updates the market's oracle index to the latest read.
3. Resolves the **historical** oracle index at expiry via `rateIndex.getIndexAt(oracle, expiryTimestamp)`. If no snapshot exists, it falls back to the freshly-updated index (prevents a permanent stuck-settlement edge case).
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`, decrements the expiry-epoch counter, and advances `earliestUnsettledEpoch` if this was the last unsettled swap in that epoch.
7. Emits `SwapClosed` with `closureType = 0` and, if pool collateral hit zero, `PoolCollateralZeroed`.

**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).            |
| `E501` | Swap already settled.                                       |
| `E506` | `block.timestamp < expiry` — too early to settle this swap. |
| `E606` | Reference rate oracle is unhealthy during the index update. |

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

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

**State changes / events:**

* Each swap: `swap.isEarlyExit = true`, then full settlement (see `makePayment`).
* Emits `SwapClosed` with `closureType = 1` per swap.

**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.                             |
| `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 `market.liquidationIncentive` out of the liquidated side's remaining collateral.

**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 (their `collateralBalance` can't cover the payment they owe), whether the **pool** is liquidatable (its `poolCollateralBacking` can't cover the payment it owes), 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. A swap is only liquidatable when one side's collateral is mathematically insufficient right now.
* **Liquidator incentive.** Paid out of the liquidated side's collateral as `incentive = collateralPayout × market.liquidationIncentive / WAD`. If the pool is the liquidated side, the incentive comes from `poolCollateralBacking`; if the buyer is, from their `collateralBalance`.
* **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`; `pool.totalCollateral` adjusted for any pool-side loss.
* `unsettledExpiryCount` decremented; `earliestUnsettledEpoch` advanced if needed.
* Emits `SwapLiquidated(marketId, swapId, liquidatedParty, buyerLiquidated, poolLiquidated, collateralTransferred, liquidator)`.
* Emits `SwapClosed` with `closureType = 2`.

**Reverts:**

| Code                                               | Reason                                                                           |
| -------------------------------------------------- | -------------------------------------------------------------------------------- |
| `E500`                                             | Swap doesn't exist.                                                              |
| `E501`                                             | Already settled.                                                                 |
| not-liquidatable revert from `Utils.liquidateSwap` | Neither buyer nor pool is underwater — the swap doesn't qualify for liquidation. |
| `E606`                                             | Reference rate oracle unhealthy.                                                 |

**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 — transferring the NFT calls `transferSwapPosition` to move the underlying position to whoever holds the token.

**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, swapToken, 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).


---

# Agent Instructions: 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:

```
GET https://docs.kairosswap.com/protocol/swaps.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
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.
