Lab I β TyphoonCover,
a parametric product end to end
You are the product engineer for a Philippines-market windstorm cover sold through a super-app. Design the trigger, encode the terms, handle the failure modes, deploy, and then study the famous product that did all this in 2017 β and still died.
L1Product design before code
L2The contract
// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// TyphoonCover β parametric windstorm cover (teaching build). /// Trigger: authorised oracle reports peak wind for the fixed zone. /// If kph >= TRIGGER during the window, every holder can claim the fixed payout. contract TyphoonCover { address public immutable insurer; address public oracle; // authorised reporter uint256 public constant PREMIUM = 0.02 ether; uint256 public constant PAYOUT = 0.2 ether; uint32 public constant TRIGGER_KPH = 185; uint64 public immutable coverStart; uint64 public immutable coverEnd; struct Cover { bool active; bool settled; } mapping(address => Cover) public covers; uint256 public soldCount; uint32 public reportedPeakKph; bool public triggered; event CoverBought(address indexed holder); event WindReported(uint32 kph, bool triggered); event PayoutClaimed(address indexed holder, uint256 amount); event PremiumRefunded(address indexed holder); error WrongPremium(); error SaleClosed(); error AlreadyCovered(); error NotOracle(); error OutsideWindow(); error NotTriggered(); error NothingToSettle(); error TooEarly(); error NoDataFailure(); constructor(address _oracle, uint64 _start, uint64 _end) payable { insurer = msg.sender; oracle = _oracle; coverStart = _start; coverEnd = _end; // deployer funds the reserve via VALUE } /// Sales close when the cover window opens β no buying mid-typhoon. function buyCover() external payable { if (msg.value != PREMIUM) revert WrongPremium(); if (block.timestamp >= coverStart) revert SaleClosed(); if (covers[msg.sender].active) revert AlreadyCovered(); // solvency-as-code: never sell cover the reserve can't pay require(address(this).balance >= (soldCount + 1) * PAYOUT, "capacity exhausted"); covers[msg.sender] = Cover(true, false); soldCount++; emit CoverBought(msg.sender); } /// Oracle reports readings during the window; the max is retained. function reportWind(uint32 kph) external { if (msg.sender != oracle) revert NotOracle(); if (block.timestamp < coverStart || block.timestamp > coverEnd) revert OutsideWindow(); if (kph > reportedPeakKph) reportedPeakKph = kph; if (kph >= TRIGGER_KPH) triggered = true; emit WindReported(kph, triggered); } /// Pull-payment claims: each holder claims their own payout (no payout loops β M02 wound #6). function claim() external { if (!triggered) revert NotTriggered(); Cover storage c = covers[msg.sender]; if (!c.active || c.settled) revert NothingToSettle(); c.settled = true; // effects before interaction (bool ok, ) = msg.sender.call{value: PAYOUT}(""); require(ok, "transfer failed"); emit PayoutClaimed(msg.sender, PAYOUT); } /// Escape hatch: window closed and the oracle NEVER reported β refund premiums. /// An oracle outage must strand the insurer, not the customer. function refundOnDataFailure() external { if (block.timestamp <= coverEnd + 3 days) revert TooEarly(); if (reportedPeakKph != 0 || triggered) revert NoDataFailure(); Cover storage c = covers[msg.sender]; if (!c.active || c.settled) revert NothingToSettle(); c.settled = true; (bool ok, ) = msg.sender.call{value: PREMIUM}(""); require(ok, "transfer failed"); emit PremiumRefunded(msg.sender); } /// Residual reserve returns to the insurer only after the settlement window. function sweep() external { require(msg.sender == insurer, "not insurer"); require(block.timestamp > coverEnd + 30 days, "settlement open"); (bool ok, ) = insurer.call{value: address(this).balance}(""); require(ok, "transfer failed"); } }
Every design decision from L1 is visible in code β that's the point of the exercise:
- Solvency-as-code in
buyCoverβ the contract cannot oversell its reserve. Compare: a regulator's capacity rule, enforced per transaction. - Pull payments in
claim()β no loop over holders, so one broken recipient can't block the book (M02 wound #6). - The refund hatch β codifies the answer to "what if the oracle goes dark?" before launch, in customers' favour.
- Time-locked sweep β the insurer can't touch the reserve until every holder has had 30 days to settle.
L3Run it in Remix
Lab 6.1 Β· Full typhoon drill
Remix VM Β· ~45 min- Stage the actors
Remix VM gives you accounts. Cast: #1 insurer (deployer), #2 oracle, #3β4 customers. Copy #2's address now.
- Deploy funded
Compile (0.8.24+). Constructor args:
_oracle= account #2's address;_start= current unix time + 300 (5 min from now β get it from Date.now()/1000|0 in your browser console);_end= start + 600. Set VALUE = 1 ether (reserve for 5 covers) and Deploy from account #1. - Sell cover
From #3 and #4: VALUE = 0.02 ether β
buyCover(). Try a 5th purchase from #3 again βAlreadyCovered(). ReadsoldCount,covers(#3). - Try to cheat time
From #3, after start time passes, buy again from a fresh account β
SaleClosed(). (In Remix VM, time advances with each mined tx; you can also redeploy with a tighter window.) - Landfall
Once inside the window, from #2 (oracle):
reportWind(140)β event showstriggered=false. ThenreportWind(192)βtriggered=true. From #4 (not oracle) try reporting βNotOracle(). - Settle
From #3:
claim()β +0.2 ETH. Claim again βNothingToSettle(). From an address that never bought βNothingToSettle(). - Post-mortem sweep
sweep()from #1 too early β "settlement open". This is your time-lock protecting customers. - Rerun the disaster-that-didn't-happen
Redeploy; sell; let the window close with no oracle report; wait past +3 days (redeploy with short windows to simulate); customers call
refundOnDataFailure(). The outage cost the insurer, not the customer β as designed.
Optional β real testnet: switch environment to "Injected Provider β MetaMask" on Sepolia, faucet some test ETH (M00 links), redeploy with small values (0.0002/0.002 ether) and repeat with two browser profiles. Everything you did locally works identically in public.
L4Edge-case review β the adjuster's mindset, in code
| Scenario | Contract behaviour | Production upgrade |
|---|---|---|
| Oracle compromised | Single point of failure (lab simplification) | Decentralised oracle committee, median-of-N, signed sources, on-chain proofs |
| Oracle silent all window | Refund hatch after grace period | Same, plus secondary data source and incident SLA |
| Wind 184 km/h, roofs gone | No payout β basis risk by design | Tiered triggers (e.g. 150/185/220 β 25/60/100%) narrow the cliff |
| Everyone claims at once | Fine β pull payments, no loops | Same pattern; batch relayers for UX |
| Insurer wants reserve early | Time-locked sweep refuses | Multisig + published settlement calendar |
| Premium currency volatility | ETH-denominated (lab) | Stablecoin premiums/payouts (see Aon 2026; M09βM10) |
L5Case file β AXA "fizzy" (2017β2019)
fizzy Β· flight-delay parametric on public Ethereum
discontinued 2019AXA launched fizzy in 2017: buy flight-delay cover; if the airline data feed showed β₯ 2 h delay, automatic payout β no claim. Technically it worked and was genuinely pioneering (a major insurer, on public Ethereum, in production).
Why it died anyway: distribution (a standalone site nobody visited at booking time β cover must live inside the purchase flow, which is why your TyphoonCover ships inside a super-app), demand (customers didn't seek the product), and unit economics (payout ratios and gas made margins thin).
Transferable lesson: the contract is ~10% of a parametric product. Distribution, pricing, and data governance are the other 90 β Etherisc and Arbol survived by solving those (M08).
L6Appendix β the professional toolchain (Foundry)
When you outgrow Remix: Foundry gives you a local compiler, a test framework in Solidity, and mainnet-fork testing.
# install (per foundry docs), then: forge init typhoon-cover && cd typhoon-cover # drop TyphoonCover.sol into src/, then a test in test/: function test_TriggerPaysHolder() public { vm.warp(start - 1); vm.prank(customer); cover.buyCover{value: 0.02 ether}(); vm.warp(start + 10); vm.prank(oracleAddr); cover.reportWind(192); vm.prank(customer); cover.claim(); assertEq(customer.balance, before + 0.2 ether); } forge test -vvv
vm.warp (time travel) and vm.prank (impersonation) make the whole Lab 6.1 drill scriptable β that's the difference between demos and engineering. The full 32-h fCC course (M02) covers this stack end to end.
Teaching contract β not audited, not production. Production parametric adds decentralised oracles, tiered triggers, stablecoin settlement, KYC gates and regulatory product approval. fizzy history per AXA announcements and contemporaneous reporting.