Skip to content

feat: trust gate hook — on-chain trust scoring for job lifecycle gating#9

Open
rnwy wants to merge 18 commits into
erc-8183:mainfrom
rnwy:main
Open

feat: trust gate hook — on-chain trust scoring for job lifecycle gating#9
rnwy wants to merge 18 commits into
erc-8183:mainfrom
rnwy:main

Conversation

@rnwy
Copy link
Copy Markdown

@rnwy rnwy commented Mar 20, 2026

Summary

Adds TrustGateHook — a provider-agnostic ERC-8183 hook that gates job lifecycle transitions by on-chain trust score, reading from any oracle that implements IRNWYTrustOracle.

New Files

contracts/hooks/TrustGateHook.sol

ERC-8183 hook (inheriting BaseERC8183Hook) that reads from any IRNWYTrustOracle implementation to gate participants by trust score:

  • _preFund — checks client trust score, reverts if below threshold
  • _preSubmit — checks provider trust score, reverts if below threshold
  • _postComplete / _postReject — emits outcome events (never reverts)

The IRNWYTrustOracle interface is inlined in the same file. The hook maps wallet addresses to agent IDs, then calls meetsThreshold() on the oracle. The oracle does all scoring; the hook is a gate, not a judge.

Implements IERC8183HookMetadata for MultiHookRouter compatibility. requiredSelectors() returns an empty array — client and provider trust checks are independent gates with no cross-selector dependency.

Reference Implementation

The RNWY Trust Oracle on Base mainnet implements IRNWYTrustOracle with 138,000+ agent scores covering ERC-8004, Olas, and Virtuals across 11 chains.

Security

Uses abi.decode throughout — no raw assembly, no delegatecall. Owner-managed admin setters guard against zero-value, idempotent, and out-of-range updates.

@JhiNResH
Copy link
Copy Markdown

#6

@rnwy
Copy link
Copy Markdown
Author

rnwy commented Mar 20, 2026

Yes, great work on #6! Complementary to it: different signal, different coverage, same hook interface.

#6 (Maiat) #9 (RNWY)
Signal Job performance Behavioral trust
Sybil detection ✅ 4 signals + coordination
Funding source tracing
Registries Virtuals ERC-8004, Olas, Virtuals
Chains Base 11 chains
Agents scored 17-23K 138K+
Signed attestations ✅ ES256 + JWKS

Composable by design. A relying party can run both to check performance and trust independently.

@rnwy
Copy link
Copy Markdown
Author

rnwy commented Mar 20, 2026

Fair correction on the sybil row — updated, thanks. The "safe to transact with" vs "worth transacting with" framing is exactly right. EvaluatorRegistry and dynamic fees are interesting additions to the hook pattern — value-based tiers especially.

All four issuers verified in Douglas's reference verifier — good foundation to build on.

@psmiratisu
Copy link
Copy Markdown
Contributor

Thanks for the PR! As we're building ERC-8183 as an open standard, I'd suggest keeping the trust oracle interface generic rather than locked to RNWY specifically.

Could you extract an ITrustOracle interface that covers the core functions, then have RNWY implement that interface as your provider? This way other trust providers can plug in too, and yours becomes the reference implementation.

Same idea as what we're discussing with PR #6 — both can implement the same interface with different data sources.

Let me know if this works!

@rnwy rnwy changed the title feat: RNWY trust gate hook — on-chain trust scoring for job lifecycle gating feat: trust gate hook — on-chain trust scoring for job lifecycle gating Mar 24, 2026
@rnwy
Copy link
Copy Markdown
Author

rnwy commented Mar 24, 2026

Thanks for the suggestion! We extracted an IRNWYTrustOracle interface from our deployed oracle and updated the hook to reference it instead of the implementation directly. The hook is now provider-agnostic.

While working on this, we looked at the ITrustOracle interface in PR #6 to see if we could share one interface across both. The challenge is that the two oracles use fundamentally different lookup models:

These answer different questions at different levels of the stack: address reputation vs. agent identity trust. A shared interface would need to be so abstract it loses the type safety that makes each one useful.

Our suggestion: both hooks accept an oracle address at construction and talk to their respective interfaces. The hooks are provider-agnostic; the oracle interfaces reflect the real differences in how trust data is keyed. A relying party can run both hooks to get address-level and agent-level signals independently.

Happy to discuss if you see a different path — this is a great standard to get right.

@psmiratisu
Copy link
Copy Markdown
Contributor

Hey @rnwy, @JhiNResH Trust gating is a useful direction, but I do not think we should end up with parallel trust-hook paths that stay separate forever. My preference would be to keep the trust direction neutral at the interface and hook level, remove as much provider-specific framing as possible, and coordinate with #9 so we end up with one canonical trust hook direction, or at least a clearly aligned split where providers implement the same neutral pattern. If the two of you think the right answer is wallet-risk trust hook plus agent-quality trust hook, that is fine, but I would like you to sort that boundary out explicitly together rather than both landing overlapping trust primitives with different framing. So I would not merge this as-is yet. Can we both work togeher to merge the 2 PRs? If you both think there are differences, lets chat!

