Skip to content

feat(protocols): add Merkl rewards claim adapter for Monad mainnet - #187

Merged
nishuzumi merged 6 commits into
nishuzumi:mainfrom
chin0312:feat/merkl-rewards-adapter-v2
Sep 15, 2026
Merged

nishuzumi merged 6 commits into
nishuzumi:mainfrom
chin0312:feat/merkl-rewards-adapter-v2

Conversation

@chin0312

@chin0312 chin0312 commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor

What and why

Add a first-class Merkl rewards Protocol package for Monad mainnet. Agents can discover an account's Merkl rewards with an on-chain cross-check, select 1-16 reward tokens, build one unsigned self-claim transaction, simulate it, and receive an exhaustive typed Receipt proving the actual payout.

Replacement / prior review provenance

This is the canonical replacement for #173. That PR closed when its source fork was accidentally deleted; the contribution was not intentionally withdrawn and was not rejected.

GitHub's preserved pull ref was recovered and verified at exact old head 985ca861220bf572eb319b6900230bdfb8156eea. pillowtalk-Qy completed the first independent audit of that exact head and found no new claim/Receipt evidence blocker. The two original commits were replayed with their original attribution, then refreshed against current main.

Replacement for #173.

Current-main refresh

This branch is based on e958f7f40c12c3ba514d93a18b02bed7eb8cdca0, the latest upstream/main fetched before this refresh.

Differences from the previously audited head:

  • resolved the README and MCP integration against the current Aave, Clober, Kintsu, Morpho, Pendle, and Uniswap composition;
  • added Merkl to the current bilingual README package-order regression contract;
  • added exact root and ordered leaf Receipt text-projection coverage for Agent-facing MCP output;
  • repaired the Merkl lockfile importer from the stale PostCSS 8.5.16 tsup peer snapshot to current main's existing PostCSS 8.5.23 snapshot;
  • rebased onto Core feat(core): allow explicitly authored empty risk lists #190 support for explicitly authored empty risk lists, switched Merkl claim metadata to risk: [], removed the temporary placeholder, and added a Receipt assertion that no Change moves assets out of the account;
  • re-ran current keyless deployment, ABI derivation, live simulation, full workspace, MCP composition, and package-order checks.

Merkl claim/query/Receipt execution semantics remain unchanged from audited head 985ca86. This refresh changes the Registry metadata to the Core-approved risk: [], removes the old compatibility placeholder, and adds a no-outbound-asset Receipt assertion; the Distributor proxy, implementation, ABI, bytecode hashes, safety boundaries, Query, Capability, and Receipt parser remain unchanged and were re-verified live.

Adapter surface

  • Query: merkl.rewards({ account })
  • Capability: merkl.claim({ tokens })
  • Typed Receipt: { operation: "claim", account, rewards: [{ token, amount }] }

The Capability accepts only an ordered list of 1-16 unique reward-token addresses. It owns exactly one direct unsigned Distributor.claim transaction and requires no approval.

Safety boundaries

  • Self-claim only: every claim user is ActionCtx.account; the Agent cannot inject a user, Distributor, recipient, cumulative amount, proof, or calldata.
  • API candidate, on-chain authority: the Merkl API supplies fresh candidates (reloadChainId=143 for construction), while the active root, claimed amount, and effective recipient are read from the Distributor.
  • Local proof verification: both Query and construction verify keccak256(abi.encode(user, token, cumulativeAmount)) with sorted proof pairs. A valid single-leaf empty proof is accepted only when the leaf equals the active root.
  • Amount semantics: the cumulative API amount is supplied to claim; the positive incremental payout is cumulative - onchain claimed. Pending rewards never enter calldata, and cumulative values are bounded to uint208.
  • Recipient enforcement: token-specific, then account-wide, then self fallback is resolved exactly like the deployed Distributor. Any redirect away from the acting account is rejected.
  • Receipt evidence: each reward must appear as the exact ordered pair Distributor.Claimed(user, token, incrementalAmount) then token.Transfer(Distributor, user, incrementalAmount). Emitters, parties, token, amount, identity, length, and order are authenticated; malformed, duplicate, missing, reordered, decoy, ambiguous, and unexplained Changes fail closed.

