Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions src/collectors/uniswapx_route_collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use reqwest::{Client, StatusCode};

use crate::{
aws_utils::cloudwatch_utils::{build_metric_future, CwMetrics, DimensionValue},
shared::{send_metric_with_order_hash, RouteInfo, MethodParameters},
shared::{normalize_erc20eth_to_native, send_metric_with_order_hash, RouteInfo, MethodParameters},
};

const ROUTING_API: &str = "https://api.uniswap.org/v1/quote";
Expand Down Expand Up @@ -353,9 +353,62 @@ impl Collector<RoutedOrder> for UniswapXRouteCollector {
}

// The Uniswap routing API requires that "ETH" be used instead of the zero address
// Also normalizes ERC20ETH to "ETH" since we route using native ETH
fn resolve_address(token: String) -> String {
if token == "0x0000000000000000000000000000000000000000" {
let normalized = normalize_erc20eth_to_native(&token);
if normalized == "0x0000000000000000000000000000000000000000" {
return "ETH".to_string();
}
token
normalized
}

#[cfg(test)]
mod tests {
use super::*;

fn resolve_address_test(token: &str) -> String {
resolve_address(token.to_string())
}

#[test]
fn test_resolve_address_erc20eth_lowercase() {
let erc20eth = "0x00000000e20e49e6dcee6e8283a0c090578f0fb9";
let result = resolve_address_test(erc20eth);
assert_eq!(result, "ETH");
}

#[test]
fn test_resolve_address_erc20eth_uppercase() {
let erc20eth = "0x00000000E20E49E6DCEE6E8283A0C090578F0FB9";
let result = resolve_address_test(erc20eth);
assert_eq!(result, "ETH");
}

#[test]
fn test_resolve_address_erc20eth_mixed_case() {
let erc20eth = "0x00000000e20E49e6dCeE6e8283A0C090578F0fb9";
let result = resolve_address_test(erc20eth);
assert_eq!(result, "ETH");
}

#[test]
fn test_resolve_address_zero_address() {
let zero = "0x0000000000000000000000000000000000000000";
let result = resolve_address_test(zero);
assert_eq!(result, "ETH");
}

#[test]
fn test_resolve_address_regular_token() {
let token = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; // WETH
let result = resolve_address_test(token);
assert_eq!(result, token);
}

#[test]
fn test_resolve_address_other_token() {
let token = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // USDC
let result = resolve_address_test(token);
assert_eq!(result, token);
}
}
88 changes: 87 additions & 1 deletion src/shared.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::sync::Arc;
use std::{str::FromStr, sync::Arc};

use alloy::{network::{AnyNetwork, EthereumWallet, TransactionBuilder}, providers::{DynProvider, Provider}, rpc::types::TransactionRequest, serde::WithOtherFields};
use alloy_primitives::{Address, U256};
Expand All @@ -8,6 +8,10 @@ const NONCE_BURN_GAS_MULTIPLIER: u128 = 10;
const NONCE_BURN_PRIORITY_FEE: u128 = 1e7 as u128; // 0.01 gwei (max priority bid possible)
const ETH_TRANSFER_GAS: u64 = 21000;

/// ERC20ETH address - same across all chains
/// ERC20ETH is an ERC20 wrapper for native ETH that uses ERC-7914 for smart wallet compatibility
pub const ERC20ETH_ADDRESS: &str = "0x00000000e20E49e6dCeE6e8283A0C090578F0fb9";

macro_rules! send_metric_with_order_hash {
($order_hash: expr, $future: expr) => {
let hash = Arc::clone($order_hash);
Expand All @@ -28,6 +32,27 @@ macro_rules! u256 {
pub(crate) use send_metric_with_order_hash;
pub(crate) use u256;

/// Normalizes ERC20ETH to native ETH (zero address) for internal processing.
/// ERC20ETH is an ERC20 wrapper for native ETH, so we treat it as native ETH
/// since that's what we'll receive during the callback.
pub fn normalize_erc20eth_to_native(token: &str) -> String {
if token.eq_ignore_ascii_case(ERC20ETH_ADDRESS) {
"0x0000000000000000000000000000000000000000".to_string()
} else {
token.to_string()
}
}

/// Normalizes ERC20ETH to native ETH (Address::ZERO) for internal processing.
pub fn normalize_erc20eth_to_native_address(token: Address) -> Address {
if let Ok(erc20eth_addr) = Address::from_str(ERC20ETH_ADDRESS) {
if token == erc20eth_addr {
return Address::ZERO;
}
}
token
}

#[derive(Clone, Debug, Deserialize)]
#[serde(tag = "type")]
pub struct MethodParameters {
Expand Down Expand Up @@ -125,4 +150,65 @@ pub async fn burn_nonce(
return Err(anyhow::anyhow!("{} - Error sending nonce burn transaction: {}", order_hash, e));
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_normalize_erc20eth_to_native_lowercase() {
let erc20eth = "0x00000000e20e49e6dcee6e8283a0c090578f0fb9";
let result = normalize_erc20eth_to_native(erc20eth);
assert_eq!(result, "0x0000000000000000000000000000000000000000");
}

#[test]
fn test_normalize_erc20eth_to_native_uppercase() {
let erc20eth = "0x00000000E20E49E6DCEE6E8283A0C090578F0FB9";
let result = normalize_erc20eth_to_native(erc20eth);
assert_eq!(result, "0x0000000000000000000000000000000000000000");
}

#[test]
fn test_normalize_erc20eth_to_native_mixed_case() {
let erc20eth = ERC20ETH_ADDRESS; // Already mixed case
let result = normalize_erc20eth_to_native(erc20eth);
assert_eq!(result, "0x0000000000000000000000000000000000000000");
}

#[test]
fn test_normalize_erc20eth_to_native_regular_token() {
let token = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
let result = normalize_erc20eth_to_native(token);
assert_eq!(result, token);
}

#[test]
fn test_normalize_erc20eth_to_native_zero_address() {
let zero = "0x0000000000000000000000000000000000000000";
let result = normalize_erc20eth_to_native(zero);
assert_eq!(result, zero); // Should remain unchanged
}

#[test]
fn test_normalize_erc20eth_to_native_address_erc20eth() {
let erc20eth = Address::from_str(ERC20ETH_ADDRESS).unwrap();
let result = normalize_erc20eth_to_native_address(erc20eth);
assert_eq!(result, Address::ZERO);
}

#[test]
fn test_normalize_erc20eth_to_native_address_regular_token() {
let token = Address::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
let result = normalize_erc20eth_to_native_address(token);
assert_eq!(result, token);
}

#[test]
fn test_normalize_erc20eth_to_native_address_zero() {
let zero = Address::ZERO;
let result = normalize_erc20eth_to_native_address(zero);
assert_eq!(result, Address::ZERO);
}
}
9 changes: 5 additions & 4 deletions src/strategies/dutchv3_strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
block_collector::NewBlock,
uniswapx_order_collector::UniswapXOrder,
uniswapx_route_collector::{OrderBatchData, OrderData, RoutedOrder},
}, shared::RouteInfo
}, shared::{normalize_erc20eth_to_native, RouteInfo}
};
use alloy::{
hex,
Expand Down Expand Up @@ -319,8 +319,9 @@ impl UniswapXDutchV3Fill {
.iter()
.filter(|(_, order_data)| !self.processing_orders.contains(&order_data.hash))
.for_each(|(_, order_data)| {
let normalized_token_in = normalize_erc20eth_to_native(&order_data.resolved.input.token);
let token_in_token_out = TokenInTokenOut {
token_in: order_data.resolved.input.token.clone(),
token_in: normalized_token_in.clone(),
token_out: order_data.resolved.outputs[0].token.clone(),
};

Expand All @@ -345,8 +346,8 @@ impl UniswapXDutchV3Fill {
amount_in,
amount_out,
amount_required,
token_in: order_data.resolved.input.token.clone(),
token_out: order_data.resolved.outputs[0].token.clone(),
token_in: normalized_token_in,
token_out: order_data.resolved.outputs[0].token.clone(), // No normalization needed (ERC20ETH won't be output)
chain_id: self.chain_id,
});
} else {
Expand Down
6 changes: 3 additions & 3 deletions src/strategies/priority_strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
uniswapx_order_collector::UniswapXOrder,
uniswapx_route_collector::{OrderBatchData, OrderData, OrderRoute, RoutedOrder},
},
shared::RouteInfo,
shared::{normalize_erc20eth_to_native, RouteInfo},
strategies::types::SubmitTxToMempoolWithExecutionMetadata,
};
use alloy::{
Expand Down Expand Up @@ -501,8 +501,8 @@ impl UniswapXPriorityFill {
amount_in,
amount_out,
amount_required: if order_data.order.is_exact_output() { amount_in } else { amount_out },
token_in: order_data.resolved.input.token.clone(),
token_out: order_data.resolved.outputs[0].token.clone(),
token_in: normalize_erc20eth_to_native(&order_data.resolved.input.token),
token_out: order_data.resolved.outputs[0].token.clone(), // No normalization needed (ERC20ETH won't be output)
chain_id: self.chain_id,
}
}
Expand Down
17 changes: 15 additions & 2 deletions src/strategies/shared.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::collectors::uniswapx_route_collector::RoutedOrder;
use crate::{
collectors::uniswapx_route_collector::RoutedOrder,
shared::{normalize_erc20eth_to_native_address, ERC20ETH_ADDRESS},
};
use alloy::{
hex,
network::{AnyNetwork, TransactionBuilder},
Expand Down Expand Up @@ -49,7 +52,10 @@ pub trait UniswapXStrategy {
let fill_contract =
UniversalRouterExecutor::new(Address::from_str(executor_address)?, client.clone());

let token_in = Address::from_str(&request.token_in)?;
// Normalize ERC20ETH to native ETH (zero address) since we'll receive native ETH during callback
let token_in = normalize_erc20eth_to_native_address(
Address::from_str(&request.token_in)?
);
let token_out = Address::from_str(&request.token_out)?;

let permit2_approval = self
Expand Down Expand Up @@ -93,9 +99,16 @@ pub trait UniswapXStrategy {
from: &str,
to: &str,
) -> Result<Vec<Token>, anyhow::Error> {
// Native ETH and ERC20ETH don't need approval
// ERC20ETH will result in native ETH transfer during callback, so no approval needed
if token == Address::ZERO {
return Ok(vec![]);
}
if let Ok(erc20eth_addr) = Address::from_str(ERC20ETH_ADDRESS) {
if token == erc20eth_addr {
return Ok(vec![]);
}
}
let token_contract = ERC20::new(token, client.clone());
let allowance = token_contract
.allowance(
Expand Down
9 changes: 5 additions & 4 deletions src/strategies/uniswapx_strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
uniswapx_order_collector::UniswapXOrder,
uniswapx_route_collector::{OrderBatchData, OrderData, RoutedOrder},
},
shared::{send_metric_with_order_hash, RouteInfo},
shared::{normalize_erc20eth_to_native, send_metric_with_order_hash, RouteInfo},
};
use alloy::{
hex,
Expand Down Expand Up @@ -267,8 +267,9 @@ impl UniswapXUniswapFill {

// group orders by token in and token out
self.open_orders.iter().for_each(|(_, order_data)| {
let normalized_token_in = normalize_erc20eth_to_native(&order_data.resolved.input.token);
let token_in_token_out = TokenInTokenOut {
token_in: order_data.resolved.input.token.clone(),
token_in: normalized_token_in.clone(),
token_out: order_data.resolved.outputs[0].token.clone(),
};

Expand All @@ -293,8 +294,8 @@ impl UniswapXUniswapFill {
amount_in,
amount_out,
amount_required,
token_in: order_data.resolved.input.token.clone(),
token_out: order_data.resolved.outputs[0].token.clone(),
token_in: normalized_token_in,
token_out: order_data.resolved.outputs[0].token.clone(), // No normalization needed (ERC20ETH won't be output)
chain_id: self.chain_id,
});
} else {
Expand Down