Skip to main content

Circuit Breaker v2 — UI, Backend & Infrastructure Guide

Overview

Circuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entry v2 (CBv2) is an on-chain rate limiterA mechanism that caps the rate of withdrawals or actions over time to reduce exploit impact.View glossary entryrate limiterA mechanism that caps the rate of withdrawals or actions over time to reduce exploit impact.View glossary entry with deferred settlement for protocol outflows. It protects against exploits by limiting how much value can leave the protocol within a time window. When an outflow exceeds available capacity, it is queued for delayed execution instead of being rejected.

Users normally do not need to interact with the Circuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entryCircuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entry contract directly for most withdrawals. They call the same redeem() / withdraw() functions they always would.

The Circuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entry only becomes visible when a withdrawal larger than the available buffer is requested and gets queued for later settlement. Most withdrawals should not be queued as long as they remain within the available buffer.

Key Concepts

TermDescription
Protected contractA contract registered with the Circuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entryCircuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entry that routes outflows through it (e.g., MintAndRedeem, ftYieldWrapperV2). Each inherits ProtectedContract and calls into the Circuit Breaker internally.
Limiter assetThe token used as the rate-limit bucket key. Buffer capacity is sized relative to TVL of this asset.
Transfer tokenThe token actually transferred. Usually the same as the limiter asset, but can differ (e.g., wrapper withdrawals may use the wrapper token as limiter but transfer strategyA yield venue tracked by the wrapper; strategies can be added/removed/reordered and capital moved between them by authorized roles.View glossary entry position tokens).
Main bufferSteady-state withdrawal capacity. Replenishes linearly over mainWindow toward a cap of maxDrawRateWad * TVL.
Elastic bufferTemporary capacity from deposits. Decays proportionally over elasticWindow. Prevents deposit-then-withdraw flash loanAn uncollateralized loan that must be borrowed and repaid within a single transaction; often used for arbitrage or attacks.View glossary entry attacks from consuming the main buffer.
Settlement delayTime a queued outflow must wait before it can be executed. Configurable between 5 minutes and 7 days. Default is 6 hours.

Architecture

graph LR
MR["MintAndRedeem (ProtectedContract)"] -->|"attemptOutflow / recordInflow"| CB["CircuitBreakerV2 (scoped limiter per contract)"]
YW["ftYieldWrapperV2 (ProtectedContract)"] -->|"attemptOutflow / recordInflow"| CB

Circuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entry logic lives directly in each engine via the ProtectedContract mixin. There is no separate router contract — each protected contract calls the Circuit Breaker internally when processing outflows.


How a Withdrawal Works

flowchart TD
A["User calls withdraw / redeem"] --> B{"CB enabled?"}
B -- NO --> C["Direct transfer to user (no rate limiting)"]
B -- YES --> D{"Recipient whitelisted?"}
D -- YES --> E["Transfer immediately (no buffer consumed)"]
D -- NO --> F["CB checks capacity: available = min(mainBuffer, elasticBuffer)"]
F --> G{"amount ≤ available?"}
G -- YES --> H["IMMEDIATE: Tokens sent to user now"]
G -- NO --> I["QUEUED: Tokens sent to CB escrow; queue entry created"]
I --> J["After settlement delay: anyone calls executeQueued(); CB releases tokens to user"]

style C fill:#d4edda,stroke:#28a745
style E fill:#d4edda,stroke:#28a745
style H fill:#d4edda,stroke:#28a745
style I fill:#fff3cd,stroke:#ffc107
style J fill:#d4edda,stroke:#28a745

Key points for UIThe visual interface through which users interact with an app or product.View glossary entry:

  • If the withdrawal is immediate, nothing special — the user gets their tokens in the same transaction.
  • If the withdrawal is queued, the UIThe visual interface through which users interact with an app or product.View glossary entryUIThe visual interface through which users interact with an app or product.View glossary entry should show a pending state with a countdown to settlesAt. Once the delay passes, a "Claim" button (or backend keeper) calls executeQueued(queueId) to release the funds.
  • executeQueued is permissionless — anyone can call it once the delay has elapsed. The UIThe visual interface through which users interact with an app or product.View glossary entry can offer a "Claim" button to the user, or a backend keeper can auto-execute on their behalf.

