diff --git a/CHANGELOG.md b/CHANGELOG.md index 2df9f81..97d7c0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,155 @@ +## [0.123.0](https://github.com/propeller-heads/tycho-execution/compare/0.122.0...0.123.0) (2025-09-02) + + +### Features + +* pin tycho-common version ([b0783fb](https://github.com/propeller-heads/tycho-execution/commit/b0783fb2c1f9e89062ba3fbb269929249afd45c7)) + +## [0.122.0](https://github.com/propeller-heads/tycho-execution/compare/0.121.0...0.122.0) (2025-08-28) + + +### Features + +* Bump tycho-common version ([8101ff3](https://github.com/propeller-heads/tycho-execution/commit/8101ff31c8bc64e80a50cc907823ebb5dd8b2f1f)) + +## [0.121.0](https://github.com/propeller-heads/tycho-execution/compare/0.120.0...0.121.0) (2025-08-28) + + +### Features + +* rename "uniswap_v4" to "uniswap_v4_hooks" ([c1d88be](https://github.com/propeller-heads/tycho-execution/commit/c1d88be3295d9f9959a271795541e7ee4bbcfc4c)) +* Support hooks (without special calldata) ([a058177](https://github.com/propeller-heads/tycho-execution/commit/a0581773cd35fa2c34f8e305f62901e7b5f1cf71)) +* **univ4:** Pass user_data as hook_data in execution ([93678d9](https://github.com/propeller-heads/tycho-execution/commit/93678d9d1987eaa9fff8c19a6fe69e7771ed582c)) +* Update UniswapV4 executor addresses ([d7e377e](https://github.com/propeller-heads/tycho-execution/commit/d7e377e44323014661b21e79197eaf1ad7748587)) + + +### Bug Fixes + +* access hooks attr, not hook ([2c38cf8](https://github.com/propeller-heads/tycho-execution/commit/2c38cf84fcbfd195112a11b770e3f70328c1d1b2)) +* Add missing uniswap v4 hooks executor addresses ([0daccfe](https://github.com/propeller-heads/tycho-execution/commit/0daccfea9ab85fcf967c593f684a75fca7f0061e)) +* Add missing uniswap v4 hooks executor addresses ([5ab9e99](https://github.com/propeller-heads/tycho-execution/commit/5ab9e9925ab6855a05226bfac9fd5723c7ca1cea)) +* After rebase fixes ([1d952b1](https://github.com/propeller-heads/tycho-execution/commit/1d952b19be226475cd7451bad899f4bbe6a4c52e)) +* Recalculate calldata after merging main ([2adcb89](https://github.com/propeller-heads/tycho-execution/commit/2adcb89e6d3666bf76f223e55bb9d8e8b6db06ee)) +* revert hooks rename ([ea9a1a8](https://github.com/propeller-heads/tycho-execution/commit/ea9a1a88ca3dae3f9099dd0b84cff456d4ad5bfc)) +* Small safe wallet fixes ([c9d09e9](https://github.com/propeller-heads/tycho-execution/commit/c9d09e91098f0a76c96ad1cd157e1a461887f468)) + +## [0.120.0](https://github.com/propeller-heads/tycho-execution/compare/0.119.0...0.120.0) (2025-08-27) + + +### Features + +* Deploy new hashflow router ([eac45fa](https://github.com/propeller-heads/tycho-execution/commit/eac45faf5a56a50dacff374da7c3701e5a1d3f33)) + +## [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) + + +### Features + +* Add protocol state to Swap object ([c217702](https://github.com/propeller-heads/tycho-execution/commit/c21770256045b3fdaddb889effa09b839f59755e)) + + +### Bug Fixes + +* Replace smart pointer with regular pointer ([95c5124](https://github.com/propeller-heads/tycho-execution/commit/95c51247f73516f387e2169e63a22311b4343b8d)) + ## [0.111.0](https://github.com/propeller-heads/tycho-execution/compare/0.110.0...0.111.0) (2025-07-15) diff --git a/Cargo.lock b/Cargo.lock index e67b69f..258d4cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4634,9 +4634,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tycho-common" -version = "0.76.0" +version = "0.82.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72ed04dc9d41942c886fc3d58af5b0e23a3e4783ac7294ed7cea61e022a5c4d" +checksum = "096c87ebe011785fcd7ed59ec501ac12b465a64fbd2914b8c0c57125c253682b" dependencies = [ "anyhow", "async-trait", @@ -4659,9 +4659,10 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.111.0" +version = "0.123.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 d880a11..cda8746 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "0.111.0" +version = "0.123.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.75.1" - +tycho-common = "0.82.0" 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 72f95f7..b25725a 100644 --- a/config/executor_addresses.json +++ b/config/executor_addresses.json @@ -5,21 +5,27 @@ "pancakeswap_v2": "0xC9db3FEB380E4fd9af239e2595ECdEcE3b5c34A4", "uniswap_v3": "0xbab7124C9662B15C6b9AF0b1f329907dD55a24FC", "pancakeswap_v3": "0x9D32e9F569B22Ae8d8C6f788037C1CD53632A059", - "uniswap_v4": "0xD11496EAb53A9521f0bC1e5c1098Ecb467103Ad9", + "uniswap_v4": "0x2C2EaB81Cf983602153E67b1890164BC4CABC6ed", + "uniswap_v4_hooks": "0x2C2EaB81Cf983602153E67b1890164BC4CABC6ed", "vm:balancer_v2": "0xB5b8dc3F0a1Be99685a0DEd015Af93bFBB55C411", "ekubo_v2": "0x263DD7AD20983b5E0392bf1F09C4493500EDb333", "vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165", "vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B", - "vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec" + "vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec", + "rfq:bebop": "0xFE42BFb115eD9671011cA52BDD23A52A2e077a7c", + "rfq:hashflow": "0x19e49Db786c87F4e46B10aFb21c0C06d34270f98" }, "base": { "uniswap_v2": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700", "uniswap_v3": "0x647bffbf8bd72bf6341ecba8b0279e090313a40d", - "uniswap_v4": "0xb5628b97f491f85766673ac4d5d47bb1af7fcc4a" + "uniswap_v4": "0x7Dfa502736C7bd84DA1402F7524214215BC9534d", + "uniswap_v4_hooks": "0x7Dfa502736C7bd84DA1402F7524214215BC9534d", + "rfq:bebop": "0x489A3f531dA3873D6585BF3f8E0dEE48CAC6F7BC" }, "unichain": { "uniswap_v2": "0x00C1b81e3C8f6347E69e2DDb90454798A6Be975E", "uniswap_v3": "0xD26A838A41af3d4815DfD745a080B2062c4124d1", - "uniswap_v4": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700" + "uniswap_v4": "0x647bfFbF8Bd72bF6341ECBa8B0279e090313A40D", + "uniswap_v4_hooks": "0x647bfFbF8Bd72bF6341ECBa8B0279e090313A40D" } } 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 130a686..4f77f11 100644 --- a/config/test_executor_addresses.json +++ b/config/test_executor_addresses.json @@ -6,10 +6,13 @@ "uniswap_v3": "0x2e234DAe75C793f67A35089C9d99245E1C58470b", "pancakeswap_v3": "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9", "uniswap_v4": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a", + "uniswap_v4_hooks": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a", "vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1", "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 1eb1f44..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() { @@ -43,6 +43,8 @@ fn main() { // the amount or the total remaining balance. split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; // Then we create a solution object with the previous swap @@ -86,52 +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, - }; - 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, - }; - 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, - }; - 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, - }; + 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 b2e2f5e..bb314d6 100644 --- a/examples/uniswapx-encoding-example/main.rs +++ b/examples/uniswapx-encoding-example/main.rs @@ -82,6 +82,8 @@ fn main() { token_out: usdc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let swap_usdc_usdt = Swap { component: ProtocolComponent { @@ -99,6 +101,8 @@ fn main() { token_out: usdt.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; // Then we create a solution object with the previous swap @@ -165,7 +169,6 @@ fn main() { println!(" ====== Simple swap DAI -> USDT ======"); println!( "The following callback data should be sent to the filler contract, along with the \ - encoded order and signature: {:?}", - hex_calldata + encoded order and signature: {hex_calldata:?}" ); } 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/README.md b/foundry/scripts/README.md index 713c308..5e937a5 100644 --- a/foundry/scripts/README.md +++ b/foundry/scripts/README.md @@ -65,7 +65,7 @@ For each of the following, you must select one of `tenderly_ethereum`, `tenderly 1. If the wallet that has the role, is a Gnosis Safe, you need to set the `SAFE_ADDRESS` env var. 2. The scripts deploy-executors, remove-executor, set-roles and revoke-role all support this. 1. If `SAFE_ADDRESS` is set, then it will propose a transaction to the safe wallet and later on it needs to be - approved in their UI to execute on chain. + approved in their UI to execute on chain. Be sure to change the PRIVATE_KEY to that which has permissions on the safe wallet. 2. If it's not set, it will submit the transaction directly to the chain. ## Deploy Uniswap X filler diff --git a/foundry/scripts/deploy-executors.js b/foundry/scripts/deploy-executors.js index e50a4fe..84cf4b9 100644 --- a/foundry/scripts/deploy-executors.js +++ b/foundry/scripts/deploy-executors.js @@ -81,6 +81,16 @@ const executors_to_deploy = { }, // Args: Permit2 {exchange: "BalancerV3Executor", args: ["0x000000000022D473030F116dDEE9F6B43aC78BA3"]}, + // Args: Bebop Settlement contract, Permit2 + { + exchange: "BebopExecutor", + args: ["0xbbbbbBB520d69a9775E85b458C58c648259FAD5F", "0x000000000022D473030F116dDEE9F6B43aC78BA3"] + }, + // Args: Hashflow router, Permit2 + { + exchange: "HashflowExecutor", + args: ["0x55084eE0fEf03f14a305cd24286359A35D735151", "0x000000000022D473030F116dDEE9F6B43aC78BA3"] + }, ], "base": [ // Args: Factory, Pool Init Code Hash, Permit2, Fee BPS @@ -116,6 +126,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/scripts/utils.js b/foundry/scripts/utils.js index 04faeb8..c59bf9e 100644 --- a/foundry/scripts/utils.js +++ b/foundry/scripts/utils.js @@ -4,7 +4,7 @@ const {EthersAdapter} = require('@safe-global/protocol-kit'); const {default: SafeApiKit} = require("@safe-global/api-kit"); const txServiceUrls = { - mainnet: "https://safe-transaction-mainnet.safe.global", + ethereum: "https://safe-transaction-mainnet.safe.global", base: "https://safe-transaction-base.safe.global", unichain: "https://safe-transaction-unichain.safe.global", }; 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 06fea2b..8f245f6 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -20,11 +20,13 @@ 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"; import "@openzeppelin/contracts/utils/Address.sol"; +import "../../lib/bytes/LibPrefixLengthEncodedByteArray.sol"; error UniswapV4Executor__InvalidDataLength(); error UniswapV4Executor__NotPoolManager(); @@ -43,8 +45,9 @@ 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; IPoolManager public immutable poolManager; address private immutable _self; @@ -86,6 +89,8 @@ contract UniswapV4Executor is bool zeroForOne, TransferType transferType, address receiver, + address hook, + bytes memory hookData, UniswapV4Executor.UniswapV4Pool[] memory pools ) = _decodeData(data); bytes memory swapData; @@ -95,7 +100,7 @@ contract UniswapV4Executor is currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn), fee: pools[0].fee, tickSpacing: pools[0].tickSpacing, - hooks: IHooks(address(0)) + hooks: IHooks(hook) }); swapData = abi.encodeWithSelector( this.swapExactInputSingle.selector, @@ -104,7 +109,7 @@ contract UniswapV4Executor is amountIn, transferType, receiver, - bytes("") + hookData ); } else { PathKey[] memory path = new PathKey[](pools.length); @@ -113,8 +118,8 @@ contract UniswapV4Executor is intermediateCurrency: Currency.wrap(pools[i].intermediaryToken), fee: pools[i].fee, tickSpacing: pools[i].tickSpacing, - hooks: IHooks(address(0)), - hookData: bytes("") + hooks: IHooks(hook), + hookData: hookData }); } @@ -144,10 +149,12 @@ contract UniswapV4Executor is bool zeroForOne, TransferType transferType, address receiver, + address hook, + bytes memory hookData, UniswapV4Pool[] memory pools ) { - if (data.length < 88) { + if (data.length < 108) { revert UniswapV4Executor__InvalidDataLength(); } @@ -156,25 +163,42 @@ contract UniswapV4Executor is zeroForOne = data[40] != 0; transferType = TransferType(uint8(data[41])); receiver = address(bytes20(data[42:62])); + hook = address(bytes20(data[62:82])); - uint256 poolsLength = (data.length - 62) / 26; // 26 bytes per pool object - pools = new UniswapV4Pool[](poolsLength); - bytes memory poolsData = data[62:]; - uint256 offset = 0; - for (uint256 i = 0; i < poolsLength; i++) { + bytes calldata remaining = data[82:]; + address firstToken = address(bytes20(remaining[0:20])); + uint24 firstFee = uint24(bytes3(remaining[20:23])); + int24 firstTickSpacing = int24(uint24(bytes3(remaining[23:26]))); + UniswapV4Pool memory firstPool = + UniswapV4Pool(firstToken, firstFee, firstTickSpacing); + + // Remaining after first pool are ple encoded + bytes[] memory encodedPools = + LibPrefixLengthEncodedByteArray.toArray(remaining[26:]); + + pools = new UniswapV4Pool[](1 + encodedPools.length); + pools[0] = firstPool; + + uint256 encodedPoolsLength = 26; + uint256 plePoolsTotalLength; + + for (uint256 i = 0; i < encodedPools.length; i++) { + bytes memory poolsData = encodedPools[i]; address intermediaryToken; uint24 fee; int24 tickSpacing; // slither-disable-next-line assembly assembly { - intermediaryToken := mload(add(poolsData, add(offset, 20))) - fee := shr(232, mload(add(poolsData, add(offset, 52)))) - tickSpacing := shr(232, mload(add(poolsData, add(offset, 55)))) + intermediaryToken := mload(add(poolsData, add(0, 20))) + fee := shr(232, mload(add(poolsData, add(0, 52)))) + tickSpacing := shr(232, mload(add(poolsData, add(0, 55)))) } - pools[i] = UniswapV4Pool(intermediaryToken, fee, tickSpacing); - offset += 26; + pools[i + 1] = UniswapV4Pool(intermediaryToken, fee, tickSpacing); + plePoolsTotalLength += 2 + encodedPoolsLength; // 2 bytes prefix + data } + + hookData = remaining[26 + plePoolsTotalLength:]; } /** 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 730de23..a56c5c5 100644 --- a/foundry/test/assets/calldata.txt +++ b/foundry/test/assets/calldata.txt @@ -3,34 +3,40 @@ 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:30ace1b1000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000005064ff624d54346285543f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000006880f27600000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068596c7e00000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041509fa14e28ad71fed11dae329c5d17b4cc634b74379cef3b5c5bfe445d5a929d0e139ad79e8ec09bc6368b9d1a5ad350d98e467e05e453d47431333ee985fd4d1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000086f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb486982508145454ce325ddbe47a25d4ec3d23119330000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000bb800003c6982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000000000000000000000000000 -test_single_encoding_strategy_usv4_eth_out:30ace1b100000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f81490b4f29aade00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000000000000000000000000000000000006880f27600000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068596c7e00000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041df4f662a218a599150507f1089ea781771903aa5ed41e5f24d57483074b469c031a18bb247df38ecf866faf5ff86cec389e3410124260218fb99dc9e01b788b01c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006cf62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000bb800003c0000000000000000000000000000000000000000 -test_sequential_swap_strategy_encoder:51bcc7b60000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006880f27600000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068596c7e00000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041176632aaa7a353a547b128b56c490c4da926ad7ba12aeace54629f750e937ab704fa1f9b9149872d39553d2d987ec882840b73a44c036d476ea0e75439346c2f1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a800525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d940004375dff511095cc5a197a54140a24efef3a416000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20102000000000000000000000000000000000000000000000000 +test_single_encoding_strategy_usv4_grouped_swap:30ace1b1000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000005064ff624d54346285543f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000068d7599b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068afd3a300000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041ebbdc04a538db8dd94efc676e9afc8ea514aab6b2aab4ef13dc295b5942058457a4367921be403a282320b0be014febcd49cf92bfde789ebc27689503f0b9e921c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009cf62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb486982508145454ce325ddbe47a25d4ec3d23119330000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000bb800003c001a6982508145454ce325ddbe47a25d4ec3d23119330061a80001f400000000 +test_single_encoding_strategy_usv4_eth_out:30ace1b100000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f81490b4f29aade00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000068d7599b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068afd3a300000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041e2ddb73e53f72458b4568486f38974071030e4ce563a291986db8e84fe6727ab622ecafedcd3790a5fb1d89de2311d28c21d6ae6042e4097097d88caa410b3c51c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000bb800003c +test_sequential_swap_strategy_encoder:51bcc7b60000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068d7599c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068afd3a400000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041d5e4a919fb659675b80ef724b13b5e58213422b52fd20d347892671adf107f4d78742558f3c5fc816949a155398fa5f495c734da2804de345dc88014e13edbaa1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a800525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d940004375dff511095cc5a197a54140a24efef3a416000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20102000000000000000000000000000000000000000000000000 test_single_swap_strategy_encoder_no_permit2:5c4b639c0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000058e7926ee858a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000 test_single_swap_strategy_encoder_no_transfer_in:5c4b639c0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000058e7926ee858a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200010000000000000000000000000000 -test_single_encoding_strategy_usv4_eth_in:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000007e0a55d4322a6e93c2379c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006880f27600000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068596c7e00000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000004128c50a468f1b4bb63b28f8dd8c33c89b065effdf718ce87632e7d16f3a22a8c661e05ccd2d9fb65719f6b3123a40e25a0b5ef1e21f939344e2b92ce0d97805b91c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006cf62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330102cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc26982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000000000000000 -test_sequential_strategy_cyclic_swap:51bcc7b60000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ec8f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000006880f27600000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068596c7e00000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000004110cf1125cc7207bf96734e97da8e1a7e101e8e9a5886128a6e607e8b3d1fb9775b00f4290b2cc85692273a9622388475ba1a62771d1a834d529af36562139e3e1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f5640010000692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc28ad599c3a0ff1de082011efddc58f1908eb6e6d8000100000000000000000000 +test_single_encoding_strategy_usv4_eth_in:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000007e0a55d4322a6e93c2379c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068d7599b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068afd3a300000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041fcaea674ca23f6a2c99960f08f3cc28c94169276db488ea05052b3cff8d6f32469d9314f4952cdbaf221e93374dd77678fa8de9d64bebfa134c21ab071360b921b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080f62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330102cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330061a80001f4 +test_sequential_strategy_cyclic_swap:51bcc7b60000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ec8f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000068d7599c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068afd3a400000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000417d4cf76ac324491f89558837c0ae1c0d785165bd181e176f4763436a3fc1dc9f3882f3c7a3640fc37a0fd69394daafefb2ec4c12261b90fafd74e18d603de22e1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f5640010000692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc28ad599c3a0ff1de082011efddc58f1908eb6e6d8000100000000000000000000 test_single_encoding_strategy_curve_st_eth:5c4b639c0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ae7ab96520de3a18e5e111b5eaab095312d7fe84000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000691d1499e622d69689cdf9004d05ec547d650ff211eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeae7ab96520de3a18e5e111b5eaab095312d7fe84dc24316b9ae028f1497c275eb9192a3ea0f670220100010002cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000 test_single -test_encode_uniswap_v4_sequential_swap:4c9edd5852cd905f086c759e8383e09bff1e68b32260fac5e5542a773aa44fbcfedf7c193bc2c5990101cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2dac17f958d2ee523a2206206994597c13d831ec70000640000012260fac5e5542a773aa44fbcfedf7c193bc2c599000bb800003c +test_encode_uniswap_v4_sequential_swap:4c9edd5852cd905f086c759e8383e09bff1e68b32260fac5e5542a773aa44fbcfedf7c193bc2c5990101cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000064000001001a2260fac5e5542a773aa44fbcfedf7c193bc2c599000bb800003c 6d70b85442ed96492800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006869398600000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006841b38e00000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041752ca399237fc5086ef89d5f6dabecfb4b43c0753ecfb7020a6a86045db423fd3be9565f79b511fe93f55f76f61b1ac8d786b04051110ca6cbe10bbf69901b871c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000 test_single_encoding_strategy_curve:5c4b639c0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000055c08ca52497e2f1534b59e2917bf524d4765257000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000691d1499e622d69689cdf9004d05ec547d650ff21155c08ca52497e2f1534b59e2917bf524d4765257c02aaa39b223fe8d0a0e5c4f27ead9083c756cc277146b0a1d08b6844376df6d9da99ba7f1b19e710201000100cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000 -test_single_swap_strategy_encoder_unwrap:30ace1b10000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be00000000000000000000000000000000000000000000000000000000000006880f27700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068596c7f00000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041ef8669fa0cfea3980202c2526db6e383b14dec72f5b0423432a6e4783a6bc56d51a24d3cf7da4852cd95bb8ad37827877e38bf4b4716111e7113b8af17f6de3d1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139501000000000000000000000000000000 -test_single_swap_strategy_encoder_wrap:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000059fb7d3830e6fc064b00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006880f27700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068596c7f00000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000004143ab32167d0352fc20dda7b7c0c3fd76452dc2bd6bd675ad3b4ffbcc908ae6401bb7df63b661cb43cab3f8397ea8db0ba510c5c1d939688e4674a108cf46180c1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200010000000000000000000000000000 -test_split_output_cyclic_swap:7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005e703f4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000006880f27800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068596c80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041cf6d57a1ab0d7cba6d5c8739142ad0f0a1ea07d50b7f82a644ec5d48125aa3f35a64e1abd5c0007274005e8f5e0031334da2e96a06b2935267f947f8e856f6e11b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000139005700010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d013950100006e01009999992e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f4cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc288e6a0c2ddd26feeb64f039a2c41296fcb3f56400001006e01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc28ad599c3a0ff1de082011efddc58f1908eb6e6d8000100000000000000 -test_split_input_cyclic_swap:7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ef619b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000006880f27800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068596c80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041cf6d57a1ab0d7cba6d5c8739142ad0f0a1ea07d50b7f82a644ec5d48125aa3f35a64e1abd5c0007274005e8f5e0031334da2e96a06b2935267f947f8e856f6e11b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000139006e00019999992e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f56400100006e00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d80100005701000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2b4e16d0168e52d35cacd2c6185b44281ec28c9dccd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000100000000000000 -test_split_swap_strategy_encoder:7c5538460000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006880f27800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068596c80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041a0be3470609c83fed0b58f0171e2e929ed46ddc952b8ccd76a54e6eb686f9ad961e9ec6a7f89423584fd0b56336640cf62b44c658b4616a2cf79b0cdf0c8980e1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000164005700028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950000005700010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d013950000005702030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d5cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20101005701030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2010100000000000000000000000000000000000000000000000000000000 +test_single_swap_strategy_encoder_unwrap:30ace1b10000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000000000000000000000000000000000000068d7599c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068afd3a400000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000004129585411da9aa88d63de0091674010616c954beb6d460858bd128fa915e5fd801f460751206f06d691251f05ec81adf17d36fb1f952543bdea532e6dace36d8d1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139501000000000000000000000000000000 +test_single_swap_strategy_encoder_wrap:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000059fb7d3830e6fc064b00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068d7599c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068afd3a400000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000004164da51648eee5e9062b79b923918275208f0a87661856b80c76f82ce03cdea2d3b0ea28d95c6e73baf9db6de61b4c524cc36d6ef591af64c2f401d11ed6c08791b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200010000000000000000000000000000 +test_split_output_cyclic_swap:7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005e703f4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000068d7599d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068afd3a5000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041a60e693f701a450048dad84706bcb2f74fe26c6af512089adaacd5b3663d66ed62e5fede66ad3c4329aac1832e42ce0fc25b1ab758b4bc6d5778b33a5c9a2a4a1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000139005700010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d013950100006e01009999992e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f4cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc288e6a0c2ddd26feeb64f039a2c41296fcb3f56400001006e01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc28ad599c3a0ff1de082011efddc58f1908eb6e6d8000100000000000000 +test_split_input_cyclic_swap:7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ef619b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000068d7599d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068afd3a5000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041a60e693f701a450048dad84706bcb2f74fe26c6af512089adaacd5b3663d66ed62e5fede66ad3c4329aac1832e42ce0fc25b1ab758b4bc6d5778b33a5c9a2a4a1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000139006e00019999992e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f56400100006e00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d80100005701000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2b4e16d0168e52d35cacd2c6185b44281ec28c9dccd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000100000000000000 +test_split_swap_strategy_encoder:7c5538460000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068d7599d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068afd3a5000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041952901f3043bbb71b32116e82a840e82bc20b6d3fbdd3d7ea669f85ed16b04002ea18dd257f208a82f9c990d3c2856dae52db48c34624c501caba3054d8619b81c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000164005700028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950000005700010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d013950000005702030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d5cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20101005701030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2010100000000000000000000000000000000000000000000000000000000 test_uniswap_v3_curve:e21dd0d30000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599000bb83ede3eca2a72b3aecc820e955b36f38437d01395cbcdf9626bc03e24f779434178a73a0b4bad62ed000000691d1499e622d69689cdf9004d05ec547d650ff2112260fac5e5542a773aa44fbcfedf7c193bc2c599dac17f958d2ee523a2206206994597c13d831ec7d51a44d3fae010294c616388b506acda1bfaae460301000102cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000 -test_multi_protocol:51bcc7b600000000000000000000000000000000000000000000005150ae84a8cdf000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a2958f36da71a9200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000005150ae84a8cdf00000000000000000000000000000000000000000000000000000000000006880f27500000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068596c7d00000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041ddbcb13f2381def6b19a68af409abaefa505ddbc282c9db0a48bf53135726b7e603b248a314f46822406c9f406793673ff0e775fa584d41c3c08283f958a59d31c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021400525615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139501000072c7183455a4c133ae270771860664b6b7ec320bb1c02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599a6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e3ede3eca2a72b3aecc820e955b36f38437d01395010200691d1499e622d69689cdf9004d05ec547d650ff2112260fac5e5542a773aa44fbcfedf7c193bc2c599dac17f958d2ee523a2206206994597c13d831ec7d51a44d3fae010294c616388b506acda1bfaae4603010001023ede3eca2a72b3aecc820e955b36f38437d013950071a0cb889707d426a7a386870a03bc70d1b0697598013ede3eca2a72b3aecc820e955b36f38437d01395dac17f958d2ee523a2206206994597c13d831ec7a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000001a36e2eb1c43200000032006cf62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000001cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000bb800003c000000000000000000000000 +test_multi_protocol:51bcc7b600000000000000000000000000000000000000000000005150ae84a8cdf000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a2958f36da71a9200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000005150ae84a8cdf000000000000000000000000000000000000000000000000000000000000068d7599a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068afd3a200000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000410f2fbeabb7bd73ecafa729d3aca29ce59f05b037661cf98dc73bed03c2562d5c2daf498ff53d61704749c93f15a31b2f6c7c385cdef7416181b7a83c4bf4b4081b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022800525615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139501000072c7183455a4c133ae270771860664b6b7ec320bb1c02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599a6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e3ede3eca2a72b3aecc820e955b36f38437d01395010200691d1499e622d69689cdf9004d05ec547d650ff2112260fac5e5542a773aa44fbcfedf7c193bc2c599dac17f958d2ee523a2206206994597c13d831ec7d51a44d3fae010294c616388b506acda1bfaae4603010001023ede3eca2a72b3aecc820e955b36f38437d013950071a0cb889707d426a7a386870a03bc70d1b0697598013ede3eca2a72b3aecc820e955b36f38437d01395dac17f958d2ee523a2206206994597c13d831ec7a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000001a36e2eb1c432000000320080f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000001cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000bb800003c000000000000000000000000000000000000000000000000 test_encode_balancer_v2:c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0102 test_ekubo_encode_swap_multi:01ca4f73fe97d0b987a0d12b39bbd562c779bab6f60000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000001a36e2eb1c43200000032 -test_encode_uniswap_v4_sequential_swap:4c9edd5852cd905f086c759e8383e09bff1e68b32260fac5e5542a773aa44fbcfedf7c193bc2c5990101cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2dac17f958d2ee523a2206206994597c13d831ec70000640000012260fac5e5542a773aa44fbcfedf7c193bc2c599000bb800003c -test_encode_uniswap_v4_simple_swap:4c9edd5852cd905f086c759e8383e09bff1e68b3dac17f958d2ee523a2206206994597c13d831ec70101cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2dac17f958d2ee523a2206206994597c13d831ec7000064000001 +test_encode_uniswap_v4_sequential_swap:4c9edd5852cd905f086c759e8383e09bff1e68b32260fac5e5542a773aa44fbcfedf7c193bc2c5990101cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000064000001001a2260fac5e5542a773aa44fbcfedf7c193bc2c599000bb800003c +test_encode_uniswap_v4_simple_swap:4c9edd5852cd905f086c759e8383e09bff1e68b3dac17f958d2ee523a2206206994597c13d831ec70101cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000064000001 test_single_encoding_strategy_maverick:5c4b639c0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000040d16fc0246ad3160ccc09b8d0d3a2cd28ae6c2f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000051a4ad4f68d0b91cfd19687c881e50f3a00242828c40d16fc0246ad3160ccc09b8d0d3a2cd28ae6c2f14cf6d2fe3e1b326114b07d22a6f6bb59e346c67cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000 test_encode_maverick_v2:40d16fc0246ad3160ccc09b8d0d3a2cd28ae6c2f14cf6d2fe3e1b326114b07d22a6f6bb59e346c671d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e01 test_encode_uniswap_v2:c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb111d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0001 test_encode_balancer_v3:7bc3485026ac48b6cf9baf0a377477fff5703af8c71ea051a5f82c67adcf634c36ffe6334793d24c85b2b559bc2d21104c4defdd6efca8a20343361d011d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e test_single_encoding_strategy_balancer_v3:5c4b639c0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000097ffedb80d4b2ca6105a07a4d90eb739c45a66600000000000000000000000030881baa943777f92dc934d53d3bfdf33382cab300000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000006503a6a84cd762d9707a21605b548aaab891562aab097ffedb80d4b2ca6105a07a4d90eb739c45a66630881baa943777f92dc934d53d3bfdf33382cab3f028ac624074d6793c36dc8a06ecec0f5a39a71800cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000 test_uniswap_v3_balancer_v3:e21dd0d3000000000000000000000000000000000000000000000000002386f26fc10000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000004a220e6096b25eadb88358cb44068a324825467500000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000d200692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599000bb83ede3eca2a72b3aecc820e955b36f38437d01395cbcdf9626bc03e24f779434178a73a0b4bad62ed0000006503a6a84cd762d9707a21605b548aaab891562aab2260fac5e5542a773aa44fbcfedf7c193bc2c5994a220e6096b25eadb88358cb44068a3248254675571bea0e99e139cd0b6b7d9352ca872dfe0d72dd01cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000 -test_single_swap_strategy_encoder:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000006d70b85442ed96492800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006880f27700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068596c7f00000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000411d60a13b5e993ef8088cb2cb7e16281f47fbc329bd0ec2a48165e7898542a7ed12fe7ffdec713b6d94e99ddf0a384674617b6190b8534491d525ecb090c8e4881c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000 +test_single_swap_strategy_encoder:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000006d70b85442ed96492800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068d7599c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068afd3a400000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041d5e4a919fb659675b80ef724b13b5e58213422b52fd20d347892671adf107f4d78742558f3c5fc816949a155398fa5f495c734da2804de345dc88014e13edbaa1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000 +test_uniswap_v3_bebop:e21dd0d30000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000000000000198473000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000034400692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f5640000002d7d6bbde9174b1cdaa358d2cf4d57d1a9f7178fbffa0b86991c6218b36c1d19d4a2e9eb0ce3606eb482260fac5e5542a773aa44fbcfedf7c193bc2c599020c0000000000000000000000000000000000000000000000000000000000198473011d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e4dcebcba00000000000000000000000000000000000000000000000000000000689dcb3c0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000bee3211ab312a8d065c4fef0247448e17a8da0000000000000000000000000000000000000000000000000002901f2d62bc91b77000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000000000000000000000000000000000007881786100000000000000000000000000000000000000000000000000000000001984730000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000000000000000000000000000000000000000000000000000000000000000a02bc8495ad1c76c31d466ce719f80400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041f3a03b07f390cd707402912278414c46190ca8ca362dd218b9a58956178cb6ee0e5755db7abe02fe15d498d092d4c6865a5eb18486b3e45e27d50d34b87bf1e21c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +test_single_encoding_strategy_bebop_aggregate:5c4b639c00000000000000000000000000000000000000000000000000000004a817c800000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000003f5b138698d22b5cfda00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a1000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000897d6bbde9174b1cdaa358d2cf4d57d1a9f7178fbffa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48faba6f8e4a5e8ab82f62fe7c39859fa577269be300020000000000000000000000000000000000000000000003f5b138698d22b5cfda01d2068e04cf586f76eece7ba5beb779d7bb1474a1a2f7489300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000689b78880000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c0000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000005a060a5c2aaaaa2fe2cda34423cac76a84c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000002901f2d62bb356ca0000000000000000000000000000000000000000000000002901f2d62bb356cb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000044f83c726000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000589400da00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000003aa5f96046644f6e37a000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000004b51a26526ddbeec60000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000417ab4332f2b091d87d56d04eee35dd49452782c782de71608c0425c5ae41f1d7e147173851c870d76720ce07d45cd8622352716b1c7965819ee2bf8c573c499ae1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000410c8da2637aa929e11caff9afdfc4c489320c6dba77cc934d88ba8956e365fd1d48983087c6e474bbb828181cdfdd17317c4c9c3ee4bc98e3769d0c05cc7a285e1c00000000000000000000000000000000000000000000000000000000000000000000000000000000 +test_single_encoding_strategy_bebop:5c4b639c000000000000000000000000000000000000000000000000000000000bebc200000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be300000000000000000000000000000000000000000000000a8aea46aa4ec5c0f500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a10000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000002d7d6bbde9174b1cdaa358d2cf4d57d1a9f7178fbffa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48faba6f8e4a5e8ab82f62fe7c39859fa577269be3000c00000000000000000000000000000000000000000000000a8aea46aa4ec5c0f501d2068e04cf586f76eece7ba5beb779d7bb1474a14dcebcba00000000000000000000000000000000000000000000000000000000689b548f0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000067336cec42645f55059eff241cb02ea5cc52ff86000000000000000000000000000000000000000000000000279ead5d9685f25b000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be3000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000a8aea46aa4ec5c0f5000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000000005230bcb979c81cebf94a3b5c08bcfa300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000414ce40058ff07f11d9224c2c8d1e58369e4a90173856202d8d2a17da48058ad683dedb742eda0d4c0cf04cf1c09138898dd7fd06f97268ea7f74ef9b42d29bf4c1b00000000000000000000000000000000000000000000000000000000000000000000000000000000 +test_sequential_swap_strategy_encoder_unwrap:51bcc7b600000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000068d7599c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068afd3a400000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000414482433ce61b4c43eb784643d410d9074f395ba565b5c0a02b014ec9200052750bf220848352350e7799d64fab60c8f7ee9c44c1d51f6c8dc08f013065f4d5531b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a800525615deb798bb3e4dfa0139dfa1b3d433cc23b72fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48004375dff511095cc5a197a54140a24efef3a416bb2b8038a1640196fbe3e38816f3e67cba72d940000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d013950102000000000000000000000000000000000000000000000000 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/foundry/test/protocols/UniswapV4.t.sol b/foundry/test/protocols/UniswapV4.t.sol index b69efc3..508283f 100644 --- a/foundry/test/protocols/UniswapV4.t.sol +++ b/foundry/test/protocols/UniswapV4.t.sol @@ -24,6 +24,8 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor { bool zeroForOne, RestrictTransferFrom.TransferType transferType, address receiver, + address hook, + bytes memory hookData, UniswapV4Pool[] memory pools ) { @@ -37,10 +39,12 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { UniswapV4ExecutorExposed uniswapV4Exposed; IERC20 USDE = IERC20(USDE_ADDR); IERC20 USDT = IERC20(USDT_ADDR); + IERC20 USDC = IERC20(USDC_ADDR); + address poolManager = 0x000000000004444c5dc75cB358380D2e3dE08A90; function setUp() public { - uint256 forkBlock = 21817316; + uint256 forkBlock = 22689128; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); uniswapV4Exposed = new UniswapV4ExecutorExposed( IPoolManager(poolManager), PERMIT2_ADDRESS @@ -73,6 +77,8 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { zeroForOne, RestrictTransferFrom.TransferType.Transfer, ALICE, + address(0), + bytes(""), pools ); @@ -82,6 +88,8 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { bool zeroForOneDecoded, RestrictTransferFrom.TransferType transferType, address receiver, + address hook, + bytes memory hookData, UniswapV4Executor.UniswapV4Pool[] memory decodedPools ) = uniswapV4Exposed.decodeData(data); @@ -93,6 +101,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { uint8(RestrictTransferFrom.TransferType.Transfer) ); assertEq(receiver, ALICE); + assertEq(hook, address(0)); assertEq(decodedPools.length, 2); assertEq(decodedPools[0].intermediaryToken, USDT_ADDR); assertEq(decodedPools[0].fee, pool1Fee); @@ -123,6 +132,8 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { true, RestrictTransferFrom.TransferType.Transfer, ALICE, + address(0), + bytes(""), pools ); @@ -180,6 +191,8 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { true, RestrictTransferFrom.TransferType.Transfer, ALICE, + address(0), + bytes(""), pools ); @@ -211,6 +224,43 @@ contract UniswapV4ExecutorTest is Constants, TestUtils { ); assertTrue(IERC20(WBTC_ADDR).balanceOf(ALICE) == amountOut); } + + function testSingleSwapEulerHook() public { + // Replicating tx: 0xb372306a81c6e840f4ec55f006da6b0b097f435802a2e6fd216998dd12fb4aca + address hook = address(0x69058613588536167BA0AA94F0CC1Fe420eF28a8); + + uint256 amountIn = 7407000000; + deal(USDC_ADDR, address(uniswapV4Exposed), amountIn); + uint256 usdcBalanceBeforeSwapExecutor = + USDC.balanceOf(address(uniswapV4Exposed)); + + UniswapV4Executor.UniswapV4Pool[] memory pools = + new UniswapV4Executor.UniswapV4Pool[](1); + pools[0] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: WETH_ADDR, + fee: uint24(500), + tickSpacing: int24(1) + }); + + bytes memory data = UniswapV4Utils.encodeExactInput( + USDC_ADDR, + WETH_ADDR, + true, + RestrictTransferFrom.TransferType.Transfer, + ALICE, + hook, + bytes(""), + pools + ); + + uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); + assertEq(amountOut, 2681115183499232721); + assertEq( + USDC.balanceOf(address(uniswapV4Exposed)), + usdcBalanceBeforeSwapExecutor - amountIn + ); + assertTrue(IERC20(WETH_ADDR).balanceOf(ALICE) == amountOut); + } } contract TychoRouterForBalancerV3Test is TychoRouterTestSetup { @@ -237,6 +287,8 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup { true, RestrictTransferFrom.TransferType.TransferFrom, ALICE, + address(0), + bytes(""), pools ); @@ -285,6 +337,8 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup { true, RestrictTransferFrom.TransferType.TransferFrom, ALICE, + address(0), + bytes(""), pools ); diff --git a/foundry/test/protocols/UniswapV4Utils.sol b/foundry/test/protocols/UniswapV4Utils.sol index c280fcb..9051f12 100644 --- a/foundry/test/protocols/UniswapV4Utils.sol +++ b/foundry/test/protocols/UniswapV4Utils.sol @@ -10,13 +10,21 @@ library UniswapV4Utils { bool zeroForOne, RestrictTransferFrom.TransferType transferType, address receiver, + address hook, + bytes memory hookData, UniswapV4Executor.UniswapV4Pool[] memory pools ) public pure returns (bytes memory) { - bytes memory encodedPools; + require(pools.length > 0, "Must have at least one pool"); - for (uint256 i = 0; i < pools.length; i++) { - encodedPools = abi.encodePacked( - encodedPools, + bytes memory firstPool = abi.encodePacked( + pools[0].intermediaryToken, + bytes3(pools[0].fee), + pools[0].tickSpacing + ); + + bytes[] memory encodedExtraPools = new bytes[](pools.length - 1); + for (uint256 i = 1; i < pools.length; i++) { + encodedExtraPools[i - 1] = abi.encodePacked( pools[i].intermediaryToken, bytes3(pools[i].fee), pools[i].tickSpacing @@ -24,7 +32,28 @@ library UniswapV4Utils { } return abi.encodePacked( - tokenIn, tokenOut, zeroForOne, transferType, receiver, encodedPools + tokenIn, + tokenOut, + zeroForOne, + transferType, + receiver, + hook, + firstPool, + pleEncode(encodedExtraPools), + hookData ); } + + function pleEncode(bytes[] memory data) + public + pure + returns (bytes memory encoded) + { + for (uint256 i = 0; i < data.length; i++) { + encoded = bytes.concat( + encoded, + abi.encodePacked(bytes2(uint16(data[i].length)), data[i]) + ); + } + } } 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/constants.rs b/src/encoding/evm/constants.rs index c47424e..6e84a3f 100644 --- a/src/encoding/evm/constants.rs +++ b/src/encoding/evm/constants.rs @@ -13,6 +13,7 @@ pub const PROTOCOL_SPECIFIC_CONFIG: &str = pub static GROUPABLE_PROTOCOLS: LazyLock> = LazyLock::new(|| { let mut set = HashSet::new(); set.insert("uniswap_v4"); + set.insert("uniswap_v4_hooks"); set.insert("vm:balancer_v3"); set.insert("ekubo_v2"); set @@ -29,6 +30,7 @@ pub static IN_TRANSFER_REQUIRED_PROTOCOLS: LazyLock> = Laz set.insert("uniswap_v3"); set.insert("pancakeswap_v3"); set.insert("uniswap_v4"); + set.insert("uniswap_v4_hooks"); set.insert("ekubo_v2"); set.insert("vm:maverick_v2"); set.insert("vm:balancer_v3"); @@ -46,6 +48,7 @@ pub static CALLBACK_CONSTRAINED_PROTOCOLS: LazyLock> = Laz set.insert("uniswap_v3"); set.insert("pancakeswap_v3"); set.insert("uniswap_v4"); + set.insert("uniswap_v4_hooks"); set.insert("ekubo_v2"); set.insert("vm:balancer_v3"); set diff --git a/src/encoding/evm/group_swaps.rs b/src/encoding/evm/group_swaps.rs index 7aca6c1..d78ccff 100644 --- a/src/encoding/evm/group_swaps.rs +++ b/src/encoding/evm/group_swaps.rs @@ -11,7 +11,7 @@ use crate::encoding::{evm::constants::GROUPABLE_PROTOCOLS, models::Swap}; /// * `protocol_system`: String, the protocol system of the swaps /// * `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, PartialEq, Debug)] +#[derive(Clone, Debug)] pub struct SwapGroup { pub token_in: Bytes, pub token_out: Bytes, @@ -20,6 +20,16 @@ pub struct SwapGroup { pub split: f64, } +impl PartialEq for SwapGroup { + fn eq(&self, other: &Self) -> bool { + self.token_in == other.token_in && + self.token_out == other.token_out && + self.protocol_system == other.protocol_system && + self.swaps == other.swaps && + self.split == other.split + } +} + /// Group consecutive swaps which can be encoded into one swap execution for gas optimization. /// /// An example where this applies is the case of USV4, which uses a PoolManager contract @@ -77,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()) @@ -95,43 +105,28 @@ 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".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, - }; - let swap_wbtc_usdc = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v4".to_string(), - ..Default::default() - }, - token_in: wbtc.clone(), - token_out: usdc.clone(), - split: 0f64, - user_data: 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, - }; - let grouped_swaps = group_swaps(&vec![ - swap_weth_wbtc.clone(), - swap_wbtc_usdc.clone(), - swap_usdc_dai.clone(), - ]); + let swap_weth_wbtc = SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() }, + weth.clone(), + wbtc.clone(), + ) + .build(); + + let swap_wbtc_usdc = SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v4".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); assert_eq!( grouped_swaps, @@ -169,54 +164,41 @@ 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".to_string(), - ..Default::default() - }, - token_in: wbtc.clone(), - token_out: weth.clone(), - split: 0f64, - user_data: None, - }; - let swap_weth_usdc = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v4".to_string(), - ..Default::default() - }, - token_in: weth.clone(), - token_out: usdc.clone(), - split: 0.5f64, - user_data: None, - }; - let swap_weth_dai = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v4".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, - }; - let swap_dai_usdc = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v4".to_string(), - ..Default::default() - }, - token_in: dai.clone(), - token_out: usdc.clone(), - split: 0f64, - user_data: None, - }; - let grouped_swaps = group_swaps(&vec![ + let swap_wbtc_weth = SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() }, + wbtc.clone(), + weth.clone(), + ) + .build(); + let swap_weth_usdc = SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() }, + weth.clone(), + usdc.clone(), + ) + .split(0.5f64) + .build(); + let swap_weth_dai = SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v4".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".to_string(), ..Default::default() }, + dai.clone(), + usdc.clone(), + ) + .build(); + let swaps = vec![ swap_wbtc_weth.clone(), swap_weth_usdc.clone(), swap_weth_dai.clone(), swap_dai_usdc.clone(), - ]); + ]; + let grouped_swaps = group_swaps(&swaps); assert_eq!( grouped_swaps, @@ -260,55 +242,46 @@ 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, - }; - let swap_wbtc_usdc = Swap { - component: ProtocolComponent { - protocol_system: "vm:balancer_v3".to_string(), - ..Default::default() - }, - token_in: wbtc.clone(), - token_out: usdc.clone(), - split: 0f64, - user_data: None, - }; - let swap_weth_dai = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v4".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, - }; - let swap_dai_usdc = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v4".to_string(), - ..Default::default() - }, - token_in: dai.clone(), - token_out: usdc.clone(), - split: 0f64, - user_data: None, - }; + weth.clone(), + wbtc.clone(), + ) + .split(0.5f64) + .build(); - let grouped_swaps = group_swaps(&vec![ + let swap_wbtc_usdc = SwapBuilder::new( + ProtocolComponent { + protocol_system: "vm:balancer_v3".to_string(), + ..Default::default() + }, + wbtc.clone(), + usdc.clone(), + ) + .build(); + let swap_weth_dai = SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() }, + weth.clone(), + dai.clone(), + ) + .build(); + let swap_dai_usdc = SwapBuilder::new( + ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() }, + dai.clone(), + usdc.clone(), + ) + .build(); + + let swaps = vec![ swap_weth_wbtc.clone(), swap_wbtc_usdc.clone(), swap_weth_dai.clone(), swap_dai_usdc.clone(), - ]); + ]; + let grouped_swaps = group_swaps(&swaps); assert_eq!( grouped_swaps, 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 88eb463..fa47be8 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -121,16 +121,25 @@ impl StrategyEncoder for SingleSwapStrategyEncoder { transfer_type: transfer, }; - let mut grouped_protocol_data: Vec = vec![]; + let mut grouped_protocol_data: Vec> = vec![]; + let mut initial_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?; - grouped_protocol_data.extend(protocol_data); + if encoding_context.group_token_in == swap.token_in { + initial_protocol_data = protocol_data; + } else { + grouped_protocol_data.push(protocol_data); + } + } + + if !grouped_protocol_data.is_empty() { + initial_protocol_data.extend(ple_encode(grouped_protocol_data)); } let swap_data = self.encode_swap_header( Bytes::from_str(swap_encoder.executor_address()) .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?, - grouped_protocol_data, + initial_protocol_data, ); Ok(EncodedSolution { function_signature: self.function_signature.clone(), @@ -228,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, } } @@ -251,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 @@ -271,17 +281,26 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { transfer_type: transfer, }; - let mut grouped_protocol_data: Vec = vec![]; + let mut grouped_protocol_data: Vec> = vec![]; + let mut initial_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?; - grouped_protocol_data.extend(protocol_data); + if encoding_context.group_token_in == swap.token_in { + initial_protocol_data = protocol_data; + } else { + grouped_protocol_data.push(protocol_data); + } + } + + if !grouped_protocol_data.is_empty() { + initial_protocol_data.extend(ple_encode(grouped_protocol_data)); } let swap_data = self.encode_swap_header( Bytes::from_str(swap_encoder.executor_address()).map_err(|_| { EncodingError::FatalError("Invalid executor address".to_string()) })?, - grouped_protocol_data, + initial_protocol_data, ); swaps.push(swap_data); } @@ -462,10 +481,19 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { transfer_type: transfer, }; - let mut grouped_protocol_data: Vec = vec![]; + let mut grouped_protocol_data: Vec> = vec![]; + let mut initial_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?; - grouped_protocol_data.extend(protocol_data); + if encoding_context.group_token_in == swap.token_in { + initial_protocol_data = protocol_data; + } else { + grouped_protocol_data.push(protocol_data); + } + } + + if !grouped_protocol_data.is_empty() { + initial_protocol_data.extend(ple_encode(grouped_protocol_data)); } let swap_data = self.encode_swap_header( @@ -475,7 +503,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { Bytes::from_str(swap_encoder.executor_address()).map_err(|_| { EncodingError::FatalError("Invalid executor address".to_string()) })?, - grouped_protocol_data, + initial_protocol_data, ); swaps.push(swap_data); } @@ -517,7 +545,6 @@ mod tests { }; use super::*; - use crate::encoding::models::Swap; fn eth_chain() -> Chain { Chain::Ethereum @@ -538,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 @@ -548,17 +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, - }; + weth.clone(), + dai.clone(), + ) + .build(); let swap_encoder_registry = get_swap_encoder_registry(); let encoder = SingleSwapStrategyEncoder::new( eth_chain(), @@ -609,17 +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, - }; + weth.clone(), + dai.clone(), + ) + .build(); let swap_encoder_registry = get_swap_encoder_registry(); let encoder = SingleSwapStrategyEncoder::new( eth_chain(), @@ -669,6 +694,7 @@ mod tests { mod sequential { use super::*; + use crate::encoding::models::SwapBuilder; #[test] fn test_sequential_swap_strategy_encoder_no_permit2() { @@ -680,28 +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, - }; - 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, - }; + wbtc.clone(), + usdc.clone(), + ) + .build(); let swap_encoder_registry = get_swap_encoder_registry(); let encoder = SequentialSwapStrategyEncoder::new( eth_chain(), @@ -759,6 +783,7 @@ mod tests { mod split { use super::*; + use crate::encoding::models::SwapBuilder; #[test] fn test_split_input_cyclic_swap() { @@ -774,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(), @@ -789,15 +814,15 @@ mod tests { }, ..Default::default() }, - token_in: usdc.clone(), - token_out: weth.clone(), - split: 0.6f64, // 60% of input - user_data: 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(), @@ -811,15 +836,14 @@ mod tests { }, ..Default::default() }, - token_in: usdc.clone(), - token_out: weth.clone(), - split: 0f64, - user_data: None, // Remaining 40% - }; + 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(), @@ -833,12 +857,10 @@ mod tests { }, ..Default::default() }, - token_in: weth.clone(), - token_out: usdc.clone(), - split: 0.0f64, - user_data: None, - }; - + weth.clone(), + usdc.clone(), + ) + .build(); let swap_encoder_registry = get_swap_encoder_registry(); let encoder = SplitSwapStrategyEncoder::new( eth_chain(), @@ -927,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: { @@ -941,14 +963,13 @@ mod tests { }, ..Default::default() }, - token_in: usdc.clone(), - token_out: weth.clone(), - split: 0.0f64, - user_data: 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(), @@ -962,16 +983,16 @@ mod tests { }, ..Default::default() }, - token_in: weth.clone(), - token_out: usdc.clone(), - split: 0.6f64, - user_data: 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(); @@ -983,11 +1004,10 @@ mod tests { }, ..Default::default() }, - token_in: weth.clone(), - token_out: usdc.clone(), - split: 0.0f64, - user_data: 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 9a46507..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,17 +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, - }]; + weth.clone(), + dai.clone(), + ) + .build()]; let result = validator.validate_swap_path(&swaps, &weth, &dai, &None, ð, &weth); assert_eq!(result, Ok(())); } @@ -228,28 +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, - }, - 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, - }, + dai.clone(), + usdc.clone(), + ) + .build(), ]; let result = validator.validate_swap_path(&swaps, &weth, &usdc, &None, ð, &weth); assert_eq!(result, Ok(())); @@ -265,29 +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, - }, + 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, - }, + wbtc.clone(), + usdc.clone(), + ) + .build(), ]; let result = validator.validate_swap_path(&disconnected_swaps, &weth, &usdc, &None, ð, &weth); @@ -305,28 +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, - }, - 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, - }, + weth.clone(), + usdc.clone(), + ) + .build(), ]; // Test with USDC as both given token and checked token @@ -342,17 +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, - }]; + weth.clone(), + dai.clone(), + ) + .split(1.0) + .build()]; let result = validator.validate_swap_path(&unreachable_swaps, &weth, &usdc, &None, ð, &weth); assert!(matches!( @@ -381,17 +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, - }]; + weth.clone(), + dai.clone(), + ) + .build()]; let result = validator.validate_split_percentages(&swaps); assert_eq!(result, Ok(())); } @@ -404,39 +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, - }, - 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, - }, - 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, - }, + weth.clone(), + dai.clone(), + ) + .build(), ]; assert!(validator .validate_split_percentages(&valid_swaps) @@ -450,28 +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, - }, - 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, - }, + weth.clone(), + dai.clone(), + ) + .split(0.3) + .build(), ]; assert!(matches!( validator.validate_split_percentages(&invalid_total_swaps), @@ -486,28 +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, - }, - 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, - }, + weth.clone(), + dai.clone(), + ) + .split(0.5) + .build(), ]; assert!(matches!( validator.validate_split_percentages(&invalid_zero_position_swaps), @@ -522,39 +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, - }, - 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, - }, - 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, - }, + weth.clone(), + dai.clone(), + ) + .build(), ]; assert!(matches!( validator.validate_split_percentages(&invalid_overflow_swaps), @@ -569,17 +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, - }]; + weth.clone(), + usdc.clone(), + ) + .build()]; let result = validator.validate_swap_path( &swaps, @@ -599,17 +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, - }]; + 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 4a23ebe..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,17 +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, - }]; + swap_token_in.clone(), + dai(), + ) + .build()]; let swap = SwapGroup { protocol_system: protocol, token_in: swap_token_in, @@ -203,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, ) { @@ -231,21 +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, - }], + 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 0797930..42a84d6 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, }; @@ -71,6 +72,11 @@ impl SwapEncoderBuilder { self.chain, self.config, )?)), + "uniswap_v4_hooks" => Ok(Box::new(UniswapV4SwapEncoder::new( + self.executor_address, + self.chain, + self.config, + )?)), "ekubo_v2" => { Ok(Box::new(EkuboSwapEncoder::new(self.executor_address, self.chain, self.config)?)) } @@ -87,6 +93,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 ae79a26..134a5d6 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}, 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, @@ -176,9 +185,28 @@ impl SwapEncoder for UniswapV4SwapEncoder { EncodingError::FatalError("Failed to pad tick spacing bytes".to_string()) })?; + let hook_address = match get_static_attribute(swap, "hooks") { + Ok(hook) => Address::from_slice(&hook), + Err(_) => Address::ZERO, + }; + let mut hook_data = AlloyBytes::new(); + if encoding_context.group_token_out == swap.token_out { + // Add hook data if it's only the last swap + hook_data = AlloyBytes::from( + swap.user_data + .clone() + .unwrap_or_default() + .to_vec(), + ); + } // Early check if this is not the first swap if encoding_context.group_token_in != swap.token_in { - return Ok((bytes_to_address(&swap.token_out)?, pool_fee_u24, pool_tick_spacing_u24) + return Ok(( + bytes_to_address(&swap.token_out)?, + pool_fee_u24, + pool_tick_spacing_u24, + hook_data, + ) .abi_encode_packed()); } @@ -199,7 +227,9 @@ impl SwapEncoder for UniswapV4SwapEncoder { zero_to_one, (encoding_context.transfer_type as u8).to_be_bytes(), bytes_to_address(&encoding_context.receiver)?, + hook_address, pool_params, + hook_data, ); Ok(args.abi_encode_packed()) @@ -634,6 +664,326 @@ 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 protocol_state = swap + .protocol_state + .as_ref() + .ok_or_else(|| { + EncodingError::FatalError("protocol_state is required for Bebop".to_string()) + })?; + let (partial_fill_offset, original_filled_taker_amount, bebop_calldata) = { + let indicatively_priced_state = protocol_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(), + ) + }; + + 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()) } @@ -651,10 +1001,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 { @@ -664,13 +1018,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, - }; + 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, @@ -710,6 +1058,7 @@ mod tests { mod uniswap_v3 { use super::*; + use crate::encoding::models::SwapBuilder; #[test] fn test_encode_uniswap_v3() { let fee = BigInt::from(500); @@ -724,13 +1073,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, - }; + let swap = SwapBuilder::new(usv3_pool, token_in.clone(), token_out.clone()).build(); let encoding_context = EncodingContext { receiver: Bytes::from("0x0000000000000000000000000000000000000001"), exact_out: false, @@ -773,6 +1116,7 @@ mod tests { mod balancer_v2 { use super::*; + use crate::encoding::models::SwapBuilder; #[test] fn test_encode_balancer_v2() { @@ -785,13 +1129,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, - }; + 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"), @@ -838,7 +1176,7 @@ mod tests { mod uniswap_v4 { use super::*; - use crate::encoding::evm::utils::write_calldata_to_file; + use crate::encoding::evm::utils::{ple_encode, write_calldata_to_file}; #[test] fn test_encode_uniswap_v4_simple_swap() { @@ -858,13 +1196,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, - }; + 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"), @@ -900,6 +1232,8 @@ mod tests { "01", // receiver "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", + // hook address (not set, so zero) + "0000000000000000000000000000000000000000", // pool params: // - intermediary token "dac17f958d2ee523a2206206994597c13d831ec7", @@ -931,13 +1265,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, - }; + let swap = SwapBuilder::new(usv4_pool, token_in.clone(), token_out.clone()).build(); let encoding_context = EncodingContext { receiver: Bytes::from("0x0000000000000000000000000000000000000001"), @@ -1028,21 +1356,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, - }; - - let second_swap = Swap { - component: usdt_wbtc_component, - token_in: usdt_address, - token_out: wbtc_address.clone(), - split: 0f64, - user_data: 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"), @@ -1057,8 +1376,11 @@ mod tests { .encode_swap(&second_swap, &context) .unwrap(); - let combined_hex = - format!("{}{}", encode(&initial_encoded_swap), encode(&second_encoded_swap)); + let combined_hex = format!( + "{}{}", + encode(&initial_encoded_swap), + encode(ple_encode(vec![second_encoded_swap])) + ); assert_eq!( combined_hex, @@ -1073,6 +1395,8 @@ mod tests { "01", // receiver "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", + // hook address (not set, so zero) + "0000000000000000000000000000000000000000", // pool params: // - intermediary token USDT "dac17f958d2ee523a2206206994597c13d831ec7", @@ -1080,6 +1404,9 @@ mod tests { "000064", // - tick spacing "000001", + // Second swap + // ple encoding + "001a", // - intermediary token WBTC "2260fac5e5542a773aa44fbcfedf7c193bc2c599", // - fee @@ -1093,7 +1420,6 @@ mod tests { } mod ekubo { use super::*; - use crate::encoding::evm::utils::write_calldata_to_file; const RECEIVER: &str = "ca4f73fe97d0b987a0d12b39bbd562c779bab6f6"; // Random address @@ -1113,13 +1439,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, - }; + let swap = SwapBuilder::new(component, token_in.clone(), token_out.clone()).build(); let encoding_context = EncodingContext { receiver: RECEIVER.into(), @@ -1172,8 +1492,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)), @@ -1184,14 +1504,13 @@ mod tests { ]), ..Default::default() }, - token_in: group_token_in.clone(), - token_out: intermediary_token.clone(), - split: 0f64, - user_data: 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)), @@ -1200,11 +1519,10 @@ mod tests { ]), ..Default::default() }, - token_in: intermediary_token.clone(), - token_out: group_token_out.clone(), - split: 0f64, - user_data: None, - }; + intermediary_token.clone(), + group_token_out.clone(), + ) + .build(); let first_encoded_swap = encoder .encode_swap(&first_swap, &encoding_context) @@ -1313,18 +1631,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, - }; + Bytes::from(token_in), + Bytes::from(token_out), + ) + .build(); + let encoder = CurveSwapEncoder::new(String::default(), Chain::Ethereum, curve_config()).unwrap(); let (i, j) = encoder @@ -1358,13 +1676,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, - }; + 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"), @@ -1430,13 +1744,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, - }; + 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"), @@ -1503,13 +1811,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, - }; + 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"), @@ -1577,13 +1879,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, - }; + 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"), @@ -1635,13 +1931,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, - }; + 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"), @@ -1681,4 +1971,254 @@ 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_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 a317b85..b6aaa02 100644 --- a/src/encoding/evm/tycho_encoders.rs +++ b/src/encoding/evm/tycho_encoders.rs @@ -14,6 +14,7 @@ use crate::encoding::{ SequentialSwapStrategyEncoder, SingleSwapStrategyEncoder, SplitSwapStrategyEncoder, }, swap_encoder::swap_encoder_registry::SwapEncoderRegistry, + utils::ple_encode, }, models::{ EncodedSolution, EncodingContext, NativeAction, Solution, Transaction, TransferType, @@ -102,7 +103,9 @@ impl TychoRouterEncoder { solution .swaps .iter() - .all(|swap| swap.split == 0.0)) + .all(|swap| swap.split == 0.0) && + !(solution.given_token == solution.checked_token && solution.swaps.len() > 2)) + // This is a special case for cyclical swaps { self.single_swap_strategy .encode_strategy(solution)? @@ -313,32 +316,44 @@ impl TychoExecutorEncoder { )) })?; - let mut grouped_protocol_data: Vec = vec![]; + let transfer = if IN_TRANSFER_REQUIRED_PROTOCOLS.contains( + &grouped_swap.swaps[0] + .component + .protocol_system + .as_str(), + ) { + TransferType::Transfer + } else { + TransferType::None + }; + let encoding_context = EncodingContext { + receiver: solution.receiver.clone(), + exact_out: solution.exact_out, + router_address: None, + group_token_in: grouped_swap.token_in.clone(), + group_token_out: grouped_swap.token_out.clone(), + transfer_type: transfer, + }; + let mut grouped_protocol_data: Vec> = vec![]; + let mut initial_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { - let transfer = if IN_TRANSFER_REQUIRED_PROTOCOLS - .contains(&swap.component.protocol_system.as_str()) - { - TransferType::Transfer - } else { - TransferType::None - }; - let encoding_context = EncodingContext { - receiver: solution.receiver.clone(), - exact_out: solution.exact_out, - router_address: None, - group_token_in: grouped_swap.token_in.clone(), - group_token_out: grouped_swap.token_out.clone(), - transfer_type: transfer, - }; let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?; - grouped_protocol_data.extend(protocol_data); + if encoding_context.group_token_in == swap.token_in { + initial_protocol_data = protocol_data; + } else { + grouped_protocol_data.push(protocol_data); + } + } + + if !grouped_protocol_data.is_empty() { + initial_protocol_data.extend(ple_encode(grouped_protocol_data)); } let executor_address = Bytes::from_str(swap_encoder.executor_address()) .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?; Ok(EncodedSolution { - swaps: grouped_protocol_data, + swaps: initial_protocol_data, interacting_with: executor_address, permit: None, function_signature: "".to_string(), @@ -393,7 +408,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() @@ -428,19 +443,18 @@ mod tests { 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".to_string(), static_attributes: static_attributes_usdc_eth, ..Default::default() }, - token_in: usdc().clone(), - token_out: eth().clone(), - split: 0f64, - user_data: None, - } + usdc().clone(), + eth().clone(), + ) + .build() } fn swap_eth_pepe_univ4() -> Swap { @@ -449,19 +463,18 @@ mod tests { 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".to_string(), static_attributes: static_attributes_eth_pepe, ..Default::default() }, - token_in: eth().clone(), - token_out: pepe().clone(), - split: 0f64, - user_data: None, - } + eth().clone(), + pepe().clone(), + ) + .build() } fn router_address() -> Bytes { @@ -499,17 +512,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, - }; + weth().clone(), + dai().clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -564,29 +576,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, - }; + 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, - }; + dai().clone(), + usdc().clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -656,17 +666,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, - }; + weth().clone(), + dai().clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -685,17 +694,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, - }; + weth().clone(), + dai().clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -719,17 +727,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, - }; + eth().clone(), + dai().clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -773,17 +780,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, - }; + dai().clone(), + weth().clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -801,17 +807,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, - }; + dai().clone(), + weth().clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -836,17 +841,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, - }; + dai().clone(), + eth().clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -876,39 +880,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, - }, - 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, - }, - 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, - }, + weth().clone(), + dai().clone(), + ) + .build(), ]; let solution = Solution { @@ -931,50 +932,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, - }, - 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, - }, - 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, - }, - 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, - }, + dai().clone(), + wbtc().clone(), + ) + .build(), ]; let solution = Solution { @@ -1004,39 +1001,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, - }, - 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, - }, - 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, - }, + dai(), + weth(), + ) + .build(), ]; let solution = Solution { @@ -1059,28 +1054,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, - }, - 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, - }, + dai(), + weth(), + ) + .build(), ]; let solution = Solution { @@ -1114,7 +1107,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() { @@ -1124,17 +1117,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, - }; + token_in.clone(), + token_out.clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -1185,16 +1177,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, - }; + token_in.clone(), + token_out.clone(), + ) + .build(); let solution = Solution { exact_out: false, @@ -1256,12 +1248,16 @@ mod tests { "01", // receiver "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", + // hook address (not set, so zero) + "0000000000000000000000000000000000000000", // first pool intermediary token (ETH) "0000000000000000000000000000000000000000", // fee "000bb8", // tick spacing "00003c", + // ple encoding + "001a", // second pool intermediary token (PEPE) "6982508145454ce325ddbe47a25d4ec3d2311933", // fee 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 5f1df64..48b224c 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -1,7 +1,11 @@ +use std::sync::Arc; + use clap::ValueEnum; use num_bigint::BigUint; use serde::{Deserialize, Serialize}; -use tycho_common::{models::protocol::ProtocolComponent, Bytes}; +use tycho_common::{ + models::protocol::ProtocolComponent, simulation::protocol_sim::ProtocolSim, Bytes, +}; use crate::encoding::serde_primitives::biguint_string; @@ -71,7 +75,7 @@ pub enum NativeAction { } /// Represents a swap operation to be performed on a pool. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Swap { /// Protocol component from tycho indexer pub component: ProtocolComponent, @@ -84,6 +88,12 @@ pub struct Swap { pub split: f64, /// Optional user data to be passed to encoding. pub user_data: Option, + /// Optional protocol state used to perform the swap. + #[serde(skip)] + 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 Swap { @@ -93,8 +103,90 @@ impl Swap { token_out: Bytes, split: f64, user_data: Option, + protocol_state: Option>, + estimated_amount_in: Option, ) -> Self { - Self { component: component.into(), token_in, token_out, split, user_data } + Self { + component: component.into(), + token_in, + token_out, + split, + user_data, + protocol_state, + estimated_amount_in, + } + } +} + +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.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, + } } } @@ -238,8 +330,15 @@ mod tests { protocol_system: "uniswap_v2".to_string(), }; let user_data = Some(Bytes::from("0x1234")); - let swap = - Swap::new(component, Bytes::from("0x12"), Bytes::from("34"), 0.5, user_data.clone()); + let swap = Swap::new( + component, + Bytes::from("0x12"), + Bytes::from("34"), + 0.5, + user_data.clone(), + None, + None, + ); assert_eq!(swap.token_in, Bytes::from("0x12")); assert_eq!(swap.token_out, Bytes::from("0x34")); assert_eq!(swap.component.protocol_system, "uniswap_v2"); 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 7e7167f..477ed27 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; @@ -48,6 +52,8 @@ fn test_uniswap_v3_uniswap_v2() { token_out: wbtc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let swap_wbtc_usdc = Swap { component: ProtocolComponent { @@ -59,6 +65,8 @@ fn test_uniswap_v3_uniswap_v2() { token_out: usdc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -127,6 +135,8 @@ fn test_uniswap_v3_uniswap_v3() { token_out: wbtc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let swap_wbtc_usdc = Swap { component: ProtocolComponent { @@ -146,6 +156,8 @@ fn test_uniswap_v3_uniswap_v3() { token_out: usdc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -213,6 +225,8 @@ fn test_uniswap_v3_curve() { token_out: wbtc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let swap_wbtc_usdt = Swap { @@ -242,6 +256,8 @@ fn test_uniswap_v3_curve() { token_out: usdt.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -301,6 +317,8 @@ fn test_balancer_v2_uniswap_v2() { token_out: wbtc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let swap_wbtc_usdc = Swap { @@ -313,6 +331,8 @@ fn test_balancer_v2_uniswap_v2() { token_out: usdc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -375,6 +395,8 @@ fn test_multi_protocol() { token_out: weth.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let balancer_swap_weth_wbtc = Swap { @@ -387,6 +409,8 @@ fn test_multi_protocol() { token_out: wbtc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let curve_swap_wbtc_usdt = Swap { @@ -416,6 +440,8 @@ fn test_multi_protocol() { token_out: usdt.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; // Ekubo @@ -438,6 +464,8 @@ fn test_multi_protocol() { token_out: usdc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; // USV4 @@ -461,6 +489,8 @@ fn test_multi_protocol() { token_out: eth.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -533,6 +563,8 @@ fn test_uniswap_v3_balancer_v3() { token_out: wbtc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let swap_wbtc_qnt = Swap { component: ProtocolComponent { @@ -544,6 +576,8 @@ fn test_uniswap_v3_balancer_v3() { token_out: qnt.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -578,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 5e55513..4edbe9f 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, 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 @@ -40,6 +44,8 @@ fn test_single_encoding_strategy_ekubo() { token_out: token_out.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -92,6 +98,8 @@ fn test_single_encoding_strategy_maverick() { token_out: token_out.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -156,6 +164,8 @@ fn test_single_encoding_strategy_usv4_eth_in() { token_out: pepe.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -222,6 +232,8 @@ fn test_single_encoding_strategy_usv4_eth_out() { token_out: eth.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -296,6 +308,8 @@ fn test_single_encoding_strategy_usv4_grouped_swap() { token_out: eth.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let swap_eth_pepe = Swap { @@ -309,6 +323,8 @@ fn test_single_encoding_strategy_usv4_grouped_swap() { token_out: pepe.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -357,7 +373,7 @@ fn test_single_encoding_strategy_usv4_grouped_swap() { let expected_swaps = String::from(concat!( // length of ple encoded swaps without padding - "0000000000000000000000000000000000000000000000000000000000000086", + "000000000000000000000000000000000000000000000000000000000000009c", // Swap data header "f62849f9a0b5bf2913b396098f7c7019b51a820a", // executor address // Protocol data @@ -366,15 +382,18 @@ fn test_single_encoding_strategy_usv4_grouped_swap() { "00", // zero2one "00", // transfer type TransferFrom "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + "0000000000000000000000000000000000000000", // hook address // First pool params "0000000000000000000000000000000000000000", // intermediary token (ETH) "000bb8", // fee "00003c", // tick spacing + // ple encoding + "001a", // Second pool params "6982508145454ce325ddbe47a25d4ec3d2311933", // intermediary token (PEPE) "0061a8", // fee "0001f4", // tick spacing - "0000000000000000000000000000000000000000000000000000" // padding + "00000000" // padding )); let hex_calldata = encode(&calldata); @@ -417,6 +436,8 @@ fn test_single_encoding_strategy_curve() { token_out: token_out.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -484,6 +505,8 @@ fn test_single_encoding_strategy_curve_st_eth() { token_out: token_out.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -537,6 +560,8 @@ fn test_single_encoding_strategy_balancer_v3() { token_out: token_out.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -572,3 +597,256 @@ 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 = 12u64; + let bebop_calldata = Bytes::from_str("0x4dcebcba00000000000000000000000000000000000000000000000000000000689b548f0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000067336cec42645f55059eff241cb02ea5cc52ff86000000000000000000000000000000000000000000000000279ead5d9685f25b000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be3000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000a8aea46aa4ec5c0f5000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000000005230bcb979c81cebf94a3b5c08bcfa300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000414ce40058ff07f11d9224c2c8d1e58369e4a90173856202d8d2a17da48058ad683dedb742eda0d4c0cf04cf1c09138898dd7fd06f97268ea7f74ef9b42d29bf4c1b00000000000000000000000000000000000000000000000000000000000000").unwrap(); + let bebop_state = MockRFQState { + quote_amount_out: amount_out.clone(), + 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"), + static_attributes: HashMap::new(), // No static attributes needed + ..Default::default() + }; + + let swap = SwapBuilder::new(bebop_component, token_in.clone(), token_out.clone()) + .estimated_amount_in(BigUint::from_str("200000000").unwrap()) + .protocol_state(Arc::new(bebop_state)) + .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 = 2u64; + + let bebop_calldata = Bytes::from_str("0xa2f7489300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000689b78880000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c0000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000005a060a5c2aaaaa2fe2cda34423cac76a84c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000002901f2d62bb356ca0000000000000000000000000000000000000000000000002901f2d62bb356cb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000044f83c726000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000589400da00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000003aa5f96046644f6e37a000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000004b51a26526ddbeec60000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000417ab4332f2b091d87d56d04eee35dd49452782c782de71608c0425c5ae41f1d7e147173851c870d76720ce07d45cd8622352716b1c7965819ee2bf8c573c499ae1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000410c8da2637aa929e11caff9afdfc4c489320c6dba77cc934d88ba8956e365fd1d48983087c6e474bbb828181cdfdd17317c4c9c3ee4bc98e3769d0c05cc7a285e1c00000000000000000000000000000000000000000000000000000000000000").unwrap(); + let bebop_state = MockRFQState { + quote_amount_out: amount_out.clone(), + 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"), + static_attributes: HashMap::new(), + ..Default::default() + }; + + let swap = SwapBuilder::new(bebop_component, token_in.clone(), token_out.clone()) + .estimated_amount_in(BigUint::from_str("20000000000").unwrap()) + .protocol_state(Arc::new(bebop_state)) + .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 b296d47..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 @@ -37,6 +37,8 @@ fn test_sequential_swap_strategy_encoder() { token_out: wbtc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let swap_wbtc_usdc = Swap { component: ProtocolComponent { @@ -48,6 +50,8 @@ fn test_sequential_swap_strategy_encoder() { token_out: usdc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -103,6 +107,8 @@ fn test_sequential_swap_strategy_encoder_no_permit2() { token_out: wbtc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let swap_wbtc_usdc = Swap { component: ProtocolComponent { @@ -114,6 +120,8 @@ fn test_sequential_swap_strategy_encoder_no_permit2() { token_out: usdc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -215,6 +223,8 @@ fn test_sequential_strategy_cyclic_swap() { token_out: weth.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; // WETH -> USDC (Pool 2) @@ -237,6 +247,8 @@ fn test_sequential_strategy_cyclic_swap() { token_out: usdc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -310,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 d252ae6..935c794 100644 --- a/tests/single_strategy_integration_tests.rs +++ b/tests/single_strategy_integration_tests.rs @@ -33,6 +33,8 @@ fn test_single_swap_strategy_encoder() { token_out: dai.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -119,6 +121,8 @@ fn test_single_swap_strategy_encoder_no_permit2() { token_out: dai.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); @@ -201,6 +205,8 @@ fn test_single_swap_strategy_encoder_no_transfer_in() { token_out: dai.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::None); @@ -284,6 +290,8 @@ fn test_single_swap_strategy_encoder_wrap() { token_out: dai.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -336,6 +344,8 @@ fn test_single_swap_strategy_encoder_unwrap() { token_out: weth(), 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 13b9422..e980a20 100644 --- a/tests/split_strategy_integration_tests.rs +++ b/tests/split_strategy_integration_tests.rs @@ -42,6 +42,8 @@ fn test_split_swap_strategy_encoder() { token_out: dai.clone(), split: 0.5f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let swap_weth_wbtc = Swap { component: ProtocolComponent { @@ -56,6 +58,8 @@ fn test_split_swap_strategy_encoder() { // It should still be very close to 50% split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let swap_dai_usdc = Swap { component: ProtocolComponent { @@ -67,6 +71,8 @@ fn test_split_swap_strategy_encoder() { token_out: usdc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let swap_wbtc_usdc = Swap { component: ProtocolComponent { @@ -78,6 +84,8 @@ fn test_split_swap_strategy_encoder() { token_out: usdc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -144,6 +152,8 @@ fn test_split_input_cyclic_swap() { token_out: weth.clone(), split: 0.6f64, // 60% of input user_data: None, + protocol_state: None, + estimated_amount_in: None, }; // USDC -> WETH (Pool 2) - 40% of input (remaining) @@ -166,6 +176,8 @@ fn test_split_input_cyclic_swap() { token_out: weth.clone(), split: 0f64, user_data: None, // Remaining 40% + protocol_state: None, + estimated_amount_in: None, }; // WETH -> USDC (Pool 2) @@ -188,6 +200,8 @@ fn test_split_input_cyclic_swap() { token_out: usdc.clone(), split: 0.0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); @@ -308,6 +322,8 @@ fn test_split_output_cyclic_swap() { token_out: weth.clone(), split: 0.0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let swap_weth_usdc_v3_pool1 = Swap { @@ -327,6 +343,8 @@ fn test_split_output_cyclic_swap() { token_out: usdc.clone(), split: 0.6f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let swap_weth_usdc_v3_pool2 = Swap { @@ -348,6 +366,8 @@ fn test_split_output_cyclic_swap() { token_out: usdc.clone(), 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 8ea134f..03752af 100644 --- a/tests/uniswap_x_integration_tests.rs +++ b/tests/uniswap_x_integration_tests.rs @@ -51,6 +51,8 @@ fn test_sequential_swap_usx() { token_out: usdc.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let swap_usdc_usdt = Swap { component: ProtocolComponent { @@ -68,6 +70,8 @@ fn test_sequential_swap_usx() { token_out: usdt.clone(), split: 0f64, user_data: None, + protocol_state: None, + estimated_amount_in: None, }; let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);