// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.10; import "../../src/executors/BebopExecutor.sol"; import {Test, console} from "forge-std/Test.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"); // Create new array with length - 4 bebopCalldataWithoutSelector = new bytes(data.length - 4); assembly { // Get pointers to the data let srcPtr := add(data, 0x24) // Skip length (0x20) and selector (0x04) let destPtr := add(bebopCalldataWithoutSelector, 0x20) // Skip length // Copy all bytes after the selector let length := sub(mload(data), 4) // Copy word by word for efficiency let words := div(length, 32) let remainder := mod(length, 32) // Copy full words for { let i := 0 } lt(i, words) { i := add(i, 1) } { mstore(add(destPtr, mul(i, 32)), mload(add(srcPtr, mul(i, 32)))) } // Copy remaining bytes if any if remainder { let lastWord := mload(add(srcPtr, mul(words, 32))) mstore(add(destPtr, mul(words, 32)), lastWord) } } } // 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 to handle test setup function swap(uint256 givenAmount, bytes calldata data) external payable override 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(); } }