// SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; /// @notice Research prototype. Not audited. No production oracle adapter is included. /// @dev Configurations are immutable per PoolId. The immutable reporter is trusted for /// session and volatility. A production adapter must authenticate feeds and calendars. contract RillunePulseHook is IHooks { using PoolIdLibrary for PoolKey; uint24 public constant DYNAMIC_FEE_FLAG = 0x800000; uint24 public constant OVERRIDE_FEE_FLAG = 0x400000; uint160 public constant REQUIRED_HOOK_FLAGS = (1 << 13) | (1 << 7); address public immutable poolManager; address public immutable owner; address public immutable reporter; struct Policy {uint16 baseBps; uint16 sensitivity; uint16 ceilingBps; uint32 ttl; bool sessionAware; bool configured;} struct Signal {uint16 volatilityBps; uint8 session; uint64 observedAt;} mapping(PoolId => Policy) public policies; mapping(PoolId => Signal) public signals; mapping(PoolId => bool) public initialized; error Unauthorized(); error InvalidAddress(); error InvalidHookFlags(); error InvalidPolicy(); error PolicyFrozen(); error UnknownPool(); error InvalidSignal(); error StaleReference(); error DynamicPoolRequired(); error UnsupportedCallback(); event PolicyRegistered(PoolId indexed poolId, uint16 baseBps, uint16 sensitivity, uint16 ceilingBps, uint32 ttl, bool sessionAware); event SignalPublished(PoolId indexed poolId, uint16 volatilityBps, uint8 session, uint64 observedAt); event FeeApplied(PoolId indexed poolId, uint24 feePips, uint16 baseBps, uint16 sessionBps, uint16 volatilityBps, bool capped); constructor(address manager_, address owner_, address reporter_) { if (manager_ == address(0) || owner_ == address(0) || reporter_ == address(0)) revert InvalidAddress(); if (uint160(address(this)) & ((1 << 14) - 1) != REQUIRED_HOOK_FLAGS) revert InvalidHookFlags(); poolManager = manager_; owner = owner_; reporter = reporter_; } modifier onlyManager() {if (msg.sender != poolManager) revert Unauthorized(); _;} function configure(PoolKey calldata key, uint16 baseBps, uint16 sensitivity, uint16 ceilingBps, uint32 ttl, bool sessionAware) external { if (msg.sender != owner) revert Unauthorized(); _validateKey(key); PoolId id = key.toId(); if (policies[id].configured) revert PolicyFrozen(); if (baseBps < 1 || baseBps > 100 || sensitivity > 20 || ceilingBps < baseBps || ceilingBps > 200 || ttl < 30 || ttl > 900) revert InvalidPolicy(); policies[id] = Policy(baseBps, sensitivity, ceilingBps, ttl, sessionAware, true); emit PolicyRegistered(id, baseBps, sensitivity, ceilingBps, ttl, sessionAware); } function publishSignal(PoolId id, uint16 volatilityBps, uint8 session, uint64 observedAt) external { if (msg.sender != reporter) revert Unauthorized(); Policy memory p = policies[id]; if (!p.configured) revert UnknownPool(); if (volatilityBps > 1000 || session > 3 || observedAt == 0 || observedAt > block.timestamp || observedAt <= signals[id].observedAt || block.timestamp - observedAt > p.ttl) revert InvalidSignal(); signals[id] = Signal(volatilityBps, session, observedAt); emit SignalPublished(id, volatilityBps, session, observedAt); } function explainFee(PoolId id) public view returns (uint24 feePips, uint16 baseBps, uint16 sessionBps, uint16 volatilityBps, bool capped) { Policy memory p = policies[id]; Signal memory s = signals[id]; if (!p.configured) revert UnknownPool(); if (s.observedAt == 0 || s.session == 3 || block.timestamp - s.observedAt > p.ttl) revert StaleReference(); baseBps = p.baseBps; sessionBps = p.sessionAware ? (s.session == 1 ? 8 : s.session == 2 ? 20 : 0) : 0; volatilityBps = uint16((uint256(s.volatilityBps) * p.sensitivity + 99) / 100); uint256 raw = uint256(baseBps) + sessionBps + volatilityBps; capped = raw > p.ceilingBps; feePips = uint24((capped ? p.ceilingBps : raw) * 100); } function beforeInitialize(address, PoolKey calldata key, uint160) external onlyManager returns (bytes4) { _validateKey(key); PoolId id = key.toId(); if (!policies[id].configured) revert UnknownPool(); initialized[id] = true; return IHooks.beforeInitialize.selector; } function beforeSwap(address, PoolKey calldata key, SwapParams calldata, bytes calldata) external onlyManager returns (bytes4, BeforeSwapDelta, uint24) { _validateKey(key); PoolId id = key.toId(); if (!initialized[id]) revert UnknownPool(); (uint24 fee, uint16 base, uint16 session, uint16 volatility, bool capped) = explainFee(id); emit FeeApplied(id, fee, base, session, volatility, capped); return (IHooks.beforeSwap.selector, BeforeSwapDelta.wrap(0), fee | OVERRIDE_FEE_FLAG); } function _validateKey(PoolKey calldata key) private view { if (address(key.hooks) != address(this) || key.fee != DYNAMIC_FEE_FLAG) revert DynamicPoolRequired(); } // Disabled permission bits mean PoolManager never calls these callbacks. function afterInitialize(address,PoolKey calldata,uint160,int24) external pure returns(bytes4){revert UnsupportedCallback();} function beforeAddLiquidity(address,PoolKey calldata,ModifyLiquidityParams calldata,bytes calldata) external pure returns(bytes4){revert UnsupportedCallback();} function afterAddLiquidity(address,PoolKey calldata,ModifyLiquidityParams calldata,BalanceDelta,BalanceDelta,bytes calldata) external pure returns(bytes4,BalanceDelta){revert UnsupportedCallback();} function beforeRemoveLiquidity(address,PoolKey calldata,ModifyLiquidityParams calldata,bytes calldata) external pure returns(bytes4){revert UnsupportedCallback();} function afterRemoveLiquidity(address,PoolKey calldata,ModifyLiquidityParams calldata,BalanceDelta,BalanceDelta,bytes calldata) external pure returns(bytes4,BalanceDelta){revert UnsupportedCallback();} function afterSwap(address,PoolKey calldata,SwapParams calldata,BalanceDelta,bytes calldata) external pure returns(bytes4,int128){revert UnsupportedCallback();} function beforeDonate(address,PoolKey calldata,uint256,uint256,bytes calldata) external pure returns(bytes4){revert UnsupportedCallback();} function afterDonate(address,PoolKey calldata,uint256,uint256,bytes calldata) external pure returns(bytes4){revert UnsupportedCallback();} }