HMMDynamicHookV2
RobinhoodHMMDynamicHookV2 is a Uniswap v4 hook on Robinhood Chain. Runs code on every swap. Can take tokens from swaps. Can block LP withdrawals. Gates who can add liquidity. Runs at pool creation. Handles donations to LPs.
- Contract name
- HMMDynamicHookV2
How this hook works
Smart feesA hook made for the pool of one launched token. It charges a trading fee that rises when the price moves fast. The collected fees are shared between the launch platform, the pool's fee collector and a liquidity manager that works the pool.
One hook, one pool
Each copy of this hook is tied to a single pool, which the launch platform's registrar sets up once. It refuses to work with any other pool.
The code that does this (registerPool):function registerPool(PoolKey calldata key, PoolConfig calldata config) external override nonReentrant { if (msg.sender != registrar) revert Unauthorized(); _requireBoundPool(PoolId.unwrap(key.toId())); if (_registered) revert AlreadyRegistered(); _validateRegistration(config); _onPoolRegistered(config); _config.collector = config.collector; _registered = true; emit PoolRegistered(boundPoolId, config.collector, liquidityLocker); }Fee rises with price swings
The fee moves between a lowest and a highest rate fixed for the pool. The faster the price has been moving, the higher the fee.
The code that does this (_calculateRate):function _calculateRate(LaunchHookFeeContextV2 memory context) internal view override returns (uint24) { uint24 minimumPips = context.minimumPips; uint24 maximumPips = context.maximumPips; uint32 sensitivity = context.feeSensitivityPipsSecondsPerTick; if (maximumPips == minimumPips || sensitivity == 0) return minimumPips; (int256 tickChange, uint32 elapsed) = _oraclePriceMovement(); // The signal is a difference of bounded oracle ticks, so it cannot be int256.min. if (tickChange < 0) tickChange = -tickChange; return DynamicFeeHookRate.calculate(minimumPips, maximumPips, sensitivity, tickChange, elapsed); }Fees are shared out
The hook holds the fees it collects. A slice is set aside for the platform's treasury and part for the pool's liquidity manager. The pool's fee collector takes the rest.
The code that does this (_accrue):function _accrue(Currency currency, uint256 amount, uint8 denominator) private nonReentrant { address asset = Currency.unwrap(currency); FeeAccounting storage accounting = _accounting(asset); uint256 claims = accounting.pendingFees - accounting.settledFees; if (amount > MAX_MANAGER_DELTA - claims) _settleClaims(currency, accounting, claims); // Preserve upstream policy exactly, including division order. This is not an author fee: // denominator 4 means (amount / 4) * 125 / 100, NOT amount / 4 or an author recipient. uint256 treasury = denominator == 0 ? 0 : (amount / denominator) * 125 / 100; uint256 retained = _retainFees(asset, amount, treasury); accounting.pendingFees += retained; accounting.pendingTreasurySweeps += treasury; accounting.aggregateManagerClaims += retained; uint256 beforeClaims = poolManager.balanceOf(address(this), uint160(asset)); poolManager.mint(address(this), uint160(asset), amount); uint256 afterClaims = poolManager.balanceOf(address(this), uint160(asset)); // The first comparison makes the subsequent difference non-underflowing. unchecked { if ( afterClaims < beforeClaims || afterClaims - beforeClaims != amount || afterClaims < accounting.aggregateManagerClaims ) revert ClaimMismatch(); } emit FeeAccrued(boundPoolId, asset, amount, treasury); }Adding liquidity is restricted
Until the launch is finished, only the platform's liquidity locker can add liquidity. After that, a pool can still limit liquidity to the locker and its liquidity manager.
The code that does this (_validateLiquidityCaller):function _validateLiquidityCaller(address sender, ModifyLiquidityParams calldata params) private view { if (_feeCheckpointActive && (sender != liquidityLocker || params.liquidityDelta != 0)) { revert Reentrancy(); } if (_openingCompletedAt == 0) { if (sender != liquidityLocker) revert OpeningNotComplete(); } else if ( _config.externalLiquidityDisabled && sender != liquidityLocker && (_feeCheckpointActive || !_isLiquidityOperator(sender)) ) { revert ExternalLiquidityDisabled(); } }
Who controls it: The pool's fee range and fee split are fixed when the launch platform's registrar sets the hook up. The hook has no admin wallet and its code cannot be upgraded. Only the pool's fee collector and its liquidity manager can take out the fees set aside for them.
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.
It runs when someone donates tokens to the pool’s LPs.
Hook history
- Took its fee from a trade 1 time
- Opened its pool 1 time
- Finished the pool's launch 1 time
- Registered its 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.