Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"script/utils/TaxHookDeployerHelper.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {TaxHookDeployer} from "./TaxHookDeployer.sol";
import {TaxHook} from "../../src/TaxHook.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol";
/**
* @title TaxHookDeployerHelper
* @notice Contract wrapper for TaxHookDeployer library to ensure proper CREATE2 deployer address
* @dev This contract is necessary because in Foundry tests, vm.prank() doesn't affect CREATE/CREATE2 operations.
* By using an actual contract as the deployer, we ensure that HookMiner and CREATE2 use the same address.
*
* Problem:
* - In tests with vm.prank(address), msg.sender changes for CALL operations
* - But CREATE/CREATE2 operations still use the test contract's address
* - This causes HookMiner to mine a salt for one address but CREATE2 uses another
* - Result: Invalid hook address without required permission flags
*
* Solution:
* - Deploy this helper contract (which has a deterministic address)
* - Call deployTaxHook() on the helper contract
* - Now HookMiner and CREATE2 both use the helper contract's address as deployer
* - Result: Valid hook address with correct permission flags
*/
contract TaxHookDeployerHelper {
/**
* @notice Deploy a TaxHook with proper address flags
* @dev This function inlines TaxHookDeployer logic but uses address(this) as the deployer
* to ensure HookMiner and CREATE2 use the same deployer address
* @param params Deployment parameters (poolManager, owner, ownerCutBps)
* @return result Deployment result containing hook contract and metadata
*/
function deployTaxHook(TaxHookDeployer.DeployParams memory params)
external
returns (TaxHookDeployer.DeployResult memory result)
{
// Validate parameters
require(params.poolManager != address(0), "TaxHookDeployer: Invalid pool manager");
require(params.owner != address(0), "TaxHookDeployer: Invalid owner");
require(params.ownerCutBps <= 1000, "TaxHookDeployer: Owner cut too high"); // Max 10%
// Define required hook flags for TaxHook
uint160 flags = uint160(
Hooks.BEFORE_SWAP_FLAG |
Hooks.AFTER_SWAP_FLAG |
Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG |
Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG
);
// Encode constructor arguments for HookMiner
bytes memory constructorArgs = abi.encode(
IPoolManager(params.poolManager),
params.owner,
params.ownerCutBps
);
// Use address(this) as the deployer for both HookMiner and CREATE2
// This ensures consistency - both operations use the helper contract's address
(address hookAddress, bytes32 salt) = HookMiner.find(
address(this), // Use helper contract as deployer, not msg.sender
flags,
type(TaxHook).creationCode,
constructorArgs
);
// Deploy the hook using CREATE2 with the mined salt
// Since this is executing in the helper contract's context, address(this) is the deployer
TaxHook hook = new TaxHook{salt: salt}(
IPoolManager(params.poolManager),
params.owner,
params.ownerCutBps
);
// Verify the hook address matches what was expected
require(address(hook) == hookAddress, "TaxHookDeployer: Hook address mismatch");
// Return deployment result
result = TaxHookDeployer.DeployResult({
hook: hook,
hookAddress: hookAddress,
salt: salt
});
return result;
}
/**
* @notice Get the deployer address that will be used for CREATE2
* @dev Useful for verification and debugging
* @return The address of this contract (the deployer)
*/
function getDeployerAddress() external view returns (address) {
return address(this);
}
}
"
},
"script/utils/TaxHookDeployer.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Script.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol";
import {TaxHook} from "../../src/TaxHook.sol";
/**
* @title TaxHookDeployer
* @notice Utility library for deploying TaxHook contracts with proper address flags
* @dev Uses HookMiner to find a salt that produces a hook address with required permissions
*/
library TaxHookDeployer {
struct DeployParams {
address poolManager; // Uniswap V4 PoolManager address
address owner; // Owner of the TaxHook contract
uint16 ownerCutBps; // Owner's cut of taxes in basis points (e.g., 300 = 3%)
}
struct DeployResult {
TaxHook hook; // Deployed TaxHook contract
address hookAddress; // Address of the deployed hook
bytes32 salt; // Salt used for CREATE2 deployment
}
/**
* @notice Deploy a TaxHook contract with proper address flags using HookMiner
* @dev The hook requires specific flags for swap hooks and delta returns
* @param params Deployment parameters
* @return result Deployment result containing hook contract and metadata
*/
function deployTaxHook(DeployParams memory params) internal returns (DeployResult memory result) {
// Validate parameters
require(params.poolManager != address(0), "TaxHookDeployer: Invalid pool manager");
require(params.owner != address(0), "TaxHookDeployer: Invalid owner");
require(params.ownerCutBps <= 1000, "TaxHookDeployer: Owner cut too high"); // Max 10%
// Define required hook flags for TaxHook
// - beforeSwap: Tax inflows (ETH coming in)
// - afterSwap: Tax outflows (ETH going out)
// - beforeSwapReturnsDelta: Return delta from beforeSwap to balance take() calls
// - afterSwapReturnsDelta: Return delta from afterSwap to balance take() calls
uint160 flags = uint160(
Hooks.BEFORE_SWAP_FLAG |
Hooks.AFTER_SWAP_FLAG |
Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG |
Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG
);
// Encode constructor arguments for HookMiner
bytes memory constructorArgs = abi.encode(
IPoolManager(params.poolManager),
params.owner,
params.ownerCutBps
);
// Use HookMiner to find a salt that produces a hook address with the required flags
address deployerAddress = msg.sender;
(address hookAddress, bytes32 salt) = HookMiner.find(
deployerAddress,
flags,
type(TaxHook).creationCode,
constructorArgs
);
// Deploy the hook using CREATE2 with the mined salt
TaxHook hook = new TaxHook{salt: salt}(
IPoolManager(params.poolManager),
params.owner,
params.ownerCutBps
);
// Verify the hook address matches what was expected
require(address(hook) == hookAddress, "TaxHookDeployer: Hook address mismatch");
// Return deployment result
result = DeployResult({
hook: hook,
hookAddress: hookAddress,
salt: salt
});
return result;
}
/**
* @notice Get the required hook flags for TaxHook
* @dev Useful for verification and testing
* @return flags The required Uniswap V4 hook flags
*/
function getRequiredFlags() internal pure returns (uint160 flags) {
return uint160(
Hooks.BEFORE_SWAP_FLAG |
Hooks.AFTER_SWAP_FLAG |
Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG |
Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG
);
}
/**
* @notice Validate that a hook address has the required flags
* @param hookAddress The hook address to validate
* @return isValid True if the address has the required flags
*/
function validateHookAddress(address hookAddress) internal pure returns (bool isValid) {
uint160 requiredFlags = getRequiredFlags();
uint160 addressFlags = uint160(hookAddress);
// Check if all required flags are present in the address
return (addressFlags & requiredFlags) == requiredFlags;
}
}
"
},
"src/TaxHook.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {BaseHook} from "@uniswap/v4-periphery/src/utils/BaseHook.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {
BeforeSwapDelta, BeforeSwapDeltaLibrary, toBeforeSwapDelta
} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {Constants} from "./Constants.sol";
/**
* @title TaxHook
* @notice A Uniswap V4 hook that applies configurable taxes on TOKEN/ETH swaps
* @dev Supports multiple tokens sharing one hook. Always taxes native ETH, not the custom token.
* Owner can take a configurable cut of all taxes collected.
*/
contract TaxHook is BaseHook, Ownable, ReentrancyGuard {
using PoolIdLibrary for PoolKey;
using SafeCast for uint256;
using CurrencyLibrary for Currency;
// Tax rate in basis points (1/100 of a percent)
// 100 = 1%, 500 = 5%, etc.
uint16 public constant TAX_RATE_DENOMINATOR = 10000;
// Flag to tell Uniswap to use the returned fee
uint24 public constant LP_FEE_OVERRIDE_FLAG = 0x400000; // 23rd bit set
// Fee override value for zero LP fees
uint24 public constant FEE_OVERRIDE = LP_FEE_OVERRIDE_FLAG; // 0 fee with override flag
// Price limit constants for internal token->ETH swaps
uint160 private constant MAX_PRICE_LIMIT = TickMath.MAX_SQRT_PRICE - 1;
uint160 private constant MIN_PRICE_LIMIT = TickMath.MIN_SQRT_PRICE + 1;
/**
* @notice Override LP fees explanation
* When returning a fee from beforeSwap:
* 1. The 23rd bit (0x400000) tells Uniswap to use the returned fee value
* 2. Setting the value to ZERO_LP_FEE means users pay 0% LP fees
* 3. Only taxes specified by the hook will be collected
*
* This allows tokens to have completely custom fee structures
* without any of the standard Uniswap LP fees.
*/
// Owner's cut of all collected taxes (in basis points, e.g., 300 = 3%)
uint16 public ownerCutBps;
// Structure to store tax configuration per token
struct TokenTaxConfig {
bool enabled; // Whether tax is enabled for this token
uint16 taxBps; // Total tax rate in basis points
uint256 collected; // Amount collected for token owner
uint256 withdrawn; // Amount withdrawn by token owner
bool exemptFromOwnerCut; // If true, owner gets no cut from this token
address tokenOwner; // Address that registered and owns this token config
}
// Mapping from custom token address to its tax configuration
mapping(address => TokenTaxConfig) public tokenTaxConfigs;
// Global owner tax tracking (across all tokens)
uint256 public ownerTaxCollected;
uint256 public ownerTaxWithdrawn;
// Flag to prevent taxing internal swaps (for token->ETH conversions)
bool private _inInternalSwap;
// Event emitted when tax is collected
event TaxCollected(
PoolId indexed poolId,
address indexed customToken,
uint256 totalTaxAmount,
uint256 ownerCut,
uint256 tokenWalletCut,
bool isInflow
);
// Event emitted when a token is registered
event TokenRegistered(
address indexed customToken, PoolId indexed poolId, uint16 taxBps, address indexed tokenOwner
);
// Event emitted when taxes are withdrawn
event TaxWithdrawn(address indexed beneficiary, uint256 amount, bool isOwnerTax);
// Event emitted when a token is exempted from owner cut
event TokenExemptionUpdated(address indexed customToken, bool exempt);
// Event emitted when token ownership is transferred
event TokenOwnershipTransferred(
address indexed customToken, address indexed previousOwner, address indexed newOwner
);
constructor(IPoolManager _poolManager, address _owner, uint16 _ownerCutBps) BaseHook(_poolManager) {
require(_ownerCutBps < TAX_RATE_DENOMINATOR, "TaxHook: Owner cut too high");
ownerCutBps = _ownerCutBps;
// Transfer ownership if not deployer
if (_owner != msg.sender) {
_transferOwnership(_owner);
}
}
/**
* @notice Register a token with this tax hook
* @param customToken The custom token address (not ETH)
* @param key The pool key for TOKEN/ETH pair
* @param taxBps Tax rate in basis points
*/
function registerToken(address customToken, PoolKey calldata key, uint16 taxBps) external {
require(customToken != address(0), "TaxHook: Invalid custom token");
require(taxBps <= Constants.MAX_TOKEN_TAX_BPS, "TaxHook: Tax rate exceeds maximum");
// Verify pool contains native ETH (address(0)) and customToken
address token0 = Currency.unwrap(key.currency0);
address token1 = Currency.unwrap(key.currency1);
require(
(token0 == address(0) && token1 == customToken) || (token0 == customToken && token1 == address(0)),
"TaxHook: Pool must be TOKEN/ETH pair"
);
// Verify exactly one currency is ETH (Fix #9)
require(
(token0 == address(0)) != (token1 == address(0)),
"TaxHook: Pool must have exactly one native ETH currency"
);
TokenTaxConfig storage config = tokenTaxConfigs[customToken];
require(!config.enabled, "TaxHook: Token already registered");
config.enabled = true;
config.taxBps = taxBps;
config.collected = 0;
config.withdrawn = 0;
config.exemptFromOwnerCut = false;
config.tokenOwner = msg.sender;
PoolId poolId = key.toId();
emit TokenRegistered(customToken, poolId, taxBps, msg.sender);
}
// ============================================
// TOKEN OWNER FUNCTIONS
// ============================================
/**
* @notice Transfer ownership of a token registration
* @param customToken The token to transfer ownership of
* @param newOwner The new owner address
*/
function transferTokenOwnership(address customToken, address newOwner) external {
TokenTaxConfig storage config = tokenTaxConfigs[customToken];
require(config.enabled, "TaxHook: Token not registered");
require(msg.sender == config.tokenOwner, "TaxHook: Only token owner can transfer");
// Validation for new owner (Fix #12)
require(newOwner != address(0), "TaxHook: Cannot transfer to zero address");
require(newOwner != address(this), "TaxHook: Cannot transfer to hook contract");
// If there's an active tax rate or unwithrawn taxes, be extra cautious
uint256 unwithdrawnTax = config.collected - config.withdrawn;
if (config.taxBps > 0 || unwithdrawnTax > 0) {
require(newOwner != address(0), "TaxHook: Cannot transfer active config to zero address");
}
address previousOwner = config.tokenOwner;
config.tokenOwner = newOwner;
emit TokenOwnershipTransferred(customToken, previousOwner, newOwner);
}
/**
* @notice Update a token's tax rate (can only decrease, not increase)
* @param customToken The token to update
* @param newTaxBps New tax rate in basis points (must be less than or equal to current rate)
*/
function updateTokenTaxRate(address customToken, uint16 newTaxBps) external {
TokenTaxConfig storage config = tokenTaxConfigs[customToken];
require(config.enabled, "TaxHook: Token not registered");
require(msg.sender == config.tokenOwner, "TaxHook: Only token owner can update");
require(newTaxBps <= config.taxBps, "TaxHook: Can only decrease tax rate");
config.taxBps = newTaxBps;
}
/**
* @notice Withdraws accumulated token-specific taxes (in native ETH)
* @param customToken The custom token whose tax to withdraw
* @param recipient The address to send the withdrawn tax to
* @dev Only callable by the token owner
*/
function withdrawTokenTax(address customToken, address recipient) external nonReentrant {
require(recipient != address(0), "TaxHook: Invalid recipient");
TokenTaxConfig storage config = tokenTaxConfigs[customToken];
require(config.enabled, "TaxHook: Token not registered");
require(msg.sender == config.tokenOwner, "TaxHook: Only token owner can withdraw");
uint256 unwithdrawnTotal = config.collected - config.withdrawn;
require(unwithdrawnTotal > 0, "TaxHook: No taxes to withdraw");
// Check ETH balance
uint256 balance = address(this).balance;
require(balance >= unwithdrawnTotal, "TaxHook: Insufficient ETH balance");
// Update the withdrawn amount
config.withdrawn += unwithdrawnTotal;
// Transfer ETH to recipient
(bool success,) = recipient.call{value: unwithdrawnTotal}("");
require(success, "TaxHook: ETH transfer failed");
emit TaxWithdrawn(recipient, unwithdrawnTotal, false);
}
// ============================================
// HOOK OWNER FUNCTIONS
// ============================================
/**
* @notice Exempt a token from owner cut
* @param customToken The token to exempt
* @param exempt Whether to exempt the token
*/
function exemptToken(address customToken, bool exempt) external onlyOwner {
TokenTaxConfig storage config = tokenTaxConfigs[customToken];
require(config.enabled, "TaxHook: Token not registered");
config.exemptFromOwnerCut = exempt;
emit TokenExemptionUpdated(customToken, exempt);
}
/**
* @notice Withdraws accumulated owner taxes (in native ETH)
* @dev Only callable by owner. Withdraws all accumulated ETH across all tokens.
*/
function withdrawOwnerTax() external onlyOwner nonReentrant {
// Calculate unwithdrawn amount
uint256 unwithdrawnTotal = ownerTaxCollected - ownerTaxWithdrawn;
require(unwithdrawnTotal > 0, "TaxHook: No owner taxes to withdraw");
// Check ETH balance
uint256 balance = address(this).balance;
require(balance >= unwithdrawnTotal, "TaxHook: Insufficient ETH balance");
// Update withdrawn amount
ownerTaxWithdrawn += unwithdrawnTotal;
// Transfer ETH to owner
(bool success,) = owner().call{value: unwithdrawnTotal}("");
require(success, "TaxHook: ETH transfer failed");
emit TaxWithdrawn(owner(), unwithdrawnTotal, true);
}
// ============================================
// HOOK PERMISSIONS & CORE LOGIC
// ============================================
/**
* @notice Define the hook permissions
* @return Hooks.Permissions The hook's permissions
*/
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeAddLiquidity: false,
afterAddLiquidity: false,
beforeRemoveLiquidity: false,
afterRemoveLiquidity: false,
beforeSwap: true, // Using beforeSwap to tax inflows
afterSwap: true, // Using afterSwap to tax outflows
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: true, // Now enabling this to return a delta in beforeSwap
afterSwapReturnDelta: true, // Now enabling this to return a delta in afterSwap
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
/**
* @notice Calculate tax amount based on value and tax rate
* @param value The value to calculate tax on
* @param taxRateBps The tax rate in basis points
* @return taxAmount The calculated tax amount
*/
function _calculateTax(uint256 value, uint16 taxRateBps) internal pure returns (uint256) {
return (value * taxRateBps) / TAX_RATE_DENOMINATOR;
}
/**
* @notice Calculate tax breakdown: total, owner cut, and token wallet cut
* @param amount The amount to calculate tax on
* @param config The token tax configuration
* @return totalTaxAmount Total tax to collect
* @return ownerCut Amount going to hook owner
* @return tokenWalletCut Amount going to token owner
*/
function _calculateTaxBreakdown(uint256 amount, TokenTaxConfig storage config)
internal
view
returns (uint256 totalTaxAmount, uint256 ownerCut, uint256 tokenWalletCut)
{
totalTaxAmount = _calculateTax(amount, config.taxBps);
if (totalTaxAmount > 0) {
ownerCut = 0;
if (!config.exemptFromOwnerCut && ownerCutBps > 0) {
ownerCut = _calculateTax(totalTaxAmount, ownerCutBps);
}
tokenWalletCut = totalTaxAmount - ownerCut;
}
}
/**
* @notice Record collected taxes in storage
* @param customToken The token address
* @param ownerCut Amount to add to owner's collected taxes
* @param tokenWalletCut Amount to add to token owner's collected taxes
*/
function _recordCollectedTax(address customToken, uint256 ownerCut, uint256 tokenWalletCut) internal {
if (ownerCut > 0) {
ownerTaxCollected += ownerCut;
}
if (tokenWalletCut > 0) {
tokenTaxConfigs[customToken].collected += tokenWalletCut;
}
}
/**
* @notice Emit tax collected event
* @param key The pool key
* @param customToken The token address
* @param totalTaxAmount Total tax collected
* @param ownerCut Amount for hook owner
* @param tokenWalletCut Amount for token owner
* @param isInflow Whether this is an inflow (beforeSwap) or outflow (afterSwap)
*/
function _emitTaxCollectedEvent(
PoolKey calldata key,
address customToken,
uint256 totalTaxAmount,
uint256 ownerCut,
uint256 tokenWalletCut,
bool isInflow
) internal {
PoolId poolId = key.toId();
emit TaxCollected(poolId, customToken, totalTaxAmount, ownerCut, tokenWalletCut, isInflow);
}
/**
* @notice Swaps tokens to ETH via the pool
* @dev Used when we need to convert token-denominated tax to ETH
* @param key The pool key
* @param tokenAmount Amount of tokens to swap
* @return ethReceived Amount of ETH received from the swap
*/
function _swapTokensToEth(PoolKey calldata key, uint256 tokenAmount) internal returns (uint256 ethReceived) {
uint256 ethBefore = address(this).balance;
// Set flag to prevent taxing this internal swap
_inInternalSwap = true;
// Execute token -> ETH swap (exact input)
BalanceDelta delta = poolManager.swap(
key,
SwapParams({
zeroForOne: false, // Token (currency1) -> ETH (currency0)
amountSpecified: -int256(tokenAmount), // Negative = exact input
sqrtPriceLimitX96: MAX_PRICE_LIMIT
}),
bytes("")
);
// Manually settle the swap deltas
Currency ethCurrency = CurrencyLibrary.ADDRESS_ZERO;
Currency tokenCurrency = key.currency1;
// Settle token input (negative delta = we owe tokens to pool)
if (delta.amount1() < 0) {
CurrencySettler.settle(tokenCurrency, poolManager, address(this), uint256(int256(-delta.amount1())), false);
}
// Take ETH output (positive delta = pool owes us ETH)
if (delta.amount0() > 0) {
CurrencySettler.take(ethCurrency, poolManager, address(this), uint256(int256(delta.amount0())), false);
}
// Clear flag
_inInternalSwap = false;
ethReceived = address(this).balance - ethBefore;
}
/**
* @notice Hook called before a swap to tax inflows
* @dev Handles: Scenario 1 - Exact input swap where ETH is input (buying tokens with exact ETH)
* @param key The pool key
* @param params The swap parameters
* @return selector The function selector
* @return delta Any delta to apply
* @return gasLimit The gas limit for the swap
*/
function _beforeSwap(address, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
// Skip taxing internal swaps (prevents recursion when converting token tax to ETH)
if (_inInternalSwap) {
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, FEE_OVERRIDE);
}
// Initialize with default values
BeforeSwapDelta deltaOut = BeforeSwapDeltaLibrary.ZERO_DELTA;
// Identify the custom token (non-ETH token)
address token0 = Currency.unwrap(key.currency0);
address token1 = Currency.unwrap(key.currency1);
address customToken = (token0 == address(0)) ? token1 : token0;
TokenTaxConfig storage config = tokenTaxConfigs[customToken];
// Skip if tax is not enabled for this token
if (!config.enabled) {
return (BaseHook.beforeSwap.selector, deltaOut, FEE_OVERRIDE);
}
// We always tax native ETH
Currency taxCurrency = CurrencyLibrary.ADDRESS_ZERO;
bool ethIsToken0 = (token0 == address(0));
// Determine if ETH is being used as input in this swap
bool isEthInput = (ethIsToken0 && params.zeroForOne) || (!ethIsToken0 && !params.zeroForOne);
// Scenario 1: Exact input swap where ETH is input (buying tokens with exact ETH amount)
// - amountSpecified < 0 (negative indicates exact input in V4)
// - ETH is the input currency
if (isEthInput && params.amountSpecified < 0) {
// Calculate absolute swap amount
uint256 absAmount = uint256(-params.amountSpecified);
// Calculate tax breakdown using helper
(uint256 totalTaxAmount, uint256 ownerCut, uint256 tokenWalletCut) =
_calculateTaxBreakdown(absAmount, config);
if (totalTaxAmount > 0) {
// Record collected taxes first (CEI pattern - Fix #8)
_recordCollectedTax(customToken, ownerCut, tokenWalletCut);
// Take the total tax from the pool
poolManager.take(taxCurrency, address(this), totalTaxAmount);
// Return a POSITIVE delta to balance out the debt created by take()
deltaOut = toBeforeSwapDelta(int128(int256(totalTaxAmount)), 0);
// Emit event using helper
_emitTaxCollectedEvent(key, customToken, totalTaxAmount, ownerCut, tokenWalletCut, true);
}
}
return (BaseHook.beforeSwap.selector, deltaOut, FEE_OVERRIDE);
}
/**
* @notice Hook called after a swap to tax ETH flows
* @dev Handles:
* - Scenario 2: Exact output swap where ETH is input (buying exact tokens with ETH)
* - Scenario 3: Exact input swap where ETH is output (selling exact tokens for ETH)
* - Scenario 4: Exact output swap where ETH is output (selling tokens for exact ETH)
* @param key The pool key
* @param params The swap parameters
* @param delta The balance delta from the swap
* @return selector The function selector
* @return afterDelta Any additional amount to withdraw
*/
function _afterSwap(address, PoolKey calldata key, SwapParams calldata params, BalanceDelta delta, bytes calldata)
internal
override
returns (bytes4, int128)
{
// Skip taxing internal swaps (prevents recursion when converting token tax to ETH)
if (_inInternalSwap) {
return (BaseHook.afterSwap.selector, 0);
}
// Default value for afterDelta
int128 afterDelta = 0;
// Identify the custom token (non-ETH token)
address token0 = Currency.unwrap(key.currency0);
address token1 = Currency.unwrap(key.currency1);
address customToken = (token0 == address(0)) ? token1 : token0;
TokenTaxConfig storage config = tokenTaxConfigs[customToken];
// Skip if tax is not enabled for this token
if (!config.enabled) {
return (BaseHook.afterSwap.selector, afterDelta);
}
// We always tax native ETH
Currency taxCurrency = CurrencyLibrary.ADDRESS_ZERO;
bool ethIsToken0 = (token0 == address(0));
// Get the ETH delta (amount0 if ETH is token0, otherwise amount1)
int128 relevantDelta = ethIsToken0 ? delta.amount0() : delta.amount1();
// Determine swap direction
bool isEthInput = (ethIsToken0 && params.zeroForOne) || (!ethIsToken0 && !params.zeroForOne);
bool isEthOutput = !isEthInput;
// Scenario 2: Exact output swap where ETH is input (buying exact amount of tokens with ETH)
// - amountSpecified > 0 (positive indicates exact output in V4)
// - ETH is the input currency
// - relevantDelta < 0 (ETH flowing INTO the pool, negative delta)
if (isEthInput && params.amountSpecified > 0 && relevantDelta < 0) {
// Tax the absolute amount of ETH consumed
uint256 absAmount = uint256(int256(-relevantDelta));
// Calculate tax breakdown using helper
(uint256 totalTaxAmount, uint256 ownerCut, uint256 tokenWalletCut) =
_calculateTaxBreakdown(absAmount, config);
if (totalTaxAmount > 0) {
// Record collected taxes first (CEI pattern - Fix #8)
_recordCollectedTax(customToken, ownerCut, tokenWalletCut);
// Take the tax from the pool
poolManager.take(taxCurrency, address(this), totalTaxAmount);
// Return a POSITIVE delta to balance out the debt created by take()
afterDelta = int128(int256(totalTaxAmount));
// Emit event using helper (isInflow=true because ETH is flowing in)
_emitTaxCollectedEvent(key, customToken, totalTaxAmount, ownerCut, tokenWalletCut, true);
}
}
// Scenario 3: Exact input sell (selling exact tokens for ETH)
// - amountSpecified < 0 (negative indicates exact input)
// - ETH is the output currency
// - relevantDelta > 0 (ETH flowing OUT of the pool, positive delta)
else if (isEthOutput && relevantDelta > 0 && params.amountSpecified < 0) {
// Tax the absolute amount of ETH received
uint256 absAmount = uint256(int256(relevantDelta));
// Calculate tax breakdown using helper
(uint256 totalTaxAmount, uint256 ownerCut, uint256 tokenWalletCut) =
_calculateTaxBreakdown(absAmount, config);
if (totalTaxAmount > 0) {
// Record collected taxes first (CEI pattern - Fix #8)
_recordCollectedTax(customToken, ownerCut, tokenWalletCut);
// Take the tax from the pool
poolManager.take(taxCurrency, address(this), totalTaxAmount);
// Return a POSITIVE delta to balance out the debt created by take()
afterDelta = int128(int256(totalTaxAmount));
// Emit event using helper (isInflow=false because ETH is flowing out)
_emitTaxCollectedEvent(key, customToken, totalTaxAmount, ownerCut, tokenWalletCut, false);
}
}
// Scenario 4: Exact output sell (selling tokens for exact ETH amount)
// - amountSpecified > 0 (positive indicates exact output)
// - ETH is the output currency
// - relevantDelta > 0 (ETH flowing OUT)
// - Problem: Can't take more ETH from pool (user specified exact ETH amount)
// - Solution: Take equivalent token tax and swap to ETH immediately
else if (isEthOutput && relevantDelta > 0 && params.amountSpecified > 0) {
// User specified exact ETH output, so we tax the token input instead
// Get token delta (the amount of tokens user sent)
int128 tokenDelta = ethIsToken0 ? delta.amount1() : delta.amount0();
// Token delta should be negative (user sending tokens to pool)
if (tokenDelta < 0) {
uint256 tokenAmount = uint256(int256(-tokenDelta));
// Calculate tax on tokens
(uint256 totalTokenTax, uint256 ownerCut, uint256 tokenWalletCut) =
_calculateTaxBreakdown(tokenAmount, config);
if (totalTokenTax > 0) {
// Note: Scenario 4 requires taking tokens and swapping them before we know the final ETH amount
// We cannot follow strict CEI pattern here because state update depends on swap result
// The _inInternalSwap flag protects against reentrancy during the swap
// Take token tax from pool
Currency tokenCurrency = ethIsToken0 ? key.currency1 : key.currency0;
poolManager.take(tokenCurrency, address(this), totalTokenTax);
// Immediately swap tokens to ETH
uint256 ethReceived = _swapTokensToEth(key, totalTokenTax);
// Now distribute the ETH received as normal
if (ethReceived > 0) {
// Recalculate breakdown based on actual ETH received (accounts for slippage)
uint256 finalOwnerCut = 0;
if (!config.exemptFromOwnerCut && ownerCutBps > 0) {
finalOwnerCut = _calculateTax(ethReceived, ownerCutBps);
}
uint256 finalTokenWalletCut = ethReceived - finalOwnerCut;
// Record collected taxes (must be after swap to get correct amounts)
_recordCollectedTax(customToken, finalOwnerCut, finalTokenWalletCut);
// Emit event
_emitTaxCollectedEvent(key, customToken, ethReceived, finalOwnerCut, finalTokenWalletCut, false);
// Return token tax amount as delta
afterDelta = int128(int256(totalTokenTax));
}
}
}
}
return (BaseHook.afterSwap.selector, afterDelta);
}
// Required receive function to handle ETH transfers
receive() external payable {}
// ============================================
// VIEW FUNCTIONS
// ============================================
/**
* @notice Get the owner's global tax info
* @return collected Total amount of owner tax collected (in native ETH)
* @return withdrawn Total amount of owner tax withdrawn
*/
function getOwnerTaxInfo() external view returns (uint256 collected, uint256 withdrawn) {
return (ownerTaxCollected, ownerTaxWithdrawn);
}
/**
* @notice Get the token-specific tax info
* @param customToken The custom token address
* @return collected Amount collected for the token owner (in native ETH)
* @return withdrawn Amount withdrawn by the token owner
*/
function getTokenTaxInfo(address customToken) external view returns (uint256 collected, uint256 withdrawn) {
TokenTaxConfig storage config = tokenTaxConfigs[customToken];
return (config.collected, config.withdrawn);
}
/**
* @notice Get the owner of a token registration
* @param customToken The custom token address
* @return tokenOwner Address that owns this token's configuration
*/
function getTokenOwner(address customToken) external view returns (address tokenOwner) {
TokenTaxConfig storage config = tokenTaxConfigs[customToken];
require(config.enabled, "TaxHook: Token not registered");
return config.tokenOwner;
}
}
"
},
"lib/v4-core/src/libraries/Hooks.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {PoolKey} from "../types/PoolKey.sol";
import {IHooks} from "../interfaces/IHooks.sol";
import {SafeCast} from "./SafeCast.sol";
import {LPFeeLibrary} from "./LPFeeLibrary.sol";
import {BalanceDelta, toBalanceDelta, BalanceDeltaLibrary} from "../types/BalanceDelta.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../types/BeforeSwapDelta.sol";
import {IPoolManager} from "../interfaces/IPoolManager.sol";
import {ModifyLiquidityParams, SwapParams} from "../types/PoolOperation.sol";
import {ParseBytes} from "./ParseBytes.sol";
import {CustomRevert} from "./CustomRevert.sol";
/// @notice V4 decides whether to invoke specific hooks by inspecting the least significant bits
/// of the address that the hooks contract is deployed to.
/// For example, a hooks contract deployed to address: 0x0000000000000000000000000000000000002400
/// has the lowest bits '10 0100 0000 0000' which would cause the 'before initialize' and 'after add liquidity' hooks to be used.
library Hooks {
using LPFeeLibrary for uint24;
using Hooks for IHooks;
using SafeCast for int256;
using BeforeSwapDeltaLibrary for BeforeSwapDelta;
using ParseBytes for bytes;
using CustomRevert for bytes4;
uint160 internal constant ALL_HOOK_MASK = uint160((1 << 14) - 1);
uint160 internal constant BEFORE_INITIALIZE_FLAG = 1 << 13;
uint160 internal constant AFTER_INITIALIZE_FLAG = 1 << 12;
uint160 internal constant BEFORE_ADD_LIQUIDITY_FLAG = 1 << 11;
uint160 internal constant AFTER_ADD_LIQUIDITY_FLAG = 1 << 10;
uint160 internal constant BEFORE_REMOVE_LIQUIDITY_FLAG = 1 << 9;
uint160 internal constant AFTER_REMOVE_LIQUIDITY_FLAG = 1 << 8;
uint160 internal constant BEFORE_SWAP_FLAG = 1 << 7;
uint160 internal constant AFTER_SWAP_FLAG = 1 << 6;
uint160 internal constant BEFORE_DONATE_FLAG = 1 << 5;
uint160 internal constant AFTER_DONATE_FLAG = 1 << 4;
uint160 internal constant BEFORE_SWAP_RETURNS_DELTA_FLAG = 1 << 3;
uint160 internal constant AFTER_SWAP_RETURNS_DELTA_FLAG = 1 << 2;
uint160 internal constant AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG = 1 << 1;
uint160 internal constant AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG = 1 << 0;
struct Permissions {
bool beforeInitialize;
bool afterInitialize;
bool beforeAddLiquidity;
bool afterAddLiquidity;
bool beforeRemoveLiquidity;
bool afterRemoveLiquidity;
bool beforeSwap;
bool afterSwap;
bool beforeDonate;
bool afterDonate;
bool beforeSwapReturnDelta;
bool afterSwapReturnDelta;
bool afterAddLiquidityReturnDelta;
bool afterRemoveLiquidityReturnDelta;
}
/// @notice Thrown if the address will not lead to the specified hook calls being called
/// @param hooks The address of the hooks contract
error HookAddressNotValid(address hooks);
/// @notice Hook did not return its selector
error InvalidHookResponse();
/// @notice Additional context for ERC-7751 wrapped error when a hook call fails
error HookCallFailed();
/// @notice The hook's delta changed the swap from exactIn to exactOut or vice versa
error HookDeltaExceedsSwapAmount();
/// @notice Utility function intended to be used in hook constructors to ensure
/// the deployed hooks address causes the intended hooks to be called
/// @param permissions The hooks that are intended to be called
/// @dev permissions param is memory as the function will be called from constructors
function validateHookPermissions(IHooks self, Permissions memory permissions) internal pure {
if (
permissions.beforeInitialize != self.hasPermission(BEFORE_INITIALIZE_FLAG)
|| permissions.afterInitialize != self.hasPermission(AFTER_INITIALIZE_FLAG)
|| permissions.beforeAddLiquidity != self.hasPermission(BEFORE_ADD_LIQUIDITY_FLAG)
|| permissions.afterAddLiquidity != self.hasPermission(AFTER_ADD_LIQUIDITY_FLAG)
|| permissions.beforeRemoveLiquidity != self.hasPermission(BEFORE_REMOVE_LIQUIDITY_FLAG)
|| permissions.afterRemoveLiquidity != self.hasPermission(AFTER_REMOVE_LIQUIDITY_FLAG)
|| permissions.beforeSwap != self.hasPermission(BEFORE_SWAP_FLAG)
|| permissions.afterSwap != self.hasPermission(AFTER_SWAP_FLAG)
|| permissions.beforeDonate != self.hasPermission(BEFORE_DONATE_FLAG)
|| permissions.afterDonate != self.hasPermission(AFTER_DONATE_FLAG)
|| permissions.beforeSwapReturnDelta != self.hasPermission(BEFORE_SWAP_RETURNS_DELTA_FLAG)
|| permissions.afterSwapReturnDelta != self.hasPermission(AFTER_SWAP_RETURNS_DELTA_FLAG)
|| permissions.afterAddLiquidityReturnDelta != self.hasPermission(AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG)
|| permissions.afterRemoveLiquidityReturnDelta
!= self.hasPermission(AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG)
) {
HookAddressNotValid.selector.revertWith(address(self));
}
}
/// @notice Ensures that the hook address includes at least one hook flag or dynamic fees, or is the 0 address
/// @param self The hook to verify
/// @param fee The fee of the pool the hook is used with
/// @return bool True if the hook address is valid
function isValidHookAddress(IHooks self, uint24 fee) internal pure returns (bool) {
// The hook can only have a flag to return a hook delta on an action if it also has the corresponding action flag
if (!self.hasPermission(BEFORE_SWAP_FLAG) && self.hasPermission(BEFORE_SWAP_RETURNS_DELTA_FLAG)) return false;
if (!self.hasPermission(AFTER_SWAP_FLAG) && self.hasPermission(AFTER_SWAP_RETURNS_DELTA_FLAG)) return false;
if (!self.hasPermission(AFTER_ADD_LIQUIDITY_FLAG) && self.hasPermission(AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG))
{
return false;
}
if (
!self.hasPermission(AFTER_REMOVE_LIQUIDITY_FLAG)
&& self.hasPermission(AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG)
) return false;
// If there is no hook contract set, then fee cannot be dynamic
// If a hook contract is set, it must have at least 1 flag set, or have a dynamic fee
return address(self) == address(0)
? !fee.isDynamicFee()
: (uint160(address(self)) & ALL_HOOK_MASK > 0 || fee.isDynamicFee());
}
/// @notice performs a hook call using the given calldata on the given hook that doesn't return a delta
/// @return result The complete data returned by the hook
function callHook(IHooks self, bytes memory data) internal returns (bytes memory result) {
bool success;
assembly ("memory-safe") {
success := call(gas(), self, 0, add(data, 0x20), mload(data), 0, 0)
}
// Revert with FailedHookCall, containing any error message to bubble up
if (!success) CustomRevert.bubbleUpAndRevertWith(address(self), bytes4(data), HookCallFailed.selector);
// The call was successful, fetch the returned data
assembly ("memory-safe") {
// allocate result byte array from the free memory pointer
result := mload(0x40)
// store new free memory pointer at the end of the array padded to 32 bytes
mstore(0x40, add(result, and(add(returndatasize(), 0x3f), not(0x1f))))
// store length in memory
mstore(result, returndatasize())
// copy return data to result
returndatacopy(add(result, 0x20), 0, returndatasize())
}
// Length must be at least 32 to contain the selector. Check expected selector and returned selector match.
if (result.length < 32 || result.parseSelector() != data.parseSelector()) {
InvalidHookResponse.selector.revertWith();
}
}
/// @notice performs a hook call using the given calldata on the given hook
/// @return int256 The delta returned by the hook
function callHookWithReturnDelta(IHooks self, bytes memory data, bool parseReturn) internal returns (int256) {
bytes memory result = callHook(self, data);
// If this hook wasn't meant to return something, default to 0 delta
if (!parseReturn) return 0;
// A length of 64 bytes is required to return a bytes4, and a 32 byte delta
if (result.length != 64) InvalidHookResponse.selector.revertWith();
return result.parseReturnDelta();
}
/// @notice modifier to prevent calling a hook if they initiated the action
modifier noSelfCall(IHooks self) {
if (msg.sender != address(self)) {
_;
}
}
/// @notice calls beforeInitialize hook if permissioned and validates return value
function beforeInitialize(IHooks self, PoolKey memory key, uint160 sqrtPriceX96) internal noSelfCall(self) {
if (self.hasPermission(BEFORE_INITIALIZE_FLAG)) {
self.callHook(abi.encodeCall(IHooks.beforeInitialize, (msg.sender, key, sqrtPriceX96)));
}
}
/// @notice calls afterInitialize hook if permissioned and validates return value
function afterInitialize(IHooks self, PoolKey memory key, uint160 sqrtPriceX96, int24 tick)
internal
noSelfCall(self)
{
if (self.hasPermission(AFTER_INITIALIZE_FLAG)) {
self.callHook(abi.encodeCall(IHooks.afterInitialize, (msg.sender, key, sqrtPriceX96, tick)));
}
}
/// @notice calls beforeModifyLiquidity hook if permissioned and validates return value
function beforeModifyLiquidity(
IHooks self,
PoolKey memory key,
ModifyLiquidityParams memory params,
bytes calldata hookData
) internal noSelfCall(self) {
if (params.liquidityDelta > 0 && self.hasPermission(BEFORE_ADD_LIQUIDITY_FLAG)) {
self.callHook(abi.encodeCall(IHooks.beforeAddLiquidity, (msg.sender, key, params, hookData)));
} else if (params.liquidityDelta <= 0 && self.hasPermission(BEFORE_REMOVE_LIQUIDITY_FLAG)) {
self.callHook(abi.encodeCall(IHooks.beforeRemoveLiquidity, (msg.sender, key, params, hookData)));
}
}
/// @notice calls afterModifyLiquidity hook if permissioned and validates return value
function afterModifyLiquidity(
IHooks self,
PoolKey memory key,
ModifyLiquidityParams memory params,
BalanceDelta delta,
BalanceDelta feesAccrued,
bytes calldata hookData
) internal returns (BalanceDelta callerDelta, BalanceDelta hookDelta) {
if (msg.sender == address(self)) return (delta, BalanceDeltaLibrary.ZERO_DELTA);
callerDelta = delta;
if (params.liquidityDelta > 0) {
if (self.hasPermission(AFTER_ADD_LIQUIDITY_FLAG)) {
hookDelta = BalanceDelta.wrap(
self.callHookWithReturnDelta(
abi.encodeCall(
IHooks.afterAddLiquidity, (msg.sender, key, params, delta, feesAccrued, hookData)
),
self.hasPermission(AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG)
)
);
callerDelta = callerDelta - hookDelta;
}
} else {
if (self.hasPermission(AFTER_REMOVE_LIQUIDITY_FLAG)) {
hookDelta = BalanceDelta.wrap(
self.callHookWithReturnDelta(
abi.encodeCall(
IHooks.afterRemoveLiquidity, (msg.sender, key, params, delta, feesAccrued, hookData)
),
self.hasPermission(AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG)
)
);
callerDelta = callerDelta - hookDelta;
}
}
}
/// @notice calls beforeSwap hook if permissioned and validates return value
function beforeSwap(IHooks self, PoolKey memory key, SwapParams memory params, bytes calldata hookData)
internal
returns (int256 amountToSwap, BeforeSwapDelta hookReturn, uint24 lpFeeOverride)
{
amountToSwap = params.amountSpecified;
if (msg.sender == address(self)) return (amountToSwap, BeforeSwapDeltaLibrary.ZERO_DELTA, lpFeeOverride);
if (self.hasPermission(BEFORE_SWAP_FLAG)) {
bytes memory result = callHook(self, abi.encodeCall(IHooks.beforeSwap, (msg.sender, key, params, hookData)));
// A length of 96 bytes is required to return a bytes4, a 32 byte delta, and an LP fee
if (result.length != 96) InvalidHookResponse.selector.revertWith();
// dynamic fee pools that want to override the cache fee, return a valid fee with the override flag. If override flag
// is set but an invalid fee is returned, the transaction will revert. Otherwise the current LP fee will be used
if (key.fee.isDynamicFee()) lpFeeOverride = result.parseFee();
// skip this logic for the case where the hook return is 0
if (self.hasPermission(BEFORE_SWAP_RETURNS_DELTA_FLAG)) {
hookReturn = BeforeSwapDelta.wrap(result.parseReturnDelta());
// any return in unspecified is passed to the afterSwap hook for handling
int128 hookDeltaSpecified = hookReturn.getSpecifiedDelta();
// Update the swap amount according to the hook's return, and check that the swap type doesn't change (exact input/output)
if (hookDeltaSpecified != 0) {
bool exactInput = amountToSwap < 0;
amountToSwap += hookDeltaSpecified;
if (exactInput ? amountToSwap > 0 : amountToSwap < 0) {
HookDeltaExceedsSwapAmount.selector.revertWith();
}
}
}
}
}
/// @notice calls afterSwap hook if permissioned and validates return value
function afterSwap(
IHooks self,
PoolKey memory key,
SwapParams memory params,
BalanceDelta swapDelta,
bytes calldata hookData,
BeforeSwapDelta beforeSwapHookReturn
) internal returns (BalanceDelta, BalanceDelta) {
if (msg.sender == address(self)) return (swapDelta, BalanceDeltaLibrary.ZERO_DELTA);
int128 hookDeltaSpecified = beforeSwapHookReturn.getSpecifiedDelta();
int128 hookDeltaUnspecified = beforeSwapHookReturn.getUnspecifiedDelta();
if (self.hasPermission(AFTER_SWAP_FLAG)) {
hookDeltaUnspecified += self.callHookWithReturnDelta(
abi.encodeCall(IHooks.afterSwap, (msg.sender, key, params, swapDelta, hookData)),
self.hasPermission(AFTER_SWAP_RETURNS_DELTA_FLAG)
).toInt128();
}
BalanceDelta hookDelta;
if (hookDeltaUnspecified != 0 || hookDeltaSpecified != 0) {
hookDelta = (params.amountSpecified < 0 == params.zeroForOne)
? toBalanceDelta(hookDeltaSpecified, hookDeltaUnspecified)
: toBalanceDelta(hookDeltaUnspecified, hookDeltaSpecified);
// the caller has to pay for (or receive) the hook's delta
swapDelta = swapDelta - hookDelta;
}
return (swapDelta, hookDelta);
}
/// @notice calls beforeDonate hook if permissioned and validates return value
function beforeDonate(IHooks self, PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData)
internal
noSelfCall(self)
{
if (self.hasPermission(BEFORE_DONATE_FLAG)) {
self.callHook(abi.encodeCall(IHooks.beforeDonate, (msg.sender, key, amount0, amount1, hookData)));
}
}
/// @notice calls afterDonate hook if permissioned and validates return value
function afterDonate(IHooks self, PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData)
internal
noSelfCall(self)
{
if (self.hasPermission(AFTER_DONATE_FLAG)) {
self.callHook(abi.encodeCall(IHooks.afterDonate, (msg.sender, key, amount0, amount1, hookData)));
}
}
function hasPermission(IHooks self, uint160 flag) internal pure returns (bool) {
return uint160(address(self)) & flag != 0;
}
}
"
},
"lib/v4-core/src/interfaces/IPoolManager.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Currency} from "../types/Currency.sol";
import {PoolKey} from "../types/PoolKey.sol";
import {IHooks} from "./IHooks.sol";
import {IERC6909Claims} from "./external/IERC6909Claims.sol";
import {IProtocolFees} from "./IProtocolFees.sol";
import {BalanceDelta} from "../types/BalanceDelta.sol";
import {PoolId} from "../types/PoolId.sol";
import {IExtsload} from "./IExtsload.sol";
import {IExttload} from "./IExttload.sol";
import {ModifyLiquidityParams, SwapParams} from "../types/PoolOperation.sol";
/// @notice Interface for the PoolManager
interface IPoolManager is IProtocolFees, IERC6909Claims, IExtsload, IExttload {
/// @notice Thrown when a currency is not netted out after the contract is unlocked
error CurrencyNotSettled();
/// @notice Thrown when trying to interact with a non-initialized pool
error PoolNotInitialized();
/// @notice Thrown when unlock is called, but the contract is already unlocked
error AlreadyUnlocked();
/// @notice Thrown when a function is called that requires the contract to be unlocked, but it is not
error ManagerLocked();
/// @notice Pools are limited to type(int16).max tickSpacing in #initialize, to prevent overflow
error TickSpacingTooLarge(int24 tickSpacing);
/// @notice Pools must have a positive non-zero tickSpacing passed to #initialize
error TickSpacingTooSmall(int24 tickSpacing);
/// @notice PoolKey must have currencies where address(currency0) < address(currency1)
error CurrenciesOutOfOrderOrEqual(address currency0, address currency1);
/// @notice Thrown when a call to updateDynamicLPFee is made by an address that is not the hook,
/// or on a pool that does not have a dynamic swap fee.
error UnauthorizedDynamicLPFeeUpdate();
/// @notice Thrown when trying to swap amount of 0
error SwapAmountCannotBeZero();
///@notice Thrown when native currency is passed to a non native settlement
error NonzeroNativeValue();
/// @notice Thrown when `clear` is called with an amount that is not exactly equal to the open currency delta.
error MustClearExactPositiveDelta();
/// @notice Emitted when a new pool is initialized
/// @param id The abi encoded hash of the pool key struct for the new pool
/// @param currency0 The first currency of the pool by address sort order
/// @param currency1 The second currency of the pool by address sort order
/// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip
/// @param tickSpacing The minimum number of ticks between initialized ticks
/// @param hooks The hooks contract address for the pool, or address(0) if none
/// @param sqrtPriceX96 The price of the pool on initialization
/// @param tick The initial tick of the pool corresponding to the initialized price
event Initialize(
PoolId indexed id,
Currency indexed currency0,
Currency indexed currency1,
uint24 fee,
int24 tickSpacing,
IHooks hooks,
uint160 sqrtPriceX96,
int24 tick
);
/// @notice Emitted when a liquidity position is modified
/// @param id The abi encoded hash of the pool key struct for the pool that was modified
/// @param sender The address that modified the pool
/// @param tickLower The lower tick of the position
/// @param tickUpper The upper tick of the position
/// @param liquidityDelta The amount of liquidity that was added or removed
/// @param salt The extra data to make positions unique
event ModifyLiquidity(
PoolId indexed id, address indexed sender, int24 tickLower, int24 tickUpper, int256 liquidityDelta, bytes32 salt
);
/// @notice Emitted for swaps between currency0 and currency1
/// @param id The abi encoded hash of the pool key struct for the pool that was modified
/// @param sender The address that initiated the swap call, and that received the callback
/// @param amount0 The delta of the currency0 balance of the pool
/// @param amount1 The delta of the currency1 balance of the pool
/// @param sqrtPriceX96 The sqrt(price) of the pool after the swap, as a Q64.96
/// @param liquidity The liquidity of the pool after the swap
/// @param tick The log base 1.0001 of the price of the pool after the swap
/// @param fee The swap fee in hundredths of a bip
event Swap(
PoolId indexed id,
address indexed sender,
int128 amount0,
int128 amount1,
uint160 sqrtPriceX96,
uint128 liquidity,
int24 tick,
uint24 fee
);
/// @notice Emitted for donations
/// @param id The abi encoded hash of the pool key struct for the pool that was donated to
/// @param sender The address that initiated the donate call
/// @param amount0 The amount donated in currency0
/// @param amount1 The amount donated in currency1
event Donate(PoolId indexed id, address indexed sender, uint256 amount0, uint256 amount1);
/// @notice All interactions on the contract that account deltas require unlocking. A caller that calls `unlock` must implement
/// `IUnlockCallback(msg.sender).unlockCallback(data)`, where they interact with the remaining functions on this contract.
/// @dev The only functions callable without an unlocking are `initialize` and `updateDynamicLPFee`
/// @param data Any data to pass to the callback, via `IUnlockCallback(msg.sender).unlockCallback(data)`
/// @return The data returned by the call to `IUnlockCallback(msg.sender).unlockCallback(data)`
function unlock(bytes calldata data) external returns (bytes memory);
/// @notice Initialize the state for a given pool ID
/// @dev A swap fee totaling MAX_SWAP_FEE (100%) makes exact output swaps impossible since the input is entirely consumed by the fee
/// @param key The pool key for the pool to initialize
/// @param sqrtPriceX96 The initial square root price
/// @return tick The initial tick of the pool
function initialize(PoolKey memory key, uint160 sqrtPriceX96) external returns (int24 tick);
/// @notice Modify the liquidity for the given pool
/// @dev Poke by calling with a zero liquidityDelta
/// @param key The pool to modify liquidity in
/// @param params The parameters for modifying the liquidity
/// @param hookData The data to pass through to the add/removeLiquidity hooks
/// @return callerDelta The balance delta of the caller of modifyLiquidity. This is the total of both principal, fee deltas, and hook deltas if applicable
/// @return feesAccrued The balance delta of the fees generated in the liquidity range. Returned for informational purposes
/// @dev Note that feesAccrued can be artificially inflated by a malicious actor and integrators should be careful using the value
/// For pools with a single liquidity position, actors can donate to themselves to inflate feeGrowthGlobal (and consequently feesAccrued)
/// atomically donating and collecting fees in the same unlockCallback may make the inflated value more extreme
function modifyLiquidity(PoolKey memory key, ModifyLiquidityParams memory params, bytes calldata hookData)
external
returns (BalanceDelta callerDelta, BalanceDelta feesAccrued);
/// @notice Swap against the given pool
/// @param key The pool to swap in
/// @param params The parameters for swapping
/// @param hookData The data to pass through to the swap hooks
/// @return swapDelta The balance delta of the address swapping
/// @dev Swapping on low liquidity pools may cause unexpected swap amounts when liquidity available is less than amountSpecified.
/// Additionally note that if interacting with hooks that have the BEFORE_SWAP_RETURNS_DELTA_FLAG or AFTER_SWAP_RETURNS_DELTA_FLAG
/// the hook may alter the swap input/output. Integrators should perform checks on the returned swapDelta.
function swap(PoolKey memory key, SwapParams memory params, bytes calldata hookData)
external
returns (BalanceDelta swapDelta);
/// @notice Donate the given currency amounts to the in-range liquidity providers of a pool
/// @dev Calls to donate can be frontrun adding just-in-time liquidity, with the aim of receiving a portion donated funds.
/// Donors should keep this in mind when designing donation mechanisms.
/// @dev This function donates to in-range LPs at slot0.tick. In certain edge-cases of the swap algorithm, the `sqrtPrice` of
/// a pool can be at the lower boundary of tick `n`, but the `slot0.tick` of the pool is already `n - 1`. In this case a call to
/// `donate` would donate to tick `n - 1` (slot0.tick) not tick `n` (getTickAtSqrtPrice(slot0.sqrtPriceX96)).
/// Read the comments in `Pool.swap()` for more information about this.
/// @param key The key of the pool to donate to
/// @param amount0 The amount of currency0 to donate
/// @param amount1 The amount of currency1 to donate
/// @param hookData The data to pass through to the donate hooks
/// @return BalanceDelta The delta of the caller after the donate
function donate(PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData)
external
returns (BalanceDelta);
/// @notice Writes the current ERC20 balance of the specified currency to transient storage
/// This is used to checkpoint balances for the manager and derive deltas for the caller.
/// @dev This MUST be called before any ERC20 tokens are sent into the contract, but can be skipped
/// for native tokens because the amount to settl
Submitted on: 2025-10-21 13:32:52
Comments
Log in to comment.
No comments yet.