Files
tycho-execution/foundry/test/protocols/Bebop.t.sol
Diana Carvalho 01ab5d22b1 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
2025-06-24 10:42:09 +01:00

1188 lines
45 KiB
Solidity

// SPDX-License-Identifier: BUSL-1.1
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 {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Permit2TestHelper} from "../Permit2TestHelper.sol";
import {SafeERC20} from
"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
using SafeERC20 for IERC20;
BebopExecutorHarness bebopExecutor;
IERC20 WETH = IERC20(WETH_ADDR);
IERC20 USDC = IERC20(USDC_ADDR);
IERC20 DAI = IERC20(DAI_ADDR);
IERC20 WBTC = IERC20(WBTC_ADDR);
IERC20 ONDO = IERC20(ONDO_ADDR);
IERC20 USDT = IERC20(USDT_ADDR);
// Test data structures for mainnet fork tests
struct SingleOrderTestData {
uint256 forkBlock;
IBebopSettlement.Single order;
bytes signature;
uint256 amountIn;
uint256 filledTakerAmount; // 0 means fill entire order
uint256 expectedAmountOut;
address sender;
address receiver;
}
struct AggregateOrderTestData {
uint256 forkBlock;
IBebopSettlement.Aggregate order;
bytes[] signatures; // Multiple signatures for multiple makers
uint256[] amountsIn;
uint256[] filledTakerAmounts; // 0 in array means fill entire amount for that token
uint256[] expectedAmountsOut;
address sender;
address receiver;
}
function setUp() public {
// Fork will be created in individual tests to allow different fork blocks
}
function testDecodeParams() public {
// Fork to ensure consistent setup
vm.createSelectFork(vm.rpcUrl("mainnet"), 22667985);
// Deploy Bebop executor harness with real settlement contract
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
bytes memory quoteData = hex"1234567890abcdef";
bytes memory signature = hex"aabbccdd";
// Create ABI-encoded MakerSignature array
IBebopSettlement.MakerSignature[] memory signatures =
new IBebopSettlement.MakerSignature[](1);
signatures[0] = IBebopSettlement.MakerSignature({
signatureBytes: signature,
flags: uint256(1) // EIP712 signature type
});
bytes memory makerSignaturesData = abi.encode(signatures);
uint256 filledTakerAmount = 1e18; // 1 WETH
bytes memory params = abi.encodePacked(
WETH_ADDR,
USDC_ADDR,
uint8(RestrictTransferFrom.TransferType.Transfer),
uint8(0), // OrderType.Single
filledTakerAmount,
uint32(quoteData.length),
quoteData,
uint32(makerSignaturesData.length),
makerSignaturesData,
uint8(1) // approvalNeeded: true
);
(
address tokenIn,
address tokenOut,
RestrictTransferFrom.TransferType transferType,
BebopExecutor.OrderType orderType,
uint256 decodedFilledTakerAmount,
bytes memory decodedQuoteData,
bytes memory decodedMakerSignaturesData,
bool decodedApprovalNeeded
) = bebopExecutor.decodeParams(params);
assertEq(tokenIn, WETH_ADDR);
assertEq(tokenOut, USDC_ADDR);
assertEq(
uint8(transferType),
uint8(RestrictTransferFrom.TransferType.Transfer)
);
assertEq(uint8(orderType), uint8(BebopExecutor.OrderType.Single));
assertEq(decodedFilledTakerAmount, filledTakerAmount);
assertEq(keccak256(decodedQuoteData), keccak256(quoteData));
assertEq(
keccak256(decodedMakerSignaturesData),
keccak256(makerSignaturesData)
);
assertTrue(decodedApprovalNeeded); // Approval needed should be true
// Also verify we can decode the signatures back
IBebopSettlement.MakerSignature[] memory decodedSignatures = abi.decode(
decodedMakerSignaturesData, (IBebopSettlement.MakerSignature[])
);
assertEq(decodedSignatures.length, 1);
assertEq(
keccak256(decodedSignatures[0].signatureBytes), keccak256(signature)
);
assertEq(decodedSignatures[0].flags, 1); // EIP712
}
// Single Order Tests
function testSingleOrder() public {
// Fork at the right block first
vm.createSelectFork(vm.rpcUrl("mainnet"), 22667985);
// Deploy Bebop executor harness that uses vm.prank
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Create test data from real mainnet transaction
// https://etherscan.io/tx/0x6279bc970273b6e526e86d9b69133c2ca1277e697ba25375f5e6fc4df50c0c94
address originalTakerAddress =
0xc5564C13A157E6240659fb81882A28091add8670;
// Using the original order data with the real settlement contract
SingleOrderTestData memory testData = SingleOrderTestData({
forkBlock: 22667985,
order: IBebopSettlement.Single({
expiry: 1749483840,
taker_address: originalTakerAddress, // Original taker address from the real order
maker_address: 0xCe79b081c0c924cb67848723ed3057234d10FC6b,
maker_nonce: 1749483765992417,
taker_token: USDC_ADDR,
maker_token: ONDO_ADDR,
taker_amount: 200000000,
maker_amount: 237212396774431060000,
receiver: originalTakerAddress,
packed_commands: 0,
flags: 51915842898789398998206002334703507894664330885127600393944965515693155942400
}),
signature: hex"eb5419631614978da217532a40f02a8f2ece37d8cfb94aaa602baabbdefb56b474f4c2048a0f56502caff4ea7411d99eed6027cd67dc1088aaf4181dcb0df7051c",
amountIn: 200000000,
filledTakerAmount: 0,
expectedAmountOut: 237212396774431060000,
sender: originalTakerAddress,
receiver: originalTakerAddress
});
// Setup: fund the original taker and have them approve the test contract (acting as router)
deal(USDC_ADDR, originalTakerAddress, testData.amountIn);
// Also fund the maker with ONDO tokens and have them approve the settlement
deal(
ONDO_ADDR, testData.order.maker_address, testData.order.maker_amount
);
vm.prank(testData.order.maker_address);
ONDO.approve(BEBOP_SETTLEMENT, testData.order.maker_amount);
// Original taker approves the test contract (router) to spend their USDC
vm.prank(originalTakerAddress);
USDC.approve(address(this), testData.amountIn);
// Test contract (router) pulls tokens from original taker and sends to executor
USDC.transferFrom(
originalTakerAddress, address(bebopExecutor), testData.amountIn
);
// Record initial balances
uint256 ondoBefore = ONDO.balanceOf(originalTakerAddress);
// Execute the swap (executor already has the tokens)
bytes memory quoteData = abi.encode(testData.order);
IBebopSettlement.MakerSignature[] memory signatures =
new IBebopSettlement.MakerSignature[](1);
signatures[0] = IBebopSettlement.MakerSignature({
signatureBytes: testData.signature,
flags: uint256(0) // ETH_SIGN
});
bytes memory makerSignaturesData = abi.encode(signatures);
bytes memory params = abi.encodePacked(
USDC_ADDR,
ONDO_ADDR,
uint8(RestrictTransferFrom.TransferType.Transfer),
uint8(BebopExecutor.OrderType.Single),
testData.filledTakerAmount,
uint32(quoteData.length),
quoteData,
uint32(makerSignaturesData.length),
makerSignaturesData,
uint8(1) // approvalNeeded: true
);
uint256 amountOut = bebopExecutor.swap(testData.amountIn, params);
// Verify results
assertEq(amountOut, testData.expectedAmountOut, "Incorrect amount out");
assertEq(
ONDO.balanceOf(originalTakerAddress) - ondoBefore,
testData.expectedAmountOut,
"ONDO balance mismatch"
);
assertEq(
USDC.balanceOf(address(bebopExecutor)), 0, "USDC left in executor"
);
assertEq(
ONDO.balanceOf(address(bebopExecutor)), 0, "ONDO left in executor"
);
}
function testSingleOrder_PartialFill() public {
// Fork at the right block first
vm.createSelectFork(vm.rpcUrl("mainnet"), 22667985);
// Deploy Bebop executor harness that uses vm.prank
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Test partial fill - only fill half of the order
address originalTakerAddress =
0xc5564C13A157E6240659fb81882A28091add8670;
// Using the same order but only filling half
SingleOrderTestData memory testData = SingleOrderTestData({
forkBlock: 22667985,
order: IBebopSettlement.Single({
expiry: 1749483840,
taker_address: originalTakerAddress,
maker_address: 0xCe79b081c0c924cb67848723ed3057234d10FC6b,
maker_nonce: 1749483765992417,
taker_token: USDC_ADDR,
maker_token: ONDO_ADDR,
taker_amount: 200000000, // 200 USDC total order
maker_amount: 237212396774431060000, // Total ONDO for full order
receiver: originalTakerAddress,
packed_commands: 0,
flags: 51915842898789398998206002334703507894664330885127600393944965515693155942400
}),
signature: hex"eb5419631614978da217532a40f02a8f2ece37d8cfb94aaa602baabbdefb56b474f4c2048a0f56502caff4ea7411d99eed6027cd67dc1088aaf4181dcb0df7051c",
amountIn: 100000000, // Only provide 100 USDC (half)
filledTakerAmount: 100000000, // Explicitly fill only 100 USDC
expectedAmountOut: 118606198387215530000, // Expected proportional ONDO output (half of 237.21)
sender: originalTakerAddress,
receiver: originalTakerAddress
});
// Setup: fund the original taker with partial amount
deal(USDC_ADDR, originalTakerAddress, testData.amountIn);
// Fund the maker with FULL amount (they need enough for any partial fill)
deal(
ONDO_ADDR, testData.order.maker_address, testData.order.maker_amount
);
vm.prank(testData.order.maker_address);
ONDO.approve(BEBOP_SETTLEMENT, testData.order.maker_amount);
// Original taker approves the test contract (router) to spend their USDC
vm.prank(originalTakerAddress);
USDC.approve(address(this), testData.amountIn);
// Test contract (router) pulls tokens from original taker and sends to executor
USDC.transferFrom(
originalTakerAddress, address(bebopExecutor), testData.amountIn
);
// Record initial balances
uint256 ondoBefore = ONDO.balanceOf(originalTakerAddress);
// Execute the partial swap (executor already has the tokens)
bytes memory quoteData = abi.encode(testData.order);
IBebopSettlement.MakerSignature[] memory signatures =
new IBebopSettlement.MakerSignature[](1);
signatures[0] = IBebopSettlement.MakerSignature({
signatureBytes: testData.signature,
flags: uint256(0) // ETH_SIGN
});
bytes memory makerSignaturesData = abi.encode(signatures);
bytes memory params = abi.encodePacked(
USDC_ADDR,
ONDO_ADDR,
uint8(RestrictTransferFrom.TransferType.Transfer),
uint8(BebopExecutor.OrderType.Single),
testData.filledTakerAmount, // Partial fill amount
uint32(quoteData.length),
quoteData,
uint32(makerSignaturesData.length),
makerSignaturesData,
uint8(1) // approvalNeeded: true
);
uint256 amountOut = bebopExecutor.swap(testData.amountIn, params);
// Verify partial fill results
assertEq(
amountOut,
testData.expectedAmountOut,
"Incorrect partial amount out"
);
assertEq(
ONDO.balanceOf(originalTakerAddress) - ondoBefore,
testData.expectedAmountOut,
"ONDO balance mismatch"
);
// Verify no tokens left in executor
assertEq(
USDC.balanceOf(address(bebopExecutor)), 0, "USDC left in executor"
);
assertEq(
ONDO.balanceOf(address(bebopExecutor)), 0, "ONDO left in executor"
);
}
// Aggregate Order Tests
function testAggregateOrder() public {
// Fork at a suitable block for aggregate order testing
vm.createSelectFork(vm.rpcUrl("mainnet"), 21370890);
// Deploy Bebop executor harness that uses vm.prank
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Store the initial ETH balance (dust from forked state)
uint256 initialExecutorBalance = address(bebopExecutor).balance;
// Create test data from real mainnet transaction
// https://etherscan.io/tx/0xec88410136c287280da87d0a37c1cb745f320406ca3ae55c678dec11996c1b1c
address originalTakerAddress =
0x7078B12Ca5B294d95e9aC16D90B7D38238d8F4E6;
// Create the 2D arrays for tokens and amounts
address[][] memory takerTokens = new address[][](2);
takerTokens[0] = new address[](1);
takerTokens[0][0] = WETH_ADDR; // WETH for first maker
takerTokens[1] = new address[](1);
takerTokens[1][0] = WETH_ADDR; // WETH for second maker
address[][] memory makerTokens = new address[][](2);
makerTokens[0] = new address[](1);
makerTokens[0][0] = USDC_ADDR; // USDC from first maker
makerTokens[1] = new address[](1);
makerTokens[1][0] = USDC_ADDR; // USDC from second maker
uint256[][] memory takerAmounts = new uint256[][](2);
takerAmounts[0] = new uint256[](1);
takerAmounts[0][0] = 5812106401997138; // First maker takes ~0.0058 ETH
takerAmounts[1] = new uint256[](1);
takerAmounts[1][0] = 4037893598002862; // Second maker takes ~0.0040 ETH
uint256[][] memory makerAmounts = new uint256[][](2);
makerAmounts[0] = new uint256[](1);
makerAmounts[0][0] = 10607211; // First maker gives ~10.6 USDC
makerAmounts[1] = new uint256[](1);
makerAmounts[1][0] = 7362350; // Second maker gives ~7.36 USDC
// Create makers array
address[] memory makerAddresses = new address[](2);
makerAddresses[0] = 0x67336Cec42645F55059EfF241Cb02eA5cC52fF86;
makerAddresses[1] = 0xBF19CbF0256f19f39A016a86Ff3551ecC6f2aAFE;
// Create maker nonces array
uint256[] memory makerNonces = new uint256[](2);
makerNonces[0] = 1746367197308;
makerNonces[1] = 15460096;
// Create the aggregate order
IBebopSettlement.Aggregate memory order = IBebopSettlement.Aggregate({
expiry: 1746367285,
taker_address: originalTakerAddress,
maker_addresses: makerAddresses,
maker_nonces: makerNonces,
taker_tokens: takerTokens,
maker_tokens: makerTokens,
taker_amounts: takerAmounts,
maker_amounts: makerAmounts,
receiver: originalTakerAddress,
commands: hex"00040004",
flags: 95769172144825922628485191511070792431742484643425438763224908097896054784000
});
// Total amounts
uint256 totalTakerAmount = takerAmounts[0][0] + takerAmounts[1][0]; // 0.00985 ETH total
uint256 totalMakerAmount = makerAmounts[0][0] + makerAmounts[1][0]; // 17.969561 USDC total
// Fund makers with USDC and approve settlement
deal(USDC_ADDR, makerAddresses[0], makerAmounts[0][0]);
deal(USDC_ADDR, makerAddresses[1], makerAmounts[1][0]);
vm.prank(makerAddresses[0]);
USDC.approve(BEBOP_SETTLEMENT, makerAmounts[0][0]);
vm.prank(makerAddresses[1]);
USDC.approve(BEBOP_SETTLEMENT, makerAmounts[1][0]);
// ETH will be sent directly with the swap call
// Fund the test contract with ETH to send with the swap
vm.deal(address(this), totalTakerAmount);
// Record initial balances
uint256 usdcBefore = USDC.balanceOf(originalTakerAddress);
// Create maker signatures
IBebopSettlement.MakerSignature[] memory signatures =
new IBebopSettlement.MakerSignature[](2);
signatures[0] = IBebopSettlement.MakerSignature({
signatureBytes: hex"d5abb425f9bac1f44d48705f41a8ab9cae207517be8553d2c03b06a88995f2f351ab8ce7627a87048178d539dd64fd2380245531a0c8e43fdc614652b1f32fc71c",
flags: 0 // ETH_SIGN
});
signatures[1] = IBebopSettlement.MakerSignature({
signatureBytes: hex"f38c698e48a3eac48f184bc324fef0b135ee13705ab38cc0bbf5a792f21002f051e445b9e7d57cf24c35e17629ea35b3263591c4abf8ca87ffa44b41301b89c41b",
flags: 0 // ETH_SIGN
});
// Encode order and signatures
bytes memory quoteData = abi.encode(order);
bytes memory makerSignaturesData = abi.encode(signatures);
// Create packed params for executor with native ETH as input
bytes memory params = abi.encodePacked(
address(0), // tokenIn: native ETH
USDC_ADDR, // tokenOut
uint8(RestrictTransferFrom.TransferType.Transfer),
uint8(BebopExecutor.OrderType.Aggregate),
uint256(0), // filledTakerAmount: 0 for full fill
uint32(quoteData.length),
quoteData,
uint32(makerSignaturesData.length),
makerSignaturesData,
uint8(0) // approvalNeeded: false for native ETH
);
// Execute the aggregate swap with ETH value
uint256 amountOut = bebopExecutor.swap{value: totalTakerAmount}(
totalTakerAmount, params
);
// Verify results
assertEq(amountOut, totalMakerAmount, "Incorrect amount out");
assertEq(
USDC.balanceOf(originalTakerAddress) - usdcBefore,
totalMakerAmount,
"USDC balance mismatch"
);
assertEq(
USDC.balanceOf(address(bebopExecutor)), 0, "USDC left in executor"
);
assertEq(
address(bebopExecutor).balance,
initialExecutorBalance,
"ETH left in executor should match initial dust amount"
);
}
function testAggregateOrder_PartialFill() public {
// Fork at a suitable block for aggregate order testing
vm.createSelectFork(vm.rpcUrl("mainnet"), 21370890);
// Deploy Bebop executor harness that uses vm.prank
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Store the initial ETH balance (dust from forked state)
uint256 initialExecutorBalance = address(bebopExecutor).balance;
// Same aggregate order as before, but with partial fill
address originalTakerAddress =
0x7078B12Ca5B294d95e9aC16D90B7D38238d8F4E6;
// Create the 2D arrays for tokens and amounts
address[][] memory takerTokens = new address[][](2);
takerTokens[0] = new address[](1);
takerTokens[0][0] = WETH_ADDR;
takerTokens[1] = new address[](1);
takerTokens[1][0] = WETH_ADDR;
address[][] memory makerTokens = new address[][](2);
makerTokens[0] = new address[](1);
makerTokens[0][0] = USDC_ADDR;
makerTokens[1] = new address[](1);
makerTokens[1][0] = USDC_ADDR;
uint256[][] memory takerAmounts = new uint256[][](2);
takerAmounts[0] = new uint256[](1);
takerAmounts[0][0] = 5812106401997138;
takerAmounts[1] = new uint256[](1);
takerAmounts[1][0] = 4037893598002862;
uint256[][] memory makerAmounts = new uint256[][](2);
makerAmounts[0] = new uint256[](1);
makerAmounts[0][0] = 10607211;
makerAmounts[1] = new uint256[](1);
makerAmounts[1][0] = 7362350;
// Create makers array
address[] memory makerAddresses = new address[](2);
makerAddresses[0] = 0x67336Cec42645F55059EfF241Cb02eA5cC52fF86;
makerAddresses[1] = 0xBF19CbF0256f19f39A016a86Ff3551ecC6f2aAFE;
// Create maker nonces array
uint256[] memory makerNonces = new uint256[](2);
makerNonces[0] = 1746367197308;
makerNonces[1] = 15460096;
// Create the aggregate order
IBebopSettlement.Aggregate memory order = IBebopSettlement.Aggregate({
expiry: 1746367285,
taker_address: originalTakerAddress,
maker_addresses: makerAddresses,
maker_nonces: makerNonces,
taker_tokens: takerTokens,
maker_tokens: makerTokens,
taker_amounts: takerAmounts,
maker_amounts: makerAmounts,
receiver: originalTakerAddress,
commands: hex"00040004",
flags: 95769172144825922628485191511070792431742484643425438763224908097896054784000
});
// Total amounts
uint256 totalTakerAmount = takerAmounts[0][0] + takerAmounts[1][0];
uint256 totalMakerAmount = makerAmounts[0][0] + makerAmounts[1][0];
// We'll do a 50% partial fill
uint256 partialFillAmount = totalTakerAmount / 2;
uint256 expectedPartialOutput = totalMakerAmount / 2;
// Fund makers with FULL amounts (they need enough for any partial fill)
deal(USDC_ADDR, makerAddresses[0], makerAmounts[0][0]);
deal(USDC_ADDR, makerAddresses[1], makerAmounts[1][0]);
vm.prank(makerAddresses[0]);
USDC.approve(BEBOP_SETTLEMENT, makerAmounts[0][0]);
vm.prank(makerAddresses[1]);
USDC.approve(BEBOP_SETTLEMENT, makerAmounts[1][0]);
// ETH will be sent directly with the swap call
// Fund the test contract with ETH to send with the swap
vm.deal(address(this), partialFillAmount);
// Record initial balances
uint256 usdcBefore = USDC.balanceOf(originalTakerAddress);
// Create maker signatures
IBebopSettlement.MakerSignature[] memory signatures =
new IBebopSettlement.MakerSignature[](2);
signatures[0] = IBebopSettlement.MakerSignature({
signatureBytes: hex"d5abb425f9bac1f44d48705f41a8ab9cae207517be8553d2c03b06a88995f2f351ab8ce7627a87048178d539dd64fd2380245531a0c8e43fdc614652b1f32fc71c",
flags: 0
});
signatures[1] = IBebopSettlement.MakerSignature({
signatureBytes: hex"f38c698e48a3eac48f184bc324fef0b135ee13705ab38cc0bbf5a792f21002f051e445b9e7d57cf24c35e17629ea35b3263591c4abf8ca87ffa44b41301b89c41b",
flags: 0
});
// Encode order and signatures
bytes memory quoteData = abi.encode(order);
bytes memory makerSignaturesData = abi.encode(signatures);
// Create packed params for executor with partial fill amount
bytes memory params = abi.encodePacked(
address(0), // tokenIn: native ETH
USDC_ADDR,
uint8(RestrictTransferFrom.TransferType.Transfer),
uint8(BebopExecutor.OrderType.Aggregate),
partialFillAmount, // Specify partial fill amount
uint32(quoteData.length),
quoteData,
uint32(makerSignaturesData.length),
makerSignaturesData,
uint8(0) // approvalNeeded: false for native ETH
);
// Execute the partial aggregate swap with ETH value
uint256 amountOut = bebopExecutor.swap{value: partialFillAmount}(
partialFillAmount, params
);
// Verify results - should be proportional to the partial fill
assertEq(
amountOut, expectedPartialOutput, "Incorrect partial amount out"
);
assertEq(
USDC.balanceOf(originalTakerAddress) - usdcBefore,
expectedPartialOutput,
"USDC balance mismatch for partial fill"
);
assertEq(
USDC.balanceOf(address(bebopExecutor)), 0, "USDC left in executor"
);
assertEq(
address(bebopExecutor).balance,
initialExecutorBalance,
"ETH left in executor should match initial dust amount"
);
}
function testInvalidDataLength() public {
// Fork to ensure consistent setup
vm.createSelectFork(vm.rpcUrl("mainnet"), 22667985);
// Deploy Bebop executor with real settlement contract
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
bytes memory quoteData = hex"1234567890abcdef";
bytes memory signature = hex"aabbccdd";
// Create ABI-encoded MakerSignature array
IBebopSettlement.MakerSignature[] memory signatures =
new IBebopSettlement.MakerSignature[](1);
signatures[0] = IBebopSettlement.MakerSignature({
signatureBytes: signature,
flags: uint256(1) // EIP712 signature type
});
bytes memory makerSignaturesData = abi.encode(signatures);
// Create params with correct length first
uint256 filledTakerAmount = 1e18;
bytes memory validParams = abi.encodePacked(
WETH_ADDR,
USDC_ADDR,
uint8(RestrictTransferFrom.TransferType.Transfer),
uint8(0), // OrderType.Single
filledTakerAmount,
uint32(quoteData.length),
quoteData,
uint32(makerSignaturesData.length),
makerSignaturesData,
uint8(1) // approvalNeeded: true
);
// Verify valid params work
bebopExecutor.decodeParams(validParams);
// Add extra bytes at the end, this should fail
bytes memory invalidParams = abi.encodePacked(validParams, hex"ff");
vm.expectRevert(BebopExecutor.BebopExecutor__InvalidDataLength.selector);
bebopExecutor.decodeParams(invalidParams);
// Try with insufficient data, should fail
bytes memory tooShortParams = abi.encodePacked(
WETH_ADDR,
USDC_ADDR,
uint8(RestrictTransferFrom.TransferType.Transfer)
);
// Missing rest of the data
vm.expectRevert(BebopExecutor.BebopExecutor__InvalidDataLength.selector);
bebopExecutor.decodeParams(tooShortParams);
}
// Integration tests
function testSwapSingleIntegration() public {
// Fork at the right block first
vm.createSelectFork(vm.rpcUrl("mainnet"), 22667985);
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Load encoded data from test_encode_bebop_single (USDC → ONDO swap)
bytes memory protocolData =
loadCallDataFromFile("test_encode_bebop_single");
// Deal 200 USDC to the executor
uint256 amountIn = 200000000; // 200 USDC
deal(USDC_ADDR, address(bebopExecutor), amountIn);
// Fund the maker with ONDO and approve settlement
address maker = 0xCe79b081c0c924cb67848723ed3057234d10FC6b;
uint256 expectedAmountOut = 237212396774431060000; // 237.21 ONDO
deal(ONDO_ADDR, maker, expectedAmountOut);
vm.prank(maker);
ONDO.approve(BEBOP_SETTLEMENT, expectedAmountOut);
// Record receiver's initial ONDO balance
address receiver = 0xc5564C13A157E6240659fb81882A28091add8670;
uint256 ondoBefore = ONDO.balanceOf(receiver);
// Execute the swap
uint256 amountOut = bebopExecutor.swap(amountIn, protocolData);
// Verify results
assertEq(amountOut, expectedAmountOut, "Incorrect amount out");
assertEq(
ONDO.balanceOf(receiver) - ondoBefore,
expectedAmountOut,
"ONDO balance mismatch"
);
assertEq(
USDC.balanceOf(address(bebopExecutor)), 0, "USDC left in executor"
);
assertEq(
ONDO.balanceOf(address(bebopExecutor)), 0, "ONDO left in executor"
);
}
function testSwapAggregateIntegration() public {
// Fork at a suitable block for aggregate order testing
vm.createSelectFork(vm.rpcUrl("mainnet"), 21370890);
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Store the initial ETH balance (dust from forked state)
uint256 initialExecutorBalance = address(bebopExecutor).balance;
// 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 encoded data from test_encode_bebop_aggregate (ETH → USDC multi-maker swap)
bytes memory protocolData =
loadCallDataFromFile("test_encode_bebop_aggregate");
// Record initial USDC balance
uint256 usdcBefore = IERC20(USDC_ADDR).balanceOf(orderTaker);
// Execute the swap with native ETH
uint256 amountOut =
bebopExecutor.swap{value: ethAmount}(ethAmount, protocolData);
// Verify results
assertEq(amountOut, expAmountOut, "Incorrect amount out");
assertEq(
IERC20(USDC_ADDR).balanceOf(orderTaker) - usdcBefore,
expAmountOut,
"USDC balance mismatch"
);
assertEq(
IERC20(USDC_ADDR).balanceOf(address(bebopExecutor)),
0,
"USDC left in executor"
);
assertEq(
address(bebopExecutor).balance,
initialExecutorBalance,
"ETH left in executor should match initial dust amount"
);
vm.stopPrank();
}
// Test exposed_getActualFilledTakerAmount function
function testGetActualFilledTakerAmount_FullFillWithZeroFilledTakerAmount()
public
{
// No need to fork, we're testing the internal function
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// When filledTakerAmount is 0 and givenAmount >= orderTakerAmount
// Should return orderTakerAmount (full fill)
uint256 givenAmount = 1000e18;
uint256 orderTakerAmount = 500e18;
uint256 filledTakerAmount = 0;
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, orderTakerAmount, filledTakerAmount
);
assertEq(
result,
orderTakerAmount,
"Should return orderTakerAmount for full fill"
);
}
function testGetActualFilledTakerAmount_PartialFillWithZeroFilledTakerAmount(
) public {
// No need to fork, we're testing the internal function
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// When filledTakerAmount is 0 and givenAmount < orderTakerAmount
// Should return givenAmount (partial fill)
uint256 givenAmount = 300e18;
uint256 orderTakerAmount = 500e18;
uint256 filledTakerAmount = 0;
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, orderTakerAmount, filledTakerAmount
);
assertEq(
result,
givenAmount,
"Should return givenAmount when less than orderTakerAmount"
);
}
function testGetActualFilledTakerAmount_ExactMatchWithZeroFilledTakerAmount(
) public {
// No need to fork, we're testing the internal function
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// When filledTakerAmount is 0 and givenAmount == orderTakerAmount
// Should return orderTakerAmount
uint256 givenAmount = 500e18;
uint256 orderTakerAmount = 500e18;
uint256 filledTakerAmount = 0;
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, orderTakerAmount, filledTakerAmount
);
assertEq(
result,
orderTakerAmount,
"Should return orderTakerAmount when equal to givenAmount"
);
}
function testGetActualFilledTakerAmount_FilledTakerAmountLessThanGivenAmount(
) public {
// No need to fork, we're testing the internal function
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// When filledTakerAmount > 0 and filledTakerAmount < givenAmount
// Should return filledTakerAmount
uint256 givenAmount = 1000e18;
uint256 orderTakerAmount = 1500e18;
uint256 filledTakerAmount = 700e18;
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, orderTakerAmount, filledTakerAmount
);
assertEq(
result,
filledTakerAmount,
"Should return filledTakerAmount when less than givenAmount"
);
}
function testGetActualFilledTakerAmount_FilledTakerAmountGreaterThanGivenAmount(
) public {
// No need to fork, we're testing the internal function
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// When filledTakerAmount > 0 and filledTakerAmount > givenAmount
// Should return givenAmount (capped)
uint256 givenAmount = 500e18;
uint256 orderTakerAmount = 1500e18;
uint256 filledTakerAmount = 700e18;
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, orderTakerAmount, filledTakerAmount
);
assertEq(
result,
givenAmount,
"Should return givenAmount when filledTakerAmount exceeds it"
);
}
function testGetActualFilledTakerAmount_FilledTakerAmountEqualsGivenAmount()
public
{
// Fork to ensure consistent setup
vm.createSelectFork(vm.rpcUrl("mainnet"), 22667985);
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// When filledTakerAmount > 0 and filledTakerAmount == givenAmount
// Should return filledTakerAmount
uint256 givenAmount = 700e18;
uint256 orderTakerAmount = 1500e18;
uint256 filledTakerAmount = 700e18;
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, orderTakerAmount, filledTakerAmount
);
assertEq(
result,
filledTakerAmount,
"Should return filledTakerAmount when equal to givenAmount"
);
}
function testGetActualFilledTakerAmount_ZeroGivenAmount() public {
// No need to fork, we're testing the internal function
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// When givenAmount is 0
// Should always return 0 regardless of other parameters
uint256 givenAmount = 0;
uint256 orderTakerAmount = 500e18;
uint256 filledTakerAmount = 0;
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, orderTakerAmount, filledTakerAmount
);
assertEq(result, 0, "Should return 0 when givenAmount is 0");
// Also test with non-zero filledTakerAmount
filledTakerAmount = 100e18;
result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, orderTakerAmount, filledTakerAmount
);
assertEq(
result,
0,
"Should return 0 when givenAmount is 0 even with filledTakerAmount"
);
}
function testGetActualFilledTakerAmount_ZeroOrderTakerAmount() public {
// No need to fork, we're testing the internal function
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// When orderTakerAmount is 0 (edge case - malformed order)
// With filledTakerAmount = 0, should return 0
uint256 givenAmount = 1000e18;
uint256 orderTakerAmount = 0;
uint256 filledTakerAmount = 0;
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, orderTakerAmount, filledTakerAmount
);
assertEq(result, 0, "Should return 0 when orderTakerAmount is 0");
}
function testGetActualFilledTakerAmount_SmallAmounts() public {
// No need to fork, we're testing the internal function
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Test with small amounts (e.g., for tokens with low decimals)
uint256 givenAmount = 100; // 100 units
uint256 orderTakerAmount = 50; // 50 units
uint256 filledTakerAmount = 0;
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, orderTakerAmount, filledTakerAmount
);
assertEq(
result, orderTakerAmount, "Should handle small amounts correctly"
);
// Test with filledTakerAmount
filledTakerAmount = 75;
result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, orderTakerAmount, filledTakerAmount
);
assertEq(
result,
filledTakerAmount,
"Should handle small filledTakerAmount correctly"
);
}
function testGetActualFilledTakerAmount_MaxUint256Values() public {
// No need to fork, we're testing the internal function
// No need to fork, we're testing the internal function
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Test with max uint256 values (edge case)
uint256 givenAmount = type(uint256).max;
uint256 orderTakerAmount = type(uint256).max - 1;
uint256 filledTakerAmount = 0;
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, orderTakerAmount, filledTakerAmount
);
assertEq(result, orderTakerAmount, "Should handle max values correctly");
// Test with max filledTakerAmount
filledTakerAmount = type(uint256).max;
givenAmount = type(uint256).max - 100;
result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, orderTakerAmount, filledTakerAmount
);
assertEq(
result,
givenAmount,
"Should cap at givenAmount even with max filledTakerAmount"
);
}
function testFuzzGetActualFilledTakerAmount(
uint256 givenAmount,
uint256 orderTakerAmount,
uint256 filledTakerAmount
) public {
// No need to fork, we're testing the internal function
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, orderTakerAmount, filledTakerAmount
);
// Verify the invariants
if (filledTakerAmount == 0) {
// When filledTakerAmount is 0, result should be min(givenAmount, orderTakerAmount)
if (givenAmount >= orderTakerAmount) {
assertEq(
result,
orderTakerAmount,
"Should return orderTakerAmount when givenAmount >= orderTakerAmount"
);
} else {
assertEq(
result,
givenAmount,
"Should return givenAmount when givenAmount < orderTakerAmount"
);
}
} else {
// When filledTakerAmount > 0, result should be min(givenAmount, filledTakerAmount)
if (filledTakerAmount > givenAmount) {
assertEq(
result,
givenAmount,
"Should return givenAmount when filledTakerAmount > givenAmount"
);
} else {
assertEq(
result,
filledTakerAmount,
"Should return filledTakerAmount when filledTakerAmount <= givenAmount"
);
}
}
// Result should never exceed givenAmount
assertLe(result, givenAmount, "Result should never exceed givenAmount");
// When filledTakerAmount is 0, result should not exceed orderTakerAmount
if (filledTakerAmount == 0) {
assertLe(
result,
orderTakerAmount,
"Result should not exceed orderTakerAmount when filledTakerAmount is 0"
);
}
}
}
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();
}
}