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
- Creator
- 0xe67d1E21AA…439D0e15Ff
- 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