// 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 != 347) { 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])); quote.effectiveTrader = address(bytes20(data[62:82])); quote.baseToken = address(bytes20(data[82:102])); quote.quoteToken = address(bytes20(data[102:122])); quote.effectiveBaseTokenAmount = 0; // Not included in the calldata, set in the swap function quote.baseTokenAmount = uint256(bytes32(data[122:154])); quote.quoteTokenAmount = uint256(bytes32(data[154:186])); quote.quoteExpiry = uint256(bytes32(data[186:218])); quote.nonce = uint256(bytes32(data[218:250])); quote.txid = bytes32(data[250:282]); quote.signature = data[282:347]; } function _balanceOf(address trader, address token) internal view returns (uint256 balance) { balance = token == NATIVE_TOKEN ? trader.balance : IERC20(token).balanceOf(trader); } }