Skip to content

Commit 9ac950f

Browse files
Add a Base fork that shows one V6 claims leaf pays again in the same batch.
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d62bea8 commit 9ac950f

1 file changed

Lines changed: 213 additions & 0 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// SPDX-License-Identifier: LicenseRef-DCL-1.0
2+
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
3+
pragma solidity =0.8.25;
4+
5+
import {Test, console2} from "forge-std-1.17.0/src/Test.sol";
6+
import {IERC20} from "@openzeppelin-contracts-5.7.0/token/ERC20/IERC20.sol";
7+
8+
/// @title RaindexV6ClaimsDoubleTakeForkTest
9+
/// @notice Fork of the Base RaindexV6 at `0xb05D…e3cE` the block before
10+
/// `0x9844…6476` took claims order
11+
/// `0xaffd6e3aaad92a44017714b1db48909098f509404c9baeafd29bf4eda874d7c8`.
12+
///
13+
/// The order is the `claims.rain` strategy. `calculate-io` requires the leaf's
14+
/// claimed flag to be unset. `handle-io` is what sets it. This deployment's
15+
/// `takeOrders4` runs every `calculate-io` in the batch before any `handle-io`,
16+
/// so a second copy of the same leaf in that batch still sees the flag unset.
17+
///
18+
/// The signed context is the one from the take in the next block,
19+
/// `0xbdfb412dc546e960b0701c891beb0c88422749c7a5f534bc9666f1f841b0f887`:
20+
/// leaf index 46, the claimed amount, and its depth-8 merkle proof.
21+
contract RaindexV6ClaimsDoubleTakeForkTest is Test {
22+
uint256 internal constant FORK_BLOCK = 52_675_841;
23+
address internal constant RAINDEX = 0xb05D73E6BCc26AEB5b67Ff68C6E9C6151073e3cE;
24+
address internal constant TAKER = 0x98440184426887970B7c3141e3019B6f39406476;
25+
address internal constant OWNER = 0xA51fd23D6E2442805130eac0712F590691e91517;
26+
address internal constant INTERPRETER = 0x3bF9bd9DA4784f75c92317e61c68493Ecc9aaBDC;
27+
address internal constant STORE = 0x1Aa775533E28B1D843e1A589034984E3a62005DC;
28+
address internal constant SIGNER = 0x48C2A79B9604ADd957884bd51559693bCC9481B7;
29+
address internal constant WETH = 0x4200000000000000000000000000000000000006;
30+
address internal constant USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913;
31+
32+
bytes32 internal constant ORDER_HASH = 0xaffd6e3aaad92a44017714b1db48909098f509404c9baeafd29bf4eda874d7c8;
33+
bytes32 internal constant NONCE = 0x7cd8721efc5b91488edf682ddf54d318cd783c50f3309f62e4fbf85c203fa5cb;
34+
bytes32 internal constant VAULT_ID = 0x75340ff2e67adee60702dc91d4fe0b8d5c23b8c444681acd2e7887e528edadb1;
35+
/// 5.970208333333340160, the leaf amount. `takeOrders4` pushes the lossy
36+
/// 6-decimal truncation, 5.970208 USDC.
37+
bytes32 internal constant CLAIM = 0xffffffee000000000000000000000000000000000000000052da70d8ff692000;
38+
/// Copies of this leaf submitted in one `takeOrders4`.
39+
uint256 internal constant CLAIM_COPIES = 3;
40+
/// Same exponent as `CLAIM`, coefficient scaled by `CLAIM_COPIES`, so every copy fits under `maximumIO`.
41+
bytes32 internal constant CLAIMS = bytes32(
42+
(uint256(CLAIM) & ~((uint256(1) << 224) - 1)) | ((uint256(CLAIM) & ((uint256(1) << 224) - 1)) * CLAIM_COPIES)
43+
);
44+
uint256 internal constant CLAIM_USDC = 5_970_208;
45+
46+
bytes internal constant BYTECODE =
47+
hex"000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000457870656374656420636c61696dee000000000000000000000000000000000000000000000000636c61696d6564e70000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000436c61696d6564e713595e07b47595143f3c8574b2a03918d8faac9de11cc20a4eb15187673efb4e000000000000000000000000000000000000000000000000000050726f6f66e500000000000000000000000022d83c031bc02737ad3eaf5aa3386de2bf7f7fbc00000000000000000000000000000000000000000000000000000000000000df0500000024005c0070009808040004031001060b100002001000010b010003001000010b01000400100000011000000d060002031001060b1000020110000100100000031004041f1200001e020000011000030110000200100001031000010c13000047020000040300010310010603100201031000060c1300000906010101100004011000000110000200100000031000010c130000461100001f1200001e0200000e0b010203100906031008060310070603100606031005060310040603100306031002060010000001100005021a000701100006001000011e020000";
48+
49+
IRaindexV6Fork internal constant RAINDEX_FORK = IRaindexV6Fork(RAINDEX);
50+
51+
function setUp() external {
52+
vm.createSelectFork("base", FORK_BLOCK);
53+
}
54+
55+
/// The order bytes hash to the order the taker took.
56+
function testFork_orderHash() external pure {
57+
assertEq(keccak256(abi.encode(_order())), ORDER_HASH, "order hash");
58+
}
59+
60+
/// One copy, with `maximumIO` equal to the leaf, pays 5.970208 USDC and
61+
/// nothing in return. That is the transfer in the next block.
62+
function testFork_historicalClaimPaysOnce() external {
63+
uint256 usdcBefore = IERC20(USDC).balanceOf(TAKER);
64+
uint256 wethBefore = IERC20(WETH).balanceOf(TAKER);
65+
66+
vm.prank(TAKER);
67+
(bytes32 takerInput, bytes32 takerOutput) = RAINDEX_FORK.takeOrders4(_config(1, CLAIM));
68+
69+
uint256 usdcAfter = IERC20(USDC).balanceOf(TAKER);
70+
console2.log("historical claim USDC received", usdcAfter - usdcBefore);
71+
assertEq(takerInput, CLAIM, "taker receives the leaf");
72+
assertEq(takerOutput, bytes32(0), "ratio 0 pays nothing");
73+
assertEq(usdcAfter - usdcBefore, CLAIM_USDC, "USDC received");
74+
assertEq(IERC20(WETH).balanceOf(TAKER), wethBefore, "WETH untouched");
75+
}
76+
77+
/// The claimed flag is visible to a later transaction. A second take of the
78+
/// same leaf reverts in `calculate-io`.
79+
function testFork_laterTransactionSeesClaimedFlag() external {
80+
uint256 before = IERC20(USDC).balanceOf(TAKER);
81+
82+
vm.prank(TAKER);
83+
RAINDEX_FORK.takeOrders4(_config(1, CLAIM));
84+
uint256 afterFirst = IERC20(USDC).balanceOf(TAKER);
85+
console2.log("later transaction USDC received after first take", afterFirst - before);
86+
87+
vm.expectRevert();
88+
vm.prank(TAKER);
89+
RAINDEX_FORK.takeOrders4(_config(1, CLAIM));
90+
91+
uint256 afterSecond = IERC20(USDC).balanceOf(TAKER);
92+
console2.log("later transaction USDC received after reverted take", afterSecond - before);
93+
assertEq(afterSecond, afterFirst, "second transaction pays nothing");
94+
}
95+
96+
/// `CLAIM_COPIES` copies in one `takeOrders4` all calculate before any
97+
/// `handle-io` sets the flag, so the leaf pays once per copy. `maximumIO`
98+
/// is that many leaves; the historical take set it to one leaf, which
99+
/// stops the loop after the first.
100+
function testFork_sameLeafPaysTwiceInOneBatch() external {
101+
uint256 before = IERC20(USDC).balanceOf(TAKER);
102+
103+
vm.prank(TAKER);
104+
(bytes32 takerInput, bytes32 takerOutput) = RAINDEX_FORK.takeOrders4(_config(CLAIM_COPIES, CLAIMS));
105+
106+
uint256 usdcAfter = IERC20(USDC).balanceOf(TAKER);
107+
console2.log("double take USDC received", usdcAfter - before);
108+
assertEq(takerOutput, bytes32(0), "ratio 0 pays nothing");
109+
assertEq(usdcAfter - before, CLAIM_COPIES * CLAIM_USDC, "leaf paid once per copy");
110+
// The returned input float is the sum of the two fills. Its exact
111+
// packing is the contract's, so the USDC balance is the assertion.
112+
assertTrue(takerInput != bytes32(0), "batch took a non-zero input");
113+
}
114+
115+
function _config(uint256 copies, bytes32 maximumIO) internal pure returns (TakeOrdersConfigV5 memory config) {
116+
TakeOrderConfigV4[] memory orders = new TakeOrderConfigV4[](copies);
117+
for (uint256 i = 0; i < copies; i++) {
118+
orders[i] = TakeOrderConfigV4({
119+
order: _order(), signedContext: _signedContext(), inputIOIndex: 0, outputIOIndex: 0
120+
});
121+
}
122+
config = TakeOrdersConfigV5({
123+
minimumIO: bytes32(0),
124+
maximumIO: maximumIO,
125+
// Coefficient 1, exponent 0. The order's ratio is 0, which is not above this.
126+
maximumIORatio: bytes32(uint256(1)),
127+
IOIsInput: true,
128+
orders: orders,
129+
data: ""
130+
});
131+
}
132+
133+
function _order() internal pure returns (OrderV4 memory) {
134+
IOV2[] memory inputs = new IOV2[](1);
135+
inputs[0] = IOV2({token: WETH, vaultId: VAULT_ID});
136+
IOV2[] memory outputs = new IOV2[](1);
137+
outputs[0] = IOV2({token: USDC, vaultId: VAULT_ID});
138+
return OrderV4({
139+
owner: OWNER,
140+
evaluable: EvaluableV4({interpreter: INTERPRETER, store: STORE, bytecode: BYTECODE}),
141+
validInputs: inputs,
142+
validOutputs: outputs,
143+
nonce: NONCE
144+
});
145+
}
146+
147+
function _signedContext() internal pure returns (SignedContextV1[] memory signed) {
148+
bytes32[] memory context = new bytes32[](10);
149+
context[0] = bytes32(uint256(0x2e));
150+
context[1] = CLAIM;
151+
context[2] = 0xa72857e104d6edf45c955edd7a7976646df6d5212fb61fa583c6392ed85621a5;
152+
context[3] = 0x06ce56df476223b87d18d2a8fcfd60519edc8760f81d816d63150d4a5d192e41;
153+
context[4] = 0xbbbb063f8db2d8551361f810c35646ca9fb841b40c532f48103aec752db7a87a;
154+
context[5] = 0x777eb73adecec219741d2ef199811e18b01c5a0eaee9375e74fdd47fd13130df;
155+
context[6] = 0x19f43218f6ba810cb612b4f6dc9792101cedfb52e3c6d025cca5239e160a59ee;
156+
context[7] = 0x18bafbf03371d5d2dec1aab04d2e6d948cf3531bf70f33dbdadfca1ab33054d2;
157+
context[8] = 0x66058ac12680ec286b66d779cb5869ab831392446b5abda03057d74c958ffaac;
158+
context[9] = 0x4beaed292b7b3037c6111adba908f32980b69e4726e8085c129d027b3a547370;
159+
signed = new SignedContextV1[](1);
160+
signed[0] = SignedContextV1({
161+
signer: SIGNER,
162+
context: context,
163+
signature: hex"971d18d4b254805659250722e1e3b7f2b795c2fc27ee1146186fd39e2b98e5923145f5bb49bccaf9baa2843a175cdbd5ac4882b9446d7eb610d5ce8db799c02b1c"
164+
});
165+
}
166+
}
167+
168+
interface IRaindexV6Fork {
169+
function takeOrders4(TakeOrdersConfigV5 calldata config)
170+
external
171+
returns (bytes32 totalTakerInput, bytes32 totalTakerOutput);
172+
}
173+
174+
struct IOV2 {
175+
address token;
176+
bytes32 vaultId;
177+
}
178+
179+
struct EvaluableV4 {
180+
address interpreter;
181+
address store;
182+
bytes bytecode;
183+
}
184+
185+
struct OrderV4 {
186+
address owner;
187+
EvaluableV4 evaluable;
188+
IOV2[] validInputs;
189+
IOV2[] validOutputs;
190+
bytes32 nonce;
191+
}
192+
193+
struct SignedContextV1 {
194+
address signer;
195+
bytes32[] context;
196+
bytes signature;
197+
}
198+
199+
struct TakeOrderConfigV4 {
200+
OrderV4 order;
201+
uint256 inputIOIndex;
202+
uint256 outputIOIndex;
203+
SignedContextV1[] signedContext;
204+
}
205+
206+
struct TakeOrdersConfigV5 {
207+
bytes32 minimumIO;
208+
bytes32 maximumIO;
209+
bytes32 maximumIORatio;
210+
bool IOIsInput;
211+
TakeOrderConfigV4[] orders;
212+
bytes data;
213+
}

0 commit comments

Comments
 (0)