@rnwy
Copy link
Copy Markdown
Author

rnwy commented Apr 15, 2026

Thanks @psmiratisu: understood on not merging as-is. We agree the right outcome is a shared interface at the hook layer.

The framing we're working toward: both hooks implement the same neutral pattern — isTrusted(address participant, uint8 threshold) returns (bool) — with each provider handling the internal lookup differently. That satisfies your structural concern without forcing the two oracles to share a data model that genuinely doesn't fit.

We'll reach out to @JhiNResH directly to coordinate and come back with a concrete joint proposal within the next few days.

@rnwy
Copy link
Copy Markdown
Author

rnwy commented Apr 17, 2026

@psmiratisu Have proposed a shared interface with @JhiNResH (isTrusted(address participant, uint8 threshold) returns (bool)). @JhiNResH when you have a cycle, happy to iterate on the signature or jump on a call in case our email went to spam. Goal is one canonical trust hook shape that both our oracles can implement.

Copy link
Copy Markdown

@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.

Security Review — CHANGES REQUIRED

Reviewed against BaseERC8183Hook.sol (the canonical hook base in this repo). Two blocking issues found.


[CRITICAL] FUND_SEL selector is wrong — client trust check silently bypassed

// TrustGateHook.sol:41 — WRONG
bytes4 public constant FUND_SEL = bytes4(keccak256("fund(uint256,bytes)"));

// BaseERC8183Hook.sol — CORRECT
bytes4 private constant SEL_FUND = bytes4(keccak256("fund(uint256,uint256,bytes)"));

AgenticCommerce.fund takes three parameters (uint256 jobId, uint256 amount, bytes optParams). The hook computes the hash of a two-parameter signature, so selector == FUND_SEL is always false. The beforeAction(fund) branch is dead code. Anyone can fund a job regardless of trust score.

The data encoding for fund is correct (abi.encode(caller, optParams)abi.decode(data, (address, bytes))). Only the selector string needs fixing:

bytes4 public constant FUND_SEL = bytes4(keccak256("fund(uint256,uint256,bytes)"));

[HIGH] No caller authentication on beforeAction / afterAction

