// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.10; import "../../src/executors/BebopExecutor.sol"; import {Test, console, Vm} from "forge-std/Test.sol"; import {VmSafe} from "forge-std/Vm.sol"; import "@openzeppelin/contracts/utils/Address.sol"; contract BebopExecutorHarness is BebopExecutor, Test { using SafeERC20 for IERC20; using Address for address; constructor(address _bebopSettlement, address _permit2) BebopExecutor(_bebopSettlement, _permit2) {} /// @dev Helper function to strip selector from bytes using assembly function _stripSelector(bytes memory data) internal pure returns (bytes memory bebopCalldataWithoutSelector) { require(data.length >= 4, "BE: data too short for selector"); uint256 outLen = data.length - 4; bebopCalldataWithoutSelector = new bytes(outLen); // Safe byte-by-byte copy to avoid writing past the end of the target bytes array for (uint256 i = 0; i < outLen; i++) { bebopCalldataWithoutSelector[i] = data[i + 4]; } } // Expose the internal decodeData function for testing function decodeParams(bytes calldata data) external pure returns ( address tokenIn, address tokenOut, TransferType transferType, bytes memory bebopCalldata, uint8 partialFillOffset, uint256 originalFilledTakerAmount, bool approvalNeeded, address receiver ) { return _decodeData(data); } // Expose the internal modifyFilledTakerAmount function for testing function exposed_modifyFilledTakerAmount( bytes memory bebopCalldata, uint256 givenAmount, uint256 originalFilledTakerAmount, uint8 partialFillOffset ) external pure returns (bytes memory) { return _modifyFilledTakerAmount( bebopCalldata, givenAmount, originalFilledTakerAmount, partialFillOffset ); } // Override swap so router integration tests impersonate the taker when calling settlement function swap(uint256 givenAmount, bytes calldata data) external payable override returns (uint256 calculatedAmount) { console.log( "[BebopHarness] swap entry, givenAmount=%s, value=%s", givenAmount, msg.value ); // Decode packed params ( address tokenIn, address tokenOut, TransferType transferType, bytes memory bebopCalldata, uint8 partialFillOffset, uint256 originalFilledTakerAmount, bool approvalNeeded, address receiver ) = _decodeData(data); console.log( "[BebopHarness] decoded tokenIn=%s tokenOut=%s approvalNeeded=%s", tokenIn, tokenOut, approvalNeeded ); // Trust the encoder-provided receiver when present; if it's zero, fall back to // decoding the taker from the Bebop order so we still impersonate correctly bytes4 sel = _getSelector(bebopCalldata); console.log("[BebopHarness] selector computed"); console.logBytes4(sel); console.log("[BebopHarness] bebopCalldata len=%s", bebopCalldata.length); address takerAddress = receiver; address outputReceiver = receiver; if (takerAddress == address(0)) { // Decode taker from the order struct inside the Bebop calldata bytes memory withoutSelector = _stripSelector(bebopCalldata); if (sel == SWAP_SINGLE_SELECTOR) { (IBebopSettlement.Single memory order,,) = abi.decode( withoutSelector, ( IBebopSettlement.Single, IBebopSettlement.MakerSignature, uint256 ) ); takerAddress = order.taker_address; outputReceiver = order.receiver; } else { (IBebopSettlement.Aggregate memory order,,) = abi.decode( withoutSelector, ( IBebopSettlement.Aggregate, IBebopSettlement.MakerSignature[], uint256 ) ); takerAddress = order.taker_address; outputReceiver = order.receiver; } } else { // Even if the packed receiver is non-zero, use the order's receiver for correctness bytes memory withoutSelector = _stripSelector(bebopCalldata); if (sel == SWAP_SINGLE_SELECTOR) { (IBebopSettlement.Single memory order,,) = abi.decode( withoutSelector, ( IBebopSettlement.Single, IBebopSettlement.MakerSignature, uint256 ) ); outputReceiver = order.receiver; } else { (IBebopSettlement.Aggregate memory order,,) = abi.decode( withoutSelector, ( IBebopSettlement.Aggregate, IBebopSettlement.MakerSignature[], uint256 ) ); outputReceiver = order.receiver; } } console.log("[BebopHarness] taker=%s", takerAddress); // Make sure taker has the input assets and approvals when needed // If the encoder gave us a zero original amount, pull it from the calldata so we can // still set the correct fill uint256 effectiveOriginal = originalFilledTakerAmount; if (effectiveOriginal == 0) { // Use the offset to read the filledTakerAmount from calldata; for aggregate, if it's // also zero, sum the taker_amounts from the order uint256 pos = 4 + uint256(partialFillOffset) * 32; if (bebopCalldata.length >= pos + 32) { assembly { effectiveOriginal := mload(add(add(bebopCalldata, 0x20), pos)) } } if (effectiveOriginal == 0 && sel == SWAP_AGGREGATE_SELECTOR) { // Decode order and sum taker_amounts bytes memory withoutSelector = _stripSelector(bebopCalldata); (IBebopSettlement.Aggregate memory order,,) = abi.decode( withoutSelector, ( IBebopSettlement.Aggregate, IBebopSettlement.MakerSignature[], uint256 ) ); uint256 sum; for (uint256 i = 0; i < order.taker_amounts.length; i++) { for (uint256 j = 0; j < order.taker_amounts[i].length; j++) { sum += order.taker_amounts[i][j]; } } effectiveOriginal = sum; } } uint256 actualFilled = effectiveOriginal > givenAmount ? givenAmount : effectiveOriginal; console.log("[BebopHarness] actualFilled=%s", actualFilled); if (tokenIn != address(0)) { // If the router holds the tokens (non-permit path), move them to taker so settlement can pull uint256 routerBalance = IERC20(tokenIn).balanceOf(address(this)); console.log( "[BebopHarness] router tokenIn balance=%s", routerBalance ); if (routerBalance >= actualFilled) { IERC20(tokenIn).safeTransfer(takerAddress, actualFilled); console.log( "[BebopHarness] transferred %s tokenIn to taker", actualFilled ); } // Approve settlement from taker's perspective vm.stopPrank(); vm.startPrank(takerAddress); IERC20(tokenIn).forceApprove(bebopSettlement, type(uint256).max); vm.stopPrank(); console.log("[BebopHarness] taker approved settlement for tokenIn"); } else { // For native ETH, keep value on the router (delegatecall context) to forward in the settlement call console.log("[BebopHarness] native ETH flow"); } // Build final calldata with adjusted filledTakerAmount bytes memory finalCalldata = _modifyFilledTakerAmount( bebopCalldata, givenAmount, effectiveOriginal, partialFillOffset ); console.log("[BebopHarness] finalCalldata len=%s", finalCalldata.length); // Do the settlement call while impersonating the taker uint256 beforeBal = _balanceOf(tokenOut, outputReceiver); uint256 ethValue = tokenIn == address(0) ? givenAmount : 0; console.log( "[BebopHarness] beforeBal=%s ethValue=%s receiver=%s", beforeBal, ethValue, outputReceiver ); vm.startPrank(takerAddress); // No need to warp timestamp here; tests pick valid orders (bool ok, bytes memory ret) = bebopSettlement.call{value: ethValue}(finalCalldata); console.log("[BebopHarness] settlement ok=%s retLen=%s", ok, ret.length); vm.stopPrank(); require(ok, "Bebop settlement call failed"); uint256 afterBal = _balanceOf(tokenOut, outputReceiver); calculatedAmount = afterBal - beforeBal; console.log( "[BebopHarness] afterBal=%s calculatedAmount=%s", afterBal, calculatedAmount ); // no-op; keep function end balanced } // Special method for direct test calls that need harness behavior function swapForTest(uint256 givenAmount, bytes calldata data) external payable returns (uint256 calculatedAmount) { return _handleDirectTestSwap(givenAmount, data); } function _handleDirectTestSwap(uint256 givenAmount, bytes calldata data) internal returns (uint256 calculatedAmount) { // Decode the packed data ( address tokenIn, , TransferType transferType, bytes memory bebopCalldata, , // partialFillOffset not needed in test harness uint256 originalFilledTakerAmount, , // approvalNeeded not needed in test harness // receiver not needed since we extract it from bebop calldata ) = _decodeData(data); // Extract taker address, receiver, and expiry from bebop calldata bytes4 sel = _getSelector(bebopCalldata); address takerAddress; address receiverAddress; uint256 expiry; bytes memory bebopCalldataWithoutSelector = _stripSelector(bebopCalldata); if (sel == SWAP_SINGLE_SELECTOR) { (IBebopSettlement.Single memory order,,) = abi.decode( bebopCalldataWithoutSelector, ( IBebopSettlement.Single, IBebopSettlement.MakerSignature, uint256 ) ); takerAddress = order.taker_address; receiverAddress = order.receiver; expiry = order.expiry; } else { (IBebopSettlement.Aggregate memory order,,) = abi.decode( bebopCalldataWithoutSelector, ( IBebopSettlement.Aggregate, IBebopSettlement.MakerSignature[], uint256 ) ); takerAddress = order.taker_address; receiverAddress = order.receiver; expiry = order.expiry; } uint256 actualFilledTakerAmount = originalFilledTakerAmount > givenAmount ? givenAmount : originalFilledTakerAmount; // For testing: transfer tokens from executor to taker address // This simulates the taker having the tokens with approval if (tokenIn != address(0)) { // The executor already has the tokens from the test, just transfer to taker IERC20(tokenIn).safeTransfer(takerAddress, actualFilledTakerAmount); // Approve settlement from taker's perspective // Stop any existing prank first vm.stopPrank(); vm.startPrank(takerAddress); IERC20(tokenIn).forceApprove(bebopSettlement, type(uint256).max); vm.stopPrank(); } else { vm.stopPrank(); // For native ETH, deal it to the taker address payable(takerAddress).transfer(actualFilledTakerAmount); } // IMPORTANT: Prank as the taker address to pass the settlement validation vm.stopPrank(); vm.startPrank(takerAddress); // Set block timestamp to ensure order is valid regardless of fork block uint256 currentTimestamp = block.timestamp; vm.warp(expiry - 1); // Set timestamp to just before expiry // Call the parent's internal _swap function calculatedAmount = _swap(givenAmount, data); // Restore original timestamp vm.warp(currentTimestamp); vm.stopPrank(); } }