AlphaDexRouterOptimized

0xcf960c10fe3201e730a223cab8fc90de36577e64

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
23,049 bytes
ABI entries
73
15 read · 18 write
License
none

Contract information

Address
0xcf960c10fe3201e730a223cab8fc90de36577e64
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x8538e71bed…13e23ebfab

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (15)

ADMIN_ROLE()bytes32
DEFAULT_ADMIN_ROLE()bytes32
FEE_DENOMINATOR()uint256
MAX_FEE_NUMERATOR()uint256
WETH()address
calcFeeAndNewAmount(uint256)uint256, uint256
feeNumerator()uint256
getAmountOut(uint256, address[], address, uint256)uint256
getAmountOutAerodrome(uint256, address[], address)uint256
getRoleAdmin(bytes32)bytes32
hasRole(bytes32, address)bool
msgSender()address
poolManager()address
supportsInterface(bytes4)bool
vault()address

Events (7)

FeeNumeratorUpdatedFeesCollectedRoleAdminChangedRoleGrantedRoleRevokedVaultUpdatedWithdrewStuck

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_vault",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "_feeNumerator",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "_weth",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_poolManager",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "Currency",
        "name": "currency",
        "type": "address"
      }
    ],
    "name": "DeltaNotNegative",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "Currency",
        "name": "currency",
        "type": "address"
      }
    ],
    "name": "DeltaNotPositive",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeNumeratorTooHigh",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "IdenticalAddresses",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InputLengthMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientInputAmount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientLiquidity",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientOutputAmount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidBips",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidPathLength",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidPoolManager",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidVault",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidWeth",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotApproved",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPoolManager",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PathMissingWeth",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PoolInputNotEthOrWeth",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PoolMissingEthOrWeth",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PoolOutputNotEthOrWeth",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TooLittleReceived",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TransactionExpired",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "action",
        "type": "uint256"
      }
    ],
    "name": "UnsupportedAction",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "minAmountOutReceived",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amountReceived",
        "type": "uint256"
      }
    ],
    "name": "V4TooLittleReceived",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "maxAmountInRequested",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amountRequested",
        "type": "uint256"
      }
    ],
    "name": "V4TooMuchRequested",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmountIn",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroFeeBps",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroMsgValue",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroRecipient",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "caller",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "oldFeeNumerator",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newFeeNumerator",
        "type": "uint256"
      }
    ],
    "name": "FeeNumeratorUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "vault",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "FeesCollected",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "previousAdminRole",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "newAdminRole",
        "type": "bytes32"
      }
    ],
    "name": "RoleAdminChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "RoleGranted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "RoleRevoked",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "caller",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldVault",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newVault",
        "type": "address"
      }
    ],
    "name": "VaultUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "caller",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "assetAddress",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "WithdrewStuck",
    "type": "event"
  },
  {
    "stateMutability": "nonpayable",
    "type": "fallback"
  },
  {
    "inputs": [],
    "name": "ADMIN_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "DEFAULT_ADMIN_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FEE_DENOMINATOR",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_FEE_NUMERATOR",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "WETH",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "calcFeeAndNewAmount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "newAmount",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "tokenIn",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "tokenOut",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "address",
            "name": "recipient",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "amountIn",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "amountOutMinimum",
            "type": "uint256"
          },
          {
            "internalType": "uint160",
            "name": "sqrtPriceLimitX96",
            "type": "uint160"
          }
        ],
        "internalType": "struct ISwapRouter.ExactInputSingleParams",
        "name": "params",
        "type": "tuple"
      },
      {
        "internalType": "address",
        "name": "poolAddress",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "exactInputSingle",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "components": [
              {
                "internalType": "Currency",
                "name": "currency0",
                "type": "address"
              },
              {
                "internalType": "Currency",
                "name": "currency1",
                "type": "address"
              },
              {
                "internalType": "uint24",
                "name": "fee",
                "type": "uint24"
              },
              {
                "internalType": "int24",
                "name": "tickSpacing",
                "type": "int24"
              },
              {
                "internalType": "contract IHooks",
                "name": "hooks",
                "type": "address"
              }
            ],
            "internalType": "struct PoolKey",
            "name": "poolKey",
            "type": "tuple"
          },
          {
            "internalType": "bool",
            "name": "zeroForOne",
            "type": "bool"
          },
          {
            "internalType": "uint128",
            "name": "amountIn",
            "type": "uint128"
          },
          {
            "internalType": "uint128",
            "name": "amountOutMinimum",
            "type": "uint128"
          },
          {
            "internalType": "address",
            "name": "recipient",
            "type": "address"
          },
          {
            "internalType": "bytes",
            "name": "hookData",
            "type": "bytes"
          },
          {
            "internalType": "uint256",
            "name": "deadline",
            "type": "uint256"
          }
        ],
        "internalType": "struct IAlphaDexRouterOptimized.ExactInputSingleV4Params",
        "name": "params",
        "type": "tuple"
      }
    ],
    "name": "exactInputSingleV4",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeNumerator",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "address[]",
        "name": "path",
        "type": "address[]"
      },
      {
        "internalType": "address",
        "name": "poolAddress",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "_feeBps",
        "type": "uint256"
      }
    ],
    "name": "getAmountOut",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "address[]",
        "name": "path",
        "type": "address[]"
      },
      {
        "internalType": "address",
        "name": "poolAddress",
        "type": "address"
      }
    ],
    "name": "getAmountOutAerodrome",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      }
    ],
    "name": "getRoleAdmin",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "grantRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "hasRole",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "msgSender",
    "outputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "int256",
        "name": "amount0Delta",
        "type": "int256"
      },
      {
        "internalType": "int256",
        "name": "amount1Delta",
        "type": "int256"
      },
      {
        "internalType": "bytes",
        "name": "_data",
        "type": "bytes"
      }
    ],
    "name": "pancakeV3SwapCallback",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "tokenIn",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "tokenOut",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "amountIn",
            "type": "uint256"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "uint160",
            "name": "sqrtPriceLimitX96",
            "type": "uint160"
          }
        ],
        "internalType": "struct IQuoterV2.QuoteExactInputSingleParams",
        "name": "params",
        "type": "tuple"
      },
      {
        "internalType": "address",
        "name": "_quoter",
        "type": "address"
      }
    ],
    "name": "quoteExactInputSingle",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      },
      {
        "internalType": "uint160",
        "name": "sqrtPriceX96After",
        "type": "uint160"
      },
      {
        "internalType": "uint32",
        "name": "initializedTicksCrossed",
        "type": "uint32"
      },
      {
        "internalType": "uint256",
        "name": "gasEstimate",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "tokenIn",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "tokenOut",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "amountIn",
            "type": "uint256"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "uint160",
            "name": "sqrtPriceLimitX96",
            "type": "uint160"
          }
        ],
        "internalType": "struct IAerodromeQuoterV2.QuoteExactInputSingleParams",
        "name": "params",
        "type": "tuple"
      },
      {
        "internalType": "address",
        "name": "_quoter",
        "type": "address"
      }
    ],
    "name": "quoteExactInputSingleAerodrome",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      },
      {
        "internalType": "uint160",
        "name": "sqrtPriceX96After",
        "type": "uint160"
      },
      {
        "internalType": "uint32",
        "name": "initializedTicksCrossed",
        "type": "uint32"
      },
      {
        "internalType": "uint256",
        "name": "gasEstimate",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "components": [
              {
                "internalType": "Currency",
                "name": "currency0",
                "type": "address"
              },
              {
                "internalType": "Currency",
                "name": "currency1",
                "type": "address"
              },
              {
                "internalType": "uint24",
                "name": "fee",
                "type": "uint24"
              },
              {
                "internalType": "int24",
                "name": "tickSpacing",
                "type": "int24"
              },
              {
                "internalType": "contract IHooks",
                "name": "hooks",
                "type": "address"
              }
            ],
            "internalType": "struct PoolKey",
            "name": "poolKey",
            "type": "tuple"
          },
          {
            "internalType": "bool",
            "name": "zeroForOne",
            "type": "bool"
          },
          {
            "internalType": "uint128",
            "name": "amountIn",
            "type": "uint128"
          },
          {
            "internalType": "uint128",
            "name": "amountOutMinimum",
            "type": "uint128"
          },
          {
            "internalType": "address",
            "name": "recipient",
            "type": "address"
          },
          {
            "internalType": "bytes",
            "name": "hookData",
            "type": "bytes"
          },
          {
            "internalType": "uint256",
            "name": "deadline",
            "type": "uint256"
          }
        ],
        "internalType": "struct IAlphaDexRouterOptimized.ExactInputSingleV4Params",
        "name": "params",
        "type": "tuple"
      },
      {
        "internalType": "address",
        "name": "_v4Quoter",
        "type": "address"
      }
    ],
    "name": "quoteExactInputSingleV4",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "gasEstimate",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "renounceRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "revokeRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "_feeNumerator",
        "type": "uint256"
      }
    ],
    "name": "setFeeNumerator",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_vault",
        "type": "address"
      }
    ],
    "name": "setVault",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes4",
        "name": "interfaceId",
        "type": "bytes4"
      }
    ],
    "name": "supportsInterface",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amountOutMin",
        "type": "uint256"
      },
      {
        "internalType": "address[]",
        "name": "path",
        "type": "address[]"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "pairAddress",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "_feeBps",
        "type": "uint256"
      }
    ],
    "name": "swapExactETHForTokensSupportingFeeOnTransferTokens",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amountOutMin",
        "type": "uint256"
      },
      {
        "internalType": "address[]",
        "name": "path",
        "type": "address[]"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "pairAddress",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "_feeBps",
        "type": "uint256"
      }
    ],
    "name": "swapExactETHForTokensSupportingFeeOnTransferTokensAerodrome",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amountOutMin",
        "type": "uint256"
      },
      {
        "internalType": "address[]",
        "name": "path",
        "type": "address[]"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "pairAddress",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "_feeBps",
        "type": "uint256"
      }
    ],
    "name": "swapExactTokensForETHSupportingFeeOnTransferTokens",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amountOutMin",
        "type": "uint256"
      },
      {
        "internalType": "address[]",
        "name": "path",
        "type": "address[]"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "pairAddress",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "_feeBps",
        "type": "uint256"
      }
    ],
    "name": "swapExactTokensForETHSupportingFeeOnTransferTokensAerodrome",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "int256",
        "name": "amount0Delta",
        "type": "int256"
      },
      {
        "internalType": "int256",
        "name": "amount1Delta",
        "type": "int256"
      },
      {
        "internalType": "bytes",
        "name": "_data",
        "type": "bytes"
      }
    ],
    "name": "uniswapV3SwapCallback",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "vault",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_assetAddress",
        "type": "address"
      }
    ],
    "name": "withdraw",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";

