Merge pull request #212 from pedrobergamini/feat/bebop-rfq-encoder-and-executor

feat: Bebop PMM RFQ
This commit is contained in:
dianacarvalho1
2025-08-14 10:23:50 +01:00
committed by GitHub
16 changed files with 1108 additions and 32 deletions

View File

@@ -10,7 +10,8 @@
"ekubo_v2": "0x263DD7AD20983b5E0392bf1F09C4493500EDb333",
"vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165",
"vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B",
"vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec"
"vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec",
"rfq:bebop": "0xEDCA8A3ACEB5db816d5CF833248d05Ed2784A304"
},
"base": {
"uniswap_v2": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700",

View File

@@ -5,6 +5,9 @@
},
"vm:curve": {
"native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
},
"rfq:bebop": {
"bebop_settlement_address": "0xbbbbbBB520d69a9775E85b458C58c648259FAD5F"
}
},
"base": {},

View File

@@ -10,6 +10,7 @@
"ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598",
"vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211",
"vm:maverick_v2": "0xA4AD4f68d0b91CFD19687c881e50f3A00242828c",
"vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb"
"vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb",
"rfq:bebop": "0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF"
}
}

View File

@@ -81,6 +81,11 @@ const executors_to_deploy = {
},
// Args: Permit2
{exchange: "BalancerV3Executor", args: ["0x000000000022D473030F116dDEE9F6B43aC78BA3"]},
// Args: Bebop Settlement contract, Permit2
{
exchange: "BebopExecutor",
args: ["0xbbbbbBB520d69a9775E85b458C58c648259FAD5F", "0x000000000022D473030F116dDEE9F6B43aC78BA3"]
},
],
"base": [
// Args: Factory, Pool Init Code Hash, Permit2, Fee BPS

View File

@@ -0,0 +1,182 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "@interfaces/IExecutor.sol";
import "../RestrictTransferFrom.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import {
IERC20,
SafeERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";
/// @title BebopExecutor
/// @notice Executor for Bebop PMM RFQ (Request for Quote) swaps
/// @dev Handles Single and Aggregate RFQ swaps through Bebop settlement contract
/// @dev Only supports single token in to single token out swaps
contract BebopExecutor is IExecutor, IExecutorErrors, RestrictTransferFrom {
using Math for uint256;
using SafeERC20 for IERC20;
using Address for address;
/// @notice Function selectors for Bebop settlement methods
bytes4 public constant SWAP_AGGREGATE_SELECTOR = 0xa2f74893;
/// @notice Bebop-specific errors
error BebopExecutor__InvalidDataLength();
error BebopExecutor__ZeroAddress();
/// @notice The Bebop settlement contract address
address public immutable bebopSettlement;
constructor(address _bebopSettlement, address _permit2)
RestrictTransferFrom(_permit2)
{
if (_bebopSettlement == address(0)) revert BebopExecutor__ZeroAddress();
bebopSettlement = _bebopSettlement;
}
/// @notice Executes a swap through Bebop's PMM RFQ system
/// @param givenAmount The amount of input token to swap
/// @param data Encoded swap data containing tokens and bebop calldata
/// @return calculatedAmount The amount of output token received
function swap(uint256 givenAmount, bytes calldata data)
external
payable
virtual
override
returns (uint256 calculatedAmount)
{
(
address tokenIn,
address tokenOut,
TransferType transferType,
uint8 partialFillOffset,
uint256 originalFilledTakerAmount,
bool approvalNeeded,
address receiver,
bytes memory bebopCalldata
) = _decodeData(data);
_transfer(address(this), transferType, address(tokenIn), givenAmount);
// 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
);
// Approve Bebop settlement to spend tokens if needed
if (approvalNeeded) {
// slither-disable-next-line unused-return
IERC20(tokenIn).forceApprove(bebopSettlement, type(uint256).max);
}
uint256 balanceBefore = _balanceOf(tokenOut, receiver);
uint256 ethValue = tokenIn == address(0) ? givenAmount : 0;
// Use OpenZeppelin's Address library for safe call with value
// This will revert if the call fails
// slither-disable-next-line unused-return
bebopSettlement.functionCallWithValue(finalCalldata, ethValue);
uint256 balanceAfter = _balanceOf(tokenOut, receiver);
calculatedAmount = balanceAfter - balanceBefore;
}
/// @dev Decodes the packed calldata
function _decodeData(bytes calldata data)
internal
pure
returns (
address tokenIn,
address tokenOut,
TransferType transferType,
uint8 partialFillOffset,
uint256 originalFilledTakerAmount,
bool approvalNeeded,
address receiver,
bytes memory bebopCalldata
)
{
// 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();
tokenIn = address(bytes20(data[0:20]));
tokenOut = address(bytes20(data[20:40]));
transferType = TransferType(uint8(data[40]));
partialFillOffset = uint8(data[41]);
originalFilledTakerAmount = uint256(bytes32(data[42:74]));
approvalNeeded = data[74] != 0;
receiver = address(bytes20(data[75:95]));
bebopCalldata = data[95:];
}
/// @dev Modifies the filledTakerAmount in the bebop calldata to handle slippage
/// @param bebopCalldata The original calldata for the bebop settlement
/// @param givenAmount The actual amount available from the router
/// @param originalFilledTakerAmount The original amount expected when the quote was generated
/// @param partialFillOffset The offset from Bebop API indicating where filledTakerAmount is located
/// @return The modified calldata with updated filledTakerAmount
function _modifyFilledTakerAmount(
bytes memory bebopCalldata,
uint256 givenAmount,
uint256 originalFilledTakerAmount,
uint8 partialFillOffset
) internal pure returns (bytes memory) {
// Use the offset from Bebop API to locate filledTakerAmount
// Position = 4 bytes (selector) + offset * 32 bytes
uint256 filledTakerAmountPos = 4 + uint256(partialFillOffset) * 32;
// Cap the fill amount at what we actually have available
uint256 newFilledTakerAmount = originalFilledTakerAmount > givenAmount
? givenAmount
: originalFilledTakerAmount;
// If the new filledTakerAmount is the same as the original, return the original calldata
if (newFilledTakerAmount == originalFilledTakerAmount) {
return bebopCalldata;
}
// Use assembly to modify the filledTakerAmount at the correct position
// slither-disable-next-line assembly
assembly {
// Get pointer to the data portion of the bytes array
let dataPtr := add(bebopCalldata, 0x20)
// Calculate the actual position and store the new value
let actualPos := add(dataPtr, filledTakerAmountPos)
mstore(actualPos, newFilledTakerAmount)
}
return bebopCalldata;
}
/// @dev Returns the balance of a token or ETH for an account
/// @param token The token address, or address(0) for ETH
/// @param account The account to get the balance of
/// @return balance The balance of the token or ETH for the account
function _balanceOf(address token, address account)
internal
view
returns (uint256)
{
return token == address(0)
? account.balance
: IERC20(token).balanceOf(account);
}
/**
* @dev Allow receiving ETH for settlement calls that require ETH
* This is needed when the executor handles native ETH swaps
* In production, ETH typically comes from router or settlement contracts
* In tests, it may come from EOA addresses via the test harness
*/
receive() external payable {
// Allow ETH transfers for Bebop settlement functionality
}
}

