fix: Move Bebop tests according to new setup

Encoding: integration tests are now separate and inside their own test folder
Execution: the final integration test should be inside of the protocol test file now and not in TychoRouterProtocolIntegration.t.sol. For this I had to move the BebopExecutionHarness.t.sol outside of the Bebop test file (because of imports)

Took 24 minutes

# Commit time for manual adjustment:
# Took 2 minutes
This commit is contained in:
Diana Carvalho
2025-06-24 10:39:58 +01:00
parent f1281eb703
commit 01ab5d22b1
10 changed files with 771 additions and 2670 deletions

View File

@@ -49,81 +49,4 @@ contract TychoRouterTestProtocolIntegration is TychoRouterTestSetup {
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 235610487387677804636755778);
}
function testSingleBebopIntegration() public {
// The calldata swaps 200 USDC for ONDO
// The receiver in the order is 0xc5564C13A157E6240659fb81882A28091add8670
address orderTaker = 0xc5564C13A157E6240659fb81882A28091add8670;
address maker = 0xCe79b081c0c924cb67848723ed3057234d10FC6b;
deal(USDC_ADDR, orderTaker, 200 * 10 ** 6); // 200 USDC
uint256 expAmountOut = 237212396774431060000; // Expected ONDO amount from calldata
// Fund the maker with ONDO and approve settlement
deal(ONDO_ADDR, maker, expAmountOut);
vm.prank(maker);
IERC20(ONDO_ADDR).approve(BEBOP_SETTLEMENT, expAmountOut);
uint256 ondoBefore = IERC20(ONDO_ADDR).balanceOf(orderTaker);
vm.startPrank(orderTaker);
IERC20(USDC_ADDR).approve(tychoRouterAddr, type(uint256).max);
// Load calldata from file
bytes memory callData =
loadCallDataFromFile("test_single_encoding_strategy_bebop");
(bool success,) = tychoRouterAddr.call(callData);
// Check the receiver's balance (not ALICE, since the order specifies a different receiver)
uint256 ondoReceived =
IERC20(ONDO_ADDR).balanceOf(orderTaker) - ondoBefore;
assertTrue(success, "Call Failed");
assertEq(ondoReceived, expAmountOut);
assertEq(
IERC20(USDC_ADDR).balanceOf(tychoRouterAddr),
0,
"USDC left in router"
);
vm.stopPrank();
}
function testBebopAggregateIntegration() public {
// Based on real transaction: https://etherscan.io/tx/0xec88410136c287280da87d0a37c1cb745f320406ca3ae55c678dec11996c1b1c
address orderTaker = 0x7078B12Ca5B294d95e9aC16D90B7D38238d8F4E6; // This is both taker and receiver in the order
uint256 ethAmount = 9850000000000000; // 0.00985 WETH
uint256 expAmountOut = 17969561; // 17.969561 USDC expected output
// Fund the two makers from the real transaction with USDC
address maker1 = 0x67336Cec42645F55059EfF241Cb02eA5cC52fF86;
address maker2 = 0xBF19CbF0256f19f39A016a86Ff3551ecC6f2aAFE;
deal(USDC_ADDR, maker1, 10607211); // Maker 1 provides 10.607211 USDC
deal(USDC_ADDR, maker2, 7362350); // Maker 2 provides 7.362350 USDC
// Makers approve settlement contract
vm.prank(maker1);
IERC20(USDC_ADDR).approve(BEBOP_SETTLEMENT, type(uint256).max);
vm.prank(maker2);
IERC20(USDC_ADDR).approve(BEBOP_SETTLEMENT, type(uint256).max);
// Fund ALICE with ETH as it will send the transaction
vm.deal(ALICE, ethAmount);
vm.startPrank(ALICE);
// Load calldata from file
bytes memory callData = loadCallDataFromFile(
"test_single_encoding_strategy_bebop_aggregate"
);
// Execute the swap
(bool success,) = tychoRouterAddr.call{value: ethAmount}(callData);
uint256 finalBalance = IERC20(USDC_ADDR).balanceOf(orderTaker);
assertTrue(success, "Call Failed");
assertEq(finalBalance, expAmountOut);
assertEq(address(tychoRouterAddr).balance, 0, "ETH left in router");
vm.stopPrank();
}
}

View File

