From 9c63e099a9ba90b46768a6dfd192bcdd651f7f22 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Fri, 24 Jan 2025 18:11:10 -0500 Subject: [PATCH 1/3] feat: UniswapV3SwapEncoder - No more supply owed action type or action data - we will always transfer from our router using a regular transfer in the callback. - The receiver will always be sent with the actual swap call, meaning that no forward received action type or data is necessary either. - We decode the pool fee as le-bytes and not be-bytes because that's how it's stored: https://github.com/propeller-heads/tycho-protocol-sdk/blob/3c08359cf112e15c137dd5256b8dc8e9cd6c1626/substreams/ethereum-uniswap-v3/src/modules/1_map_pool_created.rs#L64 --- src/encoding/evm/swap_encoder/encoders.rs | 129 +++++++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/src/encoding/evm/swap_encoder/encoders.rs b/src/encoding/evm/swap_encoder/encoders.rs index 3b6ff9a..4eee678 100644 --- a/src/encoding/evm/swap_encoder/encoders.rs +++ b/src/encoding/evm/swap_encoder/encoders.rs @@ -70,6 +70,84 @@ impl SwapEncoder for UniswapV2SwapEncoder { } } +pub struct UniswapV3SwapEncoder { + executor_address: String, +} + +impl UniswapV3SwapEncoder { + fn get_zero_to_one(sell_token_address: Address, buy_token_address: Address) -> bool { + sell_token_address < buy_token_address + } +} + +impl SwapEncoder for UniswapV3SwapEncoder { + fn new(executor_address: String) -> Self { + Self { executor_address } + } + + fn encode_swap( + &self, + swap: Swap, + encoding_context: EncodingContext, + ) -> Result, EncodingError> { + let token_in_address = bytes_to_address(&swap.token_in)?; + let token_out_address = bytes_to_address(&swap.token_out)?; + + let zero_for_one = Self::get_zero_to_one(token_in_address, token_out_address); + let component_id = Bytes::from( + decode( + swap.component + .id + .trim_start_matches("0x"), + ) + .map_err(|_| { + EncodingError::FatalError(format!( + "Failed to parse component id for Uniswap v3: {}", + swap.component.id + )) + })?, + ); + let mut pool_fee_bytes = swap + .component + .static_attributes + .get("pool_fee") + .ok_or_else(|| { + EncodingError::FatalError( + "Pool fee not found in Uniswap v3 static attributes".to_string(), + ) + })? + .as_ref() + .to_vec(); + + // Reverse to get be bytes, since this is encoded as le bytes + pool_fee_bytes.reverse(); + + let pool_fee_u24: [u8; 3] = pool_fee_bytes[pool_fee_bytes.len() - 3..] + .try_into() + .map_err(|_| { + EncodingError::FatalError( + "Pool fee static attribute must be at least 3 bytes".to_string(), + ) + })?; + + let args = ( + token_in_address, + token_out_address, + pool_fee_u24, + bytes_to_address(&encoding_context.receiver)?, + bytes_to_address(&component_id)?, + zero_for_one, + encoding_context.exact_out, + ); + + Ok(args.abi_encode_packed()) + } + + fn executor_address(&self) -> &str { + &self.executor_address + } +} + pub struct BalancerV2SwapEncoder { executor_address: String, vault_address: String, @@ -116,6 +194,8 @@ impl SwapEncoder for BalancerV2SwapEncoder { #[cfg(test)] mod tests { + use std::collections::HashMap; + use alloy::hex::encode; use tycho_core::{dto::ProtocolComponent, Bytes}; @@ -159,6 +239,53 @@ mod tests { )) ); } + #[tokio::test] + async fn test_encode_uniswap_v3() { + let encoded_pool_fee: [u8; 4] = 500u32.to_le_bytes(); + let mut static_attributes: HashMap = HashMap::new(); + static_attributes.insert("pool_fee".into(), Bytes::from(encoded_pool_fee[..3].to_vec())); + + let usv3_pool = ProtocolComponent { + id: String::from("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"), + static_attributes, + ..Default::default() + }; + let swap = Swap { + component: usv3_pool, + token_in: Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"), + token_out: Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"), + split: 0f64, + }; + let encoding_context = EncodingContext { + receiver: Bytes::from("0x0000000000000000000000000000000000000001"), + exact_out: false, + router_address: Bytes::zero(20), + }; + let encoder = UniswapV3SwapEncoder::new(String::from("0x")); + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); + assert_eq!( + hex_swap, + String::from(concat!( + // in token + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + // out token + "6b175474e89094c44da98b954eedeac495271d0f", + // fee + "0001f4", + // receiver + "0000000000000000000000000000000000000001", + // pool id + "88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", + // zero for one + "00", + // exact out + "00", + )) + ); + } #[test] fn test_encode_balancer_v2() { @@ -191,7 +318,7 @@ mod tests { "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token out "6b175474e89094c44da98b954eedeac495271d0f", - // pool id + // pool id "307838386536413063326444443236464545623634463033396132633431323936466342336635363430", // receiver "0000000000000000000000000000000000000001", From d95b74bb4ebdcff3baa9b41978f3caf974ecf6bd Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Tue, 28 Jan 2025 09:54:49 -0500 Subject: [PATCH 2/3] chore: small improvements - Easier less convoluted way to decode address from string - No more tokio test (not necessary) - zero_for_one -> zero_to_one for consistency --- src/encoding/evm/swap_encoder/encoders.rs | 48 ++++++----------------- 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/src/encoding/evm/swap_encoder/encoders.rs b/src/encoding/evm/swap_encoder/encoders.rs index 4eee678..51092cb 100644 --- a/src/encoding/evm/swap_encoder/encoders.rs +++ b/src/encoding/evm/swap_encoder/encoders.rs @@ -1,9 +1,7 @@ use std::str::FromStr; -use alloy::hex::decode; use alloy_primitives::Address; use alloy_sol_types::SolValue; -use tycho_core::Bytes; use crate::encoding::{ errors::EncodingError, @@ -37,28 +35,17 @@ impl SwapEncoder for UniswapV2SwapEncoder { let token_in_address = bytes_to_address(&swap.token_in)?; let token_out_address = bytes_to_address(&swap.token_out)?; - let zero_for_one = Self::get_zero_to_one(token_in_address, token_out_address); - let component_id = Bytes::from( - decode( - swap.component - .id - .trim_start_matches("0x"), - ) - .map_err(|_| { - EncodingError::FatalError(format!( - "Failed to parse component id for Uniswap v2: {}", - swap.component.id - )) - })?, - ); + let zero_to_one = Self::get_zero_to_one(token_in_address, token_out_address); + let component_id = Address::from_str(&swap.component.id) + .map_err(|_| EncodingError::FatalError("Invalid USV2 component id".to_string()))?; // Token in address is always needed to perform a manual transfer from the router, // since no optimizations are performed that send from one pool to the next let args = ( token_in_address, - bytes_to_address(&component_id)?, + component_id, bytes_to_address(&encoding_context.receiver)?, - zero_for_one, + zero_to_one, encoding_context.exact_out, ); @@ -93,20 +80,9 @@ impl SwapEncoder for UniswapV3SwapEncoder { let token_in_address = bytes_to_address(&swap.token_in)?; let token_out_address = bytes_to_address(&swap.token_out)?; - let zero_for_one = Self::get_zero_to_one(token_in_address, token_out_address); - let component_id = Bytes::from( - decode( - swap.component - .id - .trim_start_matches("0x"), - ) - .map_err(|_| { - EncodingError::FatalError(format!( - "Failed to parse component id for Uniswap v3: {}", - swap.component.id - )) - })?, - ); + let zero_to_one = Self::get_zero_to_one(token_in_address, token_out_address); + let component_id = Address::from_str(&swap.component.id) + .map_err(|_| EncodingError::FatalError("Invalid USV3 component id".to_string()))?; let mut pool_fee_bytes = swap .component .static_attributes @@ -135,8 +111,8 @@ impl SwapEncoder for UniswapV3SwapEncoder { token_out_address, pool_fee_u24, bytes_to_address(&encoding_context.receiver)?, - bytes_to_address(&component_id)?, - zero_for_one, + component_id, + zero_to_one, encoding_context.exact_out, ); @@ -239,8 +215,8 @@ mod tests { )) ); } - #[tokio::test] - async fn test_encode_uniswap_v3() { + #[test] + fn test_encode_uniswap_v3() { let encoded_pool_fee: [u8; 4] = 500u32.to_le_bytes(); let mut static_attributes: HashMap = HashMap::new(); static_attributes.insert("pool_fee".into(), Bytes::from(encoded_pool_fee[..3].to_vec())); From 7d35b8bb369857380303709ba324fe60719ed107 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 28 Jan 2025 15:06:00 +0000 Subject: [PATCH 3/3] chore(release): 0.19.0 [skip ci] ## [0.19.0](https://github.com/propeller-heads/tycho-execution/compare/0.18.0...0.19.0) (2025-01-28) ### Features * UniswapV3SwapEncoder ([9c63e09](https://github.com/propeller-heads/tycho-execution/commit/9c63e099a9ba90b46768a6dfd192bcdd651f7f22)), closes [/github.com/propeller-heads/tycho-protocol-sdk/blob/3c08359cf112e15c137dd5256b8dc8e9cd6c1626/substreams/ethereum-uniswap-v3/src/modules/1_map_pool_created.rs#L64](https://github.com/propeller-heads//github.com/propeller-heads/tycho-protocol-sdk/blob/3c08359cf112e15c137dd5256b8dc8e9cd6c1626/substreams/ethereum-uniswap-v3/src/modules/1_map_pool_created.rs/issues/L64) --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6b523e..c711f82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [0.19.0](https://github.com/propeller-heads/tycho-execution/compare/0.18.0...0.19.0) (2025-01-28) + + +### Features + +* UniswapV3SwapEncoder ([9c63e09](https://github.com/propeller-heads/tycho-execution/commit/9c63e099a9ba90b46768a6dfd192bcdd651f7f22)), closes [/github.com/propeller-heads/tycho-protocol-sdk/blob/3c08359cf112e15c137dd5256b8dc8e9cd6c1626/substreams/ethereum-uniswap-v3/src/modules/1_map_pool_created.rs#L64](https://github.com/propeller-heads//github.com/propeller-heads/tycho-protocol-sdk/blob/3c08359cf112e15c137dd5256b8dc8e9cd6c1626/substreams/ethereum-uniswap-v3/src/modules/1_map_pool_created.rs/issues/L64) + ## [0.18.0](https://github.com/propeller-heads/tycho-execution/compare/0.17.0...0.18.0) (2025-01-27) diff --git a/Cargo.lock b/Cargo.lock index 63aadca..39a3184 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4163,7 +4163,7 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.18.0" +version = "0.19.0" dependencies = [ "alloy", "alloy-primitives", diff --git a/Cargo.toml b/Cargo.toml index 901b438..82d601f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "0.18.0" +version = "0.19.0" edition = "2021" [dependencies]