// SPDX-License-Identifier: MIT pragma solidity 0.8.24; /* ─── HelixHook · the v4 hook that makes HELIX hook-native ───────────── * * Implements IHooks directly (no BaseHook in current v4-periphery). * Permissions: beforeSwap + afterSwap only — every other callback reverts. * * Per-swap work: * beforeSwap → settle funding for the bound market + sweep up to 3 liqs * afterSwap → record trade-cleared mark + donate 5bps of quote to HLP * ────────────────────────────────────────────────────────────────────── */ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.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, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; import {SwapParams, ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; interface IHelixPerps { function settleFunding(bytes32 marketId, uint256 markPriceWad) external; function sweepLiquidations(bytes32 marketId, uint8 max) external returns (uint8 swept); function recordMark(bytes32 marketId, uint256 markPriceWad) external; function donateHLP(bytes32 marketId, uint256 usdcAmount) external; } contract HelixHook is IHooks { using PoolIdLibrary for PoolKey; using StateLibrary for IPoolManager; uint16 public constant HLP_SWAP_FEE_BPS = 5; uint8 public constant LIQ_SWEEP_PER_SWAP = 3; uint256 public constant WAD = 1e18; uint256 public constant MIN_SWAP_AMOUNT = 1e15; IPoolManager public immutable POOL_MANAGER; IHelixPerps public immutable PERPS; address public immutable INIT_ADMIN; // can bindMarket + lockBindings, until lock mapping(PoolId => bytes32) public poolToMarket; mapping(PoolId => bool) public quoteIsOne; bool public bindingsLocked; event MarketBound(PoolId indexed pid, bytes32 indexed marketId, bool quoteIsOne); event BindingsLocked(); event PreSwap(bytes32 indexed marketId, uint8 liqsSwept); event PostSwap(bytes32 indexed marketId, uint256 mark, uint256 hlpDonation); modifier onlyPoolManager() { require(msg.sender == address(POOL_MANAGER), "only PM"); _; } constructor(IPoolManager _manager, IHelixPerps _perps) { POOL_MANAGER = _manager; PERPS = _perps; INIT_ADMIN = msg.sender; } function getHookPermissions() public pure returns (Hooks.Permissions memory) { return Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeAddLiquidity: false, afterAddLiquidity: false, beforeRemoveLiquidity: false, afterRemoveLiquidity: false, beforeSwap: true, afterSwap: true, beforeDonate: false, afterDonate: false, beforeSwapReturnDelta: false, afterSwapReturnDelta: false, afterAddLiquidityReturnDelta: false, afterRemoveLiquidityReturnDelta: false }); } function bindMarket(PoolKey calldata key, bytes32 marketId, bool _quoteIsOne) external { require(!bindingsLocked, "locked"); require(msg.sender == INIT_ADMIN, "only init admin"); PoolId pid = key.toId(); require(poolToMarket[pid] == bytes32(0), "bound"); poolToMarket[pid] = marketId; quoteIsOne[pid] = _quoteIsOne; emit MarketBound(pid, marketId, _quoteIsOne); } function lockBindings() external { require(!bindingsLocked, "locked"); require(msg.sender == INIT_ADMIN, "only init admin"); bindingsLocked = true; emit BindingsLocked(); } function beforeSwap( address, PoolKey calldata key, SwapParams calldata, bytes calldata ) external override onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) { PoolId pid = key.toId(); bytes32 mid = poolToMarket[pid]; if (mid != bytes32(0)) { uint256 preMark = _spotMark(pid); PERPS.settleFunding(mid, preMark); uint8 swept = PERPS.sweepLiquidations(mid, LIQ_SWEEP_PER_SWAP); emit PreSwap(mid, swept); } return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } function afterSwap( address, PoolKey calldata key, SwapParams calldata, BalanceDelta delta, bytes calldata ) external override onlyPoolManager returns (bytes4, int128) { PoolId pid = key.toId(); bytes32 mid = poolToMarket[pid]; if (mid == bytes32(0)) return (IHooks.afterSwap.selector, 0); bool qIsOne = quoteIsOne[pid]; uint256 mark = _markFromDelta(delta, qIsOne); uint256 quoteAbs = _quoteAbsFromDelta(delta, qIsOne); if (mark > 0 && quoteAbs >= MIN_SWAP_AMOUNT) { PERPS.recordMark(mid, mark); uint256 donation = (quoteAbs * HLP_SWAP_FEE_BPS) / 10_000; if (donation > 0) PERPS.donateHLP(mid, donation); emit PostSwap(mid, mark, donation); } return (IHooks.afterSwap.selector, 0); } // disabled IHooks callbacks — revert to enforce permission bits function beforeInitialize(address, PoolKey calldata, uint160) external pure returns (bytes4) { revert("not used"); } function afterInitialize(address, PoolKey calldata, uint160, int24) external pure returns (bytes4) { revert("not used"); } function beforeAddLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, bytes calldata) external pure returns (bytes4) { revert("not used"); } function afterAddLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, BalanceDelta, BalanceDelta, bytes calldata) external pure returns (bytes4, BalanceDelta) { revert("not used"); } function beforeRemoveLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, bytes calldata) external pure returns (bytes4) { revert("not used"); } function afterRemoveLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, BalanceDelta, BalanceDelta, bytes calldata) external pure returns (bytes4, BalanceDelta) { revert("not used"); } function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) external pure returns (bytes4) { revert("not used"); } function afterDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) external pure returns (bytes4) { revert("not used"); } function _spotMark(PoolId pid) internal view returns (uint256) { (uint160 sqrtPriceX96, , , ) = POOL_MANAGER.getSlot0(pid); return _sqrtPriceX96ToWadPrice(sqrtPriceX96, quoteIsOne[pid]); } function _sqrtPriceX96ToWadPrice(uint160 sqrtPriceX96, bool qIsOne) internal pure returns (uint256) { if (sqrtPriceX96 == 0) return 0; uint256 priceX192 = uint256(sqrtPriceX96) * uint256(sqrtPriceX96); uint256 pWad = (priceX192 * WAD) >> 192; if (qIsOne) return pWad; return pWad == 0 ? 0 : (WAD * WAD) / pWad; } function _markFromDelta(BalanceDelta delta, bool qIsOne) internal pure returns (uint256) { int128 a0 = delta.amount0(); int128 a1 = delta.amount1(); if (a0 == 0 || a1 == 0) return 0; uint256 abs0 = a0 < 0 ? uint128(-a0) : uint128(a0); uint256 abs1 = a1 < 0 ? uint128(-a1) : uint128(a1); return qIsOne ? (abs1 * WAD) / abs0 : (abs0 * WAD) / abs1; } function _quoteAbsFromDelta(BalanceDelta delta, bool qIsOne) internal pure returns (uint256) { int128 a = qIsOne ? delta.amount1() : delta.amount0(); return a < 0 ? uint128(-a) : uint128(a); } }