diff --git a/CHANGELOG.md b/CHANGELOG.md index b669f1d..4e45a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,101 @@ +## [0.119.0](https://github.com/propeller-heads/tycho-execution/compare/0.118.0...0.119.0) (2025-08-21) + + +### Features + +* Add BytecodeExporter to TestUtils ([cea964e](https://github.com/propeller-heads/tycho-execution/commit/cea964e0a54a75b1bd89007794daf467b812f021)) + +## [0.118.0](https://github.com/propeller-heads/tycho-execution/compare/0.117.0...0.118.0) (2025-08-21) + + +### Features + +* Use Arc instead of & for protocol_state in Swap ([a2a5319](https://github.com/propeller-heads/tycho-execution/commit/a2a53195edbc631bea8fbc758a3bf5ee33a87dda)) + + +### Bug Fixes + +* Updates to new datatype after merging main ([e9c1a39](https://github.com/propeller-heads/tycho-execution/commit/e9c1a39608bba786bbed2b53c4596e94ddcbe03c)) + +## [0.117.0](https://github.com/propeller-heads/tycho-execution/compare/0.116.0...0.117.0) (2025-08-21) + + +### Features + +* implement `SwapEncoder` for `Hashflow` ([93db953](https://github.com/propeller-heads/tycho-execution/commit/93db953c620f4d52e8852ff8148f2dfdbc580029)) + + +### Bug Fixes + +* in bebop's encode_swap, fail early if router address is not present ([c013bf7](https://github.com/propeller-heads/tycho-execution/commit/c013bf707225b171e76f3f3dac88624b6a3458dc)) +* in hashflow's encode_swap, fail early if router address is not present ([c506f2c](https://github.com/propeller-heads/tycho-execution/commit/c506f2c048a20d1ffd066d2903d2cc469fd167ed)) +* in HashflowExecutor, _balanceOf must use `trader` address instead of the executor's to get the balance ([a09d648](https://github.com/propeller-heads/tycho-execution/commit/a09d648f3c50f87b392c7c5eb14af07307c5ccea)) + +## [0.116.0](https://github.com/propeller-heads/tycho-execution/compare/0.115.0...0.116.0) (2025-08-19) + + +### Features + +* Bebop executor in base ([c974312](https://github.com/propeller-heads/tycho-execution/commit/c974312a377d950948e76dd1dbb3303421f9f5f8)) +* Support Bebop quote through IndicativelyPriced state ([2292061](https://github.com/propeller-heads/tycho-execution/commit/22920617ebca35199a8cc927245cfa7e6209ade8)) +* Update BebopExecutor deployed address ([72b60aa](https://github.com/propeller-heads/tycho-execution/commit/72b60aa324d136ab999ff04339433a83743aee5e)) + + +### Bug Fixes + +* Add bebop to protocol specific addresses ([6e68d19](https://github.com/propeller-heads/tycho-execution/commit/6e68d199c90395a9204329e462e5027e81e61171)) +* Implement From for EncodingError ([987956e](https://github.com/propeller-heads/tycho-execution/commit/987956eb0bea2a46115d160ea05db395ce17077a)) + +## [0.115.0](https://github.com/propeller-heads/tycho-execution/compare/0.114.0...0.115.0) (2025-08-14) + + +### Features + +* Deploy BebopExecutor ([f9ef8e0](https://github.com/propeller-heads/tycho-execution/commit/f9ef8e0dbdfb73db132f4932911fb5a666f6fa25)) +* implement encoder and executor with single order support ([7e7e3f0](https://github.com/propeller-heads/tycho-execution/commit/7e7e3f0e0187adc2f7966fd108e9edcb5e05f20f)) +* support multi and aggregate orders ([ad0a999](https://github.com/propeller-heads/tycho-execution/commit/ad0a9991da57348a026bc43d3ca230898822614e)) + + +### Bug Fixes + +* After merge fixes with SwapBuilder ([d301dfc](https://github.com/propeller-heads/tycho-execution/commit/d301dfcc44575157997d95a571633ef4e659a197)) +* After merging main fixes ([003ab7c](https://github.com/propeller-heads/tycho-execution/commit/003ab7cea82fec67dbd54bb1248f75e8c7c74c50)) +* **Bebop:** Fix AggregateOrder tests ([7024da3](https://github.com/propeller-heads/tycho-execution/commit/7024da395d9d959fc9edecc398f8a161f76d4de1)) +* **Bebop:** Fix encoding and tests ([e793478](https://github.com/propeller-heads/tycho-execution/commit/e79347842f74e05b078d3b2da4f6fad099a6927d)) +* cargo and forge fmt ([17e5354](https://github.com/propeller-heads/tycho-execution/commit/17e535494b5f2f211eef889c768f05f00843dc5b)) +* Fix test imports ([210d4fa](https://github.com/propeller-heads/tycho-execution/commit/210d4fa604de7610ce01fd918e57970dd5498e6b)) +* Move Bebop tests according to new setup ([01ab5d2](https://github.com/propeller-heads/tycho-execution/commit/01ab5d22b182072a65221f620df7fade3ab6fe7a)) +* SafeCast => V4SafeCast to fix build issue ([946c439](https://github.com/propeller-heads/tycho-execution/commit/946c4391e825661126fc30e013ac95e87e904fdc)) +* Simplify the BebopExecutor and fix Single tests ([76a09d0](https://github.com/propeller-heads/tycho-execution/commit/76a09d0402f34563a121f0973589b523ffdf3a8f)) + +## [0.114.0](https://github.com/propeller-heads/tycho-execution/compare/0.113.0...0.114.0) (2025-08-14) + + +### Features + +* add hashflow executor ([e03ecf4](https://github.com/propeller-heads/tycho-execution/commit/e03ecf48d50f08b739a9d862b4746c3a055ea6e9)) + +## [0.113.0](https://github.com/propeller-heads/tycho-execution/compare/0.112.2...0.113.0) (2025-08-14) + + +### Features + +* Add estimated_amount_in to Swap. Add SwapBuilder ([5eb9973](https://github.com/propeller-heads/tycho-execution/commit/5eb9973dbd5ca4eb2f4d15c3f4803b4bc7dfa1ea)) + +## [0.112.2](https://github.com/propeller-heads/tycho-execution/compare/0.112.1...0.112.2) (2025-08-07) + + +### Bug Fixes + +* Encode sequential swaps with unwrapping WETH correctly ([c42a5da](https://github.com/propeller-heads/tycho-execution/commit/c42a5dae6a55d315360eaaa510d9dcaf7ef8e4c0)) + +## [0.112.1](https://github.com/propeller-heads/tycho-execution/compare/0.112.0...0.112.1) (2025-07-31) + + +### Bug Fixes + +* Rename mainnet to ethereum in txServiceUrls for Safe ([51d9484](https://github.com/propeller-heads/tycho-execution/commit/51d9484de0b4c75b78dc8ca839f4ec7df40f972e)) + ## [0.112.0](https://github.com/propeller-heads/tycho-execution/compare/0.111.0...0.112.0) (2025-07-22) diff --git a/Cargo.lock b/Cargo.lock index 3b2193d..cf6c6a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4634,9 +4634,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tycho-common" -version = "0.78.2" +version = "0.81.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af4c7e2c8e194a3e9dfc5911ff0ec273b7dd52acb71dfdcf21351dd78a49576" +checksum = "68d44780891254a0155d1aaa765a8625b48672158b6e12ce4f06771e9d369ffa" dependencies = [ "anyhow", "async-trait", @@ -4659,9 +4659,10 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.112.0" +version = "0.119.0" dependencies = [ "alloy", + "async-trait", "chrono", "clap", "dotenv", @@ -4676,6 +4677,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tycho-common", + "tycho-execution", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 800ec7e..2eaacc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "0.112.0" +version = "0.119.0" edition = "2021" description = "Provides tools for encoding and executing swaps against Tycho router and protocol executors." repository = "https://github.com/propeller-heads/tycho-execution" @@ -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,17 +37,19 @@ 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.78.1" - +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 } [dev-dependencies] rstest = "0.24.0" +tycho-execution = { path = "../tycho-execution", features = ["test-utils"] } [features] default = ["evm"] evm = ["alloy"] 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 ccd9d62..c4ac219 100644 --- a/config/executor_addresses.json +++ b/config/executor_addresses.json @@ -10,12 +10,15 @@ "ekubo_v2": "0x263DD7AD20983b5E0392bf1F09C4493500EDb333", "vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165", "vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B", - "vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec" + "vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec", + "rfq:bebop": "0xFE42BFb115eD9671011cA52BDD23A52A2e077a7c", + "rfq:hashflow": "0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f" }, "base": { "uniswap_v2": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700", "uniswap_v3": "0x647bffbf8bd72bf6341ecba8b0279e090313a40d", - "uniswap_v4_hooks": "0x7Dfa502736C7bd84DA1402F7524214215BC9534d" + "uniswap_v4_hooks": "0x7Dfa502736C7bd84DA1402F7524214215BC9534d", + "rfq:bebop": "0x489A3f531dA3873D6585BF3f8E0dEE48CAC6F7BC" }, "unichain": { "uniswap_v2": "0x00C1b81e3C8f6347E69e2DDb90454798A6Be975E", diff --git a/config/protocol_specific_addresses.json b/config/protocol_specific_addresses.json index 63ce3e0..f4cbe21 100644 --- a/config/protocol_specific_addresses.json +++ b/config/protocol_specific_addresses.json @@ -5,8 +5,20 @@ }, "vm:curve": { "native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + }, + "rfq:bebop": { + "bebop_settlement_address": "0xbbbbbBB520d69a9775E85b458C58c648259FAD5F", + "native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + }, + "rfq:hashflow": { + "hashflow_router_address": "0x55084eE0fEf03f14a305cd24286359A35D735151" + } + }, + "base": { + "rfq:bebop": { + "bebop_settlement_address": "0xbbbbbBB520d69a9775E85b458C58c648259FAD5F", + "native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" } }, - "base": {}, "unichain": {} -} \ No newline at end of file +} diff --git a/config/test_executor_addresses.json b/config/test_executor_addresses.json index 1aa107c..f56a5e8 100644 --- a/config/test_executor_addresses.json +++ b/config/test_executor_addresses.json @@ -10,6 +10,8 @@ "ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598", "vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211", "vm:maverick_v2": "0xA4AD4f68d0b91CFD19687c881e50f3A00242828c", - "vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb" + "vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb", + "rfq:bebop": "0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF", + "rfq:hashflow": "0x15cF58144EF33af1e14b5208015d11F9143E27b9" } } diff --git a/docs/audits/Max_2025-08-05.pdf b/docs/audits/Max_2025-08-05.pdf new file mode 100644 index 0000000..4133c52 Binary files /dev/null and b/docs/audits/Max_2025-08-05.pdf differ diff --git a/examples/encoding-example/main.rs b/examples/encoding-example/main.rs index 9a4b9e4..d969079 100644 --- a/examples/encoding-example/main.rs +++ b/examples/encoding-example/main.rs @@ -7,7 +7,7 @@ use tycho_common::{ }; use tycho_execution::encoding::{ evm::encoder_builders::TychoRouterEncoderBuilder, - models::{Solution, Swap, UserTransferType}, + models::{Solution, Swap, SwapBuilder, UserTransferType}, }; fn main() { @@ -44,6 +44,7 @@ fn main() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; // Then we create a solution object with the previous swap @@ -87,56 +88,51 @@ fn main() { let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") .expect("Failed to create DAI address"); - let swap_weth_dai = Swap { - component: ProtocolComponent { + let swap_weth_dai = SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0.5f64, - user_data: None, - protocol_state: None, - }; - let swap_weth_wbtc = Swap { - component: ProtocolComponent { + weth.clone(), + dai.clone(), + ) + .split(0.5) + .build(); + + // Split 0 represents the remaining 50%, but to avoid any rounding errors we set this to + // 0 to signify "the remainder of the WETH value". It should still be very close to 50% + let swap_weth_wbtc = SwapBuilder::new( + ProtocolComponent { id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: wbtc.clone(), - // This represents the remaining 50%, but to avoid any rounding errors we set this to - // 0 to signify "the remainder of the WETH value". It should still be very close to 50% - split: 0f64, - user_data: None, - protocol_state: None, - }; - let swap_dai_usdc = Swap { - component: ProtocolComponent { + weth.clone(), + wbtc.clone(), + ) + .build(); + + let swap_dai_usdc = SwapBuilder::new( + ProtocolComponent { id: "0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: dai.clone(), - token_out: usdc.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; - let swap_wbtc_usdc = Swap { - component: ProtocolComponent { + dai.clone(), + usdc.clone(), + ) + .build(); + let swap_wbtc_usdc = SwapBuilder::new( + ProtocolComponent { id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: wbtc.clone(), - token_out: usdc.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + wbtc.clone(), + usdc.clone(), + ) + .build(); let mut complex_solution = solution.clone(); complex_solution.swaps = vec![swap_weth_dai, swap_weth_wbtc, swap_dai_usdc, swap_wbtc_usdc]; diff --git a/examples/uniswapx-encoding-example/main.rs b/examples/uniswapx-encoding-example/main.rs index a35b1cb..bb314d6 100644 --- a/examples/uniswapx-encoding-example/main.rs +++ b/examples/uniswapx-encoding-example/main.rs @@ -83,6 +83,7 @@ fn main() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let swap_usdc_usdt = Swap { component: ProtocolComponent { @@ -101,6 +102,7 @@ fn main() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; // Then we create a solution object with the previous swap diff --git a/foundry/foundry.toml b/foundry/foundry.toml index 9acffad..e5183c8 100644 --- a/foundry/foundry.toml +++ b/foundry/foundry.toml @@ -7,7 +7,7 @@ evm_version = 'cancun' optimizer = true optimizer_runs = 200 via_ir = true -fs_permissions = [{ access = "read", path = "./test/assets" }] +fs_permissions = [{ access = "read", path = "./test/assets" }, { access = "write", path = "./test" }] [profile.production] src = 'src' diff --git a/foundry/scripts/deploy-executors.js b/foundry/scripts/deploy-executors.js index e50a4fe..9528f0f 100644 --- a/foundry/scripts/deploy-executors.js +++ b/foundry/scripts/deploy-executors.js @@ -81,6 +81,11 @@ const executors_to_deploy = { }, // Args: Permit2 {exchange: "BalancerV3Executor", args: ["0x000000000022D473030F116dDEE9F6B43aC78BA3"]}, + // Args: Bebop Settlement contract, Permit2 + { + exchange: "BebopExecutor", + args: ["0xbbbbbBB520d69a9775E85b458C58c648259FAD5F", "0x000000000022D473030F116dDEE9F6B43aC78BA3"] + }, ], "base": [ // Args: Factory, Pool Init Code Hash, Permit2, Fee BPS @@ -116,6 +121,11 @@ const executors_to_deploy = { "0x000000000022D473030F116dDEE9F6B43aC78BA3" ] }, + // Args: Bebop Settlement contract, Permit2 + { + exchange: "BebopExecutor", + args: ["0xbbbbbBB520d69a9775E85b458C58c648259FAD5F", "0x000000000022D473030F116dDEE9F6B43aC78BA3"] + }, ], "unichain": [ // Args: Factory, Pool Init Code Hash, Permit2, Fee BPS diff --git a/foundry/src/executors/BebopExecutor.sol b/foundry/src/executors/BebopExecutor.sol new file mode 100644 index 0000000..b3c705f --- /dev/null +++ b/foundry/src/executors/BebopExecutor.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "@interfaces/IExecutor.sol"; +import "../RestrictTransferFrom.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; +import { + IERC20, + SafeERC20 +} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +/// @title BebopExecutor +/// @notice Executor for Bebop PMM RFQ (Request for Quote) swaps +/// @dev Handles Single and Aggregate RFQ swaps through Bebop settlement contract +/// @dev Only supports single token in to single token out swaps +contract BebopExecutor is IExecutor, RestrictTransferFrom { + using Math for uint256; + using SafeERC20 for IERC20; + using Address for address; + + /// @notice Bebop-specific errors + error BebopExecutor__InvalidDataLength(); + error BebopExecutor__ZeroAddress(); + + /// @notice The Bebop settlement contract address + address public immutable bebopSettlement; + + constructor(address _bebopSettlement, address _permit2) + RestrictTransferFrom(_permit2) + { + if (_bebopSettlement == address(0)) revert BebopExecutor__ZeroAddress(); + bebopSettlement = _bebopSettlement; + } + + /// @notice Executes a swap through Bebop's PMM RFQ system + /// @param givenAmount The amount of input token to swap + /// @param data Encoded swap data containing tokens and bebop calldata + /// @return calculatedAmount The amount of output token received + function swap(uint256 givenAmount, bytes calldata data) + external + payable + virtual + override + returns (uint256 calculatedAmount) + { + ( + address tokenIn, + address tokenOut, + TransferType transferType, + uint8 partialFillOffset, + uint256 originalFilledTakerAmount, + bool approvalNeeded, + address receiver, + bytes memory bebopCalldata + ) = _decodeData(data); + + _transfer(address(this), transferType, address(tokenIn), givenAmount); + + // Modify the filledTakerAmount in the calldata + // If the filledTakerAmount is the same as the original, the original calldata is returned + bytes memory finalCalldata = _modifyFilledTakerAmount( + bebopCalldata, + givenAmount, + originalFilledTakerAmount, + partialFillOffset + ); + + // Approve Bebop settlement to spend tokens if needed + if (approvalNeeded) { + // slither-disable-next-line unused-return + IERC20(tokenIn).forceApprove(bebopSettlement, type(uint256).max); + } + + uint256 balanceBefore = _balanceOf(tokenOut, receiver); + uint256 ethValue = tokenIn == address(0) ? givenAmount : 0; + + // Use OpenZeppelin's Address library for safe call with value + // This will revert if the call fails + // slither-disable-next-line unused-return + bebopSettlement.functionCallWithValue(finalCalldata, ethValue); + + uint256 balanceAfter = _balanceOf(tokenOut, receiver); + calculatedAmount = balanceAfter - balanceBefore; + } + + /// @dev Decodes the packed calldata + function _decodeData(bytes calldata data) + internal + pure + returns ( + address tokenIn, + address tokenOut, + TransferType transferType, + uint8 partialFillOffset, + uint256 originalFilledTakerAmount, + bool approvalNeeded, + address receiver, + bytes memory bebopCalldata + ) + { + // Need at least 95 bytes for the minimum fixed fields + // 20 + 20 + 1 + 1 (offset) + 32 (original amount) + 1 (approval) + 20 (receiver) = 95 + if (data.length < 95) revert BebopExecutor__InvalidDataLength(); + + tokenIn = address(bytes20(data[0:20])); + tokenOut = address(bytes20(data[20:40])); + transferType = TransferType(uint8(data[40])); + partialFillOffset = uint8(data[41]); + originalFilledTakerAmount = uint256(bytes32(data[42:74])); + approvalNeeded = data[74] != 0; + receiver = address(bytes20(data[75:95])); + bebopCalldata = data[95:]; + } + + /// @dev Modifies the filledTakerAmount in the bebop calldata to handle slippage + /// @param bebopCalldata The original calldata for the bebop settlement + /// @param givenAmount The actual amount available from the router + /// @param originalFilledTakerAmount The original amount expected when the quote was generated + /// @param partialFillOffset The offset from Bebop API indicating where filledTakerAmount is located + /// @return The modified calldata with updated filledTakerAmount + function _modifyFilledTakerAmount( + bytes memory bebopCalldata, + uint256 givenAmount, + uint256 originalFilledTakerAmount, + uint8 partialFillOffset + ) internal pure returns (bytes memory) { + // Use the offset from Bebop API to locate filledTakerAmount + // Position = 4 bytes (selector) + offset * 32 bytes + uint256 filledTakerAmountPos = 4 + uint256(partialFillOffset) * 32; + + // Cap the fill amount at what we actually have available + uint256 newFilledTakerAmount = originalFilledTakerAmount > givenAmount + ? givenAmount + : originalFilledTakerAmount; + + // If the new filledTakerAmount is the same as the original, return the original calldata + if (newFilledTakerAmount == originalFilledTakerAmount) { + return bebopCalldata; + } + + // Use assembly to modify the filledTakerAmount at the correct position + // slither-disable-next-line assembly + assembly { + // Get pointer to the data portion of the bytes array + let dataPtr := add(bebopCalldata, 0x20) + + // Calculate the actual position and store the new value + let actualPos := add(dataPtr, filledTakerAmountPos) + mstore(actualPos, newFilledTakerAmount) + } + + return bebopCalldata; + } + + /// @dev Returns the balance of a token or ETH for an account + /// @param token The token address, or address(0) for ETH + /// @param account The account to get the balance of + /// @return balance The balance of the token or ETH for the account + function _balanceOf(address token, address account) + internal + view + returns (uint256) + { + return token == address(0) + ? account.balance + : IERC20(token).balanceOf(account); + } + + /** + * @dev Allow receiving ETH for settlement calls that require ETH + * This is needed when the executor handles native ETH swaps + * In production, ETH typically comes from router or settlement contracts + * In tests, it may come from EOA addresses via the test harness + */ + receive() external payable { + // Allow ETH transfers for Bebop settlement functionality + } +} diff --git a/foundry/src/executors/HashflowExecutor.sol b/foundry/src/executors/HashflowExecutor.sol new file mode 100644 index 0000000..56be01f --- /dev/null +++ b/foundry/src/executors/HashflowExecutor.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "../RestrictTransferFrom.sol"; +import "@interfaces/IExecutor.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +error HashflowExecutor__InvalidHashflowRouter(); +error HashflowExecutor__InvalidDataLength(); + +interface IHashflowRouter { + struct RFQTQuote { + address pool; + address externalAccount; + address trader; + address effectiveTrader; + address baseToken; + address quoteToken; + uint256 effectiveBaseTokenAmount; + uint256 baseTokenAmount; + uint256 quoteTokenAmount; + uint256 quoteExpiry; + uint256 nonce; + bytes32 txid; + bytes signature; // ECDSA signature of the quote, 65 bytes + } + + function tradeRFQT(RFQTQuote calldata quote) external payable; +} + +contract HashflowExecutor is IExecutor, RestrictTransferFrom { + using SafeERC20 for IERC20; + + address public constant NATIVE_TOKEN = + 0x0000000000000000000000000000000000000000; + + /// @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 + payable + returns (uint256 calculatedAmount) + { + ( + IHashflowRouter.RFQTQuote memory quote, + bool approvalNeeded, + TransferType transferType + ) = _decodeData(data); + + // Slippage checks + if (givenAmount > quote.baseTokenAmount) { + // Do not transfer more than the quote's maximum permitted amount. + givenAmount = quote.baseTokenAmount; + } + quote.effectiveBaseTokenAmount = givenAmount; + + if (approvalNeeded && quote.baseToken != NATIVE_TOKEN) { + // slither-disable-next-line unused-return + IERC20(quote.baseToken).forceApprove( + hashflowRouter, type(uint256).max + ); + } + + uint256 ethValue = 0; + if (quote.baseToken == NATIVE_TOKEN) { + ethValue = quote.effectiveBaseTokenAmount; + } + _transfer( + address(this), transferType, address(quote.baseToken), givenAmount + ); + uint256 balanceBefore = _balanceOf(quote.trader, quote.quoteToken); + IHashflowRouter(hashflowRouter).tradeRFQT{value: ethValue}(quote); + uint256 balanceAfter = _balanceOf(quote.trader, quote.quoteToken); + calculatedAmount = balanceAfter - balanceBefore; + } + + function _decodeData(bytes calldata data) + internal + pure + returns ( + IHashflowRouter.RFQTQuote memory quote, + bool approvalNeeded, + TransferType transferType + ) + { + if (data.length != 327) { + revert HashflowExecutor__InvalidDataLength(); + } + + transferType = TransferType(uint8(data[0])); + approvalNeeded = data[1] != 0; + + quote.pool = address(bytes20(data[2:22])); + quote.externalAccount = address(bytes20(data[22:42])); + quote.trader = address(bytes20(data[42:62])); + // Assumes we never set the effectiveTrader when requesting a quote. + quote.effectiveTrader = quote.trader; + quote.baseToken = address(bytes20(data[62:82])); + quote.quoteToken = address(bytes20(data[82:102])); + // Not included in the calldata. Will be set in the swap function. + quote.effectiveBaseTokenAmount = 0; + quote.baseTokenAmount = uint256(bytes32(data[102:134])); + quote.quoteTokenAmount = uint256(bytes32(data[134:166])); + quote.quoteExpiry = uint256(bytes32(data[166:198])); + quote.nonce = uint256(bytes32(data[198:230])); + quote.txid = bytes32(data[230:262]); + quote.signature = data[262:327]; + } + + function _balanceOf(address trader, address token) + internal + view + returns (uint256 balance) + { + balance = token == NATIVE_TOKEN + ? trader.balance + : IERC20(token).balanceOf(trader); + } +} diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 887e81a..eceb410 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -19,7 +19,8 @@ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol"; import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {SafeCast as V4SafeCast} from + "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; import "../RestrictTransferFrom.sol"; @@ -43,7 +44,7 @@ contract UniswapV4Executor is { using SafeERC20 for IERC20; using CurrencyLibrary for Currency; - using SafeCast for *; + using V4SafeCast for *; using TransientStateLibrary for IPoolManager; using LibPrefixLengthEncodedByteArray for bytes; diff --git a/foundry/src/uniswap_x/UniswapXFiller.sol b/foundry/src/uniswap_x/UniswapXFiller.sol index 7826e3c..ce614b6 100644 --- a/foundry/src/uniswap_x/UniswapXFiller.sol +++ b/foundry/src/uniswap_x/UniswapXFiller.sol @@ -60,10 +60,9 @@ contract UniswapXFiller is AccessControl, IReactorCallback { ResolvedOrder[] calldata resolvedOrders, bytes calldata callbackData ) external onlyRole(REACTOR_ROLE) { - require( - resolvedOrders.length == 1, - UniswapXFiller__BatchExecutionNotSupported() - ); + if (resolvedOrders.length != 1) { + revert UniswapXFiller__BatchExecutionNotSupported(); + } ResolvedOrder memory order = resolvedOrders[0]; diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index c2ca63d..f078f36 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -55,6 +55,7 @@ contract Constants is Test, BaseConstants { address WTAO_ADDR = address(0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44); address BSGG_ADDR = address(0xdA16Cf041E2780618c49Dbae5d734B89a6Bac9b3); address GHO_ADDR = address(0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f); + address ONDO_ADDR = address(0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3); // Maverick v2 address MAVERICK_V2_FACTORY = 0x0A7e848Aca42d879EF06507Fca0E7b33A0a63c1e; @@ -124,6 +125,12 @@ contract Constants is Test, BaseConstants { // Permit2 address PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + // 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/TestUtils.sol b/foundry/test/TestUtils.sol index 3399c9f..c062cf8 100644 --- a/foundry/test/TestUtils.sol +++ b/foundry/test/TestUtils.sol @@ -26,4 +26,26 @@ contract TestUtils is Test { revert("Test calldata not found"); } + + /// @notice Export the runtime bytecode of a deployed contract to a JSON file. + /// @dev + /// This function captures the runtime bytecode (including immutables) of the deployed + /// contract at `contractAddr` and writes it to a JSON file under `test/.runtime.json`. + /// The resulting file is intended to be used for SDK testing in another repository and + /// should be copied there. It **should not** be committed in this repository. + /// @param contractAddr The address of the deployed contract to extract runtime bytecode from. + /// @param contractName The base filename for the exported JSON file. + function exportRuntimeBytecode( + address contractAddr, + string memory contractName + ) internal { + bytes memory runtime = contractAddr.code; + string memory hexCode = vm.toString(runtime); + string memory json = + string.concat('{"runtimeBytecode":"', hexCode, '"}'); + + string memory path = + string.concat("test/", contractName, ".runtime.json"); + vm.writeFile(path, json); + } } diff --git a/foundry/test/TychoRouterProtocolIntegration.t.sol b/foundry/test/TychoRouterProtocolIntegration.t.sol index f4f4ad7..267708e 100644 --- a/foundry/test/TychoRouterProtocolIntegration.t.sol +++ b/foundry/test/TychoRouterProtocolIntegration.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.26; import "./TychoRouterTestSetup.sol"; import "./protocols/UniswapV4Utils.sol"; +import "@src/executors/BebopExecutor.sol"; contract TychoRouterTestProtocolIntegration is TychoRouterTestSetup { function testMultiProtocolIntegration() public { diff --git a/foundry/test/TychoRouterSequentialSwap.t.sol b/foundry/test/TychoRouterSequentialSwap.t.sol index 748993c..8a25c0d 100644 --- a/foundry/test/TychoRouterSequentialSwap.t.sol +++ b/foundry/test/TychoRouterSequentialSwap.t.sol @@ -492,4 +492,95 @@ contract TychoRouterSequentialSwapTest is TychoRouterTestSetup { assertEq(balanceAfter - balanceBefore, 1949668893); assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } + + function testSequentialSwapWithUnwrapIntegration() public { + // Performs a sequential swap from USDC to ETH through WBTC using USV2 pools and unwrapping in + // the end + deal(USDC_ADDR, ALICE, 3_000_000_000); + uint256 balanceBefore = ALICE.balance; + + // Approve permit2 + vm.startPrank(ALICE); + IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); + bytes memory callData = + loadCallDataFromFile("test_sequential_swap_strategy_encoder_unwrap"); + (bool success,) = tychoRouterAddr.call(callData); + + vm.stopPrank(); + + uint256 balanceAfter = ALICE.balance; + + assertTrue(success, "Call Failed"); + assertEq(balanceAfter - balanceBefore, 1404194006633772805); + } +} + +contract TychoRouterSequentialSwapTestForBebop is TychoRouterTestSetup { + function getForkBlock() public pure override returns (uint256) { + return 23139046; + } + + function testUSV3BebopIntegration() public { + // Performs a sequential swap from WETH to WBTC through USDC using USV3 and Bebop RFQ + // + // WETH ──(USV3)──> USDC ───(Bebop RFQ)──> WBTC + + // The Bebop order expects: + // - 2021750881 USDC input -> 1672307 WBTC output + + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 1672307; + deal(WETH_ADDR, BOB, amountIn); + uint256 balanceBefore = IERC20(WBTC_ADDR).balanceOf(BOB); + + vm.startPrank(BOB); + IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max); + bytes memory callData = loadCallDataFromFile("test_uniswap_v3_bebop"); + (bool success,) = tychoRouterAddr.call(callData); + + vm.stopPrank(); + + uint256 balanceAfter = IERC20(WBTC_ADDR).balanceOf(BOB); + + assertTrue(success, "Call Failed"); + assertEq(balanceAfter - balanceBefore, expectedAmountOut); + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); + } +} + +contract TychoRouterSequentialSwapTestForHashflow is TychoRouterTestSetup { + function getForkBlock() public pure override returns (uint256) { + return 23175437; + } + + 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 Uniswap pool outputs: + // - 1 weth -> 4322430557 USDC + // The Hashflow tradeRFQT call expects: + // - 4308094737 USDC input -> 3714751 WBTC output + // The difference in USDC (14335820) will stay in the TychoRouter contract + + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 3714751; + 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); + assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 14335820); + } } diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 6f1193d..aadb3e1 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -4,8 +4,10 @@ pragma solidity ^0.8.26; // Executors 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 {HashflowExecutor} from "../src/executors/HashflowExecutor.sol"; import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol"; import {UniswapV2Executor} from "../src/executors/UniswapV2Executor.sol"; import { @@ -73,12 +75,14 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { CurveExecutor public curveExecutor; MaverickV2Executor public maverickv2Executor; BalancerV3Executor public balancerV3Executor; + BebopExecutor public bebopExecutor; + HashflowExecutor public hashflowExecutor; function getForkBlock() public view virtual returns (uint256) { return 22082754; } - function setUp() public { + function setUp() public virtual { uint256 forkBlock = getForkBlock(); vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); @@ -132,8 +136,11 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { maverickv2Executor = 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[](9); + address[] memory executors = new address[](11); executors[0] = address(usv2Executor); executors[1] = address(usv3Executor); executors[2] = address(pancakev3Executor); @@ -143,6 +150,8 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { executors[6] = address(curveExecutor); 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 608678c..a792acc 100644 --- a/foundry/test/assets/calldata.txt +++ b/foundry/test/assets/calldata.txt @@ -3,25 +3,25 @@ test_single_encoding_strategy_ekubo:5c4b639c000000000000000000000000000000000000 test_uniswap_v3_uniswap_v3:e21dd0d30000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599000bb83ede3eca2a72b3aecc820e955b36f38437d01395cbcdf9626bc03e24f779434178a73a0b4bad62ed000000692e234dae75c793f67a35089c9d99245e1c58470b2260fac5e5542a773aa44fbcfedf7c193bc2c599a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc299ac8ca7087fa4a2a1fb6357269965a2014abc35010100000000000000000000 test_balancer_v2_uniswap_v2:e21dd0d30000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000c80072c7183455a4c133ae270771860664b6b7ec320bb1c02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599a6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e004375dff511095cc5a197a54140a24efef3a416010000525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20102000000000000000000000000000000000000000000000000 test_sequential_swap_strategy_encoder_no_permit2:e21dd0d30000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a800525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d940004375dff511095cc5a197a54140a24efef3a416000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20102000000000000000000000000000000000000000000000000 -test_single_encoding_strategy_usv4_grouped_swap:30ace1b1000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000005064ff624d54346285543f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000068ceeb5800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068a7656000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041b6f4d77d44715b60b1e50611435a9d22d946f0410135d5994662102585ca86cd3078e6dfa12cd4c486eb951d641f80f751ac7ed3d14d70c00e8594b80d3db2a21c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009cf62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb486982508145454ce325ddbe47a25d4ec3d23119330000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000bb800003c001a6982508145454ce325ddbe47a25d4ec3d23119330061a80001f400000000 -test_single_encoding_strategy_usv4_eth_out:30ace1b100000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f81490b4f29aade00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000068ceeb5800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068a7656000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000413e4c265b4cff7b0ac5bd5dac89480c35fb824c998dee58c3b43caa67c12c40947e044bdd5906c5706323cf88d3c784bcd7b95b3608320c0c93667665207267941c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000bb800003c -test_sequential_swap_strategy_encoder:51bcc7b60000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068ceeb5900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068a7656100000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000004131ced80fde07982bb0067d792917bea42742f05e728c61a522e0f29e2c102682511340305f7effe2730ec16a4ff680039a7ca3101f624d6a783989841addeff01c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a800525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d940004375dff511095cc5a197a54140a24efef3a416000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20102000000000000000000000000000000000000000000000000 +test_single_encoding_strategy_usv4_grouped_swap:30ace1b1000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000005064ff624d54346285543f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000068c5498600000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000689dc38e00000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041c2f032a8dabe018a07aaeac09f01a4fc125d10e82cfec0a7f41dc852f7079fef7638232e29c9a8aa096c4f8426bb8e7c5ba0e7c843d91dd82de93e1deaadd0891c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000086f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb486982508145454ce325ddbe47a25d4ec3d23119330000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000bb800003c6982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000000000000000000000000000 +test_single_encoding_strategy_usv4_eth_out:30ace1b100000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f81490b4f29aade00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000068c5498600000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000689dc38e00000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000004103ac26c44fc4d4df9727b3f0fdfd479edc3182c418475309e49afabf305a59e125ed52f754c4ebf65b06c6d54780e56c83657f786b7a8f9f8620c9880bb625f51c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006cf62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000bb800003c0000000000000000000000000000000000000000 +test_sequential_swap_strategy_encoder:51bcc7b60000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068c5498700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000689dc38f00000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000416c5b3499fc44d5c798c7f0f5c87d373e7e68d3ca7fec2928974ae6158d6711e94f61c45f4632ec39a2c5f1b9e7d4435c1cfb21d5da9e7848ed5b22f4076b87021c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a800525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d940004375dff511095cc5a197a54140a24efef3a416000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20102000000000000000000000000000000000000000000000000 test_single_swap_strategy_encoder_no_permit2:5c4b639c0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000058e7926ee858a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000 test_single_swap_strategy_encoder_no_transfer_in:5c4b639c0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000058e7926ee858a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200010000000000000000000000000000 -test_single_encoding_strategy_usv4_eth_in:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000007e0a55d4322a6e93c2379c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068ceeb5800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068a7656000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041eb86152ccf129f0b5a10788005ed76d457dbbbf5807ed4afb22493de8e2efc89335a313cf83303647bdacc01daca2517be985b28eb9870f32a4ba23940a247f41c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080f62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330102cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330061a80001f4 -test_sequential_strategy_cyclic_swap:51bcc7b60000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ec8f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000068ceeb5900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068a7656100000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000410ebc7ee90b9d0d7057ffa610da85db0c62570246c313fbd78f64bdde329cd609132cb7eaeb9d3de9f69c00323eed969336ae5cd1268a173a2409b5b1a1fa46991c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f5640010000692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc28ad599c3a0ff1de082011efddc58f1908eb6e6d8000100000000000000000000 +test_single_encoding_strategy_usv4_eth_in:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000007e0a55d4322a6e93c2379c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068c5498600000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000689dc38e00000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041db6814fba1c6a08adaf472a6ad7fb4c3640ce75ddc503a65b390d0450d1f4fcc776138089c10b81b520ce4dc680cfc5b6847eeebd6c3e875a020a5989c97e1f71c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006cf62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330102cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc26982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000000000000000 +test_sequential_strategy_cyclic_swap:51bcc7b60000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ec8f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000068c5498700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000689dc38f00000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041a6194f97efe298258ff64df4aeff8fb1ac725ad3ffe4ce2c4083198c3235e9c64100eb172356c03a4528bc8f3e3615c06383ce79869b9d3cecf863d43920a66d1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f5640010000692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc28ad599c3a0ff1de082011efddc58f1908eb6e6d8000100000000000000000000 test_single_encoding_strategy_curve_st_eth:5c4b639c0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ae7ab96520de3a18e5e111b5eaab095312d7fe84000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000691d1499e622d69689cdf9004d05ec547d650ff211eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeae7ab96520de3a18e5e111b5eaab095312d7fe84dc24316b9ae028f1497c275eb9192a3ea0f670220100010002cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000 test_single test_encode_uniswap_v4_sequential_swap:4c9edd5852cd905f086c759e8383e09bff1e68b32260fac5e5542a773aa44fbcfedf7c193bc2c5990101cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000064000001001a2260fac5e5542a773aa44fbcfedf7c193bc2c599000bb800003c 6d70b85442ed96492800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006869398600000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006841b38e00000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041752ca399237fc5086ef89d5f6dabecfb4b43c0753ecfb7020a6a86045db423fd3be9565f79b511fe93f55f76f61b1ac8d786b04051110ca6cbe10bbf69901b871c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000 test_single_encoding_strategy_curve:5c4b639c0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000055c08ca52497e2f1534b59e2917bf524d4765257000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000691d1499e622d69689cdf9004d05ec547d650ff21155c08ca52497e2f1534b59e2917bf524d4765257c02aaa39b223fe8d0a0e5c4f27ead9083c756cc277146b0a1d08b6844376df6d9da99ba7f1b19e710201000100cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000 -test_single_swap_strategy_encoder_unwrap:30ace1b10000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000000000000000000000000000000000000068ceeb5b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068a7656300000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000419c2499f01625dda340bf9706d54d96c455a823ea6344eb5581f8b979cc541ff1498f97ec0a17a5e15482e71681b7a5a8809a433c0ea0ebbcdf8eb3b6ffaf06541b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139501000000000000000000000000000000 -test_single_swap_strategy_encoder_wrap:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000059fb7d3830e6fc064b00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068ceeb5b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068a7656300000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041066a1be656af457a357afd69ee31a7bbb0c8162da79a4d11f07006d1df16b59336b2c362045ccf048d7f7632b8cb207351807e3afc72f177958954d90b650f6e1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200010000000000000000000000000000 -test_split_output_cyclic_swap:7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005e703f4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000068ceeb5b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068a76563000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041b8c0507474c58eaee7bbe07be8ff03fdf3ca7a76194e8aba53a1a32aa9f199d1733daf2536d542f55f99bb2fbd28756ea67226dc8ec5982d40eba3f8aa0d07a41b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000139005700010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d013950100006e01009999992e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f4cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc288e6a0c2ddd26feeb64f039a2c41296fcb3f56400001006e01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc28ad599c3a0ff1de082011efddc58f1908eb6e6d8000100000000000000 -test_split_input_cyclic_swap:7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ef619b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000068ceeb5b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068a76563000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041b8c0507474c58eaee7bbe07be8ff03fdf3ca7a76194e8aba53a1a32aa9f199d1733daf2536d542f55f99bb2fbd28756ea67226dc8ec5982d40eba3f8aa0d07a41b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000139006e00019999992e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f56400100006e00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d80100005701000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2b4e16d0168e52d35cacd2c6185b44281ec28c9dccd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000100000000000000 -test_split_swap_strategy_encoder:7c5538460000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068ceeb5b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068a76563000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041f5fdd236caa1d3a10054e1b09666226fa43990d3a40fb81d8361d629493b421030a56df56426e7b8fa9408f225a7100b67552f2537ed5ab0b08a29be7a2307aa1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000164005700028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950000005700010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d013950000005702030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d5cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20101005701030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2010100000000000000000000000000000000000000000000000000000000 +test_single_swap_strategy_encoder_unwrap:30ace1b10000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000000000000000000000000000000000000068c5498800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000689dc39000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000004143e05de1c49f6a327a188ff031a90a35e2d9da546dcc398548f4278eacc8f0ee792d638996f17de2c867e5f9fcbf4017617b00a104b1bc0cceb02ef9ab6046351b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139501000000000000000000000000000000 +test_single_swap_strategy_encoder_wrap:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000059fb7d3830e6fc064b00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068c5498800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000689dc39000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000413cc1a94991d1f2a245b7e919d952e78654d94f7f052bca7e577cf81f51c10d63028de013e13a4bbb6566155af811db81fb8aba3005f544fec1237b484bd8b56e1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200010000000000000000000000000000 +test_split_output_cyclic_swap:7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005e703f4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000068c5498900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000689dc391000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041de211de543036b5720321011b80414de617ef5748c2d75836f40bcbdc32ca60c061680e48bf9ede91d3332ab34ef611b53da00d1aab6104a3e5db5acd90c27461b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000139005700010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d013950100006e01009999992e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f4cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc288e6a0c2ddd26feeb64f039a2c41296fcb3f56400001006e01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc28ad599c3a0ff1de082011efddc58f1908eb6e6d8000100000000000000 +test_split_input_cyclic_swap:7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ef619b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000068c5498900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000689dc391000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041de211de543036b5720321011b80414de617ef5748c2d75836f40bcbdc32ca60c061680e48bf9ede91d3332ab34ef611b53da00d1aab6104a3e5db5acd90c27461b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000139006e00019999992e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f56400100006e00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d80100005701000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2b4e16d0168e52d35cacd2c6185b44281ec28c9dccd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000100000000000000 +test_split_swap_strategy_encoder:7c5538460000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068c5498900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000689dc3910000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000410647cfcff7789872426770c224e88575116eff4a35a4edd6e0cbc2c0d752f6d734516d42e818d9fb4f2fb3ee84e9eae59f532c50e1025e5aa7f07db44c840ea31c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000164005700028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950000005700010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d013950000005702030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d5cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20101005701030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2010100000000000000000000000000000000000000000000000000000000 test_uniswap_v3_curve:e21dd0d30000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599000bb83ede3eca2a72b3aecc820e955b36f38437d01395cbcdf9626bc03e24f779434178a73a0b4bad62ed000000691d1499e622d69689cdf9004d05ec547d650ff2112260fac5e5542a773aa44fbcfedf7c193bc2c599dac17f958d2ee523a2206206994597c13d831ec7d51a44d3fae010294c616388b506acda1bfaae460301000102cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000 -test_multi_protocol:51bcc7b600000000000000000000000000000000000000000000005150ae84a8cdf000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a2958f36da71a9200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000005150ae84a8cdf000000000000000000000000000000000000000000000000000000000000068ceeb5800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068a7656000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041dce942a3692fafa67e1aae01b58b14bf5ccf20daf5e0bcb5a9145d2fd5a241455cd9cb2c3f1460f3826197c1b41eef7a0f0645f3f8bf8aacdcf57930d0e650b81c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022800525615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139501000072c7183455a4c133ae270771860664b6b7ec320bb1c02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599a6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e3ede3eca2a72b3aecc820e955b36f38437d01395010200691d1499e622d69689cdf9004d05ec547d650ff2112260fac5e5542a773aa44fbcfedf7c193bc2c599dac17f958d2ee523a2206206994597c13d831ec7d51a44d3fae010294c616388b506acda1bfaae4603010001023ede3eca2a72b3aecc820e955b36f38437d013950071a0cb889707d426a7a386870a03bc70d1b0697598013ede3eca2a72b3aecc820e955b36f38437d01395dac17f958d2ee523a2206206994597c13d831ec7a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000001a36e2eb1c432000000320080f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000001cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000bb800003c000000000000000000000000000000000000000000000000 +test_multi_protocol:51bcc7b600000000000000000000000000000000000000000000005150ae84a8cdf000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a2958f36da71a9200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000005150ae84a8cdf000000000000000000000000000000000000000000000000000000000000068c5498600000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000689dc38e00000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000004199c421072ab7a1626905f10fbe84ebe25cdef5bf6ba9347dc160bddb685ea8e533cf88c04e3d126a645f558080a2f67db7e0124471e83d39b848a98dc014b8141c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021400525615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139501000072c7183455a4c133ae270771860664b6b7ec320bb1c02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599a6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e3ede3eca2a72b3aecc820e955b36f38437d01395010200691d1499e622d69689cdf9004d05ec547d650ff2112260fac5e5542a773aa44fbcfedf7c193bc2c599dac17f958d2ee523a2206206994597c13d831ec7d51a44d3fae010294c616388b506acda1bfaae4603010001023ede3eca2a72b3aecc820e955b36f38437d013950071a0cb889707d426a7a386870a03bc70d1b0697598013ede3eca2a72b3aecc820e955b36f38437d01395dac17f958d2ee523a2206206994597c13d831ec7a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000001a36e2eb1c43200000032006cf62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000001cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000bb800003c000000000000000000000000 test_encode_balancer_v2:c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0102 test_ekubo_encode_swap_multi:01ca4f73fe97d0b987a0d12b39bbd562c779bab6f60000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000001a36e2eb1c43200000032 test_encode_uniswap_v4_sequential_swap:4c9edd5852cd905f086c759e8383e09bff1e68b32260fac5e5542a773aa44fbcfedf7c193bc2c5990101cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000064000001001a2260fac5e5542a773aa44fbcfedf7c193bc2c599000bb800003c @@ -32,5 +32,11 @@ test_encode_uniswap_v2:c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e test_encode_balancer_v3:7bc3485026ac48b6cf9baf0a377477fff5703af8c71ea051a5f82c67adcf634c36ffe6334793d24c85b2b559bc2d21104c4defdd6efca8a20343361d011d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e test_single_encoding_strategy_balancer_v3:5c4b639c0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000097ffedb80d4b2ca6105a07a4d90eb739c45a66600000000000000000000000030881baa943777f92dc934d53d3bfdf33382cab300000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000006503a6a84cd762d9707a21605b548aaab891562aab097ffedb80d4b2ca6105a07a4d90eb739c45a66630881baa943777f92dc934d53d3bfdf33382cab3f028ac624074d6793c36dc8a06ecec0f5a39a71800cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000 test_uniswap_v3_balancer_v3:e21dd0d3000000000000000000000000000000000000000000000000002386f26fc10000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000004a220e6096b25eadb88358cb44068a324825467500000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000d200692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599000bb83ede3eca2a72b3aecc820e955b36f38437d01395cbcdf9626bc03e24f779434178a73a0b4bad62ed0000006503a6a84cd762d9707a21605b548aaab891562aab2260fac5e5542a773aa44fbcfedf7c193bc2c5994a220e6096b25eadb88358cb44068a3248254675571bea0e99e139cd0b6b7d9352ca872dfe0d72dd01cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000 -test_single_swap_strategy_encoder:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000006d70b85442ed96492800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068ceeb5b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068a7656300000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041f5fdd236caa1d3a10054e1b09666226fa43990d3a40fb81d8361d629493b421030a56df56426e7b8fa9408f225a7100b67552f2537ed5ab0b08a29be7a2307aa1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000 +test_single_swap_strategy_encoder:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000006d70b85442ed96492800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068c5498800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000689dc39000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000414de2ca5fe787efb3b6c50757cacd1ac7bbb415317151afb85496990e3d323caf48506e20e18437f6bf0e82b4a336c6e05bf3a0a5f8bd0f2f60c73eb362f54a721c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000 +test_uniswap_v3_bebop:e21dd0d30000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000000000000198473000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000034400692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f5640000002d7d6bbde9174b1cdaa358d2cf4d57d1a9f7178fbffa0b86991c6218b36c1d19d4a2e9eb0ce3606eb482260fac5e5542a773aa44fbcfedf7c193bc2c599020c0000000000000000000000000000000000000000000000000000000000198473011d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e4dcebcba00000000000000000000000000000000000000000000000000000000689dcb3c0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000bee3211ab312a8d065c4fef0247448e17a8da0000000000000000000000000000000000000000000000000002901f2d62bc91b77000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000000000000000000000000000000000007881786100000000000000000000000000000000000000000000000000000000001984730000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000000000000000000000000000000000000000000000000000000000000000a02bc8495ad1c76c31d466ce719f80400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041f3a03b07f390cd707402912278414c46190ca8ca362dd218b9a58956178cb6ee0e5755db7abe02fe15d498d092d4c6865a5eb18486b3e45e27d50d34b87bf1e21c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +test_single_encoding_strategy_bebop_aggregate:5c4b639c00000000000000000000000000000000000000000000000000000004a817c800000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000003f5b138698d22b5cfda00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a1000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000897d6bbde9174b1cdaa358d2cf4d57d1a9f7178fbffa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48faba6f8e4a5e8ab82f62fe7c39859fa577269be3000200000000000000000000000000000000000000000000000000000004a817c80001d2068e04cf586f76eece7ba5beb779d7bb1474a1a2f7489300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000689b78880000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c0000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000005a060a5c2aaaaa2fe2cda34423cac76a84c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000002901f2d62bb356ca0000000000000000000000000000000000000000000000002901f2d62bb356cb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000044f83c726000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000589400da00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000003aa5f96046644f6e37a000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000004b51a26526ddbeec60000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000417ab4332f2b091d87d56d04eee35dd49452782c782de71608c0425c5ae41f1d7e147173851c870d76720ce07d45cd8622352716b1c7965819ee2bf8c573c499ae1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000410c8da2637aa929e11caff9afdfc4c489320c6dba77cc934d88ba8956e365fd1d48983087c6e474bbb828181cdfdd17317c4c9c3ee4bc98e3769d0c05cc7a285e1c00000000000000000000000000000000000000000000000000000000000000000000000000000000 +test_single_encoding_strategy_bebop:5c4b639c000000000000000000000000000000000000000000000000000000000bebc200000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be300000000000000000000000000000000000000000000000a8aea46aa4ec5c0f500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a10000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000002d7d6bbde9174b1cdaa358d2cf4d57d1a9f7178fbffa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48faba6f8e4a5e8ab82f62fe7c39859fa577269be3000c000000000000000000000000000000000000000000000000000000000bebc20001d2068e04cf586f76eece7ba5beb779d7bb1474a14dcebcba00000000000000000000000000000000000000000000000000000000689b548f0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000067336cec42645f55059eff241cb02ea5cc52ff86000000000000000000000000000000000000000000000000279ead5d9685f25b000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be3000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000a8aea46aa4ec5c0f5000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000000005230bcb979c81cebf94a3b5c08bcfa300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000414ce40058ff07f11d9224c2c8d1e58369e4a90173856202d8d2a17da48058ad683dedb742eda0d4c0cf04cf1c09138898dd7fd06f97268ea7f74ef9b42d29bf4c1b00000000000000000000000000000000000000000000000000000000000000000000000000000000 +test_sequential_swap_strategy_encoder_unwrap:51bcc7b600000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000068c5498700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000689dc38f00000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041192fc75d79a3d76bcf3c3d193cf769446abc98ff76ce2a1de183e7e46d80073836cd6ceedc30f98085188eab1098ca2f0ef03c25ebaa69cd2758988263e563c41b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a800525615deb798bb3e4dfa0139dfa1b3d433cc23b72fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48004375dff511095cc5a197a54140a24efef3a416bb2b8038a1640196fbe3e38816f3e67cba72d940000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d013950102000000000000000000000000000000000000000000000000 test_sequential_swap_usx:0101e21dd0d300000000000000000000000000000000000000000000006c6b935b8bbd4000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000769cfd80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006d9da78b6a5bedca287aa5d49613ba36b90c15c40000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470b6b175474e89094c44da98b954eedeac495271d0fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000643ede3eca2a72b3aecc820e955b36f38437d013955777d92f208679db4b9778590fa3cab3ac9e2168010000692e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48dac17f958d2ee523a2206206994597c13d831ec70000646d9da78b6a5bedca287aa5d49613ba36b90c15c43416cf6c708da44db2624d63ea0aaef7113527c6010100000000000000000000 +test_uniswap_v3_hashflow:e21dd0d30000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000000000000000000000000000000000000038aebf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001c800692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f56400000015b15cf58144ef33af1e14b5208015d11f9143e27b90201478eca1b93865dca0b9f325935eb123c8a4af011bee3211ab312a8d065c4fef0247448e17a8da000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb482260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000000000100c84f11000000000000000000000000000000000000000000000000000000000038aebf0000000000000000000000000000000000000000000000000000000068a47cd800000000000000000000000000000000000000000000000000000198c286fecb125000064000640000001747eb8c38ffffffffffffff0029642016edb36d00006ddb3b21fe8509e274ddf46c55209cdbf30360944abbca6569ed6b26740d052f419964dcb5a3bdb98b4ed1fb3642a2760b8312118599a962251f7a8f73fe4fbe1c000000000000000000000000000000000000000000000000 +test_single_encoding_strategy_hashflow:5c4b639c0000000000000000000000000000000000000000000000000000000100c84f11000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000000000000000000000000000000000000038aebf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000015b15cf58144ef33af1e14b5208015d11f9143e27b90001478eca1b93865dca0b9f325935eb123c8a4af011bee3211ab312a8d065c4fef0247448e17a8da000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb482260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000000000100c84f11000000000000000000000000000000000000000000000000000000000038aebf0000000000000000000000000000000000000000000000000000000068a47cd800000000000000000000000000000000000000000000000000000198c286fecb125000064000640000001747eb8c38ffffffffffffff0029642016edb36d00006ddb3b21fe8509e274ddf46c55209cdbf30360944abbca6569ed6b26740d052f419964dcb5a3bdb98b4ed1fb3642a2760b8312118599a962251f7a8f73fe4fbe1c0000000000 diff --git a/foundry/test/protocols/BalancerV2.t.sol b/foundry/test/protocols/BalancerV2.t.sol index 6c832d6..1540ce1 100644 --- a/foundry/test/protocols/BalancerV2.t.sol +++ b/foundry/test/protocols/BalancerV2.t.sol @@ -134,4 +134,9 @@ contract BalancerV2ExecutorTest is Constants, TestUtils { assertGt(balanceAfter, balanceBefore); assertEq(balanceAfter - balanceBefore, amountOut); } + + function testExportContract() public { + vm.skip(true); + exportRuntimeBytecode(address(balancerV2Exposed), "BalancerV2"); + } } diff --git a/foundry/test/protocols/Bebop.t.sol b/foundry/test/protocols/Bebop.t.sol new file mode 100644 index 0000000..34b59cc --- /dev/null +++ b/foundry/test/protocols/Bebop.t.sol @@ -0,0 +1,470 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "../TestUtils.sol"; +import "../TychoRouterTestSetup.sol"; +import "@src/executors/BebopExecutor.sol"; +import {Constants} from "../Constants.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Permit2TestHelper} from "../Permit2TestHelper.sol"; +import {SafeERC20} from + "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract BebopExecutorExposed is BebopExecutor { + constructor(address _bebopSettlement, address _permit2) + BebopExecutor(_bebopSettlement, _permit2) + {} + + function decodeData(bytes calldata data) + external + pure + returns ( + address tokenIn, + address tokenOut, + TransferType transferType, + uint8 partialFillOffset, + uint256 originalFilledTakerAmount, + bool approvalNeeded, + address receiver, + bytes memory bebopCalldata + ) + { + return _decodeData(data); + } +} + +contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils { + using SafeERC20 for IERC20; + + BebopExecutorExposed bebopExecutor; + + IERC20 WETH = IERC20(WETH_ADDR); + IERC20 USDC = IERC20(USDC_ADDR); + IERC20 DAI = IERC20(DAI_ADDR); + IERC20 WBTC = IERC20(WBTC_ADDR); + IERC20 ONDO = IERC20(ONDO_ADDR); + IERC20 USDT = IERC20(USDT_ADDR); + + function testDecodeData() public { + vm.createSelectFork(vm.rpcUrl("mainnet"), 22667985); + bebopExecutor = + new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS); + + bytes memory bebopCalldata = abi.encodePacked( + bytes4(0x4dcebcba), // swapSingle selector + hex"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000068470140" + ); + + uint256 originalAmountIn = 200000000; // 200 USDC + bytes memory params = abi.encodePacked( + USDC_ADDR, + ONDO_ADDR, + uint8(RestrictTransferFrom.TransferType.Transfer), + uint8(2), + originalAmountIn, + true, + address(123), + bebopCalldata + ); + + ( + address tokenIn, + address tokenOut, + RestrictTransferFrom.TransferType transferType, + uint8 decodedPartialFillOffset, + uint256 decodedOriginalAmountIn, + bool decodedApprovalNeeded, + address decodedReceiver, + bytes memory decodedBebopCalldata + ) = bebopExecutor.decodeData(params); + + assertEq(tokenIn, USDC_ADDR, "tokenIn mismatch"); + assertEq(tokenOut, ONDO_ADDR, "tokenOut mismatch"); + assertEq( + uint8(transferType), + uint8(RestrictTransferFrom.TransferType.Transfer), + "transferType mismatch" + ); + assertEq( + keccak256(decodedBebopCalldata), + keccak256(bebopCalldata), + "bebopCalldata mismatch" + ); + assertEq(decodedPartialFillOffset, 2, "partialFillOffset mismatch"); + assertEq( + decodedOriginalAmountIn, + originalAmountIn, + "originalAmountIn mismatch" + ); + assertTrue(decodedApprovalNeeded, "approvalNeeded should be true"); + assertEq(decodedReceiver, address(123), "receiver mismatch"); + } + + // Single Order Tests + function testSingleOrder() public { + // 1 WETH -> WBTC + vm.createSelectFork(vm.rpcUrl("mainnet"), 23124275); + + bebopExecutor = + new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS); + + // Quote made manually using the BebopExecutor as the taker and receiver + bytes memory bebopCalldata = + hex"4dcebcba00000000000000000000000000000000000000000000000000000000689b137a0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000bee3211ab312a8d065c4fef0247448e17a8da000000000000000000000000000000000000000000000000000279ead5d9683d8a5000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000037337c0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000000000000000000000000000000000000000000000f71248bc6c123bbf12adc837470f75640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000418e9b0fb72ed9b86f7a7345026269c02b9056efcdfb67a377c7ff6c4a62a4807a7671ae759edf29aea1b2cb8efc8659e3aedac72943cd3607985a1849256358641c00000000000000000000000000000000000000000000000000000000000000"; + address tokenIn = WETH_ADDR; + address tokenOut = WBTC_ADDR; + RestrictTransferFrom.TransferType transferType = + RestrictTransferFrom.TransferType.None; + uint8 partialFillOffset = 12; + uint256 amountIn = 1000000000000000000; + bool approvalNeeded = true; + uint256 expectedAmountOut = 3617660; + + deal(tokenIn, address(bebopExecutor), amountIn); + + bytes memory params = abi.encodePacked( + tokenIn, + tokenOut, + transferType, + partialFillOffset, + amountIn, + approvalNeeded, + address(bebopExecutor), + bebopCalldata + ); + + uint256 initialTokenOutBalance = + IERC20(tokenOut).balanceOf(address(bebopExecutor)); + + uint256 amountOut = bebopExecutor.swap(amountIn, params); + + assertEq(amountOut, expectedAmountOut, "Incorrect amount out"); + assertEq( + IERC20(tokenOut).balanceOf(address(bebopExecutor)) + - initialTokenOutBalance, + expectedAmountOut, + "WBTC should be at receiver" + ); + assertEq( + IERC20(tokenIn).balanceOf(address(bebopExecutor)), + 0, + "WETH left in executor" + ); + } + + function testSingleOrderSellingETH() public { + // 1 WETH -> WBTC + vm.createSelectFork(vm.rpcUrl("mainnet"), 23124275); + + bebopExecutor = + new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS); + + // Quote made manually using the BebopExecutor as the taker and receiver + bytes memory bebopCalldata = + hex"4dcebcba00000000000000000000000000000000000000000000000000000000689ca0cd0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f0000000000000000000000000000000000000000000000002a65384e77863d8e000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000003a96a10000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000000000000000000000000000000000000000000001c6d9e514c7a64e5c0e239b532e1a3ea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041905d474b362c4a7c901c6a4ccb5c30670a0c602456f52761b47a0a35fc3944ec1fa224bc3bc6e8925cb15258efad2cf79e22ce9720f2302d4a1a2811c54fb4341c00000000000000000000000000000000000000000000000000000000000000"; + address tokenIn = address(0); + address tokenOut = WBTC_ADDR; + RestrictTransferFrom.TransferType transferType = + RestrictTransferFrom.TransferType.None; + uint8 partialFillOffset = 12; + uint256 amountIn = 1000000000000000000; + bool approvalNeeded = false; + uint256 expectedAmountOut = 3839649; + + vm.deal(address(bebopExecutor), amountIn); + + bytes memory params = abi.encodePacked( + tokenIn, + tokenOut, + transferType, + partialFillOffset, + amountIn, + approvalNeeded, + address(bebopExecutor), + bebopCalldata + ); + + uint256 initialTokenOutBalance = + IERC20(tokenOut).balanceOf(address(bebopExecutor)); + + uint256 amountOut = bebopExecutor.swap(amountIn, params); + + assertEq(amountOut, expectedAmountOut, "Incorrect amount out"); + assertEq( + IERC20(tokenOut).balanceOf(address(bebopExecutor)) + - initialTokenOutBalance, + expectedAmountOut, + "WBTC should be at receiver" + ); + assertEq(address(bebopExecutor).balance, 0, "ETH left in executor"); + } + + function testSingleOrder_PartialFill() public { + // 0.5 WETH -> WBTC with a quote for 1 WETH + vm.createSelectFork(vm.rpcUrl("mainnet"), 23124275); + + bebopExecutor = + new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS); + + // Quote made manually using the BebopExecutor as the taker and receiver (the same as testSingleOrder) + bytes memory bebopCalldata = + hex"4dcebcba00000000000000000000000000000000000000000000000000000000689b137a0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000bee3211ab312a8d065c4fef0247448e17a8da000000000000000000000000000000000000000000000000000279ead5d9683d8a5000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000037337c0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000000000000000000000000000000000000000000000f71248bc6c123bbf12adc837470f75640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000418e9b0fb72ed9b86f7a7345026269c02b9056efcdfb67a377c7ff6c4a62a4807a7671ae759edf29aea1b2cb8efc8659e3aedac72943cd3607985a1849256358641c00000000000000000000000000000000000000000000000000000000000000"; + address tokenIn = WETH_ADDR; + address tokenOut = WBTC_ADDR; + RestrictTransferFrom.TransferType transferType = + RestrictTransferFrom.TransferType.None; + uint8 partialFillOffset = 12; + // filling only half of the quote + uint256 amountIn = 1000000000000000000 / 2; + bool approvalNeeded = true; + uint256 expectedAmountOut = 3617660 / 2; + + deal(tokenIn, address(bebopExecutor), amountIn); + + bytes memory params = abi.encodePacked( + tokenIn, + tokenOut, + transferType, + partialFillOffset, + amountIn * 2, // this is the original amount in + approvalNeeded, + address(bebopExecutor), + bebopCalldata + ); + + uint256 initialTokenOutBalance = + IERC20(tokenOut).balanceOf(address(bebopExecutor)); + + uint256 amountOut = bebopExecutor.swap(amountIn, params); + + assertEq(amountOut, expectedAmountOut, "Incorrect partial amount out"); + assertEq( + IERC20(tokenOut).balanceOf(address(bebopExecutor)) + - initialTokenOutBalance, + expectedAmountOut, + "WETH should be at receiver" + ); + assertEq( + IERC20(tokenIn).balanceOf(address(bebopExecutor)), + 0, + "WBTC left in executor" + ); + } + + // Aggregate Order Tests + function testAggregateOrder() public { + // 20k USDC -> ONDO + vm.createSelectFork(vm.rpcUrl("mainnet"), 23126278); + bebopExecutor = + new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS); + + // Quote made manually using the BebopExecutor as the taker and receiver + bytes memory bebopCalldata = + hex"a2f7489300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000689b715d0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c00000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000005a0e0c07568b14a2d2c1b4d196000fc12bc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000002a65384e777abcfe0000000000000000000000000000000000000000000000002a65384e777abcff0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be300000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000236ddb7a7000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000002713a105900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000001e7dc63f0c1d9d93df4000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000021960567af238bcfd0000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041275c4b7c3df4bfa5c33da3443d817cc6ab568ec8b0fddc30445adff2e870cdcd7d8738e23b795c2fb1ee112e12716bcef1cf648bd1ded17ef10ae493d687322e1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004187ef3d632a640b09df5f39b2fb4c5b9afb7ab4f2782fee450b17e2363d27303b45ec55b154a63993106bfc28bb4accc10fb40f7927509fed554fac01a5d88bae1c00000000000000000000000000000000000000000000000000000000000000"; + address tokenIn = USDC_ADDR; + address tokenOut = ONDO_ADDR; + RestrictTransferFrom.TransferType transferType = + RestrictTransferFrom.TransferType.None; + uint8 partialFillOffset = 2; + // filling only half of the quote + uint256 amountIn = 20000000000; + bool approvalNeeded = true; + // maker amounts from quote + uint256 expectedAmountOut = + (8999445165322964385268 + 9912843438638420000000); + + deal(tokenIn, address(bebopExecutor), amountIn); + + bytes memory params = abi.encodePacked( + tokenIn, + tokenOut, + transferType, + partialFillOffset, + amountIn, + approvalNeeded, + address(bebopExecutor), + bebopCalldata + ); + + uint256 initialTokenOutBalance = + IERC20(tokenOut).balanceOf(address(bebopExecutor)); + + uint256 amountOut = bebopExecutor.swap(amountIn, params); + + assertEq(amountOut, expectedAmountOut, "Incorrect amount out"); + + assertEq( + IERC20(tokenOut).balanceOf(address(bebopExecutor)) + - initialTokenOutBalance, + expectedAmountOut, + "ONDO should be at receiver" + ); + assertEq( + IERC20(tokenIn).balanceOf(address(bebopExecutor)), + 0, + "USDC left in executor" + ); + } + + function testAggregateOrder_PartialFill() public { + // 10k USDC -> ONDO with a quote for 20k USDC + vm.createSelectFork(vm.rpcUrl("mainnet"), 23126278); + bebopExecutor = + new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS); + + // Quote made manually using the BebopExecutor as the taker and receiver + bytes memory bebopCalldata = + hex"a2f7489300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000689b715d0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c00000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000005a0e0c07568b14a2d2c1b4d196000fc12bc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000002a65384e777abcfe0000000000000000000000000000000000000000000000002a65384e777abcff0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be300000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000236ddb7a7000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000002713a105900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000001e7dc63f0c1d9d93df4000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000021960567af238bcfd0000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041275c4b7c3df4bfa5c33da3443d817cc6ab568ec8b0fddc30445adff2e870cdcd7d8738e23b795c2fb1ee112e12716bcef1cf648bd1ded17ef10ae493d687322e1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004187ef3d632a640b09df5f39b2fb4c5b9afb7ab4f2782fee450b17e2363d27303b45ec55b154a63993106bfc28bb4accc10fb40f7927509fed554fac01a5d88bae1c00000000000000000000000000000000000000000000000000000000000000"; + address tokenIn = USDC_ADDR; + address tokenOut = ONDO_ADDR; + RestrictTransferFrom.TransferType transferType = + RestrictTransferFrom.TransferType.None; + uint8 partialFillOffset = 2; + // filling only half of the quote + uint256 amountIn = 20000000000 / 2; + bool approvalNeeded = true; + // maker amounts from quote + uint256 expectedAmountOut = + (8999445165322964385268 + 9912843438638420000000) / 2; + + deal(tokenIn, address(bebopExecutor), amountIn); + + bytes memory params = abi.encodePacked( + tokenIn, + tokenOut, + transferType, + partialFillOffset, + amountIn * 2, // this is the original amount from the quote + approvalNeeded, + address(bebopExecutor), + bebopCalldata + ); + + uint256 initialTokenOutBalance = + IERC20(tokenOut).balanceOf(address(bebopExecutor)); + + uint256 amountOut = bebopExecutor.swap(amountIn, params); + + assertEq(amountOut, expectedAmountOut, "Incorrect amount out"); + + assertEq( + IERC20(tokenOut).balanceOf(address(bebopExecutor)) + - initialTokenOutBalance, + expectedAmountOut, + "ONDO should be at receiver" + ); + assertEq( + IERC20(tokenIn).balanceOf(address(bebopExecutor)), + 1, // because of integer division, there is 1 USDC left in the executor + "USDC left in executor" + ); + } + + function testInvalidDataLength() public { + vm.createSelectFork(vm.rpcUrl("mainnet"), 22667985); + bebopExecutor = + new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS); + + // Create a mock bebop calldata + bytes memory bebopCalldata = hex"47fb5891" // swapSingle selector + hex"1234567890abcdef"; // some mock data + + // Create params with correct length first + uint256 originalAmountIn = 1e18; + bytes memory validParams = abi.encodePacked( + WETH_ADDR, + USDC_ADDR, + uint8(RestrictTransferFrom.TransferType.Transfer), + uint8(2), + originalAmountIn, + true, + address(bebopExecutor), + bebopCalldata + ); + + // Verify valid params work + bebopExecutor.decodeData(validParams); + + // In the new format, adding extra bytes at the end doesn't fail + // because bebopCalldata is variable length at the end + // So test with extra bytes should not revert + bytes memory paramsWithExtra = abi.encodePacked(validParams, hex"ff"); + // This should work as the extra byte becomes part of bebopCalldata + bebopExecutor.decodeData(paramsWithExtra); + + // Try with insufficient data, should fail + bytes memory tooShortParams = abi.encodePacked( + WETH_ADDR, + USDC_ADDR, + uint8(RestrictTransferFrom.TransferType.Transfer) + ); + // Missing rest of the data + + vm.expectRevert(BebopExecutor.BebopExecutor__InvalidDataLength.selector); + bebopExecutor.decodeData(tooShortParams); + } +} + +contract TychoRouterForBebopTest is TychoRouterTestSetup { + // Override the fork block for Bebop tests + function getForkBlock() public pure override returns (uint256) { + return 22667986; + } + + function testSingleBebopIntegration() public { + // The calldata swaps 200 USDC for ONDO + address user = 0xd2068e04Cf586f76EEcE7BA5bEB779D7bB1474A1; + deal(USDC_ADDR, user, 200000000); // 200 USDC + uint256 expAmountOut = 194477331556159832309; // Expected ONDO amount from quote + + uint256 ondoBefore = IERC20(ONDO_ADDR).balanceOf(user); + vm.startPrank(user); + IERC20(USDC_ADDR).approve(tychoRouterAddr, type(uint256).max); + + bytes memory callData = + loadCallDataFromFile("test_single_encoding_strategy_bebop"); + + (bool success,) = tychoRouterAddr.call(callData); + + assertTrue(success, "Call Failed"); + + uint256 ondoReceived = IERC20(ONDO_ADDR).balanceOf(user) - ondoBefore; + assertEq(ondoReceived, expAmountOut); + assertEq( + IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), + 0, + "USDC left in router" + ); + + vm.stopPrank(); + } + + function testBebopAggregateIntegration() public { + // The calldata swaps 20k USDC for ONDO using multiple market makers + address user = 0xd2068e04Cf586f76EEcE7BA5bEB779D7bB1474A1; + deal(USDC_ADDR, user, 20000000000); // 20k USDC + uint256 expAmountOut = 18699321819466078474202; // Expected ONDO amount from quote + + uint256 ondoBefore = IERC20(ONDO_ADDR).balanceOf(user); + vm.startPrank(user); + IERC20(USDC_ADDR).approve(tychoRouterAddr, type(uint256).max); + + bytes memory callData = loadCallDataFromFile( + "test_single_encoding_strategy_bebop_aggregate" + ); + + (bool success,) = tychoRouterAddr.call(callData); + + assertTrue(success, "Call Failed"); + + uint256 ondoReceived = IERC20(ONDO_ADDR).balanceOf(user) - ondoBefore; + assertEq(ondoReceived, expAmountOut); + assertEq( + IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), + 0, + "USDC left in router" + ); + + vm.stopPrank(); + } +} diff --git a/foundry/test/protocols/Hashflow.t.sol b/foundry/test/protocols/Hashflow.t.sol new file mode 100644 index 0000000..dee68d8 --- /dev/null +++ b/foundry/test/protocols/Hashflow.t.sol @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "../TychoRouterTestSetup.sol"; +import "@src/executors/HashflowExecutor.sol"; +import "forge-std/Test.sol"; +import {Constants} from "../Constants.sol"; + +contract HashflowUtils is Test { + constructor() {} + + function encodeRfqtQuote( + IHashflowRouter.RFQTQuote memory quote, + bool approvalNeeded, + RestrictTransferFrom.TransferType transferType + ) internal pure returns (bytes memory) { + return abi.encodePacked( + uint8(transferType), // transferType (1 byte) + approvalNeeded, // needsApproval (1 byte) + quote.pool, // pool (20 bytes) + quote.externalAccount, // externalAccount (20 bytes) + quote.trader, // trader (20 bytes) + quote.baseToken, // baseToken (20 bytes) + quote.quoteToken, // quoteToken (20 bytes) + quote.baseTokenAmount, // baseTokenAmount (32 bytes) + quote.quoteTokenAmount, // quoteTokenAmount (32 bytes) + quote.quoteExpiry, // quoteExpiry (32 bytes) + quote.nonce, // nonce (32 bytes) + quote.txid, // txid (32 bytes) + quote.signature // signature data + ); + } + + function encodeRfqtQuoteWithDefaults(IHashflowRouter.RFQTQuote memory quote) + internal + pure + returns (bytes memory) + { + return + encodeRfqtQuote(quote, true, RestrictTransferFrom.TransferType.None); + } +} + +contract HashflowExecutorECR20Test is Constants, HashflowUtils { + using SafeERC20 for IERC20; + + HashflowExecutorExposed executor; + uint256 forkBlock; + + IERC20 WETH = IERC20(WETH_ADDR); + IERC20 USDC = IERC20(USDC_ADDR); + + function setUp() public { + forkBlock = 23188416; // Using expiry date: 1755766775, ECR20 + vm.createSelectFork("mainnet", forkBlock); + executor = new HashflowExecutorExposed(HASHFLOW_ROUTER, PERMIT2_ADDRESS); + } + + function testDecodeParams() public view { + IHashflowRouter.RFQTQuote memory expected_quote = rfqtQuote(); + bytes memory encodedQuote = encodeRfqtQuoteWithDefaults(expected_quote); + ( + IHashflowRouter.RFQTQuote memory quote, + bool approvalNeeded, + RestrictTransferFrom.TransferType transferType + ) = executor.decodeData(encodedQuote); + + assertEq(quote.pool, expected_quote.pool, "pool mismatch"); + assertEq( + quote.externalAccount, + expected_quote.externalAccount, + "externalAccount mismatch" + ); + assertEq(quote.trader, expected_quote.trader, "trader mismatch"); + assertEq( + quote.effectiveTrader, + expected_quote.effectiveTrader, + "effectiveTrader mismatch" + ); + assertEq( + quote.baseToken, expected_quote.baseToken, "baseToken mismatch" + ); + assertEq( + quote.quoteToken, expected_quote.quoteToken, "quoteToken mismatch" + ); + assertEq( + quote.effectiveBaseTokenAmount, + expected_quote.effectiveBaseTokenAmount, + "effectiveBaseTokenAmount mismatch" + ); + assertEq( + quote.baseTokenAmount, + expected_quote.baseTokenAmount, + "baseTokenAmount mismatch" + ); + assertEq( + quote.quoteTokenAmount, + expected_quote.quoteTokenAmount, + "quoteTokenAmount mismatch" + ); + assertEq( + quote.quoteExpiry, + expected_quote.quoteExpiry, + "quoteExpiry mismatch" + ); + assertEq(quote.nonce, expected_quote.nonce, "nonce mismatch"); + assertEq(quote.txid, expected_quote.txid, "txid mismatch"); + assertEq( + quote.signature, expected_quote.signature, "signature mismatch" + ); + assertEq(approvalNeeded, true, "Approval flag mismatch"); + assertEq( + uint8(transferType), + uint8(RestrictTransferFrom.TransferType.None), + "Transfer type mismatch" + ); + } + + function testDecodeParamsInvalidDataLength() public { + bytes memory invalidData = new bytes(10); + vm.expectRevert(HashflowExecutor__InvalidDataLength.selector); + executor.decodeData(invalidData); + } + + function testSwapNoSlippage() public { + address trader = address(ALICE); + IHashflowRouter.RFQTQuote memory quote = rfqtQuote(); + uint256 amountIn = quote.baseTokenAmount; + bytes memory encodedQuote = encodeRfqtQuoteWithDefaults(quote); + + deal(WETH_ADDR, address(executor), amountIn); + uint256 balanceBefore = USDC.balanceOf(trader); + + vm.prank(trader); + uint256 amountOut = executor.swap(amountIn, encodedQuote); + + uint256 balanceAfter = USDC.balanceOf(trader); + assertGt(balanceAfter, balanceBefore); + assertEq(balanceAfter - balanceBefore, amountOut); + assertEq(amountOut, quote.quoteTokenAmount); + } + + function testSwapRouterAmountUnderQuoteAmount() public { + address trader = address(ALICE); + IHashflowRouter.RFQTQuote memory quote = rfqtQuote(); + uint256 amountIn = quote.baseTokenAmount - 1; + bytes memory encodedQuote = encodeRfqtQuoteWithDefaults(quote); + + deal(WETH_ADDR, address(executor), amountIn); + uint256 balanceBefore = USDC.balanceOf(trader); + + vm.prank(trader); + uint256 amountOut = executor.swap(amountIn, encodedQuote); + + uint256 balanceAfter = USDC.balanceOf(trader); + assertGt(balanceAfter, balanceBefore); + assertEq(balanceAfter - balanceBefore, amountOut); + assertLt(amountOut, quote.quoteTokenAmount); + } + + function testSwapRouterAmountOverQuoteAmount() public { + address trader = address(ALICE); + IHashflowRouter.RFQTQuote memory quote = rfqtQuote(); + uint256 amountIn = quote.baseTokenAmount + 1; + bytes memory encodedQuote = encodeRfqtQuoteWithDefaults(quote); + + deal(WETH_ADDR, address(executor), amountIn); + uint256 balanceBefore = USDC.balanceOf(trader); + + vm.prank(trader); + uint256 amountOut = executor.swap(amountIn, encodedQuote); + + uint256 balanceAfter = USDC.balanceOf(trader); + assertGt(balanceAfter, balanceBefore); + assertEq(balanceAfter - balanceBefore, amountOut); + assertEq(amountOut, quote.quoteTokenAmount); + } + + function rfqtQuote() + internal + view + returns (IHashflowRouter.RFQTQuote memory) + { + return IHashflowRouter.RFQTQuote({ + pool: address(0x5d8853028fbF6a2da43c7A828cc5f691E9456B44), + externalAccount: address(0x9bA0CF1588E1DFA905eC948F7FE5104dD40EDa31), + trader: address(ALICE), + effectiveTrader: address(ALICE), + baseToken: WETH_ADDR, + quoteToken: USDC_ADDR, + effectiveBaseTokenAmount: 0, + baseTokenAmount: 1000000000000000000, + quoteTokenAmount: 4286117034, + quoteExpiry: 1755766775, + nonce: 1755766744988, + txid: bytes32( + uint256( + 0x12500006400064000186078c183380ffffffffffffff00296d737ff6ae950000 + ) + ), + signature: hex"649d31cd74f1b11b4a3b32bd38c2525d78ce8f23bc2eaf7700899c3a396d3a137c861737dc780fa154699eafb3108a34cbb2d4e31a6f0623c169cc19e0fa296a1c" + }); + } +} + +contract HashflowExecutorNativeTest is Constants, HashflowUtils { + using SafeERC20 for IERC20; + + HashflowExecutorExposed executor; + uint256 forkBlock; + + IERC20 WETH = IERC20(WETH_ADDR); + IERC20 USDC = IERC20(USDC_ADDR); + + function setUp() public { + forkBlock = 23188504; // Using expiry date: 1755767859, Native + vm.createSelectFork("mainnet", forkBlock); + executor = new HashflowExecutorExposed(HASHFLOW_ROUTER, PERMIT2_ADDRESS); + } + + function testSwapNoSlippage() public { + address trader = address(ALICE); + IHashflowRouter.RFQTQuote memory quote = rfqtQuote(); + uint256 amountIn = quote.baseTokenAmount; + bytes memory encodedQuote = encodeRfqtQuoteWithDefaults(quote); + + vm.deal(address(executor), amountIn); + uint256 balanceBefore = USDC.balanceOf(trader); + + vm.prank(trader); + uint256 amountOut = executor.swap(amountIn, encodedQuote); + + uint256 balanceAfter = USDC.balanceOf(trader); + assertGt(balanceAfter, balanceBefore); + assertEq(balanceAfter - balanceBefore, amountOut); + assertEq(amountOut, quote.quoteTokenAmount); + } + + function rfqtQuote() + internal + view + returns (IHashflowRouter.RFQTQuote memory) + { + return IHashflowRouter.RFQTQuote({ + pool: address(0x713DC4Df480235dBe2fB766E7120Cbd4041Dcb58), + externalAccount: address(0x111BB8c3542F2B92fb41B8d913c01D3788431111), + trader: address(ALICE), + effectiveTrader: address(ALICE), + baseToken: address(0x0000000000000000000000000000000000000000), + quoteToken: USDC_ADDR, + effectiveBaseTokenAmount: 0, + baseTokenAmount: 10000000000000000, + quoteTokenAmount: 42586008, + quoteExpiry: 1755767859, + nonce: 1755767819299, + txid: bytes32( + uint256( + 0x1250000640006400018380fd594810ffffffffffffff00296d83e467cddd0000 + ) + ), + signature: hex"63c1c9c7d6902d1d4d2ae82777015433ef08366dde1c579a8c4cbc01059166064246f61f15b2cb130be8f2b28ea40d2c3586ef0133647fefa30003e70ffbd6131b" + }); + } +} + +contract HashflowExecutorExposed is HashflowExecutor { + constructor(address _hashflowRouter, address _permit2) + HashflowExecutor(_hashflowRouter, _permit2) + {} + + function decodeData(bytes calldata data) + external + pure + returns ( + IHashflowRouter.RFQTQuote memory quote, + bool approvalNeeded, + TransferType transferType + ) + { + return _decodeData(data); + } +} + +contract TychoRouterSingleSwapTestForHashflow is TychoRouterTestSetup { + function getForkBlock() public pure override returns (uint256) { + return 23175437; + } + + function testHashflowIntegration() 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_single_encoding_strategy_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/src/encoding/errors.rs b/src/encoding/errors.rs index 2949261..071b1b3 100644 --- a/src/encoding/errors.rs +++ b/src/encoding/errors.rs @@ -1,6 +1,7 @@ use std::{io, str::Utf8Error}; use thiserror::Error; +use tycho_common::simulation::errors::SimulationError; /// Represents the outer-level, user-facing errors of the tycho-execution encoding package. /// @@ -41,3 +42,15 @@ impl From for EncodingError { EncodingError::FatalError(err.to_string()) } } + +impl From for EncodingError { + fn from(err: SimulationError) -> Self { + match err { + SimulationError::FatalError(err_msg) => EncodingError::FatalError(err_msg), + SimulationError::InvalidInput(err_msg, ..) => EncodingError::InvalidInput(err_msg), + SimulationError::RecoverableError(error_msg) => { + EncodingError::RecoverableError(error_msg) + } + } + } +} 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/group_swaps.rs b/src/encoding/evm/group_swaps.rs index f513f29..26a4ff6 100644 --- a/src/encoding/evm/group_swaps.rs +++ b/src/encoding/evm/group_swaps.rs @@ -12,15 +12,15 @@ use crate::encoding::{evm::constants::GROUPABLE_PROTOCOLS, models::Swap}; /// * `swaps`: Vec, the sequence of swaps to be executed as a group /// * `split`: f64, the split percentage of the first swap in the group #[derive(Clone, Debug)] -pub struct SwapGroup<'a> { +pub struct SwapGroup { pub token_in: Bytes, pub token_out: Bytes, pub protocol_system: String, - pub swaps: Vec>, + pub swaps: Vec, pub split: f64, } -impl<'a> PartialEq for SwapGroup<'a> { +impl PartialEq for SwapGroup { fn eq(&self, other: &Self) -> bool { self.token_in == other.token_in && self.token_out == other.token_out && @@ -34,7 +34,7 @@ impl<'a> PartialEq for SwapGroup<'a> { /// /// An example where this applies is the case of USV4, which uses a PoolManager contract /// to save token transfers on consecutive swaps. -pub fn group_swaps<'a>(swaps: &'a Vec>) -> Vec> { +pub fn group_swaps(swaps: &Vec) -> Vec { let mut grouped_swaps: Vec = Vec::new(); let mut current_group: Option = None; let mut last_swap_protocol = "".to_string(); @@ -87,7 +87,7 @@ mod tests { use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use super::*; - use crate::encoding::models::Swap; + use crate::encoding::models::SwapBuilder; fn weth() -> Bytes { Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec()) @@ -105,41 +105,26 @@ mod tests { let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); - let swap_weth_wbtc = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v4_hooks".to_string(), - ..Default::default() - }, - token_in: weth.clone(), - token_out: wbtc.clone(), - // This represents the remaining 50%, but to avoid any rounding errors we set this to - // 0 to signify "the remainder of the WETH value". It should still be very close to 50% - split: 0f64, - user_data: None, - protocol_state: None, - }; - let swap_wbtc_usdc = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v4_hooks".to_string(), - ..Default::default() - }, - token_in: wbtc.clone(), - token_out: usdc.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; - let swap_usdc_dai = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: usdc.clone(), - token_out: dai.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + let swap_weth_wbtc = SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v4_hooks".to_string(), ..Default::default() }, + weth.clone(), + wbtc.clone(), + ) + .build(); + + let swap_wbtc_usdc = SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v4_hooks".to_string(), ..Default::default() }, + wbtc.clone(), + usdc.clone(), + ) + .build(); + + let swap_usdc_dai = SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v2".to_string(), ..Default::default() }, + usdc.clone(), + dai.clone(), + ) + .build(); let swaps = vec![swap_weth_wbtc.clone(), swap_wbtc_usdc.clone(), swap_usdc_dai.clone()]; let grouped_swaps = group_swaps(&swaps); @@ -179,52 +164,34 @@ mod tests { let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); - let swap_wbtc_weth = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v4_hooks".to_string(), - ..Default::default() - }, - token_in: wbtc.clone(), - token_out: weth.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; - let swap_weth_usdc = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v4_hooks".to_string(), - ..Default::default() - }, - token_in: weth.clone(), - token_out: usdc.clone(), - split: 0.5f64, - user_data: None, - protocol_state: None, - }; - let swap_weth_dai = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v4_hooks".to_string(), - ..Default::default() - }, - token_in: weth.clone(), - token_out: dai.clone(), - // This represents the remaining 50%, but to avoid any rounding errors we set this to - // 0 to signify "the remainder of the WETH value". It should still be very close to 50% - split: 0f64, - user_data: None, - protocol_state: None, - }; - let swap_dai_usdc = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v4_hooks".to_string(), - ..Default::default() - }, - token_in: dai.clone(), - token_out: usdc.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + let swap_wbtc_weth = SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v4_hooks".to_string(), ..Default::default() }, + wbtc.clone(), + weth.clone(), + ) + .build(); + let swap_weth_usdc = SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v4_hooks".to_string(), ..Default::default() }, + weth.clone(), + usdc.clone(), + ) + .split(0.5f64) + .build(); + let swap_weth_dai = SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v4_hooks".to_string(), ..Default::default() }, + weth.clone(), + dai.clone(), + ) + .build(); + // Split 0 represents the remaining 50%, but to avoid any rounding errors we set this to + // 0 to signify "the remainder of the WETH value". It should still be very close to 50% + + let swap_dai_usdc = SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v4_hooks".to_string(), ..Default::default() }, + dai.clone(), + usdc.clone(), + ) + .build(); let swaps = vec![ swap_wbtc_weth.clone(), swap_weth_usdc.clone(), @@ -275,52 +242,38 @@ mod tests { let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); - let swap_weth_wbtc = Swap { - component: ProtocolComponent { + let swap_weth_wbtc = SwapBuilder::new( + ProtocolComponent { protocol_system: "vm:balancer_v3".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: wbtc.clone(), - split: 0.5f64, - user_data: None, - protocol_state: None, - }; - let swap_wbtc_usdc = Swap { - component: ProtocolComponent { + weth.clone(), + wbtc.clone(), + ) + .split(0.5f64) + .build(); + + let swap_wbtc_usdc = SwapBuilder::new( + ProtocolComponent { protocol_system: "vm:balancer_v3".to_string(), ..Default::default() }, - token_in: wbtc.clone(), - token_out: usdc.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; - let swap_weth_dai = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v4_hooks".to_string(), - ..Default::default() - }, - token_in: weth.clone(), - token_out: dai.clone(), - // This represents the remaining 50%, but to avoid any rounding errors we set this to - // 0 to signify "the remainder of the WETH value". It should still be very close to 50% - split: 0f64, - user_data: None, - protocol_state: None, - }; - let swap_dai_usdc = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v4_hooks".to_string(), - ..Default::default() - }, - token_in: dai.clone(), - token_out: usdc.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + wbtc.clone(), + usdc.clone(), + ) + .build(); + let swap_weth_dai = SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v4_hooks".to_string(), ..Default::default() }, + weth.clone(), + dai.clone(), + ) + .build(); + let swap_dai_usdc = SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v4_hooks".to_string(), ..Default::default() }, + dai.clone(), + usdc.clone(), + ) + .build(); let swaps = vec![ swap_weth_wbtc.clone(), diff --git a/src/encoding/evm/mod.rs b/src/encoding/evm/mod.rs index b0672f2..ae6f82d 100644 --- a/src/encoding/evm/mod.rs +++ b/src/encoding/evm/mod.rs @@ -5,5 +5,7 @@ mod encoding_utils; mod group_swaps; pub mod strategy_encoder; mod swap_encoder; +#[cfg(feature = "test-utils")] +pub mod testing_utils; pub mod tycho_encoders; pub mod utils; diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 4ce535d..fa47be8 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -237,10 +237,11 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { let grouped_swaps = group_swaps(&solution.swaps); - let mut wrap = false; + let (mut wrap, mut unwrap) = (false, false); if let Some(action) = &solution.native_action { - if action == &NativeAction::Wrap { - wrap = true + match *action { + NativeAction::Wrap => wrap = true, + NativeAction::Unwrap => unwrap = true, } } @@ -260,7 +261,7 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { let next_swap = grouped_swaps.get(i + 1); let (swap_receiver, next_swap_optimization) = self .transfer_optimization - .get_receiver(&solution.receiver, next_swap)?; + .get_receiver(&solution.receiver, next_swap, unwrap)?; next_in_between_swap_optimization_allowed = next_swap_optimization; let transfer = self @@ -544,7 +545,6 @@ mod tests { }; use super::*; - use crate::encoding::models::Swap; fn eth_chain() -> Chain { Chain::Ethereum @@ -565,8 +565,8 @@ mod tests { } mod single { - use super::*; + use crate::encoding::models::SwapBuilder; #[test] fn test_single_swap_strategy_encoder() { // Performs a single swap from WETH to DAI on a USV2 pool, with no grouping @@ -575,18 +575,16 @@ mod tests { let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); - let swap = Swap { - component: ProtocolComponent { + let swap = SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + weth.clone(), + dai.clone(), + ) + .build(); let swap_encoder_registry = get_swap_encoder_registry(); let encoder = SingleSwapStrategyEncoder::new( eth_chain(), @@ -637,18 +635,16 @@ mod tests { let checked_amount = BigUint::from_str("1_640_000000000000000000").unwrap(); - let swap = Swap { - component: ProtocolComponent { + let swap = SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + weth.clone(), + dai.clone(), + ) + .build(); let swap_encoder_registry = get_swap_encoder_registry(); let encoder = SingleSwapStrategyEncoder::new( eth_chain(), @@ -698,6 +694,7 @@ mod tests { mod sequential { use super::*; + use crate::encoding::models::SwapBuilder; #[test] fn test_sequential_swap_strategy_encoder_no_permit2() { @@ -709,30 +706,26 @@ mod tests { let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - let swap_weth_wbtc = Swap { - component: ProtocolComponent { + let swap_weth_wbtc = SwapBuilder::new( + ProtocolComponent { id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: wbtc.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; - let swap_wbtc_usdc = Swap { - component: ProtocolComponent { + weth.clone(), + wbtc.clone(), + ) + .build(); + let swap_wbtc_usdc = SwapBuilder::new( + ProtocolComponent { id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: wbtc.clone(), - token_out: usdc.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + wbtc.clone(), + usdc.clone(), + ) + .build(); let swap_encoder_registry = get_swap_encoder_registry(); let encoder = SequentialSwapStrategyEncoder::new( eth_chain(), @@ -790,6 +783,7 @@ mod tests { mod split { use super::*; + use crate::encoding::models::SwapBuilder; #[test] fn test_split_input_cyclic_swap() { @@ -805,8 +799,8 @@ mod tests { let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); // USDC -> WETH (Pool 1) - 60% of input - let swap_usdc_weth_pool1 = Swap { - component: ProtocolComponent { + let swap_usdc_weth_pool1 = SwapBuilder::new( + ProtocolComponent { id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3 * Pool 1 */ protocol_system: "uniswap_v3".to_string(), @@ -820,16 +814,15 @@ mod tests { }, ..Default::default() }, - token_in: usdc.clone(), - token_out: weth.clone(), - split: 0.6f64, // 60% of input - user_data: None, - protocol_state: None, - }; + usdc.clone(), + weth.clone(), + ) + .split(0.6f64) + .build(); // USDC -> WETH (Pool 2) - 40% of input (remaining) - let swap_usdc_weth_pool2 = Swap { - component: ProtocolComponent { + let swap_usdc_weth_pool2 = SwapBuilder::new( + ProtocolComponent { id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3 * Pool 2 */ protocol_system: "uniswap_v3".to_string(), @@ -843,16 +836,14 @@ mod tests { }, ..Default::default() }, - token_in: usdc.clone(), - token_out: weth.clone(), - split: 0f64, - user_data: None, // Remaining 40% - protocol_state: None, - }; + usdc.clone(), + weth.clone(), + ) + .build(); // WETH -> USDC (Pool 2) - let swap_weth_usdc_pool2 = Swap { - component: ProtocolComponent { + let swap_weth_usdc_pool2 = SwapBuilder::new( + ProtocolComponent { id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), /* USDC-WETH USV2 * Pool 2 */ protocol_system: "uniswap_v2".to_string(), @@ -866,13 +857,10 @@ mod tests { }, ..Default::default() }, - token_in: weth.clone(), - token_out: usdc.clone(), - split: 0.0f64, - user_data: None, - protocol_state: None, - }; - + weth.clone(), + usdc.clone(), + ) + .build(); let swap_encoder_registry = get_swap_encoder_registry(); let encoder = SplitSwapStrategyEncoder::new( eth_chain(), @@ -961,8 +949,8 @@ mod tests { let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - let swap_usdc_weth_v2 = Swap { - component: ProtocolComponent { + let swap_usdc_weth_v2 = SwapBuilder::new( + ProtocolComponent { id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), // USDC-WETH USV2 protocol_system: "uniswap_v2".to_string(), static_attributes: { @@ -975,15 +963,13 @@ mod tests { }, ..Default::default() }, - token_in: usdc.clone(), - token_out: weth.clone(), - split: 0.0f64, - user_data: None, - protocol_state: None, - }; + usdc.clone(), + weth.clone(), + ) + .build(); - let swap_weth_usdc_v3_pool1 = Swap { - component: ProtocolComponent { + let swap_weth_usdc_v3_pool1 = SwapBuilder::new( + ProtocolComponent { id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3 * Pool 1 */ protocol_system: "uniswap_v3".to_string(), @@ -997,17 +983,16 @@ mod tests { }, ..Default::default() }, - token_in: weth.clone(), - token_out: usdc.clone(), - split: 0.6f64, - user_data: None, - protocol_state: None, - }; + weth.clone(), + usdc.clone(), + ) + .split(0.6f64) + .build(); - let swap_weth_usdc_v3_pool2 = Swap { - component: ProtocolComponent { + let swap_weth_usdc_v3_pool2 = SwapBuilder::new( + ProtocolComponent { id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3 - * Pool 2 */ + * Pool 1 */ protocol_system: "uniswap_v3".to_string(), static_attributes: { let mut attrs = HashMap::new(); @@ -1019,12 +1004,10 @@ mod tests { }, ..Default::default() }, - token_in: weth.clone(), - token_out: usdc.clone(), - split: 0.0f64, - user_data: None, - protocol_state: None, - }; + weth.clone(), + usdc.clone(), + ) + .build(); let swap_encoder_registry = get_swap_encoder_registry(); let encoder = SplitSwapStrategyEncoder::new( diff --git a/src/encoding/evm/strategy_encoder/strategy_validators.rs b/src/encoding/evm/strategy_encoder/strategy_validators.rs index 6303335..50a51bd 100644 --- a/src/encoding/evm/strategy_encoder/strategy_validators.rs +++ b/src/encoding/evm/strategy_encoder/strategy_validators.rs @@ -197,7 +197,7 @@ mod tests { use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use super::*; - use crate::encoding::models::Swap; + use crate::encoding::models::{Swap, SwapBuilder}; #[test] fn test_validate_path_single_swap() { @@ -205,18 +205,16 @@ mod tests { let eth = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); - let swaps = vec![Swap { - component: ProtocolComponent { + let swaps = vec![SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }]; + weth.clone(), + dai.clone(), + ) + .build()]; let result = validator.validate_swap_path(&swaps, &weth, &dai, &None, ð, &weth); assert_eq!(result, Ok(())); } @@ -229,30 +227,27 @@ mod tests { let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let swaps = vec![ - Swap { - component: ProtocolComponent { + SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0.5f64, - user_data: None, - protocol_state: None, - }, - Swap { - component: ProtocolComponent { + weth.clone(), + dai.clone(), + ) + .split(0.5f64) + .build(), + SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: dai.clone(), - token_out: usdc.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }, + dai.clone(), + usdc.clone(), + ) + .build(), ]; let result = validator.validate_swap_path(&swaps, &weth, &usdc, &None, ð, &weth); assert_eq!(result, Ok(())); @@ -268,31 +263,28 @@ mod tests { let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); let disconnected_swaps = vec![ - Swap { - component: ProtocolComponent { + SwapBuilder::new( + ProtocolComponent { id: "pool1".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0.5, - user_data: None, - protocol_state: None, - }, + weth.clone(), + dai.clone(), + ) + .split(0.5f64) + .build(), // This swap is disconnected from the WETH->DAI path - Swap { - component: ProtocolComponent { + SwapBuilder::new( + ProtocolComponent { id: "pool2".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: wbtc.clone(), - token_out: usdc.clone(), - split: 0.0, - user_data: None, - protocol_state: None, - }, + wbtc.clone(), + usdc.clone(), + ) + .build(), ]; let result = validator.validate_swap_path(&disconnected_swaps, &weth, &usdc, &None, ð, &weth); @@ -310,30 +302,26 @@ mod tests { let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let cyclic_swaps = vec![ - Swap { - component: ProtocolComponent { + SwapBuilder::new( + ProtocolComponent { id: "pool1".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: usdc.clone(), - token_out: weth.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }, - Swap { - component: ProtocolComponent { + usdc.clone(), + weth.clone(), + ) + .build(), + SwapBuilder::new( + ProtocolComponent { id: "pool2".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: usdc.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }, + weth.clone(), + usdc.clone(), + ) + .build(), ]; // Test with USDC as both given token and checked token @@ -349,18 +337,17 @@ mod tests { let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - let unreachable_swaps = vec![Swap { - component: ProtocolComponent { + let unreachable_swaps = vec![SwapBuilder::new( + ProtocolComponent { id: "pool1".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 1.0, - user_data: None, - protocol_state: None, - }]; + weth.clone(), + dai.clone(), + ) + .split(1.0) + .build()]; let result = validator.validate_swap_path(&unreachable_swaps, &weth, &usdc, &None, ð, &weth); assert!(matches!( @@ -389,18 +376,16 @@ mod tests { let validator = SplitSwapValidator; let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); - let swaps = vec![Swap { - component: ProtocolComponent { + let swaps = vec![SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }]; + weth.clone(), + dai.clone(), + ) + .build()]; let result = validator.validate_split_percentages(&swaps); assert_eq!(result, Ok(())); } @@ -413,42 +398,38 @@ mod tests { // Valid case: Multiple swaps with proper splits (50%, 30%, remainder) let valid_swaps = vec![ - Swap { - component: ProtocolComponent { + SwapBuilder::new( + ProtocolComponent { id: "pool1".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0.5, - user_data: None, - protocol_state: None, - }, - Swap { - component: ProtocolComponent { + weth.clone(), + dai.clone(), + ) + .split(0.5) + .build(), + SwapBuilder::new( + ProtocolComponent { id: "pool2".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0.3, - user_data: None, - protocol_state: None, - }, - Swap { - component: ProtocolComponent { + weth.clone(), + dai.clone(), + ) + .split(0.3) + .build(), + SwapBuilder::new( + ProtocolComponent { id: "pool3".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0.0, // Remainder (20%) - user_data: None, - protocol_state: None, - }, + weth.clone(), + dai.clone(), + ) + .build(), ]; assert!(validator .validate_split_percentages(&valid_swaps) @@ -462,30 +443,28 @@ mod tests { let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let invalid_total_swaps = vec![ - Swap { - component: ProtocolComponent { + SwapBuilder::new( + ProtocolComponent { id: "pool1".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0.7, - user_data: None, - protocol_state: None, - }, - Swap { - component: ProtocolComponent { + weth.clone(), + dai.clone(), + ) + .split(0.7) + .build(), + SwapBuilder::new( + ProtocolComponent { id: "pool2".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0.3, - user_data: None, - protocol_state: None, - }, + weth.clone(), + dai.clone(), + ) + .split(0.3) + .build(), ]; assert!(matches!( validator.validate_split_percentages(&invalid_total_swaps), @@ -500,30 +479,27 @@ mod tests { let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let invalid_zero_position_swaps = vec![ - Swap { - component: ProtocolComponent { + SwapBuilder::new( + ProtocolComponent { id: "pool1".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0.0, - user_data: None, - protocol_state: None, - }, - Swap { - component: ProtocolComponent { + weth.clone(), + dai.clone(), + ) + .build(), + SwapBuilder::new( + ProtocolComponent { id: "pool2".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0.5, - user_data: None, - protocol_state: None, - }, + weth.clone(), + dai.clone(), + ) + .split(0.5) + .build(), ]; assert!(matches!( validator.validate_split_percentages(&invalid_zero_position_swaps), @@ -538,42 +514,38 @@ mod tests { let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let invalid_overflow_swaps = vec![ - Swap { - component: ProtocolComponent { + SwapBuilder::new( + ProtocolComponent { id: "pool1".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0.6, - user_data: None, - protocol_state: None, - }, - Swap { - component: ProtocolComponent { + weth.clone(), + dai.clone(), + ) + .split(0.6) + .build(), + SwapBuilder::new( + ProtocolComponent { id: "pool2".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0.5, - user_data: None, - protocol_state: None, - }, - Swap { - component: ProtocolComponent { + weth.clone(), + dai.clone(), + ) + .split(0.5) + .build(), + SwapBuilder::new( + ProtocolComponent { id: "pool3".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0.0, - user_data: None, - protocol_state: None, - }, + weth.clone(), + dai.clone(), + ) + .build(), ]; assert!(matches!( validator.validate_split_percentages(&invalid_overflow_swaps), @@ -588,18 +560,16 @@ mod tests { let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(); - let swaps = vec![Swap { - component: ProtocolComponent { + let swaps = vec![SwapBuilder::new( + ProtocolComponent { id: "pool1".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth.clone(), - token_out: usdc.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }]; + weth.clone(), + usdc.clone(), + ) + .build()]; let result = validator.validate_swap_path( &swaps, @@ -619,18 +589,16 @@ mod tests { let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(); - let swaps = vec![Swap { - component: ProtocolComponent { + let swaps = vec![SwapBuilder::new( + ProtocolComponent { id: "pool1".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: usdc.clone(), - token_out: weth.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }]; + usdc.clone(), + weth.clone(), + ) + .build()]; let result = validator.validate_swap_path( &swaps, diff --git a/src/encoding/evm/strategy_encoder/transfer_optimizations.rs b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs index 5a62bb2..5b57abc 100644 --- a/src/encoding/evm/strategy_encoder/transfer_optimizations.rs +++ b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs @@ -82,6 +82,7 @@ impl TransferOptimization { &self, solution_receiver: &Bytes, next_swap: Option<&SwapGroup>, + unwrap: bool, ) -> Result<(Bytes, bool), EncodingError> { if let Some(next) = next_swap { // if the protocol of the next swap supports transfer in optimization @@ -104,7 +105,11 @@ impl TransferOptimization { } } else { // last swap - there is no next swap - Ok((solution_receiver.clone(), false)) + if unwrap { + Ok((self.router_address.clone(), false)) + } else { + Ok((solution_receiver.clone(), false)) + } } } } @@ -116,7 +121,7 @@ mod tests { use tycho_common::models::protocol::ProtocolComponent; use super::*; - use crate::encoding::models::Swap; + use crate::encoding::models::SwapBuilder; fn weth() -> Bytes { Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec()) @@ -169,18 +174,16 @@ mod tests { #[case] expected_transfer: TransferType, ) { // The swap token is the same as the given token, which is not the native token - let swaps = vec![Swap { - component: ProtocolComponent { + let swaps = vec![SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v2".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), ..Default::default() }, - token_in: swap_token_in.clone(), - token_out: dai(), - split: 0f64, - user_data: None, - protocol_state: None, - }]; + swap_token_in.clone(), + dai(), + ) + .build()]; let swap = SwapGroup { protocol_system: protocol, token_in: swap_token_in, @@ -204,16 +207,19 @@ mod tests { } #[rstest] - // there is no next swap -> receiver is the solution receiver - #[case(None, receiver(), false)] + // there is no next swap but there is an unwrap -> receiver is the router + #[case(None, true, router_address(), false)] + // there is no next swap and no unwrap -> receiver is the solution receiver + #[case(None, false, receiver(), false)] // protocol of next swap supports transfer in optimization - #[case(Some("uniswap_v2"), component_id(), true)] + #[case(Some("uniswap_v2"), false, component_id(), true)] // protocol of next swap supports transfer in optimization but is callback constrained - #[case(Some("uniswap_v3"), router_address(), false)] + #[case(Some("uniswap_v3"), false, router_address(), false)] // protocol of next swap does not support transfer in optimization - #[case(Some("vm:curve"), router_address(), false)] + #[case(Some("vm:curve"), false, router_address(), false)] fn test_get_receiver( #[case] protocol: Option<&str>, + #[case] unwrap: bool, #[case] expected_receiver: Bytes, #[case] expected_optimization: bool, ) { @@ -232,22 +238,20 @@ mod tests { token_in: usdc(), token_out: dai(), split: 0f64, - swaps: vec![Swap { - component: ProtocolComponent { + swaps: vec![SwapBuilder::new( + ProtocolComponent { protocol_system: protocol.unwrap().to_string(), id: component_id().to_string(), ..Default::default() }, - token_in: usdc(), - token_out: dai(), - split: 0f64, - user_data: None, - protocol_state: None, - }], + usdc(), + dai(), + ) + .build()], }) }; - let result = optimization.get_receiver(&receiver(), next_swap.as_ref()); + let result = optimization.get_receiver(&receiver(), next_swap.as_ref(), unwrap); assert!(result.is_ok()); let (actual_receiver, optimization_flag) = result.unwrap(); diff --git a/src/encoding/evm/swap_encoder/builder.rs b/src/encoding/evm/swap_encoder/builder.rs index 30ccdc7..e586b1c 100644 --- a/src/encoding/evm/swap_encoder/builder.rs +++ b/src/encoding/evm/swap_encoder/builder.rs @@ -5,8 +5,9 @@ use tycho_common::models::Chain; use crate::encoding::{ errors::EncodingError, evm::swap_encoder::swap_encoders::{ - BalancerV2SwapEncoder, BalancerV3SwapEncoder, CurveSwapEncoder, EkuboSwapEncoder, - MaverickV2SwapEncoder, UniswapV2SwapEncoder, UniswapV3SwapEncoder, UniswapV4SwapEncoder, + BalancerV2SwapEncoder, BalancerV3SwapEncoder, BebopSwapEncoder, CurveSwapEncoder, + EkuboSwapEncoder, HashflowSwapEncoder, MaverickV2SwapEncoder, UniswapV2SwapEncoder, + UniswapV3SwapEncoder, UniswapV4SwapEncoder, }, swap_encoder::SwapEncoder, }; @@ -87,6 +88,14 @@ impl SwapEncoderBuilder { self.chain, self.config, )?)), + "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 9f09f2a..ace0c9c 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -1,17 +1,26 @@ -use std::{collections::HashMap, str::FromStr}; +use std::{collections::HashMap, str::FromStr, sync::Arc}; use alloy::{ - primitives::{Address, Bytes as AlloyBytes, U8}, + primitives::{Address, Bytes as AlloyBytes, U256, U8}, sol_types::SolValue, }; use serde_json::from_str; -use tycho_common::{models::Chain, Bytes}; +use tokio::{ + runtime::{Handle, Runtime}, + task::block_in_place, +}; +use tycho_common::{ + models::{protocol::GetAmountOutParams, Chain}, + Bytes, +}; use crate::encoding::{ errors::EncodingError, evm::{ approvals::protocol_approvals_manager::ProtocolApprovalsManager, - utils::{bytes_to_address, get_static_attribute, pad_to_fixed_size}, + utils::{ + biguint_to_u256, bytes_to_address, get_runtime, get_static_attribute, pad_to_fixed_size, + }, }, models::{EncodingContext, Swap}, swap_encoder::SwapEncoder, @@ -655,6 +664,342 @@ impl SwapEncoder for BalancerV3SwapEncoder { fn executor_address(&self) -> &str { &self.executor_address } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +/// Encodes a swap on Bebop (PMM RFQ) through the given executor address. +/// +/// Bebop uses a Request-for-Quote model where quotes are obtained off-chain +/// and settled on-chain. This encoder supports PMM RFQ execution. +/// +/// # Fields +/// * `executor_address` - The address of the executor contract that will perform the swap. +/// * `settlement_address` - The address of the Bebop settlement contract. +#[derive(Clone)] +pub struct BebopSwapEncoder { + executor_address: String, + settlement_address: String, + native_token_bebop_address: Bytes, + native_token_address: Bytes, + runtime_handle: Handle, + #[allow(dead_code)] + runtime: Option>, +} + +impl SwapEncoder for BebopSwapEncoder { + fn new( + executor_address: String, + chain: Chain, + config: Option>, + ) -> Result { + let config = config.ok_or(EncodingError::FatalError( + "Missing bebop specific addresses in config".to_string(), + ))?; + let settlement_address = config + .get("bebop_settlement_address") + .ok_or(EncodingError::FatalError( + "Missing bebop settlement address in config".to_string(), + ))? + .to_string(); + let native_token_bebop_address = config + .get("native_token_address") + .ok_or(EncodingError::FatalError( + "Missing native token bebop address in config".to_string(), + ))? + .to_string(); + let native_token_bebop_address = + Bytes::from_str(&native_token_bebop_address).map_err(|_| { + EncodingError::FatalError("Invalid Bebop native token address".to_string()) + })?; + let (runtime_handle, runtime) = get_runtime()?; + Ok(Self { + executor_address, + settlement_address, + runtime_handle, + runtime, + native_token_bebop_address, + native_token_address: chain.native_token().address, + }) + } + + fn encode_swap( + &self, + swap: &Swap, + encoding_context: &EncodingContext, + ) -> Result, EncodingError> { + let token_in = bytes_to_address(&swap.token_in)?; + let token_out = bytes_to_address(&swap.token_out)?; + let sender = encoding_context + .router_address + .clone() + .ok_or(EncodingError::FatalError( + "The router address is needed to perform a Hashflow swap".to_string(), + ))?; + let approval_needed = if swap.token_in == self.native_token_address { + false + } else { + let tycho_router_address = bytes_to_address(&sender)?; + let settlement_address = Address::from_str(&self.settlement_address) + .map_err(|_| EncodingError::FatalError("Invalid settlement address".to_string()))?; + ProtocolApprovalsManager::new()?.approval_needed( + token_in, + tycho_router_address, + settlement_address, + )? + }; + + let (partial_fill_offset, original_filled_taker_amount, bebop_calldata) = + if let Some(state) = &swap.protocol_state { + let indicatively_priced_state = state + .as_indicatively_priced() + .map_err(|e| { + EncodingError::FatalError(format!("State is not indicatively priced {e}")) + })?; + let estimated_amount_in = + swap.estimated_amount_in + .clone() + .ok_or(EncodingError::FatalError( + "Estimated amount in is mandatory for a Bebop swap".to_string(), + ))?; + // Bebop uses another address for the native token than the zero address + let mut token_in = swap.token_in.clone(); + if swap.token_in == self.native_token_address { + token_in = self.native_token_bebop_address.clone() + } + let mut token_out = swap.token_out.clone(); + if swap.token_out == self.native_token_address { + token_out = self.native_token_bebop_address.clone() + } + + let params = GetAmountOutParams { + amount_in: estimated_amount_in, + token_in, + token_out, + sender: encoding_context + .router_address + .clone() + .ok_or(EncodingError::FatalError( + "The router address is needed to perform a Bebop swap".to_string(), + ))?, + receiver: encoding_context.receiver.clone(), + }; + let signed_quote = block_in_place(|| { + self.runtime_handle.block_on(async { + indicatively_priced_state + .request_signed_quote(params) + .await + }) + })?; + let bebop_calldata = signed_quote + .quote_attributes + .get("calldata") + .ok_or(EncodingError::FatalError( + "Bebop quote must have a calldata attribute".to_string(), + ))?; + let partial_fill_offset = signed_quote + .quote_attributes + .get("partial_fill_offset") + .ok_or(EncodingError::FatalError( + "Bebop quote must have a partial_fill_offset attribute".to_string(), + ))?; + let original_filled_taker_amount = biguint_to_u256(&signed_quote.amount_out); + ( + // we are only interested in the last byte to get a u8 + partial_fill_offset[partial_fill_offset.len() - 1], + original_filled_taker_amount, + bebop_calldata.to_vec(), + ) + } else { + // The user data required for Bebop is + // partial_fill_offset (u8) | original_taker_amount (U256) | calldata (bytes + // (selector ABI encoded params)) + let user_data = swap.user_data.clone().ok_or_else(|| { + EncodingError::InvalidInput( + "Bebop swaps require user_data with calldata".to_string(), + ) + })?; + + if user_data.len() < 37 { + return Err(EncodingError::InvalidInput( + "User data too short to contain offset and Bebop calldata".to_string(), + )); + } + + let partial_fill_offset = user_data[0]; + let original_filled_taker_amount = U256::from_be_slice(&user_data[1..33]); + + // The calldata should be for either swapSingle or swapAggregate + let bebop_calldata = user_data[33..].to_vec(); + (partial_fill_offset, original_filled_taker_amount, bebop_calldata) + }; + let receiver = bytes_to_address(&encoding_context.receiver)?; + + // Encode packed data for the executor + // Format: token_in | token_out | transfer_type | partial_fill_offset | + // original_filled_taker_amount | approval_needed | receiver | bebop_calldata + let args = ( + token_in, + token_out, + (encoding_context.transfer_type as u8).to_be_bytes(), + partial_fill_offset.to_be_bytes(), + original_filled_taker_amount.to_be_bytes::<32>(), + (approval_needed as u8).to_be_bytes(), + receiver, + &bebop_calldata[..], + ); + + Ok(args.abi_encode_packed()) + } + + fn executor_address(&self) -> &str { + &self.executor_address + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +#[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 hashflow router 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 sender = encoding_context + .router_address + .clone() + .ok_or(EncodingError::FatalError( + "The router address is needed to perform a Hashflow swap".to_string(), + ))?; + + // Native ETH doesn't need approval, only ERC20 tokens do + let approval_needed = if swap.token_in == self.native_token_address { + false + } else { + let tycho_router_address = bytes_to_address(&sender)?; + let hashflow_router_address = Address::from_str(&self.hashflow_router_address) + .map_err(|_| { + EncodingError::FatalError("Invalid hashflow router address address".to_string()) + })?; + ProtocolApprovalsManager::new()?.approval_needed( + bytes_to_address(&swap.token_in)?, + tycho_router_address, + hashflow_router_address, + )? + }; + + // 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", + "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()) } @@ -672,10 +1017,14 @@ mod tests { }; use super::*; - use crate::encoding::{evm::utils::write_calldata_to_file, models::TransferType}; + use crate::encoding::{ + evm::utils::write_calldata_to_file, + models::{SwapBuilder, TransferType}, + }; mod uniswap_v2 { use super::*; + use crate::encoding::models::SwapBuilder; #[test] fn test_encode_uniswap_v2() { let usv2_pool = ProtocolComponent { @@ -685,14 +1034,7 @@ mod tests { let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"); - let swap = Swap { - component: usv2_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + let swap = SwapBuilder::new(usv2_pool, token_in.clone(), token_out.clone()).build(); let encoding_context = EncodingContext { receiver: Bytes::from("0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e"), // BOB exact_out: false, @@ -732,6 +1074,7 @@ mod tests { mod uniswap_v3 { use super::*; + use crate::encoding::models::SwapBuilder; #[test] fn test_encode_uniswap_v3() { let fee = BigInt::from(500); @@ -746,14 +1089,7 @@ mod tests { }; let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"); - let swap = Swap { - component: usv3_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + let swap = SwapBuilder::new(usv3_pool, token_in.clone(), token_out.clone()).build(); let encoding_context = EncodingContext { receiver: Bytes::from("0x0000000000000000000000000000000000000001"), exact_out: false, @@ -796,6 +1132,7 @@ mod tests { mod balancer_v2 { use super::*; + use crate::encoding::models::SwapBuilder; #[test] fn test_encode_balancer_v2() { @@ -808,14 +1145,7 @@ mod tests { }; let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); let token_out = Bytes::from("0xba100000625a3754423978a60c9317c58a424e3D"); - let swap = Swap { - component: balancer_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + let swap = SwapBuilder::new(balancer_pool, token_in.clone(), token_out.clone()).build(); let encoding_context = EncodingContext { // The receiver was generated with `makeAddr("bob") using forge` receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), @@ -882,14 +1212,7 @@ mod tests { static_attributes, ..Default::default() }; - let swap = Swap { - component: usv4_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + let swap = SwapBuilder::new(usv4_pool, token_in.clone(), token_out.clone()).build(); let encoding_context = EncodingContext { // The receiver is ALICE to match the solidity tests receiver: Bytes::from("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2"), @@ -958,14 +1281,7 @@ mod tests { ..Default::default() }; - let swap = Swap { - component: usv4_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + let swap = SwapBuilder::new(usv4_pool, token_in.clone(), token_out.clone()).build(); let encoding_context = EncodingContext { receiver: Bytes::from("0x0000000000000000000000000000000000000001"), @@ -1056,23 +1372,12 @@ mod tests { ..Default::default() }; - let initial_swap = Swap { - component: usde_usdt_component, - token_in: usde_address.clone(), - token_out: usdt_address.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; - - let second_swap = Swap { - component: usdt_wbtc_component, - token_in: usdt_address, - token_out: wbtc_address.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + let initial_swap = + SwapBuilder::new(usde_usdt_component, usde_address.clone(), usdt_address.clone()) + .build(); + let second_swap = + SwapBuilder::new(usdt_wbtc_component, usdt_address.clone(), wbtc_address.clone()) + .build(); let encoder = UniswapV4SwapEncoder::new( String::from("0xF62849F9A0B5Bf2913b396098F7c7019b51A820a"), @@ -1131,7 +1436,6 @@ mod tests { } mod ekubo { use super::*; - use crate::encoding::evm::utils::write_calldata_to_file; const RECEIVER: &str = "ca4f73fe97d0b987a0d12b39bbd562c779bab6f6"; // Random address @@ -1151,14 +1455,7 @@ mod tests { let component = ProtocolComponent { static_attributes, ..Default::default() }; - let swap = Swap { - component, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + let swap = SwapBuilder::new(component, token_in.clone(), token_out.clone()).build(); let encoding_context = EncodingContext { receiver: RECEIVER.into(), @@ -1211,8 +1508,8 @@ mod tests { transfer_type: TransferType::Transfer, }; - let first_swap = Swap { - component: ProtocolComponent { + let first_swap = SwapBuilder::new( + ProtocolComponent { static_attributes: HashMap::from([ ("fee".to_string(), Bytes::from(0_u64)), ("tick_spacing".to_string(), Bytes::from(0_u32)), @@ -1223,15 +1520,13 @@ mod tests { ]), ..Default::default() }, - token_in: group_token_in.clone(), - token_out: intermediary_token.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + group_token_in.clone(), + intermediary_token.clone(), + ) + .build(); - let second_swap = Swap { - component: ProtocolComponent { + let second_swap = SwapBuilder::new( + ProtocolComponent { // 0.0025% fee & 0.005% base pool static_attributes: HashMap::from([ ("fee".to_string(), Bytes::from(461168601842738_u64)), @@ -1240,12 +1535,10 @@ mod tests { ]), ..Default::default() }, - token_in: intermediary_token.clone(), - token_out: group_token_out.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + intermediary_token.clone(), + group_token_out.clone(), + ) + .build(); let first_encoded_swap = encoder .encode_swap(&first_swap, &encoding_context) @@ -1354,19 +1647,18 @@ mod tests { ) { let mut static_attributes: HashMap = HashMap::new(); static_attributes.insert("coins".into(), Bytes::from_str(coins).unwrap()); - let swap = Swap { - component: ProtocolComponent { + let swap = SwapBuilder::new( + ProtocolComponent { id: "pool-id".into(), protocol_system: String::from("vm:curve"), static_attributes, ..Default::default() }, - token_in: Bytes::from(token_in), - token_out: Bytes::from(token_out), - split: 0f64, - user_data: None, - protocol_state: None, - }; + Bytes::from(token_in), + Bytes::from(token_out), + ) + .build(); + let encoder = CurveSwapEncoder::new(String::default(), Chain::Ethereum, curve_config()).unwrap(); let (i, j) = encoder @@ -1400,14 +1692,9 @@ mod tests { }; let token_in = Bytes::from("0x6B175474E89094C44Da98b954EedeAC495271d0F"); let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); - let swap = Swap { - component: curve_tri_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + let swap = + SwapBuilder::new(curve_tri_pool, token_in.clone(), token_out.clone()).build(); + let encoding_context = EncodingContext { // The receiver was generated with `makeAddr("bob") using forge` receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), @@ -1473,14 +1760,7 @@ mod tests { }; let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); let token_out = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); - let swap = Swap { - component: curve_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + let swap = SwapBuilder::new(curve_pool, token_in.clone(), token_out.clone()).build(); let encoding_context = EncodingContext { // The receiver was generated with `makeAddr("bob") using forge` receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), @@ -1547,14 +1827,7 @@ mod tests { }; let token_in = Bytes::from("0x0000000000000000000000000000000000000000"); let token_out = Bytes::from("0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"); - let swap = Swap { - component: curve_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + let swap = SwapBuilder::new(curve_pool, token_in.clone(), token_out.clone()).build(); let encoding_context = EncodingContext { // The receiver was generated with `makeAddr("bob") using forge` receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), @@ -1622,14 +1895,7 @@ mod tests { }; let token_in = Bytes::from("0x7bc3485026ac48b6cf9baf0a377477fff5703af8"); let token_out = Bytes::from("0xc71ea051a5f82c67adcf634c36ffe6334793d24c"); - let swap = Swap { - component: balancer_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + let swap = SwapBuilder::new(balancer_pool, token_in.clone(), token_out.clone()).build(); let encoding_context = EncodingContext { // The receiver was generated with `makeAddr("bob") using forge` receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), @@ -1681,14 +1947,7 @@ mod tests { }; let token_in = Bytes::from("0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f"); let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); - let swap = Swap { - component: maverick_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + let swap = SwapBuilder::new(maverick_pool, token_in.clone(), token_out.clone()).build(); let encoding_context = EncodingContext { // The receiver was generated with `makeAddr("bob") using forge` receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), @@ -1728,4 +1987,384 @@ mod tests { write_calldata_to_file("test_encode_maverick_v2", hex_swap.as_str()); } } + + mod bebop { + use num_bigint::BigUint; + + use super::*; + use crate::encoding::evm::testing_utils::MockRFQState; + + fn bebop_config() -> HashMap { + HashMap::from([ + ( + "bebop_settlement_address".to_string(), + "0xbbbbbBB520d69a9775E85b458C58c648259FAD5F".to_string(), + ), + ( + "native_token_address".to_string(), + "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE".to_string(), + ), + ]) + } + + #[test] + fn test_encode_bebop_single_with_user_data() { + // 200 USDC -> ONDO + let bebop_calldata= Bytes::from_str("0x4dcebcba00000000000000000000000000000000000000000000000000000000689b548f0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000067336cec42645f55059eff241cb02ea5cc52ff86000000000000000000000000000000000000000000000000279ead5d9685f25b000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be3000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000a8aea46aa4ec5c0f5000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000000005230bcb979c81cebf94a3b5c08bcfa300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000414ce40058ff07f11d9224c2c8d1e58369e4a90173856202d8d2a17da48058ad683dedb742eda0d4c0cf04cf1c09138898dd7fd06f97268ea7f74ef9b42d29bf4c1b00000000000000000000000000000000000000000000000000000000000000").unwrap(); + let original_taker_amount = U256::from_str("200000000").unwrap(); + // partialFillOffset 12 for swapSingle + let mut user_data = vec![12u8]; + user_data.extend_from_slice(&original_taker_amount.to_be_bytes::<32>()); + user_data.extend_from_slice(&bebop_calldata); + + let bebop_component = ProtocolComponent { + id: String::from("bebop-rfq"), + protocol_system: String::from("rfq:bebop"), + static_attributes: HashMap::new(), + ..Default::default() + }; + + let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC + let token_out = Bytes::from("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3"); // ONDO + + let swap = SwapBuilder::new(bebop_component, token_in.clone(), token_out.clone()) + .user_data(Bytes::from(user_data)) + .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 = BebopSwapEncoder::new( + String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), + Chain::Ethereum, + Some(bebop_config()), + ) + .unwrap(); + + let encoded_swap = encoder + .encode_swap(&swap, &encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); + + let expected_swap = String::from(concat!( + // token in + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // token out + "faba6f8e4a5e8ab82f62fe7c39859fa577269be3", + // transfer type + "01", + // partiall filled offset + "0c", + // original taker amount + "000000000000000000000000000000000000000000000000000000000bebc200", + // approval needed + "01", + //receiver, + "c5564c13a157e6240659fb81882a28091add8670", + )); + assert_eq!(hex_swap, expected_swap + &bebop_calldata.to_string()[2..]); + } + + #[test] + fn test_encode_bebop_aggregate_with_user_data() { + // 20k USDC -> ONDO + let bebop_calldata= Bytes::from_str("0xa2f7489300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000689b78880000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c0000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000005a060a5c2aaaaa2fe2cda34423cac76a84c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000002901f2d62bb356ca0000000000000000000000000000000000000000000000002901f2d62bb356cb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000044f83c726000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000589400da00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000003aa5f96046644f6e37a000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000004b51a26526ddbeec60000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000417ab4332f2b091d87d56d04eee35dd49452782c782de71608c0425c5ae41f1d7e147173851c870d76720ce07d45cd8622352716b1c7965819ee2bf8c573c499ae1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000410c8da2637aa929e11caff9afdfc4c489320c6dba77cc934d88ba8956e365fd1d48983087c6e474bbb828181cdfdd17317c4c9c3ee4bc98e3769d0c05cc7a285e1c00000000000000000000000000000000000000000000000000000000000000").unwrap(); + let original_taker_amount = U256::from_str("20000000000").unwrap(); + + // partialFillOffset is 2 for swapAggregate + let mut user_data = vec![2u8]; + user_data.extend_from_slice(&original_taker_amount.to_be_bytes::<32>()); + user_data.extend_from_slice(&bebop_calldata); + + let bebop_component = ProtocolComponent { + id: String::from("bebop-rfq"), + protocol_system: String::from("rfq:bebop"), + static_attributes: HashMap::new(), + ..Default::default() + }; + + let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC + let token_out = Bytes::from("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3"); // ONDO + + let swap = SwapBuilder::new(bebop_component, token_in.clone(), token_out.clone()) + .user_data(Bytes::from(user_data)) + .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 = BebopSwapEncoder::new( + String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), + Chain::Ethereum, + Some(bebop_config()), + ) + .unwrap(); + + let encoded_swap = encoder + .encode_swap(&swap, &encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); + + let expected_swap = String::from(concat!( + // token in + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // token out + "faba6f8e4a5e8ab82f62fe7c39859fa577269be3", + // transfer type + "01", + // partiall filled offset + "02", + // original taker amount + "00000000000000000000000000000000000000000000000000000004a817c800", + // approval needed + "01", + //receiver, + "c5564c13a157e6240659fb81882a28091add8670", + )); + + assert_eq!(hex_swap, expected_swap + &bebop_calldata.to_string()[2..]); + } + + #[test] + fn test_encode_bebop_single_with_protocol_state() { + // 3000 USDC -> 1 WETH using a mocked RFQ state to get a quote + let bebop_calldata = Bytes::from_str("0x123456").unwrap(); + let partial_fill_offset = 12u64; + let quote_amount_out = BigUint::from_str("1000000000000000000").unwrap(); + + let bebop_component = ProtocolComponent { + id: String::from("bebop-rfq"), + protocol_system: String::from("rfq:bebop"), + ..Default::default() + }; + let bebop_state = MockRFQState { + quote_amount_out, + quote_data: HashMap::from([ + ("calldata".to_string(), bebop_calldata.clone()), + ( + "partial_fill_offset".to_string(), + Bytes::from( + partial_fill_offset + .to_be_bytes() + .to_vec(), + ), + ), + ]), + }; + + let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC + let token_out = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); // WETH + + let swap = SwapBuilder::new(bebop_component, token_in.clone(), token_out.clone()) + .estimated_amount_in(BigUint::from_str("3000000000").unwrap()) + .protocol_state(Arc::new(bebop_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 = BebopSwapEncoder::new( + String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), + Chain::Ethereum, + Some(bebop_config()), + ) + .unwrap(); + + let encoded_swap = encoder + .encode_swap(&swap, &encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); + + let expected_swap = String::from(concat!( + // token in + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // token out + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + // transfer type + "01", + // partiall filled offset + "0c", + // original taker amount + "0000000000000000000000000000000000000000000000000de0b6b3a7640000", + // approval needed + "01", + //receiver, + "c5564c13a157e6240659fb81882a28091add8670", + )); + 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_fails_without_protocol_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 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("0x478eca1b93865dca0b9f325935eb123c8a4af011").unwrap(), + ), + ( + "external_account".to_string(), + Bytes::from_str("0xbee3211ab312a8d065c4fef0247448e17a8da000").unwrap(), + ), + ( + "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(3000_u64)).to_be_bytes::<32>().to_vec()), + ), + ( + "quote_token_amount".to_string(), + Bytes::from(biguint_to_u256(&BigUint::from(1_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()), + ]; + 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 + .into_iter() + .collect(), + }; + + 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(Arc::new(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..]); + } + } } diff --git a/src/encoding/evm/testing_utils.rs b/src/encoding/evm/testing_utils.rs new file mode 100644 index 0000000..fb864a6 --- /dev/null +++ b/src/encoding/evm/testing_utils.rs @@ -0,0 +1,92 @@ +// This module is used in integration tests as well +use std::{any::Any, collections::HashMap}; + +use async_trait::async_trait; +use num_bigint::BigUint; +use tycho_common::{ + dto::ProtocolStateDelta, + models::{protocol::GetAmountOutParams, token::Token}, + simulation::{ + errors::{SimulationError, TransitionError}, + indicatively_priced::{IndicativelyPriced, SignedQuote}, + protocol_sim::{Balances, GetAmountOutResult, ProtocolSim}, + }, + Bytes, +}; + +#[derive(Debug)] +pub struct MockRFQState { + pub quote_amount_out: BigUint, + pub quote_data: HashMap, +} +impl ProtocolSim for MockRFQState { + fn fee(&self) -> f64 { + panic!("MockRFQState does not implement fee") + } + + fn spot_price(&self, _base: &Token, _quote: &Token) -> Result { + panic!("MockRFQState does not implement fee") + } + + fn get_amount_out( + &self, + _amount_in: BigUint, + _token_in: &Token, + _token_out: &Token, + ) -> Result { + panic!("MockRFQState does not implement fee") + } + + fn get_limits( + &self, + _sell_token: Bytes, + _buy_token: Bytes, + ) -> Result<(BigUint, BigUint), SimulationError> { + panic!("MockRFQState does not implement fee") + } + + fn delta_transition( + &mut self, + _delta: ProtocolStateDelta, + _tokens: &HashMap, + _balances: &Balances, + ) -> Result<(), TransitionError> { + panic!("MockRFQState does not implement fee") + } + + fn clone_box(&self) -> Box { + panic!("MockRFQState does not implement fee") + } + + fn as_any(&self) -> &dyn Any { + panic!("MockRFQState does not implement fee") + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + panic!("MockRFQState does not implement fee") + } + + fn eq(&self, _other: &dyn ProtocolSim) -> bool { + panic!("MockRFQState does not implement fee") + } + + fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> { + Ok(self) + } +} + +#[async_trait] +impl IndicativelyPriced for MockRFQState { + async fn request_signed_quote( + &self, + params: GetAmountOutParams, + ) -> Result { + Ok(SignedQuote { + base_token: params.token_in, + quote_token: params.token_out, + amount_in: params.amount_in, + amount_out: self.quote_amount_out.clone(), + quote_attributes: self.quote_data.clone(), + }) + } +} diff --git a/src/encoding/evm/tycho_encoders.rs b/src/encoding/evm/tycho_encoders.rs index ed83c8e..82088ec 100644 --- a/src/encoding/evm/tycho_encoders.rs +++ b/src/encoding/evm/tycho_encoders.rs @@ -406,7 +406,7 @@ mod tests { use tycho_common::models::{protocol::ProtocolComponent, Chain}; use super::*; - use crate::encoding::models::Swap; + use crate::encoding::models::{Swap, SwapBuilder}; fn dai() -> Bytes { Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap() @@ -435,48 +435,44 @@ mod tests { // Fee and tick spacing information for this test is obtained by querying the // USV4 Position Manager contract: 0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e // Using the poolKeys function with the first 25 bytes of the pool id - fn swap_usdc_eth_univ4() -> Swap<'static> { + fn swap_usdc_eth_univ4() -> Swap { let pool_fee_usdc_eth = Bytes::from(BigInt::from(3000).to_signed_bytes_be()); let tick_spacing_usdc_eth = Bytes::from(BigInt::from(60).to_signed_bytes_be()); let mut static_attributes_usdc_eth: HashMap = HashMap::new(); static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth); static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth); - Swap { - component: ProtocolComponent { + SwapBuilder::new( + ProtocolComponent { id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d" .to_string(), protocol_system: "uniswap_v4_hooks".to_string(), static_attributes: static_attributes_usdc_eth, ..Default::default() }, - token_in: usdc().clone(), - token_out: eth().clone(), - split: 0f64, - user_data: None, - protocol_state: None, - } + usdc().clone(), + eth().clone(), + ) + .build() } - fn swap_eth_pepe_univ4() -> Swap<'static> { + fn swap_eth_pepe_univ4() -> Swap { let pool_fee_eth_pepe = Bytes::from(BigInt::from(25000).to_signed_bytes_be()); let tick_spacing_eth_pepe = Bytes::from(BigInt::from(500).to_signed_bytes_be()); let mut static_attributes_eth_pepe: HashMap = HashMap::new(); static_attributes_eth_pepe.insert("key_lp_fee".into(), pool_fee_eth_pepe); static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe); - Swap { - component: ProtocolComponent { + SwapBuilder::new( + ProtocolComponent { id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9" .to_string(), protocol_system: "uniswap_v4_hooks".to_string(), static_attributes: static_attributes_eth_pepe, ..Default::default() }, - token_in: eth().clone(), - token_out: pepe().clone(), - split: 0f64, - user_data: None, - protocol_state: None, - } + eth().clone(), + pepe().clone(), + ) + .build() } fn router_address() -> Bytes { @@ -514,18 +510,16 @@ mod tests { fn test_encode_router_calldata_single_swap() { let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let eth_amount_in = BigUint::from(1000u32); - let swap = Swap { - component: ProtocolComponent { + let swap = SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth(), - token_out: dai(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + weth().clone(), + dai().clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -580,31 +574,27 @@ mod tests { fn test_encode_router_calldata_sequential_swap() { let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let eth_amount_in = BigUint::from(1000u32); - let swap_weth_dai = Swap { - component: ProtocolComponent { + let swap_weth_dai = SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth(), - token_out: dai(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + weth().clone(), + dai().clone(), + ) + .build(); - let swap_dai_usdc = Swap { - component: ProtocolComponent { + let swap_dai_usdc = SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: dai(), - token_out: usdc(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + dai().clone(), + usdc().clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -674,18 +664,16 @@ mod tests { #[test] fn test_validate_passes_for_wrap() { let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); - let swap = Swap { - component: ProtocolComponent { + let swap = SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth(), - token_out: dai(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + weth().clone(), + dai().clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -704,18 +692,16 @@ mod tests { #[test] fn test_validate_fails_for_wrap_wrong_input() { let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); - let swap = Swap { - component: ProtocolComponent { + let swap = SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth(), - token_out: dai(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + weth().clone(), + dai().clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -739,18 +725,16 @@ mod tests { #[test] fn test_validate_fails_for_wrap_wrong_first_swap() { let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); - let swap = Swap { - component: ProtocolComponent { + let swap = SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: eth(), - token_out: dai(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + eth().clone(), + dai().clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -794,18 +778,16 @@ mod tests { #[test] fn test_validate_passes_for_unwrap() { let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); - let swap = Swap { - component: ProtocolComponent { + let swap = SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: dai(), - token_out: weth(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + dai().clone(), + weth().clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -823,18 +805,16 @@ mod tests { #[test] fn test_validate_fails_for_unwrap_wrong_output() { let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); - let swap = Swap { - component: ProtocolComponent { + let swap = SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: dai(), - token_out: weth(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + dai().clone(), + weth().clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -859,18 +839,16 @@ mod tests { #[test] fn test_validate_fails_for_unwrap_wrong_last_swap() { let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); - let swap = Swap { - component: ProtocolComponent { + let swap = SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: dai(), - token_out: eth(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + dai().clone(), + eth().clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -900,42 +878,36 @@ mod tests { // (some of the pool addresses in this test are fake) let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let swaps = vec![ - Swap { - component: ProtocolComponent { + SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: dai(), - token_out: weth(), - split: 0.5f64, - user_data: None, - protocol_state: None, - }, - Swap { - component: ProtocolComponent { + dai().clone(), + weth().clone(), + ) + .build(), + SwapBuilder::new( + ProtocolComponent { id: "0x0000000000000000000000000000000000000000".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: dai(), - token_out: weth(), - split: 0f64, - user_data: None, - protocol_state: None, - }, - Swap { - component: ProtocolComponent { + dai().clone(), + weth().clone(), + ) + .build(), + SwapBuilder::new( + ProtocolComponent { id: "0x0000000000000000000000000000000000000000".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth(), - token_out: dai(), - split: 0f64, - user_data: None, - protocol_state: None, - }, + weth().clone(), + dai().clone(), + ) + .build(), ]; let solution = Solution { @@ -958,54 +930,46 @@ mod tests { // (some of the pool addresses in this test are fake) let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let swaps = vec![ - Swap { - component: ProtocolComponent { + SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: dai(), - token_out: weth(), - split: 0f64, - user_data: None, - protocol_state: None, - }, - Swap { - component: ProtocolComponent { + dai().clone(), + weth().clone(), + ) + .build(), + SwapBuilder::new( + ProtocolComponent { id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth(), - token_out: usdc(), - split: 0f64, - user_data: None, - protocol_state: None, - }, - Swap { - component: ProtocolComponent { + weth().clone(), + usdc().clone(), + ) + .build(), + SwapBuilder::new( + ProtocolComponent { id: "0x0000000000000000000000000000000000000000".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: usdc(), - token_out: dai(), - split: 0f64, - user_data: None, - protocol_state: None, - }, - Swap { - component: ProtocolComponent { + usdc().clone(), + dai().clone(), + ) + .build(), + SwapBuilder::new( + ProtocolComponent { id: "0x0000000000000000000000000000000000000000".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: dai(), - token_out: wbtc(), - split: 0f64, - user_data: None, - protocol_state: None, - }, + dai().clone(), + wbtc().clone(), + ) + .build(), ]; let solution = Solution { @@ -1035,42 +999,37 @@ mod tests { // (some of the pool addresses in this test are fake) let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let swaps = vec![ - Swap { - component: ProtocolComponent { + SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth(), - token_out: dai(), - split: 0f64, - user_data: None, - protocol_state: None, - }, - Swap { - component: ProtocolComponent { + weth(), + dai(), + ) + .build(), + SwapBuilder::new( + ProtocolComponent { id: "0x0000000000000000000000000000000000000000".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: dai(), - token_out: weth(), - split: 0.5f64, - user_data: None, - protocol_state: None, - }, - Swap { - component: ProtocolComponent { + dai(), + weth(), + ) + .split(0.5) + .build(), + SwapBuilder::new( + ProtocolComponent { id: "0x0000000000000000000000000000000000000000".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: dai(), - token_out: weth(), - split: 0f64, - user_data: None, - protocol_state: None, - }, + dai(), + weth(), + ) + .build(), ]; let solution = Solution { @@ -1093,30 +1052,26 @@ mod tests { // (some of the pool addresses in this test are fake) let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let swaps = vec![ - Swap { - component: ProtocolComponent { + SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: weth(), - token_out: dai(), - split: 0f64, - user_data: None, - protocol_state: None, - }, - Swap { - component: ProtocolComponent { - id: "0x0000000000000000000000000000000000000000".to_string(), + weth(), + dai(), + ) + .build(), + SwapBuilder::new( + ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: dai(), - token_out: weth(), - split: 0f64, - user_data: None, - protocol_state: None, - }, + dai(), + weth(), + ) + .build(), ]; let solution = Solution { @@ -1150,7 +1105,7 @@ mod tests { use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use super::*; - use crate::encoding::models::{Solution, Swap}; + use crate::encoding::models::Solution; #[test] fn test_executor_encoder_encode() { @@ -1160,18 +1115,16 @@ mod tests { let token_in = weth(); let token_out = dai(); - let swap = Swap { - component: ProtocolComponent { + let swap = SwapBuilder::new( + ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + token_in.clone(), + token_out.clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -1222,17 +1175,16 @@ mod tests { let token_in = weth(); let token_out = dai(); - let swap = Swap { - component: ProtocolComponent { + let swap = SwapBuilder::new( + ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), protocol_system: "uniswap_v2".to_string(), ..Default::default() }, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - user_data: None, - protocol_state: None, - }; + token_in.clone(), + token_out.clone(), + ) + .build(); let solution = Solution { exact_out: false, 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/src/encoding/models.rs b/src/encoding/models.rs index 9ffc367..48b224c 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use clap::ValueEnum; use num_bigint::BigUint; use serde::{Deserialize, Serialize}; @@ -35,7 +37,7 @@ pub enum UserTransferType { /// Represents a solution containing details describing an order, and instructions for filling /// the order. #[derive(Clone, Default, Debug, Deserialize, Serialize)] -pub struct Solution<'a> { +pub struct Solution { /// Address of the sender. pub sender: Bytes, /// Address of the receiver. @@ -55,7 +57,7 @@ pub struct Solution<'a> { #[serde(with = "biguint_string")] pub checked_amount: BigUint, /// List of swaps to fulfill the solution. - pub swaps: Vec>, + pub swaps: Vec, /// If set, the corresponding native action will be executed. pub native_action: Option, } @@ -74,7 +76,7 @@ pub enum NativeAction { /// Represents a swap operation to be performed on a pool. #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Swap<'a> { +pub struct Swap { /// Protocol component from tycho indexer pub component: ProtocolComponent, /// Token being input into the pool. @@ -88,33 +90,106 @@ pub struct Swap<'a> { pub user_data: Option, /// Optional protocol state used to perform the swap. #[serde(skip)] - pub protocol_state: Option<&'a dyn ProtocolSim>, + pub protocol_state: Option>, + /// Optional estimated amount in for this Swap. This is necessary for RFQ protocols. This value + /// is used to request the quote + pub estimated_amount_in: Option, } -impl<'a> Swap<'a> { +impl Swap { pub fn new>( component: T, token_in: Bytes, token_out: Bytes, split: f64, user_data: Option, - protocol_state: Option<&'a dyn ProtocolSim>, + protocol_state: Option>, + estimated_amount_in: Option, ) -> Self { - Self { component: component.into(), token_in, token_out, split, user_data, protocol_state } + Self { + component: component.into(), + token_in, + token_out, + split, + user_data, + protocol_state, + estimated_amount_in, + } } } -impl<'a> PartialEq for Swap<'a> { +impl PartialEq for Swap { fn eq(&self, other: &Self) -> bool { self.component == other.component && self.token_in == other.token_in && self.token_out == other.token_out && self.split == other.split && - self.user_data == other.user_data + self.user_data == other.user_data && + self.estimated_amount_in == other.estimated_amount_in // Skip protocol_state comparison since trait objects don't implement PartialEq } } +pub struct SwapBuilder { + component: ProtocolComponent, + token_in: Bytes, + token_out: Bytes, + split: f64, + user_data: Option, + protocol_state: Option>, + estimated_amount_in: Option, +} + +impl SwapBuilder { + pub fn new>( + component: T, + token_in: Bytes, + token_out: Bytes, + ) -> Self { + Self { + component: component.into(), + token_in, + token_out, + split: 0.0, + user_data: None, + protocol_state: None, + estimated_amount_in: None, + } + } + + pub fn split(mut self, split: f64) -> Self { + self.split = split; + self + } + + pub fn user_data(mut self, user_data: Bytes) -> Self { + self.user_data = Some(user_data); + self + } + + pub fn protocol_state(mut self, protocol_state: Arc) -> Self { + self.protocol_state = Some(protocol_state); + self + } + + pub fn estimated_amount_in(mut self, estimated_amount_in: BigUint) -> Self { + self.estimated_amount_in = Some(estimated_amount_in); + self + } + + pub fn build(self) -> Swap { + Swap { + component: self.component, + token_in: self.token_in, + token_out: self.token_out, + split: self.split, + user_data: self.user_data, + protocol_state: self.protocol_state, + estimated_amount_in: self.estimated_amount_in, + } + } +} + /// Represents a transaction to be executed. /// /// # Fields @@ -262,6 +337,7 @@ mod tests { 0.5, user_data.clone(), None, + None, ); assert_eq!(swap.token_in, Bytes::from("0x12")); assert_eq!(swap.token_out, Bytes::from("0x34")); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index aad4116..0949b7e 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -3,7 +3,10 @@ pub mod encoding; use std::str::FromStr; -use alloy::{primitives::B256, signers::local::PrivateKeySigner}; +use alloy::{ + primitives::{B256, U256}, + signers::local::PrivateKeySigner, +}; use tycho_common::{models::Chain, Bytes}; use tycho_execution::encoding::{ evm::encoder_builders::TychoRouterEncoderBuilder, models::UserTransferType, @@ -14,6 +17,14 @@ pub fn router_address() -> Bytes { Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap() } +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 } @@ -46,6 +57,10 @@ pub fn usdt() -> Bytes { Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap() } +pub fn ondo() -> Bytes { + Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap() +} + pub fn get_signer() -> PrivateKeySigner { // Set up a mock private key for signing (Alice's pk in our contract tests) let private_key = @@ -57,10 +72,26 @@ pub fn get_signer() -> PrivateKeySigner { pub fn get_tycho_router_encoder(user_transfer_type: UserTransferType) -> Box { TychoRouterEncoderBuilder::new() - .chain(tycho_common::models::Chain::Ethereum) + .chain(Chain::Ethereum) .user_transfer_type(user_transfer_type) .executors_file_path("config/test_executor_addresses.json".to_string()) .router_address(router_address()) .build() .expect("Failed to build encoder") } + +/// Builds the complete Bebop calldata in the format expected by the encoder +/// Returns: [ partial_fill_offset (u8) | original_taker_amount (U256) | calldata (bytes (selector + +/// ABI encoded params)) ] +pub fn build_bebop_calldata( + calldata: &[u8], + partial_fill_offset: u8, + original_taker_amount: U256, +) -> Bytes { + let mut user_data = Vec::with_capacity(1 + 32 + calldata.len()); + user_data.push(partial_fill_offset); + user_data.extend_from_slice(&original_taker_amount.to_be_bytes::<32>()); + user_data.extend_from_slice(calldata); + + Bytes::from(user_data) +} diff --git a/tests/optimized_transfers_integration_tests.rs b/tests/optimized_transfers_integration_tests.rs index 43ccf05..a16fff9 100644 --- a/tests/optimized_transfers_integration_tests.rs +++ b/tests/optimized_transfers_integration_tests.rs @@ -1,15 +1,19 @@ -use std::{collections::HashMap, str::FromStr}; +use std::{collections::HashMap, str::FromStr, sync::Arc}; use alloy::hex::encode; use num_bigint::{BigInt, BigUint}; use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use tycho_execution::encoding::{ - evm::utils::write_calldata_to_file, - models::{Solution, Swap, UserTransferType}, + evm::{ + testing_utils::MockRFQState, + utils::{biguint_to_u256, write_calldata_to_file}, + }, + models::{Solution, Swap, SwapBuilder, UserTransferType}, }; use crate::common::{ - encoding::encode_tycho_router_call, eth, eth_chain, get_signer, get_tycho_router_encoder, weth, + alice_address, bob_address, encoding::encode_tycho_router_call, eth, eth_chain, get_signer, + get_tycho_router_encoder, usdc, wbtc, weth, }; mod common; @@ -49,6 +53,7 @@ fn test_uniswap_v3_uniswap_v2() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let swap_wbtc_usdc = Swap { component: ProtocolComponent { @@ -61,6 +66,7 @@ fn test_uniswap_v3_uniswap_v2() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -130,6 +136,7 @@ fn test_uniswap_v3_uniswap_v3() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let swap_wbtc_usdc = Swap { component: ProtocolComponent { @@ -150,6 +157,7 @@ fn test_uniswap_v3_uniswap_v3() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -218,6 +226,7 @@ fn test_uniswap_v3_curve() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let swap_wbtc_usdt = Swap { @@ -248,6 +257,7 @@ fn test_uniswap_v3_curve() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -308,6 +318,7 @@ fn test_balancer_v2_uniswap_v2() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let swap_wbtc_usdc = Swap { @@ -321,6 +332,7 @@ fn test_balancer_v2_uniswap_v2() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -384,6 +396,7 @@ fn test_multi_protocol() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let balancer_swap_weth_wbtc = Swap { @@ -397,6 +410,7 @@ fn test_multi_protocol() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let curve_swap_wbtc_usdt = Swap { @@ -427,6 +441,7 @@ fn test_multi_protocol() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; // Ekubo @@ -450,6 +465,7 @@ fn test_multi_protocol() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; // USV4 @@ -474,6 +490,7 @@ fn test_multi_protocol() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -547,6 +564,7 @@ fn test_uniswap_v3_balancer_v3() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let swap_wbtc_qnt = Swap { component: ProtocolComponent { @@ -559,6 +577,7 @@ fn test_uniswap_v3_balancer_v3() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -593,3 +612,227 @@ fn test_uniswap_v3_balancer_v3() { let hex_calldata = encode(&calldata); write_calldata_to_file("test_uniswap_v3_balancer_v3", hex_calldata.as_str()); } + +#[test] +fn test_uniswap_v3_bebop() { + // 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 + // Bebop RFQ + // + // WETH ───(USV3)──> USDC ───(Bebop 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 Bebop RFQ using real order data + let bebop_calldata = Bytes::from_str("0x4dcebcba00000000000000000000000000000000000000000000000000000000689dcb3c0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000bee3211ab312a8d065c4fef0247448e17a8da0000000000000000000000000000000000000000000000000002901f2d62bc91b77000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000000000000000000000000000000000007881786100000000000000000000000000000000000000000000000000000000001984730000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000000000000000000000000000000000000000000000000000000000000000a02bc8495ad1c76c31d466ce719f80400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041f3a03b07f390cd707402912278414c46190ca8ca362dd218b9a58956178cb6ee0e5755db7abe02fe15d498d092d4c6865a5eb18486b3e45e27d50d34b87bf1e21c00000000000000000000000000000000000000000000000000000000000000").unwrap(); + let partial_fill_offset = 12u64; + let quote_amount_out = BigUint::from_str("1672307").unwrap(); + + let bebop_state = MockRFQState { + quote_amount_out, + quote_data: HashMap::from([ + ("calldata".to_string(), bebop_calldata), + ( + "partial_fill_offset".to_string(), + Bytes::from( + partial_fill_offset + .to_be_bytes() + .to_vec(), + ), + ), + ]), + }; + + let bebop_component = ProtocolComponent { + id: String::from("bebop-rfq"), + protocol_system: String::from("rfq:bebop"), + ..Default::default() + }; + + let swap_usdc_wbtc = SwapBuilder::new(bebop_component, usdc.clone(), wbtc.clone()) + .estimated_amount_in(BigUint::from_str("2021750881").unwrap()) + .protocol_state(Arc::new(bebop_state)) + .build(); + + let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); + + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1000000000000000000").unwrap(), // 0.099 WETH + checked_token: wbtc, + checked_amount: BigUint::from_str("1672307").unwrap(), + sender: bob_address(), + receiver: bob_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_bebop", 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("3714751").unwrap(); + + let hashflow_state = MockRFQState { + quote_amount_out, + quote_data: HashMap::from([ + ( + "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(), + ), + ( + "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()), + ]), + }; + + 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(Arc::new(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("1000000000000000000").unwrap(), + checked_token: wbtc, + checked_amount: BigUint::from_str("3714751").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()); +} diff --git a/tests/protocol_integration_tests.rs b/tests/protocol_integration_tests.rs index 88e669f..7e237b6 100644 --- a/tests/protocol_integration_tests.rs +++ b/tests/protocol_integration_tests.rs @@ -1,18 +1,22 @@ mod common; -use std::{collections::HashMap, str::FromStr}; +use std::{collections::HashMap, str::FromStr, sync::Arc}; -use alloy::hex::encode; +use alloy::{hex, hex::encode}; use num_bigint::{BigInt, BigUint}; use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use tycho_execution::encoding::{ - evm::utils::write_calldata_to_file, - models::{Solution, Swap, UserTransferType}, + evm::{ + testing_utils::MockRFQState, + utils::{biguint_to_u256, write_calldata_to_file}, + }, + models::{Solution, Swap, SwapBuilder, UserTransferType}, }; use crate::common::{ - encoding::encode_tycho_router_call, eth, eth_chain, get_signer, get_tycho_router_encoder, pepe, - usdc, weth, + alice_address, build_bebop_calldata, encoding::encode_tycho_router_call, eth, eth_chain, + get_signer, get_tycho_router_encoder, ondo, pepe, usdc, wbtc, weth, }; + #[test] fn test_single_encoding_strategy_ekubo() { // ETH ──(EKUBO)──> USDC @@ -41,6 +45,7 @@ fn test_single_encoding_strategy_ekubo() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -94,6 +99,7 @@ fn test_single_encoding_strategy_maverick() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -159,6 +165,7 @@ fn test_single_encoding_strategy_usv4_eth_in() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -226,6 +233,7 @@ fn test_single_encoding_strategy_usv4_eth_out() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -301,6 +309,7 @@ fn test_single_encoding_strategy_usv4_grouped_swap() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let swap_eth_pepe = Swap { @@ -315,6 +324,7 @@ fn test_single_encoding_strategy_usv4_grouped_swap() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -427,6 +437,7 @@ fn test_single_encoding_strategy_curve() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -495,6 +506,7 @@ fn test_single_encoding_strategy_curve_st_eth() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -549,6 +561,7 @@ fn test_single_encoding_strategy_balancer_v3() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -584,3 +597,230 @@ fn test_single_encoding_strategy_balancer_v3() { let hex_calldata = encode(&calldata); write_calldata_to_file("test_single_encoding_strategy_balancer_v3", hex_calldata.as_str()); } + +#[test] +fn test_single_encoding_strategy_bebop() { + // The quote was done separately where the sender is the router and the receiver is a random + // user + let _router = Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(); + let user = Bytes::from_str("0xd2068e04cf586f76eece7ba5beb779d7bb1474a1").unwrap(); + + let token_in = usdc(); + let token_out = ondo(); + let amount_in = BigUint::from_str("200000000").unwrap(); // 200 USDC + let amount_out = BigUint::from_str("194477331556159832309").unwrap(); // 203.8 ONDO + let partial_fill_offset = 12; + + let calldata = Bytes::from_str("0x4dcebcba00000000000000000000000000000000000000000000000000000000689b548f0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000067336cec42645f55059eff241cb02ea5cc52ff86000000000000000000000000000000000000000000000000279ead5d9685f25b000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be3000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000a8aea46aa4ec5c0f5000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000000005230bcb979c81cebf94a3b5c08bcfa300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000414ce40058ff07f11d9224c2c8d1e58369e4a90173856202d8d2a17da48058ad683dedb742eda0d4c0cf04cf1c09138898dd7fd06f97268ea7f74ef9b42d29bf4c1b00000000000000000000000000000000000000000000000000000000000000").unwrap(); + let user_data = + build_bebop_calldata(&calldata, partial_fill_offset, biguint_to_u256(&amount_in)); + + let bebop_component = ProtocolComponent { + id: String::from("bebop-rfq"), + protocol_system: String::from("rfq:bebop"), + static_attributes: HashMap::new(), // No static attributes needed + ..Default::default() + }; + + let swap = SwapBuilder::new(bebop_component, token_in.clone(), token_out.clone()) + .user_data(user_data) + .build(); + + let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); + + let solution = Solution { + exact_out: false, + given_token: token_in, + given_amount: amount_in, + checked_token: token_out, + checked_amount: amount_out, // Expected output amount + sender: user.clone(), + receiver: user, + swaps: vec![swap], + ..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 = hex::encode(&calldata); + write_calldata_to_file("test_single_encoding_strategy_bebop", hex_calldata.as_str()); +} + +#[test] +fn test_single_encoding_strategy_bebop_aggregate() { + // The quote was done separately where the sender is the router and the receiver is a random + // user + let _router = Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(); + let user = Bytes::from_str("0xd2068e04cf586f76eece7ba5beb779d7bb1474a1").unwrap(); + + let token_in = usdc(); + let token_out = ondo(); + let amount_in = BigUint::from_str("20000000000").unwrap(); // 20k USDC + let amount_out = BigUint::from_str("18699321819466078474202").unwrap(); // 203.8 ONDO + let partial_fill_offset = 2; + + let calldata = Bytes::from_str("0xa2f7489300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000689b78880000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c0000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000005a060a5c2aaaaa2fe2cda34423cac76a84c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000002901f2d62bb356ca0000000000000000000000000000000000000000000000002901f2d62bb356cb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000044f83c726000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000589400da00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000003aa5f96046644f6e37a000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000004b51a26526ddbeec60000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000417ab4332f2b091d87d56d04eee35dd49452782c782de71608c0425c5ae41f1d7e147173851c870d76720ce07d45cd8622352716b1c7965819ee2bf8c573c499ae1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000410c8da2637aa929e11caff9afdfc4c489320c6dba77cc934d88ba8956e365fd1d48983087c6e474bbb828181cdfdd17317c4c9c3ee4bc98e3769d0c05cc7a285e1c00000000000000000000000000000000000000000000000000000000000000").unwrap(); + let user_data = + build_bebop_calldata(&calldata, partial_fill_offset, biguint_to_u256(&amount_in)); + + let bebop_component = ProtocolComponent { + id: String::from("bebop-rfq"), + protocol_system: String::from("rfq:bebop"), + static_attributes: HashMap::new(), + ..Default::default() + }; + + let swap = SwapBuilder::new(bebop_component, token_in.clone(), token_out.clone()) + .user_data(user_data) + .build(); + + let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); + + let solution = Solution { + exact_out: false, + given_token: token_in.clone(), + given_amount: amount_in, + checked_token: token_out, + checked_amount: amount_out, + sender: user.clone(), + receiver: user, + swaps: vec![swap], + ..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 = hex::encode(&calldata); + + write_calldata_to_file("test_single_encoding_strategy_bebop_aggregate", hex_calldata.as_str()); +} + +#[test] +fn test_single_encoding_strategy_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: HashMap::from([ + ( + "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(), + ), + ( + "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()), + ]), + }; + + 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(Arc::new(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_single_encoding_strategy_hashflow", hex_calldata.as_str()); +} diff --git a/tests/sequential_strategy_integration_tests.rs b/tests/sequential_strategy_integration_tests.rs index 4d2a758..b982c9b 100644 --- a/tests/sequential_strategy_integration_tests.rs +++ b/tests/sequential_strategy_integration_tests.rs @@ -6,7 +6,7 @@ use num_bigint::{BigInt, BigUint}; use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use tycho_execution::encoding::{ evm::utils::write_calldata_to_file, - models::{Solution, Swap, UserTransferType}, + models::{NativeAction, Solution, Swap, UserTransferType}, }; use crate::common::{ @@ -19,7 +19,7 @@ fn test_sequential_swap_strategy_encoder() { // 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 USDC though WBTC using USV2 pools + // Performs a sequential swap from WETH to USDC through WBTC using USV2 pools // // WETH ───(USV2)──> WBTC ───(USV2)──> USDC @@ -38,6 +38,7 @@ fn test_sequential_swap_strategy_encoder() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let swap_wbtc_usdc = Swap { component: ProtocolComponent { @@ -50,6 +51,7 @@ fn test_sequential_swap_strategy_encoder() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -106,6 +108,7 @@ fn test_sequential_swap_strategy_encoder_no_permit2() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let swap_wbtc_usdc = Swap { component: ProtocolComponent { @@ -118,6 +121,7 @@ fn test_sequential_swap_strategy_encoder_no_permit2() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -220,6 +224,7 @@ fn test_sequential_strategy_cyclic_swap() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; // WETH -> USDC (Pool 2) @@ -243,6 +248,7 @@ fn test_sequential_strategy_cyclic_swap() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -316,3 +322,77 @@ fn test_sequential_strategy_cyclic_swap() { assert_eq!(hex_calldata[1224..], expected_swaps); write_calldata_to_file("test_sequential_strategy_cyclic_swap", hex_calldata.as_str()); } + +#[test] +fn test_sequential_swap_strategy_encoder_unwrap() { + // 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 USDC to ETH through WBTC using USV2 pools and unwrapping in + // the end + // + // USDC ───(USV2)──> WBTC ───(USV2)──> WETH -> ETH + + let weth = weth(); + let wbtc = wbtc(); + let usdc = usdc(); + + let swap_usdc_wbtc = Swap { + component: ProtocolComponent { + id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: usdc.clone(), + token_out: wbtc.clone(), + split: 0f64, + user_data: None, + protocol_state: None, + estimated_amount_in: None, + }; + let swap_wbtc_weth = Swap { + component: ProtocolComponent { + id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: wbtc.clone(), + token_out: weth.clone(), + split: 0f64, + user_data: None, + protocol_state: None, + estimated_amount_in: None, + }; + let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); + + let solution = Solution { + exact_out: false, + given_token: usdc, + given_amount: BigUint::from_str("3_000_000_000").unwrap(), + checked_token: eth(), + checked_amount: BigUint::from_str("26173932").unwrap(), + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap_usdc_wbtc, swap_wbtc_weth], + native_action: Some(NativeAction::Unwrap), + }; + + 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::TransferFromPermit2, + ð(), + Some(get_signer()), + ) + .unwrap() + .data; + + let hex_calldata = encode(&calldata); + write_calldata_to_file("test_sequential_swap_strategy_encoder_unwrap", hex_calldata.as_str()); +} diff --git a/tests/single_strategy_integration_tests.rs b/tests/single_strategy_integration_tests.rs index 30302ce..935c794 100644 --- a/tests/single_strategy_integration_tests.rs +++ b/tests/single_strategy_integration_tests.rs @@ -34,6 +34,7 @@ fn test_single_swap_strategy_encoder() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -121,6 +122,7 @@ fn test_single_swap_strategy_encoder_no_permit2() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -204,6 +206,7 @@ fn test_single_swap_strategy_encoder_no_transfer_in() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::None); @@ -288,6 +291,7 @@ fn test_single_swap_strategy_encoder_wrap() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -341,6 +345,7 @@ fn test_single_swap_strategy_encoder_unwrap() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); diff --git a/tests/split_strategy_integration_tests.rs b/tests/split_strategy_integration_tests.rs index 2bd5d45..e980a20 100644 --- a/tests/split_strategy_integration_tests.rs +++ b/tests/split_strategy_integration_tests.rs @@ -43,6 +43,7 @@ fn test_split_swap_strategy_encoder() { split: 0.5f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let swap_weth_wbtc = Swap { component: ProtocolComponent { @@ -58,6 +59,7 @@ fn test_split_swap_strategy_encoder() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let swap_dai_usdc = Swap { component: ProtocolComponent { @@ -70,6 +72,7 @@ fn test_split_swap_strategy_encoder() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let swap_wbtc_usdc = Swap { component: ProtocolComponent { @@ -82,6 +85,7 @@ fn test_split_swap_strategy_encoder() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -149,6 +153,7 @@ fn test_split_input_cyclic_swap() { split: 0.6f64, // 60% of input user_data: None, protocol_state: None, + estimated_amount_in: None, }; // USDC -> WETH (Pool 2) - 40% of input (remaining) @@ -172,6 +177,7 @@ fn test_split_input_cyclic_swap() { split: 0f64, user_data: None, // Remaining 40% protocol_state: None, + estimated_amount_in: None, }; // WETH -> USDC (Pool 2) @@ -195,6 +201,7 @@ fn test_split_input_cyclic_swap() { split: 0.0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -316,6 +323,7 @@ fn test_split_output_cyclic_swap() { split: 0.0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let swap_weth_usdc_v3_pool1 = Swap { @@ -336,6 +344,7 @@ fn test_split_output_cyclic_swap() { split: 0.6f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let swap_weth_usdc_v3_pool2 = Swap { @@ -358,6 +367,7 @@ fn test_split_output_cyclic_swap() { split: 0.0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); diff --git a/tests/uniswap_x_integration_tests.rs b/tests/uniswap_x_integration_tests.rs index 0bf898d..03752af 100644 --- a/tests/uniswap_x_integration_tests.rs +++ b/tests/uniswap_x_integration_tests.rs @@ -52,6 +52,7 @@ fn test_sequential_swap_usx() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let swap_usdc_usdt = Swap { component: ProtocolComponent { @@ -70,6 +71,7 @@ fn test_sequential_swap_usx() { split: 0f64, user_data: None, protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);