Step-by-Step

  1. Pre-check capacity before the user submits:

    const [wouldBeImmediate, availableCapacity] = await cb.checkOutflow(
    contract,
    asset,
    amount,
    tvl,
    );
  2. If queued, track the queue entry via events:

    • Circuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entry emits: OutflowQueued(queueId, asset, recipient, amount, settlesAt)
  3. Wait for settlement:

    const remaining = await cb.timeUntilSettled(queueId); // seconds until executable
    const ready = await cb.isSettled(queueId); // true if executable now
  4. Execute (anyone can call):

    await cb.executeQueued(queueId);
    // or batch:
    await cb.executeQueuedBatch([queueId1, queueId2, queueId3]);

Edge Case: Pre-Check Staleness

Note: checkOutflow and withdrawalCapacity read the buffer state at call time. Between the moment the UIThe visual interface through which users interact with an app or product.View glossary entryUIThe visual interface through which users interact with an app or product.View glossary entry shows "This withdrawal will be instant" and the moment the user's transaction is mined, other transactions may consume buffer capacity. If that happens, the withdrawal that appeared instant will be queued instead.

The UIThe visual interface through which users interact with an app or product.View glossary entry should handle this gracefully:

  • Always listen for both OutflowImmediate and OutflowQueued events after a withdrawal transaction, regardless of what the pre-check indicated.
  • If a pre-check said "instant" but the transaction emits OutflowQueued, transition the UIThe visual interface through which users interact with an app or product.View glossary entry to the queued/countdown state.

Recommended: show likelihood based on buffer headroom. Rather than a binary "instant / queued" label, compare the user's amount against current capacity and convey confidence:

const capacity = await cb.withdrawalCapacity(contract, asset, tvl);
const ratio = amount / capacity; // how much of the buffer this withdrawal would consume

if (ratio <= 0.5) {
// Plenty of headroom — very unlikely to be front-run
showMessage("This withdrawal will be processed instantly.");
} else if (ratio <= 0.9) {
// Moderate headroom — possible but unlikely
showMessage(
"This withdrawal is expected to be instant, but may be queued if demand increases before confirmation.",
);
} else if (ratio <= 1.0) {
// Tight — high chance another tx consumes remaining capacity first
showWarning(
"This withdrawal is close to the instant limit and may be queued if other withdrawals are confirmed first.",
);
} else {
// Exceeds capacity — will definitely be queued
showMessage(
`This withdrawal exceeds the instant limit (${formatAmount(capacity)} available). It will be queued and claimable in ~6 hours.`,
);
}

This gives users a realistic expectation without false precision. The exact thresholds are up to the UIThe visual interface through which users interact with an app or product.View glossary entryUIThe visual interface through which users interact with an app or product.View glossary entry team — the key insight is that amount / capacity near 1.0 means the outcome is sensitive to other pending transactions.


Dual Buffer System

The rate limit is determined by the minimum of two independent buffers:

flowchart TB
subgraph buffers["Two Independent Buffers"]
direction TB

subgraph main["Main Buffer"]
direction TB
M0["Time-replenishing"]
M1["Cap = TVL × maxDrawRate (5%)"]
M2["Replenishes linearly over mainWindow (24h)"]
M3["Consumed by outflows"]
M0 --> M1 --> M2 --> M3
end

subgraph elastic["Elastic Buffer"]
direction TB
E0["Deposit-tracking"]
E1["Increases with each deposit"]
E2["Decays linearly over elasticWindow (30 min)"]
E3["Consumed first on outflows"]
E0 --> E1 --> E2 --> E3
end
end

M3 --> MIN["available = min(mainBuffer, elasticBuffer)"]
E3 --> MIN
MIN --> OUT{"withdrawal ≤ available?"}
OUT -- YES --> IMM["Instant"]
OUT -- NO --> Q["Queued"]

style IMM fill:#d4edda,stroke:#28a745
style Q fill:#fff3cd,stroke:#ffc107

