// SPDX-License-Identifier: MIT pragma solidity ^0.8.26; // ABI-compatible v4 types. No core implementation is copied. struct PoolKey { address currency0; address currency1; uint24 fee; int24 tickSpacing; address hooks; } struct SwapParams { bool zeroForOne; int256 amountSpecified; uint160 sqrtPriceLimitX96; } /// @notice Offchain signals can only select bounded deterministic policies. Not audited. contract InferenceHook { struct Signal { bytes32 poolId; uint256 chainId; bytes32 modelIdentifier; bytes32 strategyIdentifier; uint64 signalTimestamp; uint64 expiry; uint256 nonce; uint24 proposedFee; uint256 riskParameters; bytes32 dataHash; address signer; } struct Policy { uint24 fee; uint64 expiry; uint256 maxAmount; } address public immutable poolManager; address public immutable owner; address public authorizedSigner; uint24 public immutable minFee; uint24 public immutable maxFee; uint24 public immutable fallbackFee; uint64 public immutable maxSignalAge; uint256 public immutable absoluteMaxAmount; uint8 public immutable mode; uint8 public immutable sessionStart; uint8 public immutable sessionEnd; mapping(bytes32 => bool) public registered; mapping(bytes32 => uint256) public nonces; mapping(bytes32 => Policy) public policies; bytes32 public constant SIGNAL_TYPEHASH = keccak256("Signal(bytes32 poolId,uint256 chainId,bytes32 modelIdentifier,bytes32 strategyIdentifier,uint64 signalTimestamp,uint64 expiry,uint256 nonce,uint24 proposedFee,uint256 riskParameters,bytes32 dataHash,address signer)"); event SignalApplied(bytes32 indexed poolId, bytes32 indexed modelIdentifier, bytes32 dataHash, uint256 nonce, uint24 fee, uint64 expiry); event SignerChanged(address indexed signer); event PoolRegistered(bytes32 indexed poolId); error Unauthorized(); error InvalidSignal(); error InvalidConfiguration(); error RiskLimit(); constructor(address manager,address initialOwner,address signer,uint24 low,uint24 high,uint24 fallbackValue,uint64 age,uint256 maxAmount,uint8 policyMode,uint8 start,uint8 end) { if(manager==address(0)||initialOwner==address(0)||signer==address(0)||low>fallbackValue||fallbackValue>high||high>10000||age<60||age>86400||policyMode>4||start>=end||end>24) revert InvalidConfiguration(); // beforeInitialize (1<<13) and beforeSwap (1<<7), no delta-return permissions. if(uint160(address(this)) & ((1<<14)-1) != ((1<<13)|(1<<7))) revert InvalidConfiguration(); poolManager=manager;owner=initialOwner;authorizedSigner=signer;minFee=low;maxFee=high;fallbackFee=fallbackValue;maxSignalAge=age;absoluteMaxAmount=maxAmount;mode=policyMode;sessionStart=start;sessionEnd=end; } modifier onlyManager(){if(msg.sender!=poolManager)revert Unauthorized();_;} function setSigner(address signer) external {if(msg.sender!=owner||signer==address(0))revert Unauthorized();authorizedSigner=signer;emit SignerChanged(signer);} function domainSeparator() public view returns(bytes32){return keccak256(abi.encode(keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),keccak256("HookInference"),keccak256("1"),block.chainid,address(this)));} function signalDigest(Signal calldata s) public view returns(bytes32){return keccak256(abi.encodePacked("\x19\x01",domainSeparator(),keccak256(abi.encode(SIGNAL_TYPEHASH,s))));} function submitSignal(Signal calldata s,bytes calldata signature) external { if(!registered[s.poolId]||s.chainId!=block.chainid||s.signer!=authorizedSigner||s.nonce!=nonces[s.poolId]||s.signalTimestamp>block.timestamp||block.timestamp-s.signalTimestamp>maxSignalAge||s.expiry<=block.timestamp||s.expiry>s.signalTimestamp+maxSignalAge||s.proposedFeemaxFee||s.riskParameters>absoluteMaxAmount)revert InvalidSignal(); if(signature.length!=65)revert InvalidSignal();bytes32 r;bytes32 ss;uint8 v; assembly {r:=calldataload(signature.offset) ss:=calldataload(add(signature.offset,32)) v:=byte(0,calldataload(add(signature.offset,64)))} if(uint256(ss)>0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0||(v!=27&&v!=28)||ecrecover(signalDigest(s),v,r,ss)!=authorizedSigner)revert InvalidSignal(); nonces[s.poolId]++;policies[s.poolId]=Policy(s.proposedFee,s.expiry,s.riskParameters);emit SignalApplied(s.poolId,s.modelIdentifier,s.dataHash,s.nonce,s.proposedFee,s.expiry); } function beforeInitialize(address sender,PoolKey calldata key,uint160) external onlyManager returns(bytes4){ if(sender!=owner||key.hooks!=address(this)||key.fee!=0x800000)revert InvalidConfiguration();bytes32 id=keccak256(abi.encode(key));registered[id]=true;emit PoolRegistered(id);return this.beforeInitialize.selector; } function beforeSwap(address,PoolKey calldata key,SwapParams calldata params,bytes calldata) external view onlyManager returns(bytes4,int256,uint24){ bytes32 id=keccak256(abi.encode(key));if(!registered[id])revert InvalidConfiguration();Policy memory p=policies[id];bool fresh=p.expiry>block.timestamp; // Risk mode fails closed when signals expire; other modes revert to disclosed fallback fee. if(mode==1){if(!fresh)revert InvalidSignal();if(params.amountSpecified>=0||params.amountSpecified==type(int256).min)revert RiskLimit();if(uint256(-params.amountSpecified)>p.maxAmount)revert RiskLimit();} if(mode==3){uint256 hour=(block.timestamp/3600)%24;if(hour=sessionEnd)revert RiskLimit();} uint24 fee=(mode==4||!fresh)?fallbackFee:p.fee; return(this.beforeSwap.selector,0,fee|0x400000); } } /// @notice User-owned factory. CREATE2 address flags are checked by the hook constructor. contract InferenceHookFactory { event HookDeployed(address indexed hook,address indexed owner,bytes32 salt); function deploy(bytes32 salt,bytes calldata initCode) external returns(address hook){bytes32 boundSalt=keccak256(abi.encode(msg.sender,salt));bytes memory code=initCode;assembly{hook:=create2(0,add(code,32),mload(code),boundSalt)}require(hook!=address(0),"Deployment failed");emit HookDeployed(hook,msg.sender,salt);} function predict(address sender,bytes32 salt,bytes32 initCodeHash) external view returns(address){return address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff),address(this),keccak256(abi.encode(sender,salt)),initCodeHash)))));} }