BaseERC8183Hook implements an onlyERC8183(jobId) modifier that validates msg.sender against the ACP core address (or the job's registered hook). TrustGateHook skips this entirely — both hook entry points are open to any caller.

Consequences:

  • Anyone can emit TrustGated and OutcomeRecorded with arbitrary values, poisoning off-chain indexers
  • Anyone can trigger oracle reads against arbitrary wallet addresses

Fix: store erc8183Contract as immutable in constructor and add the caller check (same pattern as BaseERC8183Hook).


[MEDIUM] Admin mutations emit no events

setAgentId, setThreshold, and setOracle make no emit. Oracle swaps are invisible on-chain. setOracle also has no zero-address guard and no timelock — a compromised owner key can atomically replace the oracle with one that approves any address.

Minimum: add OracleUpdated, ThresholdUpdated, AgentIdSet events + require(oracle_ != address(0)).


[MEDIUM] agentId == 0 ambiguous sentinel

agentIds[unregisteredWallet] returns 0 by default. If RNWY assigns agentId 0 to any real agent, they are permanently locked out with a misleading NoAgentId error. Needs documentation or a separate existence flag.


[LOW] Other issues

  • Constructor: no zero-address check on oracle_ parameter
  • threshold = 0: silently disables the gate; no lower-bound validation
  • README: links to RNWYTrustGateHook.sol but the file is TrustGateHook.sol
  • No tests: gate is untested for the fund path (which is also broken by issue #1)

Full report: available on request. Happy to discuss any of these — the oracle interface design and the submit gate are both solid; the CRITICAL and HIGH issues are mechanical fixes.

rnwy added 3 commits April 17, 2026 19:21
- 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.
Covers four paths through beforeAction(fund):
- high-trust client passes
- low-trust client reverts with BelowThreshold
- unregistered caller reverts with NoAgentId
- unauthorized msg.sender reverts via BaseERC8183Hook.onlyERC8183

Includes MockRNWYTrustOracle for deterministic scoring in tests.
@rnwy
Copy link
Copy Markdown
Author

rnwy commented Apr 17, 2026

@psmiratisu, rebased per your direction.

Canonical base adopted. TrustGateHook now inherits BaseERC8183Hook. This gives correct selector routing, data decoding, and onlyERC8183 caller authentication in one change, and resolves both the fund selector mismatch and the missing caller check.

Admin events added. AgentIdSet, ThresholdUpdated, OracleUpdated, with zero-address guards on the constructor and setOracle.

agentId == 0 ambiguity resolved. Added a registered mapping separate from agentIds, so agent ID zero is a valid registered value.

Fund-path tests added. test/TrustGateHook.t.sol covers the fund gate: high-trust pass, low-trust revert, unregistered revert, unauthorized-caller revert.

Trust boundary documented. Header block on the contract sets out the wallet-risk vs. agent-quality split you raised as an acceptable outcome. This hook gates on agent-quality signals (registered agents, (agentId, chainId, registry) lookup); wallet-risk gating by address alone is a distinct primitive that could live in a separate hook.

On the shared hook-level interface. I proposed isTrusted(address participant, uint8 threshold) returns (bool) in the earlier thread, tagging @JhiNResH. Happy to add it as a thin wrapper on this hook once the signature is agreed, or as a separate adapter layer if you'd rather keep the individual hooks minimal. Holding off on landing it in code until @JhiNResH has had a chance to weigh in; the "together" part of your ask matters more than the specific signature.

Ready for your review.

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.
@rnwy
Copy link
Copy Markdown
Author

rnwy commented Apr 19, 2026

Quick note on the split shape.

@douglasborthwick-crypto shipped PR #33 (WalletStateHook) overnight, which gives the stack a clean three-way:

All three inherit BaseERC8183Hook; all three gate different lifecycle stages with non-overlapping primitives. That matches the "same neutral pattern, clean split" you raised on April 15.

@JhiNResH
Copy link
Copy Markdown

Catching up now, @rnwy @psmiratisu — sorry for the latency, the email did land in spam.

On the signature. isTrusted(address participant, uint8 threshold) returns (bool) works for me. I can retrofit it onto TrustGateACPHook (#6) as a one-liner:

function isTrusted(address participant, uint8 threshold) external view returns (bool) {
    return oracle.getTrustScore(participant) >= uint256(threshold);
}

My oracle uses a 0–100 score; RNWY's uses (agentId, chainId, registry) resolution. uint8 is a clean lowest-common-denominator — each hook normalizes its internal score into that range, and callers stay oracle-agnostic. Two small points to nail down in the interface doc:

  1. Threshold scale is 0–255. Hooks scale internally if their native score is wider (e.g. basis points). If anyone hits >255 granularity later we can bump to uint16, but YAGNI for now.
  2. No job context. isTrusted is a pure participant query — value-tiered or per-job thresholds stay inside each hook's beforeAction. The shared interface is for composability (UI badges, router policy, other hooks querying trust), not for replacing lifecycle gating.

On where it lives. I'd put it in a small standalone file — contracts/interfaces/ITrustGatingHook.sol — with just the isTrusted function and NatSpec defining the scale. Both hooks import it and implement. No base-class coupling, no shared oracle model.

On landing order. Don't block either PR on this. My suggestion: (1) merge this PR and my #6 on their current shapes, (2) open a thin co-authored follow-up PR that adds ITrustGatingHook + both implementations. That keeps each PR small and reviewable per @psmiratisu's earlier guidance, and avoids a chicken-and-egg on the interface file.

Happy to draft the interface file and open that follow-up PR this week if you want to focus on shipping #9.

@rnwy
Copy link
Copy Markdown
Author

rnwy commented Apr 21, 2026

Clean proposal, appreciate the catch-up. The shared interface approach makes sense; merge both, then the thin follow-up keeps each PR reviewable. Happy to co-author on the ITrustGatingHook file if useful, or if you want to draft it that works too. uint8 normalization is fine on our side.

@rnwy
Copy link
Copy Markdown
Author

rnwy commented May 4, 2026

Thanks @ariessa; all four addressed:

  1. PR description updated; link now points to rnwy.com/methodology
  2. Threshold range guard added in constructor and setThreshold; new error TrustGateHook__InvalidThreshold(uint8) reverts on threshold == 0 or threshold > 95 (matches the documented 0-95 score range)
  3. IRNWYTrustOracle interface inlined into TrustGateHook.sol; standalone interface file deleted
  4. MultiHookRouter support added; hook now implements IERC8183HookMetadata with requiredSelectors() returning empty (client and provider gates are independent)

Pushed. Ready for re-review.

@rnwy
Copy link
Copy Markdown
Author

rnwy commented May 4, 2026

Hi @ariessa; clarifying the two underlying questions in case the link alone isn't enough:

  1. Methodology. Full page: rnwy.com/methodology. In short: every registered agent gets a trust score that starts at 50 and adjusts based on on-chain evidence across reviewer wallet history, ownership signals (owner wallet age, agent maturity, ownership continuity), commerce activity, registration quality, and sybil detection (five signals weighted 6×/5×/3×/1× plus a flat agent-level signal). When sybil severity reaches elevated or heavy, review-based contributions are nullified. The pipeline recomputes nightly and every score shows its math on the agent page.

  2. Threshold range: 1–100, where higher = more trust required. The reference RNWY oracle uses a 0–95 calibrated range documented at the methodology link above; other oracles implementing the same interface can define their own ceiling. The constructor and setThreshold guard against threshold == 0 (gate disabled) and threshold > 100 (unreachable by convention) via TrustGateHook__InvalidThreshold(uint8).

Comment thread contracts/hooks/TrustGateHook.sol Outdated
Comment thread contracts/hooks/TrustGateHook.sol
Comment thread contracts/hooks/TrustGateHook.sol Outdated
Comment thread contracts/hooks/TrustGateHook.sol Outdated
Comment thread contracts/hooks/TrustGateHook.sol Outdated
Comment thread contracts/hooks/TrustGateHook.sol Outdated
* This is distinct from (and complementary to) WALLET-RISK gating, which
* evaluates arbitrary EOAs by address alone and answers: is this wallet
* risky? A relying party can run both hooks independently — one for
* participant-level quality checks, one for transaction-level risk.
Copy link
Copy Markdown
Collaborator

@ariessa ariessa May 5, 2026

Choose a reason for hiding this comment

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

This mentions another PR, hence not needed and should be removed.

Suggested change
* participant-level quality checks, one for transaction-level risk.

Comment thread contracts/hooks/TrustGateHook.sol Outdated
Comment thread contracts/hooks/TrustGateHook.sol Outdated
Comment thread contracts/hooks/TrustGateHook.sol Outdated
Comment thread contracts/hooks/TrustGateHook.sol Outdated
Comment thread contracts/hooks/TrustGateHook.sol Outdated
Comment thread README.md Outdated
Comment thread contracts/hooks/TrustGateHook.sol Outdated
Comment thread contracts/hooks/TrustGateHook.sol Outdated
Comment thread contracts/hooks/TrustGateHook.sol Outdated
Copy link
Copy Markdown
Collaborator

@ariessa ariessa left a comment

Choose a reason for hiding this comment

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

Hi @rnwy,

Thank you for contributing to this repo. Please address all requested changes

@rnwy
Copy link
Copy Markdown
Author

rnwy commented May 5, 2026

Thanks @ariessa; all addressed:

  • Interface and all references renamed to ITrustOracle
  • Vendor-specific references removed from NatSpec and README
  • TRUST BOUNDARY and MULTIHOOKROUTER comment blocks removed
  • Constructor docstring tightened (erc8183Contract_ now reads "ERC-8183 core (AgenticCommerce)")
  • CEI pattern applied to setThreshold and setOracle (state change before emit)
  • Dropped _postComplete and _postReject overrides; base no-ops handle routing

Pushed. Ready for re-review.

Copy link
Copy Markdown
Collaborator

@ariessa ariessa left a comment

Choose a reason for hiding this comment

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

PR is looking good!

Can you document your hook following the CONTRIBUTION guideline?

@JhiNResH
Copy link
Copy Markdown

JhiNResH commented May 6, 2026

Thanks @ariessa, @rnwy. Since my #6 #32 and this PR were converging on a similar trust-gate shape, it makes sense to consolidate the work here instead of keeping parallel PRs.

I had also joined the earlier shared trust-gating discussion here: #9 (comment)

The remaining request looks scoped to documentation: updating the TrustGateHook NatSpec to follow CONTRIBUTING.md with explicit USE CASE, FLOW, and TRUST MODEL sections, while keeping the README row aligned.

@rnwy if you’re okay with it, I’m happy to draft that documentation-only patch. No behavior changes.

@rnwy
Copy link
Copy Markdown
Author

rnwy commented May 6, 2026

Thanks @JhiNResH, appreciate the offer. I'll handle the doc patch myself — small enough that another commit makes sense before merge. On consolidation: let's keep the original plan we worked out with @psmiratisu — ship #9 and #6 separately on their own merits, then the co-authored ITrustGatingHook follow-up after both land. Same outcome, cleaner record on each PR. 🙂

…BUTING.md

Updated documentation for TrustGateHook to clarify its use case, flow, and trust model.
@rnwy
Copy link
Copy Markdown
Author

rnwy commented May 6, 2026

Restructured NatSpec into USE CASE / FLOW / TRUST MODEL per CONTRIBUTING.md. Ready for re-review.

douglasborthwick-crypto added a commit to douglasborthwick-crypto/hook-contracts that referenced this pull request May 14, 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.
@rnwy
Copy link
Copy Markdown
Author

rnwy commented May 18, 2026

Resolved merge conflicts from recent merges into main. Branch is clean and ready for re-review when you have a cycle.

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.

4 participants