For the UIThe visual interface through which users interact with an app or product.View glossary entry health gauge, use getAssetHealth() which returns:

  • mainBuffer / mainBufferCap — show as a percentage bar
  • elasticBuffer — additional transient capacity from recent deposits
  • utilizationBps — 0–10000, how much of the main buffer is consumed (0 = full capacity, 10000 = depleted)
  • pendingOutflows — total value sitting in the queue for this asset
  • isPaused — if true, all operations for this asset are halted

Queue Entry States

A queued withdrawal moves through these states. The UIThe visual interface through which users interact with an app or product.View glossary entry should reflect each:

stateDiagram-v2
[*] --> Pending : Withdrawal exceeds rate limit

Pending --> Paused : Admin/guardian paused this entry
Paused --> Pending : Owner resumed this entry

Pending --> Pending : Operator called speedUp() and settlesAt reduced

Pending --> Claimable : settlesAt reached

Claimable --> Executed : executeQueued() called and tokens sent to user
Claimable --> Invalidated : Emergency recovery occurred and entry no longer valid

Executed --> [*]
Invalidated --> [*]

UI State Mapping

Queue Statusstatus fieldsettlesAt vs nowWhat to Show
Pending (waiting)PendingsettlesAt > nowCountdown timer: "Claimable in X hours Y min"
Pending (sped up)PendingsettlesAt > now (but reduced)Updated countdown, maybe "Expedited" badge
ClaimablePendingsettlesAt ≤ now"Claim" button → calls executeQueued(queueId)
PausedPausedN/A"On hold — contact support". No claim button
ExecutedEntry deletedN/A"Completed" — entry no longer exists in storage
InvalidatedPendingAnyexecuteQueued will revert with QueueInvalidated. Show "Cancelled — emergency recovery"

How to detect invalidated entries: Compare queue.recoveryEpoch with cb.assetRecoveryEpoch(queue.token). If they differ, the entry is invalidated.


Scoped Limiters

By default, all protected contracts share the same limiter state per asset. When scoped limiter is enabled for a contract, that contract gets its own isolated buffer state. This prevents one engine's activity from affecting another's capacity.

Why Scoping Matters

graph TB
subgraph problem["Shared State (default) — PROBLEM"]
direction TB
MR_S["MintAndRedeem (TVL = 100M)"] --> SHARED["shared buffer"]
YW_S["ftYieldWrapper (TVL = 10M)"] --> SHARED
SHARED --> OSC["Buffer cap oscillates between 5M and 500K on alternating calls"]
end

subgraph solution["Scoped State (opt-in) — SOLUTION"]
direction TB
MR_I["MintAndRedeem (TVL = 100M)"] --> BUF_MR["Own buffer cap = 100M × 5% = 5M"]
YW_I["ftYieldWrapper (TVL = 10M)"] --> BUF_YW["Own buffer cap = 10M × 5% = 500K"]
GLOBAL["Config (maxDrawRate, windows) and pause state remain GLOBAL"]
end

style OSC fill:#f8d7da,stroke:#dc3545
style BUF_MR fill:#d4edda,stroke:#28a745
style BUF_YW fill:#d4edda,stroke:#28a745

Each protected contract (MintAndRedeem, ftYieldWrapperV2) has its own isolated rate-limit buffer. This means:

  • A large redemption on MintAndRedeem does not reduce capacity for ftYieldWrapperV2 withdrawals (and vice versa).
  • When calling view functions, you must pass the correct contract address as the first argument to get that contract's buffer state.
  • Always use the 3-arg view overloads that accept (contract, asset, tvl). The 2-arg versions read shared state that scoped contracts don't use.
// Correct — reads MintAndRedeem's own buffer
cb.withdrawalCapacity(mintAndRedeemAddr, ftUSDAddr, mintAndRedeemTvl);

// Correct — reads wrapper's own buffer
cb.withdrawalCapacity(wrapperAddr, ftUSDAddr, wrapperTvl);

// Wrong for scoped contracts — reads shared state (empty)
cb.withdrawalCapacity(ftUSDAddr, someTvl);

