UniHooks
ArbitrumUniHooks is a Uniswap v4 hook on Arbitrum, also known as UniHooksShared. Runs code on every swap. Can take tokens from swaps. Can block LP withdrawals. Gates who can add liquidity. Runs at pool creation.
- Contract name
- UniHooksShared
How this hook works
Trading taxA trading tax hook. It takes a tax on trades, pays extra to liquidity providers and limits some actions to approved addresses.
Takes a tax on trades
On top of the normal pool fee, it keeps part of each trade as a tax.
The code that does this (beforeSwap):function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata p, bytes calldata) internal override returns (bytes4, BeforeSwapDelta, uint24) { PoolId id = key.toId(); Config storage c = _cfg[id]; if (!seeded[id]) revert NotSeeded(); if (block.timestamp < c.tradingStart) revert TradingNotStarted(c.tradingStart); if (_allowlistActive(c) && !_allowed(id, sender)) revert NotAllowlisted(tx.origin); (bool buy,) = _isBuy(key, p); if (buy && _inWindow(id, c) && p.amountSpecified > 0) revert ExactOutputNotAllowedDuringLaunch(); if (c.sameBlockGuard) { if (buy && lastSellBlock[id][tx.origin] == block.number) revert SameBlockTrade(); if (!buy && lastBuyBlock[id][tx.origin] == block.number) revert SameBlockTrade(); } if (c.volatilityBpsPerTick > 0 || c.sandwichBps > 0) { (, int24 tick,,) = poolManager.getSlot0(id); _setTickBefore(tick); if (_openBlock[id] != block.number) { _openBlock[id] = block.number; _openTick[id] = tick; } } // Platform fee, part 1: when the amount the swapper fixed (input for exact-in, output for exact-out) // is the paired token, take 0.20% of it here. Otherwise it is taken in afterSwap from the other side. Currency specified = (p.amountSpecified < 0) == p.zeroForOne ? key.currency0 : key.currency1; if (Currency.unwrap(specified) != Currency.unwrap(projectTokenOf[id])) { uint256 fee = _abs(p.amountSpecified) * PROTOCOL_FEE_BPS / BPS; if (fee > 0) { poolManager.take(specified, protocolFeeTo, fee); emit ProtocolFee(id, Currency.unwrap(specified), fee); return (this.beforeSwap.selector, toBeforeSwapDelta(int128(int256(fee)), 0), 0); } // …Pays extra to liquidity providers
It sends extra rewards straight into the pool for liquidity providers.
The code that does this (afterSwap):function _afterSwap(address, PoolKey calldata key, SwapParams calldata p, BalanceDelta delta, bytes calldata) internal override returns (bytes4, int128) { (bool buy, Currency outCur) = _isBuy(key, p); PoolId id = key.toId(); bool exactIn = p.amountSpecified < 0; // Platform fee, part 2: the unspecified side is the paired token (exact-in sell, or exact-out buy). uint256 pfee; Currency unspecified = exactIn == p.zeroForOne ? key.currency1 : key.currency0; if (Currency.unwrap(unspecified) != Currency.unwrap(projectTokenOf[id])) { int128 u = unspecified == key.currency0 ? delta.amount0() : delta.amount1(); pfee = _abs(int256(u)) * PROTOCOL_FEE_BPS / BPS; if (pfee > 0) { poolManager.take(unspecified, protocolFeeTo, pfee); emit ProtocolFee(id, Currency.unwrap(unspecified), pfee); } } int128 outDelta = p.zeroForOne ? delta.amount1() : delta.amount0(); if (outDelta <= 0) return (this.afterSwap.selector, int128(uint128(pfee))); uint256 out = uint256(uint128(outDelta)); Config storage c = _cfg[id]; uint256 burnAmt; uint256 rewardAmt; uint256 treasuryAmt; uint256 launchAmt; if (buy) { bool inWindow = _inWindow(id, c); if (inWindow && !outCur.isAddressZero()) { uint256 supply = IERC20Supply(Currency.unwrap(outCur)).totalSupply(); uint256 maxOut = supply * c.snipeMaxBps / BPS; // The cap counts everything a wallet buys in one block, so splitting a buy does not get around it. uint256 inBlock = (_buyBlock[id][tx.origin] == block.number ? _boughtInBlock[id][tx.origin] : 0) + out; // …Approved addresses only
Some actions are limited to addresses on an approved list.
The code that does this (beforeSwap):function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata p, bytes calldata) internal override returns (bytes4, BeforeSwapDelta, uint24) { PoolId id = key.toId(); Config storage c = _cfg[id]; if (!seeded[id]) revert NotSeeded(); if (block.timestamp < c.tradingStart) revert TradingNotStarted(c.tradingStart); if (_allowlistActive(c) && !_allowed(id, sender)) revert NotAllowlisted(tx.origin); (bool buy,) = _isBuy(key, p); if (buy && _inWindow(id, c) && p.amountSpecified > 0) revert ExactOutputNotAllowedDuringLaunch(); if (c.sameBlockGuard) { if (buy && lastSellBlock[id][tx.origin] == block.number) revert SameBlockTrade(); if (!buy && lastBuyBlock[id][tx.origin] == block.number) revert SameBlockTrade(); } if (c.volatilityBpsPerTick > 0 || c.sandwichBps > 0) { (, int24 tick,,) = poolManager.getSlot0(id); _setTickBefore(tick); if (_openBlock[id] != block.number) { _openBlock[id] = block.number; _openTick[id] = tick; } } // Platform fee, part 1: when the amount the swapper fixed (input for exact-in, output for exact-out) // is the paired token, take 0.20% of it here. Otherwise it is taken in afterSwap from the other side. Currency specified = (p.amountSpecified < 0) == p.zeroForOne ? key.currency0 : key.currency1; if (Currency.unwrap(specified) != Currency.unwrap(projectTokenOf[id])) { uint256 fee = _abs(p.amountSpecified) * PROTOCOL_FEE_BPS / BPS; if (fee > 0) { poolManager.take(specified, protocolFeeTo, fee); emit ProtocolFee(id, Currency.unwrap(specified), fee); return (this.beforeSwap.selector, toBeforeSwapDelta(int128(int256(fee)), 0), 0); } // …Launch protection
It limits trading, for example a max buy or wallet size or a cooldown, to stop bots grabbing the supply before real traders.
The code that does this (afterSwap):function _afterSwap(address, PoolKey calldata key, SwapParams calldata p, BalanceDelta delta, bytes calldata) internal override returns (bytes4, int128) { (bool buy, Currency outCur) = _isBuy(key, p); PoolId id = key.toId(); bool exactIn = p.amountSpecified < 0; // Platform fee, part 2: the unspecified side is the paired token (exact-in sell, or exact-out buy). uint256 pfee; Currency unspecified = exactIn == p.zeroForOne ? key.currency1 : key.currency0; if (Currency.unwrap(unspecified) != Currency.unwrap(projectTokenOf[id])) { int128 u = unspecified == key.currency0 ? delta.amount0() : delta.amount1(); pfee = _abs(int256(u)) * PROTOCOL_FEE_BPS / BPS; if (pfee > 0) { poolManager.take(unspecified, protocolFeeTo, pfee); emit ProtocolFee(id, Currency.unwrap(unspecified), pfee); } } int128 outDelta = p.zeroForOne ? delta.amount1() : delta.amount0(); if (outDelta <= 0) return (this.afterSwap.selector, int128(uint128(pfee))); uint256 out = uint256(uint128(outDelta)); Config storage c = _cfg[id]; uint256 burnAmt; uint256 rewardAmt; uint256 treasuryAmt; uint256 launchAmt; if (buy) { bool inWindow = _inWindow(id, c); if (inWindow && !outCur.isAddressZero()) { uint256 supply = IERC20Supply(Currency.unwrap(outCur)).totalSupply(); uint256 maxOut = supply * c.snipeMaxBps / BPS; // The cap counts everything a wallet buys in one block, so splitting a buy does not get around it. uint256 inBlock = (_buyBlock[id][tx.origin] == block.number ? _boughtInBlock[id][tx.origin] : 0) + out; // …Rules for adding or removing liquidity
It decides who can add or remove liquidity, or when.
The code that does this (beforeAddLiquidity):function _beforeAddLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata p, bytes calldata) internal override returns (bytes4) { PoolId id = key.toId(); Config storage c = _cfg[id]; if (_allowlistActive(c) && !_allowed(id, sender)) revert NotAllowlisted(tx.origin); // An empty pool's price can be pushed anywhere for free. Until the creator has put liquidity in, nobody // can trade and nobody else can add liquidity, so the starting price is the one the creator set. if (!seeded[id]) { if (tx.origin != creatorOf[id]) revert NotSeeded(); if (p.liquidityDelta > 0) seeded[id] = true; } // The lock follows the liquidity, not the wallet: what the creator adds must stay in that position until // the lock ends, whoever holds the position by then. if (c.lpLockUntil != 0 && block.timestamp < c.lpLockUntil && tx.origin == creatorOf[id] && p.liquidityDelta > 0) { lockedLiquidity[id][Position.calculatePositionKey(sender, p.tickLower, p.tickUpper, p.salt)] += uint256(p.liquidityDelta); } return this.beforeAddLiquidity.selector; }
Who controls it: No admin functions: nobody can change its rules after deployment.
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 before liquidity is removed, so it can delay or refuse LP exits.
It checks every deposit before it goes in.
It runs when a pool is created, often to lock in that pool’s settings.
Hook history
Settings changes: set up a pool (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.