$5.87M drained from TrustedVolumes in one tx via a permissionless RFQ signer bug. See how the attacker bypassed auth and drained a 1inch market maker's full inventory.

$5.87M drained from TrustedVolumes in a single transaction via three chained authorization bugs in a custom 1inch RFQ proxy. Here is how the attacker turned a permissionless signer registration function into a mechanism to drain a market maker's entire inventory with no victim signature required.
TrustedVolumes operates as a 1inch market maker and resolver, providing on-chain liquidity through a custom Request-for-Quote (RFQ) proxy deployed at 0xeEeEEe53033F7227d488ae83a27Bc9A9D5051756. In an RFQ model, a maker pre-signs orders, quoting a specific price for a specific token pair. A taker presents that signed quote to the settlement contract, which verifies the signature and executes the swap atomically.
The system relies on three guarantees working in concert, the maker must have authorized who can sign orders on its behalf, each signed order must be filled only once (replay protection), and the token source for the fill must be the authenticated maker's own inventory, not an arbitrary third-party address. In the TrustedVolumes implementation, all three guarantees failed simultaneously, and the attacker exploited them in a single composed transaction.
The exploit was not a single vulnerability. It was a chain of three independent logic failures in the RFQ implementation at 0x88eb28009351Fb414A5746F5d8CA91cdc02760d8, each of which was exploitable in isolation but together enabled total inventory drainage.
Bug 1: Permissionless signer registration
The function registerAllowedOrderSigner(address signer, bool allowed) was exposed as a public, access-controlled function, but the access control was to msg.sender, not to any privileged role.


This means any externally owned account or contract could call this function and designate an arbitrary EOA as a valid signer for itself. The attacker deployed an exploit contract at 0xD4D5DB5EC65272B26F756712247281515F211E95 and immediately called registerAllowedOrderSigner(0xC3...9100, true) making their own EOA a recognized signer for the exploit contract as maker.
Bug 2: Salt read/write key mismatch replay protection that never fired
The protocol's order status tracking used different storage keys for reading (checking) and writing (marking) fill status:

The write never populated the slot that the read checked. The result: an order could be submitted and filled an unlimited number of times. The replay protection compiled, deployed, and did absolutely nothing.
Bug 3: taker field reused as the transferFrom source address
The most critical bug. When the RFQ is filled, the inventory field in the order payload, an address completely controlled by the attacker in calldata was passed directly as the from argument to transferFrom. The authentication check only proved that the attacker's EOA was a valid signer for the attacker-controlled receiver contract. It proved nothing about the inventory address.

The attacker set inventory = 0x9bA0CF1588E1DFA905eC948F7FE5104dD40EDa31 The TrustedVolumes market maker's own custody address, which held unlimited token approvals to the RFQ proxy for WETH, USDT, WBTC, and USDC. The proxy dutifully pulled from the TrustedVolumes inventory because the approval existed, not because TrustedVolumes authorized this specific trade.
Before the exploit transaction, the attacker made two preparatory moves. They funded the exploit contract with 4 wei of USDC 1 per order replay, as the RFQ structure required the taker (the exploit contract) to send 1 USDC to the inventory per fill as the maker's quoted payment. They also pre-approved the RFQ proxy from the exploit contract for this nominal USDC amount. Both steps were necessary to pass the protocol's superficial validity checks.
The TrustedVolumes inventory address at the block 25039669, immediately before the attack, held:
| Token | Inventory Balance | Allowance to RFQ Proxy |
|---|---|---|
| WETH | 1,304.20 WETH | Unlimited |
| USDT | 208,366 USDT | Unlimited |
| WBTC | 17.11 WBTC | Unlimited |
| USDC | 1,281,587 USDC | Unlimited |
You can verify it using this command.
1INVENTORY="0x9bA0CF1588E1DFA905eC948F7FE5104dD40EDa31"
2RFQ_PROXY="0xeEeEEe53033F7227d488ae83a27Bc9A9D5051756"
3BLOCK=25039669
4RPC="<https://eth.drpc.org>"
5
6echo "============================================"
7echo " TrustedVolumes Inventory — Block $BLOCK"
8echo "============================================"
9
10for TOKEN in \\
11 "WETH:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2:18" \\
12 "WBTC:0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599:8" \\
13 "USDC:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48:6" \\
14 "USDT:0xdAC17F958D2ee523a2206206994597C13D831ec7:6"; do
15NAME=$(echo $TOKEN | cut -d: -f1)
16 ADDR=$(echo $TOKEN | cut -d: -f2)
17 DECIMALS=$(echo $TOKEN | cut -d: -f3)
18
19RAW_BAL=$(cast call $ADDR "balanceOf(address)(uint256)" \\
20 $INVENTORY --block $BLOCK --rpc-url $RPC | awk '{print $1}')
21
22RAW_APPR=$(cast call $ADDR "allowance(address,address)(uint256)" \\
23 $INVENTORY $RFQ_PROXY --block $BLOCK --rpc-url $RPC | awk '{print $1}')
24
25BALANCE=$(python3 -c "print(f'{int($RAW_BAL) / 10**$DECIMALS:,.4f}')")
26 APPROVAL=$(python3 -c "print(f'{int($RAW_APPR) / 10**$DECIMALS:,.4f}')")
27
28echo ""
29echo "[$NAME]"
30echo " Balance : $BALANCE"
31echo " Approval : $APPROVAL"
32done
33
Attack transaction executed the following sequence in a single atomic call:
Step 1: Deploy the exploit contract. The exploit contract 0xD4D5...1E95 was deployed. Its constructor is immediately called registerAllowedOrderSigner(0xC3...9100, true), registering the attacker EOA as a valid signer for the exploit contract as maker.