@@ -13,16 +13,17 @@ import {
IUniswapV3Pool
} from "../src/executors/UniswapV3Executor.sol";
import {UniswapV4Executor} from "../src/executors/UniswapV4Executor.sol";
import {BebopExecutorHarness} from "./executors/BebopExecutor.t.sol";
import {BebopExecutorHarness} from "./protocols/BebopExecutionHarness.t.sol";
// Test utilities and mocks
import "./Constants.sol";
import "./TestUtils.sol";
import {Permit2TestHelper} from "./Permit2TestHelper.sol";
import {EkuboExecutor} from "../src/executors/EkuboExecutor.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol";
// Core contracts and interfaces
import "@src/TychoRouter.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {Permit2TestHelper} from "./Permit2TestHelper.sol";
import {UniswapV2Executor} from "../src/executors/UniswapV2Executor.sol";
import {UniswapV4Executor} from "../src/executors/UniswapV4Executor.sol";
contract TychoRouterExposed is TychoRouter {
constructor(address _permit2, address weth) TychoRouter(_permit2, weth) {}

File diff suppressed because one or more lines are too long

View File

@@ -2,210 +2,15 @@
pragma solidity ^0.8.26;
import "../TestUtils.sol";
import "../TychoRouterTestSetup.sol";
import "./BebopExecutionHarness.t.sol";
import "@src/executors/BebopExecutor.sol";
import {Constants} from "../Constants.sol";
import {Permit2TestHelper} from "../Permit2TestHelper.sol";
import {Test, console} from "forge-std/Test.sol";
import {StdCheats} from "forge-std/StdCheats.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Permit2TestHelper} from "../Permit2TestHelper.sol";
import {SafeERC20} from
"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract MockToken is ERC20 {
uint8 private _decimals;
constructor(string memory name_, string memory symbol_, uint8 decimals_)
ERC20(name_, symbol_)
{
_decimals = decimals_;
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function decimals() public view virtual override returns (uint8) {
return _decimals;
}
}
contract BebopExecutorHarness is BebopExecutor, Test {
using SafeERC20 for IERC20;
constructor(address _bebopSettlement, address _permit2)
BebopExecutor(_bebopSettlement, _permit2)
{}
// Expose the internal decodeData function for testing
function decodeParams(bytes calldata data)
external
pure
returns (
address tokenIn,
address tokenOut,
RestrictTransferFrom.TransferType transferType,
BebopExecutor.OrderType orderType,
uint256 filledTakerAmount,
bytes memory quoteData,
bytes memory makerSignaturesData,
bool // approvalNeeded - unused in test harness
)
{
return _decodeData(data);
}
// Expose the internal getActualFilledTakerAmount function for testing
function exposed_getActualFilledTakerAmount(
uint256 givenAmount,
uint256 orderTakerAmount,
uint256 filledTakerAmount
) external pure returns (uint256 actualFilledTakerAmount) {
return _getActualFilledTakerAmount(
givenAmount, orderTakerAmount, filledTakerAmount
);
}
// Override to prank the taker address before calling the real settlement
function _executeSingleRFQ(
address tokenIn,
address tokenOut,
TransferType transferType,
uint256 givenAmount,
uint256 filledTakerAmount,
bytes memory quoteData,
bytes memory makerSignaturesData,
bool approvalNeeded
) internal virtual override returns (uint256 amountOut) {
// Decode the order from quoteData
IBebopSettlement.Single memory order =
abi.decode(quoteData, (IBebopSettlement.Single));
uint256 actualFilledTakerAmount = _getActualFilledTakerAmount(
givenAmount, order.taker_amount, filledTakerAmount
);
// For testing: transfer tokens from executor to taker address
// This simulates the taker having the tokens with approval
if (tokenIn != address(0)) {
_transfer(
address(this), transferType, tokenIn, actualFilledTakerAmount
);
IERC20(tokenIn).safeTransfer(
order.taker_address, actualFilledTakerAmount
);
// Approve settlement from taker's perspective
// Stop any existing prank first
vm.stopPrank();
vm.startPrank(order.taker_address);
IERC20(tokenIn).forceApprove(bebopSettlement, type(uint256).max);
vm.stopPrank();
} else {
vm.stopPrank();
// For native ETH, send it to the taker address
payable(order.taker_address).transfer(actualFilledTakerAmount);
}
// IMPORTANT: Prank as the taker address to pass the settlement validation
vm.stopPrank();
vm.startPrank(order.taker_address);
// Set block timestamp to ensure order is valid regardless of fork block
uint256 currentTimestamp = block.timestamp;
vm.warp(order.expiry - 1); // Set timestamp to just before expiry
// Execute the single swap, let's test the actual settlement logic
amountOut = super._executeSingleRFQ(
tokenIn,
tokenOut,
TransferType.None, // We set transfer type to none for testing in order to keep the taker's balance unchanged as it will execute the swap
givenAmount,
filledTakerAmount,
quoteData,
makerSignaturesData,
approvalNeeded
);
// Restore original timestamp
vm.warp(currentTimestamp);
vm.stopPrank();
}
// Override to execute aggregate orders through the real settlement
function _executeAggregateRFQ(
address tokenIn,
address tokenOut,
TransferType transferType,
uint256 givenAmount,
uint256 filledTakerAmount,
bytes memory quoteData,
bytes memory makerSignaturesData,
bool approvalNeeded
) internal virtual override returns (uint256 amountOut) {
// Decode the Aggregate order
IBebopSettlement.Aggregate memory order =
abi.decode(quoteData, (IBebopSettlement.Aggregate));
// For aggregate orders, calculate total taker amount across all amounts of the 2D array
uint256 totalTakerAmount = 0;
for (uint256 i = 0; i < order.taker_amounts.length; i++) {
for (uint256 j = 0; j < order.taker_amounts[i].length; j++) {
totalTakerAmount += order.taker_amounts[i][j];
}
}
uint256 actualFilledTakerAmount = _getActualFilledTakerAmount(
givenAmount, totalTakerAmount, filledTakerAmount
);
// For testing: transfer tokens from executor to taker address
// This simulates the taker having the tokens with approval
if (tokenIn != address(0)) {
_transfer(
address(this), transferType, tokenIn, actualFilledTakerAmount
);
IERC20(tokenIn).safeTransfer(
order.taker_address, actualFilledTakerAmount
);
// Approve settlement from taker's perspective
// Stop any existing prank first
vm.stopPrank();
vm.startPrank(order.taker_address);
IERC20(tokenIn).forceApprove(bebopSettlement, type(uint256).max);
vm.stopPrank();
} else {
vm.stopPrank();
// For native ETH, send it to the taker address
payable(order.taker_address).transfer(actualFilledTakerAmount);
}
// IMPORTANT: Prank as the taker address to pass the settlement validation
vm.stopPrank();
vm.startPrank(order.taker_address);
// Set block timestamp to ensure order is valid regardless of fork block
uint256 currentTimestamp = block.timestamp;
vm.warp(order.expiry - 1); // Set timestamp to just before expiry
// Execute the aggregate swap, let's test the actual settlement logic
amountOut = super._executeAggregateRFQ(
tokenIn,
tokenOut,
TransferType.None, // We set transfer type to none for testing in order to keep the taker's balance unchanged as it will execute the swap
givenAmount,
filledTakerAmount,
quoteData,
makerSignaturesData,
approvalNeeded
);
// Restore original timestamp
vm.warp(currentTimestamp);
vm.stopPrank();
}
}
contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
using SafeERC20 for IERC20;
@@ -1301,3 +1106,82 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
}
}
}
contract TychoRouterForBebopTest is TychoRouterTestSetup {
function testSingleBebopIntegration() public {
// The calldata swaps 200 USDC for ONDO
// The receiver in the order is 0xc5564C13A157E6240659fb81882A28091add8670
address orderTaker = 0xc5564C13A157E6240659fb81882A28091add8670;
address maker = 0xCe79b081c0c924cb67848723ed3057234d10FC6b;
deal(USDC_ADDR, orderTaker, 200 * 10 ** 6); // 200 USDC
uint256 expAmountOut = 237212396774431060000; // Expected ONDO amount from calldata
// Fund the maker with ONDO and approve settlement
deal(ONDO_ADDR, maker, expAmountOut);
vm.prank(maker);
IERC20(ONDO_ADDR).approve(BEBOP_SETTLEMENT, expAmountOut);
uint256 ondoBefore = IERC20(ONDO_ADDR).balanceOf(orderTaker);
vm.startPrank(orderTaker);
IERC20(USDC_ADDR).approve(tychoRouterAddr, type(uint256).max);
// Load calldata from file
bytes memory callData =
loadCallDataFromFile("test_single_encoding_strategy_bebop");
(bool success,) = tychoRouterAddr.call(callData);
// Check the receiver's balance (not ALICE, since the order specifies a different receiver)
uint256 ondoReceived =
IERC20(ONDO_ADDR).balanceOf(orderTaker) - ondoBefore;
assertTrue(success, "Call Failed");
assertEq(ondoReceived, expAmountOut);
assertEq(
IERC20(USDC_ADDR).balanceOf(tychoRouterAddr),
0,
"USDC left in router"
);
vm.stopPrank();
}
function testBebopAggregateIntegration() public {
// Based on real transaction: https://etherscan.io/tx/0xec88410136c287280da87d0a37c1cb745f320406ca3ae55c678dec11996c1b1c
address orderTaker = 0x7078B12Ca5B294d95e9aC16D90B7D38238d8F4E6; // This is both taker and receiver in the order
uint256 ethAmount = 9850000000000000; // 0.00985 WETH
uint256 expAmountOut = 17969561; // 17.969561 USDC expected output
// Fund the two makers from the real transaction with USDC
address maker1 = 0x67336Cec42645F55059EfF241Cb02eA5cC52fF86;
address maker2 = 0xBF19CbF0256f19f39A016a86Ff3551ecC6f2aAFE;
deal(USDC_ADDR, maker1, 10607211); // Maker 1 provides 10.607211 USDC
deal(USDC_ADDR, maker2, 7362350); // Maker 2 provides 7.362350 USDC
// Makers approve settlement contract
vm.prank(maker1);
IERC20(USDC_ADDR).approve(BEBOP_SETTLEMENT, type(uint256).max);
vm.prank(maker2);
IERC20(USDC_ADDR).approve(BEBOP_SETTLEMENT, type(uint256).max);
// Fund ALICE with ETH as it will send the transaction
vm.deal(ALICE, ethAmount);
vm.startPrank(ALICE);
// Load calldata from file
bytes memory callData = loadCallDataFromFile(
"test_single_encoding_strategy_bebop_aggregate"
);
// Execute the swap
(bool success,) = tychoRouterAddr.call{value: ethAmount}(callData);
uint256 finalBalance = IERC20(USDC_ADDR).balanceOf(orderTaker);
assertTrue(success, "Call Failed");
assertEq(finalBalance, expAmountOut);
assertEq(address(tychoRouterAddr).balance, 0, "ETH left in router");
vm.stopPrank();
}
}

