diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index 559ec7b..5a2554b 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -55,6 +55,7 @@ contract Constants is Test, BaseConstants { address WTAO_ADDR = address(0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44); address BSGG_ADDR = address(0xdA16Cf041E2780618c49Dbae5d734B89a6Bac9b3); address GHO_ADDR = address(0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f); + address ONDO_ADDR = address(0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3); // Maverick v2 address MAVERICK_V2_FACTORY = 0x0A7e848Aca42d879EF06507Fca0E7b33A0a63c1e; diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 4e7aef9..e948ed2 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -18,7 +18,7 @@ import {Permit2TestHelper} from "./Permit2TestHelper.sol"; import "./TestUtils.sol"; import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol"; import {BalancerV3Executor} from "../src/executors/BalancerV3Executor.sol"; -import {MockBebopSettlement} from "./executors/BebopExecutor.t.sol"; +import {BebopSettlementMock} from "./mock/BebopSettlementMock.sol"; contract TychoRouterExposed is TychoRouter { constructor(address _permit2, address weth) TychoRouter(_permit2, weth) {} @@ -91,6 +91,13 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { tychoRouter.setExecutors(executors); vm.stopPrank(); + // Deploy our mock Bebop settlement and use vm.etch to replace the real one + // This avoids InvalidSender errors since the mock doesn't validate taker addresses + // Do this AFTER deploying executors to preserve deterministic addresses + BebopSettlementMock mockSettlement = new BebopSettlementMock(); + bytes memory mockCode = address(mockSettlement).code; + vm.etch(BEBOP_SETTLEMENT, mockCode); + vm.startPrank(BOB); tokens.push(new MockERC20("Token A", "A")); tokens.push(new MockERC20("Token B", "B")); @@ -135,11 +142,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { maverickv2Executor = new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS); balancerV3Executor = new BalancerV3Executor(PERMIT2_ADDRESS); - - // Deploy mock Bebop settlement for testing - MockBebopSettlement mockBebopSettlement = new MockBebopSettlement(); - bebopExecutor = - new BebopExecutor(address(mockBebopSettlement), PERMIT2_ADDRESS); + bebopExecutor = new BebopExecutor(BEBOP_SETTLEMENT, PERMIT2_ADDRESS); address[] memory executors = new address[](10); executors[0] = address(usv2Executor); diff --git a/foundry/test/mock/BebopSettlementMock.sol b/foundry/test/mock/BebopSettlementMock.sol new file mode 100644 index 0000000..90d2420 --- /dev/null +++ b/foundry/test/mock/BebopSettlementMock.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "@src/executors/BebopExecutor.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title BebopSettlementMock + * @notice Mock Bebop settlement contract that skips taker_address validation + * @dev This is used for testing purposes to work around the msg.sender == taker_address check + * while maintaining all other Bebop settlement logic + */ +contract BebopSettlementMock { + error InvalidSignature(); + error OrderExpired(); + error InsufficientMakerBalance(); + + // Nonce tracking to prevent replay attacks + mapping(address => mapping(uint256 => bool)) public makerNonceUsed; + + function swapSingle( + IBebopSettlement.Single calldata order, + IBebopSettlement.MakerSignature calldata makerSignature, + uint256 filledTakerAmount + ) external payable { + // Check order expiry + if (block.timestamp > order.expiry) revert OrderExpired(); + + // Check nonce hasn't been used + if (makerNonceUsed[order.maker_address][order.maker_nonce]) { + revert InvalidSignature(); + } + + // IMPORTANT: We skip the taker_address validation that would normally be here: + // if (msg.sender != order.taker_address) revert InvalidCaller(); + + // For testing, we'll do a simplified signature validation + // In reality, Bebop would validate the full order signature + // Accept both proper 65-byte signatures and test placeholders + if (makerSignature.signatureBytes.length < 4) { + revert InvalidSignature(); + } + + // Mark nonce as used + makerNonceUsed[order.maker_address][order.maker_nonce] = true; + + // Calculate amounts + uint256 actualTakerAmount = + filledTakerAmount == 0 ? order.taker_amount : filledTakerAmount; + uint256 actualMakerAmount = filledTakerAmount == 0 + ? order.maker_amount + : (order.maker_amount * filledTakerAmount) / order.taker_amount; + + // Transfer taker tokens from msg.sender to maker + if (order.taker_token == address(0)) { + // ETH transfer + require(msg.value == actualTakerAmount, "Incorrect ETH amount"); + payable(order.maker_address).transfer(actualTakerAmount); + } else { + // ERC20 transfer + IERC20(order.taker_token).transferFrom( + msg.sender, order.maker_address, actualTakerAmount + ); + } + + // Transfer maker tokens from maker to receiver + if (order.maker_token == address(0)) { + // ETH transfer - this shouldn't happen in practice + revert("ETH output not supported"); + } else { + // In the real contract, maker would need to have tokens and approve + // For testing, we'll check if maker has balance, if not we assume they're funded + uint256 makerBalance = + IERC20(order.maker_token).balanceOf(order.maker_address); + if (makerBalance < actualMakerAmount) { + revert InsufficientMakerBalance(); + } + + // Transfer from maker to receiver + // This assumes the maker has pre-approved the settlement contract + IERC20(order.maker_token).transferFrom( + order.maker_address, order.receiver, actualMakerAmount + ); + } + } + + function swapAggregate( + IBebopSettlement.Aggregate calldata order, + IBebopSettlement.MakerSignature[] calldata makerSignatures, + uint256 filledTakerAmount + ) external payable { + // Check order expiry + if (block.timestamp > order.expiry) revert OrderExpired(); + + // Check we have at least one maker + if (makerSignatures.length == 0) revert InvalidSignature(); + + // For testing, we'll do a simplified signature validation + for (uint256 i = 0; i < makerSignatures.length; i++) { + if (makerSignatures[i].signatureBytes.length < 4) { + revert InvalidSignature(); + } + } + + // Aggregate orders only support full fills + require( + filledTakerAmount == 0, + "Partial fills not supported for aggregate orders" + ); + + // Transfer taker tokens from msg.sender to makers + for (uint256 i = 0; i < order.taker_tokens.length; i++) { + uint256 takerAmount = order.taker_amounts[i]; + // Split proportionally among makers + for (uint256 j = 0; j < order.maker_addresses.length; j++) { + uint256 makerShare = takerAmount / order.maker_addresses.length; + if (j == order.maker_addresses.length - 1) { + // Last maker gets any remainder + makerShare = takerAmount + - (makerShare * (order.maker_addresses.length - 1)); + } + IERC20(order.taker_tokens[i]).transferFrom( + msg.sender, order.maker_addresses[j], makerShare + ); + } + } + + // Transfer maker tokens from each maker to receiver + for (uint256 i = 0; i < order.maker_addresses.length; i++) { + address maker = order.maker_addresses[i]; + + // Fund maker with tokens if they don't have enough (for testing) + for (uint256 j = 0; j < order.maker_tokens[i].length; j++) { + address token = order.maker_tokens[i][j]; + uint256 amount = order.maker_amounts[i][j]; + + uint256 makerBalance = IERC20(token).balanceOf(maker); + if (makerBalance < amount) { + revert InsufficientMakerBalance(); + } + + // Transfer from maker to receiver + IERC20(token).transferFrom(maker, order.receiver, amount); + } + } + } +}