On Ethereum at 2026-05-07 00:47:35 UTC, the attacker exploited an authorization design flaw in TrustedVolumes’ custom RFQ flow to create a maker/signer relationship they controlled and then settle orders against a third-party resolver’s pre-approved balances. The loss in this transaction was approximately $5.87M, consisting of 1291.16110521587917927 WETH, 206282.446876 USDT, 16.93910519 WBTC, and 1268771.488875 USDC. In practical terms, the attacker first used registerAllowedOrderSigner(address,bool) to authorize their own EOA as a valid signer for their helper contract as maker, then repeatedly invoked the RFQ execution path to make the proxy pull tokens from the TrustedVolumes resolver with transferFrom. The drained assets were routed through an attacker-created helper contract, which unwrapped WETH to ETH and forwarded the proceeds to the attacker EOA.
Root Cause
Vulnerable Contract
The vulnerable entry point was the TrustedVolumes RFQ proxy at 0xeeeeee53033f7227d488ae83a27bc9a9d5051756, which DELEGATECALLed into implementation 0x88eb28009351fb414a5746f5d8ca91cdc02760d8 during both the signer-registration step and each drain iteration. The implementation address is confirmed by the call trace in trace_callTracer.json. Source for the implementation is only available as 0x88eb28009351fb414a5746f5d8ca91cdc02760d8/recovered.sol [recovered — approximation], so the trace is the primary evidence and the recovered Solidity is supporting context.
Vulnerable Function
The exploit relies on two functions working together. The setup step is registerAllowedOrderSigner(address,bool) with selector 0xea7faa61, which writes to _allowedOrderSigner[msg.sender][signer]. The drain step is func_4112e1c2(...) with selector 0x4112e1c2, whose reconstructed source shows that it verifies _allowedOrderSigner[varg4][signer] but later transfers funds from address(varg17), not from varg4.
Vulnerable Code
function registerAllowedOrderSigner(address signer, bool allowed) public {
_allowedOrderSigner[msg.sender][signer] = allowed; // <-- attacker can self-authorize signer for their own maker address
}
function func_4112e1c2(
address varg0,
address varg1,
address varg2,
address varg3,
address varg4,
address varg5,
uint128 varg6,
uint128 varg7,
uint128 varg8,
uint128 varg9,
uint128 varg10,
uint128 varg11,
address varg12,
address varg13,
address varg14,
address varg15,
address varg16,
uint64 varg17,
uint64 varg18,
uint64 varg19
) public payable {
// ... orderHash construction omitted ...
address signer = ecrecover(orderHash, uint8(varg8), bytes32(uint256(varg9)), bytes32(uint256(varg10)));
require(_allowedOrderSigner[varg4][signer], "Rfq: notAllowedMaker"); // <-- checks signer against maker varg4
// ... expiry / salt / price checks omitted ...
address receiver = address(varg18) == address(0) ? msg.sender : address(varg18);
if (varg14 != nativeToken) {
_safeTransferFrom(uint256(uint160(varg16)), receiver, address(varg17), varg14); // <-- pulls from varg17 instead of maker varg4
bool s = IERC20(wrappedNativeToken).transferFrom(address(varg17), address(this), uint256(uint160(varg15)));
require(s);
IERC20(wrappedNativeToken).withdraw(uint256(uint160(varg15)));
_safeEthTransfer(uint256(uint160(varg15)), receiver);
} else {
_safeEthTransfer(uint256(uint160(varg16)), address(varg17));
}
_safeTransferFrom(uint256(uint160(varg15)), receiver, address(varg17), varg13); // <-- second pull also uses varg17
}
Note: This code block is based on the user-supplied reconstructed source for 0x88eb28009351fb414a5746f5d8ca91cdc02760d8, not verified source from Etherscan. The on-chain trace remains authoritative.
Why It’s Vulnerable
Expected behavior: the maker/signer authorization check should bind the signed order to the account whose assets will actually be debited. If a maker is allowed to appoint signers, the settlement path should only transfer funds from that same maker, or from another address that is explicitly and cryptographically bound into the signed order under the intended trust model.
Actual behavior: registerAllowedOrderSigner(address,bool) lets any caller set _allowedOrderSigner[msg.sender][signer], so the attacker helper could legitimately make itself the maker and authorize the attacker EOA as its signer. That alone would not be fatal if settlement were limited to the maker’s own balances. But func_4112e1c2 checks _allowedOrderSigner[varg4][signer] and then pulls tokens from address(varg17), a separate address supplied in calldata. In the exploit trace, the authorized maker was the attacker helper while the debited address was the TrustedVolumes resolver 0x9ba0cf1588e1dfa905ec948f7fe5104dd40eda31.
This mismatch is the key flaw. The contract validates signer authority relative to one address and sources funds from another. Because the resolver had already approved the RFQ proxy, the attacker only needed to create a valid maker/signer pair they controlled and then point the funding leg at the resolver. The exploit therefore was not simply “missing onlyOwner on signer registration”; it was the combination of self-service signer registration and an RFQ settlement path that fails to bind the authorized maker to the actual token owner being debited.
Normal flow vs Attack flow: under a safe RFQ design, the same principal that authorizes a signer would also be the principal whose balances are spent, or the order would explicitly authorize spending from a distinct funded account. Here, the attacker used their helper as maker, their EOA as authorized signer, and the resolver as the funding source. The trace shows that this separation let the proxy transfer assets from the resolver into the attacker helper while returning only 1 raw USDC unit to the resolver on each iteration.
Attack Execution
High-Level Flow
- The attacker EOA deployed a helper contract inside the exploit transaction.
- The helper called the TrustedVolumes RFQ proxy to authorize the attacker EOA as a valid signer for the helper’s own maker address.
- The helper probed resolver balances and allowances for the target assets.
- The helper invoked the RFQ execution path four times with itself as the authorized maker context but the TrustedVolumes resolver as the address whose tokens would actually be debited.
- During each drain, the proxy delegated into the RFQ implementation, recovered the signer, checked the maker/signer relationship, validated oracle-based loss limits, and then executed token
transferFromcalls against the resolver in proxy context. - The helper received the drained assets and sent back only
1raw USDC unit during each iteration. - The helper unwrapped the stolen WETH into ETH.
- The helper forwarded ETH, USDT, WBTC, and USDC to the attacker EOA.
Detailed Call Trace
The following flow is derived from trace_callTracer.json, decoded_calls.json, and the user-supplied reconstructed source for 0x88eb...:
0xc3ebddea4f69df717a8f5c89e7cf20c1c0389100→CREATE→0xd4d5db5ec65272b26f756712247281515f211e95- The attacker deploys the helper contract in the same transaction.
0xd4d5db5ec65272b26f756712247281515f211e95→0xeeeeee53033f7227d488ae83a27bc9a9d5051756viaCALLselector0xea7faa61registerAllowedOrderSigner(address,bool)with signer0xc3ebddea4f69df717a8f5c89e7cf20c1c0389100andallowed = true.- From the reconstructed source, this writes
_allowedOrderSigner[msg.sender][signer], so the helper becomes the maker and the attacker EOA becomes its authorized signer.
0xeeeeee53033f7227d488ae83a27bc9a9d5051756→0x88eb28009351fb414a5746f5d8ca91cdc02760d8viaDELEGATECALLselector0xea7faa61- The implementation executes the state change in proxy storage context.
- The helper performs reconnaissance calls against token contracts, including
balanceOfandallowance, to confirm the resolver’s approved balances. - First drain iteration:
- Helper → proxy via
CALLselector0x4112e1c2 - Proxy → implementation via
DELEGATECALLselector0x4112e1c2 - The first observed calldata words map cleanly onto the reconstructed source:
varg0 = USDC,varg1 = WETH,varg2 = 1,varg3 = 1291.16110521587917927e18,varg4 = helper,varg5 = resolver,varg6 = expiry,varg7 = salt,varg8/9/10 = signature,varg11 = 2. - Implementation path performs
STATICCALLs toecrecoverprecompile0x000...001, tokendecimals(), and ChainlinklatestRoundData(). - The signer check is satisfied against
_allowedOrderSigner[helper][attackerEOA]. - The settlement leg then pulls WETH from resolver
0x9ba0cf1588e1dfa905ec948f7fe5104dd40eda31into helper0xd4d5...1e95, showing that the debited account is separate from the maker used in the signer check. - Proxy also calls USDC
transferFrom(address,address,uint256)to move1raw USDC unit from helper back to resolver.
- Helper → proxy via
- Second drain iteration:
- Same
0x4112e1c2call pattern. - USDT
transferFrommoves206282.446876USDT from resolver to helper. 1raw USDC unit is returned to the resolver.
- Same
- Third drain iteration:
- Same
0x4112e1c2call pattern. - WBTC
transferFrommoves16.93910519WBTC from resolver to helper. 1raw USDC unit is returned to the resolver.
- Same
- Fourth drain iteration:
- Same
0x4112e1c2call pattern. - USDC
transferFrommoves1268771.488879USDC from resolver to helper. 1raw USDC unit is returned to the resolver.
- Same
- Post-drain monetization:
- Helper → WETH via
CALLselector0x2e1a7d4d(withdraw(uint256)) for1291.16110521587917927WETH. - WETH sends
1291.161105215879160824ETH to the helper. - Helper forwards the ETH to the attacker EOA and also transfers the stolen USDT, WBTC, and USDC to the attacker EOA.
- Helper → WETH via
The trace is still the authoritative source for this flow, but the reconstructed source materially improves the semantic interpretation: the exploit hinges on the contract checking authorization against the maker address while sourcing funds from a separate calldata-controlled address.
Financial Impact
The deterministic profit figures in funds_flow.json show net attacker gains of:
1291.16110521587917927WETH, later unwrapped to ETH206282.446876USDT16.93910519WBTC1268771.488875USDC
The victim of the direct drains was the TrustedVolumes resolver at 0x9ba0cf1588e1dfa905ec948f7fe5104dd40eda31, whose balances decreased by those same amounts. The incident brief’s total loss estimate of roughly $5.87M is consistent with the observed token mix. The protocol remained callable after the transaction, but the resolver lost the approved balances that the proxy was able to pull.
Evidence
- Selector verification:
registerAllowedOrderSigner(address,bool)→0xea7faa61transferFrom(address,address,uint256)→0x23b872ddapprove(address,uint256)→0x095ea7b3withdraw(uint256)→0x2e1a7d4dlatestRoundData()→0xfeaf968c
- Reconstructed-source evidence from the user-supplied
0x88eb...code:registerAllowedOrderSigner(address,bool)writes_allowedOrderSigner[msg.sender][signer], confirming that the helper could authorize the attacker EOA specifically for the helper’s maker slot.func_4112e1c2(...)enforcesrequire(_allowedOrderSigner[varg4][signer], "Rfq: notAllowedMaker"), but later debitsaddress(varg17)via_safeTransferFrom(..., address(varg17), ...), showing the authorization subject and funding source are distinct inputs.
- Trace-backed authorization evidence:
decoded_calls.jsonentryindex: 1shows helper → proxy call to0xea7faa61.decoded_calls.jsonentryindex: 2shows proxy → implementationDELEGATECALLfor the same selector.- The call succeeds and is followed by four drain calls using selector
0x4112e1c2(index: 9/24/39/56at depth 1 andindex: 10/25/40/57at depth 2). - The first full
0x4112e1c2calldata contains helper0xd4d5...1e95and resolver0x9ba0...da31as distinct addresses, matching the reconstructed source’s maker/funding-source split.
- Funds-flow evidence:
funds_flow.jsonrecords the resolver-to-helper transfers for WETH, USDT, WBTC, and USDC, and the helper-to-attacker transfers for USDT, WBTC, and USDC.funds_flow.jsonalso records the helper approval granting the RFQ proxy a4-unit USDC allowance, matching the four1-unit USDC returns to the resolver.
- State-diff evidence:
trace_prestateTracer.jsonshows proxy storage changes during the transaction, consistent with state mutation in proxy context during the signer-registration and order-execution paths.