feat: trust gate hook — on-chain trust scoring for job lifecycle gating#9
feat: trust gate hook — on-chain trust scoring for job lifecycle gating#9rnwy wants to merge 18 commits into
Conversation
|
Yes, great work on #6! Complementary to it: different signal, different coverage, same hook interface.
Composable by design. A relying party can run both to check performance and trust independently. |
|
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. |
|
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! |
…interface) feat: add TrustGateHook (provider-agnostic, imports IRNWYTrustOracle interface)
|
Thanks for the suggestion! We extracted an While working on this, we looked at the
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. |
|
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! |
|
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 — We'll reach out to @JhiNResH directly to coordinate and come back with a concrete joint proposal within the next few days. |
|
@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. |
JhiNResH
left a comment
There was a problem hiding this comment.
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
TrustGatedandOutcomeRecordedwith 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.solbut the file isTrustGateHook.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.
- 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.
|
@psmiratisu, rebased per your direction. Canonical base adopted. Admin events added.
Fund-path tests added. 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, On the shared hook-level interface. I proposed Ready for your review. |
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.
|
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. |
|
Catching up now, @rnwy @psmiratisu — sorry for the latency, the email did land in spam. On the signature. 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
On where it lives. I'd put it in a small standalone file — 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 Happy to draft the interface file and open that follow-up PR this week if you want to focus on shipping #9. |
|
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 |
|
Thanks @ariessa; all four addressed:
Pushed. Ready for re-review. |
|
Hi @ariessa; clarifying the two underlying questions in case the link alone isn't enough:
|
| * 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. |
There was a problem hiding this comment.
This mentions another PR, hence not needed and should be removed.
| * participant-level quality checks, one for transaction-level risk. |
|
Thanks @ariessa; all addressed:
Pushed. Ready for re-review. |
ariessa
left a comment
There was a problem hiding this comment.
PR is looking good!
Can you document your hook following the CONTRIBUTION guideline?
|
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. |
|
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.
|
Restructured NatSpec into USE CASE / FLOW / TRUST MODEL per CONTRIBUTING.md. Ready for re-review. |
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.
|
Resolved merge conflicts from recent merges into main. Branch is clean and ready for re-review when you have a cycle. |
Summary
Adds
TrustGateHook— a provider-agnostic ERC-8183 hook that gates job lifecycle transitions by on-chain trust score, reading from any oracle that implementsIRNWYTrustOracle.New Files
contracts/hooks/TrustGateHook.solERC-8183 hook (inheriting
BaseERC8183Hook) that reads from anyIRNWYTrustOracleimplementation 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
IRNWYTrustOracleinterface is inlined in the same file. The hook maps wallet addresses to agent IDs, then callsmeetsThreshold()on the oracle. The oracle does all scoring; the hook is a gate, not a judge.Implements
IERC8183HookMetadataforMultiHookRoutercompatibility.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
IRNWYTrustOraclewith 138,000+ agent scores covering ERC-8004, Olas, and Virtuals across 11 chains.0xD5fdccD492bB5568bC7aeB1f1E888e0BbA6276f4(source-verified via Sourcify)Security
Uses
abi.decodethroughout — no raw assembly, no delegatecall. Owner-managed admin setters guard against zero-value, idempotent, and out-of-range updates.