Skip to content

fix: TrustGateHook — selector mismatch, caller auth, admin events#32

Open
JhiNResH wants to merge 3 commits intoerc-8183:mainfrom
JhiNResH:fix/trust-gate-hook
Open

fix: TrustGateHook — selector mismatch, caller auth, admin events#32
JhiNResH wants to merge 3 commits intoerc-8183:mainfrom
JhiNResH:fix/trust-gate-hook

Conversation

@JhiNResH
Copy link
Copy Markdown

Fixes the blocking issues raised in review of PR #9.

Changes

CRITICAL fix — FUND_SEL selector

The original hook computed keccak256("fund(uint256,bytes)") but AgenticCommerce.fund is fund(uint256,uint256,bytes). The selector never matched, so client trust was never checked at the fund stage.

Fix: Inherit BaseERC8183Hook instead of raw IACPHook. This gives correct selector constants for free (they live in BaseERC8183Hook and match AgenticCommerce). The hook now overrides _preFund/_preSubmit/_postComplete/_postReject virtual functions — no manual selector dispatch needed.

HIGH fix — caller authentication

BaseERC8183Hook provides the onlyERC8183(jobId) modifier, which validates that msg.sender is the ERC-8183 core or the job's registered hook. Inheriting it adds this guard automatically. No extra code required.

MEDIUM fixes

  • agentId == 0 collision — added mapping(address => bool) public registered to track existence separately from the agentId value. Wallets with agentId = 0 can now be registered correctly.
  • Admin eventssetAgentId, setAgentIds, setThreshold, and setOracle now emit AgentIdSet, ThresholdUpdated, and OracleUpdated respectively.
  • Oracle zero-address guardrequire(oracle_ != address(0)) in constructor and setOracle.

LOW fixes

  • README: link now points to TrustGateHook.sol (was RNWYTrustGateHook.sol).

Constructor change (breaking)

The constructor gains a new first parameter:

// Before
constructor(address oracle_, uint8 threshold_, uint256 chainId_, string memory registry_)

// After
constructor(address erc8183Contract_, address oracle_, uint8 threshold_, uint256 chainId_, string memory registry_)

erc8183Contract_ is required by BaseERC8183Hook and stored as immutable. This is the AgenticCommerce core address (or MultiHookRouter address if used behind a router).

…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>
rnwy added a commit to rnwy/hook-contracts that referenced this pull request Apr 17, 2026
- 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.
douglasborthwick-crypto added a commit to douglasborthwick-crypto/hook-contracts that referenced this pull request Apr 18, 2026
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>
Copy link
Copy Markdown
Author

@JhiNResH JhiNResH left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

  1. [MEDIUM] No timelock on setOracle — zero-address guard added, delay still absent. Consider TimelockController before mainnet.
  2. [LOW] threshold = 0 silently disables the gate — no minimum validation. Document or add require(threshold_ > 0).
  3. [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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant