From 93db953c620f4d52e8852ff8148f2dfdbc580029 Mon Sep 17 00:00:00 2001 From: adrian Date: Fri, 15 Aug 2025 10:28:12 +0200 Subject: [PATCH] feat: implement `SwapEncoder` for `Hashflow` --- Cargo.toml | 17 +- config/executor_addresses.json | 3 +- config/protocol_specific_addresses.json | 5 +- config/test_executor_addresses.json | 3 +- foundry/src/executors/HashflowExecutor.sol | 22 +- foundry/test/Constants.sol | 3 + foundry/test/TychoRouterSequentialSwap.t.sol | 33 ++ foundry/test/TychoRouterSingleSwap.t.sol | 33 ++ foundry/test/TychoRouterTestSetup.sol | 32 +- foundry/test/assets/calldata.txt | 2 + foundry/test/protocols/Hashflow.t.sol | 10 +- src/encoding/evm/approvals/permit2.rs | 4 - .../approvals/protocol_approvals_manager.rs | 4 - src/encoding/evm/swap_encoder/builder.rs | 9 +- .../evm/swap_encoder/swap_encoders.rs | 296 +++++++++++++++++- src/encoding/evm/testing_utils.rs | 6 +- src/encoding/evm/utils.rs | 3 + tests/common/mod.rs | 4 + .../optimized_transfers_integration_tests.rs | 234 +++++++++++++- 19 files changed, 667 insertions(+), 56 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4783f4d..986110d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,12 +11,12 @@ license = "MIT" categories = ["finance", "cryptography::cryptocurrencies"] readme = "README.md" exclude = [ - "foundry/*", - "foundry", - "tests/*", - "tests/common", - ".github/*", - ".gitmodules", + "foundry/*", + "foundry", + "tests/*", + "tests/common", + ".github/*", + ".gitmodules", ] [[bin]] @@ -37,7 +37,8 @@ tokio = { version = "1.38.0", features = ["full"] } chrono = "0.4.39" clap = { version = "4.5.3", features = ["derive"] } once_cell = "1.20.2" -tycho-common = ">0.81.5" +tycho-common = ">=0.81.6" + alloy = { version = "1.0.6", features = ["providers", "rpc-types-eth", "eip712", "signer-local", "node-bindings"], optional = true } async-trait = { version = "0.1.88", optional = true } @@ -52,4 +53,4 @@ fork-tests = [] test-utils = ["async-trait"] [profile.bench] -debug = true \ No newline at end of file +debug = true diff --git a/config/executor_addresses.json b/config/executor_addresses.json index f1459d6..ebfa862 100644 --- a/config/executor_addresses.json +++ b/config/executor_addresses.json @@ -11,7 +11,8 @@ "vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165", "vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B", "vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec", - "rfq:bebop": "0xFE42BFb115eD9671011cA52BDD23A52A2e077a7c" + "rfq:bebop": "0xFE42BFb115eD9671011cA52BDD23A52A2e077a7c", + "rfq:hashflow": "0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f" }, "base": { "uniswap_v2": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700", diff --git a/config/protocol_specific_addresses.json b/config/protocol_specific_addresses.json index c2f2fd2..f4cbe21 100644 --- a/config/protocol_specific_addresses.json +++ b/config/protocol_specific_addresses.json @@ -9,6 +9,9 @@ "rfq:bebop": { "bebop_settlement_address": "0xbbbbbBB520d69a9775E85b458C58c648259FAD5F", "native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + }, + "rfq:hashflow": { + "hashflow_router_address": "0x55084eE0fEf03f14a305cd24286359A35D735151" } }, "base": { @@ -18,4 +21,4 @@ } }, "unichain": {} -} \ No newline at end of file +} diff --git a/config/test_executor_addresses.json b/config/test_executor_addresses.json index 9a2f628..f0b5d8e 100644 --- a/config/test_executor_addresses.json +++ b/config/test_executor_addresses.json @@ -11,6 +11,7 @@ "vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211", "vm:maverick_v2": "0xA4AD4f68d0b91CFD19687c881e50f3A00242828c", "vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb", - "rfq:bebop": "0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF" + "rfq:bebop": "0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF", + "rfq:hashflow": "0x15cF58144EF33af1e14b5208015d11F9143E27b9" } } diff --git a/foundry/src/executors/HashflowExecutor.sol b/foundry/src/executors/HashflowExecutor.sol index 0e3b93b..1ee9847 100644 --- a/foundry/src/executors/HashflowExecutor.sol +++ b/foundry/src/executors/HashflowExecutor.sol @@ -32,12 +32,20 @@ interface IHashflowRouter { contract HashflowExecutor is IExecutor, RestrictTransferFrom { using SafeERC20 for IERC20; - address public constant HASHFLOW_ROUTER = - 0x55084eE0fEf03f14a305cd24286359A35D735151; address public constant NATIVE_TOKEN = 0x0000000000000000000000000000000000000000; - constructor(address _permit2) RestrictTransferFrom(_permit2) {} + /// @notice The Hashflow router address + address public immutable hashflowRouter; + + constructor(address _hashflowRouter, address _permit2) + RestrictTransferFrom(_permit2) + { + if (_hashflowRouter == address(0)) { + revert HashflowExecutor__InvalidHashflowRouter(); + } + hashflowRouter = _hashflowRouter; + } function swap(uint256 givenAmount, bytes calldata data) external @@ -60,7 +68,7 @@ contract HashflowExecutor is IExecutor, RestrictTransferFrom { if (approvalNeeded && quote.baseToken != NATIVE_TOKEN) { // slither-disable-next-line unused-return IERC20(quote.baseToken).forceApprove( - HASHFLOW_ROUTER, type(uint256).max + hashflowRouter, type(uint256).max ); } @@ -72,7 +80,7 @@ contract HashflowExecutor is IExecutor, RestrictTransferFrom { address(this), transferType, address(quote.baseToken), givenAmount ); uint256 balanceBefore = _balanceOf(quote.quoteToken); - IHashflowRouter(HASHFLOW_ROUTER).tradeRFQT{value: ethValue}(quote); + IHashflowRouter(hashflowRouter).tradeRFQT{value: ethValue}(quote); uint256 balanceAfter = _balanceOf(quote.quoteToken); calculatedAmount = balanceAfter - balanceBefore; } @@ -90,8 +98,8 @@ contract HashflowExecutor is IExecutor, RestrictTransferFrom { revert HashflowExecutor__InvalidDataLength(); } - approvalNeeded = data[0] != 0; - transferType = TransferType(uint8(data[1])); + transferType = TransferType(uint8(data[0])); + approvalNeeded = data[1] != 0; quote.pool = address(bytes20(data[2:22])); quote.externalAccount = address(bytes20(data[22:42])); diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index 5d3ea09..f078f36 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -128,6 +128,9 @@ contract Constants is Test, BaseConstants { // Bebop Settlement address BEBOP_SETTLEMENT = 0xbbbbbBB520d69a9775E85b458C58c648259FAD5F; + // Hashflow Router + address HASHFLOW_ROUTER = 0x55084eE0fEf03f14a305cd24286359A35D735151; + // Pool Code Init Hashes bytes32 USV2_POOL_CODE_INIT_HASH = 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f; diff --git a/foundry/test/TychoRouterSequentialSwap.t.sol b/foundry/test/TychoRouterSequentialSwap.t.sol index f22158e..3614494 100644 --- a/foundry/test/TychoRouterSequentialSwap.t.sol +++ b/foundry/test/TychoRouterSequentialSwap.t.sol @@ -547,3 +547,36 @@ contract TychoRouterSequentialSwapTestForBebop is TychoRouterTestSetup { assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } } + +contract TychoRouterSequentialSwapTestForHashflow is TychoRouterTestSetup { + function getForkBlock() public pure override returns (uint256) { + return 23167288; + } + + function testUSV3HashflowIntegration() public { + // Performs a sequential swap from WETH to WBTC through USDC using USV3 and Hashflow RFQ + // + // WETH ──(USV3)──> USDC ───(Hashflow RFQ)──> WBTC + + // The Hashflow order expects: + // - 4308094737 USDC input -> 3738288 WBTC output + + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 3738288; + deal(WETH_ADDR, ALICE, amountIn); + uint256 balanceBefore = IERC20(WBTC_ADDR).balanceOf(ALICE); + + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max); + bytes memory callData = loadCallDataFromFile("test_uniswap_v3_hashflow"); + (bool success,) = tychoRouterAddr.call(callData); + + vm.stopPrank(); + + uint256 balanceAfter = IERC20(WBTC_ADDR).balanceOf(ALICE); + + assertTrue(success, "Call Failed"); + assertEq(balanceAfter - balanceBefore, expectedAmountOut); + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); + } +} diff --git a/foundry/test/TychoRouterSingleSwap.t.sol b/foundry/test/TychoRouterSingleSwap.t.sol index fdc1410..8e3bb0e 100644 --- a/foundry/test/TychoRouterSingleSwap.t.sol +++ b/foundry/test/TychoRouterSingleSwap.t.sol @@ -426,3 +426,36 @@ contract TychoRouterSingleSwapTest is TychoRouterTestSetup { assertEq(balanceAfter - balanceBefore, 2018817438608734439722); } } + +contract TychoRouterSingleSwapTestForHashflow is TychoRouterTestSetup { + function getForkBlock() public pure override returns (uint256) { + return 23175437; + } + + function testUSV3HashflowIntegration() public { + // Performs a swap from USDC to WBTC using Hashflow RFQ + // + // USDC ───(Hashflow RFQ)──> WBTC + + // The Hashflow order expects: + // - 4308094737 USDC input -> 3714751 WBTC output + + uint256 amountIn = 4308094737; + uint256 expectedAmountOut = 3714751; + deal(USDC_ADDR, ALICE, amountIn); + uint256 balanceBefore = IERC20(WBTC_ADDR).balanceOf(ALICE); + + vm.startPrank(ALICE); + IERC20(USDC_ADDR).approve(tychoRouterAddr, type(uint256).max); + bytes memory callData = loadCallDataFromFile("test_hashflow"); + (bool success,) = tychoRouterAddr.call(callData); + + vm.stopPrank(); + + uint256 balanceAfter = IERC20(WBTC_ADDR).balanceOf(ALICE); + + assertTrue(success, "Call Failed"); + assertEq(balanceAfter - balanceBefore, expectedAmountOut); + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); + } +} diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index ec1c0b1..006ef7a 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -2,27 +2,28 @@ pragma solidity ^0.8.26; // Executors +import "../src/executors/HashflowExecutor.sol"; +import "./Constants.sol"; +import "./TestUtils.sol"; +import "@src/TychoRouter.sol"; +import { +UniswapV3Executor, +IUniswapV3Pool +} from "../src/executors/UniswapV3Executor.sol"; import {BalancerV2Executor} from "../src/executors/BalancerV2Executor.sol"; import {BalancerV3Executor} from "../src/executors/BalancerV3Executor.sol"; import {BebopExecutor} from "../src/executors/BebopExecutor.sol"; import {CurveExecutor} from "../src/executors/CurveExecutor.sol"; -import {EkuboExecutor} from "../src/executors/EkuboExecutor.sol"; -import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol"; -import {UniswapV2Executor} from "../src/executors/UniswapV2Executor.sol"; -import { - UniswapV3Executor, - IUniswapV3Pool -} from "../src/executors/UniswapV3Executor.sol"; -import {UniswapV4Executor} from "../src/executors/UniswapV4Executor.sol"; // Test utilities and mocks -import "./Constants.sol"; -import "./TestUtils.sol"; -import {Permit2TestHelper} from "./Permit2TestHelper.sol"; +import {EkuboExecutor} from "../src/executors/EkuboExecutor.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol"; // Core contracts and interfaces -import "@src/TychoRouter.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Permit2TestHelper} from "./Permit2TestHelper.sol"; +import {UniswapV2Executor} from "../src/executors/UniswapV2Executor.sol"; +import {UniswapV4Executor} from "../src/executors/UniswapV4Executor.sol"; contract TychoRouterExposed is TychoRouter { constructor(address _permit2, address weth) TychoRouter(_permit2, weth) {} @@ -75,6 +76,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { MaverickV2Executor public maverickv2Executor; BalancerV3Executor public balancerV3Executor; BebopExecutor public bebopExecutor; + HashflowExecutor public hashflowExecutor; function getForkBlock() public view virtual returns (uint256) { return 22082754; @@ -135,8 +137,9 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS); balancerV3Executor = new BalancerV3Executor(PERMIT2_ADDRESS); bebopExecutor = new BebopExecutor(BEBOP_SETTLEMENT, PERMIT2_ADDRESS); + hashflowExecutor = new HashflowExecutor(HASHFLOW_ROUTER, PERMIT2_ADDRESS); - address[] memory executors = new address[](10); + address[] memory executors = new address[](11); executors[0] = address(usv2Executor); executors[1] = address(usv3Executor); executors[2] = address(pancakev3Executor); @@ -147,6 +150,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { executors[7] = address(maverickv2Executor); executors[8] = address(balancerV3Executor); executors[9] = address(bebopExecutor); + executors[10] = address(hashflowExecutor); return executors; } diff --git a/foundry/test/assets/calldata.txt b/foundry/test/assets/calldata.txt index ea38f49..e83503f 100644 --- a/foundry/test/assets/calldata.txt +++ b/foundry/test/assets/calldata.txt @@ -38,3 +38,5 @@ test_single_encoding_strategy_bebop_aggregate:5c4b639c00000000000000000000000000 test_single_encoding_strategy_bebop:5c4b639c000000000000000000000000000000000000000000000000000000000bebc200000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be300000000000000000000000000000000000000000000000a8aea46aa4ec5c0f500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a10000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000002d7d6bbde9174b1cdaa358d2cf4d57d1a9f7178fbffa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48faba6f8e4a5e8ab82f62fe7c39859fa577269be3000c000000000000000000000000000000000000000000000000000000000bebc20001d2068e04cf586f76eece7ba5beb779d7bb1474a14dcebcba00000000000000000000000000000000000000000000000000000000689b548f0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000067336cec42645f55059eff241cb02ea5cc52ff86000000000000000000000000000000000000000000000000279ead5d9685f25b000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be3000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000a8aea46aa4ec5c0f5000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000000005230bcb979c81cebf94a3b5c08bcfa300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000414ce40058ff07f11d9224c2c8d1e58369e4a90173856202d8d2a17da48058ad683dedb742eda0d4c0cf04cf1c09138898dd7fd06f97268ea7f74ef9b42d29bf4c1b00000000000000000000000000000000000000000000000000000000000000000000000000000000 test_sequential_swap_strategy_encoder_unwrap:51bcc7b600000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000068c5498700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000689dc38f00000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041192fc75d79a3d76bcf3c3d193cf769446abc98ff76ce2a1de183e7e46d80073836cd6ceedc30f98085188eab1098ca2f0ef03c25ebaa69cd2758988263e563c41b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a800525615deb798bb3e4dfa0139dfa1b3d433cc23b72fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48004375dff511095cc5a197a54140a24efef3a416bb2b8038a1640196fbe3e38816f3e67cba72d940000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d013950102000000000000000000000000000000000000000000000000 test_sequential_swap_usx:0101e21dd0d300000000000000000000000000000000000000000000006c6b935b8bbd4000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000769cfd80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006d9da78b6a5bedca287aa5d49613ba36b90c15c40000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470b6b175474e89094c44da98b954eedeac495271d0fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000643ede3eca2a72b3aecc820e955b36f38437d013955777d92f208679db4b9778590fa3cab3ac9e2168010000692e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48dac17f958d2ee523a2206206994597c13d831ec70000646d9da78b6a5bedca287aa5d49613ba36b90c15c43416cf6c708da44db2624d63ea0aaef7113527c6010100000000000000000000 +test_uniswap_v3_hashflow:e21dd0d30000000000000000000000000000000000000000000000000000000077359400000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000000000000000000000000000000000007735940000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000014400692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f5640000000d75615deb798bb3e4dfa0139dfa1b3d433cc23b72f0201031903307c517c11b71f8313d19afde0a4f41cb55615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000198bcad59fd125000064000640000001747937188ffffffffffffff00295e467232b36d0000fda99100ffd8adfc818a827e1698c1d1fa2f59f7723ff84bfeba0f80e5298b1077f590d8d99aec6f6801c611eb270b5d89fac02a680ab38f03f3c5d16039c6f11c0000000068a2fd62bb289bc97591f70d8216462df40ed713011b968acd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000 +test_hashflow:5c4b639c0000000000000000000000000000000000000000000000000000000100c84f11000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000000000000000000000000000000000000038aebf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016f15cf58144ef33af1e14b5208015d11f9143e27b90001478eca1b93865dca0b9f325935eb123c8a4af011bee3211ab312a8d065c4fef0247448e17a8da000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb482260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000000000100c84f11000000000000000000000000000000000000000000000000000000000038aebf0000000000000000000000000000000000000000000000000000000068a47cd800000000000000000000000000000000000000000000000000000198c286fecb125000064000640000001747eb8c38ffffffffffffff0029642016edb36d00006ddb3b21fe8509e274ddf46c55209cdbf30360944abbca6569ed6b26740d052f419964dcb5a3bdb98b4ed1fb3642a2760b8312118599a962251f7a8f73fe4fbe1c0000000000000000000000000000000000 diff --git a/foundry/test/protocols/Hashflow.t.sol b/foundry/test/protocols/Hashflow.t.sol index 73de369..2ed8eca 100644 --- a/foundry/test/protocols/Hashflow.t.sol +++ b/foundry/test/protocols/Hashflow.t.sol @@ -14,8 +14,8 @@ contract HashflowUtils is Test { RestrictTransferFrom.TransferType transferType ) internal pure returns (bytes memory) { return abi.encodePacked( - approvalNeeded, // needsApproval (1 byte) uint8(transferType), // transferType (1 byte) + approvalNeeded, // needsApproval (1 byte) quote.pool, // pool (20 bytes) quote.externalAccount, // externalAccount (20 bytes) quote.trader, // trader (20 bytes) @@ -53,7 +53,7 @@ contract HashflowExecutorECR20Test is Constants, HashflowUtils { function setUp() public { forkBlock = 23124977; // Using expiry date: 1755001853, ECR20 vm.createSelectFork("mainnet", forkBlock); - executor = new HashflowExecutorExposed(PERMIT2_ADDRESS); + executor = new HashflowExecutorExposed(HASHFLOW_ROUTER, PERMIT2_ADDRESS); } function testDecodeParams() public view { @@ -215,7 +215,7 @@ contract HashflowExecutorNativeTest is Constants, HashflowUtils { function setUp() public { forkBlock = 23125321; // Using expiry date: 1755006017, Native vm.createSelectFork("mainnet", forkBlock); - executor = new HashflowExecutorExposed(PERMIT2_ADDRESS); + executor = new HashflowExecutorExposed(HASHFLOW_ROUTER, PERMIT2_ADDRESS); } function testSwapNoSlippage() public { @@ -264,7 +264,9 @@ contract HashflowExecutorNativeTest is Constants, HashflowUtils { } contract HashflowExecutorExposed is HashflowExecutor { - constructor(address _permit2) HashflowExecutor(_permit2) {} + constructor(address _hashflowRouter, address _permit2) + HashflowExecutor(_hashflowRouter, _permit2) + {} function decodeData(bytes calldata data) external diff --git a/src/encoding/evm/approvals/permit2.rs b/src/encoding/evm/approvals/permit2.rs index 83610ce..53306b0 100644 --- a/src/encoding/evm/approvals/permit2.rs +++ b/src/encoding/evm/approvals/permit2.rs @@ -31,10 +31,6 @@ pub struct Permit2 { address: Address, client: EVMProvider, runtime_handle: Handle, - // Store the runtime to prevent it from being dropped before use. - // This is required since tycho-execution does not have a pre-existing runtime. - // However, if the library is used in a context where a runtime already exists, it is not - // necessary to store it. #[allow(dead_code)] runtime: Option>, } diff --git a/src/encoding/evm/approvals/protocol_approvals_manager.rs b/src/encoding/evm/approvals/protocol_approvals_manager.rs index 292f192..861ded9 100644 --- a/src/encoding/evm/approvals/protocol_approvals_manager.rs +++ b/src/encoding/evm/approvals/protocol_approvals_manager.rs @@ -23,10 +23,6 @@ use crate::encoding::{ pub struct ProtocolApprovalsManager { client: EVMProvider, runtime_handle: Handle, - // Store the runtime to prevent it from being dropped before use. - // This is required since tycho-execution does not have a pre-existing runtime. - // However, if the library is used in a context where a runtime already exists, it is not - // necessary to store it. #[allow(dead_code)] runtime: Option>, } diff --git a/src/encoding/evm/swap_encoder/builder.rs b/src/encoding/evm/swap_encoder/builder.rs index bd517f7..7797651 100644 --- a/src/encoding/evm/swap_encoder/builder.rs +++ b/src/encoding/evm/swap_encoder/builder.rs @@ -6,8 +6,8 @@ use crate::encoding::{ errors::EncodingError, evm::swap_encoder::swap_encoders::{ BalancerV2SwapEncoder, BalancerV3SwapEncoder, BebopSwapEncoder, CurveSwapEncoder, - EkuboSwapEncoder, MaverickV2SwapEncoder, UniswapV2SwapEncoder, UniswapV3SwapEncoder, - UniswapV4SwapEncoder, + EkuboSwapEncoder, HashflowSwapEncoder, MaverickV2SwapEncoder, UniswapV2SwapEncoder, + UniswapV3SwapEncoder, UniswapV4SwapEncoder, }, swap_encoder::SwapEncoder, }; @@ -91,6 +91,11 @@ impl SwapEncoderBuilder { "rfq:bebop" => { Ok(Box::new(BebopSwapEncoder::new(self.executor_address, self.chain, self.config)?)) } + "rfq:hashflow" => Ok(Box::new(HashflowSwapEncoder::new( + self.executor_address, + self.chain, + self.config, + )?)), _ => Err(EncodingError::FatalError(format!( "Unknown protocol system: {}", self.protocol_system diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 5ee08ff..fc292a0 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -664,10 +664,6 @@ pub struct BebopSwapEncoder { native_token_bebop_address: Bytes, native_token_address: Bytes, runtime_handle: Handle, - // Store the runtime to prevent it from being dropped before use. - // This is required since tycho-execution does not have a pre-existing runtime. - // However, if the library is used in a context where a runtime already exists, it is not - // necessary to store it. #[allow(dead_code)] runtime: Option>, } @@ -850,6 +846,148 @@ impl SwapEncoder for BebopSwapEncoder { } } +#[derive(Clone)] +pub struct HashflowSwapEncoder { + executor_address: String, + hashflow_router_address: String, + native_token_address: Bytes, + runtime_handle: Handle, + #[allow(dead_code)] + runtime: Option>, +} + +impl SwapEncoder for HashflowSwapEncoder { + fn new( + executor_address: String, + chain: Chain, + config: Option>, + ) -> Result { + let config = config.ok_or(EncodingError::FatalError( + "Missing hashflow specific addresses in config".to_string(), + ))?; + let hashflow_router_address = config + .get("hashflow_router_address") + .ok_or(EncodingError::FatalError( + "Missing bebop settlement address in config".to_string(), + ))? + .to_string(); + let native_token_address = chain.native_token().address; + let (runtime_handle, runtime) = get_runtime()?; + Ok(Self { + executor_address, + hashflow_router_address, + native_token_address, + runtime_handle, + runtime, + }) + } + + fn encode_swap( + &self, + swap: &Swap, + encoding_context: &EncodingContext, + ) -> Result, EncodingError> { + // Native tokens doesn't need approval, only ERC20 tokens do + let approval_needed: bool; + if let Some(router_address) = &encoding_context.router_address { + let tycho_router_address = bytes_to_address(router_address)?; + let hashflow_router_address = Address::from_str(&self.hashflow_router_address) + .map_err(|_| { + EncodingError::FatalError("Invalid hashflow router address address".to_string()) + })?; + + // Native ETH doesn't need approval, only ERC20 tokens do + if swap.token_in == self.native_token_address { + approval_needed = false; + } else { + approval_needed = ProtocolApprovalsManager::new()?.approval_needed( + bytes_to_address(&swap.token_in)?, + tycho_router_address, + hashflow_router_address, + )?; + } + } else { + approval_needed = true; + } + + // Get quote + let protocol_state = swap + .protocol_state + .as_ref() + .ok_or_else(|| { + EncodingError::FatalError("protocol_state is required for Hashflow".to_string()) + })?; + let amount_in = swap + .estimated_amount_in + .as_ref() + .ok_or(EncodingError::FatalError( + "Estimated amount in is mandatory for a Hashflow swap".to_string(), + ))? + .clone(); + let sender = encoding_context + .router_address + .clone() + .ok_or(EncodingError::FatalError( + "The router address is needed to perform a Hashflow swap".to_string(), + ))?; + let signed_quote = block_in_place(|| { + self.runtime_handle.block_on(async { + protocol_state + .as_indicatively_priced()? + .request_signed_quote(GetAmountOutParams { + amount_in, + token_in: swap.token_in.clone(), + token_out: swap.token_out.clone(), + sender, + receiver: encoding_context.receiver.clone(), + }) + .await + }) + })?; + + // Encode packed data for the executor + // Format: approval_needed | transfer_type | hashflow_calldata[..] + let hashflow_fields = [ + "pool", + "external_account", + "trader", + "effective_trader", + "base_token", + "quote_token", + "base_token_amount", + "quote_token_amount", + "quote_expiry", + "nonce", + "tx_id", + "signature", + ]; + let mut hashflow_calldata = vec![]; + for field in &hashflow_fields { + let value = signed_quote + .quote_attributes + .get(*field) + .ok_or(EncodingError::FatalError(format!( + "Hashflow quote must have a {field} attribute" + )))?; + hashflow_calldata.extend_from_slice(value); + } + let args = ( + (encoding_context.transfer_type as u8).to_be_bytes(), + (approval_needed as u8).to_be_bytes(), + &hashflow_calldata[..], + ); + Ok(args.abi_encode_packed()) + } + + fn executor_address(&self) -> &str { + &self.executor_address + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + #[cfg(test)] mod tests { use std::collections::HashMap; @@ -1985,7 +2123,7 @@ mod tests { }; let bebop_state = MockRFQState { quote_amount_out, - quote_calldata: bebop_calldata.clone(), + quote_data: vec![("calldata".to_string(), bebop_calldata.clone())], quote_partial_fill_offset: partial_fill_offset, }; @@ -2037,4 +2175,152 @@ mod tests { assert_eq!(hex_swap, expected_swap + &bebop_calldata.to_string()[2..]); } } + + mod hashflow { + use alloy::hex::encode; + use num_bigint::BigUint; + + use super::*; + use crate::encoding::{ + evm::testing_utils::MockRFQState, + models::{SwapBuilder, TransferType}, + }; + + fn hashflow_config() -> Option> { + Some(HashMap::from([( + "hashflow_router_address".to_string(), + "0x55084eE0fEf03f14a305cd24286359A35D735151".to_string(), + )])) + } + + #[test] + fn test_encode_hashflow_single_with_user_data() { + // Hashflow requires a swap with protocol data, otherwise will return an error + let hashflow_component = ProtocolComponent { + id: String::from("hashflow-rfq"), + protocol_system: String::from("rfq:hashflow"), + ..Default::default() + }; + + let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC + let token_out = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); // WETH + + let swap = SwapBuilder::new(hashflow_component, token_in.clone(), token_out.clone()) + .estimated_amount_in(BigUint::from_str("3000000000").unwrap()) + .build(); + + let encoding_context = EncodingContext { + receiver: Bytes::from("0xc5564C13A157E6240659fb81882A28091add8670"), + exact_out: false, + router_address: Some(Bytes::zero(20)), + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + transfer_type: TransferType::Transfer, + }; + + let encoder = HashflowSwapEncoder::new( + String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), + Chain::Ethereum, + hashflow_config(), + ) + .unwrap(); + encoder + .encode_swap(&swap, &encoding_context) + .expect_err("Should returned an error if the swap has no protocol state"); + } + + #[test] + fn test_encode_hashflow_single_with_protocol_state() { + // 3000 USDC -> 1 WETH using a mocked RFQ state to get a quote + let partial_fill_offset = 12u64; + let quote_amount_out = BigUint::from_str("1000000000000000000").unwrap(); + + let hashflow_component = ProtocolComponent { + id: String::from("hashflow-rfq"), + protocol_system: String::from("rfq:hashflow"), + ..Default::default() + }; + let hashflow_quote_data = vec![ + ( + "pool".to_string(), + Bytes::from_str("0x031903307c517c11b71f8313d19afde0a4f41cb5").unwrap(), + ), + ( + "trader".to_string(), + Bytes::from_str("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").unwrap(), + ), + ("nonce".to_string(), Bytes::from(1755512134141u64.to_be_bytes().to_vec())), + ( + "tx_id".to_string(), + Bytes::from_str( + "0x125000064000640000001747937188ffffffffffffff00295e467232b36d0000", + ) + .unwrap(), + ), + ("signature".to_string(), Bytes::from_str("0xfda99100ffd8adfc818a827e1698c1d1fa2f59f7723ff84bfeba0f80e5298b1077f590d8d99aec6f6801c611eb270b5d89fac02a680ab38f03f3c5d16039c6f11c").unwrap()), + ("quote_expiry".to_string(), Bytes::from(1755512162u64.to_be_bytes().to_vec())), + ( + "external_account".to_string(), + Bytes::from_str("0xbb289bc97591f70d8216462df40ed713011b968a").unwrap(), + ), + ( + "effective_trader".to_string(), + Bytes::from_str("0xcd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2").unwrap(), + ), + ]; + let hashflow_quote_data_values = + hashflow_quote_data + .iter() + .fold(vec![], |mut acc, (_key, value)| { + acc.extend_from_slice(value); + acc + }); + let hashflow_calldata = Bytes::from(hashflow_quote_data_values); + let hashflow_state = MockRFQState { + quote_amount_out, + quote_data: hashflow_quote_data, + quote_partial_fill_offset: partial_fill_offset, + }; + + let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC + let token_out = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); // WETH + + let swap = SwapBuilder::new(hashflow_component, token_in.clone(), token_out.clone()) + .estimated_amount_in(BigUint::from_str("3000000000").unwrap()) + .protocol_state(&hashflow_state) + .build(); + + let encoding_context = EncodingContext { + receiver: Bytes::from("0xc5564C13A157E6240659fb81882A28091add8670"), + exact_out: false, + router_address: Some(Bytes::zero(20)), + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + transfer_type: TransferType::Transfer, + }; + + let encoder = HashflowSwapEncoder::new( + String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), + Chain::Ethereum, + hashflow_config(), + ) + .unwrap(); + + let encoded_swap = encoder + .encode_swap(&swap, &encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); + + let expected_swap = String::from(concat!( + "01", // transfer type + "01", // approval needed + )); + assert_eq!(hex_swap, expected_swap + &hashflow_calldata.to_string()[2..]); + } + + #[test] + fn test_encode_hashflow_aggregate_with_protocol_state() { + todo!() + } + } } diff --git a/src/encoding/evm/testing_utils.rs b/src/encoding/evm/testing_utils.rs index f633728..e769ac8 100644 --- a/src/encoding/evm/testing_utils.rs +++ b/src/encoding/evm/testing_utils.rs @@ -17,7 +17,7 @@ use tycho_common::{ #[derive(Debug)] pub struct MockRFQState { pub quote_amount_out: BigUint, - pub quote_calldata: Bytes, + pub quote_data: Vec<(String, Bytes)>, pub quote_partial_fill_offset: u64, } impl ProtocolSim for MockRFQState { @@ -83,7 +83,9 @@ impl IndicativelyPriced for MockRFQState { params: GetAmountOutParams, ) -> Result { let mut quote_attributes: HashMap = HashMap::new(); - quote_attributes.insert("calldata".to_string(), self.quote_calldata.clone()); + for (attr, value) in &self.quote_data { + quote_attributes.insert(attr.clone(), value.clone()); + } quote_attributes.insert( "partial_fill_offset".to_string(), Bytes::from( diff --git a/src/encoding/evm/utils.rs b/src/encoding/evm/utils.rs index dc52b14..ee448e2 100644 --- a/src/encoding/evm/utils.rs +++ b/src/encoding/evm/utils.rs @@ -78,6 +78,9 @@ pub fn get_static_attribute(swap: &Swap, attribute_name: &str) -> Result .to_vec()) } +/// Returns the current Tokio runtime handle, or creates a new one if it doesn't exist. +/// It also returns the runtime to prevent it from being dropped before use. +/// This is required since tycho-execution does not have a pre-existing runtime. pub fn get_runtime() -> Result<(Handle, Option>), EncodingError> { match Handle::try_current() { Ok(h) => Ok((h, None)), diff --git a/tests/common/mod.rs b/tests/common/mod.rs index d5032d5..0949b7e 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -21,6 +21,10 @@ pub fn bob_address() -> Bytes { Bytes::from_str("0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e").unwrap() } +pub fn alice_address() -> Bytes { + Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap() +} + pub fn eth_chain() -> Chain { Chain::Ethereum } diff --git a/tests/optimized_transfers_integration_tests.rs b/tests/optimized_transfers_integration_tests.rs index e59bb23..0aa52d1 100644 --- a/tests/optimized_transfers_integration_tests.rs +++ b/tests/optimized_transfers_integration_tests.rs @@ -4,12 +4,15 @@ use alloy::hex::encode; use num_bigint::{BigInt, BigUint}; use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use tycho_execution::encoding::{ - evm::{testing_utils::MockRFQState, utils::write_calldata_to_file}, + evm::{ + testing_utils::MockRFQState, + utils::{biguint_to_u256, write_calldata_to_file}, + }, models::{Solution, Swap, SwapBuilder, UserTransferType}, }; use crate::common::{ - bob_address, encoding::encode_tycho_router_call, eth, eth_chain, get_signer, + alice_address, bob_address, encoding::encode_tycho_router_call, eth, eth_chain, get_signer, get_tycho_router_encoder, usdc, wbtc, weth, }; @@ -650,7 +653,7 @@ fn test_uniswap_v3_bebop() { let bebop_state = MockRFQState { quote_amount_out, - quote_calldata: bebop_calldata.clone(), + quote_data: vec![("calldata".to_string(), bebop_calldata)], quote_partial_fill_offset: partial_fill_offset, }; @@ -698,3 +701,228 @@ fn test_uniswap_v3_bebop() { let hex_calldata = encode(&calldata); write_calldata_to_file("test_uniswap_v3_bebop", hex_calldata.as_str()); } + +#[test] +fn test_hashflow() { + // Note: This test does not assert anything. It is only used to obtain + // integration test data for our router solidity test. + // + // Performs a swap from USDC to WBTC using Hashflow RFQ + // + // USDC ───(Hashflow RFQ)──> WBTC + + let usdc = usdc(); + let wbtc = wbtc(); + + // USDC -> WBTC via Hashflow RFQ using real order data + let quote_amount_out = BigUint::from_str("3714751").unwrap(); + + let hashflow_state = MockRFQState { + quote_amount_out, + quote_data: vec![ + ( + "pool".to_string(), + Bytes::from_str("0x478eca1b93865dca0b9f325935eb123c8a4af011").unwrap(), + ), + ( + "external_account".to_string(), + Bytes::from_str("0xbee3211ab312a8d065c4fef0247448e17a8da000").unwrap(), + ), + ( + "trader".to_string(), + Bytes::from_str("0xcd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2").unwrap(), + ), + ( + // Passing the tycho router address here has no effect + "effective_trader".to_string(), + Bytes::from_str("0xcd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2").unwrap(), + ), + ( + "base_token".to_string(), + Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), + ), + ( + "quote_token".to_string(), + Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(), + ), + ( + "base_token_amount".to_string(), + Bytes::from(biguint_to_u256(&BigUint::from(4308094737_u64)).to_be_bytes::<32>().to_vec()), + ), + ( + "quote_token_amount".to_string(), + Bytes::from(biguint_to_u256(&BigUint::from(3714751_u64)).to_be_bytes::<32>().to_vec()), + ), + ("quote_expiry".to_string(), Bytes::from(biguint_to_u256(&BigUint::from(1755610328_u64)).to_be_bytes::<32>().to_vec())), + ("nonce".to_string(), Bytes::from(biguint_to_u256(&BigUint::from(1755610283723_u64)).to_be_bytes::<32>().to_vec())), + ( + "tx_id".to_string(), + Bytes::from_str( + "0x125000064000640000001747eb8c38ffffffffffffff0029642016edb36d0000", + ) + .unwrap(), + ), + ("signature".to_string(), Bytes::from_str("0x6ddb3b21fe8509e274ddf46c55209cdbf30360944abbca6569ed6b26740d052f419964dcb5a3bdb98b4ed1fb3642a2760b8312118599a962251f7a8f73fe4fbe1c").unwrap()), + ], + quote_partial_fill_offset: 0, + }; + + let hashflow_component = ProtocolComponent { + id: String::from("hashflow-rfq"), + protocol_system: String::from("rfq:hashflow"), + ..Default::default() + }; + + let swap_usdc_wbtc = SwapBuilder::new(hashflow_component, usdc.clone(), wbtc.clone()) + .estimated_amount_in(BigUint::from_str("4308094737").unwrap()) + .protocol_state(&hashflow_state) + .build(); + + let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); + + let solution = Solution { + exact_out: false, + given_token: usdc, + given_amount: BigUint::from_str("4308094737").unwrap(), + checked_token: wbtc, + checked_amount: BigUint::from_str("3714751").unwrap(), + sender: alice_address(), + receiver: alice_address(), + swaps: vec![swap_usdc_wbtc], + ..Default::default() + }; + + let encoded_solution = encoder + .encode_solutions(vec![solution.clone()]) + .unwrap()[0] + .clone(); + + let calldata = encode_tycho_router_call( + eth_chain().id(), + encoded_solution, + &solution, + &UserTransferType::TransferFrom, + ð(), + None, + ) + .unwrap() + .data; + + let hex_calldata = encode(&calldata); + write_calldata_to_file("test_hashflow", hex_calldata.as_str()); +} + +#[test] +#[ignore] +fn test_uniswap_v3_hashflow() { + // Note: This test does not assert anything. It is only used to obtain + // integration test data for our router solidity test. + // + // Performs a sequential swap from WETH to WBTC through USDC using USV3 and + // Hashflow RFQ + // + // WETH ───(USV3)──> USDC ───(Hashflow RFQ)──> WBTC + + let weth = weth(); + let usdc = usdc(); + let wbtc = wbtc(); + + // First swap: WETH -> USDC via UniswapV3 + let swap_weth_usdc = SwapBuilder::new( + ProtocolComponent { + id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* WETH-USDC USV3 Pool + * 0.05% */ + protocol_system: "uniswap_v3".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs + .insert("fee".to_string(), Bytes::from(BigInt::from(500).to_signed_bytes_be())); + attrs + }, + ..Default::default() + }, + weth.clone(), + usdc.clone(), + ) + .build(); + + // Second swap: USDC -> WBTC via Hashflow RFQ using real order data + let quote_amount_out = BigUint::from_str("1735332").unwrap(); + + let hashflow_state = MockRFQState { + quote_amount_out, + quote_data: vec![ + ( + "pool".to_string(), + Bytes::from_str("0x031903307c517c11b71f8313d19afde0a4f41cb5").unwrap(), + ), + ( + "trader".to_string(), + Bytes::from_str("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").unwrap(), + ), + ("nonce".to_string(), Bytes::from(1755512134141u64.to_be_bytes().to_vec())), + ( + "tx_id".to_string(), + Bytes::from_str( + "0x125000064000640000001747937188ffffffffffffff00295e467232b36d0000", + ) + .unwrap(), + ), + ("signature".to_string(), Bytes::from_str("0xfda99100ffd8adfc818a827e1698c1d1fa2f59f7723ff84bfeba0f80e5298b1077f590d8d99aec6f6801c611eb270b5d89fac02a680ab38f03f3c5d16039c6f11c").unwrap()), + ("quote_expiry".to_string(), Bytes::from(1755512162u64.to_be_bytes().to_vec())), + ( + "external_account".to_string(), + Bytes::from_str("0xbb289bc97591f70d8216462df40ed713011b968a").unwrap(), + ), + ( + "effective_trader".to_string(), + Bytes::from_str("0xcd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2").unwrap(), + ), + ], + quote_partial_fill_offset: 0, + }; + + let hashflow_component = ProtocolComponent { + id: String::from("hashflow-rfq"), + protocol_system: String::from("rfq:hashflow"), + ..Default::default() + }; + + let swap_usdc_wbtc = SwapBuilder::new(hashflow_component, usdc.clone(), wbtc.clone()) + .estimated_amount_in(BigUint::from_str("2000000000").unwrap()) + .protocol_state(&hashflow_state) + .build(); + + let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); + + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("2000000000").unwrap(), + checked_token: wbtc, + checked_amount: BigUint::from_str("2000000000").unwrap(), + sender: alice_address(), + receiver: alice_address(), + swaps: vec![swap_weth_usdc, swap_usdc_wbtc], + ..Default::default() + }; + + let encoded_solution = encoder + .encode_solutions(vec![solution.clone()]) + .unwrap()[0] + .clone(); + + let calldata = encode_tycho_router_call( + eth_chain().id(), + encoded_solution, + &solution, + &UserTransferType::TransferFrom, + ð(), + None, + ) + .unwrap() + .data; + + let hex_calldata = encode(&calldata); + write_calldata_to_file("test_uniswap_v3_hashflow", hex_calldata.as_str()); +}