Appearance
L3-3: Options #
1. Problem #
Options are a trillion-dollar derivatives market in traditional finance, but on-chain implementation faces unique challenges. The core question is: how do you ensure the option seller will always honor the contract without a central clearinghouse? In TradFi, the Options Clearing Corporation (OCC) acts as a central counterparty guaranteeing all trades. In DeFi, smart contracts must eliminate counterparty risk through escrow mechanisms — the seller locks up full collateral upfront, and the buyer pays the premium upfront.
But this leads to a second problem: how to fairly calculate exercise payouts? There is no "market closing price" on-chain; you need to rely on oracles for settlement prices. Additionally, European options can only be exercised at expiration (as opposed to American options, which can be exercised anytime), requiring the contract to precisely manage time windows and state transitions.
2. Rationale #
DeFi options protocols (such as Opyn, Ribbon, Hegic) are bringing the trillion-dollar traditional market on-chain. The core advantage of on-chain options is the elimination of counterparty risk — sellers cannot default, and buyers cannot refuse to pay. Understanding the options lifecycle of write → buy → exercise is the foundation for entering DeFi derivatives development.
For developers, an options contract is an excellent exercise: it involves state machine design (Active → Purchased → Exercised), payout settlement logic, collateral management, and time locks. These patterns recur in more complex structured products and perpetual contracts.
3. Solution #
EuropeanOption.sol implements a complete European options system supporting both CALL and PUT types.
State Machine Design: the Option struct maintains four states: ACTIVE (seller has locked collateral, awaiting purchase), PURCHASED (buyer has paid premium, awaiting expiration and exercise), EXERCISED (exercised and settled), and EXPIRED (reserved but unused).
Creating an Option (Write): createOption() is called by the seller, specifying the option type, strike price, amount, premium, and expiration time. The seller must immediately transfer the underlying asset (amount) into the contract as collateral. The collateral is used to pay the buyer's payout upon exercise.
Buying an Option (Buy): the buyer calls purchaseOption(), paying the premium to the seller. After purchase, the option status changes from ACTIVE to PURCHASED, and the buyer's address is recorded as holder. The purchase must be completed before expiration.
Exercise: can only be called by the buyer after expiration. exercise() receives a settlement price (_settlementPrice; in practice from an oracle) and computes the payout:
- Call option (CALL): ITM condition =
settlementPrice > strikePrice, payout =(settlementPrice - strikePrice) * amount / settlementPrice - Put option (PUT): ITM condition =
settlementPrice < strikePrice, payout =(strikePrice - settlementPrice) * amount / strikePrice
If the option is out-of-the-money (OTM), the payout is 0. Remaining collateral is returned to the seller.
Reference source file: src/level3/EuropeanOption.sol
4. Pitfalls Encountered #
- Settlement price manipulation: the
_settlementPriceinexercise()is passed directly by the caller; a malicious caller could pass a price favorable to themselves - Collateral insufficient to cover maximum loss: the maximum loss for a call option seller is theoretically unlimited (the underlying asset price can rise indefinitely), but the collateral locked in the contract is a fixed
amount - Imprecise expiration time validation: when using
block.timestampfor comparisons, miners can manipulate the block timestamp within a small range - Premium non-refundable after purchase: if the option becomes deep out-of-the-money before expiration, the buyer cannot sell or abandon the option before expiration
- Premium paid in tokens but amount can be zero: there is no enforced minimum; a seller could create a zero-premium option that gets front-run
5. Root Causes of Pitfalls #
Settlement price manipulation is the most critical security issue for on-chain options. In the current implementation of EuropeanOption.sol, _settlementPrice is passed directly as a parameter to the exercise() function. This is a simplification that omits oracle integration — in a production environment, the settlement price should be provided by a decentralized oracle (such as Chainlink) and protected by an onlyOracle modifier.
However, even with Chainlink, there is potential manipulation risk: at the moment of expiration, an attacker could use flash loans to instantaneously manipulate the pool price, affecting Chainlink's quote (especially for illiquid assets). This requires protocol-level protections such as using TWAP (Time-Weighted Average Price) rather than instantaneous prices.
The issue of collateral being insufficient to cover maximum loss: the call option payout formula (S - K) * amount / S approaches amount as S approaches infinity. This means that if amount is fixed, the seller's maximum loss is the entire collateral. For put options, the payout (K - S) * amount / K has a maximum of amount when S=0. So the locked collateral amount in the contract exactly covers the maximum possible payout — this is correct design.
6. How to Resolve the Pitfalls #
Settlement price security: in a production environment, replace settlementPrice with an oracle query:
solidity
function exercise(uint256 _optionId) external nonReentrant {
// Use Chainlink oracle to get settlement price
uint256 settlementPrice = oracle.latestAnswer();
require(block.timestamp >= option.expiry, "Not expired");
// ... rest of logic unchanged
}
Also, it is recommended to introduce a small waiting window after expiration (e.g., 1 hour) to prevent exercise during oracle update intervals.
Premium minimum: add a minimum premium check in createOption to prevent spam creation of zero-fee options.
Expiration time safety: use block.timestamp >= option.expiry (rather than >) to allow precise exercise at the moment of expiration. The manipulation window for block.timestamp is within 30 seconds, which is negligible for options with daily expiration granularity.
7. Technical Highlights #
| Technique | Description |
|---|---|
| European vs. American | European can only be exercised at expiration; American anytime before |
| State Machine | ACTIVE → PURCHASED → EXERCISED |
| Collateral Escrow | Seller locks amount tokens in the contract |
| CALL Exercise Payout | max(settlementPrice - strike, 0) * amount / settlementPrice |
| PUT Exercise Payout | max(strike - settlementPrice, 0) * amount / strike |
| Collateral Return | Remaining collateral returned to seller after exercise |
| ReentrancyGuard | Purchase and exercise use nonReentrant |
| Custom Errors | NotWriter, NotHolder, OptionExpired, etc. |