feat: implement encoder and executor with single order support

This commit is contained in:
pedrobergamini
2025-06-04 17:57:09 -03:00
parent 8f8af59f24
commit 7e7e3f0e01
8 changed files with 922 additions and 15 deletions

View File

@@ -0,0 +1,413 @@
// 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, console2} 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 BebopExecutorExposed is BebopExecutor {
constructor(
address _bebopSettlement,
address _nativeToken,
address _permit2
) BebopExecutor(_bebopSettlement, _nativeToken, _permit2) {}
function decodeParams(bytes calldata data)
external
pure
returns (
address tokenIn,
address tokenOut,
RestrictTransferFrom.TransferType transferType,
BebopExecutor.OrderType orderType,
bytes memory quoteData,
uint8 signatureType,
bytes memory signature,
bool approvalNeeded
)
{
return _decodeData(data);
}
}
/// @notice Mock Bebop settlement contract for testing
contract MockBebopSettlement is Test, Constants {
function swapSingle(
IBebopSettlement.Single calldata order,
IBebopSettlement.MakerSignature calldata, /* makerSignature */
uint256 filledTakerAmount
) external payable returns (uint256 filledMakerAmount) {
// Basic validation
require(order.expiry >= block.timestamp, "Order expired");
require(filledTakerAmount <= order.taker_amount, "Exceeds order amount");
// Mock implementation handles input tokens
if (order.taker_token == ETH_ADDR_FOR_CURVE) {
// For ETH input, validate msg.value
require(msg.value >= filledTakerAmount, "Insufficient ETH sent");
} else {
// For ERC20 input, transfer from sender
IERC20(order.taker_token).transferFrom(
msg.sender, address(this), filledTakerAmount
);
}
// Calculate proportional maker amount
filledMakerAmount =
(filledTakerAmount * order.maker_amount) / order.taker_amount;
// Send output tokens to receiver, or back to sender if receiver is zero
address recipient =
order.receiver != address(0) ? order.receiver : msg.sender;
if (order.maker_token == ETH_ADDR_FOR_CURVE) {
// For ETH output, send ETH directly
vm.deal(recipient, recipient.balance + filledMakerAmount);
} else {
// For ERC20 output, mint tokens to recipient
deal(
order.maker_token,
recipient,
IERC20(order.maker_token).balanceOf(recipient)
+ filledMakerAmount
);
}
return filledMakerAmount;
}
}
contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
using SafeERC20 for IERC20;
BebopExecutorExposed bebopExecutor;
MockBebopSettlement mockBebopSettlement;
MockToken WETH;
MockToken USDC;
function setUp() public {
// Deploy mock tokens
WETH = new MockToken("Wrapped Ether", "WETH", 18);
USDC = new MockToken("USD Coin", "USDC", 6);
// Deploy at expected addresses
vm.etch(WETH_ADDR, address(WETH).code);
vm.etch(USDC_ADDR, address(USDC).code);
// Update references
WETH = MockToken(WETH_ADDR);
USDC = MockToken(USDC_ADDR);
// Deploy mock contracts
mockBebopSettlement = new MockBebopSettlement();
// Deploy Bebop executor
bebopExecutor = new BebopExecutorExposed(
address(mockBebopSettlement),
ETH_ADDR_FOR_CURVE, // Native token address
PERMIT2_ADDRESS
);
// Fund test accounts
WETH.mint(address(this), 100e18);
USDC.mint(address(this), 100_000e6); // Mint USDC to test contract
USDC.mint(address(mockBebopSettlement), 100_000e6);
}
// Allow test contract to receive ETH
receive() external payable {}
function testDecodeParams() public view {
bytes memory quoteData = hex"1234567890abcdef";
bytes memory signature = hex"aabbccdd";
bytes memory params = abi.encodePacked(
WETH_ADDR,
USDC_ADDR,
uint8(RestrictTransferFrom.TransferType.Transfer),
uint8(0), // OrderType.Single
uint32(quoteData.length),
quoteData,
uint8(0), // signatureType: ECDSA
uint32(signature.length),
signature,
uint8(1) // approvalNeeded: true
);
(
address tokenIn,
address tokenOut,
RestrictTransferFrom.TransferType transferType,
BebopExecutor.OrderType orderType,
bytes memory decodedQuoteData,
uint8 decodedSignatureType,
bytes memory decodedSignature,
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(keccak256(decodedQuoteData), keccak256(quoteData));
assertEq(decodedSignatureType, 0); // ECDSA signature type
assertEq(keccak256(decodedSignature), keccak256(signature));
assertTrue(decodedApprovalNeeded); // Approval needed should be true
}
function testRFQSwap() public {
uint256 amountIn = 1e18; // 1 WETH
uint256 expectedAmountOut = 3000e6; // 3000 USDC
// Create a valid Bebop order
IBebopSettlement.Single memory order = IBebopSettlement.Single({
expiry: block.timestamp + 3600,
taker_address: address(0), // Any taker
maker_address: address(mockBebopSettlement),
maker_nonce: 1,
taker_token: WETH_ADDR,
maker_token: USDC_ADDR,
taker_amount: amountIn,
maker_amount: expectedAmountOut,
receiver: address(bebopExecutor), // Output should go to executor
packed_commands: 0,
flags: 0
});
// Encode order as quote data
bytes memory quoteData = abi.encode(order);
bytes memory signature = hex"aabbccdd"; // Mock signature
bytes memory params = abi.encodePacked(
WETH_ADDR,
USDC_ADDR,
uint8(RestrictTransferFrom.TransferType.Transfer),
uint8(0), // OrderType.Single
uint32(quoteData.length),
quoteData,
uint8(0), // signatureType: ECDSA
uint32(signature.length),
signature,
uint8(1) // approvalNeeded: true
);
// Transfer WETH to executor first
WETH.transfer(address(bebopExecutor), amountIn);
// Execute swap
uint256 executorBalanceBefore = USDC.balanceOf(address(bebopExecutor));
uint256 amountOut = bebopExecutor.swap(amountIn, params);
uint256 executorBalanceAfter = USDC.balanceOf(address(bebopExecutor));
// Check that tokens ended up in the executor for the router to collect
assertEq(amountOut, expectedAmountOut);
assertEq(executorBalanceAfter - executorBalanceBefore, amountOut);
}
function testETHInput() public {
uint256 amountIn = 1e18; // 1 ETH
uint256 expectedAmountOut = 3000e6; // 3000 USDC
// Create a valid Bebop order with ETH input
IBebopSettlement.Single memory order = IBebopSettlement.Single({
expiry: block.timestamp + 3600,
taker_address: address(0), // Any taker
maker_address: address(mockBebopSettlement),
maker_nonce: 1,
taker_token: ETH_ADDR_FOR_CURVE, // ETH input
maker_token: USDC_ADDR,
taker_amount: amountIn,
maker_amount: expectedAmountOut,
receiver: address(bebopExecutor), // Output should go to executor
packed_commands: 0,
flags: 0
});
// Encode order as quote data
bytes memory quoteData = abi.encode(order);
bytes memory signature = hex"aabbccdd"; // Mock signature
bytes memory params = abi.encodePacked(
ETH_ADDR_FOR_CURVE, // ETH input
USDC_ADDR,
uint8(RestrictTransferFrom.TransferType.None), // ETH comes via msg.value
uint8(0), // OrderType.Single
uint32(quoteData.length),
quoteData,
uint8(0), // signatureType: ECDSA
uint32(signature.length),
signature,
uint8(0) // approvalNeeded: false for ETH
);
// Fund test contract with ETH
vm.deal(address(this), 10e18);
uint256 executorBalanceBefore = USDC.balanceOf(address(bebopExecutor));
uint256 amountOut =
bebopExecutor.swap{value: amountIn}(amountIn, params);
uint256 executorBalanceAfter = USDC.balanceOf(address(bebopExecutor));
// Check that tokens ended up in the executor for the router to collect
assertEq(amountOut, expectedAmountOut);
assertEq(executorBalanceAfter - executorBalanceBefore, amountOut);
}
function testETHOutput() public {
uint256 amountIn = 1000e6; // 1000 USDC
uint256 expectedAmountOut = 1e18; // 1 ETH
// Create a valid Bebop order with ETH output
IBebopSettlement.Single memory order = IBebopSettlement.Single({
expiry: block.timestamp + 3600,
taker_address: address(0), // Any taker
maker_address: address(mockBebopSettlement),
maker_nonce: 1,
taker_token: USDC_ADDR,
maker_token: ETH_ADDR_FOR_CURVE, // ETH output
taker_amount: amountIn,
maker_amount: expectedAmountOut,
receiver: address(bebopExecutor), // Output should go to executor
packed_commands: 0,
flags: 0
});
// Encode order as quote data
bytes memory quoteData = abi.encode(order);
bytes memory signature = hex"aabbccdd"; // Mock signature
bytes memory params = abi.encodePacked(
USDC_ADDR,
ETH_ADDR_FOR_CURVE, // ETH output
uint8(RestrictTransferFrom.TransferType.Transfer),
uint8(0), // OrderType.Single
uint32(quoteData.length),
quoteData,
uint8(0), // signatureType: ECDSA
uint32(signature.length),
signature,
uint8(1) // approvalNeeded: true for USDC
);
// Transfer USDC to executor first
USDC.transfer(address(bebopExecutor), amountIn);
uint256 executorEthBalanceBefore = address(bebopExecutor).balance;
uint256 amountOut = bebopExecutor.swap(amountIn, params);
uint256 executorEthBalanceAfter = address(bebopExecutor).balance;
// Make sure the ETH ended up in the executor for the router to collect
assertEq(amountOut, expectedAmountOut);
assertEq(executorEthBalanceAfter - executorEthBalanceBefore, amountOut);
}
function testExpiredQuote() public {
uint256 amountIn = 1e18;
uint256 expectedAmountOut = 3000e6;
// Create an order with expired timestamp
IBebopSettlement.Single memory order = IBebopSettlement.Single({
expiry: block.timestamp - 1, // Already expired
taker_address: address(0),
maker_address: address(mockBebopSettlement),
maker_nonce: 1,
taker_token: WETH_ADDR,
maker_token: USDC_ADDR,
taker_amount: amountIn,
maker_amount: expectedAmountOut,
receiver: address(bebopExecutor), // Output should go to executor
packed_commands: 0,
flags: 0
});
bytes memory quoteData = abi.encode(order);
bytes memory signature = hex"aabbccdd";
bytes memory params = abi.encodePacked(
WETH_ADDR,
USDC_ADDR,
uint8(RestrictTransferFrom.TransferType.Transfer),
uint8(0), // OrderType.Single
uint32(quoteData.length),
quoteData,
uint8(0), // signatureType: ECDSA
uint32(signature.length),
signature,
uint8(1) // approvalNeeded: true
);
// Transfer WETH to executor
WETH.transfer(address(bebopExecutor), amountIn);
// Should revert due to expired order
vm.expectRevert(BebopExecutor.BebopExecutor__SettlementFailed.selector);
bebopExecutor.swap(amountIn, params);
}
function testInvalidDataLength() public {
bytes memory quoteData = hex"1234567890abcdef";
bytes memory signature = hex"aabbccdd";
// Create params with correct length first
bytes memory validParams = abi.encodePacked(
WETH_ADDR,
USDC_ADDR,
uint8(RestrictTransferFrom.TransferType.Transfer),
uint8(0), // OrderType.Single
uint32(quoteData.length),
quoteData,
uint8(0), // signatureType: ECDSA
uint32(signature.length),
signature,
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);
}
}