Technical Audit Documentation
Kairos Protocol - Physically Settled Options Vaults on Base
Live Alpha: https://kairos.markets
Introduction
Kairos is a subdao of Thetanuts that provides physically settled vaults at fixed prices that automatically transition between calls and puts. The protocol enables users to earn yield on their assets while waiting for target prices, transforming traditional limit orders into yield-generating positions.
Core Value Proposition
- Yield-Generating Limit Orders: Users deposit assets at a target price and earn continuous premium yield
- Automatic Position Flipping: When price crosses strike, position automatically flips from ETH to USDC (or vice versa)
- 100% Collateralization: No liquidation risk, no margin calls
- Physical Settlement: Actual asset delivery rather than cash settlement
Design Philosophy
Architecture Overview
Contract Hierarchy
BaseVault (abstract)
└── PhysicallySettledVault (dual-asset management, recovery mode)
└── FixedStrikeVault (fixed strike strategy)
BaseOption (abstract)
└── PhysicallySettledOption (abstract, exercise window logic)
├── PhysicallySettledCallOption
└── PhysicallySettledPutOption
OptionFactory (RFQ system, option creation)
Key Design Decisions
| Decision | Tradeoff | Rationale |
|---|---|---|
| Non-upgradeable | Can't fix bugs in deployed contracts | Eliminates admin key risks, upgrade attacks |
| 100% Collateralization | Capital inefficient vs lending | No liquidation cascades, no oracle manipulation risks |
| Physical Settlement | Requires exercise action from buyer | Direct asset delivery, simpler security model |
| Fixed Strike | Less flexibility than dynamic strikes | Predictable execution, enhanced liquidity |
Audit Scope
In-Scope Contracts with Test Coverage
| Contract | Lines | Branches | Description |
|---|---|---|---|
OptionFactory.sol |
98.35% | 97.55% | RFQ system, option creation |
BaseOption.sol |
98.80% | 95.12% | Abstract base for all options |
PhysicallySettledOption.sol |
94.79% | 95.00% | Physical settlement logic |
PhysicallySettledCallOption.sol |
97.14% | 100.00% | Call option implementation |
PhysicallySettledPutOption.sol |
97.14% | 100.00% | Put option implementation |
BaseVault.sol |
97.10% | 94.44% | Abstract vault base |
PhysicallySettledVault.sol |
98.37% | 94.87% | Physical settlement vault base |
FixedStrikeVault.sol |
92.00% | 100.00% | Fixed strike strategy |
Coverage measured via forge coverage. All in-scope contracts have >90% line coverage.
OptionFactory
Overview
The OptionFactory implements a Request for Quotation (RFQ) system using a commit-reveal scheme to prevent front-running and MEV attacks.
RFQ Lifecycle
Key Functions
| Function | Access | Description |
|---|---|---|
requestForQuotation() |
Public | Create new RFQ with option parameters |
makeOfferForQuotation() |
Public | Submit signed offer commitment |
revealOffer() |
Public | Reveal offer during reveal window |
settleQuotationEarly() |
Requester | Accept offer before reveal ends |
settleQuotation() |
Public | Finalize RFQ after reveal window |
cancelQuotation() |
Requester | Cancel active RFQ |
Fee Structure
- Base Rate: 0.06% per contract (e.g., 0.0006 ETH for 1 ETH contract)
- Cap: 12.5% of premium maximum
- Charged To: Premium receiver (not split between parties)
- Referral Split: 50/50 between owner and referrer (if referral used)
Security Properties
makeOfferForQuotation() is intentionally unguarded as it performs only one storage write and no external calls. Note that isActive = false is set BEFORE external calls during settlement to support integration callbacks.
BaseOption
Overview
Abstract base contract providing common functionality for all option types including transfers, splits, and the event notification system.
State Variables
address public buyer; // Option holder (right to exercise)
address public seller; // Collateral provider (obligation)
address public collateralToken; // ERC20 used as collateral
uint256[] public strikes; // Strike price(s)
uint256 public expiryTimestamp; // When option expires
uint256 public numContracts; // Number of contracts
uint256 public collateralAmount;// Total collateral locked
uint256 public splitGeneration; // For exponential fee calculation
bytes32 public paramsHash; // Hash of immutable parameters
Split Mechanism
Options can be split into two child options, preserving the total contract count.
baseFee * 2^generation). This prevents DoS attacks where an attacker splits positions into millions of dust amounts, making settlement prohibitively expensive.
Transfer System
Role transfers (buyer/seller) require explicit approval or caller authorization:
function transfer(bool isBuyer, address target) external {
// Authorized if:
// 1. msg.sender is current role holder
// 2. msg.sender has allowance from current role holder
// 3. msg.sender is the factory
// Allowance auto-revokes after use
}
Event Notification System
Options notify buyer and seller contracts about lifecycle events:
| Event Type | Code | Data |
|---|---|---|
| Creation | 0 | empty |
| Settlement | 1 | empty |
| Transfer | 2 | (isIncoming, otherParty) |
| Close | 3 | (collateralReturned) |
| Split | 4 | (originalAmount, splitAmount, newOption) |
| Reclaim | 5 | (newOption, reclaimer) |
PhysicallySettledOption
Overview
Abstract contract for options involving actual asset delivery. Manages exercise windows, oracle failure handling, and physical settlement flows.
Key Constants
uint256 public constant EXERCISE_WINDOW = 1 hours;
// Minimum time after expiry before anyone can force-settle ITM options
Settlement Flow
Exercise Functions
| Function | Access | Description |
|---|---|---|
exercise() |
Public | Exercise ITM option (requires buyer approval for delivery tokens) |
swapAndExercise() |
Buyer only | Swap collateral to delivery asset and exercise in one tx |
doNotExercise() |
Buyer only | Explicitly forfeit exercise rights |
withdraw() |
Public | Force-settle after exercise window (attempts exercise first) |
exerciseOnOracleFailure() |
Buyer only | Exercise when oracle is unhealthy. No ITM/OTM price check occurs - buyer can exercise regardless of whether option would have been ITM. Sellers are forced to settle at strike price without oracle validation. Last-resort mechanism granting buyer full discretion during oracle failures. |
Oracle Failure Handling
exercise() and withdraw() paths are blocked as they depend on TWAP. The option remains frozen until recovery.
Recovery Sequence (Buyer-Controlled)
- Oracle recovers - Normal paths resume automatically
exerciseOnOracleFailure()- Buyer exercises at strike price without TWAP verificationdoNotExercise()- Buyer forfeits rights, collateral returns to sellerrescueERC20()- Admin fallback available 24 hours after expiry
function oracleUnhealthy() public view returns (bool unhealthy, uint8 reason) {
// reason: 0=healthy, 1=TWAP failed, 2=Price feed stale, 3=Invalid data
// Staleness threshold: max(2 * twapPeriod, 1 day)
}
Security Properties
Permissionless Exercise Design
exercise() is permissionless by design. Security relies on token approvals, not access control. Anyone can call exercise for ITM options, but only the buyer's tokens can be spent (requires buyer's approval to the option contract).
Withdraw Attempts Exercise First
When withdraw() is called on an ITM option after the 1-hour window, it first attempts to call exercise(). If the buyer has pre-approved the delivery tokens, exercise will succeed. This ensures buyers who set approvals in advance don't lose their ITM position even if they miss the exercise window.
// Simplified withdraw logic for ITM options:
function withdraw() external {
// After 1-hour window for ITM...
try this.exercise() {
return; // Success - buyer got their payout
} catch {
// Buyer didn't have approvals - treat as forfeit
// Return collateral to seller
}
}
PhysicallySettledCallOption & PhysicallySettledPutOption
Call Option (Base Collateral)
Option Type: 0x0010
- Collateral: Base asset (e.g., WETH)
- Delivery: Quote asset (e.g., USDC)
- ITM Condition:
price > strike - Delivery Amount:
numContracts * strike * deliveryDecimals / PRICE_DECIMALS / collateralDecimals - Collateral Required:
numContracts(1:1 with base asset)
Example: 1 ETH call at $3000 strike. If ETH > $3000, buyer pays $3000 USDC, receives 1 ETH.
Put Option (Quote Collateral)
Option Type: 0x0111
- Collateral: Quote asset (e.g., USDC)
- Delivery: Base asset (e.g., WETH)
- ITM Condition:
price < strike - Delivery Amount:
numContracts * deliveryDecimals / collateralDecimals - Collateral Required:
strike * numContracts / PRICE_DECIMALS
Example: 1 ETH put at $3000 strike. If ETH < $3000, buyer pays 1 ETH, receives $3000 USDC.
Option Type Encoding
// Option type bit layout: 0xABCD
// A (bits 12-15): Structure (0=Single, 1=Spread, 2=Butterfly, 3=Condor)
// B (bits 8-11): Collateral (0=Base, 1=Quote)
// C (bits 4-7): Settlement (0=Cash, 1=Physical)
// D (bits 0-3): Style (0=Call, 1=Put, 2=Iron Condor)
BaseVault
Overview
Abstract ERC20 vault managing deposits, withdrawals, option creation via RFQ, and share accounting.
Core State
// Immutables
OptionFactory public immutable factory;
bool public immutable isLongOptionStrategy;
uint256 public immutable activeOptionsStrategies;
// RFQ State
uint256 public activeRfqId = type(uint256).max; // No active RFQ
uint256 public lastRFQTimestamp;
uint256 public activeExpiryTimestamp;
// Deposit Queue
PendingDeposit[] public pendingDeposits;
uint256 public lastProcessedDepositIndex;
uint256[] public lastRecordedValuePerShare;
Deposit Flow
Withdrawal Flow
Option Creation Flow
Share Minting for Pending Deposits
When an RFQ settles, pending deposits are minted shares based on:
// For SHORT strategies (selling options):
effectiveDeposit = deposit + premiumBonus - intrinsicDeduction - itmPenalty
// Where:
// - premiumBonus = deposit * premium / collateralAmount
// - intrinsicDeduction = deposit * intrinsicValue / collateralAmount
// - itmPenalty = 1% if option is ITM (prevents ITM deposit manipulation)
// For LONG strategies (buying options):
effectiveDeposit = deposit - (deposit * totalLongOptionValuation / totalAssetsUSD)
// Where:
// - priceOfOption = premium / collateralAmount (per-contract option price)
// - totalLongOptionValuation = priceOfOption * totalOptionStrategyCollateral
// Long deposits are haircut by the proportion of vault value locked in options
Reserve Ask Calculation
function calculateReserveAsk(...) public view returns (uint256) {
uint256 timeToExpiry = getNextExpiryTimestamp() - block.timestamp;
uint256 aprBasedReserve = collateralAmount * MIN_APR_BPS * timeToExpiry / (365 days * 10000);
if (isLongOptionStrategy) {
// Buying: APR-based maximum (prevent overpaying)
return aprBasedReserve;
} else {
// Selling: max(APR-based, intrinsicValue) - prevent selling ITM at loss
uint256 intrinsicValue = calculatePayout(currentPrice);
return max(aprBasedReserve, intrinsicValue);
}
}
PhysicallySettledVault
Overview
Extends BaseVault for dual-asset management (base + quote) and physical settlement. Includes comprehensive recovery mode for fault tolerance.
Dual Asset Management
The vault manages two assets:
- Asset Index 0: Base asset (e.g., WETH) - used for Call options
- Asset Index 1: Quote asset (e.g., USDC) - used for Put options
Value Calculation
function calculateValueInUSD(uint256 amount, uint256 assetIndex) {
if (assetIndex == 1) {
// Quote asset: adjust decimals only (assume ~$1)
return amount * PRICE_DECIMALS / 10^quoteDecimals;
} else {
// Base asset: multiply by oracle price
return amount * getCurrentPrice(0) / 10^baseDecimals;
}
}
Invariant Check
MAX_VALUE_DROP_BPS in both base AND quote terms simultaneously. A drop in one denomination alone is allowed (e.g., ETH price change affects base value but not quote value).
function assertInvariant() internal view {
if (totalSupply() == 0) return;
if (isInRecoveryMode && block.timestamp >= recoveryInitiatedAt + RECOVERY_DELAY) return;
uint256[] memory currentVPS = _calculateValuePerShare();
bool baseDecreased = currentVPS[0] * 10000 < lastRecordedVPS[0] * (10000 - MAX_VALUE_DROP_BPS);
bool quoteDecreased = currentVPS[1] * 10000 < lastRecordedVPS[1] * (10000 - MAX_VALUE_DROP_BPS);
if (baseDecreased && quoteDecreased) revert InvariantViolation();
}
Recovery Mode
Recovery mode can be triggered by anyone when:
- Invariant Violation: Value drops in both base AND quote terms beyond
MAX_VALUE_DROP_BPSthreshold - Oracle Failure: Invalid data (
roundId=0,price<=0,updatedAt=0) or stale data exceeding86400 seconds (24 hours)since last update
Recovery Mode Effects
| Operation | Normal Mode | Recovery Mode |
|---|---|---|
| deposit() | Allowed | Blocked |
| withdraw() | Allowed | Allowed (after 7-day delay) |
| createOption() | Allowed | Blocked |
| rescueERC20() | Non-vault tokens only | Any token (including assets) |
Stuck RFQ Recovery
If an RFQ settles but the settlement callback fails (e.g., due to invariant violation), the owner must manually recover:
function recoverSettledRFQOption() external onlyOwner {
require(isInRecoveryMode);
require(activeRfqId != type(uint256).max);
require(pendingDeposits.length > 0);
// Verify RFQ is truly stuck
try this.callHandleActiveRfq() {
revert RFQNotStuck();
} catch {
// Transfer option to owner for manual distribution
BaseOption(optionContract).transfer(isLongOptionStrategy, owner());
}
}
FixedStrikeVault
Overview
Specialized vault creating options at a fixed strike price. The vault sells covered calls when holding ETH and cash-secured puts when holding USDC.
Key Properties
uint256 public immutable FIXED_STRIKE;
// Always a SHORT strategy (selling covered calls or secured puts)
isLongOptionStrategy = false;
// Both assets must share the SAME underlying price feed (e.g., ETH/USD)
require(assets[0].priceFeed == assets[1].priceFeed);
Automatic Asset Selection (Deposits)
The convenience deposit(uint256 amount) function selects asset based on price vs strike:
function deposit(uint256 amount) external {
uint256 currentPrice = getCurrentPrice(0);
uint256 assetIndex;
if (currentPrice < FIXED_STRIKE) {
// Below strike: deposit base asset (ETH) for covered calls
assetIndex = 0;
} else {
// Above strike: deposit quote asset (USDC) for secured puts
assetIndex = 1;
}
super.deposit(amount, assetIndex);
}
RFQ Asset Selection (Option Creation)
PhysicallySettledVault.calculateRfqAmount().
// PhysicallySettledVault.calculateRfqAmount()
function calculateRfqAmount() public view returns (uint256 collateralAmount, uint256 assetIndex) {
(uint256[] memory assetValues,,,) = getTotalAssets();
assetIndex = assetValues[0] > assetValues[1] ? 0 : 1; // Larger holding wins
collateralAmount = assets[assetIndex].token.balanceOf(address(this));
}
The position flip happens indirectly: settlement exchanges assets, changing which asset dominates holdings.
Contract Calculation
function calculateNumContracts(uint256 amount, uint256 assetIndex) {
if (assetIndex == 0) {
// Calls: 1:1 with base asset
return amount;
} else {
// Puts: amount * PRICE_DECIMALS / FIXED_STRIKE
return amount * PRICE_DECIMALS / FIXED_STRIKE;
}
}
Example Workflow
Fixed Strike at $3000 ETH
- ETH at $2800: User deposits ETH. Vault sells covered calls at $3000 strike.
- ETH rises to $3200: Calls exercised. User now holds USDC (sold ETH at $3000).
- Next cycle: Vault sells cash-secured puts at $3000 strike.
- ETH drops to $2900: Puts exercised. User now holds ETH (bought at $3000).
- Cycle continues...
Throughout this process, the user earns premium yield from selling options.
Key Invariants
1. Collateral Conservation
IERC20(collateralToken).balanceOf(option) >= option.collateralAmount()
Enforced by ERC20 transfers. Options always hold at least their stated collateral.
2. Contract Conservation on Split
parentContracts + childContracts == originalContracts
Total contracts preserved when splitting. No value extraction through splits.
3. Vault Asset Accounting
totalAssets = liquidAssets + pendingDeposits + optionCollateral
All vault value is accounted for in liquid tokens, queued deposits, or option positions.
4. Value Per Share (PhysicallySettledVault)
!(baseDropped > threshold AND quoteDropped > threshold)
Value per share cannot decrease beyond threshold in both denominations simultaneously.
5. RFQ Deposit Coverage
requesterDeposit + winnerDeposit >= premium + collateral
During active RFQ, deposits cover all required amounts for settlement.
Protection Mechanisms
Split Attack Prevention
Attack Vector
Attacker splits victim's position into millions of dust positions, making settlement prohibitively expensive in gas.
Protection
- Exponential Fees: Split fee =
baseFee * 2^generation - Economic Deterrent: After ~20 generations, fees become astronomical
- Pure Economic Defense: No minimum split value - the exponential fee alone makes dust attacks economically infeasible
Vault Share Rounding Attacks
Attack Vector
First depositor manipulates share price by depositing tiny amount then donating to vault.
Protection
- MIN_VALUE Scaling: First deposit must meet full
MIN_VALUEthreshold; subsequent deposits require onlyMIN_VALUE / MAX_QUEUE(scaled to prevent queue spam while allowing smaller follow-on deposits) - Initial Shares: First deposit always mints exactly
1e18shares - Value Per Share Tracking: Sudden drops trigger invariant violations
ITM Deposit Manipulation
Attack Vector
Depositor times deposit right before vault sells an ITM option, diluting losses across existing shareholders.
Protection
- Intrinsic Deduction: New deposits lose proportional intrinsic value
- ITM Penalty: Additional 1% penalty for ITM-causing deposits
- Reserve Price Floor: Reserve ask is max(APR-based, intrinsicValue)
Oracle Manipulation
Attack Vector
Attacker manipulates oracle to exercise favorable options or cause incorrect settlements.
Protection
- TWAP: 30-minute TWAP for settlement (matches Deribit)
- Staleness Checks: Oracle data must be fresh
- Oracle Failure Mode: Only buyer can resolve during failures
- Recovery Mode: Vault enters recovery on oracle failure
MEV/Frontrunning
Attack Vector
MEV bots frontrun offers or settle transactions for profit.
Protection
- Commit-Reveal: Offers are committed (signature only) then revealed
- Encrypted Offers: Offer details encrypted for requester using ECDH
- Early Settlement: Requester can accept before reveal window
Reentrancy
Protected Functions (with nonReentrant)
BaseVault:withdraw(),withdrawPendingDeposits(),createOption(),executeExternalCall()OptionFactory:requestForQuotation(),settleQuotation(),cancelQuotation(),revealOffer()BaseOption:transfer(),split(),reclaimCollateral()
Intentionally Unguarded Functions
BaseVault.deposit()- No external calls after state changes, uses CEI patternPhysicallySettledOption.exercise()- Terminal state (optionSettled = true) set before transfers prevents re-entry benefitOptionFactory.makeOfferForQuotation()- Single storage write, no external calls
Note: Lists above show key functions, not exhaustive. Verify nonReentrant usage in any modified code.
Attack Vectors to Review
High Priority
- Exercise Window Race: Can withdraw() and exercise() create race conditions where both succeed/fail unexpectedly?
- Split Fee Bypass: Can an attacker avoid exponential split fees through creative contract interactions?
- Recovery Mode Entry: Can an attacker force recovery mode via transient oracle issues to permanently DoS deposits?
- RFQ Settlement Order: Can ordering of reveals be manipulated to extract value?
- Pending Deposit Manipulation: Can pending deposits be weaponized during RFQ settlement?
- First Depositor Donation Attack: After first deposit, can direct token transfer inflate share price to dilute later depositors? (MIN_VALUE provides partial mitigation)
Medium Priority
- Price Oracle Dependency: What happens with extreme price movements during settlement?
- Callback Griefing: Can malicious option holders grief vaults via callbacks?
- Allowance Persistence: Are approvals properly cleaned up after transfers?
- Dust Accumulation: Can dust amounts accumulate in ways that affect accounting?
- Split Generation Overflow: After many splits, does
2^generationoverflow or create DoS? - Share Minting Arithmetic: Edge cases when
totalLongOptionValuation > totalAssetsUSDor division by zero scenarios
External Dependencies
- Chainlink Oracle: TWAP calculation depends on oracle availability and accuracy
- Swap Aggregators:
swapAndExerciserelies on authorized routers (Kyber, 1inch, 0x, Odos, Paraswap) - ERC20 Tokens: Fee-on-transfer and rebasing tokens explicitly NOT supported
Additional Security Considerations
Known Limitations & Unsupported Scenarios
| Scenario | Impact | Mitigation |
|---|---|---|
| Fee-on-transfer tokens | CRITICAL - Assets unrecoverable, accounting broken | Not supported. Do not use. |
| Negatively rebasing tokens | HIGH - Accounting mismatch, potential insolvency | Not supported. Do not use. |
| Positively rebasing tokens | MEDIUM - Works but yield accrues to seller | Acceptable if understood. Buyer loses rebase yield. |
| Malicious option implementations | CRITICAL - Factory is agnostic, accepts any impl | Users must verify implementation address or use trusted frontend |
| Oracle manipulation | MEDIUM - Incorrect ITM/OTM determination | 30-min TWAP, staleness checks, recovery mode |
| Non-8-decimal price feeds | CRITICAL - Reverts with InvalidPriceFeedDecimals() |
All Chainlink price feeds must be 8 decimals (USD convention). Note: only USD prices use 8 decimals; numContracts and collateralAmount use collateral token decimals (e.g., 18 for ETH, 6 for USDC). |
| First depositor attack | MEDIUM - Share price manipulation via donation | MIN_VALUE threshold provides partial mitigation |
| Split DoS at high generation | LOW - Exponential fees make further splits expensive | Intentional economic throttle, not a bug |
| Recovery mode permanent | LOW - Vault never accepts deposits again | Intentional. Deploy new vault if needed. |
Emergency Procedures
| Scenario | Recovery Path |
|---|---|
| Oracle failure | Recovery mode + buyer exercises via exerciseOnOracleFailure() |
| Stuck RFQ | Owner calls recoverSettledRFQOption() |
| Invariant violation | Recovery mode activated, 7-day delay for withdrawals |
| Token rescue (option) | rescueERC20() after 24 hours post-expiry |
| Token rescue (vault) | Recovery mode required for vault assets |
| Oracle failure blocking vault | _handleExpiredOptions() calls option.withdraw() which requires oracle data via getTWAP(). If oracle is stale/invalid, vault operations (withdrawals, option creation) are blocked until buyer calls exerciseOnOracleFailure() or doNotExercise() on each blocked option. |
Access Control Summary
| Role | Capabilities |
|---|---|
| Factory Owner | Withdraw fees, set MAX_RFQ_VALUE, set baseSplitFee, authorize routers |
| Vault Owner | Execute external calls via executeExternalCall() to any contract except vault itself, factory, vault assets, and active options (centralization risk: owner can interact with arbitrary external contracts); rescue tokens (recovery mode required for vault assets); set pause guardian; recover stuck RFQ |
| Pause Guardian | Pause/unpause vault (blocks deposits/createOption, not withdrawals) |
| Option Buyer | Exercise, doNotExercise, exerciseOnOracleFailure, swapAndExercise |
| Anyone | withdraw() after exercise window, initiateRecoveryMode(), createOption() |
Testing Recommendations
- Fuzz split operations with varying collateral amounts near boundaries
- Test exercise window race conditions with concurrent transactions
- Simulate oracle failures at various points in settlement lifecycle
- Test recovery mode transitions and stuck RFQ scenarios
- Verify share minting math with extreme premium/intrinsic values
- Test MAX_RFQ_VALUE capping with large deposits
- Test first depositor + donation attack scenarios
- Test split generation limits (what happens at generation 20+?)
Intentional Design Patterns (Not Bugs)
The following patterns may appear problematic but are intentional design decisions:
1. Post-Expiry RFQ Settlement Allowed
Appears as: _settleQuotation allows RFQs to settle after option expiry
Why correct: Both parties escrowed funds before expiry; late settlement finalizes the deal (OTM sellers recover collateral, ITM buyers claim payout). New RFQs for expired options ARE blocked at creation.
Location: OptionFactory.sol:1293-1305
2. Reserve Price Can Be Exceeded via Top-Up
Appears as: _handleRequesterTopUp overwrites reserve when requester accepts higher offer
Why correct: Reserve is only the passive auto-settlement cap. Explicit settlement (settleEarly/swapAndCall) requires token transfer for the delta, which IS the consent signal.
Location: OptionFactory.sol:935-1004
3. isActive Set False Before External Calls
Appears as: CEI violation in _settleQuotation
Why correct: Intentional for integration callbacks (e.g., loan handlers check RFQ state). Settlement is terminal with no re-entry path; funds already escrowed.
Location: OptionFactory.sol:1311-1334
4. Anyone Can Cancel Some RFQs
Appears as: cancelQuotation allows external callers
Why correct: Only when (a) reveal window ended, (b) no winner, (c) not limit-order mode. Cleanup path for stuck zero-offer RFQs; funds return to requester.
Location: OptionFactory.sol:1388-1415
5. Permissionless withdraw() Can Force Non-Exercise
Appears as: Third parties can forfeit buyer's ITM position
Why correct: Only after 1-hour EXERCISE_WINDOW. First attempts exercise using buyer's approvals; only forfeits if buyer can't pay. Seller protection after reasonable exercise period.
Location: PhysicallySettledOption.sol:266-309
6. Recovery Mode Preserves Stuck activeRfqId
Appears as: Failed cancelQuotation leaves RFQ stuck forever
Why correct: Intentional loss socialization. Keeps pending deposits bound to stuck option so they can't exit without loss; owner uses recoverSettledRFQOption for manual distribution.
Location: PhysicallySettledVault.sol:516-559
7. Exponential Split Fees (2^generation)
Appears as: Bug or DoS vector on user funds
Why correct: Anti-griefing control against deep split trees. Paid in ETH, forwarded to counterparty. Economic throttle, not a protocol fee on option value.
Location: BaseOption.sol:474-547
8. Withdrawal Rounding Can Drop Option Portions
Appears as: Small withdrawals lose option value silently
Why correct: Explicit zero-check at lines 963, 972, 977 returns early rather than reverting. Alternative would lock user funds. Dust loss accepted over withdrawal DoS.
Location: BaseVault.sol:960-978
9. swapAndExercise Uses Seller's Collateral
Appears as: Buyer exercises "for free" using seller's posted collateral
Why correct: This is standard physically-settled option mechanics. The buyer PAID PREMIUM upfront when entering the position. At exercise, buyer is ENTITLED to the collateral. swapAndExercise is a convenience function that atomically: (1) swaps collateral to deliveryCollateral, (2) pays seller the strike from swap proceeds, (3) gives buyer the remainder. Economically identical to normal exercise() where buyer pays strike separately.
Comparison:
exercise(): Buyer pays $50k USDC → receives 1 ETH ($60k) → net +$10kswapAndExercise(): Contract swaps 1 ETH → $60k → seller gets $50k → buyer gets $10k
Location: PhysicallySettledOption.sol:204-232
10. Vault Picks Lowest Split Generation
Appears as: Vault will DoS after ~17 withdrawals due to exponential split fees
Why correct: Vault withdrawal logic explicitly sorts options by splitGeneration and ALWAYS picks the option with the LOWEST generation (cheapest fee) to split. This prevents fee accumulation across withdrawals.
Code evidence:
// BaseVault.sol:955-956
// Split the lowest generation option (cheapest fee)
BaseOption optionToSplit = BaseOption(options[lowestGenerationIndex].optionContract);
Location: BaseVault.sol:930-956
11. Recovery Mode is Permanent (By Design)
Appears as: Transient oracle failure permanently kills vault
Why correct: Conservative safety design. Once triggered, vault blocks new deposits/RFQs but allows withdrawals. Rationale: better to "kill" a potentially compromised vault than risk repeated attacks or inconsistent state. Users can withdraw; protocol deploys new vault if needed.
No exit mechanism: isInRecoveryMode = true has no corresponding false setter. This is intentional.
Location: PhysicallySettledVault.sol:543
12. TWAP Phase Boundary Reverts
Appears as: Option settlement blocked if Chainlink transitions phases during TWAP window
Current behavior: If TWAP calculation crosses a Chainlink phase boundary (roundID % 2^64 == 0), the calculation reverts. This is rare but can temporarily block settlement.
Recovery paths:
- Physical options: Use
exerciseOnOracleFailure(),doNotExercise(), or wait forrescueERC20() - Cash options: Must wait for oracle to recover or
rescueERC20()after 24h
Future improvement: Potential fix in HistoricalPriceConsumerV3_TWAP.sol - stop at current phase boundary instead of reverting, using partial TWAP.
Location: oracles/HistoricalPriceConsumerV3_TWAP.sol
Critical Code Paths for Auditors
Line references for intensive review:
| File | Lines | Focus Area |
|---|---|---|
OptionFactory.sol |
1307-1386 | _settleQuotation - fund flows, existing option buyback logic |
OptionFactory.sol |
779-793 | _handleBestOfferAcceptance - collateral/premium transfers |
BaseVault.sol |
201-230 | deposit - first depositor handling, MIN_VALUE checks |
BaseVault.sol |
802-899 | _mintSharesForPendingDeposits - share calculation, ITM penalty |
BaseOption.sol |
415-547 | split - exponential fees, conservation invariants |
PhysicallySettledOption.sol |
156-309 | exercise/withdraw - settlement flows, oracle gating |
PhysicallySettledVault.sol |
516-559 | initiateRecoveryMode - trigger conditions, RFQ cancellation |
PhysicallySettledVault.sol |
676-700 | recoverSettledRFQOption - stuck RFQ handling |