Step 2 Fill Order 1: 1,291 WETH (~$3.02M). The attacker submitted a signed RFQ order with inventory = TrustedVolumes. Bug 1 made the signature valid. Bug 2 meant replay protection never fired. Bug 3 meant transferFrom(TrustedVolumes, exploit_contract, 1291_WETH) executed. The exploit contract sent 1 wei USDC back to TrustedVolumes as the maker's payment.



Step 3 Fill Order 2: 206,282 USDT (~$206K). Same mechanism, second drain. Replay protection still didn't fire the salt write went to the wrong storage slot.

Step 4 Fill Order 3: 16.94 WBTC (~$1.70M). Third drain. Fourth call to the fill function with an identical attack structure.

Step 5 Fill Order 4: 1,268,771 USDC (~$1.27M). Final drain. Total time elapsed: a single Ethereum block.

Step 6 Forward proceeds. The exploit contract unwrapped received WETH to ETH and forwarded all extracted assets to the attacker EOA at 0xC3EBDdEa4f69df717a8f5c89e7cF20C1c0389100.

The root cause is a single authorization boundary failure, signer authorization for a receiver does not constitute authorization to spend from an arbitrary inventory address. The fill function verified allowedOrderSigner[order.receiver][signer] confirming the attacker could act as a signer for the attacker's own contract then used that proof to justify transferFrom(order.inventory, ...), where inventory was an entirely different address the attacker specified in calldata.
The slippage logic, the Chainlink oracle pricing, and the signature recovery all worked correctly. What failed was the assumption that a valid signature from an authorized signer for receiver X conferred spending authority over inventory address Y. It did not, and the code never checked whether it should.
The secondary failure replay protection writing to a different storage key than it read compounded the damage by allowing the same attack to run four times in the same transaction without any of the fills being recorded as complete.
QuillAudits smart contract audit process covers authorization boundaries, replay protection, and adversarial calldata testing across all externally callable paths the exact areas involved in this exploit.
The permissionless registerAllowedOrderSigner function would have been flagged as a critical access control issue, since any contract could self-register and assign arbitrary signers. The replay protection flaw caused by mismatched storage keys would have been identified through nonce and storage path verification.
Most importantly, the audit would have caught that order.inventory was fully caller-controlled and used as a transferFrom source without validating ownership or authorization. Adversarial RFQ simulations would have demonstrated that any approved inventory address could be drained by an attacker-controlled receiver.
The unlimited token approvals further increased the blast radius, and approval scoping would have been recommended as a mitigation.
With a QuillAudits audit, all three bugs could have been identified and patched before deployment.
Get Your Smart Contract Audited Now.
The stolen funds are currently sitting in attacker-controlled EOAs.



The team acknowledged the incident, but at the time of writing, no further updates have been shared.
Following the incident, the 1inch team stated in a tweet that Trusted Volumes operates independently and that 1inch itself was not impacted by the attack.
Attacker EOAs:
Attack Transaction
0xc5c61b3ac39d854773b9dc34bd0cdbc8b5bbf75f18551802a0b5881fcb990513
TrustedVolumes RFQ proxy (vulnerable):
0xeEeEEe53033F7227d488ae83a27Bc9A9D5051756
RFQ implementation:
0x88eb28009351Fb414A5746F5d8CA91cdc02760d8
TrustedVolumes inventory (victim):
0x9bA0CF1588E1DFA905eC948F7FE5104dD40EDa31
Inventory owner:
0xc493F943a1fd910D01dcaBea86e61415ce43E787
The TrustedVolumes exploit highlights a core RFQ security failure, verifying a signer is not the same as verifying spending authority. The protocol confirmed the attacker could sign for their own contract but never checked whether they controlled the inventory address used in the order. That missing authorization link enabled the $5.87M drain. Broken replay protection amplified the damage, but the root issue was incomplete authorization. In RFQ systems, signer validation and token ownership must always be bound to the same address.
Contents


From day-zero risk mapping to exchange-ready audits — QuillAudits helps projects grow with confidence. Smart contracts, dApps, infrastructure, compliance — secured end-to-end.