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 SafeERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/utils/Address.sol";
import {console} from "forge-std/Test.sol";
/// @dev Bebop settlement interface for PMM RFQ swaps /// @dev Bebop settlement interface for PMM RFQ swaps
interface IBebopSettlement { interface IBebopSettlement {
@@ -147,9 +148,41 @@ contract BebopExecutor is IExecutor, IExecutorErrors, RestrictTransferFrom {
// Execute the swap with the forwarded calldata // Execute the swap with the forwarded calldata
uint256 ethValue = tokenIn == address(0) ? givenAmount : 0; 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 // Use OpenZeppelin's Address library for safe call with value
// This will revert if the call fails
bytes memory returnData =
bebopSettlement.functionCallWithValue(finalCalldata, ethValue); 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 // Calculate actual amount received by the receiver
uint256 balanceAfter = _balanceOf(tokenOut, receiver); uint256 balanceAfter = _balanceOf(tokenOut, receiver);
calculatedAmount = balanceAfter - balanceBefore; calculatedAmount = balanceAfter - balanceBefore;
@@ -170,39 +203,29 @@ contract BebopExecutor is IExecutor, IExecutorErrors, RestrictTransferFrom {
address receiver address receiver
) )
{ {
// Need at least 99 bytes for the minimum fixed fields // Need at least 95 bytes for the minimum fixed fields
// 20 + 20 + 1 + 4 (calldata length) + 1 (offset) + 32 (original amount) + 1 (approval) + 20 (receiver) = 99 // 20 + 20 + 1 + 1 (offset) + 32 (original amount) + 1 (approval) + 20 (receiver) = 95
if (data.length < 99) revert BebopExecutor__InvalidDataLength(); if (data.length < 95) revert BebopExecutor__InvalidDataLength();
// Decode fixed fields // Decode fixed fields
tokenIn = address(bytes20(data[0:20])); tokenIn = address(bytes20(data[0:20]));
tokenOut = address(bytes20(data[20:40])); tokenOut = address(bytes20(data[20:40]));
transferType = TransferType(uint8(data[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 // Extract partial fill offset
partialFillOffset = uint8(data[45 + bebopCalldataLength]); partialFillOffset = uint8(data[41]);
// Extract original amount in // Extract original amount in
originalFilledTakerAmount = uint256( originalFilledTakerAmount = uint256(bytes32(data[42:74]));
bytes32(data[46 + bebopCalldataLength:78 + bebopCalldataLength])
);
// Extract approval flag // Extract approval flag
approvalNeeded = data[78 + bebopCalldataLength] != 0; approvalNeeded = data[74] != 0;
// Extract receiver address // Extract receiver address
receiver = address( receiver = address(bytes20(data[75:95]));
bytes20(data[79 + bebopCalldataLength:99 + bebopCalldataLength])
); // Extract bebop calldata (all remaining bytes)
bebopCalldata = data[95:];
} }
/// @dev Modifies the filledTakerAmount in the bebop calldata to handle slippage /// @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 // Now using 0.099 WETH to get approximately 200 USDC from UniswapV3
uint256 wethAmount = 99000000000000000; // 0.099 WETH uint256 wethAmount = 99000000000000000; // 0.099 WETH
deal(WETH_ADDR, ALICE, wethAmount); address orderTaker = 0xc5564C13A157E6240659fb81882A28091add8670; // Must match Bebop order taker
uint256 balanceBefore = IERC20(ONDO_ADDR).balanceOf( deal(WETH_ADDR, orderTaker, wethAmount);
0xc5564C13A157E6240659fb81882A28091add8670 uint256 balanceBefore = IERC20(ONDO_ADDR).balanceOf(orderTaker);
);
// Fund the Bebop maker with ONDO and approve settlement // Fund the Bebop maker with ONDO and approve settlement
uint256 ondoAmount = 237212396774431060000; // From the real order uint256 ondoAmount = 237212396774431060000; // From the real order
@@ -537,17 +536,15 @@ contract TychoRouterSequentialSwapTest is TychoRouterTestSetup {
vm.prank(0xCe79b081c0c924cb67848723ed3057234d10FC6b); vm.prank(0xCe79b081c0c924cb67848723ed3057234d10FC6b);
IERC20(ONDO_ADDR).approve(BEBOP_SETTLEMENT, ondoAmount); IERC20(ONDO_ADDR).approve(BEBOP_SETTLEMENT, ondoAmount);
// Approve router // Approve router from the order taker
vm.startPrank(ALICE); vm.startPrank(orderTaker);
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max); IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData = loadCallDataFromFile("test_uniswap_v3_bebop"); bytes memory callData = loadCallDataFromFile("test_uniswap_v3_bebop");
(bool success,) = tychoRouterAddr.call(callData); (bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank(); vm.stopPrank();
uint256 balanceAfter = IERC20(ONDO_ADDR).balanceOf( uint256 balanceAfter = IERC20(ONDO_ADDR).balanceOf(orderTaker);
0xc5564C13A157E6240659fb81882A28091add8670
);
assertTrue(success, "Call Failed"); assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, ondoAmount); assertEq(balanceAfter - balanceBefore, ondoAmount);

View File

@@ -197,18 +197,21 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
address tokenIn, address tokenIn,
address tokenOut, address tokenOut,
RestrictTransferFrom.TransferType transferType, RestrictTransferFrom.TransferType transferType,
bytes memory bebopCalldata, uint8 partialFillOffset,
uint256 originalAmountIn, uint256 originalAmountIn,
bool approvalNeeded bool approvalNeeded,
address receiver,
bytes memory bebopCalldata
) internal pure returns (bytes memory) { ) internal pure returns (bytes memory) {
return abi.encodePacked( return abi.encodePacked(
tokenIn, tokenIn,
tokenOut, tokenOut,
uint8(transferType), uint8(transferType),
uint32(bebopCalldata.length), partialFillOffset,
bebopCalldata,
originalAmountIn, 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 override
returns (uint256 calculatedAmount) returns (uint256 calculatedAmount)
{ {
console.log( console.log("BebopExecutorHarness::swap called");
"[BebopHarness] swap entry, givenAmount=%s, value=%s", console.log(" Given amount:", givenAmount);
givenAmount, console.log(" Data length:", data.length);
msg.value console.log(" Msg.sender:", msg.sender);
); // Decode the data to get the bebop calldata
// Decode packed params
( (
address tokenIn, address tokenIn,
address tokenOut, address tokenOut,
@@ -85,168 +84,122 @@ contract BebopExecutorHarness is BebopExecutor, Test {
bool approvalNeeded, bool approvalNeeded,
address receiver address receiver
) = _decodeData(data); ) = _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 // Extract the selector to determine order type
// decoding the taker from the Bebop order so we still impersonate correctly bytes4 selector = bytes4(bebopCalldata);
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 // Extract taker address from the order - Bebop validates msg.sender == order.taker_address
// If the encoder gave us a zero original amount, pull it from the calldata so we can address takerAddress;
// still set the correct fill if (selector == SWAP_SINGLE_SELECTOR) {
uint256 effectiveOriginal = originalFilledTakerAmount; // For single orders with inline encoding, taker_address is at position 36
if (effectiveOriginal == 0) { // Position: 4 (selector) + 352 (inline order) + 32 (signature offset) = 388
// Use the offset to read the filledTakerAmount from calldata; for aggregate, if it's // But we need taker_address which is at: 4 (selector) + 32 (expiry) = 36
// also zero, sum the taker_amounts from the order
uint256 pos = 4 + uint256(partialFillOffset) * 32;
if (bebopCalldata.length >= pos + 32) {
assembly { assembly {
effectiveOriginal := let dataPtr := add(bebopCalldata, 0x20)
mload(add(add(bebopCalldata, 0x20), pos)) takerAddress := mload(add(dataPtr, 36))
} }
} 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))))
} }
if (effectiveOriginal == 0 && sel == SWAP_AGGREGATE_SELECTOR) { console.log(
// Decode order and sum taker_amounts "Extracted taker address from aggregate order:", takerAddress
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; // For ERC20 tokens, we need to handle the flow differently
} // The taker needs to have the tokens and approve the settlement
}
uint256 actualFilled =
effectiveOriginal > givenAmount ? givenAmount : effectiveOriginal;
console.log("[BebopHarness] actualFilled=%s", actualFilled);
if (tokenIn != address(0)) { if (tokenIn != address(0)) {
// If the router holds the tokens (non-permit path), move them to taker so settlement can pull // 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)); uint256 routerBalance = IERC20(tokenIn).balanceOf(address(this));
console.log("After transfer - Taker balance:", takerBalance);
console.log( console.log(
"[BebopHarness] router tokenIn balance=%s", routerBalance "After transfer - Router balance (dust):", routerBalance
); );
if (routerBalance >= actualFilled) {
IERC20(tokenIn).safeTransfer(takerAddress, actualFilled);
console.log(
"[BebopHarness] transferred %s tokenIn to taker",
actualFilled
);
}
// Approve settlement from taker's perspective // Impersonate the taker and approve settlement for what they have
vm.stopPrank();
vm.startPrank(takerAddress); vm.startPrank(takerAddress);
IERC20(tokenIn).forceApprove(bebopSettlement, type(uint256).max); IERC20(tokenIn).approve(bebopSettlement, actualFilledAmount);
console.log("Taker approved settlement for:", actualFilledAmount);
vm.stopPrank(); vm.stopPrank();
console.log("[BebopHarness] taker approved settlement for tokenIn");
// 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 { } else {
// For native ETH, keep value on the router (delegatecall context) to forward in the settlement call // For ETH, start pranking as taker
console.log("[BebopHarness] native ETH flow"); vm.startPrank(takerAddress);
} }
// Build final calldata with adjusted filledTakerAmount // Log the actual bebop call details
bytes memory finalCalldata = _modifyFilledTakerAmount( console.log("Calling Bebop settlement with:");
bebopCalldata, givenAmount, effectiveOriginal, partialFillOffset console.log(" Taker address:", takerAddress);
); console.log(" Token in:", tokenIn);
console.log("[BebopHarness] finalCalldata len=%s", finalCalldata.length); 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(); vm.stopPrank();
require(ok, "Bebop settlement call failed");
uint256 afterBal = _balanceOf(tokenOut, outputReceiver); console.log("Calculated amount returned:", calculatedAmount);
calculatedAmount = afterBal - beforeBal;
console.log(
"[BebopHarness] afterBal=%s calculatedAmount=%s",
afterBal,
calculatedAmount
);
// no-op; keep function end balanced
} }
} }