// 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); } }