Merge pull request #1 from jincubator/cyclic-route-0.123.0
Cyclic route 0.123.0
This commit is contained in:
152
CHANGELOG.md
152
CHANGELOG.md
@@ -1,3 +1,155 @@
|
||||
## [0.123.0](https://github.com/propeller-heads/tycho-execution/compare/0.122.0...0.123.0) (2025-09-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* pin tycho-common version ([b0783fb](https://github.com/propeller-heads/tycho-execution/commit/b0783fb2c1f9e89062ba3fbb269929249afd45c7))
|
||||
|
||||
## [0.122.0](https://github.com/propeller-heads/tycho-execution/compare/0.121.0...0.122.0) (2025-08-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Bump tycho-common version ([8101ff3](https://github.com/propeller-heads/tycho-execution/commit/8101ff31c8bc64e80a50cc907823ebb5dd8b2f1f))
|
||||
|
||||
## [0.121.0](https://github.com/propeller-heads/tycho-execution/compare/0.120.0...0.121.0) (2025-08-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* rename "uniswap_v4" to "uniswap_v4_hooks" ([c1d88be](https://github.com/propeller-heads/tycho-execution/commit/c1d88be3295d9f9959a271795541e7ee4bbcfc4c))
|
||||
* Support hooks (without special calldata) ([a058177](https://github.com/propeller-heads/tycho-execution/commit/a0581773cd35fa2c34f8e305f62901e7b5f1cf71))
|
||||
* **univ4:** Pass user_data as hook_data in execution ([93678d9](https://github.com/propeller-heads/tycho-execution/commit/93678d9d1987eaa9fff8c19a6fe69e7771ed582c))
|
||||
* Update UniswapV4 executor addresses ([d7e377e](https://github.com/propeller-heads/tycho-execution/commit/d7e377e44323014661b21e79197eaf1ad7748587))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* access hooks attr, not hook ([2c38cf8](https://github.com/propeller-heads/tycho-execution/commit/2c38cf84fcbfd195112a11b770e3f70328c1d1b2))
|
||||
* Add missing uniswap v4 hooks executor addresses ([0daccfe](https://github.com/propeller-heads/tycho-execution/commit/0daccfea9ab85fcf967c593f684a75fca7f0061e))
|
||||
* Add missing uniswap v4 hooks executor addresses ([5ab9e99](https://github.com/propeller-heads/tycho-execution/commit/5ab9e9925ab6855a05226bfac9fd5723c7ca1cea))
|
||||
* After rebase fixes ([1d952b1](https://github.com/propeller-heads/tycho-execution/commit/1d952b19be226475cd7451bad899f4bbe6a4c52e))
|
||||
* Recalculate calldata after merging main ([2adcb89](https://github.com/propeller-heads/tycho-execution/commit/2adcb89e6d3666bf76f223e55bb9d8e8b6db06ee))
|
||||
* revert hooks rename ([ea9a1a8](https://github.com/propeller-heads/tycho-execution/commit/ea9a1a88ca3dae3f9099dd0b84cff456d4ad5bfc))
|
||||
* Small safe wallet fixes ([c9d09e9](https://github.com/propeller-heads/tycho-execution/commit/c9d09e91098f0a76c96ad1cd157e1a461887f468))
|
||||
|
||||
## [0.120.0](https://github.com/propeller-heads/tycho-execution/compare/0.119.0...0.120.0) (2025-08-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Deploy new hashflow router ([eac45fa](https://github.com/propeller-heads/tycho-execution/commit/eac45faf5a56a50dacff374da7c3701e5a1d3f33))
|
||||
|
||||
## [0.119.0](https://github.com/propeller-heads/tycho-execution/compare/0.118.0...0.119.0) (2025-08-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add BytecodeExporter to TestUtils ([cea964e](https://github.com/propeller-heads/tycho-execution/commit/cea964e0a54a75b1bd89007794daf467b812f021))
|
||||
|
||||
## [0.118.0](https://github.com/propeller-heads/tycho-execution/compare/0.117.0...0.118.0) (2025-08-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Use Arc instead of & for protocol_state in Swap ([a2a5319](https://github.com/propeller-heads/tycho-execution/commit/a2a53195edbc631bea8fbc758a3bf5ee33a87dda))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Updates to new datatype after merging main ([e9c1a39](https://github.com/propeller-heads/tycho-execution/commit/e9c1a39608bba786bbed2b53c4596e94ddcbe03c))
|
||||
|
||||
## [0.117.0](https://github.com/propeller-heads/tycho-execution/compare/0.116.0...0.117.0) (2025-08-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* implement `SwapEncoder` for `Hashflow` ([93db953](https://github.com/propeller-heads/tycho-execution/commit/93db953c620f4d52e8852ff8148f2dfdbc580029))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* in bebop's encode_swap, fail early if router address is not present ([c013bf7](https://github.com/propeller-heads/tycho-execution/commit/c013bf707225b171e76f3f3dac88624b6a3458dc))
|
||||
* in hashflow's encode_swap, fail early if router address is not present ([c506f2c](https://github.com/propeller-heads/tycho-execution/commit/c506f2c048a20d1ffd066d2903d2cc469fd167ed))
|
||||
* in HashflowExecutor, _balanceOf must use `trader` address instead of the executor's to get the balance ([a09d648](https://github.com/propeller-heads/tycho-execution/commit/a09d648f3c50f87b392c7c5eb14af07307c5ccea))
|
||||
|
||||
## [0.116.0](https://github.com/propeller-heads/tycho-execution/compare/0.115.0...0.116.0) (2025-08-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Bebop executor in base ([c974312](https://github.com/propeller-heads/tycho-execution/commit/c974312a377d950948e76dd1dbb3303421f9f5f8))
|
||||
* Support Bebop quote through IndicativelyPriced state ([2292061](https://github.com/propeller-heads/tycho-execution/commit/22920617ebca35199a8cc927245cfa7e6209ade8))
|
||||
* Update BebopExecutor deployed address ([72b60aa](https://github.com/propeller-heads/tycho-execution/commit/72b60aa324d136ab999ff04339433a83743aee5e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add bebop to protocol specific addresses ([6e68d19](https://github.com/propeller-heads/tycho-execution/commit/6e68d199c90395a9204329e462e5027e81e61171))
|
||||
* Implement From<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)
|
||||
|
||||
|
||||
|
||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -4634,9 +4634,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tycho-common"
|
||||
version = "0.76.0"
|
||||
version = "0.82.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e72ed04dc9d41942c886fc3d58af5b0e23a3e4783ac7294ed7cea61e022a5c4d"
|
||||
checksum = "096c87ebe011785fcd7ed59ec501ac12b465a64fbd2914b8c0c57125c253682b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -4659,9 +4659,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tycho-execution"
|
||||
version = "0.111.0"
|
||||
version = "0.123.0"
|
||||
dependencies = [
|
||||
"alloy",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"clap",
|
||||
"dotenv",
|
||||
@@ -4676,6 +4677,7 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tycho-common",
|
||||
"tycho-execution",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
20
Cargo.toml
20
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tycho-execution"
|
||||
version = "0.111.0"
|
||||
version = "0.123.0"
|
||||
edition = "2021"
|
||||
description = "Provides tools for encoding and executing swaps against Tycho router and protocol executors."
|
||||
repository = "https://github.com/propeller-heads/tycho-execution"
|
||||
@@ -11,12 +11,12 @@ license = "MIT"
|
||||
categories = ["finance", "cryptography::cryptocurrencies"]
|
||||
readme = "README.md"
|
||||
exclude = [
|
||||
"foundry/*",
|
||||
"foundry",
|
||||
"tests/*",
|
||||
"tests/common",
|
||||
".github/*",
|
||||
".gitmodules",
|
||||
"foundry/*",
|
||||
"foundry",
|
||||
"tests/*",
|
||||
"tests/common",
|
||||
".github/*",
|
||||
".gitmodules",
|
||||
]
|
||||
|
||||
[[bin]]
|
||||
@@ -37,17 +37,19 @@ tokio = { version = "1.38.0", features = ["full"] }
|
||||
chrono = "0.4.39"
|
||||
clap = { version = "4.5.3", features = ["derive"] }
|
||||
once_cell = "1.20.2"
|
||||
tycho-common = ">0.75.1"
|
||||
|
||||
tycho-common = "0.82.0"
|
||||
alloy = { version = "1.0.6", features = ["providers", "rpc-types-eth", "eip712", "signer-local", "node-bindings"], optional = true }
|
||||
async-trait = { version = "0.1.88", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.24.0"
|
||||
tycho-execution = { path = "../tycho-execution", features = ["test-utils"] }
|
||||
|
||||
[features]
|
||||
default = ["evm"]
|
||||
evm = ["alloy"]
|
||||
fork-tests = []
|
||||
test-utils = ["async-trait"]
|
||||
|
||||
[profile.bench]
|
||||
debug = true
|
||||
@@ -5,21 +5,27 @@
|
||||
"pancakeswap_v2": "0xC9db3FEB380E4fd9af239e2595ECdEcE3b5c34A4",
|
||||
"uniswap_v3": "0xbab7124C9662B15C6b9AF0b1f329907dD55a24FC",
|
||||
"pancakeswap_v3": "0x9D32e9F569B22Ae8d8C6f788037C1CD53632A059",
|
||||
"uniswap_v4": "0xD11496EAb53A9521f0bC1e5c1098Ecb467103Ad9",
|
||||
"uniswap_v4": "0x2C2EaB81Cf983602153E67b1890164BC4CABC6ed",
|
||||
"uniswap_v4_hooks": "0x2C2EaB81Cf983602153E67b1890164BC4CABC6ed",
|
||||
"vm:balancer_v2": "0xB5b8dc3F0a1Be99685a0DEd015Af93bFBB55C411",
|
||||
"ekubo_v2": "0x263DD7AD20983b5E0392bf1F09C4493500EDb333",
|
||||
"vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165",
|
||||
"vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B",
|
||||
"vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec"
|
||||
"vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec",
|
||||
"rfq:bebop": "0xFE42BFb115eD9671011cA52BDD23A52A2e077a7c",
|
||||
"rfq:hashflow": "0x19e49Db786c87F4e46B10aFb21c0C06d34270f98"
|
||||
},
|
||||
"base": {
|
||||
"uniswap_v2": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700",
|
||||
"uniswap_v3": "0x647bffbf8bd72bf6341ecba8b0279e090313a40d",
|
||||
"uniswap_v4": "0xb5628b97f491f85766673ac4d5d47bb1af7fcc4a"
|
||||
"uniswap_v4": "0x7Dfa502736C7bd84DA1402F7524214215BC9534d",
|
||||
"uniswap_v4_hooks": "0x7Dfa502736C7bd84DA1402F7524214215BC9534d",
|
||||
"rfq:bebop": "0x489A3f531dA3873D6585BF3f8E0dEE48CAC6F7BC"
|
||||
},
|
||||
"unichain": {
|
||||
"uniswap_v2": "0x00C1b81e3C8f6347E69e2DDb90454798A6Be975E",
|
||||
"uniswap_v3": "0xD26A838A41af3d4815DfD745a080B2062c4124d1",
|
||||
"uniswap_v4": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700"
|
||||
"uniswap_v4": "0x647bfFbF8Bd72bF6341ECBa8B0279e090313A40D",
|
||||
"uniswap_v4_hooks": "0x647bfFbF8Bd72bF6341ECBa8B0279e090313A40D"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,20 @@
|
||||
},
|
||||
"vm:curve": {
|
||||
"native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
||||
},
|
||||
"rfq:bebop": {
|
||||
"bebop_settlement_address": "0xbbbbbBB520d69a9775E85b458C58c648259FAD5F",
|
||||
"native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
||||
},
|
||||
"rfq:hashflow": {
|
||||
"hashflow_router_address": "0x55084eE0fEf03f14a305cd24286359A35D735151"
|
||||
}
|
||||
},
|
||||
"base": {
|
||||
"rfq:bebop": {
|
||||
"bebop_settlement_address": "0xbbbbbBB520d69a9775E85b458C58c648259FAD5F",
|
||||
"native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
||||
}
|
||||
},
|
||||
"base": {},
|
||||
"unichain": {}
|
||||
}
|
||||
@@ -6,10 +6,13 @@
|
||||
"uniswap_v3": "0x2e234DAe75C793f67A35089C9d99245E1C58470b",
|
||||
"pancakeswap_v3": "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9",
|
||||
"uniswap_v4": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a",
|
||||
"uniswap_v4_hooks": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a",
|
||||
"vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1",
|
||||
"ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598",
|
||||
"vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211",
|
||||
"vm:maverick_v2": "0xA4AD4f68d0b91CFD19687c881e50f3A00242828c",
|
||||
"vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb"
|
||||
"vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb",
|
||||
"rfq:bebop": "0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF",
|
||||
"rfq:hashflow": "0x15cF58144EF33af1e14b5208015d11F9143E27b9"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
docs/audits/Max_2025-08-05.pdf
Normal file
BIN
docs/audits/Max_2025-08-05.pdf
Normal file
Binary file not shown.
@@ -7,7 +7,7 @@ use tycho_common::{
|
||||
};
|
||||
use tycho_execution::encoding::{
|
||||
evm::encoder_builders::TychoRouterEncoderBuilder,
|
||||
models::{Solution, Swap, UserTransferType},
|
||||
models::{Solution, Swap, SwapBuilder, UserTransferType},
|
||||
};
|
||||
|
||||
fn main() {
|
||||
@@ -43,6 +43,8 @@ fn main() {
|
||||
// the amount or the total remaining balance.
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
// Then we create a solution object with the previous swap
|
||||
@@ -86,52 +88,51 @@ fn main() {
|
||||
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f")
|
||||
.expect("Failed to create DAI address");
|
||||
|
||||
let swap_weth_dai = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap_weth_dai = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0.5f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap_weth_wbtc = Swap {
|
||||
component: ProtocolComponent {
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.split(0.5)
|
||||
.build();
|
||||
|
||||
// Split 0 represents the remaining 50%, but to avoid any rounding errors we set this to
|
||||
// 0 to signify "the remainder of the WETH value". It should still be very close to 50%
|
||||
let swap_weth_wbtc = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: wbtc.clone(),
|
||||
// This represents the remaining 50%, but to avoid any rounding errors we set this to
|
||||
// 0 to signify "the remainder of the WETH value". It should still be very close to 50%
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap_dai_usdc = Swap {
|
||||
component: ProtocolComponent {
|
||||
weth.clone(),
|
||||
wbtc.clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let swap_dai_usdc = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai.clone(),
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap_wbtc_usdc = Swap {
|
||||
component: ProtocolComponent {
|
||||
dai.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.build();
|
||||
let swap_wbtc_usdc = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: wbtc.clone(),
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
wbtc.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.build();
|
||||
let mut complex_solution = solution.clone();
|
||||
complex_solution.swaps = vec![swap_weth_dai, swap_weth_wbtc, swap_dai_usdc, swap_wbtc_usdc];
|
||||
|
||||
|
||||
@@ -82,6 +82,8 @@ fn main() {
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let swap_usdc_usdt = Swap {
|
||||
component: ProtocolComponent {
|
||||
@@ -99,6 +101,8 @@ fn main() {
|
||||
token_out: usdt.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
// Then we create a solution object with the previous swap
|
||||
@@ -165,7 +169,6 @@ fn main() {
|
||||
println!(" ====== Simple swap DAI -> USDT ======");
|
||||
println!(
|
||||
"The following callback data should be sent to the filler contract, along with the \
|
||||
encoded order and signature: {:?}",
|
||||
hex_calldata
|
||||
encoded order and signature: {hex_calldata:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ evm_version = 'cancun'
|
||||
optimizer = true
|
||||
optimizer_runs = 200
|
||||
via_ir = true
|
||||
fs_permissions = [{ access = "read", path = "./test/assets" }]
|
||||
fs_permissions = [{ access = "read", path = "./test/assets" }, { access = "write", path = "./test" }]
|
||||
|
||||
[profile.production]
|
||||
src = 'src'
|
||||
|
||||
@@ -65,7 +65,7 @@ For each of the following, you must select one of `tenderly_ethereum`, `tenderly
|
||||
1. If the wallet that has the role, is a Gnosis Safe, you need to set the `SAFE_ADDRESS` env var.
|
||||
2. The scripts deploy-executors, remove-executor, set-roles and revoke-role all support this.
|
||||
1. If `SAFE_ADDRESS` is set, then it will propose a transaction to the safe wallet and later on it needs to be
|
||||
approved in their UI to execute on chain.
|
||||
approved in their UI to execute on chain. Be sure to change the PRIVATE_KEY to that which has permissions on the safe wallet.
|
||||
2. If it's not set, it will submit the transaction directly to the chain.
|
||||
|
||||
## Deploy Uniswap X filler
|
||||
|
||||
@@ -81,6 +81,16 @@ const executors_to_deploy = {
|
||||
},
|
||||
// Args: Permit2
|
||||
{exchange: "BalancerV3Executor", args: ["0x000000000022D473030F116dDEE9F6B43aC78BA3"]},
|
||||
// Args: Bebop Settlement contract, Permit2
|
||||
{
|
||||
exchange: "BebopExecutor",
|
||||
args: ["0xbbbbbBB520d69a9775E85b458C58c648259FAD5F", "0x000000000022D473030F116dDEE9F6B43aC78BA3"]
|
||||
},
|
||||
// Args: Hashflow router, Permit2
|
||||
{
|
||||
exchange: "HashflowExecutor",
|
||||
args: ["0x55084eE0fEf03f14a305cd24286359A35D735151", "0x000000000022D473030F116dDEE9F6B43aC78BA3"]
|
||||
},
|
||||
],
|
||||
"base": [
|
||||
// Args: Factory, Pool Init Code Hash, Permit2, Fee BPS
|
||||
@@ -116,6 +126,11 @@ const executors_to_deploy = {
|
||||
"0x000000000022D473030F116dDEE9F6B43aC78BA3"
|
||||
]
|
||||
},
|
||||
// Args: Bebop Settlement contract, Permit2
|
||||
{
|
||||
exchange: "BebopExecutor",
|
||||
args: ["0xbbbbbBB520d69a9775E85b458C58c648259FAD5F", "0x000000000022D473030F116dDEE9F6B43aC78BA3"]
|
||||
},
|
||||
],
|
||||
"unichain": [
|
||||
// Args: Factory, Pool Init Code Hash, Permit2, Fee BPS
|
||||
|
||||
@@ -4,7 +4,7 @@ const {EthersAdapter} = require('@safe-global/protocol-kit');
|
||||
const {default: SafeApiKit} = require("@safe-global/api-kit");
|
||||
|
||||
const txServiceUrls = {
|
||||
mainnet: "https://safe-transaction-mainnet.safe.global",
|
||||
ethereum: "https://safe-transaction-mainnet.safe.global",
|
||||
base: "https://safe-transaction-base.safe.global",
|
||||
unichain: "https://safe-transaction-unichain.safe.global",
|
||||
};
|
||||
|
||||
179
foundry/src/executors/BebopExecutor.sol
Normal file
179
foundry/src/executors/BebopExecutor.sol
Normal 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
|
||||
}
|
||||
}
|
||||
130
foundry/src/executors/HashflowExecutor.sol
Normal file
130
foundry/src/executors/HashflowExecutor.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,13 @@ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
|
||||
import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
|
||||
import {IUnlockCallback} from
|
||||
"@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
|
||||
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
|
||||
import {SafeCast as V4SafeCast} from
|
||||
"@uniswap/v4-core/src/libraries/SafeCast.sol";
|
||||
import {TransientStateLibrary} from
|
||||
"@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
|
||||
import "../RestrictTransferFrom.sol";
|
||||
import "@openzeppelin/contracts/utils/Address.sol";
|
||||
import "../../lib/bytes/LibPrefixLengthEncodedByteArray.sol";
|
||||
|
||||
error UniswapV4Executor__InvalidDataLength();
|
||||
error UniswapV4Executor__NotPoolManager();
|
||||
@@ -43,8 +45,9 @@ contract UniswapV4Executor is
|
||||
{
|
||||
using SafeERC20 for IERC20;
|
||||
using CurrencyLibrary for Currency;
|
||||
using SafeCast for *;
|
||||
using V4SafeCast for *;
|
||||
using TransientStateLibrary for IPoolManager;
|
||||
using LibPrefixLengthEncodedByteArray for bytes;
|
||||
|
||||
IPoolManager public immutable poolManager;
|
||||
address private immutable _self;
|
||||
@@ -86,6 +89,8 @@ contract UniswapV4Executor is
|
||||
bool zeroForOne,
|
||||
TransferType transferType,
|
||||
address receiver,
|
||||
address hook,
|
||||
bytes memory hookData,
|
||||
UniswapV4Executor.UniswapV4Pool[] memory pools
|
||||
) = _decodeData(data);
|
||||
bytes memory swapData;
|
||||
@@ -95,7 +100,7 @@ contract UniswapV4Executor is
|
||||
currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn),
|
||||
fee: pools[0].fee,
|
||||
tickSpacing: pools[0].tickSpacing,
|
||||
hooks: IHooks(address(0))
|
||||
hooks: IHooks(hook)
|
||||
});
|
||||
swapData = abi.encodeWithSelector(
|
||||
this.swapExactInputSingle.selector,
|
||||
@@ -104,7 +109,7 @@ contract UniswapV4Executor is
|
||||
amountIn,
|
||||
transferType,
|
||||
receiver,
|
||||
bytes("")
|
||||
hookData
|
||||
);
|
||||
} else {
|
||||
PathKey[] memory path = new PathKey[](pools.length);
|
||||
@@ -113,8 +118,8 @@ contract UniswapV4Executor is
|
||||
intermediateCurrency: Currency.wrap(pools[i].intermediaryToken),
|
||||
fee: pools[i].fee,
|
||||
tickSpacing: pools[i].tickSpacing,
|
||||
hooks: IHooks(address(0)),
|
||||
hookData: bytes("")
|
||||
hooks: IHooks(hook),
|
||||
hookData: hookData
|
||||
});
|
||||
}
|
||||
|
||||
@@ -144,10 +149,12 @@ contract UniswapV4Executor is
|
||||
bool zeroForOne,
|
||||
TransferType transferType,
|
||||
address receiver,
|
||||
address hook,
|
||||
bytes memory hookData,
|
||||
UniswapV4Pool[] memory pools
|
||||
)
|
||||
{
|
||||
if (data.length < 88) {
|
||||
if (data.length < 108) {
|
||||
revert UniswapV4Executor__InvalidDataLength();
|
||||
}
|
||||
|
||||
@@ -156,25 +163,42 @@ contract UniswapV4Executor is
|
||||
zeroForOne = data[40] != 0;
|
||||
transferType = TransferType(uint8(data[41]));
|
||||
receiver = address(bytes20(data[42:62]));
|
||||
hook = address(bytes20(data[62:82]));
|
||||
|
||||
uint256 poolsLength = (data.length - 62) / 26; // 26 bytes per pool object
|
||||
pools = new UniswapV4Pool[](poolsLength);
|
||||
bytes memory poolsData = data[62:];
|
||||
uint256 offset = 0;
|
||||
for (uint256 i = 0; i < poolsLength; i++) {
|
||||
bytes calldata remaining = data[82:];
|
||||
address firstToken = address(bytes20(remaining[0:20]));
|
||||
uint24 firstFee = uint24(bytes3(remaining[20:23]));
|
||||
int24 firstTickSpacing = int24(uint24(bytes3(remaining[23:26])));
|
||||
UniswapV4Pool memory firstPool =
|
||||
UniswapV4Pool(firstToken, firstFee, firstTickSpacing);
|
||||
|
||||
// Remaining after first pool are ple encoded
|
||||
bytes[] memory encodedPools =
|
||||
LibPrefixLengthEncodedByteArray.toArray(remaining[26:]);
|
||||
|
||||
pools = new UniswapV4Pool[](1 + encodedPools.length);
|
||||
pools[0] = firstPool;
|
||||
|
||||
uint256 encodedPoolsLength = 26;
|
||||
uint256 plePoolsTotalLength;
|
||||
|
||||
for (uint256 i = 0; i < encodedPools.length; i++) {
|
||||
bytes memory poolsData = encodedPools[i];
|
||||
address intermediaryToken;
|
||||
uint24 fee;
|
||||
int24 tickSpacing;
|
||||
|
||||
// slither-disable-next-line assembly
|
||||
assembly {
|
||||
intermediaryToken := mload(add(poolsData, add(offset, 20)))
|
||||
fee := shr(232, mload(add(poolsData, add(offset, 52))))
|
||||
tickSpacing := shr(232, mload(add(poolsData, add(offset, 55))))
|
||||
intermediaryToken := mload(add(poolsData, add(0, 20)))
|
||||
fee := shr(232, mload(add(poolsData, add(0, 52))))
|
||||
tickSpacing := shr(232, mload(add(poolsData, add(0, 55))))
|
||||
}
|
||||
pools[i] = UniswapV4Pool(intermediaryToken, fee, tickSpacing);
|
||||
offset += 26;
|
||||
pools[i + 1] = UniswapV4Pool(intermediaryToken, fee, tickSpacing);
|
||||
plePoolsTotalLength += 2 + encodedPoolsLength; // 2 bytes prefix + data
|
||||
}
|
||||
|
||||
hookData = remaining[26 + plePoolsTotalLength:];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,10 +60,9 @@ contract UniswapXFiller is AccessControl, IReactorCallback {
|
||||
ResolvedOrder[] calldata resolvedOrders,
|
||||
bytes calldata callbackData
|
||||
) external onlyRole(REACTOR_ROLE) {
|
||||
require(
|
||||
resolvedOrders.length == 1,
|
||||
UniswapXFiller__BatchExecutionNotSupported()
|
||||
);
|
||||
if (resolvedOrders.length != 1) {
|
||||
revert UniswapXFiller__BatchExecutionNotSupported();
|
||||
}
|
||||
|
||||
ResolvedOrder memory order = resolvedOrders[0];
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ contract Constants is Test, BaseConstants {
|
||||
address WTAO_ADDR = address(0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44);
|
||||
address BSGG_ADDR = address(0xdA16Cf041E2780618c49Dbae5d734B89a6Bac9b3);
|
||||
address GHO_ADDR = address(0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f);
|
||||
address ONDO_ADDR = address(0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3);
|
||||
|
||||
// Maverick v2
|
||||
address MAVERICK_V2_FACTORY = 0x0A7e848Aca42d879EF06507Fca0E7b33A0a63c1e;
|
||||
@@ -124,6 +125,12 @@ contract Constants is Test, BaseConstants {
|
||||
// Permit2
|
||||
address PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
|
||||
|
||||
// Bebop Settlement
|
||||
address BEBOP_SETTLEMENT = 0xbbbbbBB520d69a9775E85b458C58c648259FAD5F;
|
||||
|
||||
// Hashflow Router
|
||||
address HASHFLOW_ROUTER = 0x55084eE0fEf03f14a305cd24286359A35D735151;
|
||||
|
||||
// Pool Code Init Hashes
|
||||
bytes32 USV2_POOL_CODE_INIT_HASH =
|
||||
0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f;
|
||||
|
||||
@@ -26,4 +26,26 @@ contract TestUtils is Test {
|
||||
|
||||
revert("Test calldata not found");
|
||||
}
|
||||
|
||||
/// @notice Export the runtime bytecode of a deployed contract to a JSON file.
|
||||
/// @dev
|
||||
/// This function captures the runtime bytecode (including immutables) of the deployed
|
||||
/// contract at `contractAddr` and writes it to a JSON file under `test/<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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ pragma solidity ^0.8.26;
|
||||
|
||||
import "./TychoRouterTestSetup.sol";
|
||||
import "./protocols/UniswapV4Utils.sol";
|
||||
import "@src/executors/BebopExecutor.sol";
|
||||
|
||||
contract TychoRouterTestProtocolIntegration is TychoRouterTestSetup {
|
||||
function testMultiProtocolIntegration() public {
|
||||
|
||||
@@ -492,4 +492,95 @@ contract TychoRouterSequentialSwapTest is TychoRouterTestSetup {
|
||||
assertEq(balanceAfter - balanceBefore, 1949668893);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
|
||||
function testSequentialSwapWithUnwrapIntegration() public {
|
||||
// Performs a sequential swap from USDC to ETH through WBTC using USV2 pools and unwrapping in
|
||||
// the end
|
||||
deal(USDC_ADDR, ALICE, 3_000_000_000);
|
||||
uint256 balanceBefore = ALICE.balance;
|
||||
|
||||
// Approve permit2
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
|
||||
bytes memory callData =
|
||||
loadCallDataFromFile("test_sequential_swap_strategy_encoder_unwrap");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = ALICE.balance;
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, 1404194006633772805);
|
||||
}
|
||||
}
|
||||
|
||||
contract TychoRouterSequentialSwapTestForBebop is TychoRouterTestSetup {
|
||||
function getForkBlock() public pure override returns (uint256) {
|
||||
return 23139046;
|
||||
}
|
||||
|
||||
function testUSV3BebopIntegration() public {
|
||||
// Performs a sequential swap from WETH to WBTC through USDC using USV3 and Bebop RFQ
|
||||
//
|
||||
// WETH ──(USV3)──> USDC ───(Bebop RFQ)──> WBTC
|
||||
|
||||
// The Bebop order expects:
|
||||
// - 2021750881 USDC input -> 1672307 WBTC output
|
||||
|
||||
uint256 amountIn = 1 ether;
|
||||
uint256 expectedAmountOut = 1672307;
|
||||
deal(WETH_ADDR, BOB, amountIn);
|
||||
uint256 balanceBefore = IERC20(WBTC_ADDR).balanceOf(BOB);
|
||||
|
||||
vm.startPrank(BOB);
|
||||
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
|
||||
bytes memory callData = loadCallDataFromFile("test_uniswap_v3_bebop");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = IERC20(WBTC_ADDR).balanceOf(BOB);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, expectedAmountOut);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
}
|
||||
|
||||
contract TychoRouterSequentialSwapTestForHashflow is TychoRouterTestSetup {
|
||||
function getForkBlock() public pure override returns (uint256) {
|
||||
return 23175437;
|
||||
}
|
||||
|
||||
function testUSV3HashflowIntegration() public {
|
||||
// Performs a sequential swap from WETH to WBTC through USDC using USV3 and Hashflow RFQ
|
||||
//
|
||||
// WETH ──(USV3)──> USDC ───(Hashflow RFQ)──> WBTC
|
||||
|
||||
// The Uniswap pool outputs:
|
||||
// - 1 weth -> 4322430557 USDC
|
||||
// The Hashflow tradeRFQT call expects:
|
||||
// - 4308094737 USDC input -> 3714751 WBTC output
|
||||
// The difference in USDC (14335820) will stay in the TychoRouter contract
|
||||
|
||||
uint256 amountIn = 1 ether;
|
||||
uint256 expectedAmountOut = 3714751;
|
||||
deal(WETH_ADDR, ALICE, amountIn);
|
||||
uint256 balanceBefore = IERC20(WBTC_ADDR).balanceOf(ALICE);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
|
||||
bytes memory callData = loadCallDataFromFile("test_uniswap_v3_hashflow");
|
||||
(bool success,) = tychoRouterAddr.call(callData);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 balanceAfter = IERC20(WBTC_ADDR).balanceOf(ALICE);
|
||||
|
||||
assertTrue(success, "Call Failed");
|
||||
assertEq(balanceAfter - balanceBefore, expectedAmountOut);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 14335820);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ pragma solidity ^0.8.26;
|
||||
// Executors
|
||||
import {BalancerV2Executor} from "../src/executors/BalancerV2Executor.sol";
|
||||
import {BalancerV3Executor} from "../src/executors/BalancerV3Executor.sol";
|
||||
import {BebopExecutor} from "../src/executors/BebopExecutor.sol";
|
||||
import {CurveExecutor} from "../src/executors/CurveExecutor.sol";
|
||||
import {EkuboExecutor} from "../src/executors/EkuboExecutor.sol";
|
||||
import {HashflowExecutor} from "../src/executors/HashflowExecutor.sol";
|
||||
import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol";
|
||||
import {UniswapV2Executor} from "../src/executors/UniswapV2Executor.sol";
|
||||
import {
|
||||
@@ -73,12 +75,14 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
|
||||
CurveExecutor public curveExecutor;
|
||||
MaverickV2Executor public maverickv2Executor;
|
||||
BalancerV3Executor public balancerV3Executor;
|
||||
BebopExecutor public bebopExecutor;
|
||||
HashflowExecutor public hashflowExecutor;
|
||||
|
||||
function getForkBlock() public view virtual returns (uint256) {
|
||||
return 22082754;
|
||||
}
|
||||
|
||||
function setUp() public {
|
||||
function setUp() public virtual {
|
||||
uint256 forkBlock = getForkBlock();
|
||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||
|
||||
@@ -132,8 +136,11 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
|
||||
maverickv2Executor =
|
||||
new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS);
|
||||
balancerV3Executor = new BalancerV3Executor(PERMIT2_ADDRESS);
|
||||
bebopExecutor = new BebopExecutor(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
|
||||
hashflowExecutor =
|
||||
new HashflowExecutor(HASHFLOW_ROUTER, PERMIT2_ADDRESS);
|
||||
|
||||
address[] memory executors = new address[](9);
|
||||
address[] memory executors = new address[](11);
|
||||
executors[0] = address(usv2Executor);
|
||||
executors[1] = address(usv3Executor);
|
||||
executors[2] = address(pancakev3Executor);
|
||||
@@ -143,6 +150,8 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
|
||||
executors[6] = address(curveExecutor);
|
||||
executors[7] = address(maverickv2Executor);
|
||||
executors[8] = address(balancerV3Executor);
|
||||
executors[9] = address(bebopExecutor);
|
||||
executors[10] = address(hashflowExecutor);
|
||||
|
||||
return executors;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -134,4 +134,9 @@ contract BalancerV2ExecutorTest is Constants, TestUtils {
|
||||
assertGt(balanceAfter, balanceBefore);
|
||||
assertEq(balanceAfter - balanceBefore, amountOut);
|
||||
}
|
||||
|
||||
function testExportContract() public {
|
||||
vm.skip(true);
|
||||
exportRuntimeBytecode(address(balancerV2Exposed), "BalancerV2");
|
||||
}
|
||||
}
|
||||
|
||||
470
foundry/test/protocols/Bebop.t.sol
Normal file
470
foundry/test/protocols/Bebop.t.sol
Normal 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();
|
||||
}
|
||||
}
|
||||
316
foundry/test/protocols/Hashflow.t.sol
Normal file
316
foundry/test/protocols/Hashflow.t.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor {
|
||||
bool zeroForOne,
|
||||
RestrictTransferFrom.TransferType transferType,
|
||||
address receiver,
|
||||
address hook,
|
||||
bytes memory hookData,
|
||||
UniswapV4Pool[] memory pools
|
||||
)
|
||||
{
|
||||
@@ -37,10 +39,12 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
|
||||
UniswapV4ExecutorExposed uniswapV4Exposed;
|
||||
IERC20 USDE = IERC20(USDE_ADDR);
|
||||
IERC20 USDT = IERC20(USDT_ADDR);
|
||||
IERC20 USDC = IERC20(USDC_ADDR);
|
||||
|
||||
address poolManager = 0x000000000004444c5dc75cB358380D2e3dE08A90;
|
||||
|
||||
function setUp() public {
|
||||
uint256 forkBlock = 21817316;
|
||||
uint256 forkBlock = 22689128;
|
||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||
uniswapV4Exposed = new UniswapV4ExecutorExposed(
|
||||
IPoolManager(poolManager), PERMIT2_ADDRESS
|
||||
@@ -73,6 +77,8 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
|
||||
zeroForOne,
|
||||
RestrictTransferFrom.TransferType.Transfer,
|
||||
ALICE,
|
||||
address(0),
|
||||
bytes(""),
|
||||
pools
|
||||
);
|
||||
|
||||
@@ -82,6 +88,8 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
|
||||
bool zeroForOneDecoded,
|
||||
RestrictTransferFrom.TransferType transferType,
|
||||
address receiver,
|
||||
address hook,
|
||||
bytes memory hookData,
|
||||
UniswapV4Executor.UniswapV4Pool[] memory decodedPools
|
||||
) = uniswapV4Exposed.decodeData(data);
|
||||
|
||||
@@ -93,6 +101,7 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
|
||||
uint8(RestrictTransferFrom.TransferType.Transfer)
|
||||
);
|
||||
assertEq(receiver, ALICE);
|
||||
assertEq(hook, address(0));
|
||||
assertEq(decodedPools.length, 2);
|
||||
assertEq(decodedPools[0].intermediaryToken, USDT_ADDR);
|
||||
assertEq(decodedPools[0].fee, pool1Fee);
|
||||
@@ -123,6 +132,8 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
|
||||
true,
|
||||
RestrictTransferFrom.TransferType.Transfer,
|
||||
ALICE,
|
||||
address(0),
|
||||
bytes(""),
|
||||
pools
|
||||
);
|
||||
|
||||
@@ -180,6 +191,8 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
|
||||
true,
|
||||
RestrictTransferFrom.TransferType.Transfer,
|
||||
ALICE,
|
||||
address(0),
|
||||
bytes(""),
|
||||
pools
|
||||
);
|
||||
|
||||
@@ -211,6 +224,43 @@ contract UniswapV4ExecutorTest is Constants, TestUtils {
|
||||
);
|
||||
assertTrue(IERC20(WBTC_ADDR).balanceOf(ALICE) == amountOut);
|
||||
}
|
||||
|
||||
function testSingleSwapEulerHook() public {
|
||||
// Replicating tx: 0xb372306a81c6e840f4ec55f006da6b0b097f435802a2e6fd216998dd12fb4aca
|
||||
address hook = address(0x69058613588536167BA0AA94F0CC1Fe420eF28a8);
|
||||
|
||||
uint256 amountIn = 7407000000;
|
||||
deal(USDC_ADDR, address(uniswapV4Exposed), amountIn);
|
||||
uint256 usdcBalanceBeforeSwapExecutor =
|
||||
USDC.balanceOf(address(uniswapV4Exposed));
|
||||
|
||||
UniswapV4Executor.UniswapV4Pool[] memory pools =
|
||||
new UniswapV4Executor.UniswapV4Pool[](1);
|
||||
pools[0] = UniswapV4Executor.UniswapV4Pool({
|
||||
intermediaryToken: WETH_ADDR,
|
||||
fee: uint24(500),
|
||||
tickSpacing: int24(1)
|
||||
});
|
||||
|
||||
bytes memory data = UniswapV4Utils.encodeExactInput(
|
||||
USDC_ADDR,
|
||||
WETH_ADDR,
|
||||
true,
|
||||
RestrictTransferFrom.TransferType.Transfer,
|
||||
ALICE,
|
||||
hook,
|
||||
bytes(""),
|
||||
pools
|
||||
);
|
||||
|
||||
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
|
||||
assertEq(amountOut, 2681115183499232721);
|
||||
assertEq(
|
||||
USDC.balanceOf(address(uniswapV4Exposed)),
|
||||
usdcBalanceBeforeSwapExecutor - amountIn
|
||||
);
|
||||
assertTrue(IERC20(WETH_ADDR).balanceOf(ALICE) == amountOut);
|
||||
}
|
||||
}
|
||||
|
||||
contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
|
||||
@@ -237,6 +287,8 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
|
||||
true,
|
||||
RestrictTransferFrom.TransferType.TransferFrom,
|
||||
ALICE,
|
||||
address(0),
|
||||
bytes(""),
|
||||
pools
|
||||
);
|
||||
|
||||
@@ -285,6 +337,8 @@ contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
|
||||
true,
|
||||
RestrictTransferFrom.TransferType.TransferFrom,
|
||||
ALICE,
|
||||
address(0),
|
||||
bytes(""),
|
||||
pools
|
||||
);
|
||||
|
||||
|
||||
@@ -10,13 +10,21 @@ library UniswapV4Utils {
|
||||
bool zeroForOne,
|
||||
RestrictTransferFrom.TransferType transferType,
|
||||
address receiver,
|
||||
address hook,
|
||||
bytes memory hookData,
|
||||
UniswapV4Executor.UniswapV4Pool[] memory pools
|
||||
) public pure returns (bytes memory) {
|
||||
bytes memory encodedPools;
|
||||
require(pools.length > 0, "Must have at least one pool");
|
||||
|
||||
for (uint256 i = 0; i < pools.length; i++) {
|
||||
encodedPools = abi.encodePacked(
|
||||
encodedPools,
|
||||
bytes memory firstPool = abi.encodePacked(
|
||||
pools[0].intermediaryToken,
|
||||
bytes3(pools[0].fee),
|
||||
pools[0].tickSpacing
|
||||
);
|
||||
|
||||
bytes[] memory encodedExtraPools = new bytes[](pools.length - 1);
|
||||
for (uint256 i = 1; i < pools.length; i++) {
|
||||
encodedExtraPools[i - 1] = abi.encodePacked(
|
||||
pools[i].intermediaryToken,
|
||||
bytes3(pools[i].fee),
|
||||
pools[i].tickSpacing
|
||||
@@ -24,7 +32,28 @@ library UniswapV4Utils {
|
||||
}
|
||||
|
||||
return abi.encodePacked(
|
||||
tokenIn, tokenOut, zeroForOne, transferType, receiver, encodedPools
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
zeroForOne,
|
||||
transferType,
|
||||
receiver,
|
||||
hook,
|
||||
firstPool,
|
||||
pleEncode(encodedExtraPools),
|
||||
hookData
|
||||
);
|
||||
}
|
||||
|
||||
function pleEncode(bytes[] memory data)
|
||||
public
|
||||
pure
|
||||
returns (bytes memory encoded)
|
||||
{
|
||||
for (uint256 i = 0; i < data.length; i++) {
|
||||
encoded = bytes.concat(
|
||||
encoded,
|
||||
abi.encodePacked(bytes2(uint16(data[i].length)), data[i])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{io, str::Utf8Error};
|
||||
|
||||
use thiserror::Error;
|
||||
use tycho_common::simulation::errors::SimulationError;
|
||||
|
||||
/// Represents the outer-level, user-facing errors of the tycho-execution encoding package.
|
||||
///
|
||||
@@ -41,3 +42,15 @@ impl From<Utf8Error> for EncodingError {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,6 @@ pub struct Permit2 {
|
||||
address: Address,
|
||||
client: EVMProvider,
|
||||
runtime_handle: Handle,
|
||||
// Store the runtime to prevent it from being dropped before use.
|
||||
// This is required since tycho-execution does not have a pre-existing runtime.
|
||||
// However, if the library is used in a context where a runtime already exists, it is not
|
||||
// necessary to store it.
|
||||
#[allow(dead_code)]
|
||||
runtime: Option<Arc<Runtime>>,
|
||||
}
|
||||
|
||||
@@ -23,10 +23,6 @@ use crate::encoding::{
|
||||
pub struct ProtocolApprovalsManager {
|
||||
client: EVMProvider,
|
||||
runtime_handle: Handle,
|
||||
// Store the runtime to prevent it from being dropped before use.
|
||||
// This is required since tycho-execution does not have a pre-existing runtime.
|
||||
// However, if the library is used in a context where a runtime already exists, it is not
|
||||
// necessary to store it.
|
||||
#[allow(dead_code)]
|
||||
runtime: Option<Arc<Runtime>>,
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ pub const PROTOCOL_SPECIFIC_CONFIG: &str =
|
||||
pub static GROUPABLE_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
let mut set = HashSet::new();
|
||||
set.insert("uniswap_v4");
|
||||
set.insert("uniswap_v4_hooks");
|
||||
set.insert("vm:balancer_v3");
|
||||
set.insert("ekubo_v2");
|
||||
set
|
||||
@@ -29,6 +30,7 @@ pub static IN_TRANSFER_REQUIRED_PROTOCOLS: LazyLock<HashSet<&'static str>> = Laz
|
||||
set.insert("uniswap_v3");
|
||||
set.insert("pancakeswap_v3");
|
||||
set.insert("uniswap_v4");
|
||||
set.insert("uniswap_v4_hooks");
|
||||
set.insert("ekubo_v2");
|
||||
set.insert("vm:maverick_v2");
|
||||
set.insert("vm:balancer_v3");
|
||||
@@ -46,6 +48,7 @@ pub static CALLBACK_CONSTRAINED_PROTOCOLS: LazyLock<HashSet<&'static str>> = Laz
|
||||
set.insert("uniswap_v3");
|
||||
set.insert("pancakeswap_v3");
|
||||
set.insert("uniswap_v4");
|
||||
set.insert("uniswap_v4_hooks");
|
||||
set.insert("ekubo_v2");
|
||||
set.insert("vm:balancer_v3");
|
||||
set
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::encoding::{evm::constants::GROUPABLE_PROTOCOLS, models::Swap};
|
||||
/// * `protocol_system`: String, the protocol system of the swaps
|
||||
/// * `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
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SwapGroup {
|
||||
pub token_in: Bytes,
|
||||
pub token_out: Bytes,
|
||||
@@ -20,6 +20,16 @@ pub struct SwapGroup {
|
||||
pub split: f64,
|
||||
}
|
||||
|
||||
impl PartialEq for SwapGroup {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.token_in == other.token_in &&
|
||||
self.token_out == other.token_out &&
|
||||
self.protocol_system == other.protocol_system &&
|
||||
self.swaps == other.swaps &&
|
||||
self.split == other.split
|
||||
}
|
||||
}
|
||||
|
||||
/// Group consecutive swaps which can be encoded into one swap execution for gas optimization.
|
||||
///
|
||||
/// An example where this applies is the case of USV4, which uses a PoolManager contract
|
||||
@@ -77,7 +87,7 @@ mod tests {
|
||||
use tycho_common::{models::protocol::ProtocolComponent, Bytes};
|
||||
|
||||
use super::*;
|
||||
use crate::encoding::models::Swap;
|
||||
use crate::encoding::models::SwapBuilder;
|
||||
|
||||
fn weth() -> Bytes {
|
||||
Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec())
|
||||
@@ -95,43 +105,28 @@ mod tests {
|
||||
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
|
||||
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
|
||||
|
||||
let swap_weth_wbtc = Swap {
|
||||
component: ProtocolComponent {
|
||||
protocol_system: "uniswap_v4".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: wbtc.clone(),
|
||||
// This represents the remaining 50%, but to avoid any rounding errors we set this to
|
||||
// 0 to signify "the remainder of the WETH value". It should still be very close to 50%
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap_wbtc_usdc = Swap {
|
||||
component: ProtocolComponent {
|
||||
protocol_system: "uniswap_v4".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: wbtc.clone(),
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap_usdc_dai = Swap {
|
||||
component: ProtocolComponent {
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: usdc.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let grouped_swaps = group_swaps(&vec![
|
||||
swap_weth_wbtc.clone(),
|
||||
swap_wbtc_usdc.clone(),
|
||||
swap_usdc_dai.clone(),
|
||||
]);
|
||||
let swap_weth_wbtc = SwapBuilder::new(
|
||||
ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() },
|
||||
weth.clone(),
|
||||
wbtc.clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let swap_wbtc_usdc = SwapBuilder::new(
|
||||
ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() },
|
||||
wbtc.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let swap_usdc_dai = SwapBuilder::new(
|
||||
ProtocolComponent { protocol_system: "uniswap_v2".to_string(), ..Default::default() },
|
||||
usdc.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.build();
|
||||
let swaps = vec![swap_weth_wbtc.clone(), swap_wbtc_usdc.clone(), swap_usdc_dai.clone()];
|
||||
let grouped_swaps = group_swaps(&swaps);
|
||||
|
||||
assert_eq!(
|
||||
grouped_swaps,
|
||||
@@ -169,54 +164,41 @@ mod tests {
|
||||
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
|
||||
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
|
||||
|
||||
let swap_wbtc_weth = Swap {
|
||||
component: ProtocolComponent {
|
||||
protocol_system: "uniswap_v4".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: wbtc.clone(),
|
||||
token_out: weth.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap_weth_usdc = Swap {
|
||||
component: ProtocolComponent {
|
||||
protocol_system: "uniswap_v4".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: usdc.clone(),
|
||||
split: 0.5f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap_weth_dai = Swap {
|
||||
component: ProtocolComponent {
|
||||
protocol_system: "uniswap_v4".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
// This represents the remaining 50%, but to avoid any rounding errors we set this to
|
||||
// 0 to signify "the remainder of the WETH value". It should still be very close to 50%
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap_dai_usdc = Swap {
|
||||
component: ProtocolComponent {
|
||||
protocol_system: "uniswap_v4".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai.clone(),
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let grouped_swaps = group_swaps(&vec![
|
||||
let swap_wbtc_weth = SwapBuilder::new(
|
||||
ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() },
|
||||
wbtc.clone(),
|
||||
weth.clone(),
|
||||
)
|
||||
.build();
|
||||
let swap_weth_usdc = SwapBuilder::new(
|
||||
ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() },
|
||||
weth.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.split(0.5f64)
|
||||
.build();
|
||||
let swap_weth_dai = SwapBuilder::new(
|
||||
ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() },
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.build();
|
||||
// Split 0 represents the remaining 50%, but to avoid any rounding errors we set this to
|
||||
// 0 to signify "the remainder of the WETH value". It should still be very close to 50%
|
||||
|
||||
let swap_dai_usdc = SwapBuilder::new(
|
||||
ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() },
|
||||
dai.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.build();
|
||||
let swaps = vec![
|
||||
swap_wbtc_weth.clone(),
|
||||
swap_weth_usdc.clone(),
|
||||
swap_weth_dai.clone(),
|
||||
swap_dai_usdc.clone(),
|
||||
]);
|
||||
];
|
||||
let grouped_swaps = group_swaps(&swaps);
|
||||
|
||||
assert_eq!(
|
||||
grouped_swaps,
|
||||
@@ -260,55 +242,46 @@ mod tests {
|
||||
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
|
||||
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
|
||||
|
||||
let swap_weth_wbtc = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap_weth_wbtc = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
protocol_system: "vm:balancer_v3".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: wbtc.clone(),
|
||||
split: 0.5f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap_wbtc_usdc = Swap {
|
||||
component: ProtocolComponent {
|
||||
protocol_system: "vm:balancer_v3".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: wbtc.clone(),
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap_weth_dai = Swap {
|
||||
component: ProtocolComponent {
|
||||
protocol_system: "uniswap_v4".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
// This represents the remaining 50%, but to avoid any rounding errors we set this to
|
||||
// 0 to signify "the remainder of the WETH value". It should still be very close to 50%
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap_dai_usdc = Swap {
|
||||
component: ProtocolComponent {
|
||||
protocol_system: "uniswap_v4".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai.clone(),
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
weth.clone(),
|
||||
wbtc.clone(),
|
||||
)
|
||||
.split(0.5f64)
|
||||
.build();
|
||||
|
||||
let grouped_swaps = group_swaps(&vec![
|
||||
let swap_wbtc_usdc = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
protocol_system: "vm:balancer_v3".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
wbtc.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.build();
|
||||
let swap_weth_dai = SwapBuilder::new(
|
||||
ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() },
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.build();
|
||||
let swap_dai_usdc = SwapBuilder::new(
|
||||
ProtocolComponent { protocol_system: "uniswap_v4".to_string(), ..Default::default() },
|
||||
dai.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let swaps = vec![
|
||||
swap_weth_wbtc.clone(),
|
||||
swap_wbtc_usdc.clone(),
|
||||
swap_weth_dai.clone(),
|
||||
swap_dai_usdc.clone(),
|
||||
]);
|
||||
];
|
||||
let grouped_swaps = group_swaps(&swaps);
|
||||
|
||||
assert_eq!(
|
||||
grouped_swaps,
|
||||
|
||||
@@ -5,5 +5,7 @@ mod encoding_utils;
|
||||
mod group_swaps;
|
||||
pub mod strategy_encoder;
|
||||
mod swap_encoder;
|
||||
#[cfg(feature = "test-utils")]
|
||||
pub mod testing_utils;
|
||||
pub mod tycho_encoders;
|
||||
pub mod utils;
|
||||
|
||||
@@ -121,16 +121,25 @@ impl StrategyEncoder for SingleSwapStrategyEncoder {
|
||||
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() {
|
||||
let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?;
|
||||
grouped_protocol_data.extend(protocol_data);
|
||||
if encoding_context.group_token_in == swap.token_in {
|
||||
initial_protocol_data = protocol_data;
|
||||
} else {
|
||||
grouped_protocol_data.push(protocol_data);
|
||||
}
|
||||
}
|
||||
|
||||
if !grouped_protocol_data.is_empty() {
|
||||
initial_protocol_data.extend(ple_encode(grouped_protocol_data));
|
||||
}
|
||||
|
||||
let swap_data = self.encode_swap_header(
|
||||
Bytes::from_str(swap_encoder.executor_address())
|
||||
.map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?,
|
||||
grouped_protocol_data,
|
||||
initial_protocol_data,
|
||||
);
|
||||
Ok(EncodedSolution {
|
||||
function_signature: self.function_signature.clone(),
|
||||
@@ -228,10 +237,11 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder {
|
||||
|
||||
let grouped_swaps = group_swaps(&solution.swaps);
|
||||
|
||||
let mut wrap = false;
|
||||
let (mut wrap, mut unwrap) = (false, false);
|
||||
if let Some(action) = &solution.native_action {
|
||||
if action == &NativeAction::Wrap {
|
||||
wrap = true
|
||||
match *action {
|
||||
NativeAction::Wrap => wrap = true,
|
||||
NativeAction::Unwrap => unwrap = true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +261,7 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder {
|
||||
let next_swap = grouped_swaps.get(i + 1);
|
||||
let (swap_receiver, next_swap_optimization) = self
|
||||
.transfer_optimization
|
||||
.get_receiver(&solution.receiver, next_swap)?;
|
||||
.get_receiver(&solution.receiver, next_swap, unwrap)?;
|
||||
next_in_between_swap_optimization_allowed = next_swap_optimization;
|
||||
|
||||
let transfer = self
|
||||
@@ -271,17 +281,26 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder {
|
||||
transfer_type: transfer,
|
||||
};
|
||||
|
||||
let mut grouped_protocol_data: Vec<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() {
|
||||
let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?;
|
||||
grouped_protocol_data.extend(protocol_data);
|
||||
if encoding_context.group_token_in == swap.token_in {
|
||||
initial_protocol_data = protocol_data;
|
||||
} else {
|
||||
grouped_protocol_data.push(protocol_data);
|
||||
}
|
||||
}
|
||||
|
||||
if !grouped_protocol_data.is_empty() {
|
||||
initial_protocol_data.extend(ple_encode(grouped_protocol_data));
|
||||
}
|
||||
|
||||
let swap_data = self.encode_swap_header(
|
||||
Bytes::from_str(swap_encoder.executor_address()).map_err(|_| {
|
||||
EncodingError::FatalError("Invalid executor address".to_string())
|
||||
})?,
|
||||
grouped_protocol_data,
|
||||
initial_protocol_data,
|
||||
);
|
||||
swaps.push(swap_data);
|
||||
}
|
||||
@@ -462,10 +481,19 @@ impl StrategyEncoder for SplitSwapStrategyEncoder {
|
||||
transfer_type: transfer,
|
||||
};
|
||||
|
||||
let mut grouped_protocol_data: Vec<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() {
|
||||
let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?;
|
||||
grouped_protocol_data.extend(protocol_data);
|
||||
if encoding_context.group_token_in == swap.token_in {
|
||||
initial_protocol_data = protocol_data;
|
||||
} else {
|
||||
grouped_protocol_data.push(protocol_data);
|
||||
}
|
||||
}
|
||||
|
||||
if !grouped_protocol_data.is_empty() {
|
||||
initial_protocol_data.extend(ple_encode(grouped_protocol_data));
|
||||
}
|
||||
|
||||
let swap_data = self.encode_swap_header(
|
||||
@@ -475,7 +503,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder {
|
||||
Bytes::from_str(swap_encoder.executor_address()).map_err(|_| {
|
||||
EncodingError::FatalError("Invalid executor address".to_string())
|
||||
})?,
|
||||
grouped_protocol_data,
|
||||
initial_protocol_data,
|
||||
);
|
||||
swaps.push(swap_data);
|
||||
}
|
||||
@@ -517,7 +545,6 @@ mod tests {
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::encoding::models::Swap;
|
||||
|
||||
fn eth_chain() -> Chain {
|
||||
Chain::Ethereum
|
||||
@@ -538,8 +565,8 @@ mod tests {
|
||||
}
|
||||
|
||||
mod single {
|
||||
|
||||
use super::*;
|
||||
use crate::encoding::models::SwapBuilder;
|
||||
#[test]
|
||||
fn test_single_swap_strategy_encoder() {
|
||||
// Performs a single swap from WETH to DAI on a USV2 pool, with no grouping
|
||||
@@ -548,17 +575,16 @@ mod tests {
|
||||
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
|
||||
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
|
||||
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.build();
|
||||
let swap_encoder_registry = get_swap_encoder_registry();
|
||||
let encoder = SingleSwapStrategyEncoder::new(
|
||||
eth_chain(),
|
||||
@@ -609,17 +635,16 @@ mod tests {
|
||||
|
||||
let checked_amount = BigUint::from_str("1_640_000000000000000000").unwrap();
|
||||
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.build();
|
||||
let swap_encoder_registry = get_swap_encoder_registry();
|
||||
let encoder = SingleSwapStrategyEncoder::new(
|
||||
eth_chain(),
|
||||
@@ -669,6 +694,7 @@ mod tests {
|
||||
|
||||
mod sequential {
|
||||
use super::*;
|
||||
use crate::encoding::models::SwapBuilder;
|
||||
|
||||
#[test]
|
||||
fn test_sequential_swap_strategy_encoder_no_permit2() {
|
||||
@@ -680,28 +706,26 @@ mod tests {
|
||||
let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap();
|
||||
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
|
||||
|
||||
let swap_weth_wbtc = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap_weth_wbtc = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: wbtc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap_wbtc_usdc = Swap {
|
||||
component: ProtocolComponent {
|
||||
weth.clone(),
|
||||
wbtc.clone(),
|
||||
)
|
||||
.build();
|
||||
let swap_wbtc_usdc = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: wbtc.clone(),
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
wbtc.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.build();
|
||||
let swap_encoder_registry = get_swap_encoder_registry();
|
||||
let encoder = SequentialSwapStrategyEncoder::new(
|
||||
eth_chain(),
|
||||
@@ -759,6 +783,7 @@ mod tests {
|
||||
|
||||
mod split {
|
||||
use super::*;
|
||||
use crate::encoding::models::SwapBuilder;
|
||||
|
||||
#[test]
|
||||
fn test_split_input_cyclic_swap() {
|
||||
@@ -774,8 +799,8 @@ mod tests {
|
||||
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
|
||||
|
||||
// USDC -> WETH (Pool 1) - 60% of input
|
||||
let swap_usdc_weth_pool1 = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap_usdc_weth_pool1 = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3
|
||||
* Pool 1 */
|
||||
protocol_system: "uniswap_v3".to_string(),
|
||||
@@ -789,15 +814,15 @@ mod tests {
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
token_in: usdc.clone(),
|
||||
token_out: weth.clone(),
|
||||
split: 0.6f64, // 60% of input
|
||||
user_data: None,
|
||||
};
|
||||
usdc.clone(),
|
||||
weth.clone(),
|
||||
)
|
||||
.split(0.6f64)
|
||||
.build();
|
||||
|
||||
// USDC -> WETH (Pool 2) - 40% of input (remaining)
|
||||
let swap_usdc_weth_pool2 = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap_usdc_weth_pool2 = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3
|
||||
* Pool 2 */
|
||||
protocol_system: "uniswap_v3".to_string(),
|
||||
@@ -811,15 +836,14 @@ mod tests {
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
token_in: usdc.clone(),
|
||||
token_out: weth.clone(),
|
||||
split: 0f64,
|
||||
user_data: None, // Remaining 40%
|
||||
};
|
||||
usdc.clone(),
|
||||
weth.clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
// WETH -> USDC (Pool 2)
|
||||
let swap_weth_usdc_pool2 = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap_weth_usdc_pool2 = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), /* USDC-WETH USV2
|
||||
* Pool 2 */
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
@@ -833,12 +857,10 @@ mod tests {
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: usdc.clone(),
|
||||
split: 0.0f64,
|
||||
user_data: None,
|
||||
};
|
||||
|
||||
weth.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.build();
|
||||
let swap_encoder_registry = get_swap_encoder_registry();
|
||||
let encoder = SplitSwapStrategyEncoder::new(
|
||||
eth_chain(),
|
||||
@@ -927,8 +949,8 @@ mod tests {
|
||||
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
|
||||
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
|
||||
|
||||
let swap_usdc_weth_v2 = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap_usdc_weth_v2 = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), // USDC-WETH USV2
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
static_attributes: {
|
||||
@@ -941,14 +963,13 @@ mod tests {
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
token_in: usdc.clone(),
|
||||
token_out: weth.clone(),
|
||||
split: 0.0f64,
|
||||
user_data: None,
|
||||
};
|
||||
usdc.clone(),
|
||||
weth.clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let swap_weth_usdc_v3_pool1 = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap_weth_usdc_v3_pool1 = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3
|
||||
* Pool 1 */
|
||||
protocol_system: "uniswap_v3".to_string(),
|
||||
@@ -962,16 +983,16 @@ mod tests {
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: usdc.clone(),
|
||||
split: 0.6f64,
|
||||
user_data: None,
|
||||
};
|
||||
weth.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.split(0.6f64)
|
||||
.build();
|
||||
|
||||
let swap_weth_usdc_v3_pool2 = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap_weth_usdc_v3_pool2 = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3
|
||||
* Pool 2 */
|
||||
* Pool 1 */
|
||||
protocol_system: "uniswap_v3".to_string(),
|
||||
static_attributes: {
|
||||
let mut attrs = HashMap::new();
|
||||
@@ -983,11 +1004,10 @@ mod tests {
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: usdc.clone(),
|
||||
split: 0.0f64,
|
||||
user_data: None,
|
||||
};
|
||||
weth.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let swap_encoder_registry = get_swap_encoder_registry();
|
||||
let encoder = SplitSwapStrategyEncoder::new(
|
||||
|
||||
@@ -197,7 +197,7 @@ mod tests {
|
||||
use tycho_common::{models::protocol::ProtocolComponent, Bytes};
|
||||
|
||||
use super::*;
|
||||
use crate::encoding::models::Swap;
|
||||
use crate::encoding::models::{Swap, SwapBuilder};
|
||||
|
||||
#[test]
|
||||
fn test_validate_path_single_swap() {
|
||||
@@ -205,17 +205,16 @@ mod tests {
|
||||
let eth = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap();
|
||||
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
|
||||
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
|
||||
let swaps = vec![Swap {
|
||||
component: ProtocolComponent {
|
||||
let swaps = vec![SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
}];
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.build()];
|
||||
let result = validator.validate_swap_path(&swaps, &weth, &dai, &None, ð, &weth);
|
||||
assert_eq!(result, Ok(()));
|
||||
}
|
||||
@@ -228,28 +227,27 @@ mod tests {
|
||||
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
|
||||
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
|
||||
let swaps = vec![
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0.5f64,
|
||||
user_data: None,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.split(0.5f64)
|
||||
.build(),
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai.clone(),
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
},
|
||||
dai.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.build(),
|
||||
];
|
||||
let result = validator.validate_swap_path(&swaps, &weth, &usdc, &None, ð, &weth);
|
||||
assert_eq!(result, Ok(()));
|
||||
@@ -265,29 +263,28 @@ mod tests {
|
||||
let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap();
|
||||
|
||||
let disconnected_swaps = vec![
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool1".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0.5,
|
||||
user_data: None,
|
||||
},
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.split(0.5f64)
|
||||
.build(),
|
||||
// This swap is disconnected from the WETH->DAI path
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool2".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: wbtc.clone(),
|
||||
token_out: usdc.clone(),
|
||||
split: 0.0,
|
||||
user_data: None,
|
||||
},
|
||||
wbtc.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.build(),
|
||||
];
|
||||
let result =
|
||||
validator.validate_swap_path(&disconnected_swaps, &weth, &usdc, &None, ð, &weth);
|
||||
@@ -305,28 +302,26 @@ mod tests {
|
||||
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
|
||||
|
||||
let cyclic_swaps = vec![
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool1".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: usdc.clone(),
|
||||
token_out: weth.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
usdc.clone(),
|
||||
weth.clone(),
|
||||
)
|
||||
.build(),
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool2".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
},
|
||||
weth.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.build(),
|
||||
];
|
||||
|
||||
// Test with USDC as both given token and checked token
|
||||
@@ -342,17 +337,17 @@ mod tests {
|
||||
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
|
||||
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
|
||||
|
||||
let unreachable_swaps = vec![Swap {
|
||||
component: ProtocolComponent {
|
||||
let unreachable_swaps = vec![SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool1".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 1.0,
|
||||
user_data: None,
|
||||
}];
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.split(1.0)
|
||||
.build()];
|
||||
let result =
|
||||
validator.validate_swap_path(&unreachable_swaps, &weth, &usdc, &None, ð, &weth);
|
||||
assert!(matches!(
|
||||
@@ -381,17 +376,16 @@ mod tests {
|
||||
let validator = SplitSwapValidator;
|
||||
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
|
||||
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
|
||||
let swaps = vec![Swap {
|
||||
component: ProtocolComponent {
|
||||
let swaps = vec![SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
}];
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.build()];
|
||||
let result = validator.validate_split_percentages(&swaps);
|
||||
assert_eq!(result, Ok(()));
|
||||
}
|
||||
@@ -404,39 +398,38 @@ mod tests {
|
||||
|
||||
// Valid case: Multiple swaps with proper splits (50%, 30%, remainder)
|
||||
let valid_swaps = vec![
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool1".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0.5,
|
||||
user_data: None,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.split(0.5)
|
||||
.build(),
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool2".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0.3,
|
||||
user_data: None,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.split(0.3)
|
||||
.build(),
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool3".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0.0, // Remainder (20%)
|
||||
user_data: None,
|
||||
},
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.build(),
|
||||
];
|
||||
assert!(validator
|
||||
.validate_split_percentages(&valid_swaps)
|
||||
@@ -450,28 +443,28 @@ mod tests {
|
||||
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
|
||||
|
||||
let invalid_total_swaps = vec![
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool1".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0.7,
|
||||
user_data: None,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.split(0.7)
|
||||
.build(),
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool2".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0.3,
|
||||
user_data: None,
|
||||
},
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.split(0.3)
|
||||
.build(),
|
||||
];
|
||||
assert!(matches!(
|
||||
validator.validate_split_percentages(&invalid_total_swaps),
|
||||
@@ -486,28 +479,27 @@ mod tests {
|
||||
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
|
||||
|
||||
let invalid_zero_position_swaps = vec![
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool1".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0.0,
|
||||
user_data: None,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.build(),
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool2".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0.5,
|
||||
user_data: None,
|
||||
},
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.split(0.5)
|
||||
.build(),
|
||||
];
|
||||
assert!(matches!(
|
||||
validator.validate_split_percentages(&invalid_zero_position_swaps),
|
||||
@@ -522,39 +514,38 @@ mod tests {
|
||||
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
|
||||
|
||||
let invalid_overflow_swaps = vec![
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool1".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0.6,
|
||||
user_data: None,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.split(0.6)
|
||||
.build(),
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool2".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0.5,
|
||||
user_data: None,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.split(0.5)
|
||||
.build(),
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool3".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: dai.clone(),
|
||||
split: 0.0,
|
||||
user_data: None,
|
||||
},
|
||||
weth.clone(),
|
||||
dai.clone(),
|
||||
)
|
||||
.build(),
|
||||
];
|
||||
assert!(matches!(
|
||||
validator.validate_split_percentages(&invalid_overflow_swaps),
|
||||
@@ -569,17 +560,16 @@ mod tests {
|
||||
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
|
||||
let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
|
||||
|
||||
let swaps = vec![Swap {
|
||||
component: ProtocolComponent {
|
||||
let swaps = vec![SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool1".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth.clone(),
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
}];
|
||||
weth.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.build()];
|
||||
|
||||
let result = validator.validate_swap_path(
|
||||
&swaps,
|
||||
@@ -599,17 +589,16 @@ mod tests {
|
||||
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
|
||||
let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
|
||||
|
||||
let swaps = vec![Swap {
|
||||
component: ProtocolComponent {
|
||||
let swaps = vec![SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool1".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: usdc.clone(),
|
||||
token_out: weth.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
}];
|
||||
usdc.clone(),
|
||||
weth.clone(),
|
||||
)
|
||||
.build()];
|
||||
|
||||
let result = validator.validate_swap_path(
|
||||
&swaps,
|
||||
|
||||
@@ -82,6 +82,7 @@ impl TransferOptimization {
|
||||
&self,
|
||||
solution_receiver: &Bytes,
|
||||
next_swap: Option<&SwapGroup>,
|
||||
unwrap: bool,
|
||||
) -> Result<(Bytes, bool), EncodingError> {
|
||||
if let Some(next) = next_swap {
|
||||
// if the protocol of the next swap supports transfer in optimization
|
||||
@@ -104,7 +105,11 @@ impl TransferOptimization {
|
||||
}
|
||||
} else {
|
||||
// last swap - there is no next swap
|
||||
Ok((solution_receiver.clone(), false))
|
||||
if unwrap {
|
||||
Ok((self.router_address.clone(), false))
|
||||
} else {
|
||||
Ok((solution_receiver.clone(), false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,7 +121,7 @@ mod tests {
|
||||
use tycho_common::models::protocol::ProtocolComponent;
|
||||
|
||||
use super::*;
|
||||
use crate::encoding::models::Swap;
|
||||
use crate::encoding::models::SwapBuilder;
|
||||
|
||||
fn weth() -> Bytes {
|
||||
Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec())
|
||||
@@ -169,17 +174,16 @@ mod tests {
|
||||
#[case] expected_transfer: TransferType,
|
||||
) {
|
||||
// The swap token is the same as the given token, which is not the native token
|
||||
let swaps = vec![Swap {
|
||||
component: ProtocolComponent {
|
||||
let swaps = vec![SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: swap_token_in.clone(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
}];
|
||||
swap_token_in.clone(),
|
||||
dai(),
|
||||
)
|
||||
.build()];
|
||||
let swap = SwapGroup {
|
||||
protocol_system: protocol,
|
||||
token_in: swap_token_in,
|
||||
@@ -203,16 +207,19 @@ mod tests {
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
// there is no next swap -> receiver is the solution receiver
|
||||
#[case(None, receiver(), false)]
|
||||
// there is no next swap but there is an unwrap -> receiver is the router
|
||||
#[case(None, true, router_address(), false)]
|
||||
// there is no next swap and no unwrap -> receiver is the solution receiver
|
||||
#[case(None, false, receiver(), false)]
|
||||
// protocol of next swap supports transfer in optimization
|
||||
#[case(Some("uniswap_v2"), component_id(), true)]
|
||||
#[case(Some("uniswap_v2"), false, component_id(), true)]
|
||||
// protocol of next swap supports transfer in optimization but is callback constrained
|
||||
#[case(Some("uniswap_v3"), router_address(), false)]
|
||||
#[case(Some("uniswap_v3"), false, router_address(), false)]
|
||||
// protocol of next swap does not support transfer in optimization
|
||||
#[case(Some("vm:curve"), router_address(), false)]
|
||||
#[case(Some("vm:curve"), false, router_address(), false)]
|
||||
fn test_get_receiver(
|
||||
#[case] protocol: Option<&str>,
|
||||
#[case] unwrap: bool,
|
||||
#[case] expected_receiver: Bytes,
|
||||
#[case] expected_optimization: bool,
|
||||
) {
|
||||
@@ -231,21 +238,20 @@ mod tests {
|
||||
token_in: usdc(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
swaps: vec![Swap {
|
||||
component: ProtocolComponent {
|
||||
swaps: vec![SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
protocol_system: protocol.unwrap().to_string(),
|
||||
id: component_id().to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: usdc(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
}],
|
||||
usdc(),
|
||||
dai(),
|
||||
)
|
||||
.build()],
|
||||
})
|
||||
};
|
||||
|
||||
let result = optimization.get_receiver(&receiver(), next_swap.as_ref());
|
||||
let result = optimization.get_receiver(&receiver(), next_swap.as_ref(), unwrap);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let (actual_receiver, optimization_flag) = result.unwrap();
|
||||
|
||||
@@ -5,8 +5,9 @@ use tycho_common::models::Chain;
|
||||
use crate::encoding::{
|
||||
errors::EncodingError,
|
||||
evm::swap_encoder::swap_encoders::{
|
||||
BalancerV2SwapEncoder, BalancerV3SwapEncoder, CurveSwapEncoder, EkuboSwapEncoder,
|
||||
MaverickV2SwapEncoder, UniswapV2SwapEncoder, UniswapV3SwapEncoder, UniswapV4SwapEncoder,
|
||||
BalancerV2SwapEncoder, BalancerV3SwapEncoder, BebopSwapEncoder, CurveSwapEncoder,
|
||||
EkuboSwapEncoder, HashflowSwapEncoder, MaverickV2SwapEncoder, UniswapV2SwapEncoder,
|
||||
UniswapV3SwapEncoder, UniswapV4SwapEncoder,
|
||||
},
|
||||
swap_encoder::SwapEncoder,
|
||||
};
|
||||
@@ -71,6 +72,11 @@ impl SwapEncoderBuilder {
|
||||
self.chain,
|
||||
self.config,
|
||||
)?)),
|
||||
"uniswap_v4_hooks" => Ok(Box::new(UniswapV4SwapEncoder::new(
|
||||
self.executor_address,
|
||||
self.chain,
|
||||
self.config,
|
||||
)?)),
|
||||
"ekubo_v2" => {
|
||||
Ok(Box::new(EkuboSwapEncoder::new(self.executor_address, self.chain, self.config)?))
|
||||
}
|
||||
@@ -87,6 +93,14 @@ impl SwapEncoderBuilder {
|
||||
self.chain,
|
||||
self.config,
|
||||
)?)),
|
||||
"rfq:bebop" => {
|
||||
Ok(Box::new(BebopSwapEncoder::new(self.executor_address, self.chain, self.config)?))
|
||||
}
|
||||
"rfq:hashflow" => Ok(Box::new(HashflowSwapEncoder::new(
|
||||
self.executor_address,
|
||||
self.chain,
|
||||
self.config,
|
||||
)?)),
|
||||
_ => Err(EncodingError::FatalError(format!(
|
||||
"Unknown protocol system: {}",
|
||||
self.protocol_system
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use std::{collections::HashMap, str::FromStr, sync::Arc};
|
||||
|
||||
use alloy::{
|
||||
primitives::{Address, Bytes as AlloyBytes, U8},
|
||||
sol_types::SolValue,
|
||||
};
|
||||
use serde_json::from_str;
|
||||
use tycho_common::{models::Chain, Bytes};
|
||||
use tokio::{
|
||||
runtime::{Handle, Runtime},
|
||||
task::block_in_place,
|
||||
};
|
||||
use tycho_common::{
|
||||
models::{protocol::GetAmountOutParams, Chain},
|
||||
Bytes,
|
||||
};
|
||||
|
||||
use crate::encoding::{
|
||||
errors::EncodingError,
|
||||
evm::{
|
||||
approvals::protocol_approvals_manager::ProtocolApprovalsManager,
|
||||
utils::{bytes_to_address, get_static_attribute, pad_to_fixed_size},
|
||||
utils::{
|
||||
biguint_to_u256, bytes_to_address, get_runtime, get_static_attribute, pad_to_fixed_size,
|
||||
},
|
||||
},
|
||||
models::{EncodingContext, Swap},
|
||||
swap_encoder::SwapEncoder,
|
||||
@@ -176,9 +185,28 @@ impl SwapEncoder for UniswapV4SwapEncoder {
|
||||
EncodingError::FatalError("Failed to pad tick spacing bytes".to_string())
|
||||
})?;
|
||||
|
||||
let hook_address = match get_static_attribute(swap, "hooks") {
|
||||
Ok(hook) => Address::from_slice(&hook),
|
||||
Err(_) => Address::ZERO,
|
||||
};
|
||||
let mut hook_data = AlloyBytes::new();
|
||||
if encoding_context.group_token_out == swap.token_out {
|
||||
// Add hook data if it's only the last swap
|
||||
hook_data = AlloyBytes::from(
|
||||
swap.user_data
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.to_vec(),
|
||||
);
|
||||
}
|
||||
// Early check if this is not the first swap
|
||||
if encoding_context.group_token_in != swap.token_in {
|
||||
return Ok((bytes_to_address(&swap.token_out)?, pool_fee_u24, pool_tick_spacing_u24)
|
||||
return Ok((
|
||||
bytes_to_address(&swap.token_out)?,
|
||||
pool_fee_u24,
|
||||
pool_tick_spacing_u24,
|
||||
hook_data,
|
||||
)
|
||||
.abi_encode_packed());
|
||||
}
|
||||
|
||||
@@ -199,7 +227,9 @@ impl SwapEncoder for UniswapV4SwapEncoder {
|
||||
zero_to_one,
|
||||
(encoding_context.transfer_type as u8).to_be_bytes(),
|
||||
bytes_to_address(&encoding_context.receiver)?,
|
||||
hook_address,
|
||||
pool_params,
|
||||
hook_data,
|
||||
);
|
||||
|
||||
Ok(args.abi_encode_packed())
|
||||
@@ -634,6 +664,326 @@ impl SwapEncoder for BalancerV3SwapEncoder {
|
||||
fn executor_address(&self) -> &str {
|
||||
&self.executor_address
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<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> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
@@ -651,10 +1001,14 @@ mod tests {
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::encoding::{evm::utils::write_calldata_to_file, models::TransferType};
|
||||
use crate::encoding::{
|
||||
evm::utils::write_calldata_to_file,
|
||||
models::{SwapBuilder, TransferType},
|
||||
};
|
||||
|
||||
mod uniswap_v2 {
|
||||
use super::*;
|
||||
use crate::encoding::models::SwapBuilder;
|
||||
#[test]
|
||||
fn test_encode_uniswap_v2() {
|
||||
let usv2_pool = ProtocolComponent {
|
||||
@@ -664,13 +1018,7 @@ mod tests {
|
||||
|
||||
let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
|
||||
let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f");
|
||||
let swap = Swap {
|
||||
component: usv2_pool,
|
||||
token_in: token_in.clone(),
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap = SwapBuilder::new(usv2_pool, token_in.clone(), token_out.clone()).build();
|
||||
let encoding_context = EncodingContext {
|
||||
receiver: Bytes::from("0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e"), // BOB
|
||||
exact_out: false,
|
||||
@@ -710,6 +1058,7 @@ mod tests {
|
||||
|
||||
mod uniswap_v3 {
|
||||
use super::*;
|
||||
use crate::encoding::models::SwapBuilder;
|
||||
#[test]
|
||||
fn test_encode_uniswap_v3() {
|
||||
let fee = BigInt::from(500);
|
||||
@@ -724,13 +1073,7 @@ mod tests {
|
||||
};
|
||||
let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
|
||||
let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f");
|
||||
let swap = Swap {
|
||||
component: usv3_pool,
|
||||
token_in: token_in.clone(),
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap = SwapBuilder::new(usv3_pool, token_in.clone(), token_out.clone()).build();
|
||||
let encoding_context = EncodingContext {
|
||||
receiver: Bytes::from("0x0000000000000000000000000000000000000001"),
|
||||
exact_out: false,
|
||||
@@ -773,6 +1116,7 @@ mod tests {
|
||||
|
||||
mod balancer_v2 {
|
||||
use super::*;
|
||||
use crate::encoding::models::SwapBuilder;
|
||||
|
||||
#[test]
|
||||
fn test_encode_balancer_v2() {
|
||||
@@ -785,13 +1129,7 @@ mod tests {
|
||||
};
|
||||
let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
|
||||
let token_out = Bytes::from("0xba100000625a3754423978a60c9317c58a424e3D");
|
||||
let swap = Swap {
|
||||
component: balancer_pool,
|
||||
token_in: token_in.clone(),
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap = SwapBuilder::new(balancer_pool, token_in.clone(), token_out.clone()).build();
|
||||
let encoding_context = EncodingContext {
|
||||
// The receiver was generated with `makeAddr("bob") using forge`
|
||||
receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"),
|
||||
@@ -838,7 +1176,7 @@ mod tests {
|
||||
|
||||
mod uniswap_v4 {
|
||||
use super::*;
|
||||
use crate::encoding::evm::utils::write_calldata_to_file;
|
||||
use crate::encoding::evm::utils::{ple_encode, write_calldata_to_file};
|
||||
|
||||
#[test]
|
||||
fn test_encode_uniswap_v4_simple_swap() {
|
||||
@@ -858,13 +1196,7 @@ mod tests {
|
||||
static_attributes,
|
||||
..Default::default()
|
||||
};
|
||||
let swap = Swap {
|
||||
component: usv4_pool,
|
||||
token_in: token_in.clone(),
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap = SwapBuilder::new(usv4_pool, token_in.clone(), token_out.clone()).build();
|
||||
let encoding_context = EncodingContext {
|
||||
// The receiver is ALICE to match the solidity tests
|
||||
receiver: Bytes::from("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2"),
|
||||
@@ -900,6 +1232,8 @@ mod tests {
|
||||
"01",
|
||||
// receiver
|
||||
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
|
||||
// hook address (not set, so zero)
|
||||
"0000000000000000000000000000000000000000",
|
||||
// pool params:
|
||||
// - intermediary token
|
||||
"dac17f958d2ee523a2206206994597c13d831ec7",
|
||||
@@ -931,13 +1265,7 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let swap = Swap {
|
||||
component: usv4_pool,
|
||||
token_in: token_in.clone(),
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap = SwapBuilder::new(usv4_pool, token_in.clone(), token_out.clone()).build();
|
||||
|
||||
let encoding_context = EncodingContext {
|
||||
receiver: Bytes::from("0x0000000000000000000000000000000000000001"),
|
||||
@@ -1028,21 +1356,12 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let initial_swap = Swap {
|
||||
component: usde_usdt_component,
|
||||
token_in: usde_address.clone(),
|
||||
token_out: usdt_address.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
|
||||
let second_swap = Swap {
|
||||
component: usdt_wbtc_component,
|
||||
token_in: usdt_address,
|
||||
token_out: wbtc_address.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let initial_swap =
|
||||
SwapBuilder::new(usde_usdt_component, usde_address.clone(), usdt_address.clone())
|
||||
.build();
|
||||
let second_swap =
|
||||
SwapBuilder::new(usdt_wbtc_component, usdt_address.clone(), wbtc_address.clone())
|
||||
.build();
|
||||
|
||||
let encoder = UniswapV4SwapEncoder::new(
|
||||
String::from("0xF62849F9A0B5Bf2913b396098F7c7019b51A820a"),
|
||||
@@ -1057,8 +1376,11 @@ mod tests {
|
||||
.encode_swap(&second_swap, &context)
|
||||
.unwrap();
|
||||
|
||||
let combined_hex =
|
||||
format!("{}{}", encode(&initial_encoded_swap), encode(&second_encoded_swap));
|
||||
let combined_hex = format!(
|
||||
"{}{}",
|
||||
encode(&initial_encoded_swap),
|
||||
encode(ple_encode(vec![second_encoded_swap]))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
combined_hex,
|
||||
@@ -1073,6 +1395,8 @@ mod tests {
|
||||
"01",
|
||||
// receiver
|
||||
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
|
||||
// hook address (not set, so zero)
|
||||
"0000000000000000000000000000000000000000",
|
||||
// pool params:
|
||||
// - intermediary token USDT
|
||||
"dac17f958d2ee523a2206206994597c13d831ec7",
|
||||
@@ -1080,6 +1404,9 @@ mod tests {
|
||||
"000064",
|
||||
// - tick spacing
|
||||
"000001",
|
||||
// Second swap
|
||||
// ple encoding
|
||||
"001a",
|
||||
// - intermediary token WBTC
|
||||
"2260fac5e5542a773aa44fbcfedf7c193bc2c599",
|
||||
// - fee
|
||||
@@ -1093,7 +1420,6 @@ mod tests {
|
||||
}
|
||||
mod ekubo {
|
||||
use super::*;
|
||||
use crate::encoding::evm::utils::write_calldata_to_file;
|
||||
|
||||
const RECEIVER: &str = "ca4f73fe97d0b987a0d12b39bbd562c779bab6f6"; // Random address
|
||||
|
||||
@@ -1113,13 +1439,7 @@ mod tests {
|
||||
|
||||
let component = ProtocolComponent { static_attributes, ..Default::default() };
|
||||
|
||||
let swap = Swap {
|
||||
component,
|
||||
token_in: token_in.clone(),
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap = SwapBuilder::new(component, token_in.clone(), token_out.clone()).build();
|
||||
|
||||
let encoding_context = EncodingContext {
|
||||
receiver: RECEIVER.into(),
|
||||
@@ -1172,8 +1492,8 @@ mod tests {
|
||||
transfer_type: TransferType::Transfer,
|
||||
};
|
||||
|
||||
let first_swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
let first_swap = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
static_attributes: HashMap::from([
|
||||
("fee".to_string(), Bytes::from(0_u64)),
|
||||
("tick_spacing".to_string(), Bytes::from(0_u32)),
|
||||
@@ -1184,14 +1504,13 @@ mod tests {
|
||||
]),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: group_token_in.clone(),
|
||||
token_out: intermediary_token.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
group_token_in.clone(),
|
||||
intermediary_token.clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let second_swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
let second_swap = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
// 0.0025% fee & 0.005% base pool
|
||||
static_attributes: HashMap::from([
|
||||
("fee".to_string(), Bytes::from(461168601842738_u64)),
|
||||
@@ -1200,11 +1519,10 @@ mod tests {
|
||||
]),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: intermediary_token.clone(),
|
||||
token_out: group_token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
intermediary_token.clone(),
|
||||
group_token_out.clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let first_encoded_swap = encoder
|
||||
.encode_swap(&first_swap, &encoding_context)
|
||||
@@ -1313,18 +1631,18 @@ mod tests {
|
||||
) {
|
||||
let mut static_attributes: HashMap<String, Bytes> = HashMap::new();
|
||||
static_attributes.insert("coins".into(), Bytes::from_str(coins).unwrap());
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "pool-id".into(),
|
||||
protocol_system: String::from("vm:curve"),
|
||||
static_attributes,
|
||||
..Default::default()
|
||||
},
|
||||
token_in: Bytes::from(token_in),
|
||||
token_out: Bytes::from(token_out),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
Bytes::from(token_in),
|
||||
Bytes::from(token_out),
|
||||
)
|
||||
.build();
|
||||
|
||||
let encoder =
|
||||
CurveSwapEncoder::new(String::default(), Chain::Ethereum, curve_config()).unwrap();
|
||||
let (i, j) = encoder
|
||||
@@ -1358,13 +1676,9 @@ mod tests {
|
||||
};
|
||||
let token_in = Bytes::from("0x6B175474E89094C44Da98b954EedeAC495271d0F");
|
||||
let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
|
||||
let swap = Swap {
|
||||
component: curve_tri_pool,
|
||||
token_in: token_in.clone(),
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap =
|
||||
SwapBuilder::new(curve_tri_pool, token_in.clone(), token_out.clone()).build();
|
||||
|
||||
let encoding_context = EncodingContext {
|
||||
// The receiver was generated with `makeAddr("bob") using forge`
|
||||
receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"),
|
||||
@@ -1430,13 +1744,7 @@ mod tests {
|
||||
};
|
||||
let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
|
||||
let token_out = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3");
|
||||
let swap = Swap {
|
||||
component: curve_pool,
|
||||
token_in: token_in.clone(),
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap = SwapBuilder::new(curve_pool, token_in.clone(), token_out.clone()).build();
|
||||
let encoding_context = EncodingContext {
|
||||
// The receiver was generated with `makeAddr("bob") using forge`
|
||||
receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"),
|
||||
@@ -1503,13 +1811,7 @@ mod tests {
|
||||
};
|
||||
let token_in = Bytes::from("0x0000000000000000000000000000000000000000");
|
||||
let token_out = Bytes::from("0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84");
|
||||
let swap = Swap {
|
||||
component: curve_pool,
|
||||
token_in: token_in.clone(),
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap = SwapBuilder::new(curve_pool, token_in.clone(), token_out.clone()).build();
|
||||
let encoding_context = EncodingContext {
|
||||
// The receiver was generated with `makeAddr("bob") using forge`
|
||||
receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"),
|
||||
@@ -1577,13 +1879,7 @@ mod tests {
|
||||
};
|
||||
let token_in = Bytes::from("0x7bc3485026ac48b6cf9baf0a377477fff5703af8");
|
||||
let token_out = Bytes::from("0xc71ea051a5f82c67adcf634c36ffe6334793d24c");
|
||||
let swap = Swap {
|
||||
component: balancer_pool,
|
||||
token_in: token_in.clone(),
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap = SwapBuilder::new(balancer_pool, token_in.clone(), token_out.clone()).build();
|
||||
let encoding_context = EncodingContext {
|
||||
// The receiver was generated with `makeAddr("bob") using forge`
|
||||
receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"),
|
||||
@@ -1635,13 +1931,7 @@ mod tests {
|
||||
};
|
||||
let token_in = Bytes::from("0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f");
|
||||
let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
|
||||
let swap = Swap {
|
||||
component: maverick_pool,
|
||||
token_in: token_in.clone(),
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
let swap = SwapBuilder::new(maverick_pool, token_in.clone(), token_out.clone()).build();
|
||||
let encoding_context = EncodingContext {
|
||||
// The receiver was generated with `makeAddr("bob") using forge`
|
||||
receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"),
|
||||
@@ -1681,4 +1971,254 @@ mod tests {
|
||||
write_calldata_to_file("test_encode_maverick_v2", hex_swap.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
mod bebop {
|
||||
use num_bigint::BigUint;
|
||||
|
||||
use super::*;
|
||||
use crate::encoding::evm::testing_utils::MockRFQState;
|
||||
|
||||
fn bebop_config() -> HashMap<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..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
92
src/encoding/evm/testing_utils.rs
Normal file
92
src/encoding/evm/testing_utils.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use crate::encoding::{
|
||||
SequentialSwapStrategyEncoder, SingleSwapStrategyEncoder, SplitSwapStrategyEncoder,
|
||||
},
|
||||
swap_encoder::swap_encoder_registry::SwapEncoderRegistry,
|
||||
utils::ple_encode,
|
||||
},
|
||||
models::{
|
||||
EncodedSolution, EncodingContext, NativeAction, Solution, Transaction, TransferType,
|
||||
@@ -102,7 +103,9 @@ impl TychoRouterEncoder {
|
||||
solution
|
||||
.swaps
|
||||
.iter()
|
||||
.all(|swap| swap.split == 0.0))
|
||||
.all(|swap| swap.split == 0.0) &&
|
||||
!(solution.given_token == solution.checked_token && solution.swaps.len() > 2))
|
||||
// This is a special case for cyclical swaps
|
||||
{
|
||||
self.single_swap_strategy
|
||||
.encode_strategy(solution)?
|
||||
@@ -313,32 +316,44 @@ impl TychoExecutorEncoder {
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut grouped_protocol_data: Vec<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() {
|
||||
let transfer = if IN_TRANSFER_REQUIRED_PROTOCOLS
|
||||
.contains(&swap.component.protocol_system.as_str())
|
||||
{
|
||||
TransferType::Transfer
|
||||
} else {
|
||||
TransferType::None
|
||||
};
|
||||
let encoding_context = EncodingContext {
|
||||
receiver: solution.receiver.clone(),
|
||||
exact_out: solution.exact_out,
|
||||
router_address: None,
|
||||
group_token_in: grouped_swap.token_in.clone(),
|
||||
group_token_out: grouped_swap.token_out.clone(),
|
||||
transfer_type: transfer,
|
||||
};
|
||||
let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?;
|
||||
grouped_protocol_data.extend(protocol_data);
|
||||
if encoding_context.group_token_in == swap.token_in {
|
||||
initial_protocol_data = protocol_data;
|
||||
} else {
|
||||
grouped_protocol_data.push(protocol_data);
|
||||
}
|
||||
}
|
||||
|
||||
if !grouped_protocol_data.is_empty() {
|
||||
initial_protocol_data.extend(ple_encode(grouped_protocol_data));
|
||||
}
|
||||
|
||||
let executor_address = Bytes::from_str(swap_encoder.executor_address())
|
||||
.map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?;
|
||||
|
||||
Ok(EncodedSolution {
|
||||
swaps: grouped_protocol_data,
|
||||
swaps: initial_protocol_data,
|
||||
interacting_with: executor_address,
|
||||
permit: None,
|
||||
function_signature: "".to_string(),
|
||||
@@ -393,7 +408,7 @@ mod tests {
|
||||
use tycho_common::models::{protocol::ProtocolComponent, Chain};
|
||||
|
||||
use super::*;
|
||||
use crate::encoding::models::Swap;
|
||||
use crate::encoding::models::{Swap, SwapBuilder};
|
||||
|
||||
fn dai() -> Bytes {
|
||||
Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap()
|
||||
@@ -428,19 +443,18 @@ mod tests {
|
||||
let mut static_attributes_usdc_eth: HashMap<String, Bytes> = HashMap::new();
|
||||
static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth);
|
||||
static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth);
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d"
|
||||
.to_string(),
|
||||
protocol_system: "uniswap_v4".to_string(),
|
||||
static_attributes: static_attributes_usdc_eth,
|
||||
..Default::default()
|
||||
},
|
||||
token_in: usdc().clone(),
|
||||
token_out: eth().clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
}
|
||||
usdc().clone(),
|
||||
eth().clone(),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
fn swap_eth_pepe_univ4() -> Swap {
|
||||
@@ -449,19 +463,18 @@ mod tests {
|
||||
let mut static_attributes_eth_pepe: HashMap<String, Bytes> = HashMap::new();
|
||||
static_attributes_eth_pepe.insert("key_lp_fee".into(), pool_fee_eth_pepe);
|
||||
static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe);
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9"
|
||||
.to_string(),
|
||||
protocol_system: "uniswap_v4".to_string(),
|
||||
static_attributes: static_attributes_eth_pepe,
|
||||
..Default::default()
|
||||
},
|
||||
token_in: eth().clone(),
|
||||
token_out: pepe().clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
}
|
||||
eth().clone(),
|
||||
pepe().clone(),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
fn router_address() -> Bytes {
|
||||
@@ -499,17 +512,16 @@ mod tests {
|
||||
fn test_encode_router_calldata_single_swap() {
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
let eth_amount_in = BigUint::from(1000u32);
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
weth().clone(),
|
||||
dai().clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
@@ -564,29 +576,27 @@ mod tests {
|
||||
fn test_encode_router_calldata_sequential_swap() {
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
let eth_amount_in = BigUint::from(1000u32);
|
||||
let swap_weth_dai = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap_weth_dai = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
weth().clone(),
|
||||
dai().clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let swap_dai_usdc = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap_dai_usdc = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: usdc(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
dai().clone(),
|
||||
usdc().clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
@@ -656,17 +666,16 @@ mod tests {
|
||||
#[test]
|
||||
fn test_validate_passes_for_wrap() {
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
weth().clone(),
|
||||
dai().clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
@@ -685,17 +694,16 @@ mod tests {
|
||||
#[test]
|
||||
fn test_validate_fails_for_wrap_wrong_input() {
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
weth().clone(),
|
||||
dai().clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
@@ -719,17 +727,16 @@ mod tests {
|
||||
#[test]
|
||||
fn test_validate_fails_for_wrap_wrong_first_swap() {
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: eth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
eth().clone(),
|
||||
dai().clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
@@ -773,17 +780,16 @@ mod tests {
|
||||
#[test]
|
||||
fn test_validate_passes_for_unwrap() {
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: weth(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
dai().clone(),
|
||||
weth().clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
@@ -801,17 +807,16 @@ mod tests {
|
||||
#[test]
|
||||
fn test_validate_fails_for_unwrap_wrong_output() {
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: weth(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
dai().clone(),
|
||||
weth().clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
@@ -836,17 +841,16 @@ mod tests {
|
||||
#[test]
|
||||
fn test_validate_fails_for_unwrap_wrong_last_swap() {
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: eth(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
dai().clone(),
|
||||
eth().clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
@@ -876,39 +880,36 @@ mod tests {
|
||||
// (some of the pool addresses in this test are fake)
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
let swaps = vec![
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: weth(),
|
||||
split: 0.5f64,
|
||||
user_data: None,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
dai().clone(),
|
||||
weth().clone(),
|
||||
)
|
||||
.build(),
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: weth(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
dai().clone(),
|
||||
weth().clone(),
|
||||
)
|
||||
.build(),
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
},
|
||||
weth().clone(),
|
||||
dai().clone(),
|
||||
)
|
||||
.build(),
|
||||
];
|
||||
|
||||
let solution = Solution {
|
||||
@@ -931,50 +932,46 @@ mod tests {
|
||||
// (some of the pool addresses in this test are fake)
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
let swaps = vec![
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: weth(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
dai().clone(),
|
||||
weth().clone(),
|
||||
)
|
||||
.build(),
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth(),
|
||||
token_out: usdc(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
weth().clone(),
|
||||
usdc().clone(),
|
||||
)
|
||||
.build(),
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: usdc(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
usdc().clone(),
|
||||
dai().clone(),
|
||||
)
|
||||
.build(),
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: wbtc(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
},
|
||||
dai().clone(),
|
||||
wbtc().clone(),
|
||||
)
|
||||
.build(),
|
||||
];
|
||||
|
||||
let solution = Solution {
|
||||
@@ -1004,39 +1001,37 @@ mod tests {
|
||||
// (some of the pool addresses in this test are fake)
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
let swaps = vec![
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
weth(),
|
||||
dai(),
|
||||
)
|
||||
.build(),
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: weth(),
|
||||
split: 0.5f64,
|
||||
user_data: None,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
dai(),
|
||||
weth(),
|
||||
)
|
||||
.split(0.5)
|
||||
.build(),
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: weth(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
},
|
||||
dai(),
|
||||
weth(),
|
||||
)
|
||||
.build(),
|
||||
];
|
||||
|
||||
let solution = Solution {
|
||||
@@ -1059,28 +1054,26 @@ mod tests {
|
||||
// (some of the pool addresses in this test are fake)
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
let swaps = vec![
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: weth(),
|
||||
token_out: dai(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
},
|
||||
Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
weth(),
|
||||
dai(),
|
||||
)
|
||||
.build(),
|
||||
SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: dai(),
|
||||
token_out: weth(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
},
|
||||
dai(),
|
||||
weth(),
|
||||
)
|
||||
.build(),
|
||||
];
|
||||
|
||||
let solution = Solution {
|
||||
@@ -1114,7 +1107,7 @@ mod tests {
|
||||
use tycho_common::{models::protocol::ProtocolComponent, Bytes};
|
||||
|
||||
use super::*;
|
||||
use crate::encoding::models::{Solution, Swap};
|
||||
use crate::encoding::models::Solution;
|
||||
|
||||
#[test]
|
||||
fn test_executor_encoder_encode() {
|
||||
@@ -1124,17 +1117,16 @@ mod tests {
|
||||
let token_in = weth();
|
||||
let token_out = dai();
|
||||
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: token_in.clone(),
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
token_in.clone(),
|
||||
token_out.clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
@@ -1185,16 +1177,16 @@ mod tests {
|
||||
let token_in = weth();
|
||||
let token_out = dai();
|
||||
|
||||
let swap = Swap {
|
||||
component: ProtocolComponent {
|
||||
let swap = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: token_in.clone(),
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
};
|
||||
token_in.clone(),
|
||||
token_out.clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
@@ -1256,12 +1248,16 @@ mod tests {
|
||||
"01",
|
||||
// receiver
|
||||
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
|
||||
// hook address (not set, so zero)
|
||||
"0000000000000000000000000000000000000000",
|
||||
// first pool intermediary token (ETH)
|
||||
"0000000000000000000000000000000000000000",
|
||||
// fee
|
||||
"000bb8",
|
||||
// tick spacing
|
||||
"00003c",
|
||||
// ple encoding
|
||||
"001a",
|
||||
// second pool intermediary token (PEPE)
|
||||
"6982508145454ce325ddbe47a25d4ec3d2311933",
|
||||
// fee
|
||||
|
||||
@@ -78,6 +78,9 @@ pub fn get_static_attribute(swap: &Swap, attribute_name: &str) -> Result<Vec<u8>
|
||||
.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> {
|
||||
match Handle::try_current() {
|
||||
Ok(h) => Ok((h, None)),
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::ValueEnum;
|
||||
use num_bigint::BigUint;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tycho_common::{models::protocol::ProtocolComponent, Bytes};
|
||||
use tycho_common::{
|
||||
models::protocol::ProtocolComponent, simulation::protocol_sim::ProtocolSim, Bytes,
|
||||
};
|
||||
|
||||
use crate::encoding::serde_primitives::biguint_string;
|
||||
|
||||
@@ -71,7 +75,7 @@ pub enum NativeAction {
|
||||
}
|
||||
|
||||
/// Represents a swap operation to be performed on a pool.
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Swap {
|
||||
/// Protocol component from tycho indexer
|
||||
pub component: ProtocolComponent,
|
||||
@@ -84,6 +88,12 @@ pub struct Swap {
|
||||
pub split: f64,
|
||||
/// Optional user data to be passed to encoding.
|
||||
pub user_data: Option<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 {
|
||||
@@ -93,8 +103,90 @@ impl Swap {
|
||||
token_out: Bytes,
|
||||
split: f64,
|
||||
user_data: Option<Bytes>,
|
||||
protocol_state: Option<Arc<dyn ProtocolSim>>,
|
||||
estimated_amount_in: Option<BigUint>,
|
||||
) -> 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(),
|
||||
};
|
||||
let user_data = Some(Bytes::from("0x1234"));
|
||||
let swap =
|
||||
Swap::new(component, Bytes::from("0x12"), Bytes::from("34"), 0.5, user_data.clone());
|
||||
let swap = Swap::new(
|
||||
component,
|
||||
Bytes::from("0x12"),
|
||||
Bytes::from("34"),
|
||||
0.5,
|
||||
user_data.clone(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(swap.token_in, Bytes::from("0x12"));
|
||||
assert_eq!(swap.token_out, Bytes::from("0x34"));
|
||||
assert_eq!(swap.component.protocol_system, "uniswap_v2");
|
||||
|
||||
@@ -3,7 +3,10 @@ pub mod encoding;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use alloy::{primitives::B256, signers::local::PrivateKeySigner};
|
||||
use alloy::{
|
||||
primitives::{B256, U256},
|
||||
signers::local::PrivateKeySigner,
|
||||
};
|
||||
use tycho_common::{models::Chain, Bytes};
|
||||
use tycho_execution::encoding::{
|
||||
evm::encoder_builders::TychoRouterEncoderBuilder, models::UserTransferType,
|
||||
@@ -14,6 +17,14 @@ pub fn router_address() -> Bytes {
|
||||
Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap()
|
||||
}
|
||||
|
||||
pub fn bob_address() -> Bytes {
|
||||
Bytes::from_str("0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e").unwrap()
|
||||
}
|
||||
|
||||
pub fn alice_address() -> Bytes {
|
||||
Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap()
|
||||
}
|
||||
|
||||
pub fn eth_chain() -> Chain {
|
||||
Chain::Ethereum
|
||||
}
|
||||
@@ -46,6 +57,10 @@ pub fn usdt() -> Bytes {
|
||||
Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap()
|
||||
}
|
||||
|
||||
pub fn ondo() -> Bytes {
|
||||
Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap()
|
||||
}
|
||||
|
||||
pub fn get_signer() -> PrivateKeySigner {
|
||||
// Set up a mock private key for signing (Alice's pk in our contract tests)
|
||||
let private_key =
|
||||
@@ -57,10 +72,26 @@ pub fn get_signer() -> PrivateKeySigner {
|
||||
|
||||
pub fn get_tycho_router_encoder(user_transfer_type: UserTransferType) -> Box<dyn TychoEncoder> {
|
||||
TychoRouterEncoderBuilder::new()
|
||||
.chain(tycho_common::models::Chain::Ethereum)
|
||||
.chain(Chain::Ethereum)
|
||||
.user_transfer_type(user_transfer_type)
|
||||
.executors_file_path("config/test_executor_addresses.json".to_string())
|
||||
.router_address(router_address())
|
||||
.build()
|
||||
.expect("Failed to build encoder")
|
||||
}
|
||||
|
||||
/// Builds the complete Bebop calldata in the format expected by the encoder
|
||||
/// Returns: [ partial_fill_offset (u8) | original_taker_amount (U256) | calldata (bytes (selector +
|
||||
/// ABI encoded params)) ]
|
||||
pub fn build_bebop_calldata(
|
||||
calldata: &[u8],
|
||||
partial_fill_offset: u8,
|
||||
original_taker_amount: U256,
|
||||
) -> Bytes {
|
||||
let mut user_data = Vec::with_capacity(1 + 32 + calldata.len());
|
||||
user_data.push(partial_fill_offset);
|
||||
user_data.extend_from_slice(&original_taker_amount.to_be_bytes::<32>());
|
||||
user_data.extend_from_slice(calldata);
|
||||
|
||||
Bytes::from(user_data)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use std::{collections::HashMap, str::FromStr, sync::Arc};
|
||||
|
||||
use alloy::hex::encode;
|
||||
use num_bigint::{BigInt, BigUint};
|
||||
use tycho_common::{models::protocol::ProtocolComponent, Bytes};
|
||||
use tycho_execution::encoding::{
|
||||
evm::utils::write_calldata_to_file,
|
||||
models::{Solution, Swap, UserTransferType},
|
||||
evm::{
|
||||
testing_utils::MockRFQState,
|
||||
utils::{biguint_to_u256, write_calldata_to_file},
|
||||
},
|
||||
models::{Solution, Swap, SwapBuilder, UserTransferType},
|
||||
};
|
||||
|
||||
use crate::common::{
|
||||
encoding::encode_tycho_router_call, eth, eth_chain, get_signer, get_tycho_router_encoder, weth,
|
||||
alice_address, bob_address, encoding::encode_tycho_router_call, eth, eth_chain, get_signer,
|
||||
get_tycho_router_encoder, usdc, wbtc, weth,
|
||||
};
|
||||
|
||||
mod common;
|
||||
@@ -48,6 +52,8 @@ fn test_uniswap_v3_uniswap_v2() {
|
||||
token_out: wbtc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let swap_wbtc_usdc = Swap {
|
||||
component: ProtocolComponent {
|
||||
@@ -59,6 +65,8 @@ fn test_uniswap_v3_uniswap_v2() {
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
|
||||
@@ -127,6 +135,8 @@ fn test_uniswap_v3_uniswap_v3() {
|
||||
token_out: wbtc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let swap_wbtc_usdc = Swap {
|
||||
component: ProtocolComponent {
|
||||
@@ -146,6 +156,8 @@ fn test_uniswap_v3_uniswap_v3() {
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
|
||||
@@ -213,6 +225,8 @@ fn test_uniswap_v3_curve() {
|
||||
token_out: wbtc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let swap_wbtc_usdt = Swap {
|
||||
@@ -242,6 +256,8 @@ fn test_uniswap_v3_curve() {
|
||||
token_out: usdt.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
|
||||
@@ -301,6 +317,8 @@ fn test_balancer_v2_uniswap_v2() {
|
||||
token_out: wbtc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let swap_wbtc_usdc = Swap {
|
||||
@@ -313,6 +331,8 @@ fn test_balancer_v2_uniswap_v2() {
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
|
||||
@@ -375,6 +395,8 @@ fn test_multi_protocol() {
|
||||
token_out: weth.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let balancer_swap_weth_wbtc = Swap {
|
||||
@@ -387,6 +409,8 @@ fn test_multi_protocol() {
|
||||
token_out: wbtc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let curve_swap_wbtc_usdt = Swap {
|
||||
@@ -416,6 +440,8 @@ fn test_multi_protocol() {
|
||||
token_out: usdt.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
// Ekubo
|
||||
@@ -438,6 +464,8 @@ fn test_multi_protocol() {
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
// USV4
|
||||
@@ -461,6 +489,8 @@ fn test_multi_protocol() {
|
||||
token_out: eth.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
|
||||
@@ -533,6 +563,8 @@ fn test_uniswap_v3_balancer_v3() {
|
||||
token_out: wbtc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let swap_wbtc_qnt = Swap {
|
||||
component: ProtocolComponent {
|
||||
@@ -544,6 +576,8 @@ fn test_uniswap_v3_balancer_v3() {
|
||||
token_out: qnt.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
|
||||
@@ -578,3 +612,227 @@ fn test_uniswap_v3_balancer_v3() {
|
||||
let hex_calldata = encode(&calldata);
|
||||
write_calldata_to_file("test_uniswap_v3_balancer_v3", hex_calldata.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uniswap_v3_bebop() {
|
||||
// Note: This test does not assert anything. It is only used to obtain
|
||||
// integration test data for our router solidity test.
|
||||
//
|
||||
// Performs a sequential swap from WETH to WBTC through USDC using USV3 and
|
||||
// Bebop RFQ
|
||||
//
|
||||
// WETH ───(USV3)──> USDC ───(Bebop RFQ)──> WBTC
|
||||
|
||||
let weth = weth();
|
||||
let usdc = usdc();
|
||||
let wbtc = wbtc();
|
||||
|
||||
// First swap: WETH -> USDC via UniswapV3
|
||||
let swap_weth_usdc = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* WETH-USDC USV3 Pool
|
||||
* 0.05% */
|
||||
protocol_system: "uniswap_v3".to_string(),
|
||||
static_attributes: {
|
||||
let mut attrs = HashMap::new();
|
||||
attrs
|
||||
.insert("fee".to_string(), Bytes::from(BigInt::from(500).to_signed_bytes_be()));
|
||||
attrs
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
weth.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
// Second swap: USDC -> WBTC via Bebop RFQ using real order data
|
||||
let bebop_calldata = Bytes::from_str("0x4dcebcba00000000000000000000000000000000000000000000000000000000689dcb3c0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000bee3211ab312a8d065c4fef0247448e17a8da0000000000000000000000000000000000000000000000000002901f2d62bc91b77000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000000000000000000000000000000000007881786100000000000000000000000000000000000000000000000000000000001984730000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000000000000000000000000000000000000000000000000000000000000000a02bc8495ad1c76c31d466ce719f80400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041f3a03b07f390cd707402912278414c46190ca8ca362dd218b9a58956178cb6ee0e5755db7abe02fe15d498d092d4c6865a5eb18486b3e45e27d50d34b87bf1e21c00000000000000000000000000000000000000000000000000000000000000").unwrap();
|
||||
let partial_fill_offset = 12u64;
|
||||
let quote_amount_out = BigUint::from_str("1672307").unwrap();
|
||||
|
||||
let bebop_state = MockRFQState {
|
||||
quote_amount_out,
|
||||
quote_data: HashMap::from([
|
||||
("calldata".to_string(), bebop_calldata),
|
||||
(
|
||||
"partial_fill_offset".to_string(),
|
||||
Bytes::from(
|
||||
partial_fill_offset
|
||||
.to_be_bytes()
|
||||
.to_vec(),
|
||||
),
|
||||
),
|
||||
]),
|
||||
};
|
||||
|
||||
let bebop_component = ProtocolComponent {
|
||||
id: String::from("bebop-rfq"),
|
||||
protocol_system: String::from("rfq:bebop"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let swap_usdc_wbtc = SwapBuilder::new(bebop_component, usdc.clone(), wbtc.clone())
|
||||
.estimated_amount_in(BigUint::from_str("2021750881").unwrap())
|
||||
.protocol_state(Arc::new(bebop_state))
|
||||
.build();
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_token: weth,
|
||||
given_amount: BigUint::from_str("1000000000000000000").unwrap(), // 0.099 WETH
|
||||
checked_token: wbtc,
|
||||
checked_amount: BigUint::from_str("1672307").unwrap(),
|
||||
sender: bob_address(),
|
||||
receiver: bob_address(),
|
||||
swaps: vec![swap_weth_usdc, swap_usdc_wbtc],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let encoded_solution = encoder
|
||||
.encode_solutions(vec![solution.clone()])
|
||||
.unwrap()[0]
|
||||
.clone();
|
||||
|
||||
let calldata = encode_tycho_router_call(
|
||||
eth_chain().id(),
|
||||
encoded_solution,
|
||||
&solution,
|
||||
&UserTransferType::TransferFrom,
|
||||
ð(),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
.data;
|
||||
|
||||
let hex_calldata = encode(&calldata);
|
||||
write_calldata_to_file("test_uniswap_v3_bebop", hex_calldata.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_uniswap_v3_hashflow() {
|
||||
// Note: This test does not assert anything. It is only used to obtain
|
||||
// integration test data for our router solidity test.
|
||||
//
|
||||
// Performs a sequential swap from WETH to WBTC through USDC using USV3 and
|
||||
// Hashflow RFQ
|
||||
//
|
||||
// WETH ───(USV3)──> USDC ───(Hashflow RFQ)──> WBTC
|
||||
|
||||
let weth = weth();
|
||||
let usdc = usdc();
|
||||
let wbtc = wbtc();
|
||||
|
||||
// First swap: WETH -> USDC via UniswapV3
|
||||
let swap_weth_usdc = SwapBuilder::new(
|
||||
ProtocolComponent {
|
||||
id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* WETH-USDC USV3 Pool
|
||||
* 0.05% */
|
||||
protocol_system: "uniswap_v3".to_string(),
|
||||
static_attributes: {
|
||||
let mut attrs = HashMap::new();
|
||||
attrs
|
||||
.insert("fee".to_string(), Bytes::from(BigInt::from(500).to_signed_bytes_be()));
|
||||
attrs
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
weth.clone(),
|
||||
usdc.clone(),
|
||||
)
|
||||
.build();
|
||||
|
||||
// Second swap: USDC -> WBTC via Hashflow RFQ using real order data
|
||||
let quote_amount_out = BigUint::from_str("3714751").unwrap();
|
||||
|
||||
let hashflow_state = MockRFQState {
|
||||
quote_amount_out,
|
||||
quote_data: HashMap::from([
|
||||
(
|
||||
"pool".to_string(),
|
||||
Bytes::from_str("0x478eca1b93865dca0b9f325935eb123c8a4af011").unwrap(),
|
||||
),
|
||||
(
|
||||
"external_account".to_string(),
|
||||
Bytes::from_str("0xbee3211ab312a8d065c4fef0247448e17a8da000").unwrap(),
|
||||
),
|
||||
(
|
||||
"trader".to_string(),
|
||||
Bytes::from_str("0xcd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2").unwrap(),
|
||||
),
|
||||
(
|
||||
"base_token".to_string(),
|
||||
Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
|
||||
),
|
||||
(
|
||||
"quote_token".to_string(),
|
||||
Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(),
|
||||
),
|
||||
(
|
||||
"base_token_amount".to_string(),
|
||||
Bytes::from(biguint_to_u256(&BigUint::from(4308094737_u64)).to_be_bytes::<32>().to_vec()),
|
||||
),
|
||||
(
|
||||
"quote_token_amount".to_string(),
|
||||
Bytes::from(biguint_to_u256(&BigUint::from(3714751_u64)).to_be_bytes::<32>().to_vec()),
|
||||
),
|
||||
("quote_expiry".to_string(), Bytes::from(biguint_to_u256(&BigUint::from(1755610328_u64)).to_be_bytes::<32>().to_vec())),
|
||||
("nonce".to_string(), Bytes::from(biguint_to_u256(&BigUint::from(1755610283723_u64)).to_be_bytes::<32>().to_vec())),
|
||||
(
|
||||
"tx_id".to_string(),
|
||||
Bytes::from_str(
|
||||
"0x125000064000640000001747eb8c38ffffffffffffff0029642016edb36d0000",
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
("signature".to_string(), Bytes::from_str("0x6ddb3b21fe8509e274ddf46c55209cdbf30360944abbca6569ed6b26740d052f419964dcb5a3bdb98b4ed1fb3642a2760b8312118599a962251f7a8f73fe4fbe1c").unwrap()),
|
||||
]),
|
||||
};
|
||||
|
||||
let hashflow_component = ProtocolComponent {
|
||||
id: String::from("hashflow-rfq"),
|
||||
protocol_system: String::from("rfq:hashflow"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let swap_usdc_wbtc = SwapBuilder::new(hashflow_component, usdc.clone(), wbtc.clone())
|
||||
.estimated_amount_in(BigUint::from_str("4308094737").unwrap())
|
||||
.protocol_state(Arc::new(hashflow_state))
|
||||
.build();
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_token: weth,
|
||||
given_amount: BigUint::from_str("1000000000000000000").unwrap(),
|
||||
checked_token: wbtc,
|
||||
checked_amount: BigUint::from_str("3714751").unwrap(),
|
||||
sender: alice_address(),
|
||||
receiver: alice_address(),
|
||||
swaps: vec![swap_weth_usdc, swap_usdc_wbtc],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let encoded_solution = encoder
|
||||
.encode_solutions(vec![solution.clone()])
|
||||
.unwrap()[0]
|
||||
.clone();
|
||||
|
||||
let calldata = encode_tycho_router_call(
|
||||
eth_chain().id(),
|
||||
encoded_solution,
|
||||
&solution,
|
||||
&UserTransferType::TransferFrom,
|
||||
ð(),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
.data;
|
||||
|
||||
let hex_calldata = encode(&calldata);
|
||||
write_calldata_to_file("test_uniswap_v3_hashflow", hex_calldata.as_str());
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
mod common;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use std::{collections::HashMap, str::FromStr, sync::Arc};
|
||||
|
||||
use alloy::hex::encode;
|
||||
use alloy::{hex, hex::encode};
|
||||
use num_bigint::{BigInt, BigUint};
|
||||
use tycho_common::{models::protocol::ProtocolComponent, Bytes};
|
||||
use tycho_execution::encoding::{
|
||||
evm::utils::write_calldata_to_file,
|
||||
models::{Solution, Swap, UserTransferType},
|
||||
evm::{
|
||||
testing_utils::MockRFQState,
|
||||
utils::{biguint_to_u256, write_calldata_to_file},
|
||||
},
|
||||
models::{Solution, Swap, SwapBuilder, UserTransferType},
|
||||
};
|
||||
|
||||
use crate::common::{
|
||||
encoding::encode_tycho_router_call, eth, eth_chain, get_signer, get_tycho_router_encoder, pepe,
|
||||
usdc, weth,
|
||||
alice_address, encoding::encode_tycho_router_call, eth, eth_chain, get_signer,
|
||||
get_tycho_router_encoder, ondo, pepe, usdc, wbtc, weth,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_single_encoding_strategy_ekubo() {
|
||||
// ETH ──(EKUBO)──> USDC
|
||||
@@ -40,6 +44,8 @@ fn test_single_encoding_strategy_ekubo() {
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
@@ -92,6 +98,8 @@ fn test_single_encoding_strategy_maverick() {
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
@@ -156,6 +164,8 @@ fn test_single_encoding_strategy_usv4_eth_in() {
|
||||
token_out: pepe.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
|
||||
|
||||
@@ -222,6 +232,8 @@ fn test_single_encoding_strategy_usv4_eth_out() {
|
||||
token_out: eth.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
|
||||
@@ -296,6 +308,8 @@ fn test_single_encoding_strategy_usv4_grouped_swap() {
|
||||
token_out: eth.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let swap_eth_pepe = Swap {
|
||||
@@ -309,6 +323,8 @@ fn test_single_encoding_strategy_usv4_grouped_swap() {
|
||||
token_out: pepe.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
|
||||
|
||||
@@ -357,7 +373,7 @@ fn test_single_encoding_strategy_usv4_grouped_swap() {
|
||||
|
||||
let expected_swaps = String::from(concat!(
|
||||
// length of ple encoded swaps without padding
|
||||
"0000000000000000000000000000000000000000000000000000000000000086",
|
||||
"000000000000000000000000000000000000000000000000000000000000009c",
|
||||
// Swap data header
|
||||
"f62849f9a0b5bf2913b396098f7c7019b51a820a", // executor address
|
||||
// Protocol data
|
||||
@@ -366,15 +382,18 @@ fn test_single_encoding_strategy_usv4_grouped_swap() {
|
||||
"00", // zero2one
|
||||
"00", // transfer type TransferFrom
|
||||
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver
|
||||
"0000000000000000000000000000000000000000", // hook address
|
||||
// First pool params
|
||||
"0000000000000000000000000000000000000000", // intermediary token (ETH)
|
||||
"000bb8", // fee
|
||||
"00003c", // tick spacing
|
||||
// ple encoding
|
||||
"001a",
|
||||
// Second pool params
|
||||
"6982508145454ce325ddbe47a25d4ec3d2311933", // intermediary token (PEPE)
|
||||
"0061a8", // fee
|
||||
"0001f4", // tick spacing
|
||||
"0000000000000000000000000000000000000000000000000000" // padding
|
||||
"00000000" // padding
|
||||
));
|
||||
|
||||
let hex_calldata = encode(&calldata);
|
||||
@@ -417,6 +436,8 @@ fn test_single_encoding_strategy_curve() {
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
@@ -484,6 +505,8 @@ fn test_single_encoding_strategy_curve_st_eth() {
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
@@ -537,6 +560,8 @@ fn test_single_encoding_strategy_balancer_v3() {
|
||||
token_out: token_out.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
@@ -572,3 +597,256 @@ fn test_single_encoding_strategy_balancer_v3() {
|
||||
let hex_calldata = encode(&calldata);
|
||||
write_calldata_to_file("test_single_encoding_strategy_balancer_v3", hex_calldata.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_encoding_strategy_bebop() {
|
||||
// The quote was done separately where the sender is the router and the receiver is a random
|
||||
// user
|
||||
let _router = Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap();
|
||||
let user = Bytes::from_str("0xd2068e04cf586f76eece7ba5beb779d7bb1474a1").unwrap();
|
||||
|
||||
let token_in = usdc();
|
||||
let token_out = ondo();
|
||||
let amount_in = BigUint::from_str("200000000").unwrap(); // 200 USDC
|
||||
let amount_out = BigUint::from_str("194477331556159832309").unwrap(); // 203.8 ONDO
|
||||
|
||||
let partial_fill_offset = 12u64;
|
||||
let bebop_calldata = Bytes::from_str("0x4dcebcba00000000000000000000000000000000000000000000000000000000689b548f0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000067336cec42645f55059eff241cb02ea5cc52ff86000000000000000000000000000000000000000000000000279ead5d9685f25b000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be3000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000a8aea46aa4ec5c0f5000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000000005230bcb979c81cebf94a3b5c08bcfa300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000414ce40058ff07f11d9224c2c8d1e58369e4a90173856202d8d2a17da48058ad683dedb742eda0d4c0cf04cf1c09138898dd7fd06f97268ea7f74ef9b42d29bf4c1b00000000000000000000000000000000000000000000000000000000000000").unwrap();
|
||||
let bebop_state = MockRFQState {
|
||||
quote_amount_out: amount_out.clone(),
|
||||
quote_data: HashMap::from([
|
||||
("calldata".to_string(), bebop_calldata),
|
||||
(
|
||||
"partial_fill_offset".to_string(),
|
||||
Bytes::from(
|
||||
partial_fill_offset
|
||||
.to_be_bytes()
|
||||
.to_vec(),
|
||||
),
|
||||
),
|
||||
]),
|
||||
};
|
||||
|
||||
let bebop_component = ProtocolComponent {
|
||||
id: String::from("bebop-rfq"),
|
||||
protocol_system: String::from("rfq:bebop"),
|
||||
static_attributes: HashMap::new(), // No static attributes needed
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let swap = SwapBuilder::new(bebop_component, token_in.clone(), token_out.clone())
|
||||
.estimated_amount_in(BigUint::from_str("200000000").unwrap())
|
||||
.protocol_state(Arc::new(bebop_state))
|
||||
.build();
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_token: token_in,
|
||||
given_amount: amount_in,
|
||||
checked_token: token_out,
|
||||
checked_amount: amount_out, // Expected output amount
|
||||
sender: user.clone(),
|
||||
receiver: user,
|
||||
swaps: vec![swap],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let encoded_solution = encoder
|
||||
.encode_solutions(vec![solution.clone()])
|
||||
.unwrap()[0]
|
||||
.clone();
|
||||
|
||||
let calldata = encode_tycho_router_call(
|
||||
eth_chain().id(),
|
||||
encoded_solution,
|
||||
&solution,
|
||||
&UserTransferType::TransferFrom,
|
||||
ð(),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
.data;
|
||||
let hex_calldata = hex::encode(&calldata);
|
||||
write_calldata_to_file("test_single_encoding_strategy_bebop", hex_calldata.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_encoding_strategy_bebop_aggregate() {
|
||||
// The quote was done separately where the sender is the router and the receiver is a random
|
||||
// user
|
||||
let _router = Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap();
|
||||
let user = Bytes::from_str("0xd2068e04cf586f76eece7ba5beb779d7bb1474a1").unwrap();
|
||||
|
||||
let token_in = usdc();
|
||||
let token_out = ondo();
|
||||
let amount_in = BigUint::from_str("20000000000").unwrap(); // 20k USDC
|
||||
let amount_out = BigUint::from_str("18699321819466078474202").unwrap(); // 203.8 ONDO
|
||||
let partial_fill_offset = 2u64;
|
||||
|
||||
let bebop_calldata = Bytes::from_str("0xa2f7489300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000689b78880000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c0000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000005a060a5c2aaaaa2fe2cda34423cac76a84c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000002901f2d62bb356ca0000000000000000000000000000000000000000000000002901f2d62bb356cb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000044f83c726000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000589400da00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000003aa5f96046644f6e37a000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000004b51a26526ddbeec60000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000417ab4332f2b091d87d56d04eee35dd49452782c782de71608c0425c5ae41f1d7e147173851c870d76720ce07d45cd8622352716b1c7965819ee2bf8c573c499ae1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000410c8da2637aa929e11caff9afdfc4c489320c6dba77cc934d88ba8956e365fd1d48983087c6e474bbb828181cdfdd17317c4c9c3ee4bc98e3769d0c05cc7a285e1c00000000000000000000000000000000000000000000000000000000000000").unwrap();
|
||||
let bebop_state = MockRFQState {
|
||||
quote_amount_out: amount_out.clone(),
|
||||
quote_data: HashMap::from([
|
||||
("calldata".to_string(), bebop_calldata),
|
||||
(
|
||||
"partial_fill_offset".to_string(),
|
||||
Bytes::from(
|
||||
partial_fill_offset
|
||||
.to_be_bytes()
|
||||
.to_vec(),
|
||||
),
|
||||
),
|
||||
]),
|
||||
};
|
||||
|
||||
let bebop_component = ProtocolComponent {
|
||||
id: String::from("bebop-rfq"),
|
||||
protocol_system: String::from("rfq:bebop"),
|
||||
static_attributes: HashMap::new(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let swap = SwapBuilder::new(bebop_component, token_in.clone(), token_out.clone())
|
||||
.estimated_amount_in(BigUint::from_str("20000000000").unwrap())
|
||||
.protocol_state(Arc::new(bebop_state))
|
||||
.build();
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_token: token_in.clone(),
|
||||
given_amount: amount_in,
|
||||
checked_token: token_out,
|
||||
checked_amount: amount_out,
|
||||
sender: user.clone(),
|
||||
receiver: user,
|
||||
swaps: vec![swap],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let encoded_solution = encoder
|
||||
.encode_solutions(vec![solution.clone()])
|
||||
.unwrap()[0]
|
||||
.clone();
|
||||
|
||||
let calldata = encode_tycho_router_call(
|
||||
eth_chain().id(),
|
||||
encoded_solution,
|
||||
&solution,
|
||||
&UserTransferType::TransferFrom,
|
||||
ð(),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
.data;
|
||||
let hex_calldata = hex::encode(&calldata);
|
||||
|
||||
write_calldata_to_file("test_single_encoding_strategy_bebop_aggregate", hex_calldata.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_encoding_strategy_hashflow() {
|
||||
// Note: This test does not assert anything. It is only used to obtain
|
||||
// integration test data for our router solidity test.
|
||||
//
|
||||
// Performs a swap from USDC to WBTC using Hashflow RFQ
|
||||
//
|
||||
// USDC ───(Hashflow RFQ)──> WBTC
|
||||
|
||||
let usdc = usdc();
|
||||
let wbtc = wbtc();
|
||||
|
||||
// USDC -> WBTC via Hashflow RFQ using real order data
|
||||
let quote_amount_out = BigUint::from_str("3714751").unwrap();
|
||||
|
||||
let hashflow_state = MockRFQState {
|
||||
quote_amount_out,
|
||||
quote_data: HashMap::from([
|
||||
(
|
||||
"pool".to_string(),
|
||||
Bytes::from_str("0x478eca1b93865dca0b9f325935eb123c8a4af011").unwrap(),
|
||||
),
|
||||
(
|
||||
"external_account".to_string(),
|
||||
Bytes::from_str("0xbee3211ab312a8d065c4fef0247448e17a8da000").unwrap(),
|
||||
),
|
||||
(
|
||||
"trader".to_string(),
|
||||
Bytes::from_str("0xcd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2").unwrap(),
|
||||
),
|
||||
(
|
||||
"base_token".to_string(),
|
||||
Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
|
||||
),
|
||||
(
|
||||
"quote_token".to_string(),
|
||||
Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(),
|
||||
),
|
||||
(
|
||||
"base_token_amount".to_string(),
|
||||
Bytes::from(biguint_to_u256(&BigUint::from(4308094737_u64)).to_be_bytes::<32>().to_vec()),
|
||||
),
|
||||
(
|
||||
"quote_token_amount".to_string(),
|
||||
Bytes::from(biguint_to_u256(&BigUint::from(3714751_u64)).to_be_bytes::<32>().to_vec()),
|
||||
),
|
||||
("quote_expiry".to_string(), Bytes::from(biguint_to_u256(&BigUint::from(1755610328_u64)).to_be_bytes::<32>().to_vec())),
|
||||
("nonce".to_string(), Bytes::from(biguint_to_u256(&BigUint::from(1755610283723_u64)).to_be_bytes::<32>().to_vec())),
|
||||
(
|
||||
"tx_id".to_string(),
|
||||
Bytes::from_str(
|
||||
"0x125000064000640000001747eb8c38ffffffffffffff0029642016edb36d0000",
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
("signature".to_string(), Bytes::from_str("0x6ddb3b21fe8509e274ddf46c55209cdbf30360944abbca6569ed6b26740d052f419964dcb5a3bdb98b4ed1fb3642a2760b8312118599a962251f7a8f73fe4fbe1c").unwrap()),
|
||||
]),
|
||||
};
|
||||
|
||||
let hashflow_component = ProtocolComponent {
|
||||
id: String::from("hashflow-rfq"),
|
||||
protocol_system: String::from("rfq:hashflow"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let swap_usdc_wbtc = SwapBuilder::new(hashflow_component, usdc.clone(), wbtc.clone())
|
||||
.estimated_amount_in(BigUint::from_str("4308094737").unwrap())
|
||||
.protocol_state(Arc::new(hashflow_state))
|
||||
.build();
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_token: usdc,
|
||||
given_amount: BigUint::from_str("4308094737").unwrap(),
|
||||
checked_token: wbtc,
|
||||
checked_amount: BigUint::from_str("3714751").unwrap(),
|
||||
sender: alice_address(),
|
||||
receiver: alice_address(),
|
||||
swaps: vec![swap_usdc_wbtc],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let encoded_solution = encoder
|
||||
.encode_solutions(vec![solution.clone()])
|
||||
.unwrap()[0]
|
||||
.clone();
|
||||
|
||||
let calldata = encode_tycho_router_call(
|
||||
eth_chain().id(),
|
||||
encoded_solution,
|
||||
&solution,
|
||||
&UserTransferType::TransferFrom,
|
||||
ð(),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
.data;
|
||||
|
||||
let hex_calldata = encode(&calldata);
|
||||
write_calldata_to_file("test_single_encoding_strategy_hashflow", hex_calldata.as_str());
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use num_bigint::{BigInt, BigUint};
|
||||
use tycho_common::{models::protocol::ProtocolComponent, Bytes};
|
||||
use tycho_execution::encoding::{
|
||||
evm::utils::write_calldata_to_file,
|
||||
models::{Solution, Swap, UserTransferType},
|
||||
models::{NativeAction, Solution, Swap, UserTransferType},
|
||||
};
|
||||
|
||||
use crate::common::{
|
||||
@@ -19,7 +19,7 @@ fn test_sequential_swap_strategy_encoder() {
|
||||
// Note: This test does not assert anything. It is only used to obtain integration
|
||||
// test data for our router solidity test.
|
||||
//
|
||||
// Performs a sequential swap from WETH to USDC though WBTC using USV2 pools
|
||||
// Performs a sequential swap from WETH to USDC through WBTC using USV2 pools
|
||||
//
|
||||
// WETH ───(USV2)──> WBTC ───(USV2)──> USDC
|
||||
|
||||
@@ -37,6 +37,8 @@ fn test_sequential_swap_strategy_encoder() {
|
||||
token_out: wbtc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let swap_wbtc_usdc = Swap {
|
||||
component: ProtocolComponent {
|
||||
@@ -48,6 +50,8 @@ fn test_sequential_swap_strategy_encoder() {
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
|
||||
|
||||
@@ -103,6 +107,8 @@ fn test_sequential_swap_strategy_encoder_no_permit2() {
|
||||
token_out: wbtc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let swap_wbtc_usdc = Swap {
|
||||
component: ProtocolComponent {
|
||||
@@ -114,6 +120,8 @@ fn test_sequential_swap_strategy_encoder_no_permit2() {
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
|
||||
@@ -215,6 +223,8 @@ fn test_sequential_strategy_cyclic_swap() {
|
||||
token_out: weth.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
// WETH -> USDC (Pool 2)
|
||||
@@ -237,6 +247,8 @@ fn test_sequential_strategy_cyclic_swap() {
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
|
||||
@@ -310,3 +322,77 @@ fn test_sequential_strategy_cyclic_swap() {
|
||||
assert_eq!(hex_calldata[1224..], expected_swaps);
|
||||
write_calldata_to_file("test_sequential_strategy_cyclic_swap", hex_calldata.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sequential_swap_strategy_encoder_unwrap() {
|
||||
// Note: This test does not assert anything. It is only used to obtain integration
|
||||
// test data for our router solidity test.
|
||||
//
|
||||
// Performs a sequential swap from USDC to ETH through WBTC using USV2 pools and unwrapping in
|
||||
// the end
|
||||
//
|
||||
// USDC ───(USV2)──> WBTC ───(USV2)──> WETH -> ETH
|
||||
|
||||
let weth = weth();
|
||||
let wbtc = wbtc();
|
||||
let usdc = usdc();
|
||||
|
||||
let swap_usdc_wbtc = Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: usdc.clone(),
|
||||
token_out: wbtc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let swap_wbtc_weth = Swap {
|
||||
component: ProtocolComponent {
|
||||
id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(),
|
||||
protocol_system: "uniswap_v2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
token_in: wbtc.clone(),
|
||||
token_out: weth.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
|
||||
|
||||
let solution = Solution {
|
||||
exact_out: false,
|
||||
given_token: usdc,
|
||||
given_amount: BigUint::from_str("3_000_000_000").unwrap(),
|
||||
checked_token: eth(),
|
||||
checked_amount: BigUint::from_str("26173932").unwrap(),
|
||||
sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
|
||||
receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
|
||||
swaps: vec![swap_usdc_wbtc, swap_wbtc_weth],
|
||||
native_action: Some(NativeAction::Unwrap),
|
||||
};
|
||||
|
||||
let encoded_solution = encoder
|
||||
.encode_solutions(vec![solution.clone()])
|
||||
.unwrap()[0]
|
||||
.clone();
|
||||
|
||||
let calldata = encode_tycho_router_call(
|
||||
eth_chain().id(),
|
||||
encoded_solution,
|
||||
&solution,
|
||||
&UserTransferType::TransferFromPermit2,
|
||||
ð(),
|
||||
Some(get_signer()),
|
||||
)
|
||||
.unwrap()
|
||||
.data;
|
||||
|
||||
let hex_calldata = encode(&calldata);
|
||||
write_calldata_to_file("test_sequential_swap_strategy_encoder_unwrap", hex_calldata.as_str());
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ fn test_single_swap_strategy_encoder() {
|
||||
token_out: dai.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
|
||||
@@ -119,6 +121,8 @@ fn test_single_swap_strategy_encoder_no_permit2() {
|
||||
token_out: dai.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
|
||||
@@ -201,6 +205,8 @@ fn test_single_swap_strategy_encoder_no_transfer_in() {
|
||||
token_out: dai.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::None);
|
||||
|
||||
@@ -284,6 +290,8 @@ fn test_single_swap_strategy_encoder_wrap() {
|
||||
token_out: dai.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
|
||||
|
||||
@@ -336,6 +344,8 @@ fn test_single_swap_strategy_encoder_unwrap() {
|
||||
token_out: weth(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ fn test_split_swap_strategy_encoder() {
|
||||
token_out: dai.clone(),
|
||||
split: 0.5f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let swap_weth_wbtc = Swap {
|
||||
component: ProtocolComponent {
|
||||
@@ -56,6 +58,8 @@ fn test_split_swap_strategy_encoder() {
|
||||
// It should still be very close to 50%
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let swap_dai_usdc = Swap {
|
||||
component: ProtocolComponent {
|
||||
@@ -67,6 +71,8 @@ fn test_split_swap_strategy_encoder() {
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let swap_wbtc_usdc = Swap {
|
||||
component: ProtocolComponent {
|
||||
@@ -78,6 +84,8 @@ fn test_split_swap_strategy_encoder() {
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
|
||||
|
||||
@@ -144,6 +152,8 @@ fn test_split_input_cyclic_swap() {
|
||||
token_out: weth.clone(),
|
||||
split: 0.6f64, // 60% of input
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
// USDC -> WETH (Pool 2) - 40% of input (remaining)
|
||||
@@ -166,6 +176,8 @@ fn test_split_input_cyclic_swap() {
|
||||
token_out: weth.clone(),
|
||||
split: 0f64,
|
||||
user_data: None, // Remaining 40%
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
// WETH -> USDC (Pool 2)
|
||||
@@ -188,6 +200,8 @@ fn test_split_input_cyclic_swap() {
|
||||
token_out: usdc.clone(),
|
||||
split: 0.0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
|
||||
@@ -308,6 +322,8 @@ fn test_split_output_cyclic_swap() {
|
||||
token_out: weth.clone(),
|
||||
split: 0.0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let swap_weth_usdc_v3_pool1 = Swap {
|
||||
@@ -327,6 +343,8 @@ fn test_split_output_cyclic_swap() {
|
||||
token_out: usdc.clone(),
|
||||
split: 0.6f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let swap_weth_usdc_v3_pool2 = Swap {
|
||||
@@ -348,6 +366,8 @@ fn test_split_output_cyclic_swap() {
|
||||
token_out: usdc.clone(),
|
||||
split: 0.0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
|
||||
|
||||
@@ -51,6 +51,8 @@ fn test_sequential_swap_usx() {
|
||||
token_out: usdc.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let swap_usdc_usdt = Swap {
|
||||
component: ProtocolComponent {
|
||||
@@ -68,6 +70,8 @@ fn test_sequential_swap_usx() {
|
||||
token_out: usdt.clone(),
|
||||
split: 0f64,
|
||||
user_data: None,
|
||||
protocol_state: None,
|
||||
estimated_amount_in: None,
|
||||
};
|
||||
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user