FoundationHookV2
EthereumFoundationHookV2 is a Uniswap v4 hook on Ethereum. Runs code on every swap. Can take tokens from swaps. Runs at pool creation.
- Contract name
- FoundationHookV2
How this hook works
Modular hookFoundation runs one token's pool with a fee in the paired coin and a set of add-on modules fixed at launch. The fee is shared between the platform, the pool creator and the modules, which each get a set budget to use.
Fee in the paired coin
Each trade pays a platform fee plus a pool creator fee, taken from the paired coin before or after the swap.
The code that does this (_beforeSwap):function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata) internal override returns (bytes4, BeforeSwapDelta, uint24) { // A standalone action may route a fee-paying swap through an external router. A swap callback // may never start another swap. Phase 3 restores the existing action only after full settlement. if (_phase != 0 && _phase != 2) revert ReentrantOperation(); if (PoolId.unwrap(key.toId()) != poolId) revert InvalidSwap(); _phase = _phase == 2 ? 3 : 1; uint256 specified = _specifiedAmount(params.amountSpecified); bool buy = params.zeroForOne == (quote < token); T.SwapContext memory context_ = T.SwapContext(poolId, sender, buy, params.amountSpecified < 0, specified, 0, 0, 0); _dispatch(T.BEFORE_SWAP, context_); if (buy != (params.amountSpecified < 0)) { return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } uint256 gross = specified; F.Result memory result; if (buy) result = F.quoteGross(gross, _creatorFeeBps(buy), feeCarry[buy]); else (gross, result) = F.quoteNet(specified, _creatorFeeBps(buy), feeCarry[buy]); // Carry is committed only after the actual full fill is validated in afterSwap. return (IHooks.beforeSwap.selector, toBeforeSwapDelta(int128(int256(_checkedFee(gross, result))), 0), 0); }Modules chosen at launch
The pool is set up with a list of modules and their share of the token creator fees. The list cannot change after launch.
The code that does this (_configureModules):function _configureModules(T.ModuleSelection[] memory selections) private { if (selections.length > T.MAX_MODULES) revert InvalidConfiguration(); address[] memory instances = new address[](selections.length); uint16[] memory shares = new uint16[](selections.length); uint256 totalSwapGas; for (uint256 i; i < selections.length; ++i) { T.ModuleSelection memory selection = selections[i]; if ( selection.factory.code.length == 0 || selection.factory.codehash != selection.factoryCodeHash || selection.configuration.length > 16_384 ) revert InvalidConfiguration(); T.ModuleContext memory context_ = T.ModuleContext(address(this), token, quote, creator, address(ledger), poolId); address instance = IFoundationModuleFactoryV1(selection.factory).createModule(context_, selection.configuration); if ( instance.code.length == 0 || instance.codehash != selection.moduleCodeHash || instance == address(poolManager) || instance == address(ledger) || instance == initializer ) { revert InvalidConfiguration(); } IFoundationModuleV1 module = IFoundationModuleV1(instance); T.Descriptor memory d = module.descriptor(); bytes32 configHash = keccak256(selection.configuration); if ( keccak256(abi.encode(module.context())) != keccak256(abi.encode(context_)) || module.configurationHash() != configHash || keccak256(abi.encode(d)) != selection.descriptorHash || d.abiVersion != 1 || d.moduleId == 0 || d.phases == 0 || d.phases > 7 || d.resources > 1 || d.beforeGas > 300_000 || d.afterGas > 300_000 || d.actionGas > 2_000_000 || ((d.phases & T.BEFORE_SWAP) != 0 && d.beforeGas < 10_000) // …Modules run on trades
Modules are called during each trade with a gas limit. If one fails, the trade still goes through.
The code that does this (_dispatch):function _dispatch(uint8 phase, T.SwapContext memory context_) private { for (uint256 i; i < _modules.length; ++i) { Module storage m = _modules[i]; if ((m.descriptor.phases & phase) == 0) continue; bool before_ = phase == T.BEFORE_SWAP; bytes4 selector = before_ ? IFoundationModuleV1.onBeforeSwap.selector : IFoundationModuleV1.onAfterSwap.selector; bool ok = _call( m, abi.encodeWithSelector(selector, context_), before_ ? m.descriptor.beforeGas : m.descriptor.afterGas, selector ); if (!ok) { if (before_ || !m.descriptor.failOpenAfter) revert ModuleFailure(i, phase); emit ModuleCallbackFailed(i, phase); } } }Fees held in a ledger
Fees go to a ledger contract where the platform, the pool creator and each module claim their part.
The code that does this (claimPlatform):function claimPlatform() external nonReentrant returns (uint256 amount) { amount = _bounded(platformReceived - platformClaimed); platformClaimed += amount; _pay(T.PLATFORM_RECIPIENT, amount); emit QuoteClaimed(T.PLATFORM_RECIPIENT, 0, amount); }
Who controls it: Nobody can change the fee or the modules after launch. The code cannot be upgraded.
Based on the hook’s published code.
What it can do
Its code executes inside each trade in pools that use it.
It can take or add tokens during a swap. Fees, burns and custom curves work this way, and so do scams.
It runs when a pool is created, often to lock in that pool’s settings.
Hook history
Nothing recorded yet.
Counted from what the hook recorded on-chain across all its pools, for every action in its published code, plus token transfers in and out of it.