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

  1. The attacker EOA deployed a helper contract inside the exploit transaction.
  2. The helper called the TrustedVolumes RFQ proxy to authorize the attacker EOA as a valid signer for the helper’s own maker address.
  3. The helper probed resolver balances and allowances for the target assets.
  4. 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.
  5. 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 transferFrom calls against the resolver in proxy context.
  6. The helper received the drained assets and sent back only 1 raw USDC unit during each iteration.
  7. The helper unwrapped the stolen WETH into ETH.
  8. 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...:

  • 0xc3ebddea4f69df717a8f5c89e7cf20c1c0389100CREATE0xd4d5db5ec65272b26f756712247281515f211e95
    • The attacker deploys the helper contract in the same transaction.
  • 0xd4d5db5ec65272b26f756712247281515f211e950xeeeeee53033f7227d488ae83a27bc9a9d5051756 via CALL selector 0xea7faa61
    • registerAllowedOrderSigner(address,bool) with signer 0xc3ebddea4f69df717a8f5c89e7cf20c1c0389100 and allowed = 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.
  • 0xeeeeee53033f7227d488ae83a27bc9a9d50517560x88eb28009351fb414a5746f5d8ca91cdc02760d8 via DELEGATECALL selector 0xea7faa61
    • The implementation executes the state change in proxy storage context.
  • The helper performs reconnaissance calls against token contracts, including balanceOf and allowance, to confirm the resolver’s approved balances.
  • First drain iteration:
    • Helper → proxy via CALL selector 0x4112e1c2
    • Proxy → implementation via DELEGATECALL selector 0x4112e1c2
    • 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 to ecrecover precompile 0x000...001, token decimals(), and Chainlink latestRoundData().
    • The signer check is satisfied against _allowedOrderSigner[helper][attackerEOA].
    • The settlement leg then pulls WETH from resolver 0x9ba0cf1588e1dfa905ec948f7fe5104dd40eda31 into helper 0xd4d5...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 move 1 raw USDC unit from helper back to resolver.
  • Second drain iteration:
    • Same 0x4112e1c2 call pattern.
    • USDT transferFrom moves 206282.446876 USDT from resolver to helper.
    • 1 raw USDC unit is returned to the resolver.
  • Third drain iteration:
    • Same 0x4112e1c2 call pattern.
    • WBTC transferFrom moves 16.93910519 WBTC from resolver to helper.
    • 1 raw USDC unit is returned to the resolver.
  • Fourth drain iteration:
    • Same 0x4112e1c2 call pattern.
    • USDC transferFrom moves 1268771.488879 USDC from resolver to helper.
    • 1 raw USDC unit is returned to the resolver.
  • Post-drain monetization:
    • Helper → WETH via CALL selector 0x2e1a7d4d (withdraw(uint256)) for 1291.16110521587917927 WETH.
    • WETH sends 1291.161105215879160824 ETH to the helper.
    • Helper forwards the ETH to the attacker EOA and also transfers the stolen USDT, WBTC, and USDC to the attacker EOA.

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.16110521587917927 WETH, later unwrapped to ETH
  • 206282.446876 USDT
  • 16.93910519 WBTC
  • 1268771.488875 USDC

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)0xea7faa61
    • transferFrom(address,address,uint256)0x23b872dd
    • approve(address,uint256)0x095ea7b3
    • withdraw(uint256)0x2e1a7d4d
    • latestRoundData()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(...) 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.
  • Trace-backed authorization evidence:
    • 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.
    • The call succeeds and is followed by four drain calls using selector 0x4112e1c2 (index: 9/24/39/56 at depth 1 and index: 10/25/40/57 at depth 2).
    • The first full 0x4112e1c2 calldata contains helper 0xd4d5...1e95 and resolver 0x9ba0...da31 as distinct addresses, matching the reconstructed source’s maker/funding-source split.
  • Funds-flow evidence:
    • 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.
  • State-diff evidence:
    • 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.