BloomFeeHookV2
BaseBloomFeeHookV2 is a Uniswap v4 hook on Base. Runs code on every swap. Can take tokens from swaps. Runs at pool creation.
- Contract name
- BloomFeeHookV2
How this hook works
Trading taxA trading tax hook. It takes a cut of every trade, charges more right after launch and keeps a price record.
Takes a cut of every trade
On top of the normal pool fee, it keeps a share of each trade. The fee builds up in the hook until it is collected.
The code that does this (beforeSwap):function beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata) external onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) { PoolId id = key.toId(); Terms memory t = terms[id]; if (t.token == address(0)) revert InvalidPool(); _observe(id); bool exactInput = params.amountSpecified < 0; bool specifiedIs0 = exactInput == params.zeroForOne; if (specifiedIs0 != t.quoteIs0) return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); // Quote is the specified leg: exact-input buy or exact-output sell. uint256 extraBps = _extraBps(t); uint256 fee; if (exactInput) { uint256 gross = uint256(-params.amountSpecified); fee = _collect(t, sender, gross, Math.mulDiv(gross, extraBps, BPS)); } else { uint256 net = uint256(params.amountSpecified); uint256 gross = _grossForNet(net, t.creatorBps, extraBps); fee = _collectFromNet(t, sender, gross, net); } return (IHooks.beforeSwap.selector, toBeforeSwapDelta(int128(int256(fee)), 0), 0); }Higher at launch
The fee starts higher right after launch and comes down over time, which makes it costly for bots to buy first.
The code that does this (extraBps):function _extraBps(Terms memory t) private view returns (uint256) { uint256 end = uint256(t.launchedAt) + LAUNCH_FEE_DURATION; if (block.timestamp >= end) return 0; // The launcher's own first buy, in the launch transaction, pays the normal fee only. if (block.number == t.launchBlock && tx.origin == t.launcher) return 0; uint256 normal = FeeMath.PLATFORM_BPS + t.creatorBps; if (normal >= LAUNCH_FEE_BPS) return 0; return (LAUNCH_FEE_BPS - normal) * (end - block.timestamp) / LAUNCH_FEE_DURATION; }Keeps a price record
It records the pool’s price over time, so other apps can read a fair average price.
The code that does this (beforeSwap):function beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata) external onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) { PoolId id = key.toId(); Terms memory t = terms[id]; if (t.token == address(0)) revert InvalidPool(); _observe(id); bool exactInput = params.amountSpecified < 0; bool specifiedIs0 = exactInput == params.zeroForOne; if (specifiedIs0 != t.quoteIs0) return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); // Quote is the specified leg: exact-input buy or exact-output sell. uint256 extraBps = _extraBps(t); uint256 fee; if (exactInput) { uint256 gross = uint256(-params.amountSpecified); fee = _collect(t, sender, gross, Math.mulDiv(gross, extraBps, BPS)); } else { uint256 net = uint256(params.amountSpecified); uint256 gross = _grossForNet(net, t.creatorBps, extraBps); fee = _collectFromNet(t, sender, gross, net); } return (IHooks.beforeSwap.selector, toBeforeSwapDelta(int128(int256(fee)), 0), 0); }Only approved callers create pools
New pools with this hook can only be created by approved callers, for example its own factory or launcher.
The code that does this (beforeInitialize):function beforeInitialize(address sender, PoolKey calldata key, uint160) external onlyPoolManager returns (bytes4) { if (key.fee != 0 || key.tickSpacing != TICK_SPACING || address(key.hooks) != address(this)) revert InvalidPool(); address c0 = Currency.unwrap(key.currency0); address c1 = Currency.unwrap(key.currency1); (address curve, address quote, address executor, bytes32 existing) = factory.launchRouting(c0); address token = c0; if (curve == address(0)) { (curve, quote, executor, existing) = factory.launchRouting(c1); token = c1; } if (curve == address(0) || quote != (token == c0 ? c1 : c0) || sender != executor || existing != bytes32(0) || IHookExecutorV2(executor).feeHook() != address(this) || address(keys[token].hooks) != address(0) || BondingCurve(curve).phase() != BondingCurve.Phase.PoolActive) revert InvalidPool(); (, , address escrow, address platform, , address vault, , , , uint16 creatorBps, uint16 holderShareBps, uint16 buybackShareBps,) = BondingCurve(curve).settings(); (address launcher,,,,,,) = factory.launches(token); PoolId id = key.toId(); terms[id] = Terms(token, quote, curve, escrow, platform, vault, quote == c0, creatorBps, holderShareBps, buybackShareBps, launcher, uint64(block.timestamp), uint64(block.number)); keys[token] = key; IERC20(quote).forceApprove(escrow, type(uint256).max); IERC20(quote).forceApprove(token, type(uint256).max); IERC20(quote).forceApprove(vault, type(uint256).max); Oracle storage o = oracles[id]; o.lastUpdate = uint64(block.timestamp); o.observations[0] = Observation(uint64(block.timestamp), 0); o.count = 1; emit CanonicalPoolRegistered(token, id, quote); // …
Who controls it: Nobody. The hook has no admin functions, and the code cannot be changed.
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
- Pool fees 1 time
- Registered a new pool with its starting price 1 time
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.