View File

@@ -19,7 +19,8 @@ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
import {IUnlockCallback} from
"@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
import {SafeCast as V4SafeCast} from
"@uniswap/v4-core/src/libraries/SafeCast.sol";
import {TransientStateLibrary} from
"@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
import "../RestrictTransferFrom.sol";
@@ -42,7 +43,7 @@ contract UniswapV4Executor is
{
using SafeERC20 for IERC20;
using CurrencyLibrary for Currency;
using SafeCast for *;
using V4SafeCast for *;
using TransientStateLibrary for IPoolManager;
IPoolManager public immutable poolManager;

View File

@@ -60,10 +60,9 @@ contract UniswapXFiller is AccessControl, IReactorCallback {
ResolvedOrder[] calldata resolvedOrders,
bytes calldata callbackData
) external onlyRole(REACTOR_ROLE) {
require(
resolvedOrders.length == 1,
UniswapXFiller__BatchExecutionNotSupported()
);
if (resolvedOrders.length != 1) {
revert UniswapXFiller__BatchExecutionNotSupported();
}
ResolvedOrder memory order = resolvedOrders[0];

View File

@@ -55,6 +55,7 @@ contract Constants is Test, BaseConstants {
address WTAO_ADDR = address(0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44);
address BSGG_ADDR = address(0xdA16Cf041E2780618c49Dbae5d734B89a6Bac9b3);
address GHO_ADDR = address(0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f);
address ONDO_ADDR = address(0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3);
// Maverick v2
address MAVERICK_V2_FACTORY = 0x0A7e848Aca42d879EF06507Fca0E7b33A0a63c1e;
@@ -124,6 +125,9 @@ contract Constants is Test, BaseConstants {
// Permit2
address PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
// Bebop Settlement
address BEBOP_SETTLEMENT = 0xbbbbbBB520d69a9775E85b458C58c648259FAD5F;
// Pool Code Init Hashes
bytes32 USV2_POOL_CODE_INIT_HASH =
0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f;

View File

@@ -3,6 +3,7 @@ pragma solidity ^0.8.26;
import "./TychoRouterTestSetup.sol";
import "./protocols/UniswapV4Utils.sol";
import "@src/executors/BebopExecutor.sol";
contract TychoRouterTestProtocolIntegration is TychoRouterTestSetup {
function testMultiProtocolIntegration() public {

View File

@@ -4,6 +4,7 @@ pragma solidity ^0.8.26;
// Executors
import {BalancerV2Executor} from "../src/executors/BalancerV2Executor.sol";
import {BalancerV3Executor} from "../src/executors/BalancerV3Executor.sol";
import {BebopExecutor} from "../src/executors/BebopExecutor.sol";
import {CurveExecutor} from "../src/executors/CurveExecutor.sol";
import {EkuboExecutor} from "../src/executors/EkuboExecutor.sol";
import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol";
@@ -73,12 +74,13 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
CurveExecutor public curveExecutor;
MaverickV2Executor public maverickv2Executor;
BalancerV3Executor public balancerV3Executor;
BebopExecutor public bebopExecutor;
function getForkBlock() public view virtual returns (uint256) {
return 22082754;
}
function setUp() public {
function setUp() public virtual {
uint256 forkBlock = getForkBlock();
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
@@ -132,8 +134,9 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
maverickv2Executor =
new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS);
balancerV3Executor = new BalancerV3Executor(PERMIT2_ADDRESS);
bebopExecutor = new BebopExecutor(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
address[] memory executors = new address[](9);
address[] memory executors = new address[](10);
executors[0] = address(usv2Executor);
executors[1] = address(usv3Executor);
executors[2] = address(pancakev3Executor);
@@ -143,6 +146,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
executors[6] = address(curveExecutor);
executors[7] = address(maverickv2Executor);
executors[8] = address(balancerV3Executor);
executors[9] = address(bebopExecutor);
return executors;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,470 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "../TestUtils.sol";
import "../TychoRouterTestSetup.sol";
import "@src/executors/BebopExecutor.sol";
import {Constants} from "../Constants.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Permit2TestHelper} from "../Permit2TestHelper.sol";
import {SafeERC20} from
"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract BebopExecutorExposed is BebopExecutor {
constructor(address _bebopSettlement, address _permit2)
BebopExecutor(_bebopSettlement, _permit2)
{}
function decodeData(bytes calldata data)
external
pure
returns (
address tokenIn,
address tokenOut,
TransferType transferType,
uint8 partialFillOffset,
uint256 originalFilledTakerAmount,
bool approvalNeeded,
address receiver,
bytes memory bebopCalldata
)
{
return _decodeData(data);
}
}
contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
using SafeERC20 for IERC20;
BebopExecutorExposed bebopExecutor;
IERC20 WETH = IERC20(WETH_ADDR);
IERC20 USDC = IERC20(USDC_ADDR);
IERC20 DAI = IERC20(DAI_ADDR);
IERC20 WBTC = IERC20(WBTC_ADDR);
IERC20 ONDO = IERC20(ONDO_ADDR);
IERC20 USDT = IERC20(USDT_ADDR);
function testDecodeData() public {
vm.createSelectFork(vm.rpcUrl("mainnet"), 22667985);
bebopExecutor =
new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
bytes memory bebopCalldata = abi.encodePacked(
bytes4(0x4dcebcba), // swapSingle selector
hex"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000068470140"
);
uint256 originalAmountIn = 200000000; // 200 USDC
bytes memory params = abi.encodePacked(
USDC_ADDR,
ONDO_ADDR,
uint8(RestrictTransferFrom.TransferType.Transfer),
uint8(2),
originalAmountIn,
true,
address(123),
bebopCalldata
);
(
address tokenIn,
address tokenOut,
RestrictTransferFrom.TransferType transferType,
uint8 decodedPartialFillOffset,
uint256 decodedOriginalAmountIn,
bool decodedApprovalNeeded,
address decodedReceiver,
bytes memory decodedBebopCalldata
) = bebopExecutor.decodeData(params);
assertEq(tokenIn, USDC_ADDR, "tokenIn mismatch");
assertEq(tokenOut, ONDO_ADDR, "tokenOut mismatch");
assertEq(
uint8(transferType),
uint8(RestrictTransferFrom.TransferType.Transfer),
"transferType mismatch"
);
assertEq(
keccak256(decodedBebopCalldata),
keccak256(bebopCalldata),
"bebopCalldata mismatch"
);
assertEq(decodedPartialFillOffset, 2, "partialFillOffset mismatch");
assertEq(
decodedOriginalAmountIn,
originalAmountIn,
"originalAmountIn mismatch"
);
assertTrue(decodedApprovalNeeded, "approvalNeeded should be true");
assertEq(decodedReceiver, address(123), "receiver mismatch");
}
// Single Order Tests
function testSingleOrder() public {
// 1 WETH -> WBTC
vm.createSelectFork(vm.rpcUrl("mainnet"), 23124275);
bebopExecutor =
new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Quote made manually using the BebopExecutor as the taker and receiver
bytes memory bebopCalldata =
hex"4dcebcba00000000000000000000000000000000000000000000000000000000689b137a0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000bee3211ab312a8d065c4fef0247448e17a8da000000000000000000000000000000000000000000000000000279ead5d9683d8a5000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000037337c0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000000000000000000000000000000000000000000000f71248bc6c123bbf12adc837470f75640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000418e9b0fb72ed9b86f7a7345026269c02b9056efcdfb67a377c7ff6c4a62a4807a7671ae759edf29aea1b2cb8efc8659e3aedac72943cd3607985a1849256358641c00000000000000000000000000000000000000000000000000000000000000";
address tokenIn = WETH_ADDR;
address tokenOut = WBTC_ADDR;
RestrictTransferFrom.TransferType transferType =
RestrictTransferFrom.TransferType.None;
uint8 partialFillOffset = 12;
uint256 amountIn = 1000000000000000000;
bool approvalNeeded = true;
uint256 expectedAmountOut = 3617660;
deal(tokenIn, address(bebopExecutor), amountIn);
bytes memory params = abi.encodePacked(
tokenIn,
tokenOut,
transferType,
partialFillOffset,
amountIn,
approvalNeeded,
address(bebopExecutor),
bebopCalldata
);
uint256 initialTokenOutBalance =
IERC20(tokenOut).balanceOf(address(bebopExecutor));
uint256 amountOut = bebopExecutor.swap(amountIn, params);
assertEq(amountOut, expectedAmountOut, "Incorrect amount out");
assertEq(
IERC20(tokenOut).balanceOf(address(bebopExecutor))
- initialTokenOutBalance,
expectedAmountOut,
"WBTC should be at receiver"
);
assertEq(
IERC20(tokenIn).balanceOf(address(bebopExecutor)),
0,
"WETH left in executor"
);
}
function testSingleOrderSellingETH() public {
// 1 WETH -> WBTC
vm.createSelectFork(vm.rpcUrl("mainnet"), 23124275);
bebopExecutor =
new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Quote made manually using the BebopExecutor as the taker and receiver
bytes memory bebopCalldata =
hex"4dcebcba00000000000000000000000000000000000000000000000000000000689ca0cd0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f0000000000000000000000000000000000000000000000002a65384e77863d8e000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000003a96a10000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000000000000000000000000000000000000000000001c6d9e514c7a64e5c0e239b532e1a3ea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041905d474b362c4a7c901c6a4ccb5c30670a0c602456f52761b47a0a35fc3944ec1fa224bc3bc6e8925cb15258efad2cf79e22ce9720f2302d4a1a2811c54fb4341c00000000000000000000000000000000000000000000000000000000000000";
address tokenIn = address(0);
address tokenOut = WBTC_ADDR;
RestrictTransferFrom.TransferType transferType =
RestrictTransferFrom.TransferType.None;
uint8 partialFillOffset = 12;
uint256 amountIn = 1000000000000000000;
bool approvalNeeded = false;
uint256 expectedAmountOut = 3839649;
vm.deal(address(bebopExecutor), amountIn);
bytes memory params = abi.encodePacked(
tokenIn,
tokenOut,
transferType,
partialFillOffset,
amountIn,
approvalNeeded,
address(bebopExecutor),
bebopCalldata
);
uint256 initialTokenOutBalance =
IERC20(tokenOut).balanceOf(address(bebopExecutor));
uint256 amountOut = bebopExecutor.swap(amountIn, params);
assertEq(amountOut, expectedAmountOut, "Incorrect amount out");
assertEq(
IERC20(tokenOut).balanceOf(address(bebopExecutor))
- initialTokenOutBalance,
expectedAmountOut,
"WBTC should be at receiver"
);
assertEq(address(bebopExecutor).balance, 0, "ETH left in executor");
}
function testSingleOrder_PartialFill() public {
// 0.5 WETH -> WBTC with a quote for 1 WETH
vm.createSelectFork(vm.rpcUrl("mainnet"), 23124275);
bebopExecutor =
new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Quote made manually using the BebopExecutor as the taker and receiver (the same as testSingleOrder)
bytes memory bebopCalldata =
hex"4dcebcba00000000000000000000000000000000000000000000000000000000689b137a0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000bee3211ab312a8d065c4fef0247448e17a8da000000000000000000000000000000000000000000000000000279ead5d9683d8a5000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000037337c0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000000000000000000000000000000000000000000000f71248bc6c123bbf12adc837470f75640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000418e9b0fb72ed9b86f7a7345026269c02b9056efcdfb67a377c7ff6c4a62a4807a7671ae759edf29aea1b2cb8efc8659e3aedac72943cd3607985a1849256358641c00000000000000000000000000000000000000000000000000000000000000";
address tokenIn = WETH_ADDR;
address tokenOut = WBTC_ADDR;
RestrictTransferFrom.TransferType transferType =
RestrictTransferFrom.TransferType.None;
uint8 partialFillOffset = 12;
// filling only half of the quote
uint256 amountIn = 1000000000000000000 / 2;
bool approvalNeeded = true;
uint256 expectedAmountOut = 3617660 / 2;
deal(tokenIn, address(bebopExecutor), amountIn);
bytes memory params = abi.encodePacked(
tokenIn,
tokenOut,
transferType,
partialFillOffset,
amountIn * 2, // this is the original amount in
approvalNeeded,
address(bebopExecutor),
bebopCalldata
);
uint256 initialTokenOutBalance =
IERC20(tokenOut).balanceOf(address(bebopExecutor));
uint256 amountOut = bebopExecutor.swap(amountIn, params);
assertEq(amountOut, expectedAmountOut, "Incorrect partial amount out");
assertEq(
IERC20(tokenOut).balanceOf(address(bebopExecutor))
- initialTokenOutBalance,
expectedAmountOut,
"WETH should be at receiver"
);
assertEq(
IERC20(tokenIn).balanceOf(address(bebopExecutor)),
0,
"WBTC left in executor"
);
}
// Aggregate Order Tests
function testAggregateOrder() public {
// 20k USDC -> ONDO
vm.createSelectFork(vm.rpcUrl("mainnet"), 23126278);
bebopExecutor =
new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Quote made manually using the BebopExecutor as the taker and receiver
bytes memory bebopCalldata =
hex"a2f7489300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000689b715d0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c00000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000005a0e0c07568b14a2d2c1b4d196000fc12bc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000002a65384e777abcfe0000000000000000000000000000000000000000000000002a65384e777abcff0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be300000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000236ddb7a7000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000002713a105900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000001e7dc63f0c1d9d93df4000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000021960567af238bcfd0000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041275c4b7c3df4bfa5c33da3443d817cc6ab568ec8b0fddc30445adff2e870cdcd7d8738e23b795c2fb1ee112e12716bcef1cf648bd1ded17ef10ae493d687322e1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004187ef3d632a640b09df5f39b2fb4c5b9afb7ab4f2782fee450b17e2363d27303b45ec55b154a63993106bfc28bb4accc10fb40f7927509fed554fac01a5d88bae1c00000000000000000000000000000000000000000000000000000000000000";
address tokenIn = USDC_ADDR;
address tokenOut = ONDO_ADDR;
RestrictTransferFrom.TransferType transferType =
RestrictTransferFrom.TransferType.None;
uint8 partialFillOffset = 2;
// filling only half of the quote
uint256 amountIn = 20000000000;
bool approvalNeeded = true;
// maker amounts from quote
uint256 expectedAmountOut =
(8999445165322964385268 + 9912843438638420000000);
deal(tokenIn, address(bebopExecutor), amountIn);
bytes memory params = abi.encodePacked(
tokenIn,
tokenOut,
transferType,
partialFillOffset,
amountIn,
approvalNeeded,
address(bebopExecutor),
bebopCalldata
);
uint256 initialTokenOutBalance =
IERC20(tokenOut).balanceOf(address(bebopExecutor));
uint256 amountOut = bebopExecutor.swap(amountIn, params);
assertEq(amountOut, expectedAmountOut, "Incorrect amount out");
assertEq(
IERC20(tokenOut).balanceOf(address(bebopExecutor))
- initialTokenOutBalance,
expectedAmountOut,
"ONDO should be at receiver"
);
assertEq(
IERC20(tokenIn).balanceOf(address(bebopExecutor)),
0,
"USDC left in executor"
);
}
function testAggregateOrder_PartialFill() public {
// 10k USDC -> ONDO with a quote for 20k USDC
vm.createSelectFork(vm.rpcUrl("mainnet"), 23126278);
bebopExecutor =
new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Quote made manually using the BebopExecutor as the taker and receiver
bytes memory bebopCalldata =
hex"a2f7489300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000689b715d0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c00000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000005a0e0c07568b14a2d2c1b4d196000fc12bc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000002a65384e777abcfe0000000000000000000000000000000000000000000000002a65384e777abcff0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be300000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000236ddb7a7000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000002713a105900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000001e7dc63f0c1d9d93df4000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000021960567af238bcfd0000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041275c4b7c3df4bfa5c33da3443d817cc6ab568ec8b0fddc30445adff2e870cdcd7d8738e23b795c2fb1ee112e12716bcef1cf648bd1ded17ef10ae493d687322e1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004187ef3d632a640b09df5f39b2fb4c5b9afb7ab4f2782fee450b17e2363d27303b45ec55b154a63993106bfc28bb4accc10fb40f7927509fed554fac01a5d88bae1c00000000000000000000000000000000000000000000000000000000000000";
address tokenIn = USDC_ADDR;
address tokenOut = ONDO_ADDR;
RestrictTransferFrom.TransferType transferType =
RestrictTransferFrom.TransferType.None;
uint8 partialFillOffset = 2;
// filling only half of the quote
uint256 amountIn = 20000000000 / 2;
bool approvalNeeded = true;
// maker amounts from quote
uint256 expectedAmountOut =
(8999445165322964385268 + 9912843438638420000000) / 2;
deal(tokenIn, address(bebopExecutor), amountIn);
bytes memory params = abi.encodePacked(
tokenIn,
tokenOut,
transferType,
partialFillOffset,
amountIn * 2, // this is the original amount from the quote
approvalNeeded,
address(bebopExecutor),
bebopCalldata
);
uint256 initialTokenOutBalance =
IERC20(tokenOut).balanceOf(address(bebopExecutor));
uint256 amountOut = bebopExecutor.swap(amountIn, params);
assertEq(amountOut, expectedAmountOut, "Incorrect amount out");
assertEq(
IERC20(tokenOut).balanceOf(address(bebopExecutor))
- initialTokenOutBalance,
expectedAmountOut,
"ONDO should be at receiver"
);
assertEq(
IERC20(tokenIn).balanceOf(address(bebopExecutor)),
1, // because of integer division, there is 1 USDC left in the executor
"USDC left in executor"
);
}
function testInvalidDataLength() public {
vm.createSelectFork(vm.rpcUrl("mainnet"), 22667985);
bebopExecutor =
new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Create a mock bebop calldata
bytes memory bebopCalldata = hex"47fb5891" // swapSingle selector
hex"1234567890abcdef"; // some mock data
// Create params with correct length first
uint256 originalAmountIn = 1e18;
bytes memory validParams = abi.encodePacked(
WETH_ADDR,
USDC_ADDR,
uint8(RestrictTransferFrom.TransferType.Transfer),
uint8(2),
originalAmountIn,
true,
address(bebopExecutor),
bebopCalldata
);
// Verify valid params work
bebopExecutor.decodeData(validParams);
// In the new format, adding extra bytes at the end doesn't fail
// because bebopCalldata is variable length at the end
// So test with extra bytes should not revert
bytes memory paramsWithExtra = abi.encodePacked(validParams, hex"ff");
// This should work as the extra byte becomes part of bebopCalldata
bebopExecutor.decodeData(paramsWithExtra);
// 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.decodeData(tooShortParams);
}
}
contract TychoRouterForBebopTest is TychoRouterTestSetup {
// Override the fork block for Bebop tests
function getForkBlock() public pure override returns (uint256) {
return 22667986;
}
function testSingleBebopIntegration() public {
// The calldata swaps 200 USDC for ONDO
address user = 0xd2068e04Cf586f76EEcE7BA5bEB779D7bB1474A1;
deal(USDC_ADDR, user, 200000000); // 200 USDC
uint256 expAmountOut = 194477331556159832309; // Expected ONDO amount from quote
uint256 ondoBefore = IERC20(ONDO_ADDR).balanceOf(user);
vm.startPrank(user);
IERC20(USDC_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_single_encoding_strategy_bebop");
(bool success,) = tychoRouterAddr.call(callData);
assertTrue(success, "Call Failed");
uint256 ondoReceived = IERC20(ONDO_ADDR).balanceOf(user) - ondoBefore;
assertEq(ondoReceived, expAmountOut);
assertEq(
IERC20(USDC_ADDR).balanceOf(tychoRouterAddr),
0,
"USDC left in router"
);
vm.stopPrank();
}
function testBebopAggregateIntegration() public {
// The calldata swaps 20k USDC for ONDO using multiple market makers
address user = 0xd2068e04Cf586f76EEcE7BA5bEB779D7bB1474A1;
deal(USDC_ADDR, user, 20000000000); // 20k USDC
uint256 expAmountOut = 18699321819466078474202; // Expected ONDO amount from quote
uint256 ondoBefore = IERC20(ONDO_ADDR).balanceOf(user);
vm.startPrank(user);
IERC20(USDC_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData = loadCallDataFromFile(
"test_single_encoding_strategy_bebop_aggregate"
);
(bool success,) = tychoRouterAddr.call(callData);
assertTrue(success, "Call Failed");
uint256 ondoReceived = IERC20(ONDO_ADDR).balanceOf(user) - ondoBefore;
assertEq(ondoReceived, expAmountOut);
assertEq(
IERC20(USDC_ADDR).balanceOf(tychoRouterAddr),
0,
"USDC left in router"
);
vm.stopPrank();
}
}

View File

@@ -5,8 +5,9 @@ use tycho_common::models::Chain;
use crate::encoding::{
errors::EncodingError,
evm::swap_encoder::swap_encoders::{
BalancerV2SwapEncoder, BalancerV3SwapEncoder, CurveSwapEncoder, EkuboSwapEncoder,
MaverickV2SwapEncoder, UniswapV2SwapEncoder, UniswapV3SwapEncoder, UniswapV4SwapEncoder,
BalancerV2SwapEncoder, BalancerV3SwapEncoder, BebopSwapEncoder, CurveSwapEncoder,
EkuboSwapEncoder, MaverickV2SwapEncoder, UniswapV2SwapEncoder, UniswapV3SwapEncoder,
UniswapV4SwapEncoder,
},
swap_encoder::SwapEncoder,
};
@@ -87,6 +88,9 @@ impl SwapEncoderBuilder {
self.chain,
self.config,
)?)),
"rfq:bebop" => {
Ok(Box::new(BebopSwapEncoder::new(self.executor_address, self.chain, self.config)?))
}
_ => Err(EncodingError::FatalError(format!(
"Unknown protocol system: {}",
self.protocol_system

View File

@@ -1,7 +1,7 @@
use std::{collections::HashMap, str::FromStr};
use alloy::{
primitives::{Address, Bytes as AlloyBytes, U8},
primitives::{Address, Bytes as AlloyBytes, U256, U8},
sol_types::SolValue,
};
use serde_json::from_str;
@@ -634,6 +634,117 @@ impl SwapEncoder for BalancerV3SwapEncoder {
fn executor_address(&self) -> &str {
&self.executor_address
}
fn clone_box(&self) -> Box<dyn SwapEncoder> {
Box::new(self.clone())
}
}
/// Encodes a swap on Bebop (PMM RFQ) through the given executor address.
///
/// Bebop uses a Request-for-Quote model where quotes are obtained off-chain
/// and settled on-chain. This encoder supports PMM RFQ execution.
///
/// # Fields
/// * `executor_address` - The address of the executor contract that will perform the swap.
/// * `settlement_address` - The address of the Bebop settlement contract.
#[derive(Clone)]
pub struct BebopSwapEncoder {
executor_address: String,
settlement_address: String,
}
impl SwapEncoder for BebopSwapEncoder {
fn new(
executor_address: String,
_chain: Chain,
config: Option<HashMap<String, String>>,
) -> Result<Self, EncodingError> {
let config = config.ok_or(EncodingError::FatalError(
"Missing bebop specific addresses in config".to_string(),
))?;
let settlement_address = config
.get("bebop_settlement_address")
.ok_or(EncodingError::FatalError(
"Missing bebop settlement address in config".to_string(),
))?
.to_string();
Ok(Self { executor_address, settlement_address })
}
fn encode_swap(
&self,
swap: &Swap,
encoding_context: &EncodingContext,
) -> Result<Vec<u8>, EncodingError> {
let token_in = bytes_to_address(&swap.token_in)?;
let token_out = bytes_to_address(&swap.token_out)?;
let token_approvals_manager = ProtocolApprovalsManager::new()?;
let approval_needed: bool;
if let Some(router_address) = &encoding_context.router_address {
let tycho_router_address = bytes_to_address(router_address)?;
let token_to_approve = token_in;
let settlement_address = Address::from_str(&self.settlement_address)
.map_err(|_| EncodingError::FatalError("Invalid settlement address".to_string()))?;
// Native ETH doesn't need approval, only ERC20 tokens do
if token_to_approve == Address::ZERO {
approval_needed = false;
} else {
approval_needed = token_approvals_manager.approval_needed(
token_to_approve,
tycho_router_address,
settlement_address,
)?;
}
} else {
approval_needed = true;
}
// The user data required for Bebop is
// partial_fill_offset (u8) | original_taker_amount (U256) | calldata (bytes (selector ABI
// encoded params))
let user_data = swap.user_data.clone().ok_or_else(|| {
EncodingError::InvalidInput("Bebop swaps require user_data with calldata".to_string())
})?;
if user_data.len() < 37 {
return Err(EncodingError::InvalidInput(
"User data too short to contain offset and Bebop calldata".to_string(),
));
}
let partial_fill_offset = user_data[0];
let original_filled_taker_amount = U256::from_be_slice(&user_data[1..33]);
// The calldata should be for either swapSingle or swapAggregate
let bebop_calldata = user_data[33..].to_vec();
let receiver = bytes_to_address(&encoding_context.receiver)?;
// Encode packed data for the executor
// Format: token_in | token_out | transfer_type | partial_fill_offset |
// original_filled_taker_amount | approval_needed | receiver | bebop_calldata
let args = (
token_in,
token_out,
(encoding_context.transfer_type as u8).to_be_bytes(),
partial_fill_offset.to_be_bytes(),
original_filled_taker_amount.to_be_bytes::<32>(),
(approval_needed as u8).to_be_bytes(),
receiver,
&bebop_calldata[..],
);
Ok(args.abi_encode_packed())
}
fn executor_address(&self) -> &str {
&self.executor_address
}
fn clone_box(&self) -> Box<dyn SwapEncoder> {
Box::new(self.clone())
}
@@ -1610,4 +1721,144 @@ mod tests {
write_calldata_to_file("test_encode_maverick_v2", hex_swap.as_str());
}
}
mod bebop {
use super::*;
#[test]
fn test_encode_bebop_single() {
// 200 USDC -> ONDO
let bebop_calldata= Bytes::from_str("0x4dcebcba00000000000000000000000000000000000000000000000000000000689b548f0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000067336cec42645f55059eff241cb02ea5cc52ff86000000000000000000000000000000000000000000000000279ead5d9685f25b000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be3000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000a8aea46aa4ec5c0f5000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000000005230bcb979c81cebf94a3b5c08bcfa300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000414ce40058ff07f11d9224c2c8d1e58369e4a90173856202d8d2a17da48058ad683dedb742eda0d4c0cf04cf1c09138898dd7fd06f97268ea7f74ef9b42d29bf4c1b00000000000000000000000000000000000000000000000000000000000000").unwrap();
let original_taker_amount = U256::from_str("200000000").unwrap();
// partialFillOffset 12 for swapSingle
let mut user_data = vec![12u8];
user_data.extend_from_slice(&original_taker_amount.to_be_bytes::<32>());
user_data.extend_from_slice(&bebop_calldata);
let bebop_component = ProtocolComponent {
id: String::from("bebop-rfq"),
protocol_system: String::from("rfq:bebop"),
static_attributes: HashMap::new(),
..Default::default()
};
let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC
let token_out = Bytes::from("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3"); // ONDO
let swap = SwapBuilder::new(bebop_component, token_in.clone(), token_out.clone())
.user_data(Bytes::from(user_data))
.build();
let encoding_context = EncodingContext {
receiver: Bytes::from("0xc5564C13A157E6240659fb81882A28091add8670"),
exact_out: false,
router_address: Some(Bytes::zero(20)),
group_token_in: token_in.clone(),
group_token_out: token_out.clone(),
transfer_type: TransferType::Transfer,
};
let encoder = BebopSwapEncoder::new(
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
Chain::Ethereum,
Some(HashMap::from([(
"bebop_settlement_address".to_string(),
"0xbbbbbBB520d69a9775E85b458C58c648259FAD5F".to_string(),
)])),
)
.unwrap();
let encoded_swap = encoder
.encode_swap(&swap, &encoding_context)
.unwrap();
let hex_swap = encode(&encoded_swap);
let expected_swap = String::from(concat!(
// token in
"a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
// token out
"faba6f8e4a5e8ab82f62fe7c39859fa577269be3",
// transfer type
"01",
// partiall filled offset
"0c",
// original taker amount
"000000000000000000000000000000000000000000000000000000000bebc200",
// approval needed
"01",
//receiver,
"c5564c13a157e6240659fb81882a28091add8670",
));
assert_eq!(hex_swap, expected_swap + &bebop_calldata.to_string()[2..]);
}
#[test]
fn test_encode_bebop_aggregate() {
// 20k USDC -> ONDO
let bebop_calldata= Bytes::from_str("0xa2f7489300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000689b78880000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c0000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000005a060a5c2aaaaa2fe2cda34423cac76a84c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000002901f2d62bb356ca0000000000000000000000000000000000000000000000002901f2d62bb356cb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000044f83c726000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000589400da00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000003aa5f96046644f6e37a000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000004b51a26526ddbeec60000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000417ab4332f2b091d87d56d04eee35dd49452782c782de71608c0425c5ae41f1d7e147173851c870d76720ce07d45cd8622352716b1c7965819ee2bf8c573c499ae1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000410c8da2637aa929e11caff9afdfc4c489320c6dba77cc934d88ba8956e365fd1d48983087c6e474bbb828181cdfdd17317c4c9c3ee4bc98e3769d0c05cc7a285e1c00000000000000000000000000000000000000000000000000000000000000").unwrap();
let original_taker_amount = U256::from_str("20000000000").unwrap();
// partialFillOffset is 2 for swapAggregate
let mut user_data = vec![2u8];
user_data.extend_from_slice(&original_taker_amount.to_be_bytes::<32>());
user_data.extend_from_slice(&bebop_calldata);
let bebop_component = ProtocolComponent {
id: String::from("bebop-rfq"),
protocol_system: String::from("rfq:bebop"),
static_attributes: HashMap::new(),
..Default::default()
};
let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC
let token_out = Bytes::from("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3"); // ONDO
let swap = SwapBuilder::new(bebop_component, token_in.clone(), token_out.clone())
.user_data(Bytes::from(user_data))
.build();
let encoding_context = EncodingContext {
receiver: Bytes::from("0xc5564C13A157E6240659fb81882A28091add8670"),
exact_out: false,
router_address: Some(Bytes::zero(20)),
group_token_in: token_in.clone(),
group_token_out: token_out.clone(),
transfer_type: TransferType::Transfer,
};
let encoder = BebopSwapEncoder::new(
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
Chain::Ethereum,
Some(HashMap::from([(
"bebop_settlement_address".to_string(),
"0xbbbbbBB520d69a9775E85b458C58c648259FAD5F".to_string(),
)])),
)
.unwrap();
let encoded_swap = encoder
.encode_swap(&swap, &encoding_context)
.unwrap();
let hex_swap = encode(&encoded_swap);
let expected_swap = String::from(concat!(
// token in
"a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
// token out
"faba6f8e4a5e8ab82f62fe7c39859fa577269be3",
// transfer type
"01",
// partiall filled offset
"02",
// original taker amount
"00000000000000000000000000000000000000000000000000000004a817c800",
// approval needed
"01",
//receiver,
"c5564c13a157e6240659fb81882a28091add8670",
));
assert_eq!(hex_swap, expected_swap + &bebop_calldata.to_string()[2..]);
}
}
}

View File

@@ -3,7 +3,10 @@ pub mod encoding;
use std::str::FromStr;
use alloy::{primitives::B256, signers::local::PrivateKeySigner};
use alloy::{
primitives::{B256, U256},
signers::local::PrivateKeySigner,
};
use tycho_common::{models::Chain, Bytes};
use tycho_execution::encoding::{
evm::encoder_builders::TychoRouterEncoderBuilder, models::UserTransferType,
@@ -46,6 +49,10 @@ pub fn usdt() -> Bytes {
Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap()
}
pub fn ondo() -> Bytes {
Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap()
}
pub fn get_signer() -> PrivateKeySigner {
// Set up a mock private key for signing (Alice's pk in our contract tests)
let private_key =
@@ -64,3 +71,19 @@ pub fn get_tycho_router_encoder(user_transfer_type: UserTransferType) -> Box<dyn
.build()
.expect("Failed to build encoder")
}
/// Builds the complete Bebop calldata in the format expected by the encoder
/// Returns: [ partial_fill_offset (u8) | original_taker_amount (U256) | calldata (bytes (selector +
/// ABI encoded params)) ]
pub fn build_bebop_calldata(
calldata: &[u8],
partial_fill_offset: u8,
original_taker_amount: U256,
) -> Bytes {
let mut user_data = Vec::with_capacity(1 + 32 + calldata.len());
user_data.push(partial_fill_offset);
user_data.extend_from_slice(&original_taker_amount.to_be_bytes::<32>());
user_data.extend_from_slice(calldata);
Bytes::from(user_data)
}

View File

@@ -1,18 +1,19 @@
mod common;
use std::{collections::HashMap, str::FromStr};
use alloy::hex::encode;
use alloy::{hex, hex::encode};
use num_bigint::{BigInt, BigUint};
use tycho_common::{models::protocol::ProtocolComponent, Bytes};
use tycho_execution::encoding::{
evm::utils::write_calldata_to_file,
models::{Solution, Swap, UserTransferType},
evm::utils::{biguint_to_u256, write_calldata_to_file},
models::{Solution, Swap, SwapBuilder, UserTransferType},
};
use crate::common::{
encoding::encode_tycho_router_call, eth, eth_chain, get_signer, get_tycho_router_encoder, pepe,
usdc, weth,
build_bebop_calldata, encoding::encode_tycho_router_call, eth, eth_chain, get_signer,
get_tycho_router_encoder, ondo, pepe, usdc, weth,
};
#[test]
fn test_single_encoding_strategy_ekubo() {
// ETH ──(EKUBO)──> USDC
@@ -590,3 +591,126 @@ fn test_single_encoding_strategy_balancer_v3() {
let hex_calldata = encode(&calldata);
write_calldata_to_file("test_single_encoding_strategy_balancer_v3", hex_calldata.as_str());
}
#[test]
fn test_single_encoding_strategy_bebop() {
// The quote was done separately where the sender is the router and the receiver is a random
// user
let _router = Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap();
let user = Bytes::from_str("0xd2068e04cf586f76eece7ba5beb779d7bb1474a1").unwrap();
let token_in = usdc();
let token_out = ondo();
let amount_in = BigUint::from_str("200000000").unwrap(); // 200 USDC
let amount_out = BigUint::from_str("194477331556159832309").unwrap(); // 203.8 ONDO
let partial_fill_offset = 12;
let calldata = Bytes::from_str("0x4dcebcba00000000000000000000000000000000000000000000000000000000689b548f0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000067336cec42645f55059eff241cb02ea5cc52ff86000000000000000000000000000000000000000000000000279ead5d9685f25b000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be3000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000a8aea46aa4ec5c0f5000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000000005230bcb979c81cebf94a3b5c08bcfa300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000414ce40058ff07f11d9224c2c8d1e58369e4a90173856202d8d2a17da48058ad683dedb742eda0d4c0cf04cf1c09138898dd7fd06f97268ea7f74ef9b42d29bf4c1b00000000000000000000000000000000000000000000000000000000000000").unwrap();
let user_data =
build_bebop_calldata(&calldata, partial_fill_offset, biguint_to_u256(&amount_in));
let bebop_component = ProtocolComponent {
id: String::from("bebop-rfq"),
protocol_system: String::from("rfq:bebop"),
static_attributes: HashMap::new(), // No static attributes needed
..Default::default()
};
let swap = SwapBuilder::new(bebop_component, token_in.clone(), token_out.clone())
.user_data(user_data)
.build();
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let solution = Solution {
exact_out: false,
given_token: token_in,
given_amount: amount_in,
checked_token: token_out,
checked_amount: amount_out, // Expected output amount
sender: user.clone(),
receiver: user,
swaps: vec![swap],
..Default::default()
};
let encoded_solution = encoder
.encode_solutions(vec![solution.clone()])
.unwrap()[0]
.clone();
let calldata = encode_tycho_router_call(
eth_chain().id(),
encoded_solution,
&solution,
&UserTransferType::TransferFrom,
&eth(),
None,
)
.unwrap()
.data;
let hex_calldata = hex::encode(&calldata);
write_calldata_to_file("test_single_encoding_strategy_bebop", hex_calldata.as_str());
}
#[test]
fn test_single_encoding_strategy_bebop_aggregate() {
// The quote was done separately where the sender is the router and the receiver is a random
// user
let _router = Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap();
let user = Bytes::from_str("0xd2068e04cf586f76eece7ba5beb779d7bb1474a1").unwrap();
let token_in = usdc();
let token_out = ondo();
let amount_in = BigUint::from_str("20000000000").unwrap(); // 20k USDC
let amount_out = BigUint::from_str("18699321819466078474202").unwrap(); // 203.8 ONDO
let partial_fill_offset = 2;
let calldata = Bytes::from_str("0xa2f7489300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000689b78880000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c0000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000005a060a5c2aaaaa2fe2cda34423cac76a84c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000002901f2d62bb356ca0000000000000000000000000000000000000000000000002901f2d62bb356cb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000044f83c726000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000589400da00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000003aa5f96046644f6e37a000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000004b51a26526ddbeec60000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000417ab4332f2b091d87d56d04eee35dd49452782c782de71608c0425c5ae41f1d7e147173851c870d76720ce07d45cd8622352716b1c7965819ee2bf8c573c499ae1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000410c8da2637aa929e11caff9afdfc4c489320c6dba77cc934d88ba8956e365fd1d48983087c6e474bbb828181cdfdd17317c4c9c3ee4bc98e3769d0c05cc7a285e1c00000000000000000000000000000000000000000000000000000000000000").unwrap();
let user_data =
build_bebop_calldata(&calldata, partial_fill_offset, biguint_to_u256(&amount_in));
let bebop_component = ProtocolComponent {
id: String::from("bebop-rfq"),
protocol_system: String::from("rfq:bebop"),
static_attributes: HashMap::new(),
..Default::default()
};
let swap = SwapBuilder::new(bebop_component, token_in.clone(), token_out.clone())
.user_data(user_data)
.build();
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let solution = Solution {
exact_out: false,
given_token: token_in.clone(),
given_amount: amount_in,
checked_token: token_out,
checked_amount: amount_out,
sender: user.clone(),
receiver: user,
swaps: vec![swap],
..Default::default()
};
let encoded_solution = encoder
.encode_solutions(vec![solution.clone()])
.unwrap()[0]
.clone();
let calldata = encode_tycho_router_call(
eth_chain().id(),
encoded_solution,
&solution,
&UserTransferType::TransferFrom,
&eth(),
None,
)
.unwrap()
.data;
let hex_calldata = hex::encode(&calldata);
write_calldata_to_file("test_single_encoding_strategy_bebop_aggregate", hex_calldata.as_str());
}