Check if a contract is scoped:

const isScoped = await cb.isScopedLimiter(contractAddress);

Whitelisted Recipients

The Circuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entryCircuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entry owner can whitelist specific recipient addresses. Withdrawals to a whitelisted address always execute immediately — they bypass the rate limiter entirely and do not consume any buffer capacity.

This is used for trusted protocol-owned addresses (e.g., treasury, other protocol contracts) that should never be rate-limited.

How It Affects the UI

  • Whitelisted recipients will never see a queued withdrawal, regardless of amount or buffer state.
  • The flowchart above shows this path: if the recipient is whitelisted, the transfer is immediate with no buffer consumed.
  • checkOutflow does not account for whitelisting — it only checks buffer capacity. A pre-check may report wouldBeImmediate = false for a large amount, but if the recipient is whitelisted the on-chain transaction will still execute immediately.

Checking Whitelist Status

// Check if a specific address is whitelisted
const isWhitelisted = await cb.isWhitelistedRecipient(recipientAddress);

// List all whitelisted addresses
const whitelist = await cb.getWhitelistedRecipients();

If your UIThe visual interface through which users interact with an app or product.View glossary entry pre-checks withdrawal outcome, check whitelist status first to avoid showing a misleading "will be queued" warning:

const isWhitelisted = await cb.isWhitelistedRecipient(userAddress);
if (isWhitelisted) {
// Always instant — skip buffer capacity check
showInstantWithdrawalUI();
} else {
const [wouldBeImmediate, capacity] = await cb.checkOutflow(
contract,
asset,
amount,
tvl,
);
// Show instant or queued UI based on capacity
}

Admin Management

Whitelist changes are owner-only and emit RecipientWhitelistUpdated(recipient, enabled). Index this event to keep whitelist state current.


Contract Calls Reference

All read calls below are view functions (no gas cost when called via eth_call).

Before a Withdrawal (Pre-Check)

Use these to show the user what will happen before they submit:

What You NeedCallReturns
Will this withdrawal be instant or queued?cb.checkOutflow(contract, asset, amount, tvl)(bool wouldBeImmediate, uint256 availableCapacity)
Max amount user can withdraw instantlycb.withdrawalCapacity(contract, asset, tvl)uint256
Full buffer health for a gaugecb.getAssetHealth(contract, asset, tvl)AssetHealth struct (see below)

Note: contract is the address of the protected contract (e.g., MintAndRedeem or ftYieldWrapperV2), not the Circuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entryCircuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entry itself. tvl is the current TVL of that contract for the given asset — see below for how to obtain it.

How to Obtain TVL

The Circuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entryCircuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entry view functions require a tvl parameter because the Circuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entryCircuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entry itself does not store TVL — it is supplied by the caller. Each protected contract computes TVL differently, and the on-chain _getTvl() override is internal, so the backend must call the appropriate public getter and apply the same floor logic.

Protected ContractPublic TVL GetterUnitsNotes
MintAndRedeemaccountedCollateralTvl()6 decimals (ftUSDA delta‑neutral, yield‑bearing stable asset designed to target $1 while minimizing liquidation risk by balancing long/short exposures (e.g., supply/stake/borrow loops).View glossary entry)Sum of (totalIn - totalOut) across enabled collaterals, normalized to 6 decimals. Does not use oracleExternal feed of asset prices. Flying Tulip futures derive pricing/settlement from in‑house trading activity to avoid oracle lag/manipulation.View glossary entry prices — assumes 1:1 nominal value per collateralAssets allowed as collateral and the maximum per‑asset size configured to manage concentration and risk.View glossary entry.
ftYieldWrapperV2valueOfCapital()underlying token decimalsbalanceOf(underlying) + sum of each strategyA yield venue tracked by the wrapper; strategies can be added/removed/reordered and capital moved between them by authorized roles.View glossary entrystrategyA yield venue tracked by the wrapper; strategies can be added/removed/reordered and capital moved between them by authorized roles.View glossary entry's valueOfCapital(). Note: does not normalize strategyA yield venue tracked by the wrapper; strategies can be added/removed/reordered and capital moved between them by authorized roles.View glossary entry position-token decimals to underlying decimals (the internal _getTvl does this normalization, but the public getter does not — in practice this is a no-op for Aave strategies where decimals match).

MintAndRedeem TVL floor: The on-chain Circuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entryCircuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entry integration applies max(accountedCollateralTvl(), minTVLForMint) so the buffer cap never drops below minTVLForMint * maxDrawRate. The backend should replicate this:

// MintAndRedeem TVL for Circuit Breaker view calls
const rawTvl = await mintAndRedeem.accountedCollateralTvl();
const minTvl = await mintAndRedeem.minTVLForMint();
const tvl = rawTvl > minTvl ? rawTvl : minTvl;

// ftYieldWrapperV2 TVL for Circuit Breaker view calls
const wrapperTvl = await wrapper.valueOfCapital();

If the wrong TVL is passed, the view functions will return incorrect capacity values (buffer cap is tvl * maxDrawRate), so getting this right is important.

After a Queued Withdrawal (Queue Management)

What You NeedCallReturns
List user's pending queue entriescb.getQueuedByRecipient(user, offset, limit)QueuedOutflow[] (paginated)
Get all queue IDs for a usercb.getQueueIdsByRecipient(user)uint256[]
Get a single queue entrycb.getQueued(queueId)QueuedOutflow struct
Is this entry ready to claim?cb.isSettled(queueId)bool
How long until claimable?cb.timeUntilSettled(queueId)uint256 (seconds remaining, 0 if ready)

Claiming (Executing a Queued Withdrawal)

ActionCallNotes
Claim a single entrycb.executeQueued(queueId)Permissionless — any address can call
Claim multiple entriescb.executeQueuedBatch(queueIds)Array of IDs, all executed in one tx

These are write transactions. They will revert if:

  • The system is globally paused
  • The asset is paused
  • The entry has Paused status (admin hold)
  • The settlement delay hasn't passed yet
  • The entry was invalidated by emergency recovery
  • The healthcheck contract (if set) returns false

System Status (Dashboard / Monitoring)

What You NeedCallReturns
Overall system statuscb.getSystemStatus()(active, admin, guardian, delay, numProtected, numAssets, numQueued)
Is system paused?cb.paused()bool
Settlement delaycb.settlementDelay()uint256 (seconds)
Total queued itemscb.activeQueueCount()uint256
Pending outflow value for an assetcb.pendingOutflows(asset)uint256

Enumeration

What You NeedCallReturns
All registered protected contractscb.getProtectedContracts()address[]
All assets with limiter statecb.getTrackedAssets()address[]
Whitelisted recipients (bypass rate limiting)cb.getWhitelistedRecipients()address[]
Per-contract isolation checkcb.isScopedLimiter(contract_)bool

Data Structures

QueuedOutflow

Returned by getQueued() and getQueuedByRecipient():

FieldTypeDescription
tokenaddressThe ERC-20 token being withdrawn
queuedAtuint40Timestamp when the entry was created
settlesAtuint40Timestamp when the entry becomes claimable
statusQueueStatus0 = Pending, 1 = Paused
recipientaddressWho receives the tokens on execution
recoveryEpochuint64Epoch at queue time; if assetRecoveryEpoch differs, entry is invalid
amountuint256Amount of token to be transferred

AssetHealth

Returned by getAssetHealth():

FieldTypeDescription
mainBufferuint256Current main buffer capacity remaining
mainBufferCapuint256Maximum main buffer capacity (TVL * maxDrawRate)
elasticBufferuint256Current elastic buffer capacity remaining
totalCapacityuint256mainBuffer + elasticBuffer
pendingOutflowsuint256Total value queued but not yet executed for this asset
utilizationBpsuint2560–10000, percentage of main buffer consumed
isPausedboolWhether this asset is paused

SystemStatus

Returned by getSystemStatus():

FieldTypeDescription
activebooltrue if not globally paused
adminaddressOwner address (multisigA multi‑signature wallet with elevated permissions used for safety‑critical admin actions.View glossary entry/governance)
guardianAddraddressGuardian — can pauseTemporarily halting token transfers or protocol operations via admin roles for safety.View glossary entry but not unpause
delayuint256Current settlement delay in seconds
numProtectedContractsuint256Count of registered protected contracts
numTrackedAssetsuint256Count of assets with limiter history
numQueuedOutflowsuint256Current open queue entries

Events Reference

Outflow Lifecycle

EventWhen EmittedKey Fields
Inflow(asset, amount, newTvl)Deposit/mint recordednewTvl = TVL after inflow
OutflowImmediate(asset, to, amount)Withdrawal within capacityInstant transfer happened
OutflowQueued(queueId, asset, recipient, amount, settlesAt)Withdrawal exceeds capacitysettlesAt = earliest execution time
OutflowLimiterContext(queueId, limiterAsset, transferToken)Queued with limiter != transfer tokenLinks the limiter bucket to the actual token
OutflowExecuted(queueId, recipient, asset, amount)Queued withdrawal settledTokens transferred to recipient

Queue State Changes

EventWhen Emitted
OutflowPaused(queueId, recipient)Admin/guardian paused a queue entry
OutflowResumed(queueId, recipient)Owner resumed a paused queue entry
OutflowSpedUp(queueId, recipient, newSettlesAt)Operator/owner shortened the delay

System State Changes

EventWhen EmittedUIThe visual interface through which users interact with an app or product.View glossary entry Action
Paused(by)Global pauseTemporarily halting token transfers or protocol operations via admin roles for safety.View glossary entry activatedShow global banner
Unpaused()Global pauseTemporarily halting token transfers or protocol operations via admin roles for safety.View glossary entry liftedRemove banner
AssetPaused(asset, by)Individual asset pausedShow per-asset banner
AssetUnpaused(asset)Individual asset unpausedRemove per-asset banner
AssetTracked(asset)First time an asset interacts with Circuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entryUse for indexing new assets without polling

Configuration Changes

EventWhen Emitted
ConfigUpdated(asset, config)Rate limit parameters changed (asset = address(0) for default)
DelayUpdated(newDelay)Settlement delay changed
ProtectedContractUpdated(contract_, enabled)Contract registered/removed
ScopedLimiterUpdated(contract_, enabled)Per-contract isolation toggled
GuardianUpdated(newGuardian)Guardian role changed
OperatorUpdated(newOperator)Operator role changed
HealthcheckUpdated(newHealthcheck)Healthcheck contract changed
RecipientWhitelistUpdated(recipient, enabled)Whitelist entry added/removed
EmergencyOverride(contract_, asset, amount)Elastic buffer manually inflated (contract_ = address(0) for shared)
EmergencyRecovery(asset, to, amount, newEpoch)All tokens swept, all queued entries invalidated

Default Parameters

ParameterDefaultDescription
maxDrawRateWad5% (0.05e18)Max outflow as % of TVL before queueing
mainWindow24 hoursMain buffer fully replenishes over this window
elasticWindow30 minutesElastic buffer fully decays over this window
settlementDelay6 hoursTime before queued entries become claimable
minTVLForMint500,000 ftUSDA delta‑neutral, yield‑bearing stable asset designed to target $1 while minimizing liquidation risk by balancing long/short exposures (e.g., supply/stake/borrow loops).View glossary entryFloor TVL to prevent rate-limit gaming at low supply

Example UI Flows

Flow 1: User Withdraws (Instant)

  1. User enters amount, clicks "Withdraw"
  2. Pre-check: Call cb.checkOutflow(contract, asset, amount, tvl)wouldBeImmediate = true
  3. Show: "This withdrawal will be processed instantly"
  4. User submits tx → tokens arrive in wallet
  5. Listen for OutflowImmediate event → show success

Flow 2: User Withdraws (Queued)

  1. User enters amount, clicks "Withdraw"
  2. Pre-check: Call cb.checkOutflow(contract, asset, amount, tvl)wouldBeImmediate = false, availableCapacity = X
  3. Show: "This withdrawal exceeds the instant limit (X available). It will be queued and claimable in ~6 hours."
  4. User confirms → tx queuesWaiting periods required to withdraw from certain staking positions (e.g., stETH withdrawals, SOL/AVAX unbonding).View glossary entry the withdrawal
  5. Listen for OutflowQueued event → show pending state with countdown to settlesAt
  6. Periodically call cb.timeUntilSettled(queueId) to update countdown
  7. When timeUntilSettled returns 0 → show "Claim" button
  8. User clicks "Claim" → calls cb.executeQueued(queueId) → tokens arrive
  9. Listen for OutflowExecuted event → show "Completed"

Flow 3: Queued Entry Gets Paused

  1. Entry is in "Pending" state with countdown
  2. Listen for OutflowPaused(queueId) event
  3. Switch to "On hold — under review" state, hide countdown
  4. If OutflowResumed(queueId) fires → return to countdown state

Flow 4: Queued Entry Gets Sped Up

  1. Entry is in "Pending" state with countdown
  2. Listen for OutflowSpedUp(queueId, recipient, newSettlesAt) event
  3. Update countdown to new settlesAt, optionally show "Expedited" badge

Keeper / Bot Integration

Executing Queued Outflows

A keeper bot should:

  1. Index OutflowQueued events to track pending entries.
  2. Poll isSettled(queueId) or compute settlesAt from the event.
  3. Call executeQueued(queueId) or executeQueuedBatch(ids) when ready.
  4. Handle reverts:
    • QueueNotSettled — too early, retry later.
    • QueueInvalidated — emergency recovery happened, skip this entry.
    • GloballyPaused / AssetPausedError — system paused, wait for Unpaused/AssetUnpaused.
    • InvalidStatus — entry was paused by admin, wait for OutflowResumed.
    • HealthcheckFailed — healthcheck contract rejected, investigate.

Batch Execution Strategy

For gas efficiency, batch multiple settled entries:

const allIds = await cb.getQueueIdsByRecipient(user); // or index from events
const settled = [];
for (const id of allIds) {
if (await cb.isSettled(id)) settled.push(id);
}
if (settled.length > 0) {
await cb.executeQueuedBatch(settled);
}

Monitoring Alerts

Recommended event-based alerts for infra/ops:

AlertTriggerSeverity
Rate limit pressureOutflowQueued frequency increasesMedium
EmergencyEmergencyRecovery or Paused emittedCritical
Config changeConfigUpdated or DelayUpdated emittedInfo
Large overrideEmergencyOverride with significant amountHigh
Asset pausedAssetPaused emittedHigh
Guardian changeGuardianUpdated emittedInfo
Scoping changeScopedLimiterUpdated emittedMedium

Admin Operations

Roles

RoleSet ByPermissions
Owner2-step transfer (Ownable2Step)Full admin: config, pauseTemporarily halting token transfers or protocol operations via admin roles for safety.View glossary entry/unpause, resume queued, emergency recover, set all roles
GuardianOwnerPauseTemporarily halting token transfers or protocol operations via admin roles for safety.View glossary entry globally, pauseTemporarily halting token transfers or protocol operations via admin roles for safety.View glossary entry per-asset, pauseTemporarily halting token transfers or protocol operations via admin roles for safety.View glossary entry individual queue entries
OperatorOwnerspeedUp() queued outflows (reduce settlesAt)

speedUp

speedUp(queueId, newSettleDelay) reduces the settlement time on a pending entry. The new settlesAt is set to block.timestamp + newSettleDelay — passing 0 makes it instantly claimable. The call reverts if the new time would be later than the current settlesAt (can only speed up, never delay further).

Current state: The operator is a single address (EOA or multisigA multi‑signature wallet with elevated permissions used for safety‑critical admin actions.View glossary entry).

Emergency Recovery

emergencyRecover(asset, to) transfers all held tokens for an asset and increments the asset's recovery epoch, invalidating all pending queue entries for that asset.

After recovery:

  • View functions (isSettled, timeUntilSettled) may still return stale values for invalidated entries. Only executeQueued checks the epoch and reverts.
  • activeQueueCount is not decremented for invalidated entries.
  • The Circuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entry instance should be replaced with a fresh proxy after recovery.

Future: Operator Contract with Relayer Support

A dedicated operator contract could replace the single-address operator to enable:

  • Relayer / multisigA multi‑signature wallet with elevated permissions used for safety‑critical admin actions.View glossary entry access — Multiple authorized relayers can call speedUp through the operator contract without each needing the operator role on the Circuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entry.
  • Fee marketplace for queue acceleration — Users with queued outflows offer a fee (tip) to incentivize a relayer to speedUp their entry. The operator contract escrows the bounty, the relayer calls speedUp through it, and the bounty pays out on success.
  • Policy rules — The operator contract could enforce constraints like minimum remaining delay or per-asset cooldowns before allowing a speed-up.

This is planned for future exploration.


Healthcheck & Future Improvements

The Circuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entryCircuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entry has an optional healthcheck contract slot that is checked at executeQueued time. If set and it returns false, the claim reverts. No healthcheck is deployed at launch — the slot is address(0).

Why the 6-Hour Default Delay

Without automated checks, the settlement delay is the team's window to detect and manually pauseTemporarily halting token transfers or protocol operations via admin roles for safety.View glossary entry a malicious outflow. 6 hours was chosen to ensure coverage across all timezones.

Planned Improvements (Post-Launch)

These would allow reducing the settlement delay:

ApproachTarget DelayHow It Would Work
Current (no healthcheck)6 hoursTeam monitors and pauses manually
On-chain invariant healthcheck~30 min–1 hourexecuteQueued auto-reverts on invalid state
Off-chain threat detection keeper~15–30 minKeeper monitors for exploits, sets an unhealthy flag to block all claims
Keeper-enabled guardian contract~15–30 minAutomated keepersExternal actors incentivized to execute liquidations or complex unwinds. Flying Tulip uses size/time‑sliced execution and rebates to reduce market impact.View glossary entrykeepersExternal actors incentivized to execute liquidations or complex unwinds. Flying Tulip uses size/time‑sliced execution and rebates to reduce market impact.View glossary entry call pause() via a guardian contract within seconds
CombinedLowest possible delayDefense in depth: all of the above

Audit Delta (CS-FTMPR)

Changes from the ChainSecurity audit that affect UIThe visual interface through which users interact with an app or product.View glossary entry/infra:

New Event: AssetTracked

event AssetTracked(address indexed asset);

Emitted once per asset on first interaction. Use for indexing new assets without polling getTrackedAssets().

New Event: ScopedLimiterUpdated

event ScopedLimiterUpdated(address indexed contract_, bool enabled);

When a contract switches to scoped limiter mode, UIThe visual interface through which users interact with an app or product.View glossary entry must use the 3-arg view overloads for that contract's health/capacity.

New View Overloads (Scoped)

Three existing views now have overloads that accept a contract_ parameter:

  • getAssetHealth(contract_, asset, tvl)
  • withdrawalCapacity(contract_, asset, tvl)
  • checkOutflow(contract_, asset, amount, tvl)

These return the scoped contract's isolated buffer state. Falls back to shared state if the contract is not scoped.

EmergencyOverride Event Includes contract_

event EmergencyOverride(address indexed contract_, address indexed asset, uint256 amount);

contract_ = address(0) for shared state overrides, actual address for scoped overrides.

OutflowLimiterContext Event

event OutflowLimiterContext(uint256 indexed queueId, address indexed limiterAsset, address indexed transferToken);

Companion event emitted alongside OutflowQueued when limiterAsset != transferToken. Links the rate-limit bucket to the actual token being transferred. Relevant for wrapper withdrawals where the limiter is the wrapper token but the transfer is a strategyA yield venue tracked by the wrapper; strategies can be added/removed/reordered and capital moved between them by authorized roles.View glossary entry position token.

QueueStatus Enum Simplified

Removed unused Executed variant. Now only:

enum QueueStatus { Pending, Paused }

If your indexer tracked Executed = 2, remove it. Executed entries are deleted from storage (amount becomes 0).