Excluded scope remains claims for other users, operator controls, recipient configuration, claimWithRecipient, callbacks, swaps, vault deposits, campaign/admin/dispute/tree/governance operations, cross-chain claims, and arbitrary Distributor addresses.

ABI/deployment provenance

  • Distributor ERC-1967/UUPS proxy: 0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae
  • Active implementation: 0x3f0fa7847b1b2e4515a93e05b29f115d9bb51d85
  • Proxy runtime hash: 0x1663c7ebc964dfa69a98528bccf3431438c5149fbabb8313d33374df61f3985a
  • Implementation runtime hash: 0x9c2edb9dffd093d12e7361203d857bf6d1bd208fcc1aeff7a8b6ac73323a04e7

The live keyless deployment suite re-read the EIP-1967 implementation slot, matched both bytecode hashes, checked required selectors and the Claimed topic, and read a non-zero active root. The committed 73-entry ABI remains deterministically rendered from the explorer-verified implementation and protected against hand edits.

Current RiskLabel treatment

The claim is inflow-only. Core #190 now allows an explicitly authored empty risk list while keeping the field required, so Merkl uses risk: [] because no current closed-set label accurately describes this inflow-only operation. The Receipt test retains relevant Changes and asserts that no asset leaves the acting account, as required by #164. No Core changes are included in this PR.

Current-main integration

  • adds @themoss/protocol-merkl to the default MCP Registry composition;
  • validates real merkl.rewards discovery/load and merkl.claim discovery/load;
  • updates both supported-protocol tables and their current package-order regression fixture;
  • locks Agent-facing root and ordered leaf Receipt text;
  • adds Merkl to the linked changeset group;
  • includes minor changesets for @themoss/protocol-merkl and @themoss/mcp-server.

Verification

Fresh local results on this exact branch:

  • pnpm install --frozen-lockfile — passed with pnpm 11.10.0
  • pnpm lint — 287 files checked
  • pnpm build — 21/22 workspace projects built (the private root is not a build project)
  • pnpm typecheck — 21/22 workspace projects passed
  • pnpm test:offline — 717 passed, 45 expected live skips
  • pnpm test — blocked by 5 unrelated Aave Monad live-fixture failures (all Merkl tests and offline tests pass); see the local verification note below
  • pnpm --filter @themoss/protocol-merkl test — 30/30 passed
  • pnpm --filter @themoss/mcp-server test — 23/23 passed
  • pnpm audit --prod --audit-level high — exited 0 with 0 high/critical vulnerabilities; 3 moderate Hono advisories are inherited through the MCP SDK
  • pnpm changeset status --since=upstream/main — Merkl and MCP server minor bumps
  • Local full pnpm test reached unrelated Aave mainnet fixture failures (expired address-book quarantine plus no live account for the Aave health/supply/borrow/repay roles); this Merkl-only change did not alter Aave sources.
  • pnpm audit --prod --audit-level high reported three moderate Hono advisories (<4.13.5) through the MCP SDK; no high or critical advisories were reported, and no dependency changed in this Merkl refresh.
  • git diff --check

Live simulation

The unsigned Monad mainnet happy path passed for public account 0x461549c73FFfB676860A0E49F5DaABEcf4E8D2d7 and reward token 0x3bd359C1119dA7Da1D913D1C4D2B7c461115433A, with incremental claimable amount 49640922692037687136. It produced one transaction, no halt, zero Warnings, and the exhaustive ordered two-Change Claimed -> Transfer Receipt. No signing, broadcasting, private key, or storage mutation occurred.

Hosted CI

GitHub created CI run 34684359452 for this refreshed head, but it is action_required and no hosted job executed pending maintainer approval. Hosted CI is therefore not claimed as passing.

ABI-online caveat

