Merge pull request #1 from jincubator/cyclic-route-0.123.0

Cyclic route 0.123.0
This commit is contained in:
John
2025-09-11 08:24:35 -07:00
committed by GitHub
50 changed files with 3798 additions and 831 deletions

View File

@@ -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<SimulationError> 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) ## [0.111.0](https://github.com/propeller-heads/tycho-execution/compare/0.110.0...0.111.0) (2025-07-15)

8
Cargo.lock generated
View File

@@ -4634,9 +4634,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "tycho-common" name = "tycho-common"
version = "0.76.0" version = "0.82.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e72ed04dc9d41942c886fc3d58af5b0e23a3e4783ac7294ed7cea61e022a5c4d" checksum = "096c87ebe011785fcd7ed59ec501ac12b465a64fbd2914b8c0c57125c253682b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -4659,9 +4659,10 @@ dependencies = [
[[package]] [[package]]
name = "tycho-execution" name = "tycho-execution"
version = "0.111.0" version = "0.123.0"
dependencies = [ dependencies = [
"alloy", "alloy",
"async-trait",
"chrono", "chrono",
"clap", "clap",
"dotenv", "dotenv",
@@ -4676,6 +4677,7 @@ dependencies = [
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio", "tokio",
"tycho-common", "tycho-common",
"tycho-execution",
] ]
[[package]] [[package]]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "tycho-execution" name = "tycho-execution"
version = "0.111.0" version = "0.123.0"
edition = "2021" edition = "2021"
description = "Provides tools for encoding and executing swaps against Tycho router and protocol executors." description = "Provides tools for encoding and executing swaps against Tycho router and protocol executors."
repository = "https://github.com/propeller-heads/tycho-execution" repository = "https://github.com/propeller-heads/tycho-execution"
@@ -11,12 +11,12 @@ license = "MIT"
categories = ["finance", "cryptography::cryptocurrencies"] categories = ["finance", "cryptography::cryptocurrencies"]
readme = "README.md" readme = "README.md"
exclude = [ exclude = [
"foundry/*", "foundry/*",
"foundry", "foundry",
"tests/*", "tests/*",
"tests/common", "tests/common",
".github/*", ".github/*",
".gitmodules", ".gitmodules",
] ]
[[bin]] [[bin]]
@@ -37,17 +37,19 @@ tokio = { version = "1.38.0", features = ["full"] }
chrono = "0.4.39" chrono = "0.4.39"
clap = { version = "4.5.3", features = ["derive"] } clap = { version = "4.5.3", features = ["derive"] }
once_cell = "1.20.2" 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 } 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] [dev-dependencies]
rstest = "0.24.0" rstest = "0.24.0"
tycho-execution = { path = "../tycho-execution", features = ["test-utils"] }
[features] [features]
default = ["evm"] default = ["evm"]
evm = ["alloy"] evm = ["alloy"]
fork-tests = [] fork-tests = []
test-utils = ["async-trait"]
[profile.bench] [profile.bench]
debug = true debug = true

View File

@@ -5,21 +5,27 @@
"pancakeswap_v2": "0xC9db3FEB380E4fd9af239e2595ECdEcE3b5c34A4", "pancakeswap_v2": "0xC9db3FEB380E4fd9af239e2595ECdEcE3b5c34A4",
"uniswap_v3": "0xbab7124C9662B15C6b9AF0b1f329907dD55a24FC", "uniswap_v3": "0xbab7124C9662B15C6b9AF0b1f329907dD55a24FC",
"pancakeswap_v3": "0x9D32e9F569B22Ae8d8C6f788037C1CD53632A059", "pancakeswap_v3": "0x9D32e9F569B22Ae8d8C6f788037C1CD53632A059",
"uniswap_v4": "0xD11496EAb53A9521f0bC1e5c1098Ecb467103Ad9", "uniswap_v4": "0x2C2EaB81Cf983602153E67b1890164BC4CABC6ed",
"uniswap_v4_hooks": "0x2C2EaB81Cf983602153E67b1890164BC4CABC6ed",
"vm:balancer_v2": "0xB5b8dc3F0a1Be99685a0DEd015Af93bFBB55C411", "vm:balancer_v2": "0xB5b8dc3F0a1Be99685a0DEd015Af93bFBB55C411",
"ekubo_v2": "0x263DD7AD20983b5E0392bf1F09C4493500EDb333", "ekubo_v2": "0x263DD7AD20983b5E0392bf1F09C4493500EDb333",
"vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165", "vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165",
"vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B", "vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B",
"vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec" "vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec",
"rfq:bebop": "0xFE42BFb115eD9671011cA52BDD23A52A2e077a7c",
"rfq:hashflow": "0x19e49Db786c87F4e46B10aFb21c0C06d34270f98"
}, },
"base": { "base": {
"uniswap_v2": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700", "uniswap_v2": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700",
"uniswap_v3": "0x647bffbf8bd72bf6341ecba8b0279e090313a40d", "uniswap_v3": "0x647bffbf8bd72bf6341ecba8b0279e090313a40d",
"uniswap_v4": "0xb5628b97f491f85766673ac4d5d47bb1af7fcc4a" "uniswap_v4": "0x7Dfa502736C7bd84DA1402F7524214215BC9534d",
"uniswap_v4_hooks": "0x7Dfa502736C7bd84DA1402F7524214215BC9534d",
"rfq:bebop": "0x489A3f531dA3873D6585BF3f8E0dEE48CAC6F7BC"
}, },
"unichain": { "unichain": {
"uniswap_v2": "0x00C1b81e3C8f6347E69e2DDb90454798A6Be975E", "uniswap_v2": "0x00C1b81e3C8f6347E69e2DDb90454798A6Be975E",
"uniswap_v3": "0xD26A838A41af3d4815DfD745a080B2062c4124d1", "uniswap_v3": "0xD26A838A41af3d4815DfD745a080B2062c4124d1",
"uniswap_v4": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700" "uniswap_v4": "0x647bfFbF8Bd72bF6341ECBa8B0279e090313A40D",
"uniswap_v4_hooks": "0x647bfFbF8Bd72bF6341ECBa8B0279e090313A40D"
} }
} }

View File

@@ -5,8 +5,20 @@
}, },
"vm:curve": { "vm:curve": {
"native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" "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": {} "unichain": {}
} }

View File

@@ -6,10 +6,13 @@
"uniswap_v3": "0x2e234DAe75C793f67A35089C9d99245E1C58470b", "uniswap_v3": "0x2e234DAe75C793f67A35089C9d99245E1C58470b",
"pancakeswap_v3": "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9", "pancakeswap_v3": "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9",
"uniswap_v4": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a", "uniswap_v4": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a",
"uniswap_v4_hooks": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a",
"vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1", "vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1",
"ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598", "ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598",
"vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211", "vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211",
"vm:maverick_v2": "0xA4AD4f68d0b91CFD19687c881e50f3A00242828c", "vm:maverick_v2": "0xA4AD4f68d0b91CFD19687c881e50f3A00242828c",
"vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb" "vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb",
"rfq:bebop": "0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF",
"rfq:hashflow": "0x15cF58144EF33af1e14b5208015d11F9143E27b9"
} }
} }

Binary file not shown.

View File

