OVERVIEW
Observes tick movement and recent swap frequency to maintain a bounded market-activity score and recommend a dynamic LP fee.
dynamic feemarket state

A pool that can feel market stress.
Observes tick movement and recent swap frequency to maintain a bounded market-activity score and recommend a dynamic LP fee.
beforeInitialize○ INACTIVEafterInitialize○ INACTIVEbeforeAddLiquidity○ INACTIVEafterAddLiquidity○ INACTIVEbeforeRemoveLiquidity○ INACTIVEafterRemoveLiquidity○ INACTIVEbeforeSwap● ACTIVEafterSwap● ACTIVEbeforeDonate○ INACTIVEafterDonate○ INACTIVEbeforeSwapReturnDelta○ INACTIVEafterSwapReturnDelta○ INACTIVEafterAddLiquidityReturnDelta○ INACTIVEafterRemoveLiquidityReturnDelta○ INACTIVE001// SPDX-License-Identifier: MIT002pragma solidity ^0.8.26;003004import {BaseHook} from "@openzeppelin/uniswap-hooks/src/base/BaseHook.sol";005import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";006import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol";007import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";008import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";009import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";010import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";011import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";012import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";013import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";014015/// @title HeartbeatHook016/// @notice Experimental activity score and dynamic LP fee controller. Not a volatility oracle.017contract HeartbeatHook is BaseHook {018using PoolIdLibrary for PoolKey;019020uint8 public constant MAX_SCORE = 100;021uint24 public constant CALM_FEE = 1_500;022uint24 public constant ACTIVE_FEE = 3_000;023uint24 public constant HOT_FEE = 6_000;024uint24 public constant STORM_FEE = 10_000;025026struct Pulse {027uint8 score;028int24 lastTick;029uint64 lastBlock;030uint64 totalSwaps;031bool initialized;032}033mapping(PoolId => Pulse) private _pulses;034035event HeartbeatUpdated(PoolId indexed poolId, uint8 oldScore, uint8 newScore, int24 tick, uint64 totalSwaps);036037constructor(IPoolManager manager) BaseHook(manager) {}038039function getHookPermissions() public pure override returns (Hooks.Permissions memory) {040return Hooks.Permissions(041false, false, false, false, false, false, true, true, false, false, false, false, false, false042);043}044045function _beforeSwap(address, PoolKey calldata key, SwapParams calldata, bytes calldata)046internal047view048override049returns (bytes4, BeforeSwapDelta, uint24)050{051if (!LPFeeLibrary.isDynamicFee(key.fee)) {052return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);053}054uint24 fee = recommendedFeeForScore(_pulses[key.toId()].score) | LPFeeLibrary.OVERRIDE_FEE_FLAG;055return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, fee);056}057058function _afterSwap(address, PoolKey calldata key, SwapParams calldata, BalanceDelta, bytes calldata)059internal060override061returns (bytes4, int128)062{063PoolId poolId = key.toId();064(, int24 currentTick,,) = StateLibrary.getSlot0(poolManager, poolId);065Pulse storage pulse = _pulses[poolId];066uint8 oldScore = pulse.score;067if (!pulse.initialized) {068pulse.initialized = true;069pulse.lastTick = currentTick;070pulse.lastBlock = uint64(block.number);071pulse.totalSwaps = 1;072} else {073pulse.score = computeNextScore(pulse.score, pulse.lastTick, currentTick, block.number - pulse.lastBlock);074pulse.lastTick = currentTick;075pulse.lastBlock = uint64(block.number);076pulse.totalSwaps += 1;077}078emit HeartbeatUpdated(poolId, oldScore, pulse.score, currentTick, pulse.totalSwaps);079return (BaseHook.afterSwap.selector, 0);080}081082function computeNextScore(uint8 previous, int24 previousTick, int24 currentTick, uint256 blockGap)083public084pure085returns (uint8)086{087uint256 score = previous;088uint256 decay = blockGap > 25 ? 25 : blockGap;089score = score > decay ? score - decay : 0;090int256 difference = int256(currentTick) - int256(previousTick);091uint256 movement = uint256(difference >= 0 ? difference : -difference);092score += movement > 40 ? 40 : movement;093if (blockGap <= 2) score += 10;094else if (blockGap <= 5) score += 5;095return uint8(score > MAX_SCORE ? MAX_SCORE : score);096}097098function recommendedFeeForScore(uint8 score) public pure returns (uint24) {099if (score <= 25) return CALM_FEE;100if (score <= 50) return ACTIVE_FEE;101if (score <= 75) return HOT_FEE;102return STORM_FEE;103}104105function getPulse(PoolId poolId) external view returns (Pulse memory) {106return _pulses[poolId];107}108109function heartbeatScore(PoolId poolId) external view returns (uint8) {110return _pulses[poolId].score;111}112113function currentRecommendedFee(PoolId poolId) external view returns (uint24) {114return recommendedFeeForScore(_pulses[poolId].score);115}116117function marketMode(PoolId poolId) external view returns (string memory) {118uint8 score = _pulses[poolId].score;119if (score <= 25) return "CALM";120if (score <= 50) return "ACTIVE";121if (score <= 75) return "HOT";122return "STORM";123}124}125
A compiled contract is not necessarily safe. Tests are not an audit. A verified source can still contain exploitable logic.