feat: Add BalancerV3Encoder

Took 1 hour 7 minutes


Took 2 minutes
This commit is contained in:
Diana Carvalho
2025-06-05 17:11:41 +01:00
parent 6ada7f2a51
commit 06ce28c8fd
5 changed files with 225 additions and 57 deletions

View File

@@ -9,7 +9,8 @@
"vm:balancer_v2": "0xB5b8dc3F0a1Be99685a0DEd015Af93bFBB55C411",
"ekubo_v2": "0xcCF8e1E39e9ddfa88282fA6a7B31eBFB41a1ED7B",
"vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165",
"vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B"
"vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B",
"vm:balancer_v3": "0x0000000000000000000000000000000000000000"
},
"base": {
"uniswap_v2": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700",

View File

@@ -9,6 +9,7 @@
"vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1",
"ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598",
"vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211",
"vm:maverick_v2": "0xA4AD4f68d0b91CFD19687c881e50f3A00242828c"
"vm:maverick_v2": "0xA4AD4f68d0b91CFD19687c881e50f3A00242828c",
"vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb"
}
}

View File

@@ -3,8 +3,8 @@ use std::collections::HashMap;
use crate::encoding::{
errors::EncodingError,
evm::swap_encoder::swap_encoders::{
BalancerV2SwapEncoder, CurveSwapEncoder, EkuboSwapEncoder, MaverickV2SwapEncoder,
UniswapV2SwapEncoder, UniswapV3SwapEncoder, UniswapV4SwapEncoder,
BalancerV2SwapEncoder, BalancerV3SwapEncoder, CurveSwapEncoder, EkuboSwapEncoder,
MaverickV2SwapEncoder, UniswapV2SwapEncoder, UniswapV3SwapEncoder, UniswapV4SwapEncoder,
},
models::Chain,
swap_encoder::SwapEncoder,
@@ -81,6 +81,11 @@ impl SwapEncoderBuilder {
self.chain,
self.config,
)?)),
"vm:balancer_v3" => Ok(Box::new(BalancerV3SwapEncoder::new(
self.executor_address,
self.chain,
self.config,
)?)),
_ => Err(EncodingError::FatalError(format!(
"Unknown protocol system: {}",
self.protocol_system

View File

@@ -232,12 +232,12 @@ impl SwapEncoder for BalancerV2SwapEncoder {
config: Option<HashMap<String, String>>,
) -> Result<Self, EncodingError> {
let config = config.ok_or(EncodingError::FatalError(
"Missing balancer specific addresses in config".to_string(),
"Missing balancer v2 specific addresses in config".to_string(),
))?;
let vault_address = config
.get("vault_address")
.ok_or(EncodingError::FatalError(
"Missing balancer vault address in config".to_string(),
"Missing balancer v2 vault address in config".to_string(),
))?
.to_string();
Ok(Self { executor_address, vault_address })
@@ -571,6 +571,51 @@ impl SwapEncoder for MaverickV2SwapEncoder {
}
}
/// Encodes a swap on a Balancer V3 pool through the given executor address.
///
/// # Fields
/// * `executor_address` - The address of the executor contract that will perform the swap.
#[derive(Clone)]
pub struct BalancerV3SwapEncoder {
executor_address: String,
}
impl SwapEncoder for BalancerV3SwapEncoder {
fn new(
executor_address: String,
_chain: Chain,
_config: Option<HashMap<String, String>>,
) -> Result<Self, EncodingError> {
Ok(Self { executor_address })
}
fn encode_swap(
&self,
swap: Swap,
encoding_context: EncodingContext,
) -> Result<Vec<u8>, EncodingError> {
let pool = Address::from_str(&swap.component.id).map_err(|_| {
EncodingError::FatalError("Invalid pool address for Balancer v3".to_string())
})?;
let args = (
bytes_to_address(&swap.token_in)?,
bytes_to_address(&swap.token_out)?,
pool,
(encoding_context.transfer_type as u8).to_be_bytes(),
bytes_to_address(&encoding_context.receiver)?,
);
Ok(args.abi_encode_packed())
}
fn executor_address(&self) -> &str {
&self.executor_address
}
fn clone_box(&self) -> Box<dyn SwapEncoder> {
Box::new(self.clone())
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
@@ -705,7 +750,6 @@ mod tests {
mod balancer_v2 {
use super::*;
use crate::encoding::evm::utils::write_calldata_to_file;
#[test]
fn test_encode_balancer_v2() {
@@ -1506,59 +1550,120 @@ mod tests {
}
}
#[test]
fn test_encode_maverick_v2() {
// GHO -> (maverick) -> USDC
let maverick_pool = ProtocolComponent {
id: String::from("0x14Cf6D2Fe3E1B326114b07d22A6F6bb59e346c67"),
protocol_system: String::from("vm:maverick_v2"),
..Default::default()
};
let token_in = Bytes::from("0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f");
let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let swap = Swap {
component: maverick_pool,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: None,
};
let encoding_context = EncodingContext {
// The receiver was generated with `makeAddr("bob") using forge`
receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"),
exact_out: false,
router_address: Some(Bytes::default()),
group_token_in: token_in.clone(),
group_token_out: token_out.clone(),
transfer_type: TransferType::Transfer,
};
let encoder = MaverickV2SwapEncoder::new(
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
TychoCoreChain::Ethereum.into(),
None,
)
.unwrap();
mod balancer_v3 {
use super::*;
let encoded_swap = encoder
.encode_swap(swap, encoding_context)
#[test]
fn test_encode_balancer_v3() {
let balancer_pool = ProtocolComponent {
id: String::from("0x85b2b559bc2d21104c4defdd6efca8a20343361d"),
protocol_system: String::from("vm:balancer_v3"),
..Default::default()
};
let token_in = Bytes::from("0x7bc3485026ac48b6cf9baf0a377477fff5703af8");
let token_out = Bytes::from("0xc71ea051a5f82c67adcf634c36ffe6334793d24c");
let swap = Swap {
component: balancer_pool,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: None,
};
let encoding_context = EncodingContext {
// The receiver was generated with `makeAddr("bob") using forge`
receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"),
exact_out: false,
router_address: Some(Bytes::zero(20)),
group_token_in: token_in.clone(),
group_token_out: token_out.clone(),
transfer_type: TransferType::Transfer,
};
let encoder = BalancerV3SwapEncoder::new(
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
TychoCoreChain::Ethereum.into(),
None,
)
.unwrap();
let hex_swap = encode(&encoded_swap);
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
"40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f",
// pool
"14Cf6D2Fe3E1B326114b07d22A6F6bb59e346c67",
// receiver
"1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e",
// transfer true
"01",
))
.to_lowercase()
);
assert_eq!(
hex_swap,
String::from(concat!(
// token in
"7bc3485026ac48b6cf9baf0a377477fff5703af8",
// token out
"c71ea051a5f82c67adcf634c36ffe6334793d24c",
// pool id
"85b2b559bc2d21104c4defdd6efca8a20343361d",
// transfer type None
"01",
// receiver
"1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e",
))
);
write_calldata_to_file("test_encode_balancer_v3", hex_swap.as_str());
}
}
write_calldata_to_file("test_encode_maverick_v2", hex_swap.as_str());
mod maverick_v2 {
use super::*;
#[test]
fn test_encode_maverick_v2() {
// GHO -> (maverick) -> USDC
let maverick_pool = ProtocolComponent {
id: String::from("0x14Cf6D2Fe3E1B326114b07d22A6F6bb59e346c67"),
protocol_system: String::from("vm:maverick_v2"),
..Default::default()
};
let token_in = Bytes::from("0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f");
let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let swap = Swap {
component: maverick_pool,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: None,
};
let encoding_context = EncodingContext {
// The receiver was generated with `makeAddr("bob") using forge`
receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"),
exact_out: false,
router_address: Some(Bytes::default()),
group_token_in: token_in.clone(),
group_token_out: token_out.clone(),
transfer_type: TransferType::Transfer,
};
let encoder = MaverickV2SwapEncoder::new(
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
TychoCoreChain::Ethereum.into(),
None,
)
.unwrap();
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
"40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f",
// pool
"14Cf6D2Fe3E1B326114b07d22A6F6bb59e346c67",
// receiver
"1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e",
// transfer true
"01",
))
.to_lowercase()
);
write_calldata_to_file("test_encode_maverick_v2", hex_swap.as_str());
}
}
}

View File

@@ -3462,6 +3462,62 @@ mod tests {
hex_calldata.as_str(),
);
}
#[test]
fn test_single_encoding_strategy_balancer_v3() {
// steakUSDTlite -> (balancer v3) -> steakUSDR
let balancer_pool = ProtocolComponent {
id: String::from("0xf028ac624074d6793c36dc8a06ecec0f5a39a718"),
protocol_system: String::from("vm:balancer_v3"),
..Default::default()
};
let token_in = Bytes::from("0x097ffedb80d4b2ca6105a07a4d90eb739c45a666");
let token_out = Bytes::from("0x30881baa943777f92dc934d53d3bfdf33382cab3");
let swap = Swap {
component: balancer_pool,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: None,
};
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let solution = Solution {
exact_out: false,
given_token: token_in,
given_amount: BigUint::from_str("1_000000000000000000").unwrap(),
checked_token: token_out,
checked_amount: BigUint::from_str("1000").unwrap(),
// Alice
sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2")
.unwrap(),
swaps: vec![swap],
..Default::default()
};
let encoded_solution = encoder
.encode_solutions(vec![solution.clone()])
.unwrap()[0]
.clone();
let calldata = encode_tycho_router_call(
eth_chain().id,
encoded_solution,
&solution,
UserTransferType::TransferFrom,
eth(),
None,
)
.unwrap()
.data;
let hex_calldata = encode(&calldata);
write_calldata_to_file(
"test_single_encoding_strategy_balancer_v3",
hex_calldata.as_str(),
);
}
}
}
}