@@ -7,7 +7,7 @@ use tycho_common::{
}; };
use tycho_execution::encoding::{ use tycho_execution::encoding::{
evm::encoder_builders::TychoRouterEncoderBuilder, evm::encoder_builders::TychoRouterEncoderBuilder,
models::{Solution, Swap, UserTransferType}, models::{Solution, Swap, SwapBuilder, UserTransferType},
}; };
fn main() { fn main() {
@@ -43,6 +43,8 @@ fn main() {
// the amount or the total remaining balance. // the amount or the total remaining balance.
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
// Then we create a solution object with the previous swap // Then we create a solution object with the previous swap
@@ -86,52 +88,51 @@ fn main() {
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f")
.expect("Failed to create DAI address"); .expect("Failed to create DAI address");
let swap_weth_dai = Swap { let swap_weth_dai = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.5f64, )
user_data: None, .split(0.5)
}; .build();
let swap_weth_wbtc = Swap {
component: ProtocolComponent { // 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(), id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: wbtc.clone(), 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% .build();
split: 0f64,
user_data: None, let swap_dai_usdc = SwapBuilder::new(
}; ProtocolComponent {
let swap_dai_usdc = Swap {
component: ProtocolComponent {
id: "0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5".to_string(), id: "0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai.clone(), dai.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0f64, )
user_data: None, .build();
}; let swap_wbtc_usdc = SwapBuilder::new(
let swap_wbtc_usdc = Swap { ProtocolComponent {
component: ProtocolComponent {
id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(), id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: wbtc.clone(), wbtc.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0f64, )
user_data: None, .build();
};
let mut complex_solution = solution.clone(); let mut complex_solution = solution.clone();
complex_solution.swaps = vec![swap_weth_dai, swap_weth_wbtc, swap_dai_usdc, swap_wbtc_usdc]; complex_solution.swaps = vec![swap_weth_dai, swap_weth_wbtc, swap_dai_usdc, swap_wbtc_usdc];

View File

@@ -82,6 +82,8 @@ fn main() {
token_out: usdc.clone(), token_out: usdc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let swap_usdc_usdt = Swap { let swap_usdc_usdt = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -99,6 +101,8 @@ fn main() {
token_out: usdt.clone(), token_out: usdt.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
// Then we create a solution object with the previous swap // Then we create a solution object with the previous swap
@@ -165,7 +169,6 @@ fn main() {
println!(" ====== Simple swap DAI -> USDT ======"); println!(" ====== Simple swap DAI -> USDT ======");
println!( println!(
"The following callback data should be sent to the filler contract, along with the \ "The following callback data should be sent to the filler contract, along with the \
encoded order and signature: {:?}", encoded order and signature: {hex_calldata:?}"
hex_calldata
); );
} }

View File

@@ -7,7 +7,7 @@ evm_version = 'cancun'
optimizer = true optimizer = true
optimizer_runs = 200 optimizer_runs = 200
via_ir = true via_ir = true
fs_permissions = [{ access = "read", path = "./test/assets" }] fs_permissions = [{ access = "read", path = "./test/assets" }, { access = "write", path = "./test" }]
[profile.production] [profile.production]
src = 'src' src = 'src'

View File

@@ -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. 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. 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 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. 2. If it's not set, it will submit the transaction directly to the chain.
## Deploy Uniswap X filler ## Deploy Uniswap X filler

View File

@@ -81,6 +81,16 @@ const executors_to_deploy = {
}, },
// Args: Permit2 // Args: Permit2
{exchange: "BalancerV3Executor", args: ["0x000000000022D473030F116dDEE9F6B43aC78BA3"]}, {exchange: "BalancerV3Executor", args: ["0x000000000022D473030F116dDEE9F6B43aC78BA3"]},
// Args: Bebop Settlement contract, Permit2
{
exchange: "BebopExecutor",
args: ["0xbbbbbBB520d69a9775E85b458C58c648259FAD5F", "0x000000000022D473030F116dDEE9F6B43aC78BA3"]
},
// Args: Hashflow router, Permit2
{
exchange: "HashflowExecutor",
args: ["0x55084eE0fEf03f14a305cd24286359A35D735151", "0x000000000022D473030F116dDEE9F6B43aC78BA3"]
},
], ],
"base": [ "base": [
// Args: Factory, Pool Init Code Hash, Permit2, Fee BPS // Args: Factory, Pool Init Code Hash, Permit2, Fee BPS
@@ -116,6 +126,11 @@ const executors_to_deploy = {
"0x000000000022D473030F116dDEE9F6B43aC78BA3" "0x000000000022D473030F116dDEE9F6B43aC78BA3"
] ]
}, },
// Args: Bebop Settlement contract, Permit2
{
exchange: "BebopExecutor",
args: ["0xbbbbbBB520d69a9775E85b458C58c648259FAD5F", "0x000000000022D473030F116dDEE9F6B43aC78BA3"]
},
], ],
"unichain": [ "unichain": [
// Args: Factory, Pool Init Code Hash, Permit2, Fee BPS // Args: Factory, Pool Init Code Hash, Permit2, Fee BPS

View File

@@ -4,7 +4,7 @@ const {EthersAdapter} = require('@safe-global/protocol-kit');
const {default: SafeApiKit} = require("@safe-global/api-kit"); const {default: SafeApiKit} = require("@safe-global/api-kit");
const txServiceUrls = { const txServiceUrls = {
mainnet: "https://safe-transaction-mainnet.safe.global", ethereum: "https://safe-transaction-mainnet.safe.global",
base: "https://safe-transaction-base.safe.global", base: "https://safe-transaction-base.safe.global",
unichain: "https://safe-transaction-unichain.safe.global", unichain: "https://safe-transaction-unichain.safe.global",
}; };

View File

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

View File

@@ -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);
}
}

View File

@@ -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 {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
import {IUnlockCallback} from import {IUnlockCallback} from
"@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; "@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 import {TransientStateLibrary} from
"@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
import "../RestrictTransferFrom.sol"; import "../RestrictTransferFrom.sol";
import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/utils/Address.sol";
import "../../lib/bytes/LibPrefixLengthEncodedByteArray.sol";
error UniswapV4Executor__InvalidDataLength(); error UniswapV4Executor__InvalidDataLength();
error UniswapV4Executor__NotPoolManager(); error UniswapV4Executor__NotPoolManager();
@@ -43,8 +45,9 @@ contract UniswapV4Executor is
{ {
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
using CurrencyLibrary for Currency; using CurrencyLibrary for Currency;
using SafeCast for *; using V4SafeCast for *;
using TransientStateLibrary for IPoolManager; using TransientStateLibrary for IPoolManager;
using LibPrefixLengthEncodedByteArray for bytes;
IPoolManager public immutable poolManager; IPoolManager public immutable poolManager;
address private immutable _self; address private immutable _self;
@@ -86,6 +89,8 @@ contract UniswapV4Executor is
bool zeroForOne, bool zeroForOne,
TransferType transferType, TransferType transferType,
address receiver, address receiver,
address hook,
bytes memory hookData,
UniswapV4Executor.UniswapV4Pool[] memory pools UniswapV4Executor.UniswapV4Pool[] memory pools
) = _decodeData(data); ) = _decodeData(data);
bytes memory swapData; bytes memory swapData;
@@ -95,7 +100,7 @@ contract UniswapV4Executor is
currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn), currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn),
fee: pools[0].fee, fee: pools[0].fee,
tickSpacing: pools[0].tickSpacing, tickSpacing: pools[0].tickSpacing,
hooks: IHooks(address(0)) hooks: IHooks(hook)
}); });
swapData = abi.encodeWithSelector( swapData = abi.encodeWithSelector(
this.swapExactInputSingle.selector, this.swapExactInputSingle.selector,
@@ -104,7 +109,7 @@ contract UniswapV4Executor is
amountIn, amountIn,
transferType, transferType,
receiver, receiver,
bytes("") hookData
); );
} else { } else {
PathKey[] memory path = new PathKey[](pools.length); PathKey[] memory path = new PathKey[](pools.length);
@@ -113,8 +118,8 @@ contract UniswapV4Executor is
intermediateCurrency: Currency.wrap(pools[i].intermediaryToken), intermediateCurrency: Currency.wrap(pools[i].intermediaryToken),
fee: pools[i].fee, fee: pools[i].fee,
tickSpacing: pools[i].tickSpacing, tickSpacing: pools[i].tickSpacing,
hooks: IHooks(address(0)), hooks: IHooks(hook),
hookData: bytes("") hookData: hookData
}); });
} }
@@ -144,10 +149,12 @@ contract UniswapV4Executor is
bool zeroForOne, bool zeroForOne,
TransferType transferType, TransferType transferType,
address receiver, address receiver,
address hook,
bytes memory hookData,
UniswapV4Pool[] memory pools UniswapV4Pool[] memory pools
) )
{ {
if (data.length < 88) { if (data.length < 108) {
revert UniswapV4Executor__InvalidDataLength(); revert UniswapV4Executor__InvalidDataLength();
} }
@@ -156,25 +163,42 @@ contract UniswapV4Executor is
zeroForOne = data[40] != 0; zeroForOne = data[40] != 0;
transferType = TransferType(uint8(data[41])); transferType = TransferType(uint8(data[41]));
receiver = address(bytes20(data[42:62])); receiver = address(bytes20(data[42:62]));
hook = address(bytes20(data[62:82]));
uint256 poolsLength = (data.length - 62) / 26; // 26 bytes per pool object bytes calldata remaining = data[82:];
pools = new UniswapV4Pool[](poolsLength); address firstToken = address(bytes20(remaining[0:20]));
bytes memory poolsData = data[62:]; uint24 firstFee = uint24(bytes3(remaining[20:23]));
uint256 offset = 0; int24 firstTickSpacing = int24(uint24(bytes3(remaining[23:26])));
for (uint256 i = 0; i < poolsLength; i++) { 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; address intermediaryToken;
uint24 fee; uint24 fee;
int24 tickSpacing; int24 tickSpacing;
// slither-disable-next-line assembly // slither-disable-next-line assembly
assembly { assembly {
intermediaryToken := mload(add(poolsData, add(offset, 20))) intermediaryToken := mload(add(poolsData, add(0, 20)))
fee := shr(232, mload(add(poolsData, add(offset, 52)))) fee := shr(232, mload(add(poolsData, add(0, 52))))
tickSpacing := shr(232, mload(add(poolsData, add(offset, 55)))) tickSpacing := shr(232, mload(add(poolsData, add(0, 55))))
} }
pools[i] = UniswapV4Pool(intermediaryToken, fee, tickSpacing); pools[i + 1] = UniswapV4Pool(intermediaryToken, fee, tickSpacing);
offset += 26; plePoolsTotalLength += 2 + encodedPoolsLength; // 2 bytes prefix + data
} }
hookData = remaining[26 + plePoolsTotalLength:];
} }
/** /**

View File

@@ -60,10 +60,9 @@ contract UniswapXFiller is AccessControl, IReactorCallback {
ResolvedOrder[] calldata resolvedOrders, ResolvedOrder[] calldata resolvedOrders,
bytes calldata callbackData bytes calldata callbackData
) external onlyRole(REACTOR_ROLE) { ) external onlyRole(REACTOR_ROLE) {
require( if (resolvedOrders.length != 1) {
resolvedOrders.length == 1, revert UniswapXFiller__BatchExecutionNotSupported();
UniswapXFiller__BatchExecutionNotSupported() }
);
ResolvedOrder memory order = resolvedOrders[0]; ResolvedOrder memory order = resolvedOrders[0];

View File

@@ -55,6 +55,7 @@ contract Constants is Test, BaseConstants {
address WTAO_ADDR = address(0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44); address WTAO_ADDR = address(0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44);
address BSGG_ADDR = address(0xdA16Cf041E2780618c49Dbae5d734B89a6Bac9b3); address BSGG_ADDR = address(0xdA16Cf041E2780618c49Dbae5d734B89a6Bac9b3);
address GHO_ADDR = address(0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f); address GHO_ADDR = address(0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f);
address ONDO_ADDR = address(0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3);
// Maverick v2 // Maverick v2
address MAVERICK_V2_FACTORY = 0x0A7e848Aca42d879EF06507Fca0E7b33A0a63c1e; address MAVERICK_V2_FACTORY = 0x0A7e848Aca42d879EF06507Fca0E7b33A0a63c1e;
@@ -124,6 +125,12 @@ contract Constants is Test, BaseConstants {
// Permit2 // Permit2
address PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; address PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
// Bebop Settlement
address BEBOP_SETTLEMENT = 0xbbbbbBB520d69a9775E85b458C58c648259FAD5F;
// Hashflow Router
address HASHFLOW_ROUTER = 0x55084eE0fEf03f14a305cd24286359A35D735151;
// Pool Code Init Hashes // Pool Code Init Hashes
bytes32 USV2_POOL_CODE_INIT_HASH = bytes32 USV2_POOL_CODE_INIT_HASH =
0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f; 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f;

View File

@@ -26,4 +26,26 @@ contract TestUtils is Test {
revert("Test calldata not found"); 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/<name>.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);
}
} }

View File

@@ -3,6 +3,7 @@ pragma solidity ^0.8.26;
import "./TychoRouterTestSetup.sol"; import "./TychoRouterTestSetup.sol";
import "./protocols/UniswapV4Utils.sol"; import "./protocols/UniswapV4Utils.sol";
import "@src/executors/BebopExecutor.sol";
contract TychoRouterTestProtocolIntegration is TychoRouterTestSetup { contract TychoRouterTestProtocolIntegration is TychoRouterTestSetup {
function testMultiProtocolIntegration() public { function testMultiProtocolIntegration() public {

View File

@@ -492,4 +492,95 @@ contract TychoRouterSequentialSwapTest is TychoRouterTestSetup {
assertEq(balanceAfter - balanceBefore, 1949668893); assertEq(balanceAfter - balanceBefore, 1949668893);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); 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);
}
} }

View File

@@ -4,8 +4,10 @@ pragma solidity ^0.8.26;
// Executors // Executors
import {BalancerV2Executor} from "../src/executors/BalancerV2Executor.sol"; import {BalancerV2Executor} from "../src/executors/BalancerV2Executor.sol";
import {BalancerV3Executor} from "../src/executors/BalancerV3Executor.sol"; import {BalancerV3Executor} from "../src/executors/BalancerV3Executor.sol";
import {BebopExecutor} from "../src/executors/BebopExecutor.sol";
import {CurveExecutor} from "../src/executors/CurveExecutor.sol"; import {CurveExecutor} from "../src/executors/CurveExecutor.sol";
import {EkuboExecutor} from "../src/executors/EkuboExecutor.sol"; import {EkuboExecutor} from "../src/executors/EkuboExecutor.sol";
import {HashflowExecutor} from "../src/executors/HashflowExecutor.sol";
import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol"; import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol";
import {UniswapV2Executor} from "../src/executors/UniswapV2Executor.sol"; import {UniswapV2Executor} from "../src/executors/UniswapV2Executor.sol";
import { import {
@@ -73,12 +75,14 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
CurveExecutor public curveExecutor; CurveExecutor public curveExecutor;
MaverickV2Executor public maverickv2Executor; MaverickV2Executor public maverickv2Executor;
BalancerV3Executor public balancerV3Executor; BalancerV3Executor public balancerV3Executor;
BebopExecutor public bebopExecutor;
HashflowExecutor public hashflowExecutor;
function getForkBlock() public view virtual returns (uint256) { function getForkBlock() public view virtual returns (uint256) {
return 22082754; return 22082754;
} }
function setUp() public { function setUp() public virtual {
uint256 forkBlock = getForkBlock(); uint256 forkBlock = getForkBlock();
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
@@ -132,8 +136,11 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
maverickv2Executor = maverickv2Executor =
new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS); new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS);
balancerV3Executor = new BalancerV3Executor(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[0] = address(usv2Executor);
executors[1] = address(usv3Executor); executors[1] = address(usv3Executor);
executors[2] = address(pancakev3Executor); executors[2] = address(pancakev3Executor);
@@ -143,6 +150,8 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
executors[6] = address(curveExecutor); executors[6] = address(curveExecutor);
executors[7] = address(maverickv2Executor); executors[7] = address(maverickv2Executor);
executors[8] = address(balancerV3Executor); executors[8] = address(balancerV3Executor);
executors[9] = address(bebopExecutor);
executors[10] = address(hashflowExecutor);
return executors; return executors;
} }

File diff suppressed because one or more lines are too long

View File

@@ -134,4 +134,9 @@ contract BalancerV2ExecutorTest is Constants, TestUtils {
assertGt(balanceAfter, balanceBefore); assertGt(balanceAfter, balanceBefore);
assertEq(balanceAfter - balanceBefore, amountOut); assertEq(balanceAfter - balanceBefore, amountOut);
} }
function testExportContract() public {
vm.skip(true);
exportRuntimeBytecode(address(balancerV2Exposed), "BalancerV2");
}
} }

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -24,6 +24,8 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor {
bool zeroForOne, bool zeroForOne,
RestrictTransferFrom.TransferType transferType, RestrictTransferFrom.TransferType transferType,
address receiver, address receiver,
address hook,
bytes memory hookData,
UniswapV4Pool[] memory pools UniswapV4Pool[] memory pools
) )
{ {
@@ -37,10 +39,12 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
UniswapV4ExecutorExposed uniswapV4Exposed; UniswapV4ExecutorExposed uniswapV4Exposed;
IERC20 USDE = IERC20(USDE_ADDR); IERC20 USDE = IERC20(USDE_ADDR);
IERC20 USDT = IERC20(USDT_ADDR); IERC20 USDT = IERC20(USDT_ADDR);
IERC20 USDC = IERC20(USDC_ADDR);
address poolManager = 0x000000000004444c5dc75cB358380D2e3dE08A90; address poolManager = 0x000000000004444c5dc75cB358380D2e3dE08A90;
function setUp() public { function setUp() public {
uint256 forkBlock = 21817316; uint256 forkBlock = 22689128;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
uniswapV4Exposed = new UniswapV4ExecutorExposed( uniswapV4Exposed = new UniswapV4ExecutorExposed(
IPoolManager(poolManager), PERMIT2_ADDRESS IPoolManager(poolManager), PERMIT2_ADDRESS
@@ -73,6 +77,8 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
zeroForOne, zeroForOne,
RestrictTransferFrom.TransferType.Transfer, RestrictTransferFrom.TransferType.Transfer,
ALICE, ALICE,
address(0),
bytes(""),
pools pools
); );
@@ -82,6 +88,8 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
bool zeroForOneDecoded, bool zeroForOneDecoded,
RestrictTransferFrom.TransferType transferType, RestrictTransferFrom.TransferType transferType,
address receiver, address receiver,
address hook,
bytes memory hookData,
UniswapV4Executor.UniswapV4Pool[] memory decodedPools UniswapV4Executor.UniswapV4Pool[] memory decodedPools
) = uniswapV4Exposed.decodeData(data); ) = uniswapV4Exposed.decodeData(data);
@@ -93,6 +101,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
uint8(RestrictTransferFrom.TransferType.Transfer) uint8(RestrictTransferFrom.TransferType.Transfer)
); );
assertEq(receiver, ALICE); assertEq(receiver, ALICE);
assertEq(hook, address(0));
assertEq(decodedPools.length, 2); assertEq(decodedPools.length, 2);
assertEq(decodedPools[0].intermediaryToken, USDT_ADDR); assertEq(decodedPools[0].intermediaryToken, USDT_ADDR);
assertEq(decodedPools[0].fee, pool1Fee); assertEq(decodedPools[0].fee, pool1Fee);
@@ -123,6 +132,8 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
true, true,
RestrictTransferFrom.TransferType.Transfer, RestrictTransferFrom.TransferType.Transfer,
ALICE, ALICE,
address(0),
bytes(""),
pools pools
); );
@@ -180,6 +191,8 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
true, true,
RestrictTransferFrom.TransferType.Transfer, RestrictTransferFrom.TransferType.Transfer,
ALICE, ALICE,
address(0),
bytes(""),
pools pools
); );
@@ -211,6 +224,43 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
); );
assertTrue(IERC20(WBTC_ADDR).balanceOf(ALICE) == amountOut); 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 { contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
@@ -237,6 +287,8 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
true, true,
RestrictTransferFrom.TransferType.TransferFrom, RestrictTransferFrom.TransferType.TransferFrom,
ALICE, ALICE,
address(0),
bytes(""),
pools pools
); );
@@ -285,6 +337,8 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
true, true,
RestrictTransferFrom.TransferType.TransferFrom, RestrictTransferFrom.TransferType.TransferFrom,
ALICE, ALICE,
address(0),
bytes(""),
pools pools
); );

View File

@@ -10,13 +10,21 @@ library UniswapV4Utils {
bool zeroForOne, bool zeroForOne,
RestrictTransferFrom.TransferType transferType, RestrictTransferFrom.TransferType transferType,
address receiver, address receiver,
address hook,
bytes memory hookData,
UniswapV4Executor.UniswapV4Pool[] memory pools UniswapV4Executor.UniswapV4Pool[] memory pools
) public pure returns (bytes memory) { ) 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++) { bytes memory firstPool = abi.encodePacked(
encodedPools = abi.encodePacked( pools[0].intermediaryToken,
encodedPools, 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, pools[i].intermediaryToken,
bytes3(pools[i].fee), bytes3(pools[i].fee),
pools[i].tickSpacing pools[i].tickSpacing
@@ -24,7 +32,28 @@ library UniswapV4Utils {
} }
return abi.encodePacked( 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])
);
}
}
} }

View File

@@ -1,6 +1,7 @@
use std::{io, str::Utf8Error}; use std::{io, str::Utf8Error};
use thiserror::Error; use thiserror::Error;
use tycho_common::simulation::errors::SimulationError;
/// Represents the outer-level, user-facing errors of the tycho-execution encoding package. /// Represents the outer-level, user-facing errors of the tycho-execution encoding package.
/// ///
@@ -41,3 +42,15 @@ impl From<Utf8Error> for EncodingError {
EncodingError::FatalError(err.to_string()) EncodingError::FatalError(err.to_string())
} }
} }
impl From<SimulationError> 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)
}
}
}
}

View File

@@ -31,10 +31,6 @@ pub struct Permit2 {
address: Address, address: Address,
client: EVMProvider, client: EVMProvider,
runtime_handle: Handle, 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)] #[allow(dead_code)]
runtime: Option<Arc<Runtime>>, runtime: Option<Arc<Runtime>>,
} }

View File

@@ -23,10 +23,6 @@ use crate::encoding::{
pub struct ProtocolApprovalsManager { pub struct ProtocolApprovalsManager {
client: EVMProvider, client: EVMProvider,
runtime_handle: Handle, 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)] #[allow(dead_code)]
runtime: Option<Arc<Runtime>>, runtime: Option<Arc<Runtime>>,
} }

View File

@@ -13,6 +13,7 @@ pub const PROTOCOL_SPECIFIC_CONFIG: &str =
pub static GROUPABLE_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| { pub static GROUPABLE_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
let mut set = HashSet::new(); let mut set = HashSet::new();
set.insert("uniswap_v4"); set.insert("uniswap_v4");
set.insert("uniswap_v4_hooks");
set.insert("vm:balancer_v3"); set.insert("vm:balancer_v3");
set.insert("ekubo_v2"); set.insert("ekubo_v2");
set set
@@ -29,6 +30,7 @@ pub static IN_TRANSFER_REQUIRED_PROTOCOLS: LazyLock<HashSet<&'static str>> = Laz
set.insert("uniswap_v3"); set.insert("uniswap_v3");
set.insert("pancakeswap_v3"); set.insert("pancakeswap_v3");
set.insert("uniswap_v4"); set.insert("uniswap_v4");
set.insert("uniswap_v4_hooks");
set.insert("ekubo_v2"); set.insert("ekubo_v2");
set.insert("vm:maverick_v2"); set.insert("vm:maverick_v2");
set.insert("vm:balancer_v3"); set.insert("vm:balancer_v3");
@@ -46,6 +48,7 @@ pub static CALLBACK_CONSTRAINED_PROTOCOLS: LazyLock<HashSet<&'static str>> = Laz
set.insert("uniswap_v3"); set.insert("uniswap_v3");
set.insert("pancakeswap_v3"); set.insert("pancakeswap_v3");
set.insert("uniswap_v4"); set.insert("uniswap_v4");
set.insert("uniswap_v4_hooks");
set.insert("ekubo_v2"); set.insert("ekubo_v2");
set.insert("vm:balancer_v3"); set.insert("vm:balancer_v3");
set set

View File

@@ -11,7 +11,7 @@ use crate::encoding::{evm::constants::GROUPABLE_PROTOCOLS, models::Swap};
/// * `protocol_system`: String, the protocol system of the swaps /// * `protocol_system`: String, the protocol system of the swaps
/// * `swaps`: Vec<Swap>, the sequence of swaps to be executed as a group /// * `swaps`: Vec<Swap>, the sequence of swaps to be executed as a group
/// * `split`: f64, the split percentage of the first swap in the group /// * `split`: f64, the split percentage of the first swap in the group
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, Debug)]
pub struct SwapGroup { pub struct SwapGroup {
pub token_in: Bytes, pub token_in: Bytes,
pub token_out: Bytes, pub token_out: Bytes,
@@ -20,6 +20,16 @@ pub struct SwapGroup {
pub split: f64, 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. /// 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 /// 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 tycho_common::{models::protocol::ProtocolComponent, Bytes};
use super::*; use super::*;
use crate::encoding::models::Swap; use crate::encoding::models::SwapBuilder;
fn weth() -> Bytes { fn weth() -> Bytes {
Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec()) Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec())
@@ -95,43 +105,28 @@ mod tests {
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swap_weth_wbtc = Swap { let swap_weth_wbtc = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() },
protocol_system: "uniswap_v4".to_string(), weth.clone(),
..Default::default() wbtc.clone(),
}, )
token_in: weth.clone(), .build();
token_out: wbtc.clone(),
// This represents the remaining 50%, but to avoid any rounding errors we set this to let swap_wbtc_usdc = SwapBuilder::new(
// 0 to signify "the remainder of the WETH value". It should still be very close to 50% ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() },
split: 0f64, wbtc.clone(),
user_data: None, usdc.clone(),
}; )
let swap_wbtc_usdc = Swap { .build();
component: ProtocolComponent {
protocol_system: "uniswap_v4".to_string(), let swap_usdc_dai = SwapBuilder::new(
..Default::default() ProtocolComponent { protocol_system: "uniswap_v2".to_string(), ..Default::default() },
}, usdc.clone(),
token_in: wbtc.clone(), dai.clone(),
token_out: usdc.clone(), )
split: 0f64, .build();
user_data: None, let swaps = vec![swap_weth_wbtc.clone(), swap_wbtc_usdc.clone(), swap_usdc_dai.clone()];
}; let grouped_swaps = group_swaps(&swaps);
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(),
]);
assert_eq!( assert_eq!(
grouped_swaps, grouped_swaps,
@@ -169,54 +164,41 @@ mod tests {
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swap_wbtc_weth = Swap { let swap_wbtc_weth = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() },
protocol_system: "uniswap_v4".to_string(), wbtc.clone(),
..Default::default() weth.clone(),
}, )
token_in: wbtc.clone(), .build();
token_out: weth.clone(), let swap_weth_usdc = SwapBuilder::new(
split: 0f64, ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() },
user_data: None, weth.clone(),
}; usdc.clone(),
let swap_weth_usdc = Swap { )
component: ProtocolComponent { .split(0.5f64)
protocol_system: "uniswap_v4".to_string(), .build();
..Default::default() let swap_weth_dai = SwapBuilder::new(
}, ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() },
token_in: weth.clone(), weth.clone(),
token_out: usdc.clone(), dai.clone(),
split: 0.5f64, )
user_data: None, .build();
}; // Split 0 represents the remaining 50%, but to avoid any rounding errors we set this to
let swap_weth_dai = Swap { // 0 to signify "the remainder of the WETH value". It should still be very close to 50%
component: ProtocolComponent {
protocol_system: "uniswap_v4".to_string(), let swap_dai_usdc = SwapBuilder::new(
..Default::default() ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() },
}, dai.clone(),
token_in: weth.clone(), usdc.clone(),
token_out: dai.clone(), )
// This represents the remaining 50%, but to avoid any rounding errors we set this to .build();
// 0 to signify "the remainder of the WETH value". It should still be very close to 50% let swaps = vec![
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![
swap_wbtc_weth.clone(), swap_wbtc_weth.clone(),
swap_weth_usdc.clone(), swap_weth_usdc.clone(),
swap_weth_dai.clone(), swap_weth_dai.clone(),
swap_dai_usdc.clone(), swap_dai_usdc.clone(),
]); ];
let grouped_swaps = group_swaps(&swaps);
assert_eq!( assert_eq!(
grouped_swaps, grouped_swaps,
@@ -260,55 +242,46 @@ mod tests {
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swap_weth_wbtc = Swap { let swap_weth_wbtc = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
protocol_system: "vm:balancer_v3".to_string(), protocol_system: "vm:balancer_v3".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: wbtc.clone(), wbtc.clone(),
split: 0.5f64, )
user_data: None, .split(0.5f64)
}; .build();
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,
};
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_weth_wbtc.clone(),
swap_wbtc_usdc.clone(), swap_wbtc_usdc.clone(),
swap_weth_dai.clone(), swap_weth_dai.clone(),
swap_dai_usdc.clone(), swap_dai_usdc.clone(),
]); ];
let grouped_swaps = group_swaps(&swaps);
assert_eq!( assert_eq!(
grouped_swaps, grouped_swaps,

View File

@@ -5,5 +5,7 @@ mod encoding_utils;
mod group_swaps; mod group_swaps;
pub mod strategy_encoder; pub mod strategy_encoder;
mod swap_encoder; mod swap_encoder;
#[cfg(feature = "test-utils")]
pub mod testing_utils;
pub mod tycho_encoders; pub mod tycho_encoders;
pub mod utils; pub mod utils;

View File

@@ -121,16 +121,25 @@ impl StrategyEncoder for SingleSwapStrategyEncoder {
transfer_type: transfer, transfer_type: transfer,
}; };
let mut grouped_protocol_data: Vec<u8> = vec![]; let mut grouped_protocol_data: Vec<Vec<u8>> = vec![];
let mut initial_protocol_data: Vec<u8> = vec![];
for swap in grouped_swap.swaps.iter() { for swap in grouped_swap.swaps.iter() {
let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?; 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( let swap_data = self.encode_swap_header(
Bytes::from_str(swap_encoder.executor_address()) Bytes::from_str(swap_encoder.executor_address())
.map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?, .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?,
grouped_protocol_data, initial_protocol_data,
); );
Ok(EncodedSolution { Ok(EncodedSolution {
function_signature: self.function_signature.clone(), function_signature: self.function_signature.clone(),
@@ -228,10 +237,11 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder {
let grouped_swaps = group_swaps(&solution.swaps); 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 let Some(action) = &solution.native_action {
if action == &NativeAction::Wrap { match *action {
wrap = true 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 next_swap = grouped_swaps.get(i + 1);
let (swap_receiver, next_swap_optimization) = self let (swap_receiver, next_swap_optimization) = self
.transfer_optimization .transfer_optimization
.get_receiver(&solution.receiver, next_swap)?; .get_receiver(&solution.receiver, next_swap, unwrap)?;
next_in_between_swap_optimization_allowed = next_swap_optimization; next_in_between_swap_optimization_allowed = next_swap_optimization;
let transfer = self let transfer = self
@@ -271,17 +281,26 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder {
transfer_type: transfer, transfer_type: transfer,
}; };
let mut grouped_protocol_data: Vec<u8> = vec![]; let mut grouped_protocol_data: Vec<Vec<u8>> = vec![];
let mut initial_protocol_data: Vec<u8> = vec![];
for swap in grouped_swap.swaps.iter() { for swap in grouped_swap.swaps.iter() {
let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?; 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( let swap_data = self.encode_swap_header(
Bytes::from_str(swap_encoder.executor_address()).map_err(|_| { Bytes::from_str(swap_encoder.executor_address()).map_err(|_| {
EncodingError::FatalError("Invalid executor address".to_string()) EncodingError::FatalError("Invalid executor address".to_string())
})?, })?,
grouped_protocol_data, initial_protocol_data,
); );
swaps.push(swap_data); swaps.push(swap_data);
} }
@@ -462,10 +481,19 @@ impl StrategyEncoder for SplitSwapStrategyEncoder {
transfer_type: transfer, transfer_type: transfer,
}; };
let mut grouped_protocol_data: Vec<u8> = vec![]; let mut grouped_protocol_data: Vec<Vec<u8>> = vec![];
let mut initial_protocol_data: Vec<u8> = vec![];
for swap in grouped_swap.swaps.iter() { for swap in grouped_swap.swaps.iter() {
let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?; 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( let swap_data = self.encode_swap_header(
@@ -475,7 +503,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder {
Bytes::from_str(swap_encoder.executor_address()).map_err(|_| { Bytes::from_str(swap_encoder.executor_address()).map_err(|_| {
EncodingError::FatalError("Invalid executor address".to_string()) EncodingError::FatalError("Invalid executor address".to_string())
})?, })?,
grouped_protocol_data, initial_protocol_data,
); );
swaps.push(swap_data); swaps.push(swap_data);
} }
@@ -517,7 +545,6 @@ mod tests {
}; };
use super::*; use super::*;
use crate::encoding::models::Swap;
fn eth_chain() -> Chain { fn eth_chain() -> Chain {
Chain::Ethereum Chain::Ethereum
@@ -538,8 +565,8 @@ mod tests {
} }
mod single { mod single {
use super::*; use super::*;
use crate::encoding::models::SwapBuilder;
#[test] #[test]
fn test_single_swap_strategy_encoder() { fn test_single_swap_strategy_encoder() {
// Performs a single swap from WETH to DAI on a USV2 pool, with no grouping // 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 weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0f64, )
user_data: None, .build();
};
let swap_encoder_registry = get_swap_encoder_registry(); let swap_encoder_registry = get_swap_encoder_registry();
let encoder = SingleSwapStrategyEncoder::new( let encoder = SingleSwapStrategyEncoder::new(
eth_chain(), eth_chain(),
@@ -609,17 +635,16 @@ mod tests {
let checked_amount = BigUint::from_str("1_640_000000000000000000").unwrap(); let checked_amount = BigUint::from_str("1_640_000000000000000000").unwrap();
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0f64, )
user_data: None, .build();
};
let swap_encoder_registry = get_swap_encoder_registry(); let swap_encoder_registry = get_swap_encoder_registry();
let encoder = SingleSwapStrategyEncoder::new( let encoder = SingleSwapStrategyEncoder::new(
eth_chain(), eth_chain(),
@@ -669,6 +694,7 @@ mod tests {
mod sequential { mod sequential {
use super::*; use super::*;
use crate::encoding::models::SwapBuilder;
#[test] #[test]
fn test_sequential_swap_strategy_encoder_no_permit2() { fn test_sequential_swap_strategy_encoder_no_permit2() {
@@ -680,28 +706,26 @@ mod tests {
let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let swap_weth_wbtc = Swap { let swap_weth_wbtc = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(), id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: wbtc.clone(), wbtc.clone(),
split: 0f64, )
user_data: None, .build();
}; let swap_wbtc_usdc = SwapBuilder::new(
let swap_wbtc_usdc = Swap { ProtocolComponent {
component: ProtocolComponent {
id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(), id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: wbtc.clone(), wbtc.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0f64, )
user_data: None, .build();
};
let swap_encoder_registry = get_swap_encoder_registry(); let swap_encoder_registry = get_swap_encoder_registry();
let encoder = SequentialSwapStrategyEncoder::new( let encoder = SequentialSwapStrategyEncoder::new(
eth_chain(), eth_chain(),
@@ -759,6 +783,7 @@ mod tests {
mod split { mod split {
use super::*; use super::*;
use crate::encoding::models::SwapBuilder;
#[test] #[test]
fn test_split_input_cyclic_swap() { fn test_split_input_cyclic_swap() {
@@ -774,8 +799,8 @@ mod tests {
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
// USDC -> WETH (Pool 1) - 60% of input // USDC -> WETH (Pool 1) - 60% of input
let swap_usdc_weth_pool1 = Swap { let swap_usdc_weth_pool1 = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3 id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3
* Pool 1 */ * Pool 1 */
protocol_system: "uniswap_v3".to_string(), protocol_system: "uniswap_v3".to_string(),
@@ -789,15 +814,15 @@ mod tests {
}, },
..Default::default() ..Default::default()
}, },
token_in: usdc.clone(), usdc.clone(),
token_out: weth.clone(), weth.clone(),
split: 0.6f64, // 60% of input )
user_data: None, .split(0.6f64)
}; .build();
// USDC -> WETH (Pool 2) - 40% of input (remaining) // USDC -> WETH (Pool 2) - 40% of input (remaining)
let swap_usdc_weth_pool2 = Swap { let swap_usdc_weth_pool2 = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3 id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3
* Pool 2 */ * Pool 2 */
protocol_system: "uniswap_v3".to_string(), protocol_system: "uniswap_v3".to_string(),
@@ -811,15 +836,14 @@ mod tests {
}, },
..Default::default() ..Default::default()
}, },
token_in: usdc.clone(), usdc.clone(),
token_out: weth.clone(), weth.clone(),
split: 0f64, )
user_data: None, // Remaining 40% .build();
};
// WETH -> USDC (Pool 2) // WETH -> USDC (Pool 2)
let swap_weth_usdc_pool2 = Swap { let swap_weth_usdc_pool2 = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), /* USDC-WETH USV2 id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), /* USDC-WETH USV2
* Pool 2 */ * Pool 2 */
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
@@ -833,12 +857,10 @@ mod tests {
}, },
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0.0f64, )
user_data: None, .build();
};
let swap_encoder_registry = get_swap_encoder_registry(); let swap_encoder_registry = get_swap_encoder_registry();
let encoder = SplitSwapStrategyEncoder::new( let encoder = SplitSwapStrategyEncoder::new(
eth_chain(), eth_chain(),
@@ -927,8 +949,8 @@ mod tests {
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let swap_usdc_weth_v2 = Swap { let swap_usdc_weth_v2 = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), // USDC-WETH USV2 id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), // USDC-WETH USV2
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
static_attributes: { static_attributes: {
@@ -941,14 +963,13 @@ mod tests {
}, },
..Default::default() ..Default::default()
}, },
token_in: usdc.clone(), usdc.clone(),
token_out: weth.clone(), weth.clone(),
split: 0.0f64, )
user_data: None, .build();
};
let swap_weth_usdc_v3_pool1 = Swap { let swap_weth_usdc_v3_pool1 = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3 id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3
* Pool 1 */ * Pool 1 */
protocol_system: "uniswap_v3".to_string(), protocol_system: "uniswap_v3".to_string(),
@@ -962,16 +983,16 @@ mod tests {
}, },
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0.6f64, )
user_data: None, .split(0.6f64)
}; .build();
let swap_weth_usdc_v3_pool2 = Swap { let swap_weth_usdc_v3_pool2 = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3 id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3
* Pool 2 */ * Pool 1 */
protocol_system: "uniswap_v3".to_string(), protocol_system: "uniswap_v3".to_string(),
static_attributes: { static_attributes: {
let mut attrs = HashMap::new(); let mut attrs = HashMap::new();
@@ -983,11 +1004,10 @@ mod tests {
}, },
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0.0f64, )
user_data: None, .build();
};
let swap_encoder_registry = get_swap_encoder_registry(); let swap_encoder_registry = get_swap_encoder_registry();
let encoder = SplitSwapStrategyEncoder::new( let encoder = SplitSwapStrategyEncoder::new(

View File

@@ -197,7 +197,7 @@ mod tests {
use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use tycho_common::{models::protocol::ProtocolComponent, Bytes};
use super::*; use super::*;
use crate::encoding::models::Swap; use crate::encoding::models::{Swap, SwapBuilder};
#[test] #[test]
fn test_validate_path_single_swap() { fn test_validate_path_single_swap() {
@@ -205,17 +205,16 @@ mod tests {
let eth = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(); let eth = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap();
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swaps = vec![Swap { let swaps = vec![SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0f64, )
user_data: None, .build()];
}];
let result = validator.validate_swap_path(&swaps, &weth, &dai, &None, &eth, &weth); let result = validator.validate_swap_path(&swaps, &weth, &dai, &None, &eth, &weth);
assert_eq!(result, Ok(())); assert_eq!(result, Ok(()));
} }
@@ -228,28 +227,27 @@ mod tests {
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let swaps = vec![ let swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.5f64, )
user_data: None, .split(0.5f64)
}, .build(),
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai.clone(), dai.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0f64, )
user_data: None, .build(),
},
]; ];
let result = validator.validate_swap_path(&swaps, &weth, &usdc, &None, &eth, &weth); let result = validator.validate_swap_path(&swaps, &weth, &usdc, &None, &eth, &weth);
assert_eq!(result, Ok(())); assert_eq!(result, Ok(()));
@@ -265,29 +263,28 @@ mod tests {
let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap();
let disconnected_swaps = vec![ let disconnected_swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.5, )
user_data: None, .split(0.5f64)
}, .build(),
// This swap is disconnected from the WETH->DAI path // This swap is disconnected from the WETH->DAI path
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool2".to_string(), id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: wbtc.clone(), wbtc.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0.0, )
user_data: None, .build(),
},
]; ];
let result = let result =
validator.validate_swap_path(&disconnected_swaps, &weth, &usdc, &None, &eth, &weth); validator.validate_swap_path(&disconnected_swaps, &weth, &usdc, &None, &eth, &weth);
@@ -305,28 +302,26 @@ mod tests {
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let cyclic_swaps = vec![ let cyclic_swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: usdc.clone(), usdc.clone(),
token_out: weth.clone(), weth.clone(),
split: 0f64, )
user_data: None, .build(),
}, SwapBuilder::new(
Swap { ProtocolComponent {
component: ProtocolComponent {
id: "pool2".to_string(), id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0f64, )
user_data: None, .build(),
},
]; ];
// Test with USDC as both given token and checked token // Test with USDC as both given token and checked token
@@ -342,17 +337,17 @@ mod tests {
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let unreachable_swaps = vec![Swap { let unreachable_swaps = vec![SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 1.0, )
user_data: None, .split(1.0)
}]; .build()];
let result = let result =
validator.validate_swap_path(&unreachable_swaps, &weth, &usdc, &None, &eth, &weth); validator.validate_swap_path(&unreachable_swaps, &weth, &usdc, &None, &eth, &weth);
assert!(matches!( assert!(matches!(
@@ -381,17 +376,16 @@ mod tests {
let validator = SplitSwapValidator; let validator = SplitSwapValidator;
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swaps = vec![Swap { let swaps = vec![SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0f64, )
user_data: None, .build()];
}];
let result = validator.validate_split_percentages(&swaps); let result = validator.validate_split_percentages(&swaps);
assert_eq!(result, Ok(())); assert_eq!(result, Ok(()));
} }
@@ -404,39 +398,38 @@ mod tests {
// Valid case: Multiple swaps with proper splits (50%, 30%, remainder) // Valid case: Multiple swaps with proper splits (50%, 30%, remainder)
let valid_swaps = vec![ let valid_swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.5, )
user_data: None, .split(0.5)
}, .build(),
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool2".to_string(), id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.3, )
user_data: None, .split(0.3)
}, .build(),
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool3".to_string(), id: "pool3".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.0, // Remainder (20%) )
user_data: None, .build(),
},
]; ];
assert!(validator assert!(validator
.validate_split_percentages(&valid_swaps) .validate_split_percentages(&valid_swaps)
@@ -450,28 +443,28 @@ mod tests {
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let invalid_total_swaps = vec![ let invalid_total_swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.7, )
user_data: None, .split(0.7)
}, .build(),
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool2".to_string(), id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.3, )
user_data: None, .split(0.3)
}, .build(),
]; ];
assert!(matches!( assert!(matches!(
validator.validate_split_percentages(&invalid_total_swaps), validator.validate_split_percentages(&invalid_total_swaps),
@@ -486,28 +479,27 @@ mod tests {
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let invalid_zero_position_swaps = vec![ let invalid_zero_position_swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.0, )
user_data: None, .build(),
}, SwapBuilder::new(
Swap { ProtocolComponent {
component: ProtocolComponent {
id: "pool2".to_string(), id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.5, )
user_data: None, .split(0.5)
}, .build(),
]; ];
assert!(matches!( assert!(matches!(
validator.validate_split_percentages(&invalid_zero_position_swaps), validator.validate_split_percentages(&invalid_zero_position_swaps),
@@ -522,39 +514,38 @@ mod tests {
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let invalid_overflow_swaps = vec![ let invalid_overflow_swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.6, )
user_data: None, .split(0.6)
}, .build(),
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool2".to_string(), id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.5, )
user_data: None, .split(0.5)
}, .build(),
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool3".to_string(), id: "pool3".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.0, )
user_data: None, .build(),
},
]; ];
assert!(matches!( assert!(matches!(
validator.validate_split_percentages(&invalid_overflow_swaps), validator.validate_split_percentages(&invalid_overflow_swaps),
@@ -569,17 +560,16 @@ mod tests {
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(); let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
let swaps = vec![Swap { let swaps = vec![SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0f64, )
user_data: None, .build()];
}];
let result = validator.validate_swap_path( let result = validator.validate_swap_path(
&swaps, &swaps,
@@ -599,17 +589,16 @@ mod tests {
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(); let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
let swaps = vec![Swap { let swaps = vec![SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: usdc.clone(), usdc.clone(),
token_out: weth.clone(), weth.clone(),
split: 0f64, )
user_data: None, .build()];
}];
let result = validator.validate_swap_path( let result = validator.validate_swap_path(
&swaps, &swaps,

View File

@@ -82,6 +82,7 @@ impl TransferOptimization {
&self, &self,
solution_receiver: &Bytes, solution_receiver: &Bytes,
next_swap: Option<&SwapGroup>, next_swap: Option<&SwapGroup>,
unwrap: bool,
) -> Result<(Bytes, bool), EncodingError> { ) -> Result<(Bytes, bool), EncodingError> {
if let Some(next) = next_swap { if let Some(next) = next_swap {
// if the protocol of the next swap supports transfer in optimization // if the protocol of the next swap supports transfer in optimization
@@ -104,7 +105,11 @@ impl TransferOptimization {
} }
} else { } else {
// last swap - there is no next swap // 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 tycho_common::models::protocol::ProtocolComponent;
use super::*; use super::*;
use crate::encoding::models::Swap; use crate::encoding::models::SwapBuilder;
fn weth() -> Bytes { fn weth() -> Bytes {
Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec()) Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec())
@@ -169,17 +174,16 @@ mod tests {
#[case] expected_transfer: TransferType, #[case] expected_transfer: TransferType,
) { ) {
// The swap token is the same as the given token, which is not the native token // The swap token is the same as the given token, which is not the native token
let swaps = vec![Swap { let swaps = vec![SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
..Default::default() ..Default::default()
}, },
token_in: swap_token_in.clone(), swap_token_in.clone(),
token_out: dai(), dai(),
split: 0f64, )
user_data: None, .build()];
}];
let swap = SwapGroup { let swap = SwapGroup {
protocol_system: protocol, protocol_system: protocol,
token_in: swap_token_in, token_in: swap_token_in,
@@ -203,16 +207,19 @@ mod tests {
} }
#[rstest] #[rstest]
// there is no next swap -> receiver is the solution receiver // there is no next swap but there is an unwrap -> receiver is the router
#[case(None, receiver(), false)] #[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 // 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 // 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 // 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( fn test_get_receiver(
#[case] protocol: Option<&str>, #[case] protocol: Option<&str>,
#[case] unwrap: bool,
#[case] expected_receiver: Bytes, #[case] expected_receiver: Bytes,
#[case] expected_optimization: bool, #[case] expected_optimization: bool,
) { ) {
@@ -231,21 +238,20 @@ mod tests {
token_in: usdc(), token_in: usdc(),
token_out: dai(), token_out: dai(),
split: 0f64, split: 0f64,
swaps: vec![Swap { swaps: vec![SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
protocol_system: protocol.unwrap().to_string(), protocol_system: protocol.unwrap().to_string(),
id: component_id().to_string(), id: component_id().to_string(),
..Default::default() ..Default::default()
}, },
token_in: usdc(), usdc(),
token_out: dai(), dai(),
split: 0f64, )
user_data: None, .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()); assert!(result.is_ok());
let (actual_receiver, optimization_flag) = result.unwrap(); let (actual_receiver, optimization_flag) = result.unwrap();

View File

@@ -5,8 +5,9 @@ use tycho_common::models::Chain;
use crate::encoding::{ use crate::encoding::{
errors::EncodingError, errors::EncodingError,
evm::swap_encoder::swap_encoders::{ evm::swap_encoder::swap_encoders::{
BalancerV2SwapEncoder, BalancerV3SwapEncoder, CurveSwapEncoder, EkuboSwapEncoder, BalancerV2SwapEncoder, BalancerV3SwapEncoder, BebopSwapEncoder, CurveSwapEncoder,
MaverickV2SwapEncoder, UniswapV2SwapEncoder, UniswapV3SwapEncoder, UniswapV4SwapEncoder, EkuboSwapEncoder, HashflowSwapEncoder, MaverickV2SwapEncoder, UniswapV2SwapEncoder,
UniswapV3SwapEncoder, UniswapV4SwapEncoder,
}, },
swap_encoder::SwapEncoder, swap_encoder::SwapEncoder,
}; };
@@ -71,6 +72,11 @@ impl SwapEncoderBuilder {
self.chain, self.chain,
self.config, self.config,
)?)), )?)),
"uniswap_v4_hooks" => Ok(Box::new(UniswapV4SwapEncoder::new(
self.executor_address,
self.chain,
self.config,
)?)),
"ekubo_v2" => { "ekubo_v2" => {
Ok(Box::new(EkuboSwapEncoder::new(self.executor_address, self.chain, self.config)?)) Ok(Box::new(EkuboSwapEncoder::new(self.executor_address, self.chain, self.config)?))
} }
@@ -87,6 +93,14 @@ impl SwapEncoderBuilder {
self.chain, self.chain,
self.config, 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!( _ => Err(EncodingError::FatalError(format!(
"Unknown protocol system: {}", "Unknown protocol system: {}",
self.protocol_system self.protocol_system

View File

@@ -1,17 +1,26 @@
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr, sync::Arc};
use alloy::{ use alloy::{
primitives::{Address, Bytes as AlloyBytes, U8}, primitives::{Address, Bytes as AlloyBytes, U8},
sol_types::SolValue, sol_types::SolValue,
}; };
use serde_json::from_str; 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::{ use crate::encoding::{
errors::EncodingError, errors::EncodingError,
evm::{ evm::{
approvals::protocol_approvals_manager::ProtocolApprovalsManager, 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}, models::{EncodingContext, Swap},
swap_encoder::SwapEncoder, swap_encoder::SwapEncoder,
@@ -176,9 +185,28 @@ impl SwapEncoder for UniswapV4SwapEncoder {
EncodingError::FatalError("Failed to pad tick spacing bytes".to_string()) 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 // Early check if this is not the first swap
if encoding_context.group_token_in != swap.token_in { 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()); .abi_encode_packed());
} }
@@ -199,7 +227,9 @@ impl SwapEncoder for UniswapV4SwapEncoder {
zero_to_one, zero_to_one,
(encoding_context.transfer_type as u8).to_be_bytes(), (encoding_context.transfer_type as u8).to_be_bytes(),
bytes_to_address(&encoding_context.receiver)?, bytes_to_address(&encoding_context.receiver)?,
hook_address,
pool_params, pool_params,
hook_data,
); );
Ok(args.abi_encode_packed()) Ok(args.abi_encode_packed())
@@ -634,6 +664,326 @@ impl SwapEncoder for BalancerV3SwapEncoder {
fn executor_address(&self) -> &str { fn executor_address(&self) -> &str {
&self.executor_address &self.executor_address
} }
fn clone_box(&self) -> Box<dyn SwapEncoder> {
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<Arc<Runtime>>,
}
impl SwapEncoder for BebopSwapEncoder {
fn new(
executor_address: String,
chain: Chain,
config: Option<HashMap<String, String>>,
) -> Result<Self, EncodingError> {
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<Vec<u8>, 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<dyn SwapEncoder> {
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<Arc<Runtime>>,
}
impl SwapEncoder for HashflowSwapEncoder {
fn new(
executor_address: String,
chain: Chain,
config: Option<HashMap<String, String>>,
) -> Result<Self, EncodingError> {
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<Vec<u8>, 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<dyn SwapEncoder> { fn clone_box(&self) -> Box<dyn SwapEncoder> {
Box::new(self.clone()) Box::new(self.clone())
} }
@@ -651,10 +1001,14 @@ mod tests {
}; };
use super::*; 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 { mod uniswap_v2 {
use super::*; use super::*;
use crate::encoding::models::SwapBuilder;
#[test] #[test]
fn test_encode_uniswap_v2() { fn test_encode_uniswap_v2() {
let usv2_pool = ProtocolComponent { let usv2_pool = ProtocolComponent {
@@ -664,13 +1018,7 @@ mod tests {
let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"); let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f");
let swap = Swap { let swap = SwapBuilder::new(usv2_pool, token_in.clone(), token_out.clone()).build();
component: usv2_pool,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: None,
};
let encoding_context = EncodingContext { let encoding_context = EncodingContext {
receiver: Bytes::from("0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e"), // BOB receiver: Bytes::from("0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e"), // BOB
exact_out: false, exact_out: false,
@@ -710,6 +1058,7 @@ mod tests {
mod uniswap_v3 { mod uniswap_v3 {
use super::*; use super::*;
use crate::encoding::models::SwapBuilder;
#[test] #[test]
fn test_encode_uniswap_v3() { fn test_encode_uniswap_v3() {
let fee = BigInt::from(500); let fee = BigInt::from(500);
@@ -724,13 +1073,7 @@ mod tests {
}; };
let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"); let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f");
let swap = Swap { let swap = SwapBuilder::new(usv3_pool, token_in.clone(), token_out.clone()).build();
component: usv3_pool,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: None,
};
let encoding_context = EncodingContext { let encoding_context = EncodingContext {
receiver: Bytes::from("0x0000000000000000000000000000000000000001"), receiver: Bytes::from("0x0000000000000000000000000000000000000001"),
exact_out: false, exact_out: false,
@@ -773,6 +1116,7 @@ mod tests {
mod balancer_v2 { mod balancer_v2 {
use super::*; use super::*;
use crate::encoding::models::SwapBuilder;
#[test] #[test]
fn test_encode_balancer_v2() { fn test_encode_balancer_v2() {
@@ -785,13 +1129,7 @@ mod tests {
}; };
let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
let token_out = Bytes::from("0xba100000625a3754423978a60c9317c58a424e3D"); let token_out = Bytes::from("0xba100000625a3754423978a60c9317c58a424e3D");
let swap = Swap { let swap = SwapBuilder::new(balancer_pool, token_in.clone(), token_out.clone()).build();
component: balancer_pool,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: None,
};
let encoding_context = EncodingContext { let encoding_context = EncodingContext {
// The receiver was generated with `makeAddr("bob") using forge` // The receiver was generated with `makeAddr("bob") using forge`
receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"),
@@ -838,7 +1176,7 @@ mod tests {
mod uniswap_v4 { mod uniswap_v4 {
use super::*; use super::*;
use crate::encoding::evm::utils::write_calldata_to_file; use crate::encoding::evm::utils::{ple_encode, write_calldata_to_file};
#[test] #[test]
fn test_encode_uniswap_v4_simple_swap() { fn test_encode_uniswap_v4_simple_swap() {
@@ -858,13 +1196,7 @@ mod tests {
static_attributes, static_attributes,
..Default::default() ..Default::default()
}; };
let swap = Swap { let swap = SwapBuilder::new(usv4_pool, token_in.clone(), token_out.clone()).build();
component: usv4_pool,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: None,
};
let encoding_context = EncodingContext { let encoding_context = EncodingContext {
// The receiver is ALICE to match the solidity tests // The receiver is ALICE to match the solidity tests
receiver: Bytes::from("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2"), receiver: Bytes::from("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2"),
@@ -900,6 +1232,8 @@ mod tests {
"01", "01",
// receiver // receiver
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// pool params: // pool params:
// - intermediary token // - intermediary token
"dac17f958d2ee523a2206206994597c13d831ec7", "dac17f958d2ee523a2206206994597c13d831ec7",
@@ -931,13 +1265,7 @@ mod tests {
..Default::default() ..Default::default()
}; };
let swap = Swap { let swap = SwapBuilder::new(usv4_pool, token_in.clone(), token_out.clone()).build();
component: usv4_pool,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: None,
};
let encoding_context = EncodingContext { let encoding_context = EncodingContext {
receiver: Bytes::from("0x0000000000000000000000000000000000000001"), receiver: Bytes::from("0x0000000000000000000000000000000000000001"),
@@ -1028,21 +1356,12 @@ mod tests {
..Default::default() ..Default::default()
}; };
let initial_swap = Swap { let initial_swap =
component: usde_usdt_component, SwapBuilder::new(usde_usdt_component, usde_address.clone(), usdt_address.clone())
token_in: usde_address.clone(), .build();
token_out: usdt_address.clone(), let second_swap =
split: 0f64, SwapBuilder::new(usdt_wbtc_component, usdt_address.clone(), wbtc_address.clone())
user_data: None, .build();
};
let second_swap = Swap {
component: usdt_wbtc_component,
token_in: usdt_address,
token_out: wbtc_address.clone(),
split: 0f64,
user_data: None,
};
let encoder = UniswapV4SwapEncoder::new( let encoder = UniswapV4SwapEncoder::new(
String::from("0xF62849F9A0B5Bf2913b396098F7c7019b51A820a"), String::from("0xF62849F9A0B5Bf2913b396098F7c7019b51A820a"),
@@ -1057,8 +1376,11 @@ mod tests {
.encode_swap(&second_swap, &context) .encode_swap(&second_swap, &context)
.unwrap(); .unwrap();
let combined_hex = let combined_hex = format!(
format!("{}{}", encode(&initial_encoded_swap), encode(&second_encoded_swap)); "{}{}",
encode(&initial_encoded_swap),
encode(ple_encode(vec![second_encoded_swap]))
);
assert_eq!( assert_eq!(
combined_hex, combined_hex,
@@ -1073,6 +1395,8 @@ mod tests {
"01", "01",
// receiver // receiver
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// pool params: // pool params:
// - intermediary token USDT // - intermediary token USDT
"dac17f958d2ee523a2206206994597c13d831ec7", "dac17f958d2ee523a2206206994597c13d831ec7",
@@ -1080,6 +1404,9 @@ mod tests {
"000064", "000064",
// - tick spacing // - tick spacing
"000001", "000001",
// Second swap
// ple encoding
"001a",
// - intermediary token WBTC // - intermediary token WBTC
"2260fac5e5542a773aa44fbcfedf7c193bc2c599", "2260fac5e5542a773aa44fbcfedf7c193bc2c599",
// - fee // - fee
@@ -1093,7 +1420,6 @@ mod tests {
} }
mod ekubo { mod ekubo {
use super::*; use super::*;
use crate::encoding::evm::utils::write_calldata_to_file;
const RECEIVER: &str = "ca4f73fe97d0b987a0d12b39bbd562c779bab6f6"; // Random address const RECEIVER: &str = "ca4f73fe97d0b987a0d12b39bbd562c779bab6f6"; // Random address
@@ -1113,13 +1439,7 @@ mod tests {
let component = ProtocolComponent { static_attributes, ..Default::default() }; let component = ProtocolComponent { static_attributes, ..Default::default() };
let swap = Swap { let swap = SwapBuilder::new(component, token_in.clone(), token_out.clone()).build();
component,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: None,
};
let encoding_context = EncodingContext { let encoding_context = EncodingContext {
receiver: RECEIVER.into(), receiver: RECEIVER.into(),
@@ -1172,8 +1492,8 @@ mod tests {
transfer_type: TransferType::Transfer, transfer_type: TransferType::Transfer,
}; };
let first_swap = Swap { let first_swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
static_attributes: HashMap::from([ static_attributes: HashMap::from([
("fee".to_string(), Bytes::from(0_u64)), ("fee".to_string(), Bytes::from(0_u64)),
("tick_spacing".to_string(), Bytes::from(0_u32)), ("tick_spacing".to_string(), Bytes::from(0_u32)),
@@ -1184,14 +1504,13 @@ mod tests {
]), ]),
..Default::default() ..Default::default()
}, },
token_in: group_token_in.clone(), group_token_in.clone(),
token_out: intermediary_token.clone(), intermediary_token.clone(),
split: 0f64, )
user_data: None, .build();
};
let second_swap = Swap { let second_swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
// 0.0025% fee & 0.005% base pool // 0.0025% fee & 0.005% base pool
static_attributes: HashMap::from([ static_attributes: HashMap::from([
("fee".to_string(), Bytes::from(461168601842738_u64)), ("fee".to_string(), Bytes::from(461168601842738_u64)),
@@ -1200,11 +1519,10 @@ mod tests {
]), ]),
..Default::default() ..Default::default()
}, },
token_in: intermediary_token.clone(), intermediary_token.clone(),
token_out: group_token_out.clone(), group_token_out.clone(),
split: 0f64, )
user_data: None, .build();
};
let first_encoded_swap = encoder let first_encoded_swap = encoder
.encode_swap(&first_swap, &encoding_context) .encode_swap(&first_swap, &encoding_context)
@@ -1313,18 +1631,18 @@ mod tests {
) { ) {
let mut static_attributes: HashMap<String, Bytes> = HashMap::new(); let mut static_attributes: HashMap<String, Bytes> = HashMap::new();
static_attributes.insert("coins".into(), Bytes::from_str(coins).unwrap()); static_attributes.insert("coins".into(), Bytes::from_str(coins).unwrap());
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool-id".into(), id: "pool-id".into(),
protocol_system: String::from("vm:curve"), protocol_system: String::from("vm:curve"),
static_attributes, static_attributes,
..Default::default() ..Default::default()
}, },
token_in: Bytes::from(token_in), Bytes::from(token_in),
token_out: Bytes::from(token_out), Bytes::from(token_out),
split: 0f64, )
user_data: None, .build();
};
let encoder = let encoder =
CurveSwapEncoder::new(String::default(), Chain::Ethereum, curve_config()).unwrap(); CurveSwapEncoder::new(String::default(), Chain::Ethereum, curve_config()).unwrap();
let (i, j) = encoder let (i, j) = encoder
@@ -1358,13 +1676,9 @@ mod tests {
}; };
let token_in = Bytes::from("0x6B175474E89094C44Da98b954EedeAC495271d0F"); let token_in = Bytes::from("0x6B175474E89094C44Da98b954EedeAC495271d0F");
let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let swap = Swap { let swap =
component: curve_tri_pool, SwapBuilder::new(curve_tri_pool, token_in.clone(), token_out.clone()).build();
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: None,
};
let encoding_context = EncodingContext { let encoding_context = EncodingContext {
// The receiver was generated with `makeAddr("bob") using forge` // The receiver was generated with `makeAddr("bob") using forge`
receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"),
@@ -1430,13 +1744,7 @@ mod tests {
}; };
let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let token_out = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); let token_out = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3");
let swap = Swap { let swap = SwapBuilder::new(curve_pool, token_in.clone(), token_out.clone()).build();
component: curve_pool,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: None,
};
let encoding_context = EncodingContext { let encoding_context = EncodingContext {
// The receiver was generated with `makeAddr("bob") using forge` // The receiver was generated with `makeAddr("bob") using forge`
receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"),
@@ -1503,13 +1811,7 @@ mod tests {
}; };
let token_in = Bytes::from("0x0000000000000000000000000000000000000000"); let token_in = Bytes::from("0x0000000000000000000000000000000000000000");
let token_out = Bytes::from("0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"); let token_out = Bytes::from("0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84");
let swap = Swap { let swap = SwapBuilder::new(curve_pool, token_in.clone(), token_out.clone()).build();
component: curve_pool,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: None,
};
let encoding_context = EncodingContext { let encoding_context = EncodingContext {
// The receiver was generated with `makeAddr("bob") using forge` // The receiver was generated with `makeAddr("bob") using forge`
receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"),
@@ -1577,13 +1879,7 @@ mod tests {
}; };
let token_in = Bytes::from("0x7bc3485026ac48b6cf9baf0a377477fff5703af8"); let token_in = Bytes::from("0x7bc3485026ac48b6cf9baf0a377477fff5703af8");
let token_out = Bytes::from("0xc71ea051a5f82c67adcf634c36ffe6334793d24c"); let token_out = Bytes::from("0xc71ea051a5f82c67adcf634c36ffe6334793d24c");
let swap = Swap { let swap = SwapBuilder::new(balancer_pool, token_in.clone(), token_out.clone()).build();
component: balancer_pool,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: None,
};
let encoding_context = EncodingContext { let encoding_context = EncodingContext {
// The receiver was generated with `makeAddr("bob") using forge` // The receiver was generated with `makeAddr("bob") using forge`
receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"),
@@ -1635,13 +1931,7 @@ mod tests {
}; };
let token_in = Bytes::from("0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f"); let token_in = Bytes::from("0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f");
let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let swap = Swap { let swap = SwapBuilder::new(maverick_pool, token_in.clone(), token_out.clone()).build();
component: maverick_pool,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: None,
};
let encoding_context = EncodingContext { let encoding_context = EncodingContext {
// The receiver was generated with `makeAddr("bob") using forge` // The receiver was generated with `makeAddr("bob") using forge`
receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"),
@@ -1681,4 +1971,254 @@ mod tests {
write_calldata_to_file("test_encode_maverick_v2", hex_swap.as_str()); 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<String, String> {
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<HashMap<String, String>> {
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..]);
}
}
} }

View File

@@ -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<String, Bytes>,
}
impl ProtocolSim for MockRFQState {
fn fee(&self) -> f64 {
panic!("MockRFQState does not implement fee")
}
fn spot_price(&self, _base: &Token, _quote: &Token) -> Result<f64, SimulationError> {
panic!("MockRFQState does not implement fee")
}
fn get_amount_out(
&self,
_amount_in: BigUint,
_token_in: &Token,
_token_out: &Token,
) -> Result<GetAmountOutResult, SimulationError> {
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<Bytes, Token>,
_balances: &Balances,
) -> Result<(), TransitionError<String>> {
panic!("MockRFQState does not implement fee")
}
fn clone_box(&self) -> Box<dyn ProtocolSim> {
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<SignedQuote, SimulationError> {
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(),
})
}
}

View File

@@ -14,6 +14,7 @@ use crate::encoding::{
SequentialSwapStrategyEncoder, SingleSwapStrategyEncoder, SplitSwapStrategyEncoder, SequentialSwapStrategyEncoder, SingleSwapStrategyEncoder, SplitSwapStrategyEncoder,
}, },
swap_encoder::swap_encoder_registry::SwapEncoderRegistry, swap_encoder::swap_encoder_registry::SwapEncoderRegistry,
utils::ple_encode,
}, },
models::{ models::{
EncodedSolution, EncodingContext, NativeAction, Solution, Transaction, TransferType, EncodedSolution, EncodingContext, NativeAction, Solution, Transaction, TransferType,
@@ -102,7 +103,9 @@ impl TychoRouterEncoder {
solution solution
.swaps .swaps
.iter() .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 self.single_swap_strategy
.encode_strategy(solution)? .encode_strategy(solution)?
@@ -313,32 +316,44 @@ impl TychoExecutorEncoder {
)) ))
})?; })?;
let mut grouped_protocol_data: Vec<u8> = 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<u8>> = vec![];
let mut initial_protocol_data: Vec<u8> = vec![];
for swap in grouped_swap.swaps.iter() { 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)?; 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()) let executor_address = Bytes::from_str(swap_encoder.executor_address())
.map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?; .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?;
Ok(EncodedSolution { Ok(EncodedSolution {
swaps: grouped_protocol_data, swaps: initial_protocol_data,
interacting_with: executor_address, interacting_with: executor_address,
permit: None, permit: None,
function_signature: "".to_string(), function_signature: "".to_string(),
@@ -393,7 +408,7 @@ mod tests {
use tycho_common::models::{protocol::ProtocolComponent, Chain}; use tycho_common::models::{protocol::ProtocolComponent, Chain};
use super::*; use super::*;
use crate::encoding::models::Swap; use crate::encoding::models::{Swap, SwapBuilder};
fn dai() -> Bytes { fn dai() -> Bytes {
Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap() Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap()
@@ -428,19 +443,18 @@ mod tests {
let mut static_attributes_usdc_eth: HashMap<String, Bytes> = HashMap::new(); let mut static_attributes_usdc_eth: HashMap<String, Bytes> = HashMap::new();
static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth); 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); static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth);
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d" id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d"
.to_string(), .to_string(),
protocol_system: "uniswap_v4".to_string(), protocol_system: "uniswap_v4".to_string(),
static_attributes: static_attributes_usdc_eth, static_attributes: static_attributes_usdc_eth,
..Default::default() ..Default::default()
}, },
token_in: usdc().clone(), usdc().clone(),
token_out: eth().clone(), eth().clone(),
split: 0f64, )
user_data: None, .build()
}
} }
fn swap_eth_pepe_univ4() -> Swap { fn swap_eth_pepe_univ4() -> Swap {
@@ -449,19 +463,18 @@ mod tests {
let mut static_attributes_eth_pepe: HashMap<String, Bytes> = HashMap::new(); let mut static_attributes_eth_pepe: HashMap<String, Bytes> = HashMap::new();
static_attributes_eth_pepe.insert("key_lp_fee".into(), pool_fee_eth_pepe); 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); static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe);
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9" id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9"
.to_string(), .to_string(),
protocol_system: "uniswap_v4".to_string(), protocol_system: "uniswap_v4".to_string(),
static_attributes: static_attributes_eth_pepe, static_attributes: static_attributes_eth_pepe,
..Default::default() ..Default::default()
}, },
token_in: eth().clone(), eth().clone(),
token_out: pepe().clone(), pepe().clone(),
split: 0f64, )
user_data: None, .build()
}
} }
fn router_address() -> Bytes { fn router_address() -> Bytes {
@@ -499,17 +512,16 @@ mod tests {
fn test_encode_router_calldata_single_swap() { fn test_encode_router_calldata_single_swap() {
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let eth_amount_in = BigUint::from(1000u32); let eth_amount_in = BigUint::from(1000u32);
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth(), weth().clone(),
token_out: dai(), dai().clone(),
split: 0f64, )
user_data: None, .build();
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -564,29 +576,27 @@ mod tests {
fn test_encode_router_calldata_sequential_swap() { fn test_encode_router_calldata_sequential_swap() {
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let eth_amount_in = BigUint::from(1000u32); let eth_amount_in = BigUint::from(1000u32);
let swap_weth_dai = Swap { let swap_weth_dai = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth(), weth().clone(),
token_out: dai(), dai().clone(),
split: 0f64, )
user_data: None, .build();
};
let swap_dai_usdc = Swap { let swap_dai_usdc = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai().clone(),
token_out: usdc(), usdc().clone(),
split: 0f64, )
user_data: None, .build();
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -656,17 +666,16 @@ mod tests {
#[test] #[test]
fn test_validate_passes_for_wrap() { fn test_validate_passes_for_wrap() {
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth(), weth().clone(),
token_out: dai(), dai().clone(),
split: 0f64, )
user_data: None, .build();
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -685,17 +694,16 @@ mod tests {
#[test] #[test]
fn test_validate_fails_for_wrap_wrong_input() { fn test_validate_fails_for_wrap_wrong_input() {
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth(), weth().clone(),
token_out: dai(), dai().clone(),
split: 0f64, )
user_data: None, .build();
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -719,17 +727,16 @@ mod tests {
#[test] #[test]
fn test_validate_fails_for_wrap_wrong_first_swap() { fn test_validate_fails_for_wrap_wrong_first_swap() {
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: eth(), eth().clone(),
token_out: dai(), dai().clone(),
split: 0f64, )
user_data: None, .build();
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -773,17 +780,16 @@ mod tests {
#[test] #[test]
fn test_validate_passes_for_unwrap() { fn test_validate_passes_for_unwrap() {
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai().clone(),
token_out: weth(), weth().clone(),
split: 0f64, )
user_data: None, .build();
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -801,17 +807,16 @@ mod tests {
#[test] #[test]
fn test_validate_fails_for_unwrap_wrong_output() { fn test_validate_fails_for_unwrap_wrong_output() {
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai().clone(),
token_out: weth(), weth().clone(),
split: 0f64, )
user_data: None, .build();
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -836,17 +841,16 @@ mod tests {
#[test] #[test]
fn test_validate_fails_for_unwrap_wrong_last_swap() { fn test_validate_fails_for_unwrap_wrong_last_swap() {
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai().clone(),
token_out: eth(), eth().clone(),
split: 0f64, )
user_data: None, .build();
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -876,39 +880,36 @@ mod tests {
// (some of the pool addresses in this test are fake) // (some of the pool addresses in this test are fake)
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swaps = vec![ let swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai().clone(),
token_out: weth(), weth().clone(),
split: 0.5f64, )
user_data: None, .build(),
}, SwapBuilder::new(
Swap { ProtocolComponent {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(), id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai().clone(),
token_out: weth(), weth().clone(),
split: 0f64, )
user_data: None, .build(),
}, SwapBuilder::new(
Swap { ProtocolComponent {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(), id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth(), weth().clone(),
token_out: dai(), dai().clone(),
split: 0f64, )
user_data: None, .build(),
},
]; ];
let solution = Solution { let solution = Solution {
@@ -931,50 +932,46 @@ mod tests {
// (some of the pool addresses in this test are fake) // (some of the pool addresses in this test are fake)
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swaps = vec![ let swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai().clone(),
token_out: weth(), weth().clone(),
split: 0f64, )
user_data: None, .build(),
}, SwapBuilder::new(
Swap { ProtocolComponent {
component: ProtocolComponent {
id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth(), weth().clone(),
token_out: usdc(), usdc().clone(),
split: 0f64, )
user_data: None, .build(),
}, SwapBuilder::new(
Swap { ProtocolComponent {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(), id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: usdc(), usdc().clone(),
token_out: dai(), dai().clone(),
split: 0f64, )
user_data: None, .build(),
}, SwapBuilder::new(
Swap { ProtocolComponent {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(), id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai().clone(),
token_out: wbtc(), wbtc().clone(),
split: 0f64, )
user_data: None, .build(),
},
]; ];
let solution = Solution { let solution = Solution {
@@ -1004,39 +1001,37 @@ mod tests {
// (some of the pool addresses in this test are fake) // (some of the pool addresses in this test are fake)
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swaps = vec![ let swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth(), weth(),
token_out: dai(), dai(),
split: 0f64, )
user_data: None, .build(),
}, SwapBuilder::new(
Swap { ProtocolComponent {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(), id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai(),
token_out: weth(), weth(),
split: 0.5f64, )
user_data: None, .split(0.5)
}, .build(),
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(), id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai(),
token_out: weth(), weth(),
split: 0f64, )
user_data: None, .build(),
},
]; ];
let solution = Solution { let solution = Solution {
@@ -1059,28 +1054,26 @@ mod tests {
// (some of the pool addresses in this test are fake) // (some of the pool addresses in this test are fake)
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swaps = vec![ let swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth(), weth(),
token_out: dai(), dai(),
split: 0f64, )
user_data: None, .build(),
}, SwapBuilder::new(
Swap { ProtocolComponent {
component: ProtocolComponent { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai(),
token_out: weth(), weth(),
split: 0f64, )
user_data: None, .build(),
},
]; ];
let solution = Solution { let solution = Solution {
@@ -1114,7 +1107,7 @@ mod tests {
use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use tycho_common::{models::protocol::ProtocolComponent, Bytes};
use super::*; use super::*;
use crate::encoding::models::{Solution, Swap}; use crate::encoding::models::Solution;
#[test] #[test]
fn test_executor_encoder_encode() { fn test_executor_encoder_encode() {
@@ -1124,17 +1117,16 @@ mod tests {
let token_in = weth(); let token_in = weth();
let token_out = dai(); let token_out = dai();
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: token_in.clone(), token_in.clone(),
token_out: token_out.clone(), token_out.clone(),
split: 0f64, )
user_data: None, .build();
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -1185,16 +1177,16 @@ mod tests {
let token_in = weth(); let token_in = weth();
let token_out = dai(); let token_out = dai();
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: token_in.clone(), token_in.clone(),
token_out: token_out.clone(), token_out.clone(),
split: 0f64, )
user_data: None, .build();
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -1256,12 +1248,16 @@ mod tests {
"01", "01",
// receiver // receiver
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
// hook address (not set, so zero)
"0000000000000000000000000000000000000000",
// first pool intermediary token (ETH) // first pool intermediary token (ETH)
"0000000000000000000000000000000000000000", "0000000000000000000000000000000000000000",
// fee // fee
"000bb8", "000bb8",
// tick spacing // tick spacing
"00003c", "00003c",
// ple encoding
"001a",
// second pool intermediary token (PEPE) // second pool intermediary token (PEPE)
"6982508145454ce325ddbe47a25d4ec3d2311933", "6982508145454ce325ddbe47a25d4ec3d2311933",
// fee // fee

View File

@@ -78,6 +78,9 @@ pub fn get_static_attribute(swap: &Swap, attribute_name: &str) -> Result<Vec<u8>
.to_vec()) .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<Arc<Runtime>>), EncodingError> { pub fn get_runtime() -> Result<(Handle, Option<Arc<Runtime>>), EncodingError> {
match Handle::try_current() { match Handle::try_current() {
Ok(h) => Ok((h, None)), Ok(h) => Ok((h, None)),

View File

@@ -1,7 +1,11 @@
use std::sync::Arc;
use clap::ValueEnum; use clap::ValueEnum;
use num_bigint::BigUint; use num_bigint::BigUint;
use serde::{Deserialize, Serialize}; 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; use crate::encoding::serde_primitives::biguint_string;
@@ -71,7 +75,7 @@ pub enum NativeAction {
} }
/// Represents a swap operation to be performed on a pool. /// Represents a swap operation to be performed on a pool.
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Swap { pub struct Swap {
/// Protocol component from tycho indexer /// Protocol component from tycho indexer
pub component: ProtocolComponent, pub component: ProtocolComponent,
@@ -84,6 +88,12 @@ pub struct Swap {
pub split: f64, pub split: f64,
/// Optional user data to be passed to encoding. /// Optional user data to be passed to encoding.
pub user_data: Option<Bytes>, pub user_data: Option<Bytes>,
/// Optional protocol state used to perform the swap.
#[serde(skip)]
pub protocol_state: Option<Arc<dyn ProtocolSim>>,
/// 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<BigUint>,
} }
impl Swap { impl Swap {
@@ -93,8 +103,90 @@ impl Swap {
token_out: Bytes, token_out: Bytes,
split: f64, split: f64,
user_data: Option<Bytes>, user_data: Option<Bytes>,
protocol_state: Option<Arc<dyn ProtocolSim>>,
estimated_amount_in: Option<BigUint>,
) -> Self { ) -> 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<Bytes>,
protocol_state: Option<Arc<dyn ProtocolSim>>,
estimated_amount_in: Option<BigUint>,
}
impl SwapBuilder {
pub fn new<T: Into<ProtocolComponent>>(
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<dyn ProtocolSim>) -> 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(), protocol_system: "uniswap_v2".to_string(),
}; };
let user_data = Some(Bytes::from("0x1234")); let user_data = Some(Bytes::from("0x1234"));
let swap = let swap = Swap::new(
Swap::new(component, Bytes::from("0x12"), Bytes::from("34"), 0.5, user_data.clone()); 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_in, Bytes::from("0x12"));
assert_eq!(swap.token_out, Bytes::from("0x34")); assert_eq!(swap.token_out, Bytes::from("0x34"));
assert_eq!(swap.component.protocol_system, "uniswap_v2"); assert_eq!(swap.component.protocol_system, "uniswap_v2");

View File

@@ -3,7 +3,10 @@ pub mod encoding;
use std::str::FromStr; 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_common::{models::Chain, Bytes};
use tycho_execution::encoding::{ use tycho_execution::encoding::{
evm::encoder_builders::TychoRouterEncoderBuilder, models::UserTransferType, evm::encoder_builders::TychoRouterEncoderBuilder, models::UserTransferType,
@@ -14,6 +17,14 @@ pub fn router_address() -> Bytes {
Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap() 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 { pub fn eth_chain() -> Chain {
Chain::Ethereum Chain::Ethereum
} }
@@ -46,6 +57,10 @@ pub fn usdt() -> Bytes {
Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap() Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap()
} }
pub fn ondo() -> Bytes {
Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap()
}
pub fn get_signer() -> PrivateKeySigner { pub fn get_signer() -> PrivateKeySigner {
// Set up a mock private key for signing (Alice's pk in our contract tests) // Set up a mock private key for signing (Alice's pk in our contract tests)
let private_key = let private_key =
@@ -57,10 +72,26 @@ pub fn get_signer() -> PrivateKeySigner {
pub fn get_tycho_router_encoder(user_transfer_type: UserTransferType) -> Box<dyn TychoEncoder> { pub fn get_tycho_router_encoder(user_transfer_type: UserTransferType) -> Box<dyn TychoEncoder> {
TychoRouterEncoderBuilder::new() TychoRouterEncoderBuilder::new()
.chain(tycho_common::models::Chain::Ethereum) .chain(Chain::Ethereum)
.user_transfer_type(user_transfer_type) .user_transfer_type(user_transfer_type)
.executors_file_path("config/test_executor_addresses.json".to_string()) .executors_file_path("config/test_executor_addresses.json".to_string())
.router_address(router_address()) .router_address(router_address())
.build() .build()
.expect("Failed to build encoder") .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)
}

View File

@@ -1,15 +1,19 @@
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr, sync::Arc};
use alloy::hex::encode; use alloy::hex::encode;
use num_bigint::{BigInt, BigUint}; use num_bigint::{BigInt, BigUint};
use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use tycho_common::{models::protocol::ProtocolComponent, Bytes};
use tycho_execution::encoding::{ use tycho_execution::encoding::{
evm::utils::write_calldata_to_file, evm::{
models::{Solution, Swap, UserTransferType}, testing_utils::MockRFQState,
utils::{biguint_to_u256, write_calldata_to_file},
},
models::{Solution, Swap, SwapBuilder, UserTransferType},
}; };
use crate::common::{ 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; mod common;
@@ -48,6 +52,8 @@ fn test_uniswap_v3_uniswap_v2() {
token_out: wbtc.clone(), token_out: wbtc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let swap_wbtc_usdc = Swap { let swap_wbtc_usdc = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -59,6 +65,8 @@ fn test_uniswap_v3_uniswap_v2() {
token_out: usdc.clone(), token_out: usdc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -127,6 +135,8 @@ fn test_uniswap_v3_uniswap_v3() {
token_out: wbtc.clone(), token_out: wbtc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let swap_wbtc_usdc = Swap { let swap_wbtc_usdc = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -146,6 +156,8 @@ fn test_uniswap_v3_uniswap_v3() {
token_out: usdc.clone(), token_out: usdc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -213,6 +225,8 @@ fn test_uniswap_v3_curve() {
token_out: wbtc.clone(), token_out: wbtc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let swap_wbtc_usdt = Swap { let swap_wbtc_usdt = Swap {
@@ -242,6 +256,8 @@ fn test_uniswap_v3_curve() {
token_out: usdt.clone(), token_out: usdt.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -301,6 +317,8 @@ fn test_balancer_v2_uniswap_v2() {
token_out: wbtc.clone(), token_out: wbtc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let swap_wbtc_usdc = Swap { let swap_wbtc_usdc = Swap {
@@ -313,6 +331,8 @@ fn test_balancer_v2_uniswap_v2() {
token_out: usdc.clone(), token_out: usdc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -375,6 +395,8 @@ fn test_multi_protocol() {
token_out: weth.clone(), token_out: weth.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let balancer_swap_weth_wbtc = Swap { let balancer_swap_weth_wbtc = Swap {
@@ -387,6 +409,8 @@ fn test_multi_protocol() {
token_out: wbtc.clone(), token_out: wbtc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let curve_swap_wbtc_usdt = Swap { let curve_swap_wbtc_usdt = Swap {
@@ -416,6 +440,8 @@ fn test_multi_protocol() {
token_out: usdt.clone(), token_out: usdt.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
// Ekubo // Ekubo
@@ -438,6 +464,8 @@ fn test_multi_protocol() {
token_out: usdc.clone(), token_out: usdc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
// USV4 // USV4
@@ -461,6 +489,8 @@ fn test_multi_protocol() {
token_out: eth.clone(), token_out: eth.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -533,6 +563,8 @@ fn test_uniswap_v3_balancer_v3() {
token_out: wbtc.clone(), token_out: wbtc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let swap_wbtc_qnt = Swap { let swap_wbtc_qnt = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -544,6 +576,8 @@ fn test_uniswap_v3_balancer_v3() {
token_out: qnt.clone(), token_out: qnt.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -578,3 +612,227 @@ fn test_uniswap_v3_balancer_v3() {
let hex_calldata = encode(&calldata); let hex_calldata = encode(&calldata);
write_calldata_to_file("test_uniswap_v3_balancer_v3", hex_calldata.as_str()); 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,
&eth(),
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,
&eth(),
None,
)
.unwrap()
.data;
let hex_calldata = encode(&calldata);
write_calldata_to_file("test_uniswap_v3_hashflow", hex_calldata.as_str());
}

View File

@@ -1,18 +1,22 @@
mod common; 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 num_bigint::{BigInt, BigUint};
use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use tycho_common::{models::protocol::ProtocolComponent, Bytes};
use tycho_execution::encoding::{ use tycho_execution::encoding::{
evm::utils::write_calldata_to_file, evm::{
models::{Solution, Swap, UserTransferType}, testing_utils::MockRFQState,
utils::{biguint_to_u256, write_calldata_to_file},
},
models::{Solution, Swap, SwapBuilder, UserTransferType},
}; };
use crate::common::{ use crate::common::{
encoding::encode_tycho_router_call, eth, eth_chain, get_signer, get_tycho_router_encoder, pepe, alice_address, encoding::encode_tycho_router_call, eth, eth_chain, get_signer,
usdc, weth, get_tycho_router_encoder, ondo, pepe, usdc, wbtc, weth,
}; };
#[test] #[test]
fn test_single_encoding_strategy_ekubo() { fn test_single_encoding_strategy_ekubo() {
// ETH ──(EKUBO)──> USDC // ETH ──(EKUBO)──> USDC
@@ -40,6 +44,8 @@ fn test_single_encoding_strategy_ekubo() {
token_out: token_out.clone(), token_out: token_out.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -92,6 +98,8 @@ fn test_single_encoding_strategy_maverick() {
token_out: token_out.clone(), token_out: token_out.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -156,6 +164,8 @@ fn test_single_encoding_strategy_usv4_eth_in() {
token_out: pepe.clone(), token_out: pepe.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -222,6 +232,8 @@ fn test_single_encoding_strategy_usv4_eth_out() {
token_out: eth.clone(), token_out: eth.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -296,6 +308,8 @@ fn test_single_encoding_strategy_usv4_grouped_swap() {
token_out: eth.clone(), token_out: eth.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let swap_eth_pepe = Swap { let swap_eth_pepe = Swap {
@@ -309,6 +323,8 @@ fn test_single_encoding_strategy_usv4_grouped_swap() {
token_out: pepe.clone(), token_out: pepe.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); 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!( let expected_swaps = String::from(concat!(
// length of ple encoded swaps without padding // length of ple encoded swaps without padding
"0000000000000000000000000000000000000000000000000000000000000086", "000000000000000000000000000000000000000000000000000000000000009c",
// Swap data header // Swap data header
"f62849f9a0b5bf2913b396098f7c7019b51a820a", // executor address "f62849f9a0b5bf2913b396098f7c7019b51a820a", // executor address
// Protocol data // Protocol data
@@ -366,15 +382,18 @@ fn test_single_encoding_strategy_usv4_grouped_swap() {
"00", // zero2one "00", // zero2one
"00", // transfer type TransferFrom "00", // transfer type TransferFrom
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver
"0000000000000000000000000000000000000000", // hook address
// First pool params // First pool params
"0000000000000000000000000000000000000000", // intermediary token (ETH) "0000000000000000000000000000000000000000", // intermediary token (ETH)
"000bb8", // fee "000bb8", // fee
"00003c", // tick spacing "00003c", // tick spacing
// ple encoding
"001a",
// Second pool params // Second pool params
"6982508145454ce325ddbe47a25d4ec3d2311933", // intermediary token (PEPE) "6982508145454ce325ddbe47a25d4ec3d2311933", // intermediary token (PEPE)
"0061a8", // fee "0061a8", // fee
"0001f4", // tick spacing "0001f4", // tick spacing
"0000000000000000000000000000000000000000000000000000" // padding "00000000" // padding
)); ));
let hex_calldata = encode(&calldata); let hex_calldata = encode(&calldata);
@@ -417,6 +436,8 @@ fn test_single_encoding_strategy_curve() {
token_out: token_out.clone(), token_out: token_out.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); 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(), token_out: token_out.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -537,6 +560,8 @@ fn test_single_encoding_strategy_balancer_v3() {
token_out: token_out.clone(), token_out: token_out.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -572,3 +597,256 @@ fn test_single_encoding_strategy_balancer_v3() {
let hex_calldata = encode(&calldata); let hex_calldata = encode(&calldata);
write_calldata_to_file("test_single_encoding_strategy_balancer_v3", hex_calldata.as_str()); 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,
&eth(),
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,
&eth(),
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,
&eth(),
None,
)
.unwrap()
.data;
let hex_calldata = encode(&calldata);
write_calldata_to_file("test_single_encoding_strategy_hashflow", hex_calldata.as_str());
}

View File

@@ -6,7 +6,7 @@ use num_bigint::{BigInt, BigUint};
use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use tycho_common::{models::protocol::ProtocolComponent, Bytes};
use tycho_execution::encoding::{ use tycho_execution::encoding::{
evm::utils::write_calldata_to_file, evm::utils::write_calldata_to_file,
models::{Solution, Swap, UserTransferType}, models::{NativeAction, Solution, Swap, UserTransferType},
}; };
use crate::common::{ 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 // Note: This test does not assert anything. It is only used to obtain integration
// test data for our router solidity test. // 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 // WETH ───(USV2)──> WBTC ───(USV2)──> USDC
@@ -37,6 +37,8 @@ fn test_sequential_swap_strategy_encoder() {
token_out: wbtc.clone(), token_out: wbtc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let swap_wbtc_usdc = Swap { let swap_wbtc_usdc = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -48,6 +50,8 @@ fn test_sequential_swap_strategy_encoder() {
token_out: usdc.clone(), token_out: usdc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -103,6 +107,8 @@ fn test_sequential_swap_strategy_encoder_no_permit2() {
token_out: wbtc.clone(), token_out: wbtc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let swap_wbtc_usdc = Swap { let swap_wbtc_usdc = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -114,6 +120,8 @@ fn test_sequential_swap_strategy_encoder_no_permit2() {
token_out: usdc.clone(), token_out: usdc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -215,6 +223,8 @@ fn test_sequential_strategy_cyclic_swap() {
token_out: weth.clone(), token_out: weth.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
// WETH -> USDC (Pool 2) // WETH -> USDC (Pool 2)
@@ -237,6 +247,8 @@ fn test_sequential_strategy_cyclic_swap() {
token_out: usdc.clone(), token_out: usdc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); 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); assert_eq!(hex_calldata[1224..], expected_swaps);
write_calldata_to_file("test_sequential_strategy_cyclic_swap", hex_calldata.as_str()); 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,
&eth(),
Some(get_signer()),
)
.unwrap()
.data;
let hex_calldata = encode(&calldata);
write_calldata_to_file("test_sequential_swap_strategy_encoder_unwrap", hex_calldata.as_str());
}

View File

@@ -33,6 +33,8 @@ fn test_single_swap_strategy_encoder() {
token_out: dai.clone(), token_out: dai.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -119,6 +121,8 @@ fn test_single_swap_strategy_encoder_no_permit2() {
token_out: dai.clone(), token_out: dai.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); 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(), token_out: dai.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::None); let encoder = get_tycho_router_encoder(UserTransferType::None);
@@ -284,6 +290,8 @@ fn test_single_swap_strategy_encoder_wrap() {
token_out: dai.clone(), token_out: dai.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -336,6 +344,8 @@ fn test_single_swap_strategy_encoder_unwrap() {
token_out: weth(), token_out: weth(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);

View File

@@ -42,6 +42,8 @@ fn test_split_swap_strategy_encoder() {
token_out: dai.clone(), token_out: dai.clone(),
split: 0.5f64, split: 0.5f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let swap_weth_wbtc = Swap { let swap_weth_wbtc = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -56,6 +58,8 @@ fn test_split_swap_strategy_encoder() {
// It should still be very close to 50% // It should still be very close to 50%
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let swap_dai_usdc = Swap { let swap_dai_usdc = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -67,6 +71,8 @@ fn test_split_swap_strategy_encoder() {
token_out: usdc.clone(), token_out: usdc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let swap_wbtc_usdc = Swap { let swap_wbtc_usdc = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -78,6 +84,8 @@ fn test_split_swap_strategy_encoder() {
token_out: usdc.clone(), token_out: usdc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -144,6 +152,8 @@ fn test_split_input_cyclic_swap() {
token_out: weth.clone(), token_out: weth.clone(),
split: 0.6f64, // 60% of input split: 0.6f64, // 60% of input
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
// USDC -> WETH (Pool 2) - 40% of input (remaining) // USDC -> WETH (Pool 2) - 40% of input (remaining)
@@ -166,6 +176,8 @@ fn test_split_input_cyclic_swap() {
token_out: weth.clone(), token_out: weth.clone(),
split: 0f64, split: 0f64,
user_data: None, // Remaining 40% user_data: None, // Remaining 40%
protocol_state: None,
estimated_amount_in: None,
}; };
// WETH -> USDC (Pool 2) // WETH -> USDC (Pool 2)
@@ -188,6 +200,8 @@ fn test_split_input_cyclic_swap() {
token_out: usdc.clone(), token_out: usdc.clone(),
split: 0.0f64, split: 0.0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -308,6 +322,8 @@ fn test_split_output_cyclic_swap() {
token_out: weth.clone(), token_out: weth.clone(),
split: 0.0f64, split: 0.0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let swap_weth_usdc_v3_pool1 = Swap { let swap_weth_usdc_v3_pool1 = Swap {
@@ -327,6 +343,8 @@ fn test_split_output_cyclic_swap() {
token_out: usdc.clone(), token_out: usdc.clone(),
split: 0.6f64, split: 0.6f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let swap_weth_usdc_v3_pool2 = Swap { let swap_weth_usdc_v3_pool2 = Swap {
@@ -348,6 +366,8 @@ fn test_split_output_cyclic_swap() {
token_out: usdc.clone(), token_out: usdc.clone(),
split: 0.0f64, split: 0.0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);

View File

@@ -51,6 +51,8 @@ fn test_sequential_swap_usx() {
token_out: usdc.clone(), token_out: usdc.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let swap_usdc_usdt = Swap { let swap_usdc_usdt = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -68,6 +70,8 @@ fn test_sequential_swap_usx() {
token_out: usdt.clone(), token_out: usdt.clone(),
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);