diff --git a/contracts/hooks/TrustGateHook.sol b/contracts/hooks/TrustGateHook.sol new file mode 100644 index 0000000..f346baa --- /dev/null +++ b/contracts/hooks/TrustGateHook.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "../BaseERC8183Hook.sol"; +import "../interfaces/IERC8183HookMetadata.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +/// @title IRNWYTrustOracle +/// @notice Minimal interface for agent-identity-based trust oracles. +/// @dev Lookup key is (agentId, chainId, registry) to support multi-chain, +/// multi-registry agent identity resolution. +interface IRNWYTrustOracle { + function getScore( + uint256 agentId, + uint256 chainId, + string calldata registry + ) external view returns ( + uint8 score, + uint8 tier, + uint8 sybilSeverity, + uint40 updatedAt + ); + + function hasScore( + uint256 agentId, + uint256 chainId, + string calldata registry + ) external view returns (bool); + + function meetsThreshold( + uint256 agentId, + uint256 chainId, + string calldata registry, + uint8 threshold + ) external view returns (bool); + + function agentCount() external view returns (uint256); +} + +/** + * @title TrustGateHook + * @notice Hook that gates ERC-8183 job funding and submission by agent trust score. + * + * @dev Inherits BaseERC8183Hook for correct selector routing, data decoding, + * and onlyERC8183 caller authentication. Reads from any oracle implementing + * IRNWYTrustOracle - an agent-identity-based trust interface using + * (agentId, chainId, registry) lookups across multiple chains and registries. + * + * Reference implementation: RNWY Trust Oracle on Base mainnet + * (138,000+ agent scores covering ERC-8004, Olas, and Virtuals). + * + * Hook points: + * - _preFund(fund) -> Check client trust, revert if below threshold + * - _preSubmit(submit) -> Check provider trust, revert if below threshold. + * Also checks client trust when job status is Open + * (zero-budget path skips fund(), so _preFund never fires). + * - _postComplete -> Emit outcome event + * - _postReject -> Emit outcome event + * + * The hook maps wallet addresses to agent IDs via a registry managed by the + * hook owner. The oracle does all scoring - the hook is a gate, not a judge. + * + * Use case: prevent low-trust clients from funding jobs and low-trust providers + * from submitting work, while keeping scoring outside the hook. When used with + * MultiHookRouter, configure this hook for both fund and submit selectors. + */ +contract TrustGateHook is BaseERC8183Hook, Ownable, IERC8183HookMetadata { + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + bytes4 private constant SEL_FUND = + bytes4(keccak256("fund(uint256,uint256,bytes)")); + bytes4 private constant SEL_SUBMIT = + bytes4(keccak256("submit(uint256,bytes32,bytes)")); + + IRNWYTrustOracle public oracle; + uint8 public threshold; + uint256 public defaultChainId; + string public defaultRegistry; + + /// @notice Wallet address -> agent ID + mapping(address => uint256) public agentIds; + + /// @notice Tracks which wallets have been explicitly registered. + /// @dev Separate from agentIds so that agentId = 0 is a valid registered value. + mapping(address => bool) public registered; + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event AgentIdSet(address indexed wallet, uint256 agentId); + event ThresholdUpdated(uint8 oldThreshold, uint8 newThreshold); + event OracleUpdated(address indexed oldOracle, address indexed newOracle); + event TrustGated(uint256 indexed jobId, address indexed agent, uint256 agentId, bool allowed); + event OutcomeRecorded(uint256 indexed jobId, bool completed); + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error TrustGateHook__NoAgentId(address agent); + error TrustGateHook__BelowThreshold(uint256 jobId, address agent, uint256 agentId, uint8 threshold); + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /** + * @param erc8183Contract_ Address of the ERC-8183 core or MultiHookRouter. + * @param oracle_ Address of any IRNWYTrustOracle implementation + * @param threshold_ Minimum trust score required to pass the gate + * @param chainId_ Default chain ID for oracle lookups (e.g., 8453 for Base) + * @param registry_ Default registry for oracle lookups (e.g., "erc8004") + */ + constructor( + address erc8183Contract_, + address oracle_, + uint8 threshold_, + uint256 chainId_, + string memory registry_ + ) BaseERC8183Hook(erc8183Contract_) Ownable(msg.sender) { + require(oracle_ != address(0), "TrustGateHook: zero oracle"); + oracle = IRNWYTrustOracle(oracle_); + threshold = threshold_; + defaultChainId = chainId_; + defaultRegistry = registry_; + } + + /*////////////////////////////////////////////////////////////// + BaseERC8183Hook overrides + //////////////////////////////////////////////////////////////*/ + + /// @dev Gates the fund transition by client trust score. + function _preFund( + uint256 jobId, + address caller, + bytes memory + ) internal override { + _checkTrust(jobId, caller); + } + + /// @dev Gates the submit transition by provider trust score. + /// Also gates the client on zero-budget jobs (status == Open), because + /// those jobs skip fund() entirely, so _preFund never fires for the client. + function _preSubmit( + uint256 jobId, + address caller, + bytes32, + bytes memory + ) internal override { + _checkTrust(jobId, caller); // provider + + AgenticCommerce.Job memory job = AgenticCommerce(erc8183Contract).getJob(jobId); + if (job.status == AgenticCommerce.JobStatus.Open) { + _checkTrust(jobId, job.client); // client - zero-budget path only + } + } + + /// @dev Records completion outcome. Never reverts. + function _postComplete( + uint256 jobId, + address, + bytes32, + bytes memory + ) internal override { + emit OutcomeRecorded(jobId, true); + } + + /// @dev Records rejection outcome. Never reverts. + function _postReject( + uint256 jobId, + address, + bytes32, + bytes memory + ) internal override { + emit OutcomeRecorded(jobId, false); + } + + /*////////////////////////////////////////////////////////////// + ADMIN + //////////////////////////////////////////////////////////////*/ + + /// @notice Register a wallet -> agent ID mapping. + function setAgentId(address wallet, uint256 agentId) external onlyOwner { + agentIds[wallet] = agentId; + registered[wallet] = true; + emit AgentIdSet(wallet, agentId); + } + + /// @notice Batch-register wallet -> agent ID mappings. + function setAgentIds(address[] calldata wallets, uint256[] calldata ids) external onlyOwner { + require(wallets.length == ids.length, "TrustGateHook: array length mismatch"); + for (uint256 i = 0; i < wallets.length; i++) { + agentIds[wallets[i]] = ids[i]; + registered[wallets[i]] = true; + emit AgentIdSet(wallets[i], ids[i]); + } + } + + /// @notice Update the minimum trust score threshold. + function setThreshold(uint8 threshold_) external onlyOwner { + emit ThresholdUpdated(threshold, threshold_); + threshold = threshold_; + } + + /// @notice Update the oracle address (must implement IRNWYTrustOracle). + function setOracle(address oracle_) external onlyOwner { + require(oracle_ != address(0), "TrustGateHook: zero oracle"); + emit OracleUpdated(address(oracle), oracle_); + oracle = IRNWYTrustOracle(oracle_); + } + + /*////////////////////////////////////////////////////////////// + IERC8183HookMetadata + //////////////////////////////////////////////////////////////*/ + + function requiredSelectors() external pure returns (bytes4[] memory selectors) { + selectors = new bytes4[](2); + selectors[0] = SEL_FUND; + selectors[1] = SEL_SUBMIT; + } + + function supportsInterface( + bytes4 interfaceId + ) public view override(BaseERC8183Hook) returns (bool) { + return + interfaceId == type(IERC8183HookMetadata).interfaceId || + super.supportsInterface(interfaceId); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////*/ + + function _checkTrust(uint256 jobId, address agent) internal { + if (!registered[agent]) revert TrustGateHook__NoAgentId(agent); + + uint256 agentId = agentIds[agent]; + bool passes = oracle.meetsThreshold(agentId, defaultChainId, defaultRegistry, threshold); + + if (!passes) { + emit TrustGated(jobId, agent, agentId, false); + revert TrustGateHook__BelowThreshold(jobId, agent, agentId, threshold); + } + + emit TrustGated(jobId, agent, agentId, true); + } +}