Skip to content

hardening(viem-chain): anchor wallet/public clients to chainId (SSOT 7.4) - #4

Merged
Hiksang merged 1 commit into
mainfrom
hardening/viem-chain-anchor
May 6, 2026
Merged

hardening(viem-chain): anchor wallet/public clients to chainId (SSOT 7.4)#4
Hiksang merged 1 commit into
mainfrom
hardening/viem-chain-anchor

Conversation

@Hiksang

@Hiksang Hiksang commented May 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

F3 follow-up tracked in PR #2
(qa/2026-05-05-test-foundation report). Anchors every viem
WalletClient / PublicClient built by the CLI to an explicit
chainId at construction time, instead of relying on viem's auto-fetch
of eth_chainId from the RPC.

Threat model: a MITM (or simply mis-pointed) RPC can return a
chainId for a fork the user did not intend to broadcast on. With
no anchor at the client, viem's signed payload uses whatever the RPC
returned. With this PR, the chainId is baked at new Executor(...)
time from the bundled chains.toml, so an RPC that drifts no longer
causes a tx to be signed for the wrong chain.

Changes

File Change
packages/defi-core/src/registry/chain.ts ChainConfig.viemChain() returns a viem-compatible Chain from the toml entry (id, name, native currency, rpc, optional explorer + multicall3). Local ViemChainShape interface keeps defi-core viem-agnostic.
packages/defi-core/src/provider.ts getProvider(rpcUrl, chain?) accepts an optional anchor; cache key becomes ${rpcUrl}@${chain.id} to prevent collisions across anchors.
packages/defi-cli/src/executor.ts Executor constructor takes an optional 4th chain arg. All 5 createPublicClient / createWalletClient sites spread this.chainOpt() so the anchor flows through fee fetching, simulation, allowance probing, and broadcast. Backwards-compatible.
packages/defi-cli/src/cli.ts makeExecutor() threads chain.viemChain() into the Executor.
packages/defi-core/src/registry/chain.test.ts 4 unit tests pinning the helper contract.

Test plan

  • Host: pnpm -C ts -r build clean.
  • Host: pnpm -C ts -r test82/82 pass (defi-core 32 + defi-protocols 21 + defi-cli 29).
  • Host: pnpm -C ts -r lint clean.
  • Docker: requires PR QA: SSOT establishment + test foundation (v1.0.12 baseline) #2 (Dockerfile) to merge first.
  • Reviewer to validate: signed tx on a real broadcast carries the expected chainId field.

Coordination with sibling PRs

🤖 Generated with Claude Code

…tion

SSOT 7.4 hardening: every viem WalletClient / PublicClient built by the
CLI now carries an explicit `chain` parameter pinned to the chains.toml
entry, instead of relying on viem's auto-fetch of `eth_chainId` from
the RPC. This defends against:

- MITM RPCs that lie about eth_chainId (returning a chainId for a fork
  the user did not intend to broadcast on).
- Offline-signing flows that need a deterministic chainId baked into
  the signature without an extra RPC round-trip.
- RPC drift / endpoint reassignment between client construction and
  the actual broadcast.

Changes:

1. defi-core/src/registry/chain.ts
   - ChainConfig.viemChain() builds a viem-compatible Chain from the
     toml entry: id (chain_id), name, nativeCurrency (native_token,
     18 decimals), rpcUrls.default.http (effectiveRpcUrl()), optional
     blockExplorers (explorer_url) and contracts.multicall3.
   - Returns a local ViemChainShape interface so defi-core stays
     viem-agnostic at the package boundary; consumers cast to viem's
     Chain when they wire the result.

2. defi-core/src/provider.ts
   - getProvider(rpcUrl, chain?) — when chain is provided, the cache
     key becomes `${rpcUrl}@${chain.id}` so two callers with the same
     RPC but different anchors don't collide on cached client.

3. defi-cli/src/executor.ts
   - Executor constructor accepts an optional 4th `chain: Chain` arg,
     stored as readonly. Backwards-compatible — existing callers
     (executor.test.ts, scripts) don't pass it and continue to work.
   - All five createPublicClient / createWalletClient call sites now
     spread chainOpt() so the anchor flows through fee fetching,
     simulation, allowance probing, and broadcast.

4. defi-cli/src/cli.ts
   - makeExecutor() pulls the ChainConfig from the registry and threads
     `chain.viemChain()` into the new Executor 4th arg.

5. defi-core/src/registry/chain.test.ts (new, 4 tests)
   - viemChain() returns full viem Chain shape with all required fields.
   - viemChain() omits optional explorer / multicall3 when missing.
   - viemChain() honors env-var RPC overrides via effectiveRpcUrl().
   - chainId is preserved verbatim — a config typo (chain_id = 0)
     surfaces immediately rather than auto-fetched at runtime.

Verified: monorepo build/test/lint all clean — 82/82 tests pass
(defi-core 32 + defi-protocols 21 + defi-cli 29).

Refs: F3 in docs/qa-reports/2026-05-05-test-foundation.md.
@Hiksang
Hiksang merged commit f36c257 into main May 6, 2026
4 checks passed
@Hiksang
Hiksang deleted the hardening/viem-chain-anchor branch May 6, 2026 08:59
Hiksang added a commit that referenced this pull request May 6, 2026
PR #3 (feat/slippage-protection) closed 14 of the 15 sites that the
2026-05-05 baseline catalogued in qa/slippage.test.ts:KNOWN_INFINITE_SLIPPAGE.
The line numbers also shifted when PR #4 (viem-chain anchor) refactored
uniswap_v3.ts, so every entry in the original snapshot became stale on
2026-05-06 main and the test went red on every new PR (e.g. #7).

This commit re-grounds the snapshot against current main:

  Adapter scan (regex unchanged): exactly 1 hit remains in the entire
  defi-protocols/src tree —

    dex/uniswap_v3.ts:262   amountOutMinimum: 0n  (eth_call simulation
                            inside quote() fallback; never broadcasts a tx)

  PR #3 hardened the other 14 sites:
    algebra_v3.ts:86,273,278,304     -> applyMinSlippage / explicit min args
    balancer_v3.ts:37                -> applyMinSlippage
    thena_cl.ts:78,165,190           -> applyMinSlippage / explicit min args
    uniswap_v3.ts:90,242,343,344,    -> applyMinSlippage / explicit min args
                  363,364,403

In addition to refreshing the set, this commit fixes a self-trigger
bug: uniswap_v3.ts:94 carries a documentation comment that warns
against `amountOutMinimum: 0n`, and that comment matched the regex
on its own — driving a false-positive "novel site" failure. Adding a
`commentLinePattern` skip in both `it` blocks keeps the guard's
intent (catch real broadcast paths) while letting docs reference the
forbidden literal.

Verified:
  - pnpm -C ts -r build      — synced + dist rebuilt clean.
  - pnpm -C ts -r lint       — 3 packages, tsc --noEmit clean.
  - pnpm -C ts -r test       — 64/64 passing (was 62/64 before this fix).

Refs: PR #7 CI failure (3x Build & Test red) was the smoke; root cause
was the line-number drift, not anything Monad-related.
@Hiksang
Hiksang restored the hardening/viem-chain-anchor branch May 8, 2026 11:04
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.

1 participant