Skip to content

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

Open
rnwy wants to merge 9 commits intoerc-8183:mainfrom
rnwy:main
Open

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

Conversation

@rnwy
Copy link
Copy Markdown

@rnwy rnwy commented Mar 20, 2026

Summary

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

New Files

contracts/interfaces/IRNWYTrustOracle.sol

Extracted interface for agent-identity-based trust oracles. Lookup key is (agentId, chainId, registry) — supports multi-chain, multi-registry agent identity resolution.

Four functions: getScore(), hasScore(), meetsThreshold(), agentCount().

contracts/hooks/TrustGateHook.sol

IACPHook that reads from any IRNWYTrustOracle implementation to gate participants by trust score:

  • beforeAction(fund) — checks client trust score, reverts if below threshold
  • beforeAction(submit) — checks provider trust score, reverts if below threshold
  • afterAction(complete/reject) — emits outcome events (never reverts)

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.

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. Interface extraction is a structural refactor; zero logic changes from the original submission.

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

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.

3 participants