MONADSCAN_API_KEY was unavailable, so the keyed online ABI comparison was not run. All keyless ABI derivation, proxy/implementation, bytecode, selector, topic, read-surface, and live simulation checks passed.

AI assistance

OpenAI Codex assisted with recovery, current-main adaptation, and verification. The results above were run against the resulting committed tree.

@chin0312

Copy link
Copy Markdown
Contributor Author

Hi @pillowtalk-Qy @nishuzumi — this is the refreshed replacement for accidentally closed #173. I recovered exact audited head 985ca86, updated it against current main, addressed the lockfile blocker from the first audit, and reran the current verification suite. The PR body lists the exact differences from the previously audited head. @pillowtalk-Qy, a range-diff/final review against this exact head would be very welcome.

@chin0312

chin0312 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Hi @portdeveloper — could #187 be registered as my current MOST claim?

This is the refreshed replacement for #173, which was accidentally closed when I deleted the source fork. The Merkl implementation was already independently reviewed on the old exact head and #187 brings it forward onto current main.

I noticed MOST claims are normally registered from scoped issues, while this contribution originated directly as a PR rather than from a separate Merkl issue. If a linked scoped issue is required for the claim, please let me know the preferred way to handle it.

@nishuzumi nishuzumi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I audited exact head e0877da53b44c6a6db8b9f9d04201c2ec195b5cf against current main@e5ef4f310a2d879e26b73edb207f662f231ba948.

This PR gives Agents first-class Merkl reward discovery and safe fixed-Distributor self-claim construction, and adds the package to the default MCP composition. The claim boundary, off-chain-candidate/on-chain-verification model, cumulative-versus-incremental accounting, exhaustive Receipt, deployment provenance, and current-main lock refresh are coherent. Two changes remain required:

  1. [medium] Publish an accurate RiskLabel. packages/protocols/merkl/src/adapter.ts:121-124 declares risk: ["fundOut"] while explicitly acknowledging that this claim is inflow-only. CONTEXT.md defines fundOut as assets leaving the account in the current transaction. Registry.load therefore exposes a known-false public safety fact to Agents. Existing aPriori/FastLane placeholders are documented debt, not permission to add another contradictory contract. Please resolve #164 with the maintainer-approved Core representation, then use that representation here; do not ship fundOut for this self-claim.

  2. [low] Complete the required compile-time Protocol fixture. packages/protocols/merkl/test/types.fixture.ts covers parameter and result inference, but not this package's ABI-generic Handle contract or invalid Receipt-name binding. CONTRIBUTING.md:48, ADR 0001, and the package template require positive and @ts-expect-error negative coverage for valid/invalid Handle calls and Receipt-name autocomplete. Please add valid claim/read Handle calls, rejected unknown/bad-argument calls, and an invalid Receipt binding fixture.

Independent verification on this exact tree passed frozen install, lint, build, typecheck, the full clean-environment offline suite, the focused MCP suite, the focused Merkl live suite (30/30, including a zero-Warning self-claim with exact Claimed -> Transfer evidence), the keyed MonadScan semantic ABI suite (3/3), and pnpm audit --prod --audit-level high with no known vulnerabilities. I also independently matched the verified Distributor source, active ERC-1967 implementation, current on-chain root, API candidate, and Merkle proof. GitHub-hosted checks are not currently reported for this PR.

@chin0312

chin0312 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review. I’ve addressed the compile-time fixture finding in e0aa707.

The Merkl fixture now covers the ABI-typed Distributor Handle with valid claim and read calls, rejects unknown methods and invalid arguments with @ts-expect-error, and covers both valid and invalid Receipt-name bindings. No production Merkl behavior or Core code changed.

Fresh local checks pass: frozen install, lint, build, typecheck, Merkl 30/30, offline suite, and git diff --check.

The remaining blocker is the RiskLabel point. I’ve intentionally left that untouched rather than choosing a Core representation inside this PR. Could you confirm the intended direction for #164 — fundIn or an explicitly authored risk: [] — so the approved Core representation can land and I can adopt it here?

@nishuzumi

Copy link
Copy Markdown
Owner

