|
| 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