import "./extensions/FeeExtension.sol";
import "./dexes/uniswapV3/v3-periphery/interfaces/ISwapRouter.sol";
import "./dexes/uniswapV3/v3-periphery/interfaces/IQuoterV2.sol";
import "./dexes/uniswapV3/v3-periphery/interfaces/IAerodromeQuoter.sol";
import "./interfaces/IUniswapV3Pool.sol";
import "./interfaces/IUniswapV2Pair.sol";
import "./libs/CustomBytesLib.sol";
import "./interfaces/IWETH9.sol";
import "./interfaces/IAerodromePool.sol";
import "./interfaces/IAlphaDexRouterOptimized.sol";

import {V4Router} from "@uniswap/v4-periphery/src/V4Router.sol";
import {IV4Router} from "@uniswap/v4-periphery/src/interfaces/IV4Router.sol";
import {IV4Quoter} from "@uniswap/v4-periphery/src/interfaces/IV4Quoter.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {ActionConstants} from "@uniswap/v4-periphery/src/libraries/ActionConstants.sol";

/// @title Fee-taking extension for any business logic
/// @notice Swap router supporting Aerodrome V2, Uniswap V2, Aerodrome V3, Uniswap V3, Uniswap V4 and fee taking functionality
contract AlphaDexRouterOptimized is IAlphaDexRouterOptimized, AccessControl, FeeExtension, V4Router {
	using SafeERC20 for IERC20;
	using BytesLib for bytes;

	uint160 internal constant MIN_SQRT_RATIO = 4295128739;
	/// @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK)
	uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342;

	/// @notice Admin role identifier for AccessControl
	bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");

	/// @notice ERC20-wrapped chain native token address
	address public immutable WETH;

	/// @dev The length of the bytes encoded address
	uint256 private constant ADDR_SIZE = 20;
	/// @dev The length of the bytes encoded fee
	uint256 private constant FEE_SIZE = 3;

	/// @dev The offset of a single token address and pool fee
	uint256 private constant NEXT_OFFSET = ADDR_SIZE + FEE_SIZE;

	struct SwapCallbackData {
		bytes path;
		address payer;
	}

	function _onlyOnePoolSwap(address[] calldata path) internal pure {
		if (path.length != 2) revert InvalidPathLength();
	}

	function _transactionExpired(uint256 deadline) internal view {
		if (deadline <= block.timestamp) revert TransactionExpired();
	}

	function _onlyPathWithWeth(address tokenIn, address tokenOut) internal view {
		if (tokenIn != WETH && tokenOut != WETH) revert PathMissingWeth();
	}

	function _nonZeroMsgValue() internal view {
		if (msg.value == 0) revert ZeroMsgValue();
	}

	/// @notice Returns true if the currency is native ETH (address(0)) or the configured WETH.
	function _isEthOrWeth(Currency c) internal view returns (bool) {
		address a = Currency.unwrap(c);
		return a == address(0) || a == WETH;
	}

	/// @notice Requires that at least one side of the V4 pool is native ETH or WETH so a fee can be taken in ETH.
	function _onlyPathWithEthOrWeth(Currency currency0, Currency currency1) internal view {
		if (!_isEthOrWeth(currency0) && !_isEthOrWeth(currency1)) revert PoolMissingEthOrWeth();
	}

	modifier setCaller() {
		assembly {
			tstore(0xf96697eee2ca87ab28d91d24669f6938fc1a53696304f2127c73b3e16b571df0, caller())
		}
		_;
		assembly {
			tstore(0xf96697eee2ca87ab28d91d24669f6938fc1a53696304f2127c73b3e16b571df0, 0)
		}
	}

	/// @notice Returns the address considered the executor of the Uniswap V4 actions.
	/// @dev Overrides BaseActionsRouter.msgSender; see CALLER_SLOT above.
	function msgSender() public view override returns (address sender) {
		assembly {
			sender := tload(0xf96697eee2ca87ab28d91d24669f6938fc1a53696304f2127c73b3e16b571df0)
		}
	}

	/// @notice Sets initial vault address, fee numerator, WETH address, Uniswap V4 PoolManager
	/// and distributes AccessControl roles
	constructor(
		address _vault,
		uint _feeNumerator,
		address _weth,
		address _poolManager
	) FeeExtension(_vault, _feeNumerator) V4Router(IPoolManager(_poolManager)) {
		_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
		_grantRole(ADMIN_ROLE, msg.sender);

		if (_weth == address(0)) revert InvalidWeth();
		if (_poolManager == address(0)) revert InvalidPoolManager();
		WETH = _weth;
	}

	/// @notice Allows the contract to receive ETH
	receive() external payable {}

	/// @notice Allows the contract to serve calls for nonexistent methods
	fallback(bytes calldata data) external returns (bytes memory) {}

	/// @notice Performs the Uniswap V2 `swapExactETHForTokensSupportingFeeOnTransferTokens` function.
	/// @dev The function deducts a fee from the input ETH amount and deposits the remaining ETH as WETH.
	///      It transfers WETH to the specified pair address before processing the swap.
	/// @param amountOutMin The minimum amount of output tokens expected from the swap.
	/// @param path The token swap path, where the first token must be WETH.
	/// @param to The address to receive the output tokens.
	/// @param deadline The latest timestamp by which the transaction is valid.
	/// @param pairAddress The address of the liquidity pair to perform the swap.
	/// @param _feeBps The fee in basis points (bps) to be deducted from the input ETH amount.
	/// @notice This function enforces the following restrictions:
	///         1. Only swaps involving a single pool are allowed.
	///         2. The first two tokens in the path must include WETH.
	///         3. The transaction must not be expired.
	/// @notice The function uses `msg.value` as the ETH input and rejects zero-value transactions.
	/// @custom:modifiers
	///         - `onlyOnePoolSwap(path)`: Ensures the swap path involves a single pool.
	///         - `onlyPathWithWeth(path[0], path[1])`: Ensures the first two tokens in the path include WETH.
	///         - `transactionExpired(deadline)`: Ensures the transaction is within the valid timeframe.
	function swapExactETHForTokensSupportingFeeOnTransferTokens(
		uint256 amountOutMin,
		address[] calldata path,
		address to,
		uint256 deadline,
		address pairAddress,
		uint256 _feeBps
	)
		external
		payable
	{
		_onlyOnePoolSwap(path);
		_onlyPathWithWeth(path[0], path[1]);
		_transactionExpired(deadline);
		_nonZeroMsgValue();
		{
			uint256 newAmountIn = takeFee(msg.value);
			IWETH9(WETH).deposit{value: newAmountIn}();
			IERC20(WETH).safeTransfer(pairAddress, newAmountIn);
		}

		processSwapExactETHForTokensSupportingFeeOnTransferTokens(amountOutMin, path, to, pairAddress, _feeBps, false);
	}

	/// @notice Performs the Uniswap V2 `swapExactTokensForETHSupportingFeeOnTransferTokens` function.
	/// @dev This function swaps an exact amount of input tokens for ETH, deducting a fee from the output ETH amount.
	///      It uses the same signature as the standard Uniswap V2 router function, with an additional parameter for the pair address.
	/// @param amountIn The exact amount of input tokens to be swapped.
	/// @param amountOutMin The minimum amount of ETH expected as output from the swap.
	/// @param path The token swap path, where the last token must be WETH.
	/// @param to The address to receive the output ETH.
	/// @param deadline The latest timestamp by which the transaction is valid.
	/// @param pairAddress The address of the liquidity pair to perform the swap.
	/// @param _feeBps The fee in basis points (bps) to be deducted from the output ETH amount.
	/// @notice This function enforces the following restrictions:
	///         1. Only swaps involving a single pool are allowed.
	///         2. The last token in the path must be WETH.
	///         3. The transaction must not be expired.
	/// @custom:modifiers
	///         - `transactionExpired(deadline)`: Ensures the transaction is within the valid timeframe.
	///         - `onlyOnePoolSwap(path)`: Ensures the swap path involves a single pool.
	///         - `onlyPathWithWeth(path[0], path[1])`: Ensures the last token in the path is WETH.
	function swapExactTokensForETHSupportingFeeOnTransferTokens(
		uint256 amountIn,
		uint256 amountOutMin,
		address[] calldata path,
		address to,
		uint256 deadline,
		address pairAddress,
		uint256 _feeBps
	) external {
		_transactionExpired(deadline);
		_onlyOnePoolSwap(path);
		_onlyPathWithWeth(path[0], path[1]);
		IERC20(path[0]).safeTransferFrom(msg.sender, pairAddress, amountIn);
		_swapSupportingFeeOnTransferTokens(path, pairAddress, address(this), _feeBps, false);
		processSwapExactTokensForETHSupportingFeeOnTransferTokens(amountOutMin, to);
	}

	/// @notice Performs the Aerodrome V2 `swapExactETHForTokensSupportingFeeOnTransferTokens` function.
	/// @dev The function deducts a fee from the input ETH amount and deposits the remaining ETH as WETH.
	///      It transfers WETH to the specified pair address before processing the swap.
	/// @param amountOutMin The minimum amount of output tokens expected from the swap.
	/// @param path The token swap path, where the first token must be WETH.
	/// @param to The address to receive the output tokens.
	/// @param deadline The latest timestamp by which the transaction is valid.
	/// @param pairAddress The address of the liquidity pair to perform the swap.
	/// @param _feeBps The fee in basis points (bps) to be deducted from the input ETH amount.
	/// @notice This function enforces the following restrictions:
	///         1. Only swaps involving a single pool are allowed.
	///         2. The first two tokens in the path must include WETH.
	///         3. The transaction must not be expired.
	/// @notice The function uses `msg.value` as the ETH input and rejects zero-value transactions.
	/// @custom:modifiers
	///         - `onlyOnePoolSwap(path)`: Ensures the swap path involves a single pool.
	///         - `onlyPathWithWeth(path[0], path[1])`: Ensures the first two tokens in the path include WETH.
	///         - `transactionExpired(deadline)`: Ensures the transaction is within the valid timeframe.
	function swapExactETHForTokensSupportingFeeOnTransferTokensAerodrome(
		uint256 amountOutMin,
		address[] calldata path,
		address to,
		uint256 deadline,
		address pairAddress,
		uint256 _feeBps
	)
		external
		payable
	{
		_onlyOnePoolSwap(path);
		_onlyPathWithWeth(path[0], path[1]);
		_transactionExpired(deadline);
		_nonZeroMsgValue();
		{
			uint256 newAmountIn = takeFee(msg.value);
			IWETH9(WETH).deposit{value: newAmountIn}();
			IERC20(WETH).safeTransfer(pairAddress, newAmountIn);
		}

		processSwapExactETHForTokensSupportingFeeOnTransferTokens(amountOutMin, path, to, pairAddress, _feeBps, true);
	}

	/// @notice Performs the Aerodrome V2 `swapExactTokensForETHSupportingFeeOnTransferTokens` function.
	/// @dev This function swaps an exact amount of input tokens for ETH, deducting a fee from the output ETH amount.
	///      It uses the same signature as the standard Uniswap V2 router function, with an additional parameter for the pair address.
	/// @param amountIn The exact amount of input tokens to be swapped.
	/// @param amountOutMin The minimum amount of ETH expected as output from the swap.
	/// @param path The token swap path, where the last token must be WETH.
	/// @param to The address to receive the output ETH.
	/// @param deadline The latest timestamp by which the transaction is valid.
	/// @param pairAddress The address of the liquidity pair to perform the swap.
	/// @param _feeBps The fee in basis points (bps) to be deducted from the output ETH amount.
	/// @notice This function enforces the following restrictions:
	///         1. Only swaps involving a single pool are allowed.
	///         2. The last token in the path must be WETH.
	///         3. The transaction must not be expired.
	/// @custom:modifiers
	///         - `transactionExpired(deadline)`: Ensures the transaction is within the valid timeframe.
	///         - `onlyOnePoolSwap(path)`: Ensures the swap path involves a single pool.
	///         - `onlyPathWithWeth(path[0], path[1])`: Ensures the last token in the path is WETH.
	function swapExactTokensForETHSupportingFeeOnTransferTokensAerodrome(
		uint256 amountIn,
		uint256 amountOutMin,
		address[] calldata path,
		address to,
		uint256 deadline,
		address pairAddress,
		uint256 _feeBps
	) external {
		_transactionExpired(deadline);
		_onlyOnePoolSwap(path);
		_onlyPathWithWeth(path[0], path[1]);
		IERC20(path[0]).safeTransferFrom(msg.sender, pairAddress, amountIn);
		_swapSupportingFeeOnTransferTokens(path, pairAddress, address(this), _feeBps, true);
		processSwapExactTokensForETHSupportingFeeOnTransferTokens(amountOutMin, to);
	}

	/// @notice Computes the output amount for a given exact input in a single V3 pool swap, including fee deductions.
	/// @dev Supports fee-taking logic for swaps involving WETH as input or output.
	/// @param params The parameters for the quote, structured as `QuoteExactInputSingleParams`:
	///        - `tokenIn`: The token being swapped in.
	///        - `tokenOut`: The token being swapped out.
	///        - `fee`: The fee tier of the token pool for the swap.
	///        - `amountIn`: The exact input amount for the swap.
	///        - `sqrtPriceLimitX96`: The maximum price limit for the pool that cannot be exceeded during the swap.
	/// @param _quoter The address of the QuoterV2 associated with the chosen V3 router.
	/// @return amountOut The amount of `tokenOut` that would be received.
	/// @return sqrtPriceX96After The square root price of the pool after the swap.
	/// @return initializedTicksCrossed The number of initialized ticks crossed during the swap.
	/// @return gasEstimate An estimate of the gas consumed for the swap.
	function quoteExactInputSingle(
		IQuoterV2.QuoteExactInputSingleParams memory params,
		address _quoter
	)
		external
		returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)
	{
		_onlyPathWithWeth(params.tokenIn, params.tokenOut);
		if (params.tokenIn == WETH) {
			(, params.amountIn) = calcFeeAndNewAmount(params.amountIn);
		}

		(amountOut, sqrtPriceX96After, initializedTicksCrossed, gasEstimate) = IQuoterV2(_quoter).quoteExactInputSingle(
			params
		);

		if (params.tokenOut == WETH) {
			(, amountOut) = calcFeeAndNewAmount(amountOut);
		}
	}

	/// @notice Computes the output amount for a given exact input in a single V3 pool swap on Aerodrome, including fee deductions.
	/// @dev Supports fee-taking logic for swaps involving WETH as input or output.
	/// @param params The parameters for the quote, structured as `QuoteExactInputSingleParams`:
	///        - `tokenIn`: The token being swapped in.
	///        - `tokenOut`: The token being swapped out.
	///        - `tickSpacing`: The tick spacing of the token pool for the swap.
	///        - `amountIn`: The exact input amount for the swap.
	///        - `sqrtPriceLimitX96`: The maximum price limit for the pool that cannot be exceeded during the swap.
	/// @param _quoter The address of the Aerodrome QuoterV2 associated with the chosen V3 router.
	/// @return amountOut The amount of `tokenOut` that would be received.
	/// @return sqrtPriceX96After The square root price of the pool after the swap.
	/// @return initializedTicksCrossed The number of initialized ticks crossed during the swap.
	/// @return gasEstimate An estimate of the gas consumed for the swap.
	function quoteExactInputSingleAerodrome(
		IAerodromeQuoterV2.QuoteExactInputSingleParams memory params,
		address _quoter
	)
		external
		returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)
	{
		_onlyPathWithWeth(params.tokenIn, params.tokenOut);
		if (params.tokenIn == WETH) {
			(, params.amountIn) = calcFeeAndNewAmount(params.amountIn);
		}

		(amountOut, sqrtPriceX96After, initializedTicksCrossed, gasEstimate) = IAerodromeQuoterV2(_quoter)
			.quoteExactInputSingle(params);

		if (params.tokenOut == WETH) {
			(, amountOut) = calcFeeAndNewAmount(amountOut);
		}
	}

	/// @notice Executes an `exactInputSingle` swap with support for ETH as either the input or output token.
	/// @dev This function supports swaps where either:
	///      - ETH is the input token and an ERC20 token is the output.
	///      - An ERC20 token is the input and ETH is the output.
	///      Fees are deducted from either the input ETH or output ETH, depending on the swap direction.
	/// @param params The parameters required for the swap, encoded as `ISwapRouter.ExactInputSingleParams`.
	///               Includes details like the input and output tokens, the amount, recipient, and price limits.
	/// @param poolAddress The address of the Uniswap V3 liquidity pool used for the swap.
	/// @param deadline The latest timestamp by which the transaction is valid.
	/// @return amountOut The amount of the output token received after the swap.
	/// @notice This function enforces the following restrictions:
	///         - Only swaps between ETH and an ERC20 token are allowed (no ERC20-to-ERC20 swaps).
	///         - The input and output tokens must include WETH.
	/// @custom:guards
	///         - `_onlyPathWithWeth(params.tokenIn, params.tokenOut)`: Ensures one of the tokens in the swap is WETH.
	///         - `_transactionExpired(deadline)`: reverts if `deadline <= block.timestamp`.
	function exactInputSingle(
		ISwapRouter.ExactInputSingleParams calldata params,
		address poolAddress,
		uint256 deadline
	) external payable returns (uint256 amountOut) {
		_onlyPathWithWeth(params.tokenIn, params.tokenOut);
		_transactionExpired(deadline);
		// A zero recipient is rewritten to address(this) inside exactInputInternal, which would
		// strand the swap output (or burn the ETH) in/from the router; reject it up front.
		if (params.recipient == address(0)) revert ZeroRecipient();
		if (params.tokenIn == WETH) {
			amountOut = _exactInputSingleEthToToken(params, poolAddress);
		} else {
			amountOut = _exactInputSingleTokenToEth(params, poolAddress);
		}
	}

	/// @dev ERC20->ETH leg of `exactInputSingle`. Pulls the input token, swaps it to WETH, unwraps,
	/// takes the fee on the ETH output, then sends the ETH to the recipient. A V3 exact-input swap can
	/// consume less than `amountIn` (a price limit was reached or liquidity is shallow); the unconsumed
	/// input would otherwise be stranded in the router, so it is refunded to the caller, bounded by a
	/// pre-swap snapshot so pre-existing/stuck balances of the input token are never touched. Kept as a
	/// separate function so its locals get their own stack frame (mirrors `_exactInputSingleEthToToken`).
	function _exactInputSingleTokenToEth(
		ISwapRouter.ExactInputSingleParams calldata params,
		address poolAddress
	) private returns (uint256 amountOut) {
		if (params.amountIn == 0) revert ZeroAmountIn();
		uint256 tokenInBefore = IERC20(params.tokenIn).balanceOf(address(this));
		IERC20(params.tokenIn).safeTransferFrom(msg.sender, address(this), params.amountIn);

		amountOut = exactInputInternal(
			poolAddress,
			params.amountIn,
			address(this),
			params.sqrtPriceLimitX96,
			SwapCallbackData({
				path: abi.encodePacked(params.tokenIn, params.fee, params.tokenOut),
				payer: msg.sender
			})
		);
		if (amountOut < params.amountOutMinimum) revert TooLittleReceived();

		// Refund only this swap's unconsumed input; the pre-existing balance (tokenInBefore) stays put.
		uint256 inLeft = IERC20(params.tokenIn).balanceOf(address(this)) - tokenInBefore;
		if (inLeft > 0) {
			IERC20(params.tokenIn).safeTransfer(msg.sender, inLeft);
		}

		IWETH9(WETH).withdraw(IERC20(WETH).balanceOf(address(this)));
		amountOut = takeFee(address(this).balance);
		if (address(this).balance < params.amountOutMinimum) revert InsufficientOutputAmount();
		Address.sendValue(payable(params.recipient), address(this).balance);
	}

	/// @dev ETH->ERC20 leg of `exactInputSingle`. Takes the fee on the ETH input, swaps the
	/// remainder, then refunds any unconsumed input back to the caller. A V3 exact-input swap can
	/// consume less than the requested amount (a price limit was reached or liquidity is shallow);
	/// without this refund that remainder would be stranded in the router. The refund is bounded by
	/// a pre-swap snapshot so pre-existing/stuck ETH the router holds is never touched (mirrors the
	/// Uniswap periphery refundETH pattern). Kept as a separate function so its locals get their own
	/// stack frame — `exactInputSingle` is already at the legacy-codegen stack limit.
	function _exactInputSingleEthToToken(
		ISwapRouter.ExactInputSingleParams calldata params,
		address poolAddress
	) private returns (uint256 amountOut) {
		_nonZeroMsgValue();
		// balance already includes msg.value here, so this is the router's pre-existing ETH.
		uint256 ethBefore = address(this).balance - msg.value;
		amountOut = exactInputInternal(
			poolAddress,
			takeFee(msg.value),
			params.recipient,
			params.sqrtPriceLimitX96,
			SwapCallbackData({
				path: abi.encodePacked(params.tokenIn, params.fee, params.tokenOut),
				payer: msg.sender
			})
		);
		if (amountOut < params.amountOutMinimum) revert TooLittleReceived();
		// Refund only this swap's unconsumed input; pre-existing funds (== ethBefore) stay put.
		uint256 refund = address(this).balance - ethBefore;
		if (refund > 0) {
			Address.sendValue(payable(msg.sender), refund);
		}
	}

	/// @notice Performs a single-pool Uniswap V4 exact-input swap, taking the ETH fee exactly once on the
	/// ETH/WETH side. ETH->ERC20 takes the fee from the input ETH (then swaps the remainder); ERC20->ETH
	/// swaps first, unwraps WETH if needed, then takes the fee from the output ETH. The pool must have
	/// ETH or WETH on one side (enforced by `_onlyPathWithEthOrWeth`).
	/// @param params The V4 swap parameters; see `ExactInputSingleV4Params`.
	/// @return amountOut The net amount the recipient actually received (post-fee).
	function exactInputSingleV4(ExactInputSingleV4Params calldata params)
		external
		payable
		setCaller
		returns (uint256 amountOut)
	{
		_transactionExpired(params.deadline);
		_onlyPathWithEthOrWeth(params.poolKey.currency0, params.poolKey.currency1);
		if (params.recipient == address(0)) revert ZeroRecipient();
		Currency inCurrency = params.zeroForOne ? params.poolKey.currency0 : params.poolKey.currency1;
		Currency outCurrency = params.zeroForOne ? params.poolKey.currency1 : params.poolKey.currency0;

		if (msg.value > 0) {
			// ETH -> ERC20: take fee on the ETH input, then swap the remainder.
			if (!_isEthOrWeth(inCurrency)) revert PoolInputNotEthOrWeth();
			uint256 swapAmount = takeFee(msg.value);

			address outToken = Currency.unwrap(outCurrency);
			uint256 balanceBefore = IERC20(outToken).balanceOf(params.recipient);

			bytes memory actions = abi.encodePacked(
				uint8(Actions.SWAP_EXACT_IN_SINGLE),
				uint8(Actions.SETTLE),
				uint8(Actions.TAKE)
			);
			bytes[] memory actionParams = new bytes[](3);
			actionParams[0] = abi.encode(
				IV4Router.ExactInputSingleParams({
					poolKey: params.poolKey,
					zeroForOne: params.zeroForOne,
					amountIn: uint128(swapAmount),
					amountOutMinimum: 0,
					hookData: params.hookData
				})
			);
			actionParams[1] = abi.encode(inCurrency, ActionConstants.OPEN_DELTA, false);
			actionParams[2] = abi.encode(outCurrency, params.recipient, ActionConstants.OPEN_DELTA);

			poolManager.unlock(abi.encode(actions, actionParams));

			amountOut = IERC20(outToken).balanceOf(params.recipient) - balanceBefore;
			if (amountOut < params.amountOutMinimum) revert InsufficientOutputAmount();
		} else {
			// ERC20 -> ETH: swap first (output taken to this contract), unwrap WETH if needed, then take fee on ETH.
			if (params.amountIn == 0) revert ZeroAmountIn();
			if (!_isEthOrWeth(outCurrency)) revert PoolOutputNotEthOrWeth();

			// SWAP_EXACT_IN_SINGLE: swap `params.amountIn` of the ERC20 input.
			// SETTLE_ALL: settle the full input debt (max = params.amountIn) -> _pay pulls ERC20 from msgSender().
			// TAKE: send the entire output credit to this contract so we can unwrap/fee/forward.
			bytes memory actions = abi.encodePacked(
				uint8(Actions.SWAP_EXACT_IN_SINGLE),
				uint8(Actions.SETTLE_ALL),
				uint8(Actions.TAKE)
			);
			bytes[] memory actionParams = new bytes[](3);
			actionParams[0] = abi.encode(
				IV4Router.ExactInputSingleParams({
					poolKey: params.poolKey,
					zeroForOne: params.zeroForOne,
					amountIn: params.amountIn,
					amountOutMinimum: 0,
					hookData: params.hookData
				})
			);
			actionParams[1] = abi.encode(inCurrency, uint256(params.amountIn));
			actionParams[2] = abi.encode(outCurrency, ActionConstants.ADDRESS_THIS, ActionConstants.OPEN_DELTA);

			bool outIsWeth = Currency.unwrap(outCurrency) == WETH;
			uint256 ethBefore = address(this).balance;
			uint256 wethBefore = outIsWeth ? IERC20(WETH).balanceOf(address(this)) : 0;

			poolManager.unlock(abi.encode(actions, actionParams));

			if (outIsWeth) {
				IWETH9(WETH).withdraw(IERC20(WETH).balanceOf(address(this)) - wethBefore);
			}

			uint256 grossOut = address(this).balance - ethBefore;
			uint256 amountOutNet = takeFee(grossOut);
			if (amountOutNet < params.amountOutMinimum) revert InsufficientOutputAmount();
			Address.sendValue(payable(params.recipient), amountOutNet);
			amountOut = amountOutNet;
		}
	}

	/// @notice Computes the output amount for a given exact input in a single Uniswap V4 pool swap, including fee deductions.
	/// @dev Mirrors the V3 `quoteExactInputSingle` fee pattern: when the input side is ETH/WETH the fee is deducted from
	/// the input before quoting; otherwise the fee is deducted from the quoted output. The pool must have ETH or WETH on
	/// one side (enforced by `_onlyPathWithEthOrWeth`). `IV4Quoter.quoteExactInputSingle` is non-view (it reverts internally
	/// and is meant to be called off-chain via `staticCall`).
	/// @param params The V4 quote parameters; see `ExactInputSingleV4Params`.
	/// @param _v4Quoter The address of the Uniswap V4 Quoter.
	/// @return amountOut The net amount the recipient would receive (post-fee).
	/// @return gasEstimate Estimated gas units used for the swap.
	function quoteExactInputSingleV4(
		ExactInputSingleV4Params calldata params,
		address _v4Quoter
	)
		external
		returns (uint256 amountOut, uint256 gasEstimate)
	{
		_onlyPathWithEthOrWeth(params.poolKey.currency0, params.poolKey.currency1);
		Currency inCurrency = params.zeroForOne ? params.poolKey.currency0 : params.poolKey.currency1;
		bool inputIsEthSide = _isEthOrWeth(inCurrency);
		uint128 quoteAmountIn = params.amountIn;
		if (inputIsEthSide) {
			(, uint256 netIn) = calcFeeAndNewAmount(params.amountIn);
			quoteAmountIn = uint128(netIn);
		}
		IV4Quoter.QuoteExactSingleParams memory qp = IV4Quoter.QuoteExactSingleParams({
			poolKey: params.poolKey,
			zeroForOne: params.zeroForOne,
			exactAmount: quoteAmountIn,
			hookData: params.hookData
		});
		(amountOut, gasEstimate) = IV4Quoter(_v4Quoter).quoteExactInputSingle(qp);
		if (!inputIsEthSide) {
			(, amountOut) = calcFeeAndNewAmount(amountOut);
		}
	}

	function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata _data) external {
		_v3SwapCallback(amount0Delta, amount1Delta, _data);
	}

	function pancakeV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata _data) external {
		_v3SwapCallback(amount0Delta, amount1Delta, _data);
	}

	/// @dev Shared body for the Uniswap-V3 and PancakeSwap-V3 swap callbacks (their logic is identical).
	/// Both external selectors are retained so each pool type still calls its own entrypoint.
	function _v3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata _data) private {
		require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported
		SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData));
		(address tokenIn, address tokenOut, ) = decodeFirstPool(data.path);

		(bool isExactInput, uint256 amountToPay) = amount0Delta > 0
			? (tokenIn < tokenOut, uint256(amount0Delta))
			: (tokenOut < tokenIn, uint256(amount1Delta));
		if (!isExactInput) {
			tokenIn = tokenOut; // swap in/out because exact output swaps are reversed
		}
		pay(tokenIn, address(this), msg.sender, amountToPay);
	}

	/// @notice Sets a new fee numerator, accessible by admin only
	function setFeeNumerator(uint _feeNumerator) external onlyRole(ADMIN_ROLE) {
		_setFeeNumerator(_feeNumerator);
	}

	/// @notice Sets a new vault address, accessible by admin only
	/// @param _vault New vault address
	function setVault(address _vault) external onlyRole(ADMIN_ROLE) {
		_setVault(_vault);
	}

	/// @notice Performs stuck assets withdraw
	/// @param _assetAddress Address of asset to be withdrawn, zero address means native ETH
	function withdraw(address _assetAddress) external onlyRole(ADMIN_ROLE) {
		if (_assetAddress == address(0)) {
			address self = address(this);
			uint amount = self.balance;
			Address.sendValue(payable(msg.sender), amount);
			emit WithdrewStuck(msg.sender, _assetAddress, amount);
		} else {
			uint amount = IERC20(_assetAddress).balanceOf(address(this));
			IERC20(_assetAddress).safeTransfer(msg.sender, amount);
			emit WithdrewStuck(msg.sender, _assetAddress, amount);
		}
	}

	/// @notice Calculates the output amount for a given input amount when performing a token swap.
	/// @dev This function determines the reserves for the input and output tokens, applies the pool's fee structure,
	///      and calculates the resulting output amount. It handles paths involving WETH differently to account for fees.
	/// @param amountIn The amount of input tokens to be swapped.
	/// @param path The token swap path, where the first two tokens determine the pool.
	/// @param poolAddress The address of the liquidity pool to perform the swap.
	/// @param _feeBps The fee in basis points (bps) applied to the swap.
	/// @return amountOut The amount of output tokens expected after the swap.
	/// @notice This function enforces the following restrictions:
	///         1. Only swaps involving a single pool are allowed.
	///         2. The first two tokens in the path must include WETH.
	/// @custom:modifiers
	///         - `onlyOnePoolSwap(path)`: Ensures the swap path involves a single pool.
	///         - `onlyPathWithWeth(path[0], path[1])`: Ensures the first two tokens in the path include WETH.
	function getAmountOut(
		uint256 amountIn,
		address[] calldata path,
		address poolAddress,
		uint256 _feeBps
	) public view returns (uint256 amountOut) {
		_onlyOnePoolSwap(path);
		_onlyPathWithWeth(path[0], path[1]);
		uint256 reserveInput;
		uint256 reserveOutput;
		{
			(address input, address output) = (path[0], path[1]);
			(address token0, ) = sortTokens(input, output);
			(uint256 reserve0, uint256 reserve1, ) = IUniswapV2Pair(poolAddress).getReserves();
			(reserveInput, reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
		}

		if (path[0] == WETH) {
			(, uint256 newAmountIn) = calcFeeAndNewAmount(amountIn);

			return getAmountOutWithFee(newAmountIn, reserveInput, reserveOutput, _feeBps);
		} else {
			amountOut = getAmountOutWithFee(amountIn, reserveInput, reserveOutput, _feeBps);
			(, uint256 newAmountOut) = calcFeeAndNewAmount(amountOut);
			return newAmountOut;
		}
	}

	/// @notice Calculates the output amount for a given input amount when performing a token swap.
	/// @dev This function determines the reserves for the input and output tokens, applies the pool's fee structure,
	///      and calculates the resulting output amount. It handles paths involving WETH differently to account for fees.
	/// @param amountIn The amount of input tokens to be swapped.
	/// @param path The token swap path, where the first two tokens determine the pool.
	/// @param poolAddress The address of the liquidity pool to perform the swap.
	/// @return amountOut The amount of output tokens expected after the swap.
	/// @notice This function enforces the following restrictions:
	///         1. Only swaps involving a single pool are allowed.
	///         2. The first two tokens in the path must include WETH.
	/// @custom:modifiers
	///         - `onlyOnePoolSwap(path)`: Ensures the swap path involves a single pool.
	///         - `onlyPathWithWeth(path[0], path[1])`: Ensures the first two tokens in the path include WETH.
	function getAmountOutAerodrome(
		uint256 amountIn,
		address[] calldata path,
		address poolAddress
	) public view returns (uint256 amountOut) {
		_onlyOnePoolSwap(path);
		_onlyPathWithWeth(path[0], path[1]);
		if (path[0] == WETH) {
			(, uint256 newAmountIn) = calcFeeAndNewAmount(amountIn);
			return IAerodromePool(poolAddress).getAmountOut(newAmountIn, path[0]);
		} else {
			amountOut = IAerodromePool(poolAddress).getAmountOut(amountIn, path[0]);
			(, uint256 newAmountOut) = calcFeeAndNewAmount(amountOut);
			return newAmountOut;
		}
	}

	// For pancakeswap put 25
	// For uniswap v2 put 30
	// For apeswap v2 put 10
	// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
	function getAmountOutWithFee(
		uint256 amountIn,
		uint256 reserveIn,
		uint256 reserveOut,
		uint256 feeBps
	) internal pure returns (uint256 amountOut) {
		if (amountIn == 0) revert InsufficientInputAmount();
		if (reserveIn == 0 || reserveOut == 0) revert InsufficientLiquidity();
		if (feeBps == 0) revert ZeroFeeBps();

		uint256 feeNumerator = 10000 - feeBps;
		uint256 amountInWithFee = amountIn * feeNumerator;
		uint256 numerator = amountInWithFee * reserveOut;
		uint256 denominator = (reserveIn * 10000) + amountInWithFee;
		amountOut = numerator / denominator;
	}

	/// @notice Decodes the first pool in path
	/// @param path The bytes encoded swap path
	/// @return tokenA The first token of the given pool
	/// @return tokenB The second token of the given pool
	/// @return fee The fee level of the pool
	function decodeFirstPool(bytes memory path) internal pure returns (address tokenA, address tokenB, uint24 fee) {
		tokenA = path.toAddress(0);
		fee = path.toUint24(ADDR_SIZE);
		tokenB = path.toAddress(NEXT_OFFSET);
	}

	// **** SWAP (supporting fee-on-transfer tokens) ****
	// requires the initial amount to have already been sent to the first pair
	function _swapSupportingFeeOnTransferTokens(
		address[] memory path,
		address pairAddress,
		address _to,
		uint256 _feeBps,
		bool isAerodrome
	) internal virtual {
		for (uint256 i; i < path.length - 1; i++) {
			(address input, address output) = (path[i], path[i + 1]);
			(address token0, ) = sortTokens(input, output);
			IUniswapV2Pair pair = IUniswapV2Pair(pairAddress);
			uint256 amountInput;
			uint256 amountOutput;
			{
				// scope to avoid stack too deep errors
				uint256 reserveInput;
				uint256 reserveOutput;
				{
					(uint256 reserve0, uint256 reserve1, ) = pair.getReserves();
					(reserveInput, reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
				}
				amountInput = IERC20(input).balanceOf(address(pair)) - reserveInput;
				if (isAerodrome) {
					amountOutput = IAerodromePool(pairAddress).getAmountOut(amountInput, input);
				} else {
					amountOutput = getAmountOutWithFee(amountInput, reserveInput, reserveOutput, _feeBps);
				}
			}
			(uint256 amount0Out, uint256 amount1Out) = input == token0
				? (uint256(0), amountOutput)
				: (amountOutput, uint256(0));
			address to = i < path.length - 2 ? pairAddress : _to;
			pair.swap(amount0Out, amount1Out, to, new bytes(0));
		}
	}

	function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
		if (tokenA == tokenB) revert IdenticalAddresses();
		(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
		if (token0 == address(0)) revert ZeroAddress();
	}

	/// @param token The token to pay
	/// @param payer The entity that must pay
	/// @param recipient The entity that will receive payment
	/// @param value The amount to pay
	function pay(address token, address payer, address recipient, uint256 value) internal {
		if (token == WETH && address(this).balance >= value) {
			// pay with WETH9
			IWETH9(WETH).deposit{value: value}(); // wrap only what is needed to pay
			IERC20(WETH).transfer(recipient, value);
		} else if (payer == address(this)) {
			// pay with tokens already in the contract (for the exact input multihop case)
			IERC20(token).safeTransfer(recipient, value);
		} else {
			revert NotApproved();
		}
	}

	/// @notice Pays tokens owed to the Uniswap V4 PoolManager during settlement.
	/// @dev Called by DeltaResolver._settle, which has already invoked poolManager.sync(currency)
	/// and will call poolManager.settle() afterwards, so this function must only deliver the
	/// funds to the PoolManager and must NOT call settle itself. Native currency is settled
	/// directly by _settle (settle{value:...}) and never reaches _pay.
	/// @param token The currency to pay (guaranteed by the base to be non-native).
	/// @param payer The address that should provide the tokens.
	/// @param amount The number of tokens to deliver to the PoolManager.
	function _pay(Currency token, address payer, uint256 amount) internal override {
		address tokenAddr = Currency.unwrap(token);

		if (token.isAddressZero()) {
			return;
		} else if (tokenAddr == WETH && payer == address(this)) {
			IWETH9(WETH).deposit{value: amount}();
			IERC20(WETH).safeTransfer(address(poolManager), amount);
		} else {
			IERC20(tokenAddr).safeTransferFrom(payer, address(poolManager), amount);
		}
	}

	/// @dev Performs a single exact input swap
	function exactInputInternal(
		address poolAddress,
		uint256 amountIn,
		address recipient,
		uint160 sqrtPriceLimitX96,
		SwapCallbackData memory data
	) private returns (uint256 amountOut) {
		// allow swapping to the router address with address 0
		if (recipient == address(0)) recipient = address(this);

		(address tokenIn, address tokenOut, uint24 fee) = decodeFirstPool(data.path);

		bool zeroForOne = tokenIn < tokenOut;

		(int256 amount0, int256 amount1) = IUniswapV3Pool(poolAddress).swap(
			recipient,
			zeroForOne,
			int256(amountIn),
			sqrtPriceLimitX96 == 0 ? (zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1) : sqrtPriceLimitX96,
			abi.encode(data)
		);

		return uint256(-(zeroForOne ? amount1 : amount0));
	}

	function processSwapExactTokensForETHSupportingFeeOnTransferTokens(uint256 amountOutMin, address to) internal {
		uint256 amountOut = IERC20(WETH).balanceOf(address(this));
		IWETH9(WETH).withdraw(amountOut);
		takeFee(address(this).balance);
		if (address(this).balance < amountOutMin) revert InsufficientOutputAmount();
		Address.sendValue(payable(to), address(this).balance);
	}

	function processSwapExactETHForTokensSupportingFeeOnTransferTokens(
		uint256 amountOutMin,
		address[] calldata path,
		address to,
		address pairAddress,
		uint256 _feeBps,
		bool isAerodrome
	) internal {
		uint256 balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
		_swapSupportingFeeOnTransferTokens(path, pairAddress, to, _feeBps, isAerodrome);
		if (IERC20(path[path.length - 1]).balanceOf(to) - balanceBefore < amountOutMin) revert InsufficientOutputAmount();
	}
}
Chain explorer5373msChain node87ms