chore: fix single encoding bebop tests
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user