chore: fix single encoding bebop tests

This commit is contained in:
pedrobergamini
2025-08-10 16:59:06 -03:00
parent 30a5bd0585
commit 97a961ecb8
5 changed files with 187 additions and 212 deletions

View File

@@ -9,6 +9,7 @@ import {
SafeERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import {console} from "forge-std/Test.sol";
/// @dev Bebop settlement interface for PMM RFQ swaps
interface IBebopSettlement {
@@ -147,8 +148,40 @@ contract BebopExecutor is IExecutor, IExecutorErrors, RestrictTransferFrom {
// Execute the swap with the forwarded calldata
uint256 ethValue = tokenIn == address(0) ? givenAmount : 0;
// Debug: Check msg.sender before settlement call
console.log(
"BebopExecutor: About to call settlement, msg.sender:", msg.sender
);
// Debug: Let's check what's in the calldata
bytes4 selector = _getSelector(finalCalldata);
if (selector == SWAP_AGGREGATE_SELECTOR) {
// Try to extract taker_address from the aggregate order
if (finalCalldata.length > 100) {
// Read the offset to the order struct
uint256 orderOffset;
assembly {
orderOffset := mload(add(finalCalldata, 0x24))
}
// The taker_address is at orderOffset + 4 (selector) + 32 (after expiry)
address orderTaker;
assembly {
orderTaker :=
mload(add(finalCalldata, add(0x24, add(orderOffset, 32))))
}
console.log("Order taker_address in calldata:", orderTaker);
}
}
// Use OpenZeppelin's Address library for safe call with value
bebopSettlement.functionCallWithValue(finalCalldata, ethValue);
// This will revert if the call fails
bytes memory returnData =
bebopSettlement.functionCallWithValue(finalCalldata, ethValue);
// Check if any tokens were actually transferred
if (returnData.length > 0) {
// Bebop might return some data, log it for debugging
}
// Calculate actual amount received by the receiver
uint256 balanceAfter = _balanceOf(tokenOut, receiver);
@@ -170,39 +203,29 @@ contract BebopExecutor is IExecutor, IExecutorErrors, RestrictTransferFrom {
address receiver
)
{
// Need at least 99 bytes for the minimum fixed fields
// 20 + 20 + 1 + 4 (calldata length) + 1 (offset) + 32 (original amount) + 1 (approval) + 20 (receiver) = 99
if (data.length < 99) revert BebopExecutor__InvalidDataLength();
// Need at least 95 bytes for the minimum fixed fields
// 20 + 20 + 1 + 1 (offset) + 32 (original amount) + 1 (approval) + 20 (receiver) = 95
if (data.length < 95) revert BebopExecutor__InvalidDataLength();
// Decode fixed fields
tokenIn = address(bytes20(data[0:20]));
tokenOut = address(bytes20(data[20:40]));
transferType = TransferType(uint8(data[40]));
// Get bebop calldata length and validate
uint32 bebopCalldataLength = uint32(bytes4(data[41:45]));
if (data.length != 99 + bebopCalldataLength) {
revert BebopExecutor__InvalidDataLength();
}
// Extract bebop calldata
bebopCalldata = data[45:45 + bebopCalldataLength];
// Extract partial fill offset
partialFillOffset = uint8(data[45 + bebopCalldataLength]);
partialFillOffset = uint8(data[41]);
// Extract original amount in
originalFilledTakerAmount = uint256(
bytes32(data[46 + bebopCalldataLength:78 + bebopCalldataLength])
);
originalFilledTakerAmount = uint256(bytes32(data[42:74]));
// Extract approval flag
approvalNeeded = data[78 + bebopCalldataLength] != 0;
approvalNeeded = data[74] != 0;
// Extract receiver address
receiver = address(
bytes20(data[79 + bebopCalldataLength:99 + bebopCalldataLength])
);
receiver = address(bytes20(data[75:95]));
// Extract bebop calldata (all remaining bytes)
bebopCalldata = data[95:];
}
/// @dev Modifies the filledTakerAmount in the bebop calldata to handle slippage

View File

@@ -526,10 +526,9 @@ contract TychoRouterSequentialSwapTest is TychoRouterTestSetup {
// Now using 0.099 WETH to get approximately 200 USDC from UniswapV3
uint256 wethAmount = 99000000000000000; // 0.099 WETH
deal(WETH_ADDR, ALICE, wethAmount);
uint256 balanceBefore = IERC20(ONDO_ADDR).balanceOf(
0xc5564C13A157E6240659fb81882A28091add8670
);
address orderTaker = 0xc5564C13A157E6240659fb81882A28091add8670; // Must match Bebop order taker
deal(WETH_ADDR, orderTaker, wethAmount);
uint256 balanceBefore = IERC20(ONDO_ADDR).balanceOf(orderTaker);
// Fund the Bebop maker with ONDO and approve settlement
uint256 ondoAmount = 237212396774431060000; // From the real order
@@ -537,17 +536,15 @@ contract TychoRouterSequentialSwapTest is TychoRouterTestSetup {
vm.prank(0xCe79b081c0c924cb67848723ed3057234d10FC6b);
IERC20(ONDO_ADDR).approve(BEBOP_SETTLEMENT, ondoAmount);
// Approve router
vm.startPrank(ALICE);
// Approve router from the order taker
vm.startPrank(orderTaker);
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData = loadCallDataFromFile("test_uniswap_v3_bebop");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(ONDO_ADDR).balanceOf(
0xc5564C13A157E6240659fb81882A28091add8670
);
uint256 balanceAfter = IERC20(ONDO_ADDR).balanceOf(orderTaker);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, ondoAmount);

View File

@@ -197,18 +197,21 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
address tokenIn,
address tokenOut,
RestrictTransferFrom.TransferType transferType,
bytes memory bebopCalldata,
uint8 partialFillOffset,
uint256 originalAmountIn,
bool approvalNeeded
bool approvalNeeded,
address receiver,
bytes memory bebopCalldata
) internal pure returns (bytes memory) {
return abi.encodePacked(
tokenIn,
tokenOut,
uint8(transferType),
uint32(bebopCalldata.length),
bebopCalldata,
partialFillOffset,
originalAmountIn,
approvalNeeded ? uint8(1) : uint8(0)
approvalNeeded ? uint8(1) : uint8(0),
receiver,
bebopCalldata
);
}

File diff suppressed because one or more lines are too long

View File

@@ -69,12 +69,11 @@ contract BebopExecutorHarness is BebopExecutor, Test {
override
returns (uint256 calculatedAmount)
{
console.log(
"[BebopHarness] swap entry, givenAmount=%s, value=%s",
givenAmount,
msg.value
);
// Decode packed params
console.log("BebopExecutorHarness::swap called");
console.log(" Given amount:", givenAmount);
console.log(" Data length:", data.length);
console.log(" Msg.sender:", msg.sender);
// Decode the data to get the bebop calldata
(
address tokenIn,
address tokenOut,
@@ -85,168 +84,122 @@ contract BebopExecutorHarness is BebopExecutor, Test {
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);
// Extract the selector to determine order type
bytes4 selector = bytes4(bebopCalldata);
// 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))
}
// Extract taker address from the order - Bebop validates msg.sender == order.taker_address
address takerAddress;
if (selector == SWAP_SINGLE_SELECTOR) {
// For single orders with inline encoding, taker_address is at position 36
// Position: 4 (selector) + 352 (inline order) + 32 (signature offset) = 388
// But we need taker_address which is at: 4 (selector) + 32 (expiry) = 36
assembly {
let dataPtr := add(bebopCalldata, 0x20)
takerAddress := mload(add(dataPtr, 36))
}
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;
} else if (selector == SWAP_AGGREGATE_SELECTOR) {
// For aggregate orders, extract taker_address from the calldata
// The aggregate order struct is passed as a calldata parameter
// We need to read the offset to the order struct, then extract taker_address
assembly {
let dataPtr := add(bebopCalldata, 0x20)
// Read the offset to the order struct (first parameter after selector)
let orderOffset := mload(add(dataPtr, 0x04))
// The taker_address is at orderOffset + 4 (selector) + 32 (after expiry)
takerAddress :=
mload(add(dataPtr, add(0x04, add(orderOffset, 32))))
}
}
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
"Extracted taker address from aggregate order:", takerAddress
);
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);
// For ERC20 tokens, we need to handle the flow differently
// The taker needs to have the tokens and approve the settlement
if (tokenIn != address(0)) {
// When called via delegatecall from the router, address(this) is the router
// So we check the balance of address(this) which will be the router
uint256 balance = IERC20(tokenIn).balanceOf(address(this));
console.log("Balance of tokenIn at address(this):", balance);
console.log("Address(this):", address(this));
// If we don't have tokens, the taker should have them
if (balance < givenAmount) {
// Try to transfer from the taker (who should have approved the router)
console.log("Transferring from taker to address(this)");
IERC20(tokenIn).transferFrom(
takerAddress, address(this), givenAmount
);
balance = IERC20(tokenIn).balanceOf(address(this));
console.log("Balance after transfer:", balance);
}
// Calculate the modified filledTakerAmount (what will actually be used)
bytes memory modifiedCalldata = _modifyFilledTakerAmount(
bebopCalldata,
givenAmount,
originalFilledTakerAmount,
partialFillOffset
);
// Extract the actual filledTakerAmount that will be used
uint256 actualFilledAmount = originalFilledTakerAmount > givenAmount
? givenAmount
: originalFilledTakerAmount;
console.log(
"Original filled taker amount:", originalFilledTakerAmount
);
console.log("Actual filled amount to use:", actualFilledAmount);
// Only transfer what's needed to the taker, keep the rest in router
IERC20(tokenIn).transfer(takerAddress, actualFilledAmount);
console.log("Transferred tokens to taker:", actualFilledAmount);
// Check balances after transfer
uint256 takerBalance = IERC20(tokenIn).balanceOf(takerAddress);
uint256 routerBalance = IERC20(tokenIn).balanceOf(address(this));
console.log("After transfer - Taker balance:", takerBalance);
console.log(
"After transfer - Router balance (dust):", routerBalance
);
// Impersonate the taker and approve settlement for what they have
vm.startPrank(takerAddress);
IERC20(tokenIn).approve(bebopSettlement, actualFilledAmount);
console.log("Taker approved settlement for:", actualFilledAmount);
vm.stopPrank();
// Check if taker still has the tokens
takerBalance = IERC20(tokenIn).balanceOf(takerAddress);
console.log("After approval - Taker balance:", takerBalance);
// Start pranking as taker for the actual swap
vm.startPrank(takerAddress);
} else {
// For ETH, start pranking as taker
vm.startPrank(takerAddress);
}
// Log the actual bebop call details
console.log("Calling Bebop settlement with:");
console.log(" Taker address:", takerAddress);
console.log(" Token in:", tokenIn);
console.log(" Token out:", tokenOut);
console.log(" Given amount:", givenAmount);
console.log(" Receiver:", receiver);
console.log(" Bebop calldata length:", bebopCalldata.length);
console.log(" Natural msg.sender (no prank):", msg.sender);
// Call the parent implementation which handles the actual swap
// The taker prank is already active from above
console.log("About to call _swap, msg.sender is:", msg.sender);
console.log("Pranked as taker:", takerAddress);
calculatedAmount = _swap(givenAmount, data);
// 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
console.log("Calculated amount returned:", calculatedAmount);
}
}