fix: TrustGateHook — selector mismatch, caller auth, admin events#32
fix: TrustGateHook — selector mismatch, caller auth, admin events#32JhiNResH wants to merge 3 commits intoerc-8183:mainfrom
Conversation
…ents Critical: - Inherit BaseERC8183Hook instead of raw IACPHook — fixes FUND_SEL selector (was fund(uint256,bytes), must be fund(uint256,uint256,bytes)) and adds onlyERC8183 caller authentication for free - Override _preFund/_preSubmit/_postComplete/_postReject virtual functions instead of hand-rolling beforeAction/afterAction dispatch High: - Remove manual selector constants (now inherited from BaseERC8183Hook) - Remove manual supportsInterface (handled by BaseERC8183Hook + ERC165) Medium: - Add registered mapping to track agentId existence separately from value (fixes agentId=0 collision with Solidity default mapping) - Emit AgentIdSet, ThresholdUpdated, OracleUpdated on all admin mutations - Add zero-address guard on oracle in constructor and setOracle Low: - Fix README: TrustGateHook.sol (was RNWYTrustGateHook.sol) - Constructor now requires erc8183Contract_ address (BaseERC8183Hook param) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Inherit BaseERC8183Hook for correct selector routing, data decoding, and onlyERC8183 caller authentication - Add registered mapping so agentId = 0 is a valid registered value - Add AgentIdSet, ThresholdUpdated, OracleUpdated admin events - Add zero-address guards on constructor and setOracle - Document wallet-risk vs. agent-quality trust boundary in header Addresses review feedback from @JhiNResH in erc-8183#32 and coordination request from @psmiratisu.
Adds a minimal ERC-8183 hook that gates the fund stage on a condition-based wallet-state verifier. Complements existing score-based gating (TrustGateHook, erc-8183#9/erc-8183#32) and content-based verification (ReasoningVerifierHook, erc-8183#31) with a third shape: "does this wallet satisfy a named condition set right now?" - contracts/interfaces/IWalletStateVerifier.sol Minimal (bool verified, uint256 validUntil) interface keyed on (wallet, conditionsHash). Hooks stay stateless views. - contracts/hooks/WalletStateHook.sol Inherits BaseERC8183Hook + IERC8183HookMetadata. Immutable verifier + conditionsHash (deploy one hook per distinct condition set, mirrors the minConfidence immutable pattern in ReasoningVerifierHook). Overrides _preFund only — verifier.checkWalletState(caller, conditionsHash) → pass/fail + freshness, reverts otherwise. - contracts/examples/InsumerWalletStateVerifier.sol Reference IWalletStateVerifier implementation. Relayer-push model with optional RIP-7212 P256VERIFY precompile verification of off-chain ECDSA P-256 (ES256) attestation signatures. Works on Base, Arbitrum, Optimism, Polygon, Scroll, ZKsync, Celo — standard ERC-8183 L2 footprint. - test/WalletStateHook.t.sol 21 tests, all passing. Covers constructor guards, _preFund happy path, not-verified revert, expired-attestation revert, validUntil boundary, selector isolation, ERC-165 interface support, verifier relayer auth, and signature-mode flag. Stacked on top of erc-8183#30 (IACPHook → IERC8183Hook rename). Targets main; will rebase cleanly once erc-8183#30 merges.
Finding 1 [90] — Zero-budget jobs skip fund(), so _preFund never fires for the client. _preSubmit now also calls _checkTrust(client) when job.status == Open, closing the gate on the zero-budget path. Finding 3 [75] — onlyERC8183 fallback (msg.sender == job.hook) let any whitelisted hook call sibling hooks with attacker-controlled data, emitting spoofed TrustGated events. Fixed by gating the fallback path behind a new virtual _isAuthorizedRouter() function that defaults to false. Hooks that need router support (e.g. behind MultiHookRouter) can override it. Findings 2 [80] and 4 [70] are in AgenticCommerce (submodule) and will be filed against erc-8183/base-contracts separately. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JhiNResH
left a comment
There was a problem hiding this comment.
Differential Security Review — APPROVED
Reviewer: JhiNResH / Claude (claude-sonnet-4-6)
Date: 2026-04-18
Scope: +249, -3 — TrustGateHook rewrite + BaseERC8183Hook auth hardening
Verdict: ✅ APPROVE — 0 new vulnerabilities introduced
All CRITICAL and HIGH findings from the PR #9 review are correctly resolved. Two additional findings from the 8-agent solidity audit are also fixed.
Fixes verified
| Finding | Severity | Verified |
|---|---|---|
FUND_SEL wrong selector |
CRITICAL | ✅ Eliminated — BaseERC8183Hook inheritance removes manual selectors entirely |
No onlyERC8183 caller auth |
HIGH | ✅ Inherited modifier + _isAuthorizedRouter gate (default false) |
agentId=0 sentinel collision |
MEDIUM | ✅ registered bool mapping tracks existence separately |
| Admin mutations emit no events | MEDIUM | ✅ AgentIdSet, ThresholdUpdated, OracleUpdated on all mutations |
setOracle zero-address |
LOW | ✅ require(oracle_ != address(0)) in constructor and setter |
| README broken link | LOW | ✅ TrustGateHook.sol |
| Zero-budget client trust bypass | HIGH (audit F1) | ✅ _preSubmit reads job.status and checks client when Open |
Cross-hook invocation via job.hook |
HIGH (audit F3) | ✅ _isAuthorizedRouter gate — secure by default, opt-in for routers |
BaseERC8183Hook modifier change analysis
The onlyERC8183 tightening is correct. BEFORE: any address equal to job[jobId].hook could call beforeAction/afterAction on any hook, enabling cross-hook invocation. AFTER: the fallback path requires _isAuthorizedRouter() to return true first — which defaults to false. Only the erc8183Contract itself can call hooks unless a hook explicitly opts into router support. Blast radius = 1 inheritor (TrustGateHook), which correctly takes the default.
Zero-budget path correctness
_preSubmit calls getJob(jobId) while the job is still in pre-transition state (inside beforeAction). If status == Open, both provider AND client trust are checked. For the funded path (status == Funded), client was already gated in _preFund. All paths covered. ✅
Residual items (pre-existing, not blocking merge)
- [MEDIUM] No timelock on
setOracle— zero-address guard added, delay still absent. ConsiderTimelockControllerbefore mainnet. - [LOW]
threshold = 0silently disables the gate — no minimum validation. Document or addrequire(threshold_ > 0). - [HIGH risk factor] Zero test coverage on all new production code. Recommend adding Foundry tests before production deployment.
Full report: DIFFERENTIAL_REVIEW_PR32.md in reviewer worktree.
Adds the test coverage flagged as a HIGH risk factor in the differential review of this PR. test/TrustGateHook.t.sol (20 tests, all pass): - fund: registered+trusted succeeds, unregistered reverts, below-threshold reverts - submit funded path: only provider checked - submit zero-budget path: both provider and client checked - submit zero-budget: unregistered/below-threshold client reverts - beforeAction/afterAction: non-ACP caller reverts (OnlyERC8183Contract) - complete/reject: OutcomeRecorded events emitted - agentId=0: registered with zero-id passes, unregistered zero-mapping does not - setOracle(0): reverts, setOracle/setThreshold/setAgentId emit events - constructor zero oracle: reverts Support files: - contracts/BaseACPHook.sol: compatibility shim so BiddingHook/FundTransferHook compile (they import the old BaseACPHook name; stub re-exports BaseERC8183Hook with an acpContract alias pointing to the same address) - foundry.toml: add @acp/ remapping (backwards-compat for old hook imports) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes the blocking issues raised in review of PR #9.
Changes
CRITICAL fix —
FUND_SELselectorThe original hook computed
keccak256("fund(uint256,bytes)")butAgenticCommerce.fundisfund(uint256,uint256,bytes). The selector never matched, so client trust was never checked at thefundstage.Fix: Inherit
BaseERC8183Hookinstead of rawIACPHook. This gives correct selector constants for free (they live inBaseERC8183Hookand matchAgenticCommerce). The hook now overrides_preFund/_preSubmit/_postComplete/_postRejectvirtual functions — no manual selector dispatch needed.HIGH fix — caller authentication
BaseERC8183Hookprovides theonlyERC8183(jobId)modifier, which validates thatmsg.senderis the ERC-8183 core or the job's registered hook. Inheriting it adds this guard automatically. No extra code required.MEDIUM fixes
agentId == 0collision — addedmapping(address => bool) public registeredto track existence separately from the agentId value. Wallets withagentId = 0can now be registered correctly.setAgentId,setAgentIds,setThreshold, andsetOraclenow emitAgentIdSet,ThresholdUpdated, andOracleUpdatedrespectively.require(oracle_ != address(0))in constructor andsetOracle.LOW fixes
TrustGateHook.sol(wasRNWYTrustGateHook.sol).Constructor change (breaking)
The constructor gains a new first parameter:
erc8183Contract_is required byBaseERC8183Hookand stored as immutable. This is the AgenticCommerce core address (or MultiHookRouter address if used behind a router).