For the complete documentation index, see llms.txt. This page is also available as Markdown.

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.

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


buySwap

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). 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 — 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, getSwapNetAmount, transferSwapPosition.


makePayment

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

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, liquidateSwap, claimEscrow, updateMarketRateIndex.


exitSwapEarly

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, makePayment.


liquidateSwap

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.

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 to check a swap's P&L before attempting liquidation, claimEscrow for the escrow fallback.


transferSwapPosition

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.


claimEscrow

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, exitSwapEarly, liquidateSwap, setAuthorization.


advanceExpiryPointer

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.

Last updated