Files
tycho-execution/foundry/test/executors/BebopExecutor.t.sol

603 lines
23 KiB
Solidity

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "../TestUtils.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 {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
)
{
return _decodeData(data);
}
// 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
) internal virtual override returns (uint256 amountOut) {
// Decode the order from quoteData
IBebopSettlement.Single memory order =
abi.decode(quoteData, (IBebopSettlement.Single));
// Decode the MakerSignature array (should contain exactly 1 signature for Single orders)
IBebopSettlement.MakerSignature[] memory signatures =
abi.decode(makerSignaturesData, (IBebopSettlement.MakerSignature[]));
// Validate that there is exactly one maker signature
if (signatures.length != 1) {
revert BebopExecutor__InvalidInput();
}
// Get the maker signature from the first and only element of the array
IBebopSettlement.MakerSignature memory sig = signatures[0];
uint256 actualFilledTakerAmount = _getActualFilledTakerAmount(
givenAmount, order.taker_amount, filledTakerAmount
);
// Transfer tokens to executor
_transfer(address(this), transferType, tokenIn, givenAmount);
// For testing: transfer tokens from executor to taker address
// This simulates the taker having the tokens with approval
if (tokenIn != address(0)) {
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();
}
// Record balances before swap to calculate amountOut
uint256 balanceBefore = tokenOut == address(0)
? order.receiver.balance
: IERC20(tokenOut).balanceOf(order.receiver);
// Execute the swap with ETH value if needed
uint256 ethValue = tokenIn == address(0) ? actualFilledTakerAmount : 0;
// 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
// Use swapSingle - tokens are now in taker's wallet with approval
// slither-disable-next-line arbitrary-send-eth
IBebopSettlement(bebopSettlement).swapSingle{value: ethValue}(
order, sig, actualFilledTakerAmount
);
// Restore original timestamp
vm.warp(currentTimestamp);
vm.stopPrank();
// Calculate actual amount received
uint256 balanceAfter = tokenOut == address(0)
? order.receiver.balance
: IERC20(tokenOut).balanceOf(order.receiver);
amountOut = balanceAfter - balanceBefore;
}
// 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));
// // Decode the MakerSignature array (can contain multiple signatures for Aggregate orders)
// IBebopSettlement.MakerSignature[] memory signatures =
// abi.decode(makerSignaturesData, (IBebopSettlement.MakerSignature[]));
// // Aggregate orders should have at least one signature
// if (signatures.length == 0) {
// revert BebopExecutor__InvalidInput();
// }
// // For aggregate orders, calculate total taker amount across all makers
// uint256 totalTakerAmount = 0;
// for (uint256 i = 0; i < order.taker_amounts.length; i++) {
// totalTakerAmount += order.taker_amounts[i][0];
// }
// uint256 actualFilledTakerAmount = _getActualFilledTakerAmount(
// givenAmount, totalTakerAmount, filledTakerAmount
// );
// // Transfer tokens to executor
// _transfer(address(this), transferType, tokenIn, givenAmount);
// // For testing: transfer tokens from executor to taker address
// // This simulates the taker having the tokens with approval
// if (tokenIn != address(0)) {
// 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();
// }
// // Record balances before swap to calculate amountOut
// uint256 balanceBefore = tokenOut == address(0)
// ? order.receiver.balance
// : IERC20(tokenOut).balanceOf(order.receiver);
// // Execute the swap with ETH value if needed
// uint256 ethValue = tokenIn == address(0) ? actualFilledTakerAmount : 0;
// // 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 swap - tokens are now in taker's wallet with approval
// // slither-disable-next-line arbitrary-send-eth
// IBebopSettlement(bebopSettlement).swapAggregate{value: ethValue}(
// order, signatures, actualFilledTakerAmount
// );
// // Restore original timestamp
// vm.warp(currentTimestamp);
// vm.stopPrank();
// // Calculate actual amount received
// uint256 balanceAfter = tokenOut == address(0)
// ? order.receiver.balance
// : IERC20(tokenOut).balanceOf(order.receiver);
// amountOut = balanceAfter - balanceBefore;
}
}
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
}
// Allow test contract to receive ETH
receive() external payable {}
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 {
vm.skip(true);
}
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);
}
}