feat: Support Bebop quote through IndicativelyPriced state

Made a new feature gate (test-utils) for MockRFQState

Took 2 hours 19 minutes
This commit is contained in:
Diana Carvalho
2025-08-14 12:56:03 +01:00
parent 6d88d0a144
commit 22920617eb
10 changed files with 474 additions and 56 deletions

6
Cargo.lock generated
View File

@@ -4634,9 +4634,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tycho-common"
version = "0.78.2"
version = "0.81.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af4c7e2c8e194a3e9dfc5911ff0ec273b7dd52acb71dfdcf21351dd78a49576"
checksum = "68d44780891254a0155d1aaa765a8625b48672158b6e12ce4f06771e9d369ffa"
dependencies = [
"anyhow",
"async-trait",
@@ -4662,6 +4662,7 @@ name = "tycho-execution"
version = "0.115.0"
dependencies = [
"alloy",
"async-trait",
"chrono",
"clap",
"dotenv",
@@ -4676,6 +4677,7 @@ dependencies = [
"thiserror 1.0.69",
"tokio",
"tycho-common",
"tycho-execution",
]
[[package]]

View File

@@ -37,17 +37,19 @@ tokio = { version = "1.38.0", features = ["full"] }
chrono = "0.4.39"
clap = { version = "4.5.3", features = ["derive"] }
once_cell = "1.20.2"
tycho-common = ">0.78.1"
tycho-common = ">0.81.5"
alloy = { version = "1.0.6", features = ["providers", "rpc-types-eth", "eip712", "signer-local", "node-bindings"], optional = true }
async-trait = { version = "0.1.88", optional = true }
[dev-dependencies]
rstest = "0.24.0"
tycho-execution = { path = "../tycho-execution", features = ["test-utils"] }
[features]
default = ["evm"]
evm = ["alloy"]
fork-tests = []
test-utils = ["async-trait"]
[profile.bench]
debug = true

View File

@@ -7,7 +7,8 @@
"native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
},
"rfq:bebop": {
"bebop_settlement_address": "0xbbbbbBB520d69a9775E85b458C58c648259FAD5F"
"bebop_settlement_address": "0xbbbbbBB520d69a9775E85b458C58c648259FAD5F",
"native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
}
},
"base": {},

View File

@@ -514,3 +514,36 @@ contract TychoRouterSequentialSwapTest is TychoRouterTestSetup {
assertEq(balanceAfter - balanceBefore, 1404194006633772805);
}
}
contract TychoRouterSequentialSwapTestForBebop is TychoRouterTestSetup {
function getForkBlock() public pure override returns (uint256) {
return 23139046;
}
function testUSV3BebopIntegration() public {
// Performs a sequential swap from WETH to WBTC through USDC using USV3 and Bebop RFQ
//
// WETH ──(USV3)──> USDC ───(Bebop RFQ)──> WBTC
// The Bebop order expects:
// - 2021750881 USDC input -> 1672307 WBTC output
uint256 amountIn = 1 ether;
uint256 expectedAmountOut = 1672307;
deal(WETH_ADDR, BOB, amountIn);
uint256 balanceBefore = IERC20(WBTC_ADDR).balanceOf(BOB);
vm.startPrank(BOB);
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData = loadCallDataFromFile("test_uniswap_v3_bebop");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(WBTC_ADDR).balanceOf(BOB);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, expectedAmountOut);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
}

File diff suppressed because one or more lines are too long

View File

@@ -5,5 +5,7 @@ mod encoding_utils;
mod group_swaps;
pub mod strategy_encoder;
mod swap_encoder;
#[cfg(feature = "test-utils")]
pub mod testing_utils;
pub mod tycho_encoders;
pub mod utils;

View File