#164 is decided: risk: [] with the field kept required. Once the Core PR lands, switch claim to risk: [], drop the placeholder comment, and add a Receipt assertion that no Change leaves the account. The fixture fix in e0aa707 looks right; I will re-review once the RiskLabel change is in.

@portdeveloper

Copy link
Copy Markdown
Collaborator

Core #190 merged on September 9, so the dependency for this revision is available. Please rebase onto current main and apply the change nishuzumi requested: set claim to risk: [], remove the placeholder, and add the Receipt assertion that no Change leaves the account. Then request his re-review on the updated head.

@chin0312
chin0312 force-pushed the feat/merkl-rewards-adapter-v2 branch from e0aa707 to 5b119d4 Compare September 12, 2026 08:51
@chin0312

Copy link
Copy Markdown
Contributor Author

@nishuzumi Core #190 is now included in current main. I rebased #187 onto e958f7f, switched Merkl claim metadata to the explicitly authored risk: [], removed the placeholder, and added the Receipt assertion that no Change leaves the acting account. Fresh results: Merkl 30/30, offline 717 passed with 45 expected live skips, MCP 23/23, and typecheck/build/lint pass; full pnpm test is blocked only by five unrelated Aave live-fixture failures. Please re-review the updated head 5b119d4.

@portdeveloper

Copy link
Copy Markdown
Collaborator

@nishuzumi there are fresh revisions for your re-review: #187 now uses risk: [] and includes the no-outbound Receipt assertion; #199 switches the unverified Router to the degraded provenance path. On #199, please check the ABI binding: the test currently checks function names in the vendored ABI, then hashes handwritten signatures.

#198 is past the review SLA and still needs the keyed aprMON ABI check. I read its source-table and generation changes, but the shared Aave failure still prevents a complete hosted live-suite result. Please finish its review and the pending Merkl/Kuru re-reviews once the validation is available.

@nishuzumi nishuzumi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Re-reviewed exact head 5b119d4e against main@f6df1b0, plus my follow-up cd040b2.

Both items from the 2026-09-01 review are resolved. claim now declares risk: [] with the placeholder gone; the type fixture covers the ABI-typed Distributor Handle (valid claim/read calls, rejected unknown/bad-argument calls) and both valid and invalid Receipt-name bindings — all sixteen @ts-expect-error directives are load-bearing. The package delta since the audited head is exactly the four requested hunks; the non-package hunks are byte-identical across the rebase; the lockfile adds only the merkl importer.

On the risk: [] contract: I probed seven outbound shapes (ERC-20 or native Transfer from the acting account in every position) and claimReceipt fails closed on all of them — strict Claimed/Transfer pairing, Distributor-only sender, user-only recipient, amount equality — so no Merkl Receipt can carry an outbound Change. The test-level receiptMovesAssetsOut check is therefore documentary, and cd040b2 makes it honest: it is asserted on the multi-token Receipt too, and one committed case shows the parser rejecting a Transfer from the account and the helper flipping to true, so the declaration is demonstrably refutable rather than asserted only on the happy path.

Verification on this head: frozen install, lint, build, typecheck, full offline suite (717 passed / 45 skipped), Merkl live 30/30 including the zero-Warning mainnet self-claim, keyed MonadScan ABI suite 3/3, MCP 23/23. pnpm audit --prod is red on the PR head only because its base predates #196; on the merged tree it reports no advisories. Remaining live failures (Aave #201, Kuru #194) are baseline.

@nishuzumi
nishuzumi merged commit 8a3bc89 into nishuzumi:main Sep 15, 2026
@nishuzumi

Copy link
Copy Markdown
Owner

@chin0312 merged as 8a3bc89 — thanks for carrying this through two Core decisions. I pushed cd040b2 before the squash: the no-outbound assertion now also runs on the multi-token Receipt, and one case shows the parser rejecting a Transfer from the acting account while the helper returns true, so risk: [] is demonstrably refutable in the suite and not only asserted on the happy path. Merkl ships in the default MCP composition from the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants