Circuit Breaker v2 — UI, Backend & Infrastructure Guide
Overview
Circuit BreakerCircuit 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 limiterrate 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 BreakerCircuit 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 BreakerCircuit 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
| Term | Description |
|---|---|
| Protected contract | A contract registered with the Circuit BreakerCircuit 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 asset | The token used as the rate-limit bucket key. Buffer capacity is sized relative to TVL of this asset. |
| Transfer token | The 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 strategystrategyA 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 buffer | Steady-state withdrawal capacity. Replenishes linearly over mainWindow toward a cap of maxDrawRateWad * TVL. |
| Elastic buffer | Temporary capacity from deposits. Decays proportionally over elasticWindow. Prevents deposit-then-withdraw flash loanflash 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 delay | Time 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 BreakerCircuit 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 UIUIThe 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 UIUIThe 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) callsexecuteQueued(queueId)to release the funds. executeQueuedis permissionless — anyone can call it once the delay has elapsed. The UIUIThe 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
-
Pre-check capacity before the user submits:
const [wouldBeImmediate, availableCapacity] = await cb.checkOutflow(
contract,
asset,
amount,
tvl,
); -
If queued, track the queue entry via events:
- Circuit BreakerCircuit 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)
- Circuit BreakerCircuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entry emits:
-
Wait for settlement:
const remaining = await cb.timeUntilSettled(queueId); // seconds until executable
const ready = await cb.isSettled(queueId); // true if executable now -
Execute (anyone can call):
await cb.executeQueued(queueId);
// or batch:
await cb.executeQueuedBatch([queueId1, queueId2, queueId3]);
Edge Case: Pre-Check Staleness
Note:
checkOutflowandwithdrawalCapacityread the buffer state at call time. Between the moment the UIUIThe 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 UIUIThe visual interface through which users interact with an app or product.View glossary entry should handle this gracefully:
- Always listen for both
OutflowImmediateandOutflowQueuedevents after a withdrawal transaction, regardless of what the pre-check indicated. - If a pre-check said "instant" but the transaction emits
OutflowQueued, transition the UIUIThe 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 UIUIThe 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 UIUIThe 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 barelasticBuffer— additional transient capacity from recent depositsutilizationBps— 0–10000, how much of the main buffer is consumed (0 = full capacity, 10000 = depleted)pendingOutflows— total value sitting in the queue for this assetisPaused— if true, all operations for this asset are halted
Queue Entry States
A queued withdrawal moves through these states. The UIUIThe 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 Status | status field | settlesAt vs now | What to Show |
|---|---|---|---|
| Pending (waiting) | Pending | settlesAt > now | Countdown timer: "Claimable in X hours Y min" |
| Pending (sped up) | Pending | settlesAt > now (but reduced) | Updated countdown, maybe "Expedited" badge |
| Claimable | Pending | settlesAt ≤ now | "Claim" button → calls executeQueued(queueId) |
| Paused | Paused | N/A | "On hold — contact support". No claim button |
| Executed | Entry deleted | N/A | "Completed" — entry no longer exists in storage |
| Invalidated | Pending | Any | executeQueued 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
contractaddress 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 BreakerCircuit 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.
checkOutflowdoes not account for whitelisting — it only checks buffer capacity. A pre-check may reportwouldBeImmediate = falsefor 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 UIUIThe 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 Need | Call | Returns |
|---|---|---|
| Will this withdrawal be instant or queued? | cb.checkOutflow(contract, asset, amount, tvl) | (bool wouldBeImmediate, uint256 availableCapacity) |
| Max amount user can withdraw instantly | cb.withdrawalCapacity(contract, asset, tvl) | uint256 |
| Full buffer health for a gauge | cb.getAssetHealth(contract, asset, tvl) | AssetHealth struct (see below) |
Note:
contractis the address of the protected contract (e.g., MintAndRedeem or ftYieldWrapperV2), not the Circuit BreakerCircuit 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.tvlis the current TVL of that contract for the given asset — see below for how to obtain it.
How to Obtain TVL
The Circuit BreakerCircuit 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 BreakerCircuit 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 Contract | Public TVL Getter | Units | Notes |
|---|---|---|---|
| MintAndRedeem | accountedCollateralTvl() | 6 decimals (ftUSDftUSDA 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 oracleoracleExternal 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 collateralcollateralAssets allowed as collateral and the maximum per‑asset size configured to manage concentration and risk.View glossary entry. |
| ftYieldWrapperV2 | valueOfCapital() | underlying token decimals | balanceOf(underlying) + sum of each strategystrategyA 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 strategystrategyA 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 BreakerCircuit 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 Need | Call | Returns |
|---|---|---|
| List user's pending queue entries | cb.getQueuedByRecipient(user, offset, limit) | QueuedOutflow[] (paginated) |
| Get all queue IDs for a user | cb.getQueueIdsByRecipient(user) | uint256[] |
| Get a single queue entry | cb.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)
| Action | Call | Notes |
|---|---|---|
| Claim a single entry | cb.executeQueued(queueId) | Permissionless — any address can call |
| Claim multiple entries | cb.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
Pausedstatus (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 Need | Call | Returns |
|---|---|---|
| Overall system status | cb.getSystemStatus() | (active, admin, guardian, delay, numProtected, numAssets, numQueued) |
| Is system paused? | cb.paused() | bool |
| Settlement delay | cb.settlementDelay() | uint256 (seconds) |
| Total queued items | cb.activeQueueCount() | uint256 |
| Pending outflow value for an asset | cb.pendingOutflows(asset) | uint256 |
Enumeration
| What You Need | Call | Returns |
|---|---|---|
| All registered protected contracts | cb.getProtectedContracts() | address[] |
| All assets with limiter state | cb.getTrackedAssets() | address[] |
| Whitelisted recipients (bypass rate limiting) | cb.getWhitelistedRecipients() | address[] |
| Per-contract isolation check | cb.isScopedLimiter(contract_) | bool |
Data Structures
QueuedOutflow
Returned by getQueued() and getQueuedByRecipient():
| Field | Type | Description |
|---|---|---|
token | address | The ERC-20 token being withdrawn |
queuedAt | uint40 | Timestamp when the entry was created |
settlesAt | uint40 | Timestamp when the entry becomes claimable |
status | QueueStatus | 0 = Pending, 1 = Paused |
recipient | address | Who receives the tokens on execution |
recoveryEpoch | uint64 | Epoch at queue time; if assetRecoveryEpoch differs, entry is invalid |
amount | uint256 | Amount of token to be transferred |
AssetHealth
Returned by getAssetHealth():
| Field | Type | Description |
|---|---|---|
mainBuffer | uint256 | Current main buffer capacity remaining |
mainBufferCap | uint256 | Maximum main buffer capacity (TVL * maxDrawRate) |
elasticBuffer | uint256 | Current elastic buffer capacity remaining |
totalCapacity | uint256 | mainBuffer + elasticBuffer |
pendingOutflows | uint256 | Total value queued but not yet executed for this asset |
utilizationBps | uint256 | 0–10000, percentage of main buffer consumed |
isPaused | bool | Whether this asset is paused |
SystemStatus
Returned by getSystemStatus():
| Field | Type | Description |
|---|---|---|
active | bool | true if not globally paused |
admin | address | Owner address (multisigmultisigA multi‑signature wallet with elevated permissions used for safety‑critical admin actions.View glossary entry/governance) |
guardianAddr | address | Guardian — can pausepauseTemporarily halting token transfers or protocol operations via admin roles for safety.View glossary entry but not unpause |
delay | uint256 | Current settlement delay in seconds |
numProtectedContracts | uint256 | Count of registered protected contracts |
numTrackedAssets | uint256 | Count of assets with limiter history |
numQueuedOutflows | uint256 | Current open queue entries |
Events Reference
Outflow Lifecycle
| Event | When Emitted | Key Fields |
|---|---|---|
Inflow(asset, amount, newTvl) | Deposit/mint recorded | newTvl = TVL after inflow |
OutflowImmediate(asset, to, amount) | Withdrawal within capacity | Instant transfer happened |
OutflowQueued(queueId, asset, recipient, amount, settlesAt) | Withdrawal exceeds capacity | settlesAt = earliest execution time |
OutflowLimiterContext(queueId, limiterAsset, transferToken) | Queued with limiter != transfer token | Links the limiter bucket to the actual token |
OutflowExecuted(queueId, recipient, asset, amount) | Queued withdrawal settled | Tokens transferred to recipient |
Queue State Changes
| Event | When 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
| Event | When Emitted | UIUIThe visual interface through which users interact with an app or product.View glossary entry Action |
|---|---|---|
Paused(by) | Global pausepauseTemporarily halting token transfers or protocol operations via admin roles for safety.View glossary entry activated | Show global banner |
Unpaused() | Global pausepauseTemporarily halting token transfers or protocol operations via admin roles for safety.View glossary entry lifted | Remove banner |
AssetPaused(asset, by) | Individual asset paused | Show per-asset banner |
AssetUnpaused(asset) | Individual asset unpaused | Remove per-asset banner |
AssetTracked(asset) | First time an asset interacts with Circuit BreakerCircuit BreakerA rate‑limiting safety mechanism that throttles withdrawals or outflows to reduce exploit blast radius.View glossary entry | Use for indexing new assets without polling |
Configuration Changes
| Event | When 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
| Parameter | Default | Description |
|---|---|---|
maxDrawRateWad | 5% (0.05e18) | Max outflow as % of TVL before queueing |
mainWindow | 24 hours | Main buffer fully replenishes over this window |
elasticWindow | 30 minutes | Elastic buffer fully decays over this window |
settlementDelay | 6 hours | Time before queued entries become claimable |
minTVLForMint | 500,000 ftUSDftUSDA 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 | Floor TVL to prevent rate-limit gaming at low supply |
Example UI Flows
Flow 1: User Withdraws (Instant)
- User enters amount, clicks "Withdraw"
- Pre-check: Call
cb.checkOutflow(contract, asset, amount, tvl)→wouldBeImmediate = true - Show: "This withdrawal will be processed instantly"
- User submits tx → tokens arrive in wallet
- Listen for
OutflowImmediateevent → show success
Flow 2: User Withdraws (Queued)
- User enters amount, clicks "Withdraw"
- Pre-check: Call
cb.checkOutflow(contract, asset, amount, tvl)→wouldBeImmediate = false, availableCapacity = X - Show: "This withdrawal exceeds the instant limit (X available). It will be queued and claimable in ~6 hours."
- User confirms → tx queuesqueuesWaiting periods required to withdraw from certain staking positions (e.g., stETH withdrawals, SOL/AVAX unbonding).View glossary entry the withdrawal
- Listen for
OutflowQueuedevent → show pending state with countdown tosettlesAt - Periodically call
cb.timeUntilSettled(queueId)to update countdown - When
timeUntilSettledreturns0→ show "Claim" button - User clicks "Claim" → calls
cb.executeQueued(queueId)→ tokens arrive - Listen for
OutflowExecutedevent → show "Completed"
Flow 3: Queued Entry Gets Paused
- Entry is in "Pending" state with countdown
- Listen for
OutflowPaused(queueId)event - Switch to "On hold — under review" state, hide countdown
- If
OutflowResumed(queueId)fires → return to countdown state
Flow 4: Queued Entry Gets Sped Up
- Entry is in "Pending" state with countdown
- Listen for
OutflowSpedUp(queueId, recipient, newSettlesAt)event - Update countdown to new
settlesAt, optionally show "Expedited" badge
Keeper / Bot Integration
Executing Queued Outflows
A keeper bot should:
- Index
OutflowQueuedevents to track pending entries. - Poll
isSettled(queueId)or computesettlesAtfrom the event. - Call
executeQueued(queueId)orexecuteQueuedBatch(ids)when ready. - Handle reverts:
QueueNotSettled— too early, retry later.QueueInvalidated— emergency recovery happened, skip this entry.GloballyPaused/AssetPausedError— system paused, wait forUnpaused/AssetUnpaused.InvalidStatus— entry was paused by admin, wait forOutflowResumed.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:
| Alert | Trigger | Severity |
|---|---|---|
| Rate limit pressure | OutflowQueued frequency increases | Medium |
| Emergency | EmergencyRecovery or Paused emitted | Critical |
| Config change | ConfigUpdated or DelayUpdated emitted | Info |
| Large override | EmergencyOverride with significant amount | High |
| Asset paused | AssetPaused emitted | High |
| Guardian change | GuardianUpdated emitted | Info |
| Scoping change | ScopedLimiterUpdated emitted | Medium |
Admin Operations
Roles
| Role | Set By | Permissions |
|---|---|---|
| Owner | 2-step transfer (Ownable2Step) | Full admin: config, pausepauseTemporarily halting token transfers or protocol operations via admin roles for safety.View glossary entry/unpause, resume queued, emergency recover, set all roles |
| Guardian | Owner | PausePauseTemporarily halting token transfers or protocol operations via admin roles for safety.View glossary entry globally, pausepauseTemporarily halting token transfers or protocol operations via admin roles for safety.View glossary entry per-asset, pausepauseTemporarily halting token transfers or protocol operations via admin roles for safety.View glossary entry individual queue entries |
| Operator | Owner | speedUp() 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 multisigmultisigA 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. OnlyexecuteQueuedchecks the epoch and reverts. activeQueueCountis not decremented for invalidated entries.- The Circuit BreakerCircuit 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 / multisigmultisigA multi‑signature wallet with elevated permissions used for safety‑critical admin actions.View glossary entry access — Multiple authorized relayers can call
speedUpthrough the operator contract without each needing the operator role on the Circuit BreakerCircuit 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
speedUptheir entry. The operator contract escrows the bounty, the relayer callsspeedUpthrough 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 BreakerCircuit 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 pausepauseTemporarily 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:
| Approach | Target Delay | How It Would Work |
|---|---|---|
| Current (no healthcheck) | 6 hours | Team monitors and pauses manually |
| On-chain invariant healthcheck | ~30 min–1 hour | executeQueued auto-reverts on invalid state |
| Off-chain threat detection keeper | ~15–30 min | Keeper monitors for exploits, sets an unhealthy flag to block all claims |
| Keeper-enabled guardian contract | ~15–30 min | Automated keeperskeepersExternal 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 |
| Combined | Lowest possible delay | Defense in depth: all of the above |
Audit Delta (CS-FTMPR)
Changes from the ChainSecurity audit that affect UIUIThe 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, UIUIThe 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 strategystrategyA 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).