feat: add usv4 swap encoder with single swap test

This commit is contained in:
royvardhan
2025-02-18 20:58:00 +05:30
parent 4f37cd392b
commit 789416b2cd

View File

@@ -1,6 +1,6 @@
use std::str::FromStr;
use alloy_primitives::{Address, Bytes as AlloyBytes};
use alloy_primitives::{keccak256, Address, Bytes as AlloyBytes};
use alloy_sol_types::SolValue;
use crate::encoding::{
@@ -147,6 +147,140 @@ impl SwapEncoder for UniswapV3SwapEncoder {
}
}
/// Encodes a swap on a Uniswap V4 pool through the given executor address.
///
/// # Fields
/// * `executor_address` - The address of the executor contract that will perform the swap.
/// * `executor_selector` - The selector of the swap function in the executor contract.
/// * `callback_selector` - The pre-computed selector of the callback function in the executor
/// contract.
#[derive(Clone)]
pub struct UniswapV4SwapEncoder {
executor_address: String,
executor_selector: String,
callback_selector: [u8; 4],
}
impl UniswapV4SwapEncoder {
fn get_zero_to_one(sell_token_address: Address, buy_token_address: Address) -> bool {
sell_token_address < buy_token_address
}
fn encode_pool_params(
intermediary_token: Address,
fee: [u8; 3],
tick_spacing: [u8; 3],
) -> Vec<u8> {
let mut encoded = Vec::with_capacity(26);
// Encode intermediary token (20 bytes)
encoded.extend_from_slice(intermediary_token.as_ref());
// Encode fee (3 bytes)
encoded.extend_from_slice(fee.as_ref());
// Encode tick spacing (3 bytes)
encoded.extend_from_slice(tick_spacing.as_ref());
encoded
}
}
impl SwapEncoder for UniswapV4SwapEncoder {
fn new(executor_address: String) -> Self {
// Pre-compute the callback selector for "unlockCallback(bytes)"
// This matches how Solidity computes function selectors
let callback_selector = keccak256(b"unlockCallback(bytes)")[..4]
.try_into()
.unwrap();
Self {
executor_address,
executor_selector: "swap(uint256,bytes)".to_string(),
callback_selector,
}
}
fn encode_swap(
&self,
swap: Swap,
encoding_context: EncodingContext,
) -> Result<Vec<u8>, EncodingError> {
let token_in_address = bytes_to_address(&swap.token_in)?;
let token_out_address = bytes_to_address(&swap.token_out)?;
let mut amount_out_min = [0u8; 32];
amount_out_min[31] = 1;
let zero_to_one = Self::get_zero_to_one(token_in_address, token_out_address);
let callback_executor = bytes_to_address(&encoding_context.router_address)?;
let fee = swap
.component
.static_attributes
.get("fee")
.ok_or_else(|| {
EncodingError::FatalError(
"Pool fee not found in Uniswap v4 static attributes".to_string(),
)
})?
.to_vec();
// Pad on the left with zeros if the fee is less than 3 bytes
let mut padded_fee_bytes = [0u8; 3];
let start = 3 - fee.len();
padded_fee_bytes[start..].copy_from_slice(&fee);
let pool_fee_u24: [u8; 3] = padded_fee_bytes[(padded_fee_bytes.len() - 3)..]
.try_into()
.map_err(|_| EncodingError::FatalError("Failed to extract fee bytes".to_string()))?;
let tick_spacing = swap
.component
.static_attributes
.get("tickSpacing")
.ok_or_else(|| {
EncodingError::FatalError(
"Pool tick spacing not found in Uniswap v4 static attributes".to_string(),
)
})?
.to_vec();
// Pad on the left with zeros if the tick spacing is less than 3 bytes
let mut padded_tick_spacing_bytes = [0u8; 3];
let start = 3 - tick_spacing.len();
padded_tick_spacing_bytes[start..].copy_from_slice(&tick_spacing);
let pool_tick_spacing_u24: [u8; 3] = padded_tick_spacing_bytes
[(padded_tick_spacing_bytes.len() - 3)..]
.try_into()
.map_err(|_| {
EncodingError::FatalError("Failed to extract tick spacing bytes".to_string())
})?;
let pool_params =
Self::encode_pool_params(token_out_address, pool_fee_u24, pool_tick_spacing_u24);
let args = (
token_in_address,
token_out_address,
amount_out_min,
zero_to_one,
callback_executor,
self.callback_selector,
pool_params,
);
Ok(args.abi_encode_packed())
}
fn executor_address(&self) -> &str {
&self.executor_address
}
fn executor_selector(&self) -> &str {
&self.executor_selector
}
fn clone_box(&self) -> Box<dyn SwapEncoder> {
Box::new(self.clone())
}
}
/// Encodes a swap on a Balancer V2 pool through the given executor address.
///
/// # Fields
@@ -344,4 +478,68 @@ mod tests {
))
);
}
#[test]
fn test_encode_uniswap_v4() {
let fee = BigInt::from(100);
let tick_spacing = BigInt::from(1);
let encoded_pool_fee = Bytes::from(fee.to_signed_bytes_be());
let encoded_tick_spacing = Bytes::from(tick_spacing.to_signed_bytes_be());
let mut static_attributes: HashMap<String, Bytes> = HashMap::new();
static_attributes.insert("fee".into(), Bytes::from(encoded_pool_fee.to_vec()));
static_attributes.insert("tickSpacing".into(), Bytes::from(encoded_tick_spacing.to_vec()));
let usv4_pool = ProtocolComponent {
// Pool manager
id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"),
static_attributes,
..Default::default()
};
let swap = Swap {
component: usv4_pool,
token_in: Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"), // USDE
token_out: Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"), // USDT
split: 0f64,
};
let encoding_context = EncodingContext {
// The receiver address was taken from `address(uniswapV4Exposed)` in the
// UniswapV4Executor.t.sol
receiver: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"),
exact_out: false,
// Same as the executor address
router_address: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"),
};
let encoder =
UniswapV4SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"));
let encoded_swap = encoder
.encode_swap(swap, encoding_context)
.unwrap();
let hex_swap = encode(&encoded_swap);
assert_eq!(
hex_swap,
String::from(concat!(
// token in
"4c9edd5852cd905f086c759e8383e09bff1e68b3",
// token out
"dac17f958d2ee523a2206206994597c13d831ec7",
// amount out min (0 as u128)
"0000000000000000000000000000000000000000000000000000000000000001",
// zero for one
"01",
// router address
"5615deb798bb3e4dfa0139dfa1b3d433cc23b72f",
// callback selector for "unlockCallback(bytes)"
"91dd7346",
// pool params:
// - intermediary token (20 bytes)
"dac17f958d2ee523a2206206994597c13d831ec7",
// - fee (3 bytes)
"000064",
// - tick spacing (3 bytes)
"000001"
))
);
}
}