@@ -1,17 +1,26 @@
use std::{collections::HashMap, str::FromStr};
use std::{collections::HashMap, str::FromStr, sync::Arc};
use alloy::{
primitives::{Address, Bytes as AlloyBytes, U256, U8},
sol_types::SolValue,
};
use serde_json::from_str;
use tycho_common::{models::Chain, Bytes};
use tokio::{
runtime::{Handle, Runtime},
task::block_in_place,
};
use tycho_common::{
models::{protocol::GetAmountOutParams, Chain},
Bytes,
};
use crate::encoding::{
errors::EncodingError,
evm::{
approvals::protocol_approvals_manager::ProtocolApprovalsManager,
utils::{bytes_to_address, get_static_attribute, pad_to_fixed_size},
utils::{
biguint_to_u256, bytes_to_address, get_runtime, get_static_attribute, pad_to_fixed_size,
},
},
models::{EncodingContext, Swap},
swap_encoder::SwapEncoder,
@@ -652,12 +661,21 @@ impl SwapEncoder for BalancerV3SwapEncoder {
pub struct BebopSwapEncoder {
executor_address: String,
settlement_address: String,
native_token_bebop_address: String,
native_token_address: Bytes,
runtime_handle: Handle,
// Store the runtime to prevent it from being dropped before use.
// This is required since tycho-execution does not have a pre-existing runtime.
// However, if the library is used in a context where a runtime already exists, it is not
// necessary to store it.
#[allow(dead_code)]
runtime: Option<Arc<Runtime>>,
}
impl SwapEncoder for BebopSwapEncoder {
fn new(
executor_address: String,
_chain: Chain,
chain: Chain,
config: Option<HashMap<String, String>>,
) -> Result<Self, EncodingError> {
let config = config.ok_or(EncodingError::FatalError(
@@ -669,7 +687,21 @@ impl SwapEncoder for BebopSwapEncoder {
"Missing bebop settlement address in config".to_string(),
))?
.to_string();
Ok(Self { executor_address, settlement_address })
let native_token_bebop_address = config
.get("native_token_address")
.ok_or(EncodingError::FatalError(
"Missing native token bebop address in config".to_string(),
))?
.to_string();
let (runtime_handle, runtime) = get_runtime()?;
Ok(Self {
executor_address,
settlement_address,
runtime_handle,
runtime,
native_token_bebop_address,
native_token_address: chain.native_token().address,
})
}
fn encode_swap(
@@ -685,16 +717,15 @@ impl SwapEncoder for BebopSwapEncoder {
if let Some(router_address) = &encoding_context.router_address {
let tycho_router_address = bytes_to_address(router_address)?;
let token_to_approve = token_in;
let settlement_address = Address::from_str(&self.settlement_address)
.map_err(|_| EncodingError::FatalError("Invalid settlement address".to_string()))?;
// Native ETH doesn't need approval, only ERC20 tokens do
if token_to_approve == Address::ZERO {
if swap.token_in == self.native_token_address {
approval_needed = false;
} else {
approval_needed = token_approvals_manager.approval_needed(
token_to_approve,
token_in,
tycho_router_address,
settlement_address,
)?;
@@ -703,25 +734,98 @@ impl SwapEncoder for BebopSwapEncoder {
approval_needed = true;
}
// The user data required for Bebop is
// partial_fill_offset (u8) | original_taker_amount (U256) | calldata (bytes (selector ABI
// encoded params))
let user_data = swap.user_data.clone().ok_or_else(|| {
EncodingError::InvalidInput("Bebop swaps require user_data with calldata".to_string())
})?;
let (partial_fill_offset, original_filled_taker_amount, bebop_calldata) =
if let Some(state) = swap.protocol_state {
let indicatively_priced_state = state
.as_indicatively_priced()
.map_err(|e| {
EncodingError::FatalError(format!("State is not indicatively priced {e}"))
})?;
let estimated_amount_in =
swap.estimated_amount_in
.clone()
.ok_or(EncodingError::FatalError(
"Estimated amount in is mandatory for a Bebop swap".to_string(),
))?;
// Bebop uses another address for the native token than the zero address
let bebop_native_address = Bytes::from_str(&self.native_token_bebop_address)
.map_err(|_| {
EncodingError::FatalError(
"Invalid Bebop native token curve address".to_string(),
)
})?;
if user_data.len() < 37 {
return Err(EncodingError::InvalidInput(
"User data too short to contain offset and Bebop calldata".to_string(),
));
}
let mut token_in = swap.token_in.clone();
if swap.token_in == self.native_token_address {
token_in = bebop_native_address.clone()
}
let mut token_out = swap.token_out.clone();
if swap.token_out == self.native_token_address {
token_out = bebop_native_address
}
let partial_fill_offset = user_data[0];
let original_filled_taker_amount = U256::from_be_slice(&user_data[1..33]);
let params = GetAmountOutParams {
amount_in: estimated_amount_in,
token_in,
token_out,
sender: encoding_context
.router_address
.clone()
.ok_or(EncodingError::FatalError(
"The router address is needed to perform a Bebop swap".to_string(),
))?,
receiver: encoding_context.receiver.clone(),
};
let signed_quote = block_in_place(|| {
self.runtime_handle.block_on(async {
indicatively_priced_state
.request_signed_quote(params)
.await
})
})
.map_err(|e| EncodingError::FatalError(format!("Failed to get Bebop quote {e}")))?;
let bebop_calldata = signed_quote
.quote_attributes
.get("calldata")
.ok_or(EncodingError::FatalError(
"Bebop quote must have a calldata attribute".to_string(),
))?;
let partial_fill_offset = signed_quote
.quote_attributes
.get("partial_fill_offset")
.ok_or(EncodingError::FatalError(
"Bebop quote must have a partial_fill_offset attribute".to_string(),
))?;
let original_filled_taker_amount = biguint_to_u256(&signed_quote.amount_out);
(
// we are only interested in the last byte to get a u8
partial_fill_offset[partial_fill_offset.len() - 1],
original_filled_taker_amount,
bebop_calldata.to_vec(),
)
} else {
// The user data required for Bebop is
// partial_fill_offset (u8) | original_taker_amount (U256) | calldata (bytes
// (selector ABI encoded params))
let user_data = swap.user_data.clone().ok_or_else(|| {
EncodingError::InvalidInput(
"Bebop swaps require user_data with calldata".to_string(),
)
})?;
// The calldata should be for either swapSingle or swapAggregate
let bebop_calldata = user_data[33..].to_vec();
if user_data.len() < 37 {
return Err(EncodingError::InvalidInput(
"User data too short to contain offset and Bebop calldata".to_string(),
));
}
let partial_fill_offset = user_data[0];
let original_filled_taker_amount = U256::from_be_slice(&user_data[1..33]);
// The calldata should be for either swapSingle or swapAggregate
let bebop_calldata = user_data[33..].to_vec();
(partial_fill_offset, original_filled_taker_amount, bebop_calldata)
};
let receiver = bytes_to_address(&encoding_context.receiver)?;
// Encode packed data for the executor
@@ -1723,10 +1827,26 @@ mod tests {
}
mod bebop {
use num_bigint::BigUint;
use super::*;
use crate::encoding::evm::testing_utils::MockRFQState;
fn bebop_config() -> HashMap<String, String> {
HashMap::from([
(
"bebop_settlement_address".to_string(),
"0xbbbbbBB520d69a9775E85b458C58c648259FAD5F".to_string(),
),
(
"native_token_address".to_string(),
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE".to_string(),
),
])
}
#[test]
fn test_encode_bebop_single() {
fn test_encode_bebop_single_with_user_data() {
// 200 USDC -> ONDO
let bebop_calldata= Bytes::from_str("0x4dcebcba00000000000000000000000000000000000000000000000000000000689b548f0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000067336cec42645f55059eff241cb02ea5cc52ff86000000000000000000000000000000000000000000000000279ead5d9685f25b000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be3000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000a8aea46aa4ec5c0f5000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000000005230bcb979c81cebf94a3b5c08bcfa300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000414ce40058ff07f11d9224c2c8d1e58369e4a90173856202d8d2a17da48058ad683dedb742eda0d4c0cf04cf1c09138898dd7fd06f97268ea7f74ef9b42d29bf4c1b00000000000000000000000000000000000000000000000000000000000000").unwrap();
let original_taker_amount = U256::from_str("200000000").unwrap();
@@ -1761,10 +1881,7 @@ mod tests {
let encoder = BebopSwapEncoder::new(
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
Chain::Ethereum,
Some(HashMap::from([(
"bebop_settlement_address".to_string(),
"0xbbbbbBB520d69a9775E85b458C58c648259FAD5F".to_string(),
)])),
Some(bebop_config()),
)
.unwrap();
@@ -1793,7 +1910,7 @@ mod tests {
}
#[test]
fn test_encode_bebop_aggregate() {
fn test_encode_bebop_aggregate_with_user_data() {
// 20k USDC -> ONDO
let bebop_calldata= Bytes::from_str("0xa2f7489300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000689b78880000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c0000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000005a060a5c2aaaaa2fe2cda34423cac76a84c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000002901f2d62bb356ca0000000000000000000000000000000000000000000000002901f2d62bb356cb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000044f83c726000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000589400da00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000003aa5f96046644f6e37a000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000004b51a26526ddbeec60000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000417ab4332f2b091d87d56d04eee35dd49452782c782de71608c0425c5ae41f1d7e147173851c870d76720ce07d45cd8622352716b1c7965819ee2bf8c573c499ae1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000410c8da2637aa929e11caff9afdfc4c489320c6dba77cc934d88ba8956e365fd1d48983087c6e474bbb828181cdfdd17317c4c9c3ee4bc98e3769d0c05cc7a285e1c00000000000000000000000000000000000000000000000000000000000000").unwrap();
let original_taker_amount = U256::from_str("20000000000").unwrap();
@@ -1829,10 +1946,7 @@ mod tests {
let encoder = BebopSwapEncoder::new(
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
Chain::Ethereum,
Some(HashMap::from([(
"bebop_settlement_address".to_string(),
"0xbbbbbBB520d69a9775E85b458C58c648259FAD5F".to_string(),
)])),
Some(bebop_config()),
)
.unwrap();
@@ -1860,5 +1974,71 @@ mod tests {
assert_eq!(hex_swap, expected_swap + &bebop_calldata.to_string()[2..]);
}
#[test]
fn test_encode_bebop_single_with_protocol_state() {
// 3000 USDC -> 1 WETH using a mocked RFQ state to get a quote
let bebop_calldata = Bytes::from_str("0x123456").unwrap();
let partial_fill_offset = 12u64;
let quote_amount_out = BigUint::from_str("1000000000000000000").unwrap();
let bebop_component = ProtocolComponent {
id: String::from("bebop-rfq"),
protocol_system: String::from("rfq:bebop"),
..Default::default()
};
let bebop_state = MockRFQState {
quote_amount_out,
quote_calldata: bebop_calldata.clone(),
quote_partial_fill_offset: partial_fill_offset,
};
let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC
let token_out = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); // WETH
let swap = SwapBuilder::new(bebop_component, token_in.clone(), token_out.clone())
.estimated_amount_in(BigUint::from_str("3000000000").unwrap())
.protocol_state(&bebop_state)
.build();
let encoding_context = EncodingContext {
receiver: Bytes::from("0xc5564C13A157E6240659fb81882A28091add8670"),
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 = BebopSwapEncoder::new(
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
Chain::Ethereum,
Some(bebop_config()),
)
.unwrap();
let encoded_swap = encoder
.encode_swap(&swap, &encoding_context)
.unwrap();
let hex_swap = encode(&encoded_swap);
let expected_swap = String::from(concat!(
// token in
"a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
// token out
"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
// transfer type
"01",
// partiall filled offset
"0c",
// original taker amount
"0000000000000000000000000000000000000000000000000de0b6b3a7640000",
// approval needed
"01",
//receiver,
"c5564c13a157e6240659fb81882a28091add8670",
));
assert_eq!(hex_swap, expected_swap + &bebop_calldata.to_string()[2..]);
}
}
}

View File

@@ -0,0 +1,104 @@
// This module is used in integration tests as well
use std::{any::Any, collections::HashMap};
use async_trait::async_trait;
use num_bigint::BigUint;
use tycho_common::{
dto::ProtocolStateDelta,
models::{protocol::GetAmountOutParams, token::Token},
simulation::{
errors::{SimulationError, TransitionError},
indicatively_priced::{IndicativelyPriced, SignedQuote},
protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
},
Bytes,
};
#[derive(Debug)]
pub struct MockRFQState {
pub quote_amount_out: BigUint,
pub quote_calldata: Bytes,
pub quote_partial_fill_offset: u64,
}
impl ProtocolSim for MockRFQState {
fn fee(&self) -> f64 {
panic!("MockRFQState does not implement fee")
}
fn spot_price(&self, _base: &Token, _quote: &Token) -> Result<f64, SimulationError> {
panic!("MockRFQState does not implement fee")
}
fn get_amount_out(
&self,
_amount_in: BigUint,
_token_in: &Token,
_token_out: &Token,
) -> Result<GetAmountOutResult, SimulationError> {
panic!("MockRFQState does not implement fee")
}
fn get_limits(
&self,
_sell_token: Bytes,
_buy_token: Bytes,
) -> Result<(BigUint, BigUint), SimulationError> {
panic!("MockRFQState does not implement fee")
}
fn delta_transition(
&mut self,
_delta: ProtocolStateDelta,
_tokens: &HashMap<Bytes, Token>,
_balances: &Balances,
) -> Result<(), TransitionError<String>> {
panic!("MockRFQState does not implement fee")
}
fn clone_box(&self) -> Box<dyn ProtocolSim> {
panic!("MockRFQState does not implement fee")
}
fn as_any(&self) -> &dyn Any {
panic!("MockRFQState does not implement fee")
}
fn as_any_mut(&mut self) -> &mut dyn Any {
panic!("MockRFQState does not implement fee")
}
fn eq(&self, _other: &dyn ProtocolSim) -> bool {
panic!("MockRFQState does not implement fee")
}
fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
Ok(self)
}
}
#[async_trait]
impl IndicativelyPriced for MockRFQState {
async fn request_signed_quote(
&self,
params: GetAmountOutParams,
) -> Result<SignedQuote, SimulationError> {
let mut quote_attributes: HashMap<String, Bytes> = HashMap::new();
quote_attributes.insert("calldata".to_string(), self.quote_calldata.clone());
quote_attributes.insert(
"partial_fill_offset".to_string(),
Bytes::from(
self.quote_partial_fill_offset
.to_be_bytes()
.to_vec(),
),
);
Ok(SignedQuote {
base_token: params.token_in,
quote_token: params.token_out,
amount_in: params.amount_in,
amount_out: self.quote_amount_out.clone(),
quote_attributes,
})
}
}

View File

@@ -17,6 +17,10 @@ pub fn router_address() -> Bytes {
Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap()
}
pub fn bob_address() -> Bytes {
Bytes::from_str("0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e").unwrap()
}
pub fn eth_chain() -> Chain {
Chain::Ethereum
}
@@ -64,7 +68,7 @@ pub fn get_signer() -> PrivateKeySigner {
pub fn get_tycho_router_encoder(user_transfer_type: UserTransferType) -> Box<dyn TychoEncoder> {
TychoRouterEncoderBuilder::new()
.chain(tycho_common::models::Chain::Ethereum)
.chain(Chain::Ethereum)
.user_transfer_type(user_transfer_type)
.executors_file_path("config/test_executor_addresses.json".to_string())
.router_address(router_address())

View File

@@ -4,12 +4,13 @@ use alloy::hex::encode;
use num_bigint::{BigInt, BigUint};
use tycho_common::{models::protocol::ProtocolComponent, Bytes};
use tycho_execution::encoding::{
evm::utils::write_calldata_to_file,
models::{Solution, Swap, UserTransferType},
evm::{testing_utils::MockRFQState, utils::write_calldata_to_file},
models::{Solution, Swap, SwapBuilder, UserTransferType},
};
use crate::common::{
encoding::encode_tycho_router_call, eth, eth_chain, get_signer, get_tycho_router_encoder, weth,
bob_address, encoding::encode_tycho_router_call, eth, eth_chain, get_signer,
get_tycho_router_encoder, usdc, wbtc, weth,
};
mod common;
@@ -608,3 +609,92 @@ fn test_uniswap_v3_balancer_v3() {
let hex_calldata = encode(&calldata);
write_calldata_to_file("test_uniswap_v3_balancer_v3", hex_calldata.as_str());
}
#[test]
fn test_uniswap_v3_bebop() {
// Note: This test does not assert anything. It is only used to obtain
// integration test data for our router solidity test.
//
// Performs a sequential swap from WETH to ONDO through USDC using USV3 and
// Bebop RFQ
//
// WETH ───(USV3)──> USDC ───(Bebop RFQ)──> WBTC
let weth = weth();
let usdc = usdc();
let wbtc = wbtc();
// First swap: WETH -> USDC via UniswapV3
let swap_weth_usdc = SwapBuilder::new(
ProtocolComponent {
id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* WETH-USDC USV3 Pool
* 0.05% */
protocol_system: "uniswap_v3".to_string(),
static_attributes: {
let mut attrs = HashMap::new();
attrs
.insert("fee".to_string(), Bytes::from(BigInt::from(500).to_signed_bytes_be()));
attrs
},
..Default::default()
},
weth.clone(),
usdc.clone(),
)
.build();
// Second swap: USDC -> WBTC via Bebop RFQ using real order data
let bebop_calldata = Bytes::from_str("0x4dcebcba00000000000000000000000000000000000000000000000000000000689dcb3c0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000bee3211ab312a8d065c4fef0247448e17a8da0000000000000000000000000000000000000000000000000002901f2d62bc91b77000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000000000000000000000000000000000007881786100000000000000000000000000000000000000000000000000000000001984730000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000000000000000000000000000000000000000000000000000000000000000a02bc8495ad1c76c31d466ce719f80400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041f3a03b07f390cd707402912278414c46190ca8ca362dd218b9a58956178cb6ee0e5755db7abe02fe15d498d092d4c6865a5eb18486b3e45e27d50d34b87bf1e21c00000000000000000000000000000000000000000000000000000000000000").unwrap();
let partial_fill_offset = 12u64;
let quote_amount_out = BigUint::from_str("1672307").unwrap();
let bebop_state = MockRFQState {
quote_amount_out,
quote_calldata: bebop_calldata.clone(),
quote_partial_fill_offset: partial_fill_offset,
};
let bebop_component = ProtocolComponent {
id: String::from("bebop-rfq"),
protocol_system: String::from("rfq:bebop"),
..Default::default()
};
let swap_usdc_wbtc = SwapBuilder::new(bebop_component, usdc.clone(), wbtc.clone())
.estimated_amount_in(BigUint::from_str("2021750881").unwrap())
.protocol_state(&bebop_state)
.build();
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let solution = Solution {
exact_out: false,
given_token: weth,
given_amount: BigUint::from_str("1000000000000000000").unwrap(), // 0.099 WETH
checked_token: wbtc,
checked_amount: BigUint::from_str("1672307").unwrap(),
sender: bob_address(),
receiver: bob_address(),
swaps: vec![swap_weth_usdc, swap_usdc_wbtc],
..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_uniswap_v3_bebop", hex_calldata.as_str());
}