chore: start implementing test changes related to partialFillOffset and encoded receiver

This commit is contained in:
pedrobergamini
2025-08-05 22:45:48 -03:00
parent 2583e9239e
commit 1336eb5f90
4 changed files with 125 additions and 277 deletions

View File

@@ -114,13 +114,17 @@ contract BebopExecutor is IExecutor, IExecutorErrors, RestrictTransferFrom {
bytes memory bebopCalldata,
uint8 partialFillOffset,
uint256 originalFilledTakerAmount,
bool approvalNeeded
bool approvalNeeded,
address receiver
) = _decodeData(data);
// Modify the filledTakerAmount in the calldata
// If the filledTakerAmount is the same as the original, the original calldata is returned
bytes memory finalCalldata = _modifyFilledTakerAmount(
bebopCalldata, givenAmount, originalFilledTakerAmount, partialFillOffset
bebopCalldata,
givenAmount,
originalFilledTakerAmount,
partialFillOffset
);
// Transfer tokens if needed
@@ -134,9 +138,8 @@ contract BebopExecutor is IExecutor, IExecutorErrors, RestrictTransferFrom {
IERC20(tokenIn).forceApprove(bebopSettlement, type(uint256).max);
}
// Bebop orders specify the receiver, so we need to check the receiver's balance
// We'll use the executor's balance since Bebop should send tokens here for the router to collect
uint256 balanceBefore = _balanceOf(tokenOut, address(this));
// Check the receiver's balance before the swap
uint256 balanceBefore = _balanceOf(tokenOut, receiver);
// Execute the swap with the forwarded calldata
uint256 ethValue = tokenIn == address(0) ? givenAmount : 0;
@@ -144,8 +147,8 @@ contract BebopExecutor is IExecutor, IExecutorErrors, RestrictTransferFrom {
// Use OpenZeppelin's Address library for safe call with value
bebopSettlement.functionCallWithValue(finalCalldata, ethValue);
// Calculate actual amount received by the executor
uint256 balanceAfter = _balanceOf(tokenOut, address(this));
// Calculate actual amount received by the receiver
uint256 balanceAfter = _balanceOf(tokenOut, receiver);
calculatedAmount = balanceAfter - balanceBefore;
}
@@ -160,12 +163,13 @@ contract BebopExecutor is IExecutor, IExecutorErrors, RestrictTransferFrom {
bytes memory bebopCalldata,
uint8 partialFillOffset,
uint256 originalFilledTakerAmount,
bool approvalNeeded
bool approvalNeeded,
address receiver
)
{
// Need at least 79 bytes for the minimum fixed fields
// 20 + 20 + 1 + 4 (calldata length) + 1 (offset) + 32 (original amount) + 1 (approval) = 79
if (data.length < 79) revert BebopExecutor__InvalidDataLength();
// 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();
// Decode fixed fields
tokenIn = address(bytes20(data[0:20]));
@@ -174,7 +178,7 @@ contract BebopExecutor is IExecutor, IExecutorErrors, RestrictTransferFrom {
// Get bebop calldata length and validate
uint32 bebopCalldataLength = uint32(bytes4(data[41:45]));
if (data.length != 79 + bebopCalldataLength) {
if (data.length != 99 + bebopCalldataLength) {
revert BebopExecutor__InvalidDataLength();
}
@@ -191,6 +195,11 @@ contract BebopExecutor is IExecutor, IExecutorErrors, RestrictTransferFrom {
// Extract approval flag
approvalNeeded = data[78 + bebopCalldataLength] != 0;
// Extract receiver address
receiver = address(
bytes20(data[79 + bebopCalldataLength:99 + bebopCalldataLength])
);
}
/// @dev Modifies the filledTakerAmount in the bebop calldata to handle slippage
@@ -210,8 +219,9 @@ contract BebopExecutor is IExecutor, IExecutorErrors, RestrictTransferFrom {
uint256 filledTakerAmountPos = 4 + uint256(partialFillOffset) * 32;
// Cap the fill amount at what we actually have available
uint256 newFilledTakerAmount =
originalFilledTakerAmount > givenAmount ? givenAmount : originalFilledTakerAmount;
uint256 newFilledTakerAmount = originalFilledTakerAmount > givenAmount
? givenAmount
: originalFilledTakerAmount;
// If the new filledTakerAmount is the same as the original, return the original calldata
if (newFilledTakerAmount == originalFilledTakerAmount) {

File diff suppressed because one or more lines are too long

View File

@@ -102,7 +102,8 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
bebopCalldata,
uint8(12), // partialFillOffset for swapSingle (388 = 4 + 12*32)
originalAmountIn,
uint8(1) // approvalNeeded: true
uint8(1), // approvalNeeded: true
address(123)
);
// Test decoding
@@ -113,7 +114,8 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
bytes memory decodedBebopCalldata,
uint8 decodedPartialFillOffset,
uint256 decodedOriginalAmountIn,
bool decodedApprovalNeeded
bool decodedApprovalNeeded,
address decodedReceiver
) = bebopExecutor.decodeParams(params);
assertEq(tokenIn, USDC_ADDR, "tokenIn mismatch");
@@ -135,6 +137,7 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
"originalAmountIn mismatch"
);
assertTrue(decodedApprovalNeeded, "approvalNeeded should be true");
assertEq(decodedReceiver, address(123), "receiver mismatch");
}
// Single Order Tests
@@ -217,18 +220,22 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
bebopCalldata,
uint8(12), // partialFillOffset for swapSingle (388 = 4 + 12*32)
testData.order.taker_amount, // originalAmountIn (matches what encoder would produce)
uint8(1) // approvalNeeded: true
uint8(1), // approvalNeeded: true
originalTakerAddress // receiver from order
);
// Check initial ONDO balance of receiver
uint256 initialOndoBalance = ONDO.balanceOf(originalTakerAddress);
uint256 amountOut = bebopExecutor.swap(testData.amountIn, params);
// Verify results
assertEq(amountOut, testData.expectedAmountOut, "Incorrect amount out");
// The harness transfers tokens to the executor to simulate proper flow
// Since we're using real order data, tokens go to the original receiver
assertEq(
ONDO.balanceOf(address(bebopExecutor)),
ONDO.balanceOf(originalTakerAddress) - initialOndoBalance,
testData.expectedAmountOut,
"ONDO should be in executor"
"ONDO should be at receiver"
);
assertEq(
USDC.balanceOf(address(bebopExecutor)), 0, "USDC left in executor"
@@ -313,9 +320,13 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
bebopCalldata,
uint8(12), // partialFillOffset for swapSingle (388 = 4 + 12*32)
testData.order.taker_amount, // originalAmountIn (full order amount)
uint8(1) // approvalNeeded: true
uint8(1), // approvalNeeded: true
originalTakerAddress // receiver from order
);
// Check initial ONDO balance of receiver
uint256 initialOndoBalance = ONDO.balanceOf(originalTakerAddress);
uint256 amountOut = bebopExecutor.swap(testData.amountIn, params);
// Verify partial fill results
@@ -324,11 +335,11 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
testData.expectedAmountOut,
"Incorrect partial amount out"
);
// The harness transfers tokens to the executor to simulate proper flow
// Since we're using real order data, tokens go to the original receiver
assertEq(
ONDO.balanceOf(address(bebopExecutor)),
ONDO.balanceOf(originalTakerAddress) - initialOndoBalance,
testData.expectedAmountOut,
"ONDO should be in executor"
"ONDO should be at receiver"
);
assertEq(
USDC.balanceOf(address(bebopExecutor)), 0, "USDC left in executor"
@@ -448,9 +459,13 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
bebopCalldata,
uint8(2), // partialFillOffset for swapAggregate (68 = 4 + 2*32)
totalTakerAmount, // originalAmountIn
uint8(0) // approvalNeeded: false for native ETH
uint8(0), // approvalNeeded: false for native ETH
originalTakerAddress // receiver from order
);
// Check initial USDC balance of receiver
uint256 initialUsdcBalance = USDC.balanceOf(originalTakerAddress);
// Execute the aggregate swap with ETH value
uint256 amountOut = bebopExecutor.swap{value: totalTakerAmount}(
totalTakerAmount, params
@@ -458,11 +473,11 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
// Verify results
assertEq(amountOut, totalMakerAmount, "Incorrect amount out");
// The harness transfers tokens to the executor to simulate proper flow
// Since we're using real order data, tokens go to the original receiver
assertEq(
USDC.balanceOf(address(bebopExecutor)),
USDC.balanceOf(originalTakerAddress) - initialUsdcBalance,
totalMakerAmount,
"USDC should be in executor"
"USDC should be at receiver"
);
// ETH balance check - the harness may have different balance due to test setup
// Just ensure no excessive ETH is stuck
@@ -588,9 +603,13 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
bebopCalldata,
uint8(2), // partialFillOffset for swapAggregate (68 = 4 + 2*32)
totalTakerAmount, // originalAmountIn (full order amount)
uint8(0) // approvalNeeded: false for native ETH
uint8(0), // approvalNeeded: false for native ETH
originalTakerAddress // receiver from order
);
// Check initial USDC balance of receiver
uint256 initialUsdcBalance = USDC.balanceOf(originalTakerAddress);
// Execute the partial aggregate swap with ETH value
uint256 amountOut = bebopExecutor.swap{value: partialFillAmount}(
partialFillAmount, params
@@ -600,11 +619,11 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
assertEq(
amountOut, expectedPartialOutput, "Incorrect partial amount out"
);
// The harness transfers tokens to the executor to simulate proper flow
// Since we're using real order data, tokens go to the original receiver
assertEq(
USDC.balanceOf(address(bebopExecutor)),
USDC.balanceOf(originalTakerAddress) - initialUsdcBalance,
expectedPartialOutput,
"USDC should be in executor"
"USDC should be at receiver"
);
// ETH balance check - the harness may have different balance due to test setup
// Just ensure no excessive ETH is stuck
@@ -637,7 +656,8 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
bebopCalldata,
uint8(12), // partialFillOffset for swapSingle (388 = 4 + 12*32)
originalAmountIn,
uint8(1) // approvalNeeded: true
uint8(1), // approvalNeeded: true
address(bebopExecutor)
);
// Verify valid params work
@@ -713,7 +733,8 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
bebopCalldata,
uint8(12), // partialFillOffset for swapSingle (388 = 4 + 12*32)
uint256(200000000), // originalAmountIn
uint8(1) // approvalNeeded: true
uint8(1), // approvalNeeded: true
originalTakerAddress // receiver from order
);
// Deal 200 USDC to the executor
@@ -727,16 +748,19 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
vm.prank(maker);
ONDO.approve(BEBOP_SETTLEMENT, expectedAmountOut);
// Check initial ONDO balance of receiver
uint256 initialOndoBalance = ONDO.balanceOf(originalTakerAddress);
// Execute the swap
uint256 amountOut = bebopExecutor.swap(amountIn, protocolData);
// Verify results
assertEq(amountOut, expectedAmountOut, "Incorrect amount out");
// The harness transfers tokens to the executor to simulate proper flow
// Since we're using historical data, tokens go to the original receiver
assertEq(
ONDO.balanceOf(address(bebopExecutor)),
ONDO.balanceOf(originalTakerAddress) - initialOndoBalance,
expectedAmountOut,
"ONDO should be in executor"
"ONDO should be at receiver"
);
assertEq(
USDC.balanceOf(address(bebopExecutor)), 0, "USDC left in executor"
@@ -834,7 +858,8 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
bebopCalldata,
uint8(2), // partialFillOffset for swapAggregate (68 = 4 + 2*32)
ethAmount, // originalAmountIn
uint8(0) // approvalNeeded: false for native ETH
uint8(0), // approvalNeeded: false for native ETH
orderTaker // receiver from order
);
// Fund the two makers from the real transaction with USDC
@@ -854,17 +879,20 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
vm.deal(ALICE, ethAmount);
vm.startPrank(ALICE);
// Check initial USDC balance of receiver
uint256 initialUsdcBalance = IERC20(USDC_ADDR).balanceOf(orderTaker);
// Execute the swap with native ETH
uint256 amountOut =
bebopExecutor.swap{value: ethAmount}(ethAmount, protocolData);
// Verify results
assertEq(amountOut, expAmountOut, "Incorrect amount out");
// The harness transfers tokens to the executor to simulate proper flow
// Since we're using historical data, tokens go to the original receiver
assertEq(
IERC20(USDC_ADDR).balanceOf(address(bebopExecutor)),
IERC20(USDC_ADDR).balanceOf(orderTaker) - initialUsdcBalance,
expAmountOut,
"USDC should be in executor"
"USDC should be at receiver"
);
// ETH balance check - the harness may have different balance due to test setup
// Just ensure no excessive ETH is stuck
@@ -876,202 +904,6 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
vm.stopPrank();
}
// Test exposed_getActualFilledTakerAmount function
function testGetActualFilledTakerAmount_FilledLessThanGiven() public {
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// When filledTakerAmount < givenAmount
// Should return filledTakerAmount
uint256 givenAmount = 1000e18;
uint256 filledTakerAmount = 700e18;
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, filledTakerAmount
);
assertEq(
result,
filledTakerAmount,
"Should return filledTakerAmount when less than givenAmount"
);
}
function testGetActualFilledTakerAmount_FilledGreaterThanGiven() public {
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// When filledTakerAmount > givenAmount
// Should return givenAmount (capped)
uint256 givenAmount = 500e18;
uint256 filledTakerAmount = 700e18;
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, filledTakerAmount
);
assertEq(
result,
givenAmount,
"Should return givenAmount when filledTakerAmount exceeds it"
);
}
function testGetActualFilledTakerAmount_FilledEqualsGiven() public {
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// When filledTakerAmount == givenAmount
// Should return filledTakerAmount
uint256 givenAmount = 700e18;
uint256 filledTakerAmount = 700e18;
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, filledTakerAmount
);
assertEq(
result,
filledTakerAmount,
"Should return filledTakerAmount when equal to givenAmount"
);
}
function testGetActualFilledTakerAmount_ZeroGivenAmount() public {
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// When givenAmount is 0
// Should always return 0 regardless of filledTakerAmount
uint256 givenAmount = 0;
uint256 filledTakerAmount = 100e18;
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, filledTakerAmount
);
assertEq(result, 0, "Should return 0 when givenAmount is 0");
}
function testGetActualFilledTakerAmount_ZeroFilledTakerAmount() public {
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// When filledTakerAmount is 0 (encoder should prevent this, but test edge case)
// Should return 0
uint256 givenAmount = 1000e18;
uint256 filledTakerAmount = 0;
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, filledTakerAmount
);
assertEq(result, 0, "Should return 0 when filledTakerAmount is 0");
}
function testGetActualFilledTakerAmount_SmallAmounts() public {
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Test with small amounts (e.g., for tokens with low decimals)
uint256 givenAmount = 100; // 100 units
uint256 filledTakerAmount = 50; // 50 units
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, filledTakerAmount
);
assertEq(
result, filledTakerAmount, "Should handle small amounts correctly"
);
// Test when filledTakerAmount exceeds givenAmount
filledTakerAmount = 150;
result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, filledTakerAmount
);
assertEq(
result, givenAmount, "Should cap at givenAmount for small amounts"
);
}
function testGetActualFilledTakerAmount_MaxUint256Values() public {
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Test with max uint256 values (edge case)
uint256 givenAmount = type(uint256).max;
uint256 filledTakerAmount = type(uint256).max - 1;
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, filledTakerAmount
);
assertEq(
result, filledTakerAmount, "Should handle max values correctly"
);
// Test with filledTakerAmount exceeding givenAmount
givenAmount = type(uint256).max - 100;
filledTakerAmount = type(uint256).max;
result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, filledTakerAmount
);
assertEq(
result,
givenAmount,
"Should cap at givenAmount even with max filledTakerAmount"
);
}
function testFuzzGetActualFilledTakerAmount(
uint256 givenAmount,
uint256 filledTakerAmount
) public {
// Deploy Bebop executor harness
bebopExecutor =
new BebopExecutorHarness(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
uint256 result = bebopExecutor.exposed_getActualFilledTakerAmount(
givenAmount, filledTakerAmount
);
// Verify the invariants
// Result should be min(givenAmount, filledTakerAmount)
if (filledTakerAmount > givenAmount) {
assertEq(
result,
givenAmount,
"Should return givenAmount when filledTakerAmount > givenAmount"
);
} else {
assertEq(
result,
filledTakerAmount,
"Should return filledTakerAmount when filledTakerAmount <= givenAmount"
);
}
// Result should never exceed givenAmount
assertLe(result, givenAmount, "Result should never exceed givenAmount");
// Result should never exceed filledTakerAmount
assertLe(
result,
filledTakerAmount,
"Result should never exceed filledTakerAmount"
);
}
// Test exposed_modifyFilledTakerAmount function
function testModifyFilledTakerAmount_SingleOrder() public {
// Deploy Bebop executor harness
@@ -1108,7 +940,10 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
bytes memory modifiedCalldata = bebopExecutor
.exposed_modifyFilledTakerAmount(
originalCalldata, givenAmount, originalAmountIn, 12 // partialFillOffset for swapSingle
originalCalldata,
givenAmount,
originalAmountIn,
12 // partialFillOffset for swapSingle
);
// Decode the modified calldata to verify the filledTakerAmount was updated
@@ -1184,7 +1019,10 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
bytes memory modifiedCalldata = bebopExecutor
.exposed_modifyFilledTakerAmount(
originalCalldata, givenAmount, originalAmountIn, 2 // partialFillOffset for swapAggregate
originalCalldata,
givenAmount,
originalAmountIn,
2 // partialFillOffset for swapAggregate
);
// Decode the modified calldata to verify the filledTakerAmount was updated
@@ -1243,7 +1081,10 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
// So we'll test that it properly sets the value we want
bytes memory modifiedCalldata = bebopExecutor
.exposed_modifyFilledTakerAmount(
originalCalldata, givenAmount, originalAmountIn, 12 // partialFillOffset for swapSingle
originalCalldata,
givenAmount,
originalAmountIn,
12 // partialFillOffset for swapSingle
);
// Extract the new filledTakerAmount
@@ -1259,7 +1100,10 @@ contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
// Normal test - amounts match so calldata should be unchanged
bytes memory modifiedCalldata = bebopExecutor
.exposed_modifyFilledTakerAmount(
originalCalldata, givenAmount, originalAmountIn, 12 // partialFillOffset for swapSingle
originalCalldata,
givenAmount,
originalAmountIn,
12 // partialFillOffset for swapSingle
);
assertEq(

View File

@@ -60,21 +60,13 @@ contract BebopExecutorHarness is BebopExecutor, Test {
bytes memory bebopCalldata,
uint8 partialFillOffset,
uint256 originalFilledTakerAmount,
bool approvalNeeded
bool approvalNeeded,
address receiver
)
{
return _decodeData(data);
}
// No longer needed since we inlined the logic
function exposed_getActualFilledTakerAmount(
uint256 givenAmount,
uint256 filledTakerAmount
) external pure returns (uint256 actualFilledTakerAmount) {
// Inline the simple logic here for backward compatibility
actualFilledTakerAmount = filledTakerAmount > givenAmount ? givenAmount : filledTakerAmount;
}
// Expose the internal modifyFilledTakerAmount function for testing
function exposed_modifyFilledTakerAmount(
bytes memory bebopCalldata,
@@ -83,7 +75,10 @@ contract BebopExecutorHarness is BebopExecutor, Test {
uint8 partialFillOffset
) external pure returns (bytes memory) {
return _modifyFilledTakerAmount(
bebopCalldata, givenAmount, originalFilledTakerAmount, partialFillOffset
bebopCalldata,
givenAmount,
originalFilledTakerAmount,
partialFillOffset
);
}
@@ -102,6 +97,8 @@ contract BebopExecutorHarness is BebopExecutor, Test {
bytes memory bebopCalldata,
, // partialFillOffset not needed in test harness
uint256 originalFilledTakerAmount,
, // approvalNeeded not needed in test harness
// receiver not needed since we extract it from bebop calldata
) = _decodeData(data);
// Extract taker address, receiver, and expiry from bebop calldata
@@ -139,16 +136,13 @@ contract BebopExecutorHarness is BebopExecutor, Test {
expiry = order.expiry;
}
// Inline the simple logic since _getActualFilledTakerAmount was removed
uint256 actualFilledTakerAmount =
originalFilledTakerAmount > givenAmount ? givenAmount : originalFilledTakerAmount;
uint256 actualFilledTakerAmount = originalFilledTakerAmount
> givenAmount ? givenAmount : originalFilledTakerAmount;
// For testing: transfer tokens from executor to taker address
// This simulates the taker having the tokens with approval
if (tokenIn != address(0)) {
_transfer(
address(this), transferType, tokenIn, actualFilledTakerAmount
);
// The executor already has the tokens from the test, just transfer to taker
IERC20(tokenIn).safeTransfer(takerAddress, actualFilledTakerAmount);
// Approve settlement from taker's perspective