diff --git a/contracts/.solhintignore b/contracts/.solhintignore new file mode 100644 index 00000000..bd51b673 --- /dev/null +++ b/contracts/.solhintignore @@ -0,0 +1 @@ +MurkyMerkleBase.sol diff --git a/contracts/src/7683/T1ERC7683.sol b/contracts/src/7683/T1ERC7683.sol index 31447308..d45e8700 100644 --- a/contracts/src/7683/T1ERC7683.sol +++ b/contracts/src/7683/T1ERC7683.sol @@ -22,6 +22,11 @@ import { import { T1Permit2 } from "./T1Permit2.sol"; import { IT1XChainReader } from "../libraries/xChain/IT1XChainReader.sol"; +struct UserTokens { + address receiver; + address token; +} + /// @title T1ERC7683 /// @author t1 Labs contract T1ERC7683 is IT1ERC7683, T1Permit2, AccessControlUpgradeable, EIP712 { @@ -437,7 +442,61 @@ contract T1ERC7683 is IT1ERC7683, T1Permit2, AccessControlUpgradeable, EIP712 { function handleReadResultWithProof(bytes calldata encodedProofOfRead) external whenSettleNotPaused { (bytes32 requestId, bytes memory result) = xChainRead.verifyProofOfRead(encodedProofOfRead); - bytes32 orderId = settlementReadRequestToOrderId[requestId]; + (bytes32 orderId, bool isSettled, address inputToken, address settlementReceiver, uint256 amount) = + _decodeOrdersToSettle(requestId, result); + + if (isSettled) { + _transferTokenOut(inputToken, settlementReceiver, amount); + } + + emit SettlementVerified(orderId, isSettled); + } + + /// @notice Use result of proof of read to handle batch of orders depending on the result + /// Also enforce auction winner bid if the orderId has closed auction. + /// @param encodedProofsOfRead The encoded proofs of read which are formatted as following: + /// abi.encode(uint256 batchIndex, bytes32 requestId, uint256 position, bytes result, bytes proof) + function handleBatchOfReadResultsWithProofs(bytes[] calldata encodedProofsOfRead) external whenSettleNotPaused { + (bytes32[] memory requestIds, bytes[] memory results) = xChainRead.verifyProofsOfRead(encodedProofsOfRead); + + bytes32[] memory orderIds = new bytes32[](encodedProofsOfRead.length); + bool[] memory areSettled = new bool[](encodedProofsOfRead.length); + + UserTokens[] memory userTokenKeys = new UserTokens[](encodedProofsOfRead.length); + uint256[] memory amounts = new uint256[](encodedProofsOfRead.length); + uint256 uniqueUserTokenCount = 0; + + for (uint256 i = 0; i < encodedProofsOfRead.length; i++) { + (bytes32 orderId, bool isSettled, address inputToken, address settlementReceiver, uint256 amount) = + _decodeOrdersToSettle(requestIds[i], results[i]); + + orderIds[i] = orderId; + areSettled[i] = isSettled; + + if (isSettled) { + uniqueUserTokenCount = _updateOrInsertUserToken( + userTokenKeys, amounts, uniqueUserTokenCount, settlementReceiver, inputToken, amount + ); + } + } + + for (uint256 i = 0; i < userTokenKeys.length; i++) { + if (userTokenKeys[i].receiver != address(0)) { + _transferTokenOut(userTokenKeys[i].token, userTokenKeys[i].receiver, amounts[i]); + } + } + + emit SettlementBatchVerified(orderIds, areSettled); + } + + function _decodeOrdersToSettle( + bytes32 requestId, + bytes memory result + ) + internal + returns (bytes32 orderId, bool isSettled, address inputToken, address settlementReceiver, uint256 amount) + { + orderId = settlementReadRequestToOrderId[requestId]; // Ensure we have a valid order if (orderId == bytes32(0)) revert InvalidOrder(); @@ -445,7 +504,7 @@ contract T1ERC7683 is IT1ERC7683, T1Permit2, AccessControlUpgradeable, EIP712 { delete settlementReadRequestToOrderId[requestId]; // Check if the order is FILLED based on result length - bool isSettled = (result.length != 0); + isSettled = (result.length != 0); // process the settlement if verified Status status = orderStatus[orderId]; @@ -461,15 +520,47 @@ contract T1ERC7683 is IT1ERC7683, T1Permit2, AccessControlUpgradeable, EIP712 { for (uint256 i = 0; i < _orderIds.length; i++) { if (_settled) { - (, address settlementReceiver) = abi.decode(_ordersFillerData[i], (uint256, address)); - _handleSettleOrder( + (, settlementReceiver) = abi.decode(_ordersFillerData[i], (uint256, address)); + (inputToken, amount) = _handleSettleOrder( orderData.destinationDomain, orderData.destinationSettler, _orderIds[i], settlementReceiver ); } } } + } + + /// @dev Tries to increment amount to be sent if given (settlementReceiver,token) pair exists. Creates a new pair + /// otherwise + /// @param userTokenKeys Existing (settlementReceiver,token) pairs + /// @param amountsToBeSent Existing amounts to be sent for these (settlementReceiver,token) pairs + /// @param uniqueUserTokenCount Number of existing (settlementReceiver,token) pairs so far + /// @param settlementReceiver The currently processed receiver address + /// @param inputToken The currently processed token address (could be 0, which is native token) + /// @param amount The currently processed token amount + function _updateOrInsertUserToken( + UserTokens[] memory userTokenKeys, + uint256[] memory amountsToBeSent, + uint256 uniqueUserTokenCount, + address settlementReceiver, + address inputToken, + uint256 amount + ) + internal + pure + returns (uint256) + { + // try to find existing (receiver,token) pair and increment amount + for (uint256 j = 0; j < uniqueUserTokenCount; j++) { + if (userTokenKeys[j].receiver == settlementReceiver && userTokenKeys[j].token == inputToken) { + amountsToBeSent[j] += amount; + return uniqueUserTokenCount; + } + } - emit SettlementVerified(orderId, isSettled); + // create a new (receiver,token) pair otherwise + userTokenKeys[uniqueUserTokenCount] = UserTokens({ receiver: settlementReceiver, token: inputToken }); + amountsToBeSent[uniqueUserTokenCount] = amount; + return uniqueUserTokenCount + 1; } /// @dev Handles settling an individual order, should be called by the inheriting contract when receiving a setting @@ -486,6 +577,7 @@ contract T1ERC7683 is IT1ERC7683, T1Permit2, AccessControlUpgradeable, EIP712 { ) internal virtual + returns (address inputToken, uint256 amount) { (bool isEligible, OrderData memory orderData) = _checkOrderEligibility(_messageOrigin, _messageSender, _orderId); @@ -493,9 +585,8 @@ contract T1ERC7683 is IT1ERC7683, T1Permit2, AccessControlUpgradeable, EIP712 { orderStatus[_orderId] = Status.SETTLED; - address inputToken = TypeCasts.bytes32ToAddress(orderData.inputToken); - - _transferTokenOut(inputToken, settlementReceiver, orderData.amountIn); + inputToken = TypeCasts.bytes32ToAddress(orderData.inputToken); + amount = orderData.amountIn; emit Settled(_orderId, settlementReceiver); } diff --git a/contracts/src/interfaces/IT1ERC7683.sol b/contracts/src/interfaces/IT1ERC7683.sol index 5d124db7..933e73dc 100644 --- a/contracts/src/interfaces/IT1ERC7683.sol +++ b/contracts/src/interfaces/IT1ERC7683.sol @@ -60,6 +60,12 @@ interface IT1ERC7683 is IOriginSettler, IDestinationSettler { * @param isSettled Whether the order is settled */ event SettlementVerified(bytes32 indexed orderId, bool isSettled); + /** + * @notice Emitted when a batch of order settlements is verified + * @param orderIds The IDs of all verified orders + * @param areSettled Whether given orders are settled + */ + event SettlementBatchVerified(bytes32[] indexed orderIds, bool[] areSettled); /** * @notice Emitted when an order is settled. * @param orderId The ID of the settled order. @@ -142,6 +148,12 @@ interface IT1ERC7683 is IOriginSettler, IDestinationSettler { /// abi.encode(uint256 batchIndex, bytes32 requestId, uint256 position, bytes result, bytes proof) function handleReadResultWithProof(bytes calldata encodedProofOfRead) external; + /// @notice Use result of proof of read to handle batch of orders depending on the result + /// Also enforce auction winner bid if the orderId has closed auction. + /// @param encodedProofsOfRead The encoded proofs of read which are formatted as following: + /// abi.encode(uint256 batchIndex, bytes32 requestId, uint256 position, bytes result, bytes proof) + function handleBatchOfReadResultsWithProofs(bytes[] calldata encodedProofsOfRead) external; + /// @notice Refunds a batch of expired GaslessCrossChainOrders on the chain where the orders were opened. /// This process needs a proof of read triggered by `verifyRefund` that proves the intent has not be filled. /// @param _orders An array of GaslessCrossChainOrders to refund. diff --git a/contracts/src/libraries/xChain/IT1XChainReader.sol b/contracts/src/libraries/xChain/IT1XChainReader.sol index f96aa7ae..8107d9f3 100644 --- a/contracts/src/libraries/xChain/IT1XChainReader.sol +++ b/contracts/src/libraries/xChain/IT1XChainReader.sol @@ -14,6 +14,10 @@ interface IT1XChainReader { function requestRead(ReadRequest calldata request) external payable returns (bytes32 requestId); function commitProofOfReadRoot(uint256 batchIndex, bytes32 newRoot) external; function verifyProofOfRead(bytes calldata encodedProofOfRead) external view returns (bytes32, bytes memory); + function verifyProofsOfRead(bytes[] calldata encodedProofOfRead) + external + view + returns (bytes32[] memory requestIds, bytes[] memory results); function verifyProofOfReadWithResult( bytes calldata encodedProofOfRead, bytes calldata result diff --git a/contracts/src/libraries/xChain/T1XChainReader.sol b/contracts/src/libraries/xChain/T1XChainReader.sol index fe2012b7..91c98b4b 100644 --- a/contracts/src/libraries/xChain/T1XChainReader.sol +++ b/contracts/src/libraries/xChain/T1XChainReader.sol @@ -214,6 +214,33 @@ contract T1XChainReader is IT1XChainReader, OwnableUpgradeable, ReentrancyGuardU return requestId; } + /** + * @notice Verifies a batch of many proofs of read and returns the raw function results for all of them + * @param encodedProofsOfRead Array of encoded proofs of read + * @return requestIds The IDs of all read requests, in the same order + * @return results The raw ABI-encoded return values from the target function for all read requests, in the same + * order + */ + function verifyProofsOfRead(bytes[] calldata encodedProofsOfRead) + external + view + override + returns (bytes32[] memory requestIds, bytes[] memory results) + { + requestIds = new bytes32[](encodedProofsOfRead.length); + results = new bytes[](encodedProofsOfRead.length); + + for (uint256 i = 0; i < encodedProofsOfRead.length; i++) { + (uint256 batchIndex, bytes32 requestId, uint256 position, bytes memory result, bytes memory proof) = + abi.decode(encodedProofsOfRead[i], (uint256, bytes32, uint256, bytes, bytes)); + + _verifyProofOfRead(batchIndex, requestId, position, result, proof); + + requestIds[i] = requestId; + results[i] = result; + } + } + function _verifyProofOfRead( uint256 batchIndex, bytes32 requestId, diff --git a/contracts/src/test/7683/ClosedAuctionTest.sol b/contracts/src/test/7683/ClosedAuctionTest.sol index 2f1adcf5..b0b25b06 100644 --- a/contracts/src/test/7683/ClosedAuctionTest.sol +++ b/contracts/src/test/7683/ClosedAuctionTest.sol @@ -189,7 +189,7 @@ contract ClosedAuctionTest is T1XChainReaderBaseTestSetup { } function _openOrder(bool closedAuction) internal returns (OrderData memory orderData, bytes32 orderId) { - orderData = _prepareOrderData(); + orderData = _prepareOrderData(amount); orderData.closedAuction = closedAuction; OnchainCrossChainOrder memory order = _prepareOnchainOrder(OrderEncoder.encode(orderData), orderData.fillDeadline, OrderEncoder.orderDataType()); diff --git a/contracts/src/test/7683/PausableTest.t.sol b/contracts/src/test/7683/PausableTest.t.sol index dc98bfab..5e6a7ed7 100644 --- a/contracts/src/test/7683/PausableTest.t.sol +++ b/contracts/src/test/7683/PausableTest.t.sol @@ -116,7 +116,7 @@ contract PausableTest is T1XChainReaderBaseTestSetup { } function test_canOpenWhenNotPaused() public { - OrderData memory orderData = _prepareOrderData(); + OrderData memory orderData = _prepareOrderData(amount); OnchainCrossChainOrder memory order = _prepareOnchainOrder(OrderEncoder.encode(orderData), orderData.fillDeadline, OrderEncoder.orderDataType()); @@ -135,7 +135,7 @@ contract PausableTest is T1XChainReaderBaseTestSetup { inputToken.approve(permit2, type(uint256).max); uint32 openDeadline = uint32(block.timestamp + 100); - OrderData memory orderData = _prepareOrderData(); + OrderData memory orderData = _prepareOrderData(amount); GaslessCrossChainOrder memory order = _prepareGaslessOrder( address(l1T1ERC7683), kakaroto, @@ -167,7 +167,7 @@ contract PausableTest is T1XChainReaderBaseTestSetup { } function test_canSettleWhenNotPaused() public { - (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(); + (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(kakaroto, vegeta, amount); bytes memory result = abi.encode(l2T1ERC7683.getFilledOrderStatus(orderId)); (bytes32 root, bytes memory proof) = _generateMerkleTree(requestId, result, position); originReader.commitProofOfReadRoot(batchIndex, root); @@ -183,7 +183,7 @@ contract PausableTest is T1XChainReaderBaseTestSetup { function test_cannotOpenWhenPaused() public { l1T1ERC7683.pauseOpen(); - OrderData memory orderData = _prepareOrderData(); + OrderData memory orderData = _prepareOrderData(amount); OnchainCrossChainOrder memory order = _prepareOnchainOrder(OrderEncoder.encode(orderData), orderData.fillDeadline, OrderEncoder.orderDataType()); @@ -197,7 +197,7 @@ contract PausableTest is T1XChainReaderBaseTestSetup { function test_cannotOpenForWhenPaused() public { l1T1ERC7683.pauseOpen(); - OrderData memory orderData = _prepareOrderData(); + OrderData memory orderData = _prepareOrderData(amount); GaslessCrossChainOrder memory order = _prepareGaslessOrder( address(l1T1ERC7683), kakaroto, @@ -217,7 +217,7 @@ contract PausableTest is T1XChainReaderBaseTestSetup { } function test_cannotSettleWhenPaused() public { - (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(); + (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(kakaroto, vegeta, amount); bytes memory result = abi.encode(l2T1ERC7683.getFilledOrderStatus(orderId)); (bytes32 root, bytes memory proof) = _generateMerkleTree(requestId, result, position); originReader.commitProofOfReadRoot(batchIndex, root); @@ -288,7 +288,7 @@ contract PausableTest is T1XChainReaderBaseTestSetup { function test_cannotRefundForWhenPaused() public { vm.warp(2000); // Set block.timestamp to 2000 uint32 deadline = 1000; // Past deadline for testing refund - OrderData memory defaultOrderData = _prepareOrderData(); + OrderData memory defaultOrderData = _prepareOrderData(amount); defaultOrderData.fillDeadline = deadline; bytes memory orderData = OrderEncoder.encode(defaultOrderData); diff --git a/contracts/src/test/7683/SolverRepaymentBatchingTest.t.sol b/contracts/src/test/7683/SolverRepaymentBatchingTest.t.sol new file mode 100644 index 00000000..3469789b --- /dev/null +++ b/contracts/src/test/7683/SolverRepaymentBatchingTest.t.sol @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { T1MurkyMerkle } from "../utils/T1MurkyMerkle.sol"; + +import { IT1ERC7683 } from "../../../src/interfaces/IT1ERC7683.sol"; + +import { T1XChainReaderTest } from "./T1XChainReaderTest.t.sol"; + +import { console } from "forge-std/console.sol"; + +struct MerkleLeafHelper { + bytes32 orderId; + bytes32 treeLeaf; + bytes result; + bytes32 requestId; +} + +contract SolverRepaymentBatchingTest is T1XChainReaderTest { + T1MurkyMerkle internal tree = new T1MurkyMerkle(); + + uint256 internal intentCount = 20; + + function test_ERC7683BatchSolverRepayment_noBatching() public { + uint256[] memory amounts = _generateRandomArray(intentCount); + uint256 summedAmounts = 0; + + bytes32[] memory treeLeaves = new bytes32[](intentCount); + MerkleLeafHelper[] memory helpers = new MerkleLeafHelper[](intentCount); + for (uint256 i = 0; i < intentCount; i++) { + helpers[i] = _openFillOrder_and_generateMerkleLeaf(kakaroto, vegeta, amounts[i]); + treeLeaves[i] = helpers[i].treeLeaf; + summedAmounts += amounts[i]; + } + + bytes32 root = tree.getRoot(treeLeaves); + originReader.commitProofOfReadRoot(batchIndex, root); + + bytes[] memory encodedProofs = new bytes[](intentCount); + for (uint256 i = 0; i < intentCount; i++) { + bytes memory flattenedProof = _flattenProof(tree.getProof(treeLeaves, i)); + encodedProofs[i] = abi.encode(batchIndex, helpers[i].requestId, i, helpers[i].result, flattenedProof); + } + + uint256 balanceSolverBeforeSettle = inputToken.balanceOf(address(vegeta)); + + uint256 gasBefore = gasleft(); + for (uint256 i = 0; i < intentCount; i++) { + l1T1ERC7683.handleReadResultWithProof(encodedProofs[i]); + } + uint256 gasAfter = gasleft(); + console.log( + "Gas used for handling proofs in test_ERC7683BatchSolverRepayment_noBatching:", gasBefore - gasAfter + ); + + uint256 balanceSolverAfterSettle = inputToken.balanceOf(address(vegeta)); + + assertEq( + balanceSolverBeforeSettle + summedAmounts, + balanceSolverAfterSettle, + "vegeta balance increased by batch inputs amount" + ); + + for (uint256 i = 0; i < intentCount; i++) { + assertEq( + uint8(l1T1ERC7683.orderStatus(helpers[i].orderId)), + uint8(IT1ERC7683.Status.SETTLED), + "Order should be settled" + ); + } + } + + function test_ERC7683BatchSolverRepayment_sameSolver_withBatching() public { + uint256[] memory amounts = _generateRandomArray(intentCount); + uint256 summedAmounts = 0; + + bytes32[] memory treeLeaves = new bytes32[](intentCount); + MerkleLeafHelper[] memory helpers = new MerkleLeafHelper[](intentCount); + for (uint256 i = 0; i < intentCount; i++) { + helpers[i] = _openFillOrder_and_generateMerkleLeaf(kakaroto, vegeta, amounts[i]); + treeLeaves[i] = helpers[i].treeLeaf; + summedAmounts += amounts[i]; + } + + bytes32 root = tree.getRoot(treeLeaves); + originReader.commitProofOfReadRoot(batchIndex, root); + + bytes[] memory encodedProofs = new bytes[](intentCount); + for (uint256 i = 0; i < intentCount; i++) { + bytes memory flattenedProof = _flattenProof(tree.getProof(treeLeaves, i)); + encodedProofs[i] = abi.encode(batchIndex, helpers[i].requestId, i, helpers[i].result, flattenedProof); + } + + uint256 balanceSolverBeforeSettle = inputToken.balanceOf(address(vegeta)); + + uint256 gasBefore = gasleft(); + l1T1ERC7683.handleBatchOfReadResultsWithProofs(encodedProofs); + uint256 gasAfter = gasleft(); + console.log( + "Gas used for handling proofs in test_ERC7683BatchSolverRepayment_sameSolver_withBatching:", + gasBefore - gasAfter + ); + + uint256 balanceSolverAfterSettle = inputToken.balanceOf(address(vegeta)); + + assertEq( + balanceSolverBeforeSettle + summedAmounts, + balanceSolverAfterSettle, + "vegeta balance increased by batch inputs amount" + ); + + for (uint256 i = 0; i < intentCount; i++) { + assertEq( + uint8(l1T1ERC7683.orderStatus(helpers[i].orderId)), + uint8(IT1ERC7683.Status.SETTLED), + "Order should be settled" + ); + } + } + + function test_ERC7683BatchSolverRepayment_twoSolvers_withBatching() public { + uint256[] memory amounts = _generateRandomArray(intentCount); + uint256 summedAmounts = 0; + + bytes32[] memory treeLeaves = new bytes32[](intentCount); + MerkleLeafHelper[] memory helpers = new MerkleLeafHelper[](intentCount); + for (uint256 i = 0; i < intentCount; i++) { + helpers[i] = _openFillOrder_and_generateMerkleLeaf(kakaroto, i % 2 == 0 ? vegeta : karpincho, amounts[i]); + treeLeaves[i] = helpers[i].treeLeaf; + summedAmounts += amounts[i]; + } + + bytes32 root = tree.getRoot(treeLeaves); + originReader.commitProofOfReadRoot(batchIndex, root); + + bytes[] memory encodedProofs = new bytes[](intentCount); + for (uint256 i = 0; i < intentCount; i++) { + bytes memory flattenedProof = _flattenProof(tree.getProof(treeLeaves, i)); + encodedProofs[i] = abi.encode(batchIndex, helpers[i].requestId, i, helpers[i].result, flattenedProof); + } + + uint256 balanceVegetaBefore = inputToken.balanceOf(address(vegeta)); + uint256 balanceKarpinchoBefore = inputToken.balanceOf(address(karpincho)); + + uint256 gasBefore = gasleft(); + l1T1ERC7683.handleBatchOfReadResultsWithProofs(encodedProofs); + uint256 gasAfter = gasleft(); + console.log( + "Gas used for handling proofs in test_ERC7683BatchSolverRepayment_twoSolvers_withBatching:", + gasBefore - gasAfter + ); + + uint256 balanceVegetaAfter = inputToken.balanceOf(address(vegeta)); + uint256 balanceKarpinchoAfter = inputToken.balanceOf(address(karpincho)); + + assertEq( + balanceVegetaBefore + balanceKarpinchoBefore + summedAmounts, + balanceVegetaAfter + balanceKarpinchoAfter, + "vegeta/karpincho balance increased by batch inputs amount" + ); + + for (uint256 i = 0; i < intentCount; i++) { + assertEq( + uint8(l1T1ERC7683.orderStatus(helpers[i].orderId)), + uint8(IT1ERC7683.Status.SETTLED), + "Order should be settled" + ); + } + } + + function _openFillOrder_and_generateMerkleLeaf( + address opener, + address solver, + uint256 amount + ) + internal + returns (MerkleLeafHelper memory merkleLeafHelper) + { + (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(opener, solver, amount); + bytes memory result = abi.encode(l2T1ERC7683.getFilledOrderStatus(orderId)); + bytes32 xChainReadResultHash = keccak256(result); + bytes32 treeLeaf = keccak256(abi.encodePacked(xChainReadResultHash, requestId)); + merkleLeafHelper = + MerkleLeafHelper({ orderId: orderId, treeLeaf: treeLeaf, result: result, requestId: requestId }); + } + + function _flattenProof(bytes32[] memory proof) internal pure returns (bytes memory proofBytes) { + proofBytes = new bytes(proof.length * 32); + for (uint256 i = 0; i < proof.length; i++) { + assembly { + // store each 32-byte element at the correct offset + mstore(add(proofBytes, add(32, mul(i, 32))), mload(add(proof, add(32, mul(i, 32))))) + } + } + } + + function _generateRandomArray(uint256 length) internal view returns (uint256[] memory arr) { + arr = new uint256[](length); + for (uint256 i = 0; i < length; i++) { + arr[i] = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender, i))) % 1000; + } + } +} diff --git a/contracts/src/test/7683/T1XChainReaderBaseTestSetup.sol b/contracts/src/test/7683/T1XChainReaderBaseTestSetup.sol index 674a102f..53f5403a 100644 --- a/contracts/src/test/7683/T1XChainReaderBaseTestSetup.sol +++ b/contracts/src/test/7683/T1XChainReaderBaseTestSetup.sol @@ -31,6 +31,7 @@ contract T1XChainReaderBaseTestSetup is BaseTest { address internal owner = makeAddr("owner"); address internal sender = makeAddr("sender"); address internal feeVault; + uint256 internal senderNonce = 1; function labelAccounts() internal { vm.label(owner, "Owner"); @@ -54,13 +55,21 @@ contract T1XChainReaderBaseTestSetup is BaseTest { receive() external payable { } - function _openAndFillOrder() internal virtual returns (OrderData memory, bytes32 orderId, bytes32 requestId) { - OrderData memory orderData = _prepareOrderData(); + function _openAndFillOrder( + address opener, + address filler, + uint256 amountIn + ) + internal + virtual + returns (OrderData memory, bytes32 orderId, bytes32 requestId) + { + OrderData memory orderData = _prepareOrderData(amountIn); OnchainCrossChainOrder memory order = _prepareOnchainOrder(OrderEncoder.encode(orderData), orderData.fillDeadline, OrderEncoder.orderDataType()); - vm.startPrank(kakaroto); - inputToken.approve(address(l1T1ERC7683), amount); + vm.startPrank(opener); + inputToken.approve(address(l1T1ERC7683), amountIn); vm.recordLogs(); l1T1ERC7683.open(order); vm.stopPrank(); @@ -68,30 +77,30 @@ contract T1XChainReaderBaseTestSetup is BaseTest { (bytes32 orderId_,) = _getOrderIDFromLogs(); assertEq(uint8(l1T1ERC7683.orderStatus(orderId_)), uint8(IT1ERC7683.Status.OPENED)); - vm.startPrank(vegeta); - outputToken.approve(address(l2T1ERC7683), amount); + vm.startPrank(filler); + outputToken.approve(address(l2T1ERC7683), amountIn); bytes memory originData = OrderEncoder.encode(orderData); - bytes memory fillerData = abi.encode(amount, TypeCasts.addressToBytes32(vegeta)); + bytes memory fillerData = abi.encode(amountIn, TypeCasts.addressToBytes32(filler)); l2T1ERC7683.fill(orderId_, originData, fillerData); assertEq(uint8(l2T1ERC7683.orderStatus(orderId_)), uint8(IT1ERC7683.Status.FILLED)); vm.stopPrank(); - vm.startPrank(vegeta); + vm.startPrank(filler); bytes32 requestId_ = l1T1ERC7683.verifySettlement(destination, orderId_); vm.stopPrank(); return (orderData, orderId_, requestId_); } - function _prepareOrderData() internal view virtual returns (OrderData memory) { + function _prepareOrderData(uint256 amountIn) internal virtual returns (OrderData memory) { return OrderData({ sender: TypeCasts.addressToBytes32(kakaroto), recipient: TypeCasts.addressToBytes32(karpincho), inputToken: TypeCasts.addressToBytes32(address(inputToken)), outputToken: TypeCasts.addressToBytes32(address(outputToken)), - amountIn: amount, - minAmountOut: amount, - senderNonce: 1, + amountIn: amountIn, + minAmountOut: amountIn, + senderNonce: senderNonce++, originDomain: origin, destinationDomain: destination, destinationSettler: address(l2T1ERC7683).addressToBytes32(), diff --git a/contracts/src/test/7683/T1XChainReaderTest.t.sol b/contracts/src/test/7683/T1XChainReaderTest.t.sol index 01fa9c66..62f3f646 100644 --- a/contracts/src/test/7683/T1XChainReaderTest.t.sol +++ b/contracts/src/test/7683/T1XChainReaderTest.t.sol @@ -56,7 +56,7 @@ contract T1XChainReaderTest is T1XChainReaderBaseTestSetup { // writes the new merkle root for the target batch // 5. Solver calls handleReadResultWithProof on 7683 contract with merkle proof, settles intent and releases funds function test_ERC7683SettlementFlow() public { - (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(); + (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(kakaroto, vegeta, amount); // 4. Process the read request on L2 (destination chain) & Relay the result back to L1 { @@ -83,7 +83,7 @@ contract T1XChainReaderTest is T1XChainReaderBaseTestSetup { function test_ERC7683SettlementFlowWithAnotherTreePosition() public { position = 3; - (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(); + (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(kakaroto, vegeta, amount); // 4. Process the read request on L2 (destination chain) & Relay the result back to L1 { @@ -107,7 +107,7 @@ contract T1XChainReaderTest is T1XChainReaderBaseTestSetup { } function test_shouldFillWithAmountOutHigherThanLimit() public { - OrderData memory orderData = _prepareOrderData(); + OrderData memory orderData = _prepareOrderData(amount); OnchainCrossChainOrder memory order = _prepareOnchainOrder(OrderEncoder.encode(orderData), orderData.fillDeadline, OrderEncoder.orderDataType()); @@ -131,7 +131,7 @@ contract T1XChainReaderTest is T1XChainReaderBaseTestSetup { } function test_revertFillWithAmountOutLowerThanLimit() public { - OrderData memory orderData = _prepareOrderData(); + OrderData memory orderData = _prepareOrderData(amount); OnchainCrossChainOrder memory order = _prepareOnchainOrder(OrderEncoder.encode(orderData), orderData.fillDeadline, OrderEncoder.orderDataType()); @@ -174,7 +174,7 @@ contract T1XChainReaderTest is T1XChainReaderBaseTestSetup { } function test_revertWithInvalidProofData() public { - (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(); + (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(kakaroto, vegeta, amount); bytes memory result = abi.encode(l2T1ERC7683.getFilledOrderStatus(orderId)); (bytes32 root,) = _generateMerkleTree(requestId, result, position); @@ -187,7 +187,7 @@ contract T1XChainReaderTest is T1XChainReaderBaseTestSetup { } function test_revertWithInvalidProof() public { - (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(); + (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(kakaroto, vegeta, amount); bytes memory result = abi.encode(l2T1ERC7683.getFilledOrderStatus(orderId)); (bytes32 root,) = _generateMerkleTree(requestId, result, position); @@ -204,7 +204,7 @@ contract T1XChainReaderTest is T1XChainReaderBaseTestSetup { } function test_revertWithInvalidResultData() public { - (,, bytes32 requestId) = _openAndFillOrder(); + (,, bytes32 requestId) = _openAndFillOrder(kakaroto, vegeta, amount); // 4. First, set up the proof root by calling handle on the reader @@ -222,7 +222,7 @@ contract T1XChainReaderTest is T1XChainReaderBaseTestSetup { } function test_sameProofShouldNotSettleTwice() public { - (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(); + (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(kakaroto, vegeta, amount); bytes memory result = abi.encode(l2T1ERC7683.getFilledOrderStatus(orderId)); (bytes32 root, bytes memory proof) = _generateMerkleTree(requestId, result, position); @@ -238,7 +238,7 @@ contract T1XChainReaderTest is T1XChainReaderBaseTestSetup { } function test_settlementIfStatusIsRefundRequested() public { - (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(); + (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(kakaroto, vegeta, amount); bytes memory result = abi.encode(l2T1ERC7683.getFilledOrderStatus(orderId)); (bytes32 root, bytes memory proof) = _generateMerkleTree(requestId, result, position); @@ -266,7 +266,7 @@ contract T1XChainReaderTest is T1XChainReaderBaseTestSetup { } function test_settlementIfReadRequestedTwice() public { - (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(); + (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(kakaroto, vegeta, amount); bytes memory result = abi.encode(l2T1ERC7683.getFilledOrderStatus(orderId)); (bytes32 root, bytes memory proof) = _generateMerkleTree(requestId, result, position); @@ -291,7 +291,7 @@ contract T1XChainReaderTest is T1XChainReaderBaseTestSetup { } function test_settlementWithEmptyResultData() public { - (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(); + (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(kakaroto, vegeta, amount); // 4. First, set up the proof root by calling handle on the reader @@ -562,7 +562,7 @@ contract T1XChainReaderTest is T1XChainReaderBaseTestSetup { } function test_verifyProofOfReadWithResult() public { - (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(); + (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(kakaroto, vegeta, amount); bytes memory result = abi.encode(l2T1ERC7683.getFilledOrderStatus(orderId)); (bytes32 root, bytes memory proof) = _generateMerkleTree(requestId, result, position); @@ -576,7 +576,7 @@ contract T1XChainReaderTest is T1XChainReaderBaseTestSetup { } function test_verifyProofOfReadWithResult_InvalidProof() public { - (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(); + (, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(kakaroto, vegeta, amount); bytes memory result = abi.encode(l2T1ERC7683.getFilledOrderStatus(orderId)); (bytes32 root,) = _generateMerkleTree(requestId, result, position); diff --git a/contracts/src/test/utils/MurkyMerkleBase.sol b/contracts/src/test/utils/MurkyMerkleBase.sol new file mode 100644 index 00000000..571b1a60 --- /dev/null +++ b/contracts/src/test/utils/MurkyMerkleBase.sol @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +abstract contract MurkyMerkleBase { + /** + * + * CONSTRUCTOR * + * + */ + constructor() { } + + /** + * + * VIRTUAL HASHING FUNCTIONS * + * + */ + function hashLeafPairs(bytes32 left, bytes32 right) public pure virtual returns (bytes32 _hash); + + /** + * + * PROOF VERIFICATION * + * + */ + function verifyProof( + bytes32 root, + bytes32[] memory proof, + bytes32 valueToProve + ) + external + pure + virtual + returns (bool) + { + // proof length must be less than max array size + bytes32 rollingHash = valueToProve; + uint256 length = proof.length; + unchecked { + for (uint256 i = 0; i < length; ++i) { + rollingHash = hashLeafPairs(rollingHash, proof[i]); + } + } + return root == rollingHash; + } + + /** + * + * PROOF GENERATION * + * + */ + function getRoot(bytes32[] memory data) public pure virtual returns (bytes32) { + require(data.length > 1, "won't generate root for single leaf"); + while (data.length > 1) { + data = hashLevel(data); + } + return data[0]; + } + + function getProof(bytes32[] memory data, uint256 node) public pure virtual returns (bytes32[] memory) { + require(data.length > 1, "won't generate proof for single leaf"); + // The size of the proof is equal to the ceiling of log2(numLeaves) + bytes32[] memory result = new bytes32[](log2ceilBitMagic(data.length)); + uint256 pos = 0; + + // Two overflow risks: node, pos + // node: max array size is 2**256-1. Largest index in the array will be 1 less than that. Also, + // for dynamic arrays, size is limited to 2**64-1 + // pos: pos is bounded by log2(data.length), which should be less than type(uint256).max + while (data.length > 1) { + unchecked { + if (node & 0x1 == 1) { + result[pos] = data[node - 1]; + } else if (node + 1 == data.length) { + result[pos] = bytes32(0); + } else { + result[pos] = data[node + 1]; + } + ++pos; + node /= 2; + } + data = hashLevel(data); + } + return result; + } + + ///@dev function is private to prevent unsafe data from being passed + function hashLevel(bytes32[] memory data) private pure returns (bytes32[] memory) { + bytes32[] memory result; + + // Function is private, and all internal callers check that data.length >=2. + // Underflow is not possible as lowest possible value for data/result index is 1 + // overflow should be safe as length is / 2 always. + unchecked { + uint256 length = data.length; + if (length & 0x1 == 1) { + result = new bytes32[](length / 2 + 1); + result[result.length - 1] = hashLeafPairs(data[length - 1], bytes32(0)); + } else { + result = new bytes32[](length / 2); + } + // pos is upper bounded by data.length / 2, so safe even if array is at max size + uint256 pos = 0; + for (uint256 i = 0; i < length - 1; i += 2) { + result[pos] = hashLeafPairs(data[i], data[i + 1]); + ++pos; + } + } + return result; + } + + /** + * + * MATH "LIBRARY" * + * + */ + + /// @dev Note that x is assumed > 0 + function log2ceil(uint256 x) public pure returns (uint256) { + uint256 ceil = 0; + uint256 pOf2; + // If x is a power of 2, then this function will return a ceiling + // that is 1 greater than the actual ceiling. So we need to check if + // x is a power of 2, and subtract one from ceil if so. + assembly { + // we check by seeing if x == (~x + 1) & x. This applies a mask + // to find the lowest set bit of x and then checks it for equality + // with x. If they are equal, then x is a power of 2. + + /* Example + x has single bit set + x := 0000_1000 + (~x + 1) = (1111_0111) + 1 = 1111_1000 + (1111_1000 & 0000_1000) = 0000_1000 == x + + x has multiple bits set + x := 1001_0010 + (~x + 1) = (0110_1101 + 1) = 0110_1110 + (0110_1110 & x) = 0000_0010 != x + */ + + // we do some assembly magic to treat the bool as an integer later on + pOf2 := eq(and(add(not(x), 1), x), x) + } + + // if x == type(uint256).max, than ceil is capped at 256 + // if x == 0, then pO2 == 0, so ceil won't underflow + unchecked { + while (x > 0) { + x >>= 1; + ceil++; + } + ceil -= pOf2; // see above + } + return ceil; + } + + /// Original bitmagic adapted from https://github.com/paulrberg/prb-math/blob/main/contracts/PRBMath.sol + /// @dev Note that x assumed > 1 + function log2ceilBitMagic(uint256 x) public pure returns (uint256) { + if (x <= 1) { + return 0; + } + uint256 msb = 0; + uint256 _x = x; + if (x >= 2 ** 128) { + x >>= 128; + msb += 128; + } + if (x >= 2 ** 64) { + x >>= 64; + msb += 64; + } + if (x >= 2 ** 32) { + x >>= 32; + msb += 32; + } + if (x >= 2 ** 16) { + x >>= 16; + msb += 16; + } + if (x >= 2 ** 8) { + x >>= 8; + msb += 8; + } + if (x >= 2 ** 4) { + x >>= 4; + msb += 4; + } + if (x >= 2 ** 2) { + x >>= 2; + msb += 2; + } + if (x >= 2 ** 1) { + msb += 1; + } + + uint256 lsb = (~_x + 1) & _x; + if ((lsb == _x) && (msb > 0)) { + return msb; + } else { + return msb + 1; + } + } +} diff --git a/contracts/src/test/utils/T1MurkyMerkle.sol b/contracts/src/test/utils/T1MurkyMerkle.sol new file mode 100644 index 00000000..8862fb81 --- /dev/null +++ b/contracts/src/test/utils/T1MurkyMerkle.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { MurkyMerkleBase } from "./MurkyMerkleBase.sol"; + +/// @notice Nascent, simple, kinda efficient (and improving!) Merkle proof generator and verifier +/// @author dmfxyz +/// @dev Note Generic Merkle Tree +contract T1MurkyMerkle is MurkyMerkleBase { + /** + * + * HASHING FUNCTION * + * + */ + + /// ascending sort and concat prior to hashing + function hashLeafPairs(bytes32 left, bytes32 right) public pure override returns (bytes32 _hash) { + assembly { + mstore(0x00, left) + mstore(0x20, right) + _hash := keccak256(0x00, 0x40) + } + } +}