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.
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.
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.
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.
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.
transferFrom calls against the resolver in proxy context.1 raw USDC unit during each iteration.The following flow is derived from trace_callTracer.json, decoded_calls.json, and the user-supplied reconstructed source for 0x88eb...:
0xc3ebddea4f69df717a8f5c89e7cf20c1c0389100 → CREATE → 0xd4d5db5ec65272b26f756712247281515f211e950xd4d5db5ec65272b26f756712247281515f211e95 → 0xeeeeee53033f7227d488ae83a27bc9a9d5051756 via CALL selector 0xea7faa61registerAllowedOrderSigner(address,bool) with signer 0xc3ebddea4f69df717a8f5c89e7cf20c1c0389100 and allowed = true._allowedOrderSigner[msg.sender][signer], so the helper becomes the maker and the attacker EOA becomes its authorized signer.0xeeeeee53033f7227d488ae83a27bc9a9d5051756 → 0x88eb28009351fb414a5746f5d8ca91cdc02760d8 via DELEGATECALL selector 0xea7faa61balanceOf and allowance, to confirm the resolver’s approved balances.CALL selector 0x4112e1c2DELEGATECALL selector 0x4112e1c2varg0 = USDC, varg1 = WETH, varg2 = 1, varg3 = 1291.16110521587917927e18, varg4 = helper, varg5 = resolver, varg6 = expiry, varg7 = salt, varg8/9/10 = signature, varg11 = 2.STATICCALLs to ecrecover precompile 0x000...001, token decimals(), and Chainlink latestRoundData()._allowedOrderSigner[helper][attackerEOA].0x9ba0cf1588e1dfa905ec948f7fe5104dd40eda31 into helper 0xd4d5...1e95, showing that the debited account is separate from the maker used in the signer check.transferFrom(address,address,uint256) to move 1 raw USDC unit from helper back to resolver.0x4112e1c2 call pattern.transferFrom moves 206282.446876 USDT from resolver to helper.1 raw USDC unit is returned to the resolver.0x4112e1c2 call pattern.transferFrom moves 16.93910519 WBTC from resolver to helper.1 raw USDC unit is returned to the resolver.0x4112e1c2 call pattern.transferFrom moves 1268771.488879 USDC from resolver to helper.1 raw USDC unit is returned to the resolver.CALL selector 0x2e1a7d4d (withdraw(uint256)) for 1291.16110521587917927 WETH.1291.161105215879160824 ETH to the helper.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.
The deterministic profit figures in funds_flow.json show net attacker gains of:
1291.16110521587917927 WETH, later unwrapped to ETH206282.446876 USDT16.93910519 WBTC1268771.488875 USDCThe 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.
registerAllowedOrderSigner(address,bool) → 0xea7faa61transferFrom(address,address,uint256) → 0x23b872ddapprove(address,uint256) → 0x095ea7b3withdraw(uint256) → 0x2e1a7d4dlatestRoundData() → 0xfeaf968c0x88eb... 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(...) enforces require(_allowedOrderSigner[varg4][signer], "Rfq: notAllowedMaker"), but later debits address(varg17) via _safeTransferFrom(..., address(varg17), ...), showing the authorization subject and funding source are distinct inputs.decoded_calls.json entry index: 1 shows helper → proxy call to 0xea7faa61.decoded_calls.json entry index: 2 shows proxy → implementation DELEGATECALL for the same selector.0x4112e1c2 (index: 9/24/39/56 at depth 1 and index: 10/25/40/57 at depth 2).0x4112e1c2 calldata contains helper 0xd4d5...1e95 and resolver 0x9ba0...da31 as distinct addresses, matching the reconstructed source’s maker/funding-source split.funds_flow.json records the resolver-to-helper transfers for WETH, USDT, WBTC, and USDC, and the helper-to-attacker transfers for USDT, WBTC, and USDC.funds_flow.json also records the helper approval granting the RFQ proxy a 4-unit USDC allowance, matching the four 1-unit USDC returns to the resolver.trace_prestateTracer.json shows proxy storage changes during the transaction, consistent with state mutation in proxy context during the signer-registration and order-execution paths.