Merge pull request #251 from propeller-heads/rfq/dc/ENG-4696-request-code-in-encoding
feat: Support Bebop quote through IndicativelyPriced state
This commit is contained in:
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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
|
||||
@@ -11,12 +11,13 @@
|
||||
"vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165",
|
||||
"vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B",
|
||||
"vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec",
|
||||
"rfq:bebop": "0xEDCA8A3ACEB5db816d5CF833248d05Ed2784A304"
|
||||
"rfq:bebop": "0xFE42BFb115eD9671011cA52BDD23A52A2e077a7c"
|
||||
},
|
||||
"base": {
|
||||
"uniswap_v2": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700",
|
||||
"uniswap_v3": "0x647bffbf8bd72bf6341ecba8b0279e090313a40d",
|
||||
"uniswap_v4": "0xb5628b97f491f85766673ac4d5d47bb1af7fcc4a"
|
||||
"uniswap_v4": "0xb5628b97f491f85766673ac4d5d47bb1af7fcc4a",
|
||||
"rfq:bebop": "0x489A3f531dA3873D6585BF3f8E0dEE48CAC6F7BC"
|
||||
},
|
||||
"unichain": {
|
||||
"uniswap_v2": "0x00C1b81e3C8f6347E69e2DDb90454798A6Be975E",
|
||||
|
||||
@@ -7,9 +7,15 @@
|
||||
"native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
||||
},
|
||||
"rfq:bebop": {
|
||||
"bebop_settlement_address": "0xbbbbbBB520d69a9775E85b458C58c648259FAD5F"
|
||||
"bebop_settlement_address": "0xbbbbbBB520d69a9775E85b458C58c648259FAD5F",
|
||||
"native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
||||
}
|
||||
},
|
||||
"base": {
|
||||
"rfq:bebop": {
|
||||
"bebop_settlement_address": "0xbbbbbBB520d69a9775E85b458C58c648259FAD5F",
|
||||
"native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
||||
}
|
||||
},
|
||||
"base": {},
|
||||
"unichain": {}
|
||||
}
|
||||
@@ -121,6 +121,11 @@ const executors_to_deploy = {
|
||||
"0x000000000022D473030F116dDEE9F6B43aC78BA3"
|
||||
]
|
||||
},
|
||||
// Args: Bebop Settlement contract, Permit2
|
||||
{
|
||||
exchange: "BebopExecutor",
|
||||
args: ["0xbbbbbBB520d69a9775E85b458C58c648259FAD5F", "0x000000000022D473030F116dDEE9F6B43aC78BA3"]
|
||||
},
|
||||
],
|
||||
"unichain": [
|
||||
// Args: Factory, Pool Init Code Hash, Permit2, Fee BPS
|
||||
|
||||
@@ -14,14 +14,11 @@ import "@openzeppelin/contracts/utils/Address.sol";
|
||||
/// @notice Executor for Bebop PMM RFQ (Request for Quote) swaps
|
||||
/// @dev Handles Single and Aggregate RFQ swaps through Bebop settlement contract
|
||||
/// @dev Only supports single token in to single token out swaps
|
||||
contract BebopExecutor is IExecutor, IExecutorErrors, RestrictTransferFrom {
|
||||
contract BebopExecutor is IExecutor, RestrictTransferFrom {
|
||||
using Math for uint256;
|
||||
using SafeERC20 for IERC20;
|
||||
using Address for address;
|
||||
|
||||
/// @notice Function selectors for Bebop settlement methods
|
||||
bytes4 public constant SWAP_AGGREGATE_SELECTOR = 0xa2f74893;
|
||||
|
||||
/// @notice Bebop-specific errors
|
||||
error BebopExecutor__InvalidDataLength();
|
||||
error BebopExecutor__ZeroAddress();
|
||||
|
||||
@@ -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
@@ -1,6 +1,7 @@
|
||||
use std::{io, str::Utf8Error};
|
||||
|
||||
use thiserror::Error;
|
||||
use tycho_common::simulation::errors::SimulationError;
|
||||
|
||||
/// Represents the outer-level, user-facing errors of the tycho-execution encoding package.
|
||||
///
|
||||
@@ -41,3 +42,15 @@ impl From<Utf8Error> for EncodingError {
|
||||
EncodingError::FatalError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SimulationError> for EncodingError {
|
||||
fn from(err: SimulationError) -> Self {
|
||||
match err {
|
||||
SimulationError::FatalError(err_msg) => EncodingError::FatalError(err_msg),
|
||||
SimulationError::InvalidInput(err_msg, ..) => EncodingError::InvalidInput(err_msg),
|
||||
SimulationError::RecoverableError(error_msg) => {
|
||||
EncodingError::RecoverableError(error_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: Bytes,
|
||||
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,25 @@ 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 native_token_bebop_address =
|
||||
Bytes::from_str(&native_token_bebop_address).map_err(|_| {
|
||||
EncodingError::FatalError("Invalid Bebop native token address".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 +721,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 +738,90 @@ 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 mut token_in = swap.token_in.clone();
|
||||
if swap.token_in == self.native_token_address {
|
||||
token_in = self.native_token_bebop_address.clone()
|
||||
}
|
||||
let mut token_out = swap.token_out.clone();
|
||||
if swap.token_out == self.native_token_address {
|
||||
token_out = self.native_token_bebop_address.clone()
|
||||
}
|
||||
|
||||
if user_data.len() < 37 {
|
||||
return Err(EncodingError::InvalidInput(
|
||||
"User data too short to contain offset and Bebop calldata".to_string(),
|
||||
));
|
||||
}
|
||||
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
|
||||
})
|
||||
})?;
|
||||
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(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let partial_fill_offset = user_data[0];
|
||||
let original_filled_taker_amount = U256::from_be_slice(&user_data[1..33]);
|
||||
if user_data.len() < 37 {
|
||||
return Err(EncodingError::InvalidInput(
|
||||
"User data too short to contain offset and Bebop calldata".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// The calldata should be for either swapSingle or swapAggregate
|
||||
let bebop_calldata = user_data[33..].to_vec();
|
||||
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 +1823,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 +1877,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 +1906,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 +1942,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 +1970,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..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
104
src/encoding/evm/testing_utils.rs
Normal file
104
src/encoding/evm/testing_utils.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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 WBTC 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,
|
||||
ð(),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
.data;
|
||||
|
||||
let hex_calldata = encode(&calldata);
|
||||
write_calldata_to_file("test_uniswap_v3_bebop", hex_calldata.as_str());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user