Merge pull request #212 from pedrobergamini/feat/bebop-rfq-encoder-and-executor
feat: Bebop PMM RFQ
This commit is contained in:
@@ -10,7 +10,8 @@
|
|||||||
"ekubo_v2": "0x263DD7AD20983b5E0392bf1F09C4493500EDb333",
|
"ekubo_v2": "0x263DD7AD20983b5E0392bf1F09C4493500EDb333",
|
||||||
"vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165",
|
"vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165",
|
||||||
"vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B",
|
"vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B",
|
||||||
"vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec"
|
"vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec",
|
||||||
|
"rfq:bebop": "0xEDCA8A3ACEB5db816d5CF833248d05Ed2784A304"
|
||||||
},
|
},
|
||||||
"base": {
|
"base": {
|
||||||
"uniswap_v2": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700",
|
"uniswap_v2": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700",
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
},
|
},
|
||||||
"vm:curve": {
|
"vm:curve": {
|
||||||
"native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
"native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
||||||
|
},
|
||||||
|
"rfq:bebop": {
|
||||||
|
"bebop_settlement_address": "0xbbbbbBB520d69a9775E85b458C58c648259FAD5F"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"base": {},
|
"base": {},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598",
|
"ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598",
|
||||||
"vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211",
|
"vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211",
|
||||||
"vm:maverick_v2": "0xA4AD4f68d0b91CFD19687c881e50f3A00242828c",
|
"vm:maverick_v2": "0xA4AD4f68d0b91CFD19687c881e50f3A00242828c",
|
||||||
"vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb"
|
"vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb",
|
||||||
|
"rfq:bebop": "0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ const executors_to_deploy = {
|
|||||||
},
|
},
|
||||||
// Args: Permit2
|
// Args: Permit2
|
||||||
{exchange: "BalancerV3Executor", args: ["0x000000000022D473030F116dDEE9F6B43aC78BA3"]},
|
{exchange: "BalancerV3Executor", args: ["0x000000000022D473030F116dDEE9F6B43aC78BA3"]},
|
||||||
|
// Args: Bebop Settlement contract, Permit2
|
||||||
|
{
|
||||||
|
exchange: "BebopExecutor",
|
||||||
|
args: ["0xbbbbbBB520d69a9775E85b458C58c648259FAD5F", "0x000000000022D473030F116dDEE9F6B43aC78BA3"]
|
||||||
|
},
|
||||||
],
|
],
|
||||||
"base": [
|
"base": [
|
||||||
// Args: Factory, Pool Init Code Hash, Permit2, Fee BPS
|
// Args: Factory, Pool Init Code Hash, Permit2, Fee BPS
|
||||||
|
|||||||
182
foundry/src/executors/BebopExecutor.sol
Normal file
182
foundry/src/executors/BebopExecutor.sol
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
|
||||||
import {IUnlockCallback} from
|
import {IUnlockCallback} from
|
||||||
"@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
|
"@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
|
import {TransientStateLibrary} from
|
||||||
"@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
|
"@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
|
||||||
import "../RestrictTransferFrom.sol";
|
import "../RestrictTransferFrom.sol";
|
||||||
@@ -42,7 +43,7 @@ contract UniswapV4Executor is
|
|||||||
{
|
{
|
||||||
using SafeERC20 for IERC20;
|
using SafeERC20 for IERC20;
|
||||||
using CurrencyLibrary for Currency;
|
using CurrencyLibrary for Currency;
|
||||||
using SafeCast for *;
|
using V4SafeCast for *;
|
||||||
using TransientStateLibrary for IPoolManager;
|
using TransientStateLibrary for IPoolManager;
|
||||||
|
|
||||||
IPoolManager public immutable poolManager;
|
IPoolManager public immutable poolManager;
|
||||||
|
|||||||
@@ -60,10 +60,9 @@ contract UniswapXFiller is AccessControl, IReactorCallback {
|
|||||||
ResolvedOrder[] calldata resolvedOrders,
|
ResolvedOrder[] calldata resolvedOrders,
|
||||||
bytes calldata callbackData
|
bytes calldata callbackData
|
||||||
) external onlyRole(REACTOR_ROLE) {
|
) external onlyRole(REACTOR_ROLE) {
|
||||||
require(
|
if (resolvedOrders.length != 1) {
|
||||||
resolvedOrders.length == 1,
|
revert UniswapXFiller__BatchExecutionNotSupported();
|
||||||
UniswapXFiller__BatchExecutionNotSupported()
|
}
|
||||||
);
|
|
||||||
|
|
||||||
ResolvedOrder memory order = resolvedOrders[0];
|
ResolvedOrder memory order = resolvedOrders[0];
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ contract Constants is Test, BaseConstants {
|
|||||||
address WTAO_ADDR = address(0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44);
|
address WTAO_ADDR = address(0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44);
|
||||||
address BSGG_ADDR = address(0xdA16Cf041E2780618c49Dbae5d734B89a6Bac9b3);
|
address BSGG_ADDR = address(0xdA16Cf041E2780618c49Dbae5d734B89a6Bac9b3);
|
||||||
address GHO_ADDR = address(0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f);
|
address GHO_ADDR = address(0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f);
|
||||||
|
address ONDO_ADDR = address(0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3);
|
||||||
|
|
||||||
// Maverick v2
|
// Maverick v2
|
||||||
address MAVERICK_V2_FACTORY = 0x0A7e848Aca42d879EF06507Fca0E7b33A0a63c1e;
|
address MAVERICK_V2_FACTORY = 0x0A7e848Aca42d879EF06507Fca0E7b33A0a63c1e;
|
||||||
@@ -124,6 +125,9 @@ contract Constants is Test, BaseConstants {
|
|||||||
// Permit2
|
// Permit2
|
||||||
address PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
|
address PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
|
||||||
|
|
||||||
|
// Bebop Settlement
|
||||||
|
address BEBOP_SETTLEMENT = 0xbbbbbBB520d69a9775E85b458C58c648259FAD5F;
|
||||||
|
|
||||||
// Pool Code Init Hashes
|
// Pool Code Init Hashes
|
||||||
bytes32 USV2_POOL_CODE_INIT_HASH =
|
bytes32 USV2_POOL_CODE_INIT_HASH =
|
||||||
0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f;
|
0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pragma solidity ^0.8.26;
|
|||||||
|
|
||||||
import "./TychoRouterTestSetup.sol";
|
import "./TychoRouterTestSetup.sol";
|
||||||
import "./protocols/UniswapV4Utils.sol";
|
import "./protocols/UniswapV4Utils.sol";
|
||||||
|
import "@src/executors/BebopExecutor.sol";
|
||||||
|
|
||||||
contract TychoRouterTestProtocolIntegration is TychoRouterTestSetup {
|
contract TychoRouterTestProtocolIntegration is TychoRouterTestSetup {
|
||||||
function testMultiProtocolIntegration() public {
|
function testMultiProtocolIntegration() public {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pragma solidity ^0.8.26;
|
|||||||
// Executors
|
// Executors
|
||||||
import {BalancerV2Executor} from "../src/executors/BalancerV2Executor.sol";
|
import {BalancerV2Executor} from "../src/executors/BalancerV2Executor.sol";
|
||||||
import {BalancerV3Executor} from "../src/executors/BalancerV3Executor.sol";
|
import {BalancerV3Executor} from "../src/executors/BalancerV3Executor.sol";
|
||||||
|
import {BebopExecutor} from "../src/executors/BebopExecutor.sol";
|
||||||
import {CurveExecutor} from "../src/executors/CurveExecutor.sol";
|
import {CurveExecutor} from "../src/executors/CurveExecutor.sol";
|
||||||
import {EkuboExecutor} from "../src/executors/EkuboExecutor.sol";
|
import {EkuboExecutor} from "../src/executors/EkuboExecutor.sol";
|
||||||
import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol";
|
import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol";
|
||||||
@@ -73,12 +74,13 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
|
|||||||
CurveExecutor public curveExecutor;
|
CurveExecutor public curveExecutor;
|
||||||
MaverickV2Executor public maverickv2Executor;
|
MaverickV2Executor public maverickv2Executor;
|
||||||
BalancerV3Executor public balancerV3Executor;
|
BalancerV3Executor public balancerV3Executor;
|
||||||
|
BebopExecutor public bebopExecutor;
|
||||||
|
|
||||||
function getForkBlock() public view virtual returns (uint256) {
|
function getForkBlock() public view virtual returns (uint256) {
|
||||||
return 22082754;
|
return 22082754;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setUp() public {
|
function setUp() public virtual {
|
||||||
uint256 forkBlock = getForkBlock();
|
uint256 forkBlock = getForkBlock();
|
||||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||||
|
|
||||||
@@ -132,8 +134,9 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
|
|||||||
maverickv2Executor =
|
maverickv2Executor =
|
||||||
new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS);
|
new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS);
|
||||||
balancerV3Executor = new BalancerV3Executor(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[0] = address(usv2Executor);
|
||||||
executors[1] = address(usv3Executor);
|
executors[1] = address(usv3Executor);
|
||||||
executors[2] = address(pancakev3Executor);
|
executors[2] = address(pancakev3Executor);
|
||||||
@@ -143,6 +146,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
|
|||||||
executors[6] = address(curveExecutor);
|
executors[6] = address(curveExecutor);
|
||||||
executors[7] = address(maverickv2Executor);
|
executors[7] = address(maverickv2Executor);
|
||||||
executors[8] = address(balancerV3Executor);
|
executors[8] = address(balancerV3Executor);
|
||||||
|
executors[9] = address(bebopExecutor);
|
||||||
|
|
||||||
return executors;
|
return executors;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
470
foundry/test/protocols/Bebop.t.sol
Normal file
470
foundry/test/protocols/Bebop.t.sol
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,9 @@ use tycho_common::models::Chain;
|
|||||||
use crate::encoding::{
|
use crate::encoding::{
|
||||||
errors::EncodingError,
|
errors::EncodingError,
|
||||||
evm::swap_encoder::swap_encoders::{
|
evm::swap_encoder::swap_encoders::{
|
||||||
BalancerV2SwapEncoder, BalancerV3SwapEncoder, CurveSwapEncoder, EkuboSwapEncoder,
|
BalancerV2SwapEncoder, BalancerV3SwapEncoder, BebopSwapEncoder, CurveSwapEncoder,
|
||||||
MaverickV2SwapEncoder, UniswapV2SwapEncoder, UniswapV3SwapEncoder, UniswapV4SwapEncoder,
|
EkuboSwapEncoder, MaverickV2SwapEncoder, UniswapV2SwapEncoder, UniswapV3SwapEncoder,
|
||||||
|
UniswapV4SwapEncoder,
|
||||||
},
|
},
|
||||||
swap_encoder::SwapEncoder,
|
swap_encoder::SwapEncoder,
|
||||||
};
|
};
|
||||||
@@ -87,6 +88,9 @@ impl SwapEncoderBuilder {
|
|||||||
self.chain,
|
self.chain,
|
||||||
self.config,
|
self.config,
|
||||||
)?)),
|
)?)),
|
||||||
|
"rfq:bebop" => {
|
||||||
|
Ok(Box::new(BebopSwapEncoder::new(self.executor_address, self.chain, self.config)?))
|
||||||
|
}
|
||||||
_ => Err(EncodingError::FatalError(format!(
|
_ => Err(EncodingError::FatalError(format!(
|
||||||
"Unknown protocol system: {}",
|
"Unknown protocol system: {}",
|
||||||
self.protocol_system
|
self.protocol_system
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::{collections::HashMap, str::FromStr};
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
use alloy::{
|
use alloy::{
|
||||||
primitives::{Address, Bytes as AlloyBytes, U8},
|
primitives::{Address, Bytes as AlloyBytes, U256, U8},
|
||||||
sol_types::SolValue,
|
sol_types::SolValue,
|
||||||
};
|
};
|
||||||
use serde_json::from_str;
|
use serde_json::from_str;
|
||||||
@@ -634,6 +634,117 @@ impl SwapEncoder for BalancerV3SwapEncoder {
|
|||||||
fn executor_address(&self) -> &str {
|
fn executor_address(&self) -> &str {
|
||||||
&self.executor_address
|
&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> {
|
fn clone_box(&self) -> Box<dyn SwapEncoder> {
|
||||||
Box::new(self.clone())
|
Box::new(self.clone())
|
||||||
}
|
}
|
||||||
@@ -1610,4 +1721,144 @@ mod tests {
|
|||||||
write_calldata_to_file("test_encode_maverick_v2", hex_swap.as_str());
|
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..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ pub mod encoding;
|
|||||||
|
|
||||||
use std::str::FromStr;
|
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_common::{models::Chain, Bytes};
|
||||||
use tycho_execution::encoding::{
|
use tycho_execution::encoding::{
|
||||||
evm::encoder_builders::TychoRouterEncoderBuilder, models::UserTransferType,
|
evm::encoder_builders::TychoRouterEncoderBuilder, models::UserTransferType,
|
||||||
@@ -46,6 +49,10 @@ pub fn usdt() -> Bytes {
|
|||||||
Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap()
|
Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ondo() -> Bytes {
|
||||||
|
Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_signer() -> PrivateKeySigner {
|
pub fn get_signer() -> PrivateKeySigner {
|
||||||
// Set up a mock private key for signing (Alice's pk in our contract tests)
|
// Set up a mock private key for signing (Alice's pk in our contract tests)
|
||||||
let private_key =
|
let private_key =
|
||||||
@@ -64,3 +71,19 @@ pub fn get_tycho_router_encoder(user_transfer_type: UserTransferType) -> Box<dyn
|
|||||||
.build()
|
.build()
|
||||||
.expect("Failed to build encoder")
|
.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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
mod common;
|
mod common;
|
||||||
use std::{collections::HashMap, str::FromStr};
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
use alloy::hex::encode;
|
use alloy::{hex, hex::encode};
|
||||||
use num_bigint::{BigInt, BigUint};
|
use num_bigint::{BigInt, BigUint};
|
||||||
use tycho_common::{models::protocol::ProtocolComponent, Bytes};
|
use tycho_common::{models::protocol::ProtocolComponent, Bytes};
|
||||||
use tycho_execution::encoding::{
|
use tycho_execution::encoding::{
|
||||||
evm::utils::write_calldata_to_file,
|
evm::utils::{biguint_to_u256, write_calldata_to_file},
|
||||||
models::{Solution, Swap, UserTransferType},
|
models::{Solution, Swap, SwapBuilder, UserTransferType},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::common::{
|
use crate::common::{
|
||||||
encoding::encode_tycho_router_call, eth, eth_chain, get_signer, get_tycho_router_encoder, pepe,
|
build_bebop_calldata, encoding::encode_tycho_router_call, eth, eth_chain, get_signer,
|
||||||
usdc, weth,
|
get_tycho_router_encoder, ondo, pepe, usdc, weth,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_single_encoding_strategy_ekubo() {
|
fn test_single_encoding_strategy_ekubo() {
|
||||||
// ETH ──(EKUBO)──> USDC
|
// ETH ──(EKUBO)──> USDC
|
||||||
@@ -590,3 +591,126 @@ fn test_single_encoding_strategy_balancer_v3() {
|
|||||||
let hex_calldata = encode(&calldata);
|
let hex_calldata = encode(&calldata);
|
||||||
write_calldata_to_file("test_single_encoding_strategy_balancer_v3", hex_calldata.as_str());
|
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,
|
||||||
|
ð(),
|
||||||
|
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,
|
||||||
|
ð(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.data;
|
||||||
|
let hex_calldata = hex::encode(&calldata);
|
||||||
|
|
||||||
|
write_calldata_to_file("test_single_encoding_strategy_bebop_aggregate", hex_calldata.as_str());
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user