chore: merge main

This commit is contained in:
TAMARA LIPOWSKI
2025-08-21 14:31:05 -04:00
45 changed files with 3514 additions and 854 deletions

View File

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

View File

@@ -0,0 +1,130 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "../RestrictTransferFrom.sol";
import "@interfaces/IExecutor.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
error HashflowExecutor__InvalidHashflowRouter();
error HashflowExecutor__InvalidDataLength();
interface IHashflowRouter {
struct RFQTQuote {
address pool;
address externalAccount;
address trader;
address effectiveTrader;
address baseToken;
address quoteToken;
uint256 effectiveBaseTokenAmount;
uint256 baseTokenAmount;
uint256 quoteTokenAmount;
uint256 quoteExpiry;
uint256 nonce;
bytes32 txid;
bytes signature; // ECDSA signature of the quote, 65 bytes
}
function tradeRFQT(RFQTQuote calldata quote) external payable;
}
contract HashflowExecutor is IExecutor, RestrictTransferFrom {
using SafeERC20 for IERC20;
address public constant NATIVE_TOKEN =
0x0000000000000000000000000000000000000000;
/// @notice The Hashflow router address
address public immutable hashflowRouter;
constructor(address _hashflowRouter, address _permit2)
RestrictTransferFrom(_permit2)
{
if (_hashflowRouter == address(0)) {
revert HashflowExecutor__InvalidHashflowRouter();
}
hashflowRouter = _hashflowRouter;
}
function swap(uint256 givenAmount, bytes calldata data)
external
payable
returns (uint256 calculatedAmount)
{
(
IHashflowRouter.RFQTQuote memory quote,
bool approvalNeeded,
TransferType transferType
) = _decodeData(data);
// Slippage checks
if (givenAmount > quote.baseTokenAmount) {
// Do not transfer more than the quote's maximum permitted amount.
givenAmount = quote.baseTokenAmount;
}
quote.effectiveBaseTokenAmount = givenAmount;
if (approvalNeeded && quote.baseToken != NATIVE_TOKEN) {
// slither-disable-next-line unused-return
IERC20(quote.baseToken).forceApprove(
hashflowRouter, type(uint256).max
);
}
uint256 ethValue = 0;
if (quote.baseToken == NATIVE_TOKEN) {
ethValue = quote.effectiveBaseTokenAmount;
}
_transfer(
address(this), transferType, address(quote.baseToken), givenAmount
);
uint256 balanceBefore = _balanceOf(quote.trader, quote.quoteToken);
IHashflowRouter(hashflowRouter).tradeRFQT{value: ethValue}(quote);
uint256 balanceAfter = _balanceOf(quote.trader, quote.quoteToken);
calculatedAmount = balanceAfter - balanceBefore;
}
function _decodeData(bytes calldata data)
internal
pure
returns (
IHashflowRouter.RFQTQuote memory quote,
bool approvalNeeded,
TransferType transferType
)
{
if (data.length != 327) {
revert HashflowExecutor__InvalidDataLength();
}
transferType = TransferType(uint8(data[0]));
approvalNeeded = data[1] != 0;
quote.pool = address(bytes20(data[2:22]));
quote.externalAccount = address(bytes20(data[22:42]));
quote.trader = address(bytes20(data[42:62]));
// Assumes we never set the effectiveTrader when requesting a quote.
quote.effectiveTrader = quote.trader;
quote.baseToken = address(bytes20(data[62:82]));
quote.quoteToken = address(bytes20(data[82:102]));
// Not included in the calldata. Will be set in the swap function.
quote.effectiveBaseTokenAmount = 0;
quote.baseTokenAmount = uint256(bytes32(data[102:134]));
quote.quoteTokenAmount = uint256(bytes32(data[134:166]));
quote.quoteExpiry = uint256(bytes32(data[166:198]));
quote.nonce = uint256(bytes32(data[198:230]));
quote.txid = bytes32(data[230:262]);
quote.signature = data[262:327];
}
function _balanceOf(address trader, address token)
internal
view
returns (uint256 balance)
{
balance = token == NATIVE_TOKEN
? trader.balance
: IERC20(token).balanceOf(trader);
}
}

View File

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

View File

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