359 lines
13 KiB
Solidity
359 lines
13 KiB
Solidity
// 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";
|
|
|
|
/// @dev Bebop settlement interface for PMM RFQ swaps
|
|
interface IBebopSettlement {
|
|
struct Single {
|
|
uint256 expiry;
|
|
address taker_address;
|
|
address maker_address;
|
|
uint256 maker_nonce;
|
|
address taker_token;
|
|
address maker_token;
|
|
uint256 taker_amount;
|
|
uint256 maker_amount;
|
|
address receiver;
|
|
uint256 packed_commands;
|
|
uint256 flags;
|
|
}
|
|
|
|
struct Aggregate {
|
|
uint256 expiry;
|
|
address taker_address;
|
|
address[] maker_addresses;
|
|
uint256[] maker_nonces;
|
|
address[][] taker_tokens;
|
|
address[][] maker_tokens;
|
|
uint256[][] taker_amounts;
|
|
uint256[][] maker_amounts;
|
|
address receiver;
|
|
bytes commands;
|
|
uint256 flags; // `hashAggregateOrder` doesn't use this field for AggregateOrder hash
|
|
}
|
|
|
|
struct MakerSignature {
|
|
bytes signatureBytes;
|
|
uint256 flags;
|
|
}
|
|
|
|
function swapSingle(
|
|
Single calldata order,
|
|
MakerSignature calldata makerSignature,
|
|
uint256 filledTakerAmount
|
|
) external payable;
|
|
|
|
function swapAggregate(
|
|
Aggregate calldata order,
|
|
MakerSignature[] calldata makerSignatures,
|
|
uint256 filledTakerAmount
|
|
) external payable;
|
|
}
|
|
|
|
/// @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;
|
|
|
|
/// @notice Bebop order types
|
|
enum OrderType {
|
|
Single, // 0: Single token pair trade
|
|
Aggregate // 1: Multi-maker trade with single token in/out
|
|
|
|
}
|
|
|
|
/// @notice Bebop-specific errors
|
|
error BebopExecutor__InvalidDataLength();
|
|
error BebopExecutor__InvalidInput();
|
|
error BebopExecutor__InvalidSignatureLength();
|
|
error BebopExecutor__InvalidSignatureType();
|
|
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 quote information
|
|
/// @return calculatedAmount The amount of output token received
|
|
function swap(uint256 givenAmount, bytes calldata data)
|
|
external
|
|
payable
|
|
override
|
|
returns (uint256 calculatedAmount)
|
|
{
|
|
// Decode the packed data
|
|
(
|
|
address tokenIn,
|
|
address tokenOut,
|
|
TransferType transferType,
|
|
OrderType orderType,
|
|
uint256 filledTakerAmount,
|
|
bytes memory quoteData,
|
|
bytes memory makerSignaturesData,
|
|
bool approvalNeeded
|
|
) = _decodeData(data);
|
|
|
|
// Execute RFQ swap based on order type
|
|
if (orderType == OrderType.Single) {
|
|
calculatedAmount = _executeSingleRFQ(
|
|
tokenIn,
|
|
tokenOut,
|
|
transferType,
|
|
givenAmount,
|
|
filledTakerAmount,
|
|
quoteData,
|
|
makerSignaturesData,
|
|
approvalNeeded
|
|
);
|
|
} else if (orderType == OrderType.Aggregate) {
|
|
calculatedAmount = _executeAggregateRFQ(
|
|
tokenIn,
|
|
tokenOut,
|
|
transferType,
|
|
givenAmount,
|
|
filledTakerAmount,
|
|
quoteData,
|
|
makerSignaturesData,
|
|
approvalNeeded
|
|
);
|
|
} else {
|
|
revert BebopExecutor__InvalidInput();
|
|
}
|
|
}
|
|
|
|
/// @dev Decodes the packed calldata
|
|
function _decodeData(bytes calldata data)
|
|
internal
|
|
pure
|
|
returns (
|
|
address tokenIn,
|
|
address tokenOut,
|
|
TransferType transferType,
|
|
OrderType orderType,
|
|
uint256 filledTakerAmount,
|
|
bytes memory quoteData,
|
|
bytes memory makerSignaturesData,
|
|
bool approvalNeeded
|
|
)
|
|
{
|
|
// Need at least 83 bytes for the minimum fixed fields
|
|
// 20 + 20 + 1 + 1 + 32 (filledTakerAmount) + 4 (quote length) + 4 (maker sigs length) + 1 (approval) = 83
|
|
if (data.length < 83) revert BebopExecutor__InvalidDataLength();
|
|
|
|
// Decode fixed fields
|
|
tokenIn = address(bytes20(data[0:20]));
|
|
tokenOut = address(bytes20(data[20:40]));
|
|
transferType = TransferType(uint8(data[40]));
|
|
orderType = OrderType(uint8(data[41]));
|
|
filledTakerAmount = uint256(bytes32(data[42:74]));
|
|
|
|
// Get quote data length and validate
|
|
uint32 quoteDataLength = uint32(bytes4(data[74:78]));
|
|
if (data.length < 78 + quoteDataLength + 4) {
|
|
revert BebopExecutor__InvalidDataLength();
|
|
}
|
|
|
|
// Extract quote data
|
|
quoteData = data[78:78 + quoteDataLength];
|
|
|
|
// Get maker signatures data length
|
|
uint32 makerSignaturesLength =
|
|
uint32(bytes4(data[78 + quoteDataLength:82 + quoteDataLength]));
|
|
|
|
// Validate total length
|
|
// 78 + quoteDataLength + 4 + makerSignaturesLength + 1 (approval)
|
|
uint256 expectedLength = 83 + quoteDataLength + makerSignaturesLength;
|
|
if (data.length != expectedLength) {
|
|
revert BebopExecutor__InvalidDataLength();
|
|
}
|
|
|
|
// Extract maker signatures data (ABI encoded MakerSignature array)
|
|
makerSignaturesData = data[
|
|
82 + quoteDataLength:82 + quoteDataLength + makerSignaturesLength
|
|
];
|
|
|
|
// Extract approval flag
|
|
approvalNeeded = data[82 + quoteDataLength + makerSignaturesLength] != 0;
|
|
}
|
|
|
|
/// @dev Executes a Single RFQ swap through Bebop settlement
|
|
function _executeSingleRFQ(
|
|
address tokenIn,
|
|
address tokenOut,
|
|
TransferType transferType,
|
|
uint256 givenAmount,
|
|
uint256 filledTakerAmount,
|
|
bytes memory quoteData,
|
|
bytes memory makerSignaturesData,
|
|
bool approvalNeeded
|
|
) internal virtual returns (uint256 amountOut) {
|
|
// Decode the order from quoteData
|
|
IBebopSettlement.Single memory order =
|
|
abi.decode(quoteData, (IBebopSettlement.Single));
|
|
|
|
// Decode the MakerSignature array (should contain exactly 1 signature for Single orders)
|
|
IBebopSettlement.MakerSignature[] memory signatures =
|
|
abi.decode(makerSignaturesData, (IBebopSettlement.MakerSignature[]));
|
|
|
|
// Validate that there is exactly one maker signature
|
|
if (signatures.length != 1) {
|
|
revert BebopExecutor__InvalidInput();
|
|
}
|
|
|
|
// Get the maker signature from the first and only element of the array
|
|
IBebopSettlement.MakerSignature memory sig = signatures[0];
|
|
|
|
uint256 actualFilledTakerAmount = _getActualFilledTakerAmount(
|
|
givenAmount, order.taker_amount, filledTakerAmount
|
|
);
|
|
|
|
if (tokenIn != address(0)) {
|
|
// Transfer tokens to executor
|
|
_transfer(address(this), transferType, tokenIn, givenAmount);
|
|
}
|
|
|
|
// Approve Bebop settlement to spend tokens if needed
|
|
if (approvalNeeded) {
|
|
// slither-disable-next-line unused-return
|
|
IERC20(tokenIn).forceApprove(bebopSettlement, type(uint256).max);
|
|
}
|
|
|
|
// Record balances before swap to calculate amountOut
|
|
uint256 balanceBefore = _balanceOf(tokenOut, order.receiver);
|
|
|
|
// Execute the swap with ETH value if needed
|
|
uint256 ethValue = tokenIn == address(0) ? actualFilledTakerAmount : 0;
|
|
|
|
// Use swapSingle since tokens are in the executor with approval
|
|
// slither-disable-next-line arbitrary-send-eth
|
|
IBebopSettlement(bebopSettlement).swapSingle{value: ethValue}(
|
|
order, sig, actualFilledTakerAmount
|
|
);
|
|
|
|
// Calculate actual amount received
|
|
uint256 balanceAfter = _balanceOf(tokenOut, order.receiver);
|
|
|
|
amountOut = balanceAfter - balanceBefore;
|
|
}
|
|
|
|
/// @dev Executes an Aggregate RFQ swap through Bebop settlement
|
|
function _executeAggregateRFQ(
|
|
address tokenIn,
|
|
address tokenOut,
|
|
TransferType transferType,
|
|
uint256 givenAmount,
|
|
uint256 filledTakerAmount,
|
|
bytes memory quoteData,
|
|
bytes memory makerSignaturesData,
|
|
bool approvalNeeded
|
|
) internal virtual returns (uint256 amountOut) {
|
|
// Decode the Aggregate order
|
|
IBebopSettlement.Aggregate memory order =
|
|
abi.decode(quoteData, (IBebopSettlement.Aggregate));
|
|
|
|
// Decode the MakerSignature array (can contain multiple signatures for Aggregate orders)
|
|
IBebopSettlement.MakerSignature[] memory signatures =
|
|
abi.decode(makerSignaturesData, (IBebopSettlement.MakerSignature[]));
|
|
|
|
// Aggregate orders should have at least one signature
|
|
if (signatures.length == 0) {
|
|
revert BebopExecutor__InvalidInput();
|
|
}
|
|
|
|
// For aggregate orders, calculate total taker amount across all amounts of the 2D array
|
|
uint256 totalTakerAmount = 0;
|
|
for (uint256 i = 0; i < order.taker_amounts.length; i++) {
|
|
for (uint256 j = 0; j < order.taker_amounts[i].length; j++) {
|
|
totalTakerAmount += order.taker_amounts[i][j];
|
|
}
|
|
}
|
|
|
|
uint256 actualFilledTakerAmount = _getActualFilledTakerAmount(
|
|
givenAmount, totalTakerAmount, filledTakerAmount
|
|
);
|
|
|
|
if (tokenIn != address(0)) {
|
|
// Transfer tokens to executor
|
|
_transfer(address(this), transferType, tokenIn, givenAmount);
|
|
}
|
|
|
|
// Approve Bebop settlement to spend tokens if needed
|
|
if (approvalNeeded) {
|
|
// slither-disable-next-line unused-return
|
|
IERC20(tokenIn).forceApprove(bebopSettlement, type(uint256).max);
|
|
}
|
|
|
|
// Record balance before swap
|
|
uint256 balanceBefore = _balanceOf(tokenOut, order.receiver);
|
|
|
|
// Execute the swap
|
|
uint256 ethValue = tokenIn == address(0) ? actualFilledTakerAmount : 0;
|
|
|
|
// Execute the swap (tokens are in executor, approved to settlement)
|
|
// slither-disable-next-line arbitrary-send-eth
|
|
IBebopSettlement(bebopSettlement).swapAggregate{value: ethValue}(
|
|
order, signatures, actualFilledTakerAmount
|
|
);
|
|
|
|
// Calculate actual amount received
|
|
uint256 balanceAfter = _balanceOf(tokenOut, order.receiver);
|
|
|
|
amountOut = balanceAfter - balanceBefore;
|
|
}
|
|
|
|
/**
|
|
* @dev Determines the actual taker amount to be filled for a Bebop order
|
|
* @notice This function handles two scenarios:
|
|
* 1. When filledTakerAmount is 0: Uses the full order amount if givenAmount is sufficient,
|
|
* otherwise returns givenAmount to partially fill the order
|
|
* 2. When filledTakerAmount > 0: Caps the fill at the minimum of filledTakerAmount and givenAmount
|
|
* to ensure we don't attempt to fill more than available
|
|
* @param givenAmount The amount of tokens available from the router for this swap
|
|
* @param orderTakerAmount The full taker amount specified in the Bebop order
|
|
* @param filledTakerAmount The requested fill amount (0 means fill entire order)
|
|
* @return actualFilledTakerAmount The amount that will actually be filled
|
|
*/
|
|
function _getActualFilledTakerAmount(
|
|
uint256 givenAmount,
|
|
uint256 orderTakerAmount,
|
|
uint256 filledTakerAmount
|
|
) internal pure returns (uint256 actualFilledTakerAmount) {
|
|
actualFilledTakerAmount = filledTakerAmount == 0
|
|
? (givenAmount >= orderTakerAmount ? orderTakerAmount : givenAmount)
|
|
: (filledTakerAmount > givenAmount ? givenAmount : filledTakerAmount);
|
|
}
|
|
|
|
/// @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);
|
|
}
|
|
}
|