View File

@@ -0,0 +1,182 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.10;
import "../../src/executors/BebopExecutor.sol";
import {Test, console} from "forge-std/Test.sol";
contract BebopExecutorHarness is BebopExecutor, Test {
using SafeERC20 for IERC20;
constructor(address _bebopSettlement, address _permit2)
BebopExecutor(_bebopSettlement, _permit2)
{}
// Expose the internal decodeData function for testing
function decodeParams(bytes calldata data)
external
pure
returns (
address tokenIn,
address tokenOut,
RestrictTransferFrom.TransferType transferType,
BebopExecutor.OrderType orderType,
uint256 filledTakerAmount,
bytes memory quoteData,
bytes memory makerSignaturesData,
bool // approvalNeeded - unused in test harness
)
{
return _decodeData(data);
}
// Expose the internal getActualFilledTakerAmount function for testing
function exposed_getActualFilledTakerAmount(
uint256 givenAmount,
uint256 orderTakerAmount,
uint256 filledTakerAmount
) external pure returns (uint256 actualFilledTakerAmount) {
return _getActualFilledTakerAmount(
givenAmount, orderTakerAmount, filledTakerAmount
);
}
// Override to prank the taker address before calling the real settlement
function _executeSingleRFQ(
address tokenIn,
address tokenOut,
TransferType transferType,
uint256 givenAmount,
uint256 filledTakerAmount,
bytes memory quoteData,
bytes memory makerSignaturesData,
bool approvalNeeded
) internal virtual override returns (uint256 amountOut) {
// Decode the order from quoteData
IBebopSettlement.Single memory order =
abi.decode(quoteData, (IBebopSettlement.Single));
uint256 actualFilledTakerAmount = _getActualFilledTakerAmount(
givenAmount, order.taker_amount, filledTakerAmount
);
// For testing: transfer tokens from executor to taker address
// This simulates the taker having the tokens with approval
if (tokenIn != address(0)) {
_transfer(
address(this), transferType, tokenIn, actualFilledTakerAmount
);
IERC20(tokenIn).safeTransfer(
order.taker_address, actualFilledTakerAmount
);
// Approve settlement from taker's perspective
// Stop any existing prank first
vm.stopPrank();
vm.startPrank(order.taker_address);
IERC20(tokenIn).forceApprove(bebopSettlement, type(uint256).max);
vm.stopPrank();
} else {
vm.stopPrank();
// For native ETH, send it to the taker address
payable(order.taker_address).transfer(actualFilledTakerAmount);
}
// IMPORTANT: Prank as the taker address to pass the settlement validation
vm.stopPrank();
vm.startPrank(order.taker_address);
// Set block timestamp to ensure order is valid regardless of fork block
uint256 currentTimestamp = block.timestamp;
vm.warp(order.expiry - 1); // Set timestamp to just before expiry
// Execute the single swap, let's test the actual settlement logic
amountOut = super._executeSingleRFQ(
tokenIn,
tokenOut,
TransferType.None, // We set transfer type to none for testing in order to keep the taker's balance unchanged as it will execute the swap
givenAmount,
filledTakerAmount,
quoteData,
makerSignaturesData,
approvalNeeded
);
// Restore original timestamp
vm.warp(currentTimestamp);
vm.stopPrank();
}
// Override to execute aggregate orders through the real settlement
function _executeAggregateRFQ(
address tokenIn,
address tokenOut,
TransferType transferType,
uint256 givenAmount,
uint256 filledTakerAmount,
bytes memory quoteData,
bytes memory makerSignaturesData,
bool approvalNeeded
) internal virtual override returns (uint256 amountOut) {
// Decode the Aggregate order
IBebopSettlement.Aggregate memory order =
abi.decode(quoteData, (IBebopSettlement.Aggregate));
// For aggregate orders, calculate total taker amount across all amounts of the 2D array
uint256 totalTakerAmount = 0;
for (uint256 i = 0; i < order.taker_amounts.length; i++) {
for (uint256 j = 0; j < order.taker_amounts[i].length; j++) {
totalTakerAmount += order.taker_amounts[i][j];
}
}
uint256 actualFilledTakerAmount = _getActualFilledTakerAmount(
givenAmount, totalTakerAmount, filledTakerAmount
);
// For testing: transfer tokens from executor to taker address
// This simulates the taker having the tokens with approval
if (tokenIn != address(0)) {
_transfer(
address(this), transferType, tokenIn, actualFilledTakerAmount
);
IERC20(tokenIn).safeTransfer(
order.taker_address, actualFilledTakerAmount
);
// Approve settlement from taker's perspective
// Stop any existing prank first
vm.stopPrank();
vm.startPrank(order.taker_address);
IERC20(tokenIn).forceApprove(bebopSettlement, type(uint256).max);
vm.stopPrank();
} else {
vm.stopPrank();
// For native ETH, send it to the taker address
payable(order.taker_address).transfer(actualFilledTakerAmount);
}
// IMPORTANT: Prank as the taker address to pass the settlement validation
vm.stopPrank();
vm.startPrank(order.taker_address);
// Set block timestamp to ensure order is valid regardless of fork block
uint256 currentTimestamp = block.timestamp;
vm.warp(order.expiry - 1); // Set timestamp to just before expiry
// Execute the aggregate swap, let's test the actual settlement logic
amountOut = super._executeAggregateRFQ(
tokenIn,
tokenOut,
TransferType.None, // We set transfer type to none for testing in order to keep the taker's balance unchanged as it will execute the swap
givenAmount,
filledTakerAmount,
quoteData,
makerSignaturesData,
approvalNeeded
);
// Restore original timestamp
vm.warp(currentTimestamp);
vm.stopPrank();
}
}

View File

@@ -776,8 +776,8 @@ impl SwapEncoder for BebopSwapEncoder {
fn encode_swap(
&self,
swap: Swap,
encoding_context: EncodingContext,
swap: &Swap,
encoding_context: &EncodingContext,
) -> Result<Vec<u8>, EncodingError> {
let token_in = bytes_to_address(&swap.token_in)?;
let token_out = bytes_to_address(&swap.token_out)?;
@@ -785,8 +785,8 @@ impl SwapEncoder for BebopSwapEncoder {
let token_approvals_manager = ProtocolApprovalsManager::new()?;
let approval_needed: bool;
if let Some(router_address) = encoding_context.router_address {
let tycho_router_address = bytes_to_address(&router_address)?;
if let Some(router_address) = &encoding_context.router_address {
let tycho_router_address = bytes_to_address(router_address)?;
let token_to_approve = token_in.clone();
let settlement_address = Address::from_str(&self.settlement_address)
.map_err(|_| EncodingError::FatalError("Invalid settlement address".to_string()))?;
@@ -809,7 +809,7 @@ impl SwapEncoder for BebopSwapEncoder {
Self::validate_component_id(&swap.component.id)?;
// Extract data from user_data (required for Bebop)
let user_data = swap.user_data.ok_or_else(|| {
let user_data = swap.user_data.clone().ok_or_else(|| {
EncodingError::InvalidInput(
"Bebop swaps require user_data with quote and signature".to_string(),
)
@@ -2202,7 +2202,7 @@ mod tests {
.unwrap();
let encoded_swap = encoder
.encode_swap(swap, encoding_context)
.encode_swap(&swap, &encoding_context)
.unwrap();
let hex_swap = encode(&encoded_swap);
@@ -2352,7 +2352,7 @@ mod tests {
.unwrap();
let encoded_swap = encoder
.encode_swap(swap, encoding_context)
.encode_swap(&swap, &encoding_context)
.unwrap();
let hex_swap = encode(&encoded_swap);

File diff suppressed because it is too large Load Diff

View File

@@ -3,11 +3,14 @@ pub mod encoding;
use std::str::FromStr;
use alloy::{primitives::B256, signers::local::PrivateKeySigner};
use alloy::{
primitives::{B256, U256},
signers::local::PrivateKeySigner,
};
use tycho_common::{models::Chain as TychoCommonChain, Bytes};
use tycho_execution::encoding::{
evm::encoder_builders::TychoRouterEncoderBuilder,
models::{Chain, UserTransferType},
models::{BebopOrderType, Chain, UserTransferType},
tycho_encoder::TychoEncoder,
};
@@ -43,6 +46,10 @@ pub fn pepe() -> Bytes {
Bytes::from_str("0x6982508145454Ce325dDbE47a25d4ec3d2311933").unwrap()
}
pub fn ondo() -> Bytes {
Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap()
}
pub fn get_signer() -> PrivateKeySigner {
// Set up a mock private key for signing (Alice's pk in our contract tests)
let private_key =
@@ -61,3 +68,91 @@ pub fn get_tycho_router_encoder(user_transfer_type: UserTransferType) -> Box<dyn
.build()
.expect("Failed to build encoder")
}
/// Builds Bebop user_data with support for single or multiple signatures
///
/// # Arguments
/// * `order_type` - The type of Bebop order (Single or Aggregate)
/// * `filled_taker_amount` - Amount to fill (0 means fill entire order)
/// * `quote_data` - The ABI-encoded order data
/// * `signatures` - Vector of (signature_bytes, signature_type) tuples
/// - For Single orders: expects exactly 1 signature
/// - For Aggregate orders: expects 1 or more signatures (one per maker)
pub fn build_bebop_user_data(
order_type: BebopOrderType,
filled_taker_amount: U256,
quote_data: &[u8],
signatures: Vec<(Vec<u8>, u8)>, // (signature, signature_type)
) -> Bytes {
// ABI encode MakerSignature[] array
// Format: offset_to_array | array_length | [offset_to_struct_i]... | [struct_i_data]...
let mut encoded_maker_sigs = Vec::new();
// Calculate total size needed
let array_offset = 32; // offset to array start
let array_length_size = 32;
let struct_offsets_size = 32 * signatures.len();
let _header_size = array_length_size + struct_offsets_size;
// Build each struct's data and calculate offsets
let mut struct_data = Vec::new();
let mut struct_offsets = Vec::new();
// Offsets are relative to the start of array data, not the absolute position
// Array data starts after array length, so first offset is after all offset values
let mut current_offset = struct_offsets_size; // Just the space for offsets, not including array length
for (signature, signature_type) in &signatures {
struct_offsets.push(current_offset);
// Each struct contains:
// - offset to signatureBytes (32 bytes) - always 0x40 (64)
// - flags (32 bytes)
// - signatureBytes length (32 bytes)
// - signatureBytes data (padded to 32 bytes)
let mut struct_bytes = Vec::new();
// Offset to signatureBytes within this struct
struct_bytes.extend_from_slice(&U256::from(64).to_be_bytes::<32>());
// Flags (contains signature type) - AFTER the offset, not before!
let flags = U256::from(*signature_type);
struct_bytes.extend_from_slice(&flags.to_be_bytes::<32>());
// SignatureBytes length
struct_bytes.extend_from_slice(&U256::from(signature.len()).to_be_bytes::<32>());
// SignatureBytes data (padded to 32 byte boundary)
struct_bytes.extend_from_slice(signature);
let padding = (32 - (signature.len() % 32)) % 32;
struct_bytes.extend_from_slice(&vec![0u8; padding]);
current_offset += struct_bytes.len();
struct_data.push(struct_bytes);
}
// Build the complete ABI encoded array
// Offset to array (always 0x20 for a single parameter)
encoded_maker_sigs.extend_from_slice(&U256::from(array_offset).to_be_bytes::<32>());
// Array length
encoded_maker_sigs.extend_from_slice(&U256::from(signatures.len()).to_be_bytes::<32>());
// Struct offsets (relative to start of array data)
for offset in struct_offsets {
encoded_maker_sigs.extend_from_slice(&U256::from(offset).to_be_bytes::<32>());
}
// Struct data
for data in struct_data {
encoded_maker_sigs.extend_from_slice(&data);
}
// Build complete user_data
let mut user_data = Vec::new();
user_data.push(order_type as u8);
user_data.extend_from_slice(&filled_taker_amount.to_be_bytes::<32>());
user_data.extend_from_slice(&(quote_data.len() as u32).to_be_bytes());
user_data.extend_from_slice(quote_data);
user_data.extend_from_slice(&encoded_maker_sigs);
Bytes::from(user_data)
}

View File

@@ -1,15 +1,20 @@
use std::{collections::HashMap, str::FromStr};
use alloy::hex::encode;
use alloy::{
hex::encode,
primitives::{Address, U256},
sol_types::SolValue,
};
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},
models::{BebopOrderType, Solution, Swap, UserTransferType},
};
use crate::common::{
encoding::encode_tycho_router_call, eth, eth_chain, get_signer, get_tycho_router_encoder, weth,
build_bebop_user_data, encoding::encode_tycho_router_call, eth, eth_chain, get_signer,
get_tycho_router_encoder, ondo, usdc, weth,
};
mod common;
@@ -578,3 +583,134 @@ 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)──> ONDO
let weth = weth();
let usdc = usdc();
let ondo = ondo();
// First swap: WETH -> USDC via UniswapV3
let swap_weth_usdc = Swap {
component: 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()
},
token_in: weth.clone(),
token_out: usdc.clone(),
split: 0f64,
user_data: None,
};
// Second swap: USDC -> ONDO via Bebop RFQ using real order data
// Using the same real order from the mainnet transaction at block 22667985
let expiry = 1749483840u64; // Real expiry from the order
let taker_address = Address::from_str("0xc5564C13A157E6240659fb81882A28091add8670").unwrap(); // Real taker
let maker_address = Address::from_str("0xCe79b081c0c924cb67848723ed3057234d10FC6b").unwrap(); // Real maker
let maker_nonce = 1749483765992417u64; // Real nonce
let taker_token = Address::from_str(&usdc.to_string()).unwrap();
let maker_token = Address::from_str(&ondo.to_string()).unwrap();
// Using the real order amounts
let taker_amount = U256::from_str("200000000").unwrap(); // 200 USDC (6 decimals)
let maker_amount = U256::from_str("237212396774431060000").unwrap(); // 237.21 ONDO (18 decimals)
let receiver = Address::from_str("0xc5564C13A157E6240659fb81882A28091add8670").unwrap(); // Real receiver
let packed_commands = U256::ZERO;
let flags = U256::from_str(
"51915842898789398998206002334703507894664330885127600393944965515693155942400",
)
.unwrap(); // Real flags
// Encode using standard ABI encoding (not packed)
let quote_data = (
expiry,
taker_address,
maker_address,
maker_nonce,
taker_token,
maker_token,
taker_amount,
maker_amount,
receiver,
packed_commands,
flags,
)
.abi_encode();
// Real signature from the order
let signature = hex::decode("eb5419631614978da217532a40f02a8f2ece37d8cfb94aaa602baabbdefb56b474f4c2048a0f56502caff4ea7411d99eed6027cd67dc1088aaf4181dcb0df7051c").unwrap();
// Build user_data with the quote and signature
let user_data = build_bebop_user_data(
BebopOrderType::Single,
U256::from(0), // 0 means fill entire order
&quote_data,
vec![(signature, 0)], // ETH_SIGN signature type (0)
);
let bebop_component = ProtocolComponent {
id: String::from("bebop-rfq"),
protocol_system: String::from("rfq:bebop"),
static_attributes: HashMap::new(), // No static attributes needed
..Default::default()
};
let swap_usdc_ondo = Swap {
component: bebop_component,
token_in: usdc.clone(),
token_out: ondo.clone(),
split: 0f64,
user_data: Some(user_data),
};
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let solution = Solution {
exact_out: false,
given_token: weth,
// Use ~0.099 WETH to get approximately 200 USDC from UniswapV3
// This should leave only dust amount in the router after Bebop consumes 200
// USDC
given_amount: BigUint::from_str("99000000000000000").unwrap(), // 0.099 WETH
checked_token: ondo,
checked_amount: BigUint::from_str("237212396774431060000").unwrap(), /* Expected ONDO from Bebop order */
sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
receiver: Bytes::from_str("0xc5564C13A157E6240659fb81882A28091add8670").unwrap(), /* Using the real order receiver */
swaps: vec![swap_weth_usdc, swap_usdc_ondo],
..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());
}

View File

@@ -1,18 +1,24 @@
mod common;
use std::{collections::HashMap, str::FromStr};
use alloy::hex::encode;
use alloy::{
hex,
hex::encode,
primitives::{Address, U256},
sol_types::SolValue,
};
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},
models::{BebopOrderType, Solution, Swap, UserTransferType},
};
use crate::common::{
encoding::encode_tycho_router_call, eth, eth_chain, get_signer, get_tycho_router_encoder, pepe,
usdc, weth,
build_bebop_user_data, encoding::encode_tycho_router_call, eth, eth_chain, get_signer,
get_tycho_router_encoder, ondo, pepe, usdc, weth,
};
#[test]
fn test_single_encoding_strategy_ekubo() {
// ETH ──(EKUBO)──> USDC
@@ -572,3 +578,233 @@ fn test_single_encoding_strategy_balancer_v3() {
let hex_calldata = encode(&calldata);
write_calldata_to_file("test_single_encoding_strategy_balancer_v3", hex_calldata.as_str());
}
#[test]
fn test_single_encoding_strategy_bebop() {
// Use the same mainnet data from Solidity tests
// Transaction: https://etherscan.io/tx/0x6279bc970273b6e526e86d9b69133c2ca1277e697ba25375f5e6fc4df50c0c94
let token_in = usdc();
let token_out = ondo();
let amount_in = BigUint::from_str("200000000").unwrap(); // 200 USDC
let amount_out = BigUint::from_str("237212396774431060000").unwrap(); // 237.21 ONDO
// Create the exact same order from mainnet
let expiry = 1749483840u64;
let taker_address = Address::from_str("0xc5564C13A157E6240659fb81882A28091add8670").unwrap(); // Order receiver from mainnet
let maker_address = Address::from_str("0xCe79b081c0c924cb67848723ed3057234d10FC6b").unwrap();
let maker_nonce = 1749483765992417u64;
let taker_token = Address::from_str(&token_in.to_string()).unwrap();
let maker_token = Address::from_str(&token_out.to_string()).unwrap();
let taker_amount = U256::from_str(&amount_in.to_string()).unwrap();
let maker_amount = U256::from_str(&amount_out.to_string()).unwrap();
let receiver = taker_address; // Same as taker_address in this order
let packed_commands = U256::ZERO;
let flags = U256::from_str(
"51915842898789398998206002334703507894664330885127600393944965515693155942400",
)
.unwrap();
// Encode using standard ABI encoding (not packed)
let quote_data = (
expiry,
taker_address,
maker_address,
maker_nonce,
taker_token,
maker_token,
taker_amount,
maker_amount,
receiver,
packed_commands,
flags,
)
.abi_encode();
// Real signature from mainnet
let signature = hex::decode("eb5419631614978da217532a40f02a8f2ece37d8cfb94aaa602baabbdefb56b474f4c2048a0f56502caff4ea7411d99eed6027cd67dc1088aaf4181dcb0df7051c").unwrap();
// Build user_data with the quote and signature
let user_data = build_bebop_user_data(
BebopOrderType::Single,
U256::ZERO, // 0 means fill entire order
&quote_data,
vec![(signature, 0)], // ETH_SIGN signature type
);
let bebop_component = ProtocolComponent {
id: String::from("bebop-rfq"),
protocol_system: String::from("rfq:bebop"),
static_attributes: HashMap::new(), // No static attributes needed
..Default::default()
};
let swap = Swap {
component: bebop_component,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: Some(user_data),
};
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let taker_address_bytes = Bytes::from_str(&taker_address.to_string()).unwrap();
let solution = Solution {
exact_out: false,
given_token: token_in,
given_amount: amount_in,
checked_token: token_out,
checked_amount: amount_out, // Expected output amount
// Use the order's taker address as sender and receiver
sender: taker_address_bytes.clone(),
receiver: taker_address_bytes,
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 = hex::encode(&calldata);
write_calldata_to_file("test_single_encoding_strategy_bebop", hex_calldata.as_str());
}
#[test]
fn test_single_encoding_strategy_bebop_aggregate() {
// Use real mainnet aggregate order data from CLAUDE.md
// Transaction: https://etherscan.io/tx/0xec88410136c287280da87d0a37c1cb745f320406ca3ae55c678dec11996c1b1c
// For testing, use WETH directly to avoid delegatecall + native ETH complexities
let token_in = eth();
let token_out = usdc();
let amount_in = BigUint::from_str("9850000000000000").unwrap(); // 0.00985 WETH
let amount_out = BigUint::from_str("17969561").unwrap(); // 17.969561 USDC
// Create the exact aggregate order from mainnet
let expiry = 1746367285u64;
let taker_address = Address::from_str("0x7078B12Ca5B294d95e9aC16D90B7D38238d8F4E6").unwrap();
let receiver = Address::from_str("0x7078B12Ca5B294d95e9aC16D90B7D38238d8F4E6").unwrap();
// Set up makers
let maker_addresses = vec![
Address::from_str("0x67336Cec42645F55059EfF241Cb02eA5cC52fF86").unwrap(),
Address::from_str("0xBF19CbF0256f19f39A016a86Ff3551ecC6f2aAFE").unwrap(),
];
let maker_nonces = vec![U256::from(1746367197308u64), U256::from(15460096u64)];
// 2D arrays for tokens
// We use WETH as a taker token even when handling native ETH
let taker_tokens = vec![vec![Address::from_slice(&weth())], vec![Address::from_slice(&weth())]];
let maker_tokens =
vec![vec![Address::from_slice(&token_out)], vec![Address::from_slice(&token_out)]];
// 2D arrays for amounts
let taker_amounts = vec![
vec![U256::from_str("5812106401997138").unwrap()],
vec![U256::from_str("4037893598002862").unwrap()],
];
let maker_amounts =
vec![vec![U256::from_str("10607211").unwrap()], vec![U256::from_str("7362350").unwrap()]];
// Commands and flags from the real transaction
let commands = hex!("00040004").to_vec();
let flags = U256::from_str(
"95769172144825922628485191511070792431742484643425438763224908097896054784000",
)
.unwrap();
// Encode Aggregate order - must match IBebopSettlement.Aggregate struct exactly
let quote_data = (
U256::from(expiry), // expiry as U256
taker_address,
maker_addresses,
maker_nonces, // Array of maker nonces
taker_tokens, // 2D array
maker_tokens,
taker_amounts, // 2D array
maker_amounts,
receiver,
commands,
flags,
)
.abi_encode();
// Use real signatures from the mainnet transaction
let sig1 = hex::decode("d5abb425f9bac1f44d48705f41a8ab9cae207517be8553d2c03b06a88995f2f351ab8ce7627a87048178d539dd64fd2380245531a0c8e43fdc614652b1f32fc71c").unwrap();
let sig2 = hex::decode("f38c698e48a3eac48f184bc324fef0b135ee13705ab38cc0bbf5a792f21002f051e445b9e7d57cf24c35e17629ea35b3263591c4abf8ca87ffa44b41301b89c41b").unwrap();
// Build user_data with ETH_SIGN flag (0) for both signatures
let signatures = vec![
(sig1, 0u8), // ETH_SIGN for maker 1
(sig2, 0u8), // ETH_SIGN for maker 2
];
let user_data = build_bebop_user_data(
BebopOrderType::Aggregate,
U256::from(0), // 0 means fill entire aggregate order
&quote_data,
signatures,
);
let bebop_component = ProtocolComponent {
id: String::from("bebop-rfq"),
protocol_system: String::from("rfq:bebop"),
static_attributes: HashMap::new(),
..Default::default()
};
let swap = Swap {
component: bebop_component,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
user_data: Some(user_data),
};
// Use TransferFrom for WETH token transfer
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let solution = Solution {
exact_out: false,
given_token: token_in.clone(),
given_amount: amount_in,
checked_token: token_out,
checked_amount: amount_out,
// Use ALICE as sender but order receiver as receiver
sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), /* ALICE */
receiver: Bytes::from_str("0x7078B12Ca5B294d95e9aC16D90B7D38238d8F4E6").unwrap(), /* Order receiver */
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::None,
&eth(),
None,
)
.unwrap()
.data;
let hex_calldata = hex::encode(&calldata);
write_calldata_to_file("test_single_encoding_strategy_bebop_aggregate", hex_calldata.as_str());
}