chore: merge main

This commit is contained in:
TAMARA LIPOWSKI
2025-02-05 13:48:00 -05:00
12 changed files with 674 additions and 73 deletions

View File

@@ -1,3 +1,26 @@
## [0.31.0](https://github.com/propeller-heads/tycho-execution/compare/0.30.1...0.31.0) (2025-02-05)
### Features
* add tests for split swap validations ([b69aef9](https://github.com/propeller-heads/tycho-execution/commit/b69aef9b8f1d253bb465a39669bd18aa5f355aa5))
* add tests for wrap unwrap case ([4d97c3f](https://github.com/propeller-heads/tycho-execution/commit/4d97c3f16d263c975f1f42bebae9666af789eb10))
* add validation for split swap ([f80ffa9](https://github.com/propeller-heads/tycho-execution/commit/f80ffa924f1da626bef0751c92c09fb133d2ba85))
### Bug Fixes
* checks in validations ([95edd5b](https://github.com/propeller-heads/tycho-execution/commit/95edd5b1fe99fd96163dcf74c2a570a7c8a480a1))
* get_mock_split_swap_strategy_encoder ([7b72263](https://github.com/propeller-heads/tycho-execution/commit/7b7226356d3bde61da987946dbc10ae3eec33722))
* use native action to validate path ([c787f5e](https://github.com/propeller-heads/tycho-execution/commit/c787f5e722ad8a9f9a24e6ea09f59dfcf5f82239))
## [0.30.1](https://github.com/propeller-heads/tycho-execution/compare/0.30.0...0.30.1) (2025-02-04)
### Bug Fixes
* deprecated signature ([576f89d](https://github.com/propeller-heads/tycho-execution/commit/576f89d24ca25ab37ae59b4db97cbff946d6da58))
## [0.30.0](https://github.com/propeller-heads/tycho-execution/compare/0.29.1...0.30.0) (2025-02-04)

176
Cargo.lock generated
View File

@@ -47,9 +47,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "alloy"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea8ebf106e84a1c37f86244df7da0c7587e697b71a0d565cce079449b85ac6f8"
checksum = "bbcc41e8a11a4975b18ec6afba2cc48d591fa63336a4c526dacb50479a8d6b35"
dependencies = [
"alloy-consensus",
"alloy-core",
@@ -82,20 +82,35 @@ dependencies = [
[[package]]
name = "alloy-consensus"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41ed961a48297c732a5d97ee321aa8bb5009ecadbcb077d8bec90cb54e651629"
checksum = "f4138dc275554afa6f18c4217262ac9388790b2fc393c2dfe03c51d357abf013"
dependencies = [
"alloy-eips",
"alloy-primitives",
"alloy-rlp",
"alloy-serde",
"alloy-trie",
"auto_impl",
"c-kzg",
"derive_more",
"serde",
]
[[package]]
name = "alloy-consensus-any"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa04e1882c31288ce1028fdf31b6ea94cfa9eafa2e497f903ded631c8c6a42c"
dependencies = [
"alloy-consensus",
"alloy-eips",
"alloy-primitives",
"alloy-rlp",
"alloy-serde",
"serde",
]
[[package]]
name = "alloy-core"
version = "0.8.19"
@@ -140,9 +155,9 @@ dependencies = [
[[package]]
name = "alloy-eip7702"
version = "0.3.2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ffc577390ce50234e02d841214b3dc0bea6aaaae8e04bbf3cb82e9a45da9eb"
checksum = "cabf647eb4650c91a9d38cb6f972bb320009e7e9d61765fb688a86f1563b33e8"
dependencies = [
"alloy-primitives",
"alloy-rlp",
@@ -152,9 +167,9 @@ dependencies = [
[[package]]
name = "alloy-eips"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b69e06cf9c37be824b9d26d6d101114fdde6af0c87de2828b414c05c4b3daa71"
checksum = "52dd5869ed09e399003e0e0ec6903d981b2a92e74c5d37e6b40890bad2517526"
dependencies = [
"alloy-eip2930",
"alloy-eip7702",
@@ -170,12 +185,14 @@ dependencies = [
[[package]]
name = "alloy-genesis"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dde15e14944a88bd6a57d325e9a49b75558746fe16aaccc79713ae50a6a9574c"
checksum = "e7d2a7fe5c1a9bd6793829ea21a636f30fc2b3f5d2e7418ba86d96e41dd1f460"
dependencies = [
"alloy-eips",
"alloy-primitives",
"alloy-serde",
"alloy-trie",
"serde",
]
@@ -193,29 +210,31 @@ dependencies = [
[[package]]
name = "alloy-json-rpc"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af5979e0d5a7bf9c7eb79749121e8256e59021af611322aee56e77e20776b4b3"
checksum = "2008bedb8159a255b46b7c8614516eda06679ea82f620913679afbd8031fea72"
dependencies = [
"alloy-primitives",
"alloy-sol-types",
"serde",
"serde_json",
"thiserror 1.0.69",
"thiserror 2.0.11",
"tracing",
]
[[package]]
name = "alloy-network"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "204237129086ce5dc17a58025e93739b01b45313841f98fa339eb1d780511e57"
checksum = "4556f01fe41d0677495df10a648ddcf7ce118b0e8aa9642a0e2b6dd1fb7259de"
dependencies = [
"alloy-consensus",
"alloy-consensus-any",
"alloy-eips",
"alloy-json-rpc",
"alloy-network-primitives",
"alloy-primitives",
"alloy-rpc-types-any",
"alloy-rpc-types-eth",
"alloy-serde",
"alloy-signer",
@@ -223,14 +242,16 @@ dependencies = [
"async-trait",
"auto_impl",
"futures-utils-wasm",
"thiserror 1.0.69",
"serde",
"serde_json",
"thiserror 2.0.11",
]
[[package]]
name = "alloy-network-primitives"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "514f70ee2a953db21631cd817b13a1571474ec77ddc03d47616d5e8203489fde"
checksum = "f31c3c6b71340a1d076831823f09cb6e02de01de5c6630a9631bdb36f947ff80"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -268,9 +289,9 @@ dependencies = [
[[package]]
name = "alloy-provider"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4814d141ede360bb6cd1b4b064f1aab9de391e7c4d0d4d50ac89ea4bc1e25fbd"
checksum = "5a22c4441b3ebe2d77fa9cf629ba68c3f713eb91779cff84275393db97eddd82"
dependencies = [
"alloy-chains",
"alloy-consensus",
@@ -296,7 +317,7 @@ dependencies = [
"schnellru",
"serde",
"serde_json",
"thiserror 1.0.69",
"thiserror 2.0.11",
"tokio",
"tracing",
"url",
@@ -327,9 +348,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-client"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fc2bd1e7403463a5f2c61e955bcc9d3072b63aa177442b0f9aa6a6d22a941e3"
checksum = "d06a292b37e182e514903ede6e623b9de96420e8109ce300da288a96d88b7e4b"
dependencies = [
"alloy-json-rpc",
"alloy-primitives",
@@ -350,9 +371,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eea9bf1abdd506f985a53533f5ac01296bcd6102c5e139bbc5d40bc468d2c916"
checksum = "9383845dd924939e7ab0298bbfe231505e20928907d7905aa3bf112287305e06"
dependencies = [
"alloy-primitives",
"alloy-rpc-types-eth",
@@ -361,29 +382,41 @@ dependencies = [
]
[[package]]
name = "alloy-rpc-types-eth"
version = "0.5.4"
name = "alloy-rpc-types-any"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b034779a4850b4b03f5be5ea674a1cf7d746b2da762b34d1860ab45e48ca27"
checksum = "ca445cef0eb6c2cf51cfb4e214fbf1ebd00893ae2e6f3b944c8101b07990f988"
dependencies = [
"alloy-consensus-any",
"alloy-rpc-types-eth",
"alloy-serde",
]
[[package]]
name = "alloy-rpc-types-eth"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0938bc615c02421bd86c1733ca7205cc3d99a122d9f9bff05726bd604b76a5c2"
dependencies = [
"alloy-consensus",
"alloy-consensus-any",
"alloy-eips",
"alloy-network-primitives",
"alloy-primitives",
"alloy-rlp",
"alloy-serde",
"alloy-sol-types",
"derive_more",
"itertools 0.13.0",
"serde",
"serde_json",
"thiserror 2.0.11",
]
[[package]]
name = "alloy-serde"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "028e72eaa9703e4882344983cfe7636ce06d8cce104a78ea62fd19b46659efc4"
checksum = "ae0465c71d4dced7525f408d84873aeebb71faf807d22d74c4a426430ccd9b55"
dependencies = [
"alloy-primitives",
"serde",
@@ -392,9 +425,9 @@ dependencies = [
[[package]]
name = "alloy-signer"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "592c185d7100258c041afac51877660c7bf6213447999787197db4842f0e938e"
checksum = "9bfa395ad5cc952c82358d31e4c68b27bf4a89a5456d9b27e226e77dac50e4ff"
dependencies = [
"alloy-dyn-abi",
"alloy-primitives",
@@ -403,14 +436,14 @@ dependencies = [
"auto_impl",
"elliptic-curve",
"k256",
"thiserror 1.0.69",
"thiserror 2.0.11",
]
[[package]]
name = "alloy-signer-aws"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a406102908a4e51834f32c4e5c1b29aa2c407b3fd23a5cad129c28b56d85e1b8"
checksum = "0eb06810c34427d499863817eb506acf57cb9ded9224b374116cae4e22dbd4e9"
dependencies = [
"alloy-consensus",
"alloy-network",
@@ -420,15 +453,15 @@ dependencies = [
"aws-sdk-kms",
"k256",
"spki",
"thiserror 1.0.69",
"thiserror 2.0.11",
"tracing",
]
[[package]]
name = "alloy-signer-gcp"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8d363e12280cb43747d3b62a1e6f00d595bc1a56464bb20200c6b6ca5d68185"
checksum = "d629e63fec8802ad53706d46e8eceeeae2b135c6648d0de41669a523bf17df4a"
dependencies = [
"alloy-consensus",
"alloy-network",
@@ -438,15 +471,15 @@ dependencies = [
"gcloud-sdk",
"k256",
"spki",
"thiserror 1.0.69",
"thiserror 2.0.11",
"tracing",
]
[[package]]
name = "alloy-signer-ledger"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a642c9f66ac73ae0d5398ce7ce3ce5bdfad5658d549abd48ea48962e585dca"
checksum = "b426789566a19252cb46b757d91543a6f8e70330c72f312b86c5878595d092ef"
dependencies = [
"alloy-consensus",
"alloy-dyn-abi",
@@ -458,15 +491,15 @@ dependencies = [
"coins-ledger",
"futures-util",
"semver 1.0.24",
"thiserror 1.0.69",
"thiserror 2.0.11",
"tracing",
]
[[package]]
name = "alloy-signer-local"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6614f02fc1d5b079b2a4a5320018317b506fd0a6d67c1fd5542a71201724986c"
checksum = "fbdc63ce9eda1283fcbaca66ba4a414b841c0e3edbeef9c86a71242fc9e84ccc"
dependencies = [
"alloy-consensus",
"alloy-network",
@@ -475,7 +508,7 @@ dependencies = [
"async-trait",
"k256",
"rand",
"thiserror 1.0.69",
"thiserror 2.0.11",
]
[[package]]
@@ -550,9 +583,9 @@ dependencies = [
[[package]]
name = "alloy-transport"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be77579633ebbc1266ae6fd7694f75c408beb1aeb6865d0b18f22893c265a061"
checksum = "d17722a198f33bbd25337660787aea8b8f57814febb7c746bc30407bdfc39448"
dependencies = [
"alloy-json-rpc",
"base64 0.22.1",
@@ -560,7 +593,7 @@ dependencies = [
"futures-utils-wasm",
"serde",
"serde_json",
"thiserror 1.0.69",
"thiserror 2.0.11",
"tokio",
"tower 0.5.2",
"tracing",
@@ -570,9 +603,9 @@ dependencies = [
[[package]]
name = "alloy-transport-http"
version = "0.5.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91fd1a5d0827939847983b46f2f79510361f901dc82f8e3c38ac7397af142c6e"
checksum = "6e1509599021330a31c4a6816b655e34bf67acb1cc03c564e09fd8754ff6c5de"
dependencies = [
"alloy-json-rpc",
"alloy-transport",
@@ -583,6 +616,22 @@ dependencies = [
"url",
]
[[package]]
name = "alloy-trie"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6917c79e837aa7b77b7a6dae9f89cbe15313ac161c4d3cfaf8909ef21f3d22d8"
dependencies = [
"alloy-primitives",
"alloy-rlp",
"arrayvec",
"derive_more",
"nybbles",
"serde",
"smallvec",
"tracing",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
@@ -733,6 +782,9 @@ name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
dependencies = [
"serde",
]
[[package]]
name = "async-compression"
@@ -2590,6 +2642,19 @@ dependencies = [
"syn 2.0.96",
]
[[package]]
name = "nybbles"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8983bb634df7248924ee0c4c3a749609b5abcb082c28fffe3254b3eb3602b307"
dependencies = [
"alloy-rlp",
"const-hex",
"proptest",
"serde",
"smallvec",
]
[[package]]
name = "object"
version = "0.36.7"
@@ -3644,6 +3709,9 @@ name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
dependencies = [
"serde",
]
[[package]]
name = "socket2"
@@ -4163,7 +4231,7 @@ dependencies = [
[[package]]
name = "tycho-execution"
version = "0.30.0"
version = "0.31.0"
dependencies = [
"alloy",
"alloy-primitives",
@@ -4470,9 +4538,9 @@ dependencies = [
[[package]]
name = "wasmtimer"
version = "0.2.1"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7ed9d8b15c7fb594d72bfb4b5a276f3d2029333cd93a932f376f5937f6f80ee"
checksum = "0048ad49a55b9deb3953841fa1fc5858f0efbcb7a18868c899a360269fac1b23"
dependencies = [
"futures",
"js-sys",

View File

@@ -1,6 +1,6 @@
[package]
name = "tycho-execution"
version = "0.30.0"
version = "0.31.0"
edition = "2021"
[dependencies]
@@ -15,7 +15,7 @@ thiserror = "1.0.69"
tokio = { version = "1.38.0", features = ["full"] }
chrono = "0.4.39"
alloy = { version = "0.5.4", features = ["providers", "rpc-types-eth", "eip712", "signer-local"], optional = true }
alloy = { version = "0.9.2", features = ["providers", "rpc-types-eth", "eip712", "signer-local"], optional = true }
alloy-sol-types = { version = "0.8.14", optional = true }
alloy-primitives = { version = "0.8.9", optional = true }
tycho-core = { git = "https://github.com/propeller-heads/tycho-indexer.git", package = "tycho-core", tag = "0.46.0" }

View File

@@ -1,7 +1,10 @@
use std::str::FromStr;
use num_bigint::BigUint;
use tycho_core::{dto::ProtocolComponent, models::Chain, Bytes};
use tycho_core::{
dto::{Chain, ProtocolComponent},
Bytes,
};
use tycho_execution::encoding::{
evm::{
strategy_encoder::strategy_encoder_registry::EVMStrategyEncoderRegistry,

View File

@@ -7,14 +7,12 @@ use alloy::{
signers::{local::PrivateKeySigner, SignerSync},
transports::BoxTransport,
};
#[allow(deprecated)]
use alloy_primitives::Signature;
use alloy_primitives::B256;
use alloy_primitives::{PrimitiveSignature as Signature, B256};
use alloy_sol_types::{eip712_domain, sol, SolStruct, SolValue};
use chrono::Utc;
use num_bigint::BigUint;
use tokio::runtime::Runtime;
use tycho_core::{models::Chain, Bytes};
use tycho_core::{dto::Chain, Bytes};
use crate::encoding::{
errors::EncodingError,
@@ -117,7 +115,6 @@ impl Permit2 {
}
}
/// Creates permit single and signature
#[allow(deprecated)]
pub fn get_permit(
&self,
spender: &Bytes,

View File

@@ -1,5 +1,5 @@
use alloy_primitives::hex;
use tycho_core::{models::Chain, Bytes};
use tycho_core::{dto::Chain, Bytes};
pub fn native_address(chain: Chain) -> Bytes {
match chain {

View File

@@ -1,4 +1,4 @@
use tycho_core::models::Chain;
use tycho_core::dto::Chain;
pub struct ChainId(u64);

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use tycho_core::models::Chain;
use tycho_core::dto::Chain;
use crate::encoding::{
errors::EncodingError,

View File

@@ -1,19 +1,23 @@
use std::{cmp::max, collections::HashSet, str::FromStr};
use std::{
cmp::max,
collections::{HashMap, HashSet, VecDeque},
str::FromStr,
};
use alloy_primitives::{aliases::U24, FixedBytes, U256, U8};
use alloy_sol_types::SolValue;
use num_bigint::BigUint;
use tycho_core::{keccak256, models::Chain, Bytes};
use tycho_core::{dto::Chain, keccak256, Bytes};
use crate::encoding::{
errors::EncodingError,
evm::{
approvals::permit2::Permit2,
constants::wrapped_address,
constants::{native_address, wrapped_address},
swap_encoder::swap_encoder_registry::SwapEncoderRegistry,
utils::{biguint_to_u256, bytes_to_address, encode_input, percentage_to_uint24},
},
models::{EncodingContext, NativeAction, Solution},
models::{EncodingContext, NativeAction, Solution, Swap},
strategy_encoder::StrategyEncoder,
swap_encoder::SwapEncoder,
};
@@ -59,6 +63,7 @@ pub struct SplitSwapStrategyEncoder {
permit2: Permit2,
selector: String,
wrapped_address: Bytes,
native_address: Bytes,
}
impl SplitSwapStrategyEncoder {
@@ -69,13 +74,156 @@ impl SplitSwapStrategyEncoder {
) -> Result<Self, EncodingError> {
let selector = "swap(uint256,address,address,uint256,bool,bool,uint256,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string();
let wrapped_address = wrapped_address(chain);
let native_address = native_address(chain);
Ok(Self {
permit2: Permit2::new(signer_pk, chain)?,
selector,
swap_encoder_registry,
wrapped_address,
native_address,
})
}
fn validate_split_percentages(&self, swaps: &[Swap]) -> Result<(), EncodingError> {
let mut swaps_by_token: HashMap<Bytes, Vec<&Swap>> = HashMap::new();
for swap in swaps {
if swap.split >= 1.0 {
return Err(EncodingError::InvalidInput(format!(
"Split percentage must be less than 1 (100%), got {}",
swap.split
)));
}
swaps_by_token
.entry(swap.token_in.clone())
.or_default()
.push(swap);
}
for (token, token_swaps) in swaps_by_token {
// Single swaps don't need remainder handling
if token_swaps.len() == 1 {
if token_swaps[0].split != 0.0 {
return Err(EncodingError::InvalidInput(format!(
"Single swap must have 0% split for token {:?}",
token
)));
}
continue;
}
let mut found_zero_split = false;
let mut total_percentage = 0.0;
for (i, swap) in token_swaps.iter().enumerate() {
match (swap.split == 0.0, i == token_swaps.len() - 1) {
(true, false) => {
return Err(EncodingError::InvalidInput(format!(
"The 0% split for token {:?} must be the last swap",
token
)))
}
(true, true) => found_zero_split = true,
(false, _) => {
if swap.split < 0.0 {
return Err(EncodingError::InvalidInput(format!(
"All splits must be >= 0% for token {:?}",
token
)));
}
total_percentage += swap.split;
}
}
}
if !found_zero_split {
return Err(EncodingError::InvalidInput(format!(
"Token {:?} must have exactly one 0% split for remainder handling",
token
)));
}
// Total must be <100% to leave room for remainder
if total_percentage >= 1.0 {
return Err(EncodingError::InvalidInput(format!(
"Total of non-remainder splits for token {:?} must be <100%, got {}%",
token,
total_percentage * 100.0
)));
}
}
Ok(())
}
fn validate_swap_path(
&self,
swaps: &[Swap],
given_token: &Bytes,
checked_token: &Bytes,
native_action: &Option<NativeAction>,
) -> Result<(), EncodingError> {
// Convert ETH to WETH only if there's a corresponding wrap/unwrap action
let given_token = if *given_token == *self.native_address {
match native_action {
Some(NativeAction::Wrap) => &self.wrapped_address,
_ => given_token,
}
} else {
given_token
};
let checked_token = if *checked_token == *self.native_address {
match native_action {
Some(NativeAction::Unwrap) => &self.wrapped_address,
_ => checked_token,
}
} else {
checked_token
};
// Build directed graph of token flows
let mut graph: HashMap<&Bytes, HashSet<&Bytes>> = HashMap::new();
for swap in swaps {
graph
.entry(&swap.token_in)
.or_default()
.insert(&swap.token_out);
}
// BFS from validation_given
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(given_token);
while let Some(token) = queue.pop_front() {
if !visited.insert(token) {
continue;
}
// Early success check
if token == checked_token && visited.len() == graph.len() + 1 {
return Ok(());
}
if let Some(next_tokens) = graph.get(token) {
for &next_token in next_tokens {
if !visited.contains(next_token) {
queue.push_back(next_token);
}
}
}
}
// If we get here, either checked_token wasn't reached or not all tokens were visited
if !visited.contains(checked_token) {
Err(EncodingError::InvalidInput(
"Checked token is not reachable through swap path".to_string(),
))
} else {
Err(EncodingError::InvalidInput(
"Some tokens are not connected to the main path".to_string(),
))
}
}
}
impl EVMStrategyEncoder for SplitSwapStrategyEncoder {}
impl StrategyEncoder for SplitSwapStrategyEncoder {
@@ -84,6 +232,13 @@ impl StrategyEncoder for SplitSwapStrategyEncoder {
solution: Solution,
router_address: Bytes,
) -> Result<(Vec<u8>, Bytes), EncodingError> {
self.validate_split_percentages(&solution.swaps)?;
self.validate_swap_path(
&solution.swaps,
&solution.given_token,
&solution.checked_token,
&solution.native_action,
)?;
let (permit, signature) = self.permit2.get_permit(
&router_address,
&solution.sender,
@@ -676,4 +831,359 @@ mod tests {
let _hex_calldata = encode(&calldata);
println!("{}", _hex_calldata);
}
fn get_mock_split_swap_strategy_encoder() -> SplitSwapStrategyEncoder {
let private_key =
"0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string();
let swap_encoder_registry = get_swap_encoder_registry();
SplitSwapStrategyEncoder::new(private_key, Chain::Ethereum, swap_encoder_registry).unwrap()
}
#[test]
fn test_validate_path_single_swap() {
let encoder = get_mock_split_swap_strategy_encoder();
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swaps = vec![Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
split: 0f64,
}];
let result = encoder.validate_swap_path(&swaps, &weth, &dai, &None);
assert_eq!(result, Ok(()));
}
#[test]
fn test_validate_path_multiple_swaps() {
let encoder = get_mock_split_swap_strategy_encoder();
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let swaps = vec![
Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
split: 0.5f64,
},
Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: dai.clone(),
token_out: usdc.clone(),
split: 0f64,
},
];
let result = encoder.validate_swap_path(&swaps, &weth, &usdc, &None);
assert_eq!(result, Ok(()));
}
#[test]
fn test_validate_path_disconnected() {
let encoder = get_mock_split_swap_strategy_encoder();
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap();
let disconnected_swaps = vec![
Swap {
component: ProtocolComponent {
id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
split: 0.5,
},
// This swap is disconnected from the WETH->DAI path
Swap {
component: ProtocolComponent {
id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: wbtc.clone(),
token_out: usdc.clone(),
split: 0.0,
},
];
let result = encoder.validate_swap_path(&disconnected_swaps, &weth, &usdc, &None);
assert!(matches!(
result,
Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path")
));
}
#[test]
fn test_validate_path_unreachable_checked_token() {
let encoder = get_mock_split_swap_strategy_encoder();
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let unreachable_swaps = vec![Swap {
component: ProtocolComponent {
id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
split: 1.0,
}];
let result = encoder.validate_swap_path(&unreachable_swaps, &weth, &usdc, &None);
assert!(matches!(
result,
Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path")
));
}
#[test]
fn test_validate_path_empty_swaps() {
let encoder = get_mock_split_swap_strategy_encoder();
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let empty_swaps: Vec<Swap> = vec![];
let result = encoder.validate_swap_path(&empty_swaps, &weth, &usdc, &None);
assert!(matches!(
result,
Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path")
));
}
#[test]
fn test_validate_swap_single() {
let encoder = get_mock_split_swap_strategy_encoder();
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swaps = vec![Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
split: 0f64,
}];
let result = encoder.validate_split_percentages(&swaps);
assert_eq!(result, Ok(()));
}
#[test]
fn test_validate_swaps_multiple() {
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
// Valid case: Multiple swaps with proper splits (50%, 30%, remainder)
let valid_swaps = vec![
Swap {
component: ProtocolComponent {
id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
split: 0.5,
},
Swap {
component: ProtocolComponent {
id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
split: 0.3,
},
Swap {
component: 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%)
},
];
let encoder = get_mock_split_swap_strategy_encoder();
assert!(encoder
.validate_split_percentages(&valid_swaps)
.is_ok());
}
#[test]
fn test_validate_swaps_no_remainder_split() {
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let invalid_total_swaps = vec![
Swap {
component: ProtocolComponent {
id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
split: 0.7,
},
Swap {
component: ProtocolComponent {
id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
split: 0.3,
},
];
let encoder = get_mock_split_swap_strategy_encoder();
assert!(matches!(
encoder.validate_split_percentages(&invalid_total_swaps),
Err(EncodingError::InvalidInput(msg)) if msg.contains("must have exactly one 0% split")
));
}
#[test]
fn test_validate_swaps_zero_split_not_at_end() {
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let invalid_zero_position_swaps = vec![
Swap {
component: ProtocolComponent {
id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
split: 0.0,
},
Swap {
component: ProtocolComponent {
id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
split: 0.5,
},
];
let encoder = get_mock_split_swap_strategy_encoder();
assert!(matches!(
encoder.validate_split_percentages(&invalid_zero_position_swaps),
Err(EncodingError::InvalidInput(msg)) if msg.contains("must be the last swap")
));
}
#[test]
fn test_validate_swaps_splits_exceed_hundred_percent() {
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let invalid_overflow_swaps = vec![
Swap {
component: ProtocolComponent {
id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
split: 0.6,
},
Swap {
component: ProtocolComponent {
id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
split: 0.5,
},
Swap {
component: ProtocolComponent {
id: "pool3".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
split: 0.0,
},
];
let encoder = get_mock_split_swap_strategy_encoder();
assert!(matches!(
encoder.validate_split_percentages(&invalid_overflow_swaps),
Err(EncodingError::InvalidInput(msg)) if msg.contains("must be <100%")
));
}
#[test]
fn test_validate_path_wrap_eth_given_token() {
let encoder = get_mock_split_swap_strategy_encoder();
let eth = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
let swaps = vec![Swap {
component: ProtocolComponent {
id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: usdc.clone(),
split: 0f64,
}];
let result = encoder.validate_swap_path(&swaps, &eth, &usdc, &Some(NativeAction::Wrap));
assert_eq!(result, Ok(()));
}
#[test]
fn test_validate_token_path_connectivity_wrap_eth_checked_token() {
let encoder = get_mock_split_swap_strategy_encoder();
let eth = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
let swaps = vec![Swap {
component: ProtocolComponent {
id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: usdc.clone(),
token_out: weth.clone(),
split: 0f64,
}];
let result = encoder.validate_swap_path(&swaps, &usdc, &eth, &Some(NativeAction::Unwrap));
assert_eq!(result, Ok(()));
}
}

View File

@@ -1,6 +1,6 @@
use std::{collections::HashMap, fs};
use tycho_core::models::Chain;
use tycho_core::dto::Chain;
use crate::encoding::{
errors::EncodingError, evm::swap_encoder::builder::SwapEncoderBuilder,

View File

@@ -1,7 +1,7 @@
use std::str::FromStr;
use num_bigint::BigUint;
use tycho_core::{models::Chain, Bytes};
use tycho_core::{dto::Chain, Bytes};
use crate::encoding::{
errors::EncodingError,
@@ -119,7 +119,7 @@ impl<S: StrategyEncoderRegistry> TychoEncoder<S> for EVMTychoEncoder<S> {
#[cfg(test)]
mod tests {
use tycho_core::{dto::ProtocolComponent, models::Chain};
use tycho_core::dto::{Chain, ProtocolComponent};
use super::*;
use crate::encoding::{

View File

@@ -1,4 +1,4 @@
use tycho_core::{models::Chain, Bytes};
use tycho_core::{dto::Chain, Bytes};
use crate::encoding::{errors::EncodingError, models::Solution, swap_encoder::SwapEncoder};