// SPDX-License-Identifier: MIT pragma solidity ^0.8.26; import {SealedPolicyRegistry} from "./SealedPolicyRegistry.sol"; /// @title FeeSplitHook — reference Uniswap v4 hook enforcing a sealed fee split (GH-W1-001 §2.2). /// @notice Minimal reference: the hook holds a fee split (creator / liquidity / treasury in bps) whose sha256 must be a /// registered policy hash in the SealedPolicyRegistry. afterSwap splits the hook fee by the sealed split; any /// setSplit whose hash is not registered is REFUSED. This is the policy-enforcement core; attaching it to a live /// PoolManager pool needs a mined hook address (permission bits), which is a deployment step, not a policy step. contract FeeSplitHook { struct Split { uint16 creatorBps; uint16 liquidityBps; uint16 treasuryBps; } SealedPolicyRegistry public immutable registry; bytes32 public constant POLICY_ID = keccak256("glass-hull/fee-split/v1"); address public immutable poolManager; address public creator; address public liquidityVault; address public treasury; Split public split; bytes32 public splitHash; event SplitApplied(uint16 creatorBps, uint16 liquidityBps, uint16 treasuryBps, bytes32 policyHash); event SplitRefused(uint16 creatorBps, uint16 liquidityBps, uint16 treasuryBps, bytes32 policyHash, string reason); event FeeRouted(uint256 feeAmount, uint256 toCreator, uint256 toLiquidity, uint256 toTreasury); constructor(address _registry, address _poolManager, address _creator, address _liquidityVault, address _treasury) { registry = SealedPolicyRegistry(_registry); poolManager = _poolManager; creator = _creator; liquidityVault = _liquidityVault; treasury = _treasury; } /// @notice The split's policy hash: sha256 of the canonical JSON the OPA bundle carries. function hashOf(uint16 c, uint16 l, uint16 t) public pure returns (bytes32) { return sha256(abi.encodePacked('{"policy":"fee-split/v1","creator_bps":', _u(c), ',"liquidity_bps":', _u(l), ',"treasury_bps":', _u(t), '}')); } /// @notice Change the split. PASS only when the registry holds this exact split's hash under POLICY_ID; REFUSE otherwise. function setSplit(uint16 c, uint16 l, uint16 t) external { bytes32 h = hashOf(c, l, t); if (c + l + t != 10000) { emit SplitRefused(c, l, t, h, "sum!=10000"); revert("HOOK: split must sum to 10000 bps"); } if (!registry.isRegistered(POLICY_ID, h)) { emit SplitRefused(c, l, t, h, "policy-hash-not-sealed"); revert("HOOK: split not in sealed policy"); } split = Split(c, l, t); splitHash = h; emit SplitApplied(c, l, t, h); } /// @notice v4 afterSwap entry (selector-compatible); routes a hook fee by the sealed split. Only the PoolManager may call. function afterSwap(address, bytes32, bytes calldata, int256 delta, bytes calldata) external returns (bytes4, int128) { require(msg.sender == poolManager, "HOOK: only PoolManager"); uint256 fee = uint256(delta < 0 ? -delta : delta) * 25 / 10000; // 0.25 % hook fee on the swapped amount (reference) (uint256 a, uint256 b, uint256 cc) = route(fee); emit FeeRouted(fee, a, b, cc); return (this.afterSwap.selector, int128(int256(fee))); } function route(uint256 fee) public view returns (uint256 toCreator, uint256 toLiquidity, uint256 toTreasury) { require(splitHash != bytes32(0), "HOOK: no sealed split"); toCreator = fee * split.creatorBps / 10000; toLiquidity = fee * split.liquidityBps / 10000; toTreasury = fee - toCreator - toLiquidity; } function _u(uint16 v) internal pure returns (bytes memory) { if (v == 0) return "0"; bytes memory b = new bytes(5); uint256 i = 5; uint256 x = v; while (x > 0) { i--; b[i] = bytes1(uint8(48 + x % 10)); x /= 10; } bytes memory o = new bytes(5 - i); for (uint256 j = 0; j < o.length; j++) o[j] = b[i + j]; return o; } }