feat: add usv4 swap encoder with single swap test
This commit is contained in:
@@ -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"
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user