Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/ens-mainnet-l1-opt-in-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@openzeppelin/adapter-evm": minor
"@openzeppelin/adapter-evm-core": minor
---

Opt-in mainnet-L1 miss-fallback for forward+reverse ENS resolution (definitive-miss only, never-silent) with cross-network provenance triplet; requires @openzeppelin/ui-types ^3.3.0.
38 changes: 38 additions & 0 deletions docs/releases/003-ens-mainnet-l1-opt-in-fallback/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Changelog — 003 Mainnet-L1 Opt-In Fallback

> Draft release notes. Changesets merges this body into `packages/adapter-evm/CHANGELOG.md` (and core
> changelog if generated) when the version PR runs `changeset version`.

## [@openzeppelin/[email protected]] — pending publish

### Minor Changes

- **Opt-in mainnet-L1 miss-fallback (default OFF)** for ENS `resolveName` and `resolveAddress` on
non-mainnet-bound adapters. When `enableMainnetL1MissFallback: true` and `ensL1Client` is wired,
consult mainnet L1 only after a **definitive bound-chain empty** / `NAME_NOT_FOUND` on UR-carrying
chains. Gateway, transport, and timeout failures remain terminal (never-silent-fallback).
- **Cross-network fallback provenance triplet** on L1 miss-fallback successes after UR bound-empty
miss: `resolvedViaNetworkFallback`, `queriedOnNetworkId`, `resolvedOnNetworkId` on base
`ResolutionProvenance`. Non-UR direct L1 and canonical forward `001` 1b paths omit the triplet.
`scopedToNetworkId` stays absent on L1 fallback hits (global display preserved).
- **Reframes `002` reverse L1 ladder** behind the same opt-in — always-on reverse miss-fallback is
no longer the default; integrators opt in explicitly.

### Changed

- Raise `@openzeppelin/ui-types` peer/dev floor from `^3.2.0` to **`^3.3.0`** (triplet fields).
Consumers must install ui-types `^3.3.0` alongside this adapter version.

### Migration Guide

1. `pnpm add @openzeppelin/adapter-evm@^2.3.0 @openzeppelin/ui-types@^3.3.0`
2. Default behavior unchanged if you omit `enableMainnetL1MissFallback`.
3. To restore cross-network ENS after bound-empty miss, pass `enableMainnetL1MissFallback: true` on
`createNameResolution` options.
4. Read fallback disclaimer via `resolvedViaNetworkFallback === true` — not from absent
`scopedToNetworkId`.

## [@openzeppelin/[email protected]] — workspace only (private)

Bundled into `@openzeppelin/adapter-evm` — not published to npm independently. Version advances in
lockstep for workspace truth and changelog attribution.
98 changes: 98 additions & 0 deletions docs/releases/003-ens-mainnet-l1-opt-in-fallback/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Release — 003 Mainnet-L1 Opt-In Fallback (`@openzeppelin/adapter-evm`)

> Ships opt-in mainnet-L1 miss-fallback for ENS forward and reverse resolution (default **OFF**),
> cross-network fallback provenance on definitive bound-empty misses, and the `002` reverse ladder
> reframe — exclusively through a **public `@openzeppelin/adapter-evm` npm bump** that bundles
> private `@openzeppelin/adapter-evm-core`.

## Overview

**Audience:** npm consumers of `@openzeppelin/adapter-evm`, release engineers, and UIKit integrators
pinning adapter + `@openzeppelin/ui-types` together.

**What this release delivers:**

- **SF-1** — `enableMainnetL1MissFallback` opt-in (strict `true` only; default OFF).
- **SF-2** — Cross-network provenance triplet on base `ResolutionProvenance` when L1 miss-fallback
succeeds after a **UR bound-empty miss** (`resolvedViaNetworkFallback`, `queriedOnNetworkId`,
`resolvedOnNetworkId`). Non-UR direct L1 omits the triplet.
- **SF-3 / SF-4** — Reverse and forward mainnet-L1 miss-fallback ladders (definitive-miss only;
never-silent-fallback on gateway/transport failures).

**Prerequisite:** `@openzeppelin/ui-types@^3.3.0` (triplet fields). Publish ui-types **before** or
alongside this adapter release.

**Release-trap guard:** `@openzeppelin/adapter-evm-core` is `private: true` and bundled via `tsdown`
`noExternal`. A core-only version bump does **not** reach npm consumers — the `002` failure mode.
This release uses a **dual-package changeset** bumping **both** `adapter-evm` and `adapter-evm-core`.

## Quick Start (consumer)

```bash
pnpm add @openzeppelin/adapter-evm@^2.3.0 @openzeppelin/ui-types@^3.3.0
```

Opt-in is **explicit** — wiring `ensL1Client` alone does not enable miss-fallback:

```ts
import { createNameResolution } from '@openzeppelin/adapter-evm';

const capability = createNameResolution(networkConfig, {
publicClient: boundClient,
ensL1Client: mainnetClient,
enableMainnetL1MissFallback: true, // default OFF when omitted
});
```

Default posture is unchanged: without `enableMainnetL1MissFallback: true`, bound-empty reverse and
bound `NAME_NOT_FOUND` forward stay terminal (no L1 consult).

## Expected versions (after `changeset version`)

| Package | From | To |
|---------|------|-----|
| `@openzeppelin/adapter-evm` | `2.2.0` | `2.3.0` (minor) |
| `@openzeppelin/adapter-evm-core` | `1.2.0` | `1.3.0` (minor, private) |
| Linked adapters (`midnight`, `polkadot`, `solana`, `stellar`) | `2.2.0` | `2.3.0` (Changesets linked group) |

Exact numbers are produced by Changesets — do not hand-edit `package.json` versions.

## Key Concepts

- **Install `adapter-evm`, not core.** npm consumers receive bundled core in `adapter-evm/dist`.
Workspace `[email protected]` alone does not satisfy SC-005.
- **Dual changeset is mandatory.** `.changeset/ens-mainnet-l1-opt-in-fallback.md` lists both packages.
- **ui-types floor `^3.3.0`.** Required for triplet field types at compile time and runtime honesty.
- **Emission rule.** Triplet appears only after UR bound-empty → L1 success with opt-in ON; canonical
non-UR forward L1 (`001` 1b) and non-UR direct reverse omit it.
- **Dist verification.** Post-build grep on `packages/adapter-evm/dist` proves the bundle contains the
003 delta (see Integration Guide — maintainer checklist).

## Feature documentation

Runtime behavior is documented in the name-resolution package docs:

- [`packages/adapter-evm-core/docs/name-resolution/README.md`](../../packages/adapter-evm-core/docs/name-resolution/README.md)
- [`integration-guide.md`](../../packages/adapter-evm-core/docs/name-resolution/integration-guide.md) —
Patterns 8–10 (opt-in ladders + triplet)

## Integration Guide

See [integration-guide.md](./integration-guide.md) for consumer adoption, migration, and maintainer
release verification.

## Changelog

See [CHANGELOG.md](./CHANGELOG.md) for the release notes body (merged into package CHANGELOGs by
Changesets on version PR).

## Safety

- **Default OFF** — no silent cross-network resolution; integrators must opt in explicitly.
- **Never-silent-fallback** — bound gateway/timeout failures do not fall through to L1.
- **Do not infer fallback** from absent `scopedToNetworkId`; use `resolvedViaNetworkFallback === true`.
- **Pin ui-types `^3.3.0`** with the adapter bump — older ui-types lack triplet fields.

## License

Inherits the `@openzeppelin/adapter-evm` package license.
120 changes: 120 additions & 0 deletions docs/releases/003-ens-mainnet-l1-opt-in-fallback/integration-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Integration Guide — 003 Mainnet-L1 Opt-In Fallback Release

## Pattern 1: Consumer upgrade (npm)

```bash
pnpm add @openzeppelin/adapter-evm@^2.3.0 @openzeppelin/ui-types@^3.3.0
```

After publish, confirm the installed version:

```bash
npm view @openzeppelin/adapter-evm version
```

**Smoke check** — bundled dist should expose the opt-in seam (no separate core install):

```bash
rg 'enableMainnetL1MissFallback' node_modules/@openzeppelin/adapter-evm/dist/
rg 'resolvedViaNetworkFallback' node_modules/@openzeppelin/adapter-evm/dist/
```

Both patterns should match. If absent, you are on a pre-003 publish (`2.2.0` or earlier) or a stale
lockfile.

## Pattern 2: Opt-in wiring (dapp / UIKit)

Shared runtime profiles wire `ensL1Client` but **do not** pass `enableMainnetL1MissFallback: true` —
safe default preserved. To enable cross-network ENS after bound-empty miss:

```ts
import { createNameResolution } from '@openzeppelin/adapter-evm';

const nameResolution = createNameResolution(networkConfig, {
publicClient: boundClient,
ensL1Client: mainnetClient,
enableMainnetL1MissFallback: true,
});
```

Read cross-network disclaimer from provenance (UIKit: `@openzeppelin/ui-utils` `isCrossNetworkFallback`):

```ts
import type { ResolutionProvenance } from '@openzeppelin/ui-types';

function isCrossNetworkFallback(
p: Pick<ResolutionProvenance, 'resolvedViaNetworkFallback'>,
): boolean {
return p.resolvedViaNetworkFallback === true;
}
```

Full behavior matrices: [`name-resolution/integration-guide.md`](../../packages/adapter-evm-core/docs/name-resolution/integration-guide.md) Patterns 8–10.

## Pattern 3: Migration from pre-003 (`[email protected]`)

| Topic | Before (`2.2.0` npm) | After (`2.3.0`+) |
|-------|----------------------|------------------|
| Reverse L1 after bound-empty | Always-on when `ensL1Client` wired (002 workspace only; **not** on npm `2.2.0`) | Opt-in gated; **OFF** by default |
| Forward UR bound miss | Bound-UR-authoritative (`NAME_NOT_FOUND` terminal) | Opt-in enables one L1 consult |
| Provenance triplet | Absent | Present on UR bound-empty → L1 success only |
| ui-types floor | `^3.2.0` | `^3.3.0` required |

**No breaking default:** omitting `enableMainnetL1MissFallback` preserves safe posture. Apps that
relied on workspace-only 002 always-on reverse must explicitly opt in after upgrade.

## Pattern 4: Maintainer release verification (pre-merge)

Run after `pnpm build` at monorepo root (post-changeset version PR or local `changeset version`):

```bash
DIST=packages/adapter-evm/dist
for pat in \
enableMainnetL1MissFallback \
mayConsultL1ForMissFallback \
resolvedViaNetworkFallback \
queriedOnNetworkId \
resolvedOnNetworkId \
networkFallbackProvenanceFields \
ethereum-mainnet
do
rg -l "$pat" "$DIST"/*.mjs "$DIST"/*.cjs || { echo "FAIL: $pat"; exit 1; }
done
echo "SF-5 dist verification PASS"
```

**Release-trap checklist:**

- [ ] Changeset lists **both** `@openzeppelin/adapter-evm` and `@openzeppelin/adapter-evm-core`
- [ ] ui-types floor `^3.3.0` in both EVM `package.json` files (Code stage — already applied)
- [ ] Dist grep V-1–V-7 PASS
- [ ] `@openzeppelin/[email protected]` on npm before publish
- [ ] Do **not** merge core-only changeset

## Pattern 5: Monorepo test infra (`vitest` ui-types dedupe)

The workspace may resolve multiple `@openzeppelin/ui-types` copies (adapter + UIKit sibling). Shared
vitest config dedupes ui-types for consistent `instanceof` / type identity in tests:

```ts
// vitest.shared.config.ts
resolve: {
dedupe: ['@openzeppelin/ui-types', '@openzeppelin/ui-renderer'],
},
```

This is a **test-infra** fix for multi-version workspace layouts — not a runtime consumer requirement.

## Common Mistakes

- **Bumping only `adapter-evm-core`.** Does not republish npm bundle — 002 trap repeats.
- **Expecting triplet on non-UR direct L1.** Canonical `001` 1b paths omit it.
- **Upgrading adapter without ui-types `^3.3.0`.** Triplet fields missing on consumer types.
- **Assuming `ensL1Client` implies opt-in.** Gate is separate; default remains OFF.
- **Trusting workspace core version without dist grep.** Version bump ≠ bundled bits.

## See also

- [README](./README.md) — release overview
- [CHANGELOG](./CHANGELOG.md) — release notes draft
- `.changeset/ens-mainnet-l1-opt-in-fallback.md` — source changeset body
59 changes: 38 additions & 21 deletions packages/adapter-evm-core/docs/name-resolution/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ covering both directions plus ENS v2 and mainnet-L1 reverse:
- **ENS v2** (L1-only: CCIP-Read / offchain gateways + cross-chain via `coinType`), SF-5 — every
forward result now carries an EVM-specific `EnsProvenance` with **observed** offchain facts you
narrow to via `isEnsProvenance`, and an L2-bound runtime can resolve chain-scoped names on L1.
- **Mainnet-L1 reverse miss-fallback**, SF-1 (002) — on non-mainnet-bound adapters, reverse tries
the bound chain first; on a **definitive empty** only, consults the gated mainnet L1 client for the
default primary name (+ avatar), with provenance that marks **global vs network-local** scope via
base `scopedToNetworkId`.
- **Mainnet-L1 miss-fallback (003)**, SF-1–SF-4 — on non-mainnet-bound adapters, **opt-in gated**
(default **OFF**): after a **definitive bound-chain empty** on UR-carrying chains, consult mainnet L1
for the default primary / forward record when the integrator sets
`enableMainnetL1MissFallback: true`. Reverse reframes 002's always-on ladder behind the same switch.
L1-fallback successes emit a **chain-agnostic fallback triplet** on base `ResolutionProvenance`
(`resolvedViaNetworkFallback`, `queriedOnNetworkId`, `resolvedOnNetworkId`) — owned by
`@openzeppelin/[email protected]`, not adapter-invented fields.

## Overview

Expand Down Expand Up @@ -168,18 +171,28 @@ always-present `system: 'ens'` discriminant; `label` is a display string that is
mismatched name. Either it returns a name that provably forward-resolves back to the queried
address (`forwardVerified: true`), or it returns `ADDRESS_NOT_FOUND`. There is no
`forwardVerified: false` path in this adapter — the anti-spoofing decision is made *for* you.
- **Reverse miss-fallback (002 SF-1, Option B).** On a non-mainnet-bound adapter with a wired
`ensL1Client`: when the bound chain has a Universal Resolver, bound reverse runs first; only on a
**definitive empty** (no usable primary — not a gateway/transport failure) does the adapter consult
mainnet L1 for the default primary (`coinType` 60). Bound-local hits win without L1; bound failures
surface typed errors and **never** silently fall through to L1. Non-UR + L1 → L1 direct; non-UR + no
L1 → `UNSUPPORTED_NETWORK`; mainnet-bound → bound only.
- **Reverse miss-fallback (003 SF-3 / 002 reframe, opt-in gated).** On a non-mainnet-bound adapter
with a wired `ensL1Client` and **`enableMainnetL1MissFallback: true`**: when the bound chain has a
Universal Resolver, bound reverse runs first; only on a **definitive empty** (no usable primary — not
a gateway/transport failure) does the adapter consult mainnet L1 for the default primary (`coinType`
60). With opt-in **OFF** (default), bound-empty returns `ADDRESS_NOT_FOUND` without L1. Bound-local
hits win without L1; bound failures surface typed errors and **never** silently fall through to L1.
Non-UR + opt-in ON → L1 direct (**no** fallback triplet — canonical `001` 1b path); non-UR + no L1
→ `UNSUPPORTED_NETWORK`; mainnet-bound → bound only.
- **Cross-network fallback provenance (003 SF-2).** When L1 miss-fallback succeeds after a **real
bound-empty miss** on a UR-carrying chain (forward or reverse), provenance carries the coordinated
base triplet: `resolvedViaNetworkFallback: true`, `queriedOnNetworkId` (bound `networkConfig.id`),
`resolvedOnNetworkId` (`ethereum-mainnet`). **`scopedToNetworkId` stays absent** on these hits
(002 D-R7 — global display on any row). The triplet is **omitted** on bound-local hits, mainnet-bound
hits, and canonical non-UR forward L1 (`001` SF-5 branch 1b). Classify with
`resolvedViaNetworkFallback === true` only — never infer from absent `scopedToNetworkId`, `label`, or
`external` (Principle II). UIKit copy uses `@openzeppelin/ui-utils` `isCrossNetworkFallback`.
- **Reverse provenance scope (chain-agnostic gate).** Distinguish global vs network-local using base
`scopedToNetworkId` **only** — absent ⇒ global / mainnet identity (show on any row); present ⇒
network-local (hide on rows scoped to a different network). **Never** use `isEnsProvenance` or
`coinType` as the display-safety gate (Principle II). L1 reverse hits may carry `EnsProvenance`
(`coinType: 60`) as adapter-internal enrichment; Sepolia-local hits carry `scopedToNetworkId` without
`EnsProvenance`.
`coinType` as the display-safety gate (Principle II). L1 miss-fallback hits may carry `EnsProvenance`
(`coinType: 60`) as adapter-internal enrichment; the **fallback disclaimer** uses the triplet, not
`EnsProvenance`. Sepolia-local hits carry `scopedToNetworkId` without `EnsProvenance`.
- **Avatar is best-effort and isolated.** The reverse call fetches the avatar in a separate,
failure- and latency-isolated step. A slow, failing, or missing avatar can only omit
`avatarUrl` — it never delays past the reverse read's error surface and never fails the result.
Expand Down Expand Up @@ -248,14 +261,18 @@ registration, the consumer resolve loop, and common mistakes. Runnable examples
- **Forward provenance carries no avatar.** `EnsProvenance` (`system`, `coinType`,
`scopedToNetworkId`, `external`) describes how the *address* was resolved, not display metadata.
Avatars are reverse-only (`avatarUrl` on `ResolvedName`).
- **ENS resolution starts on L1 (forward); reverse may miss-fallback to L1.** The forward path uses
the bound client when the bound chain carries an ENS Universal Resolver (mainnet-bound, `coinType` 60);
otherwise, **iff** the runtime wired an `ensL1Client`, it resolves the name chain-scoped on L1
(`coinType = toCoinType(boundChainId)`) and stamps `scopedToNetworkId`. A bound network with no
Universal Resolver **and** no `ensL1Client` returns `UNSUPPORTED_NETWORK` **before any I/O** — there
is never a silent cross-chain fallback. The reverse path (`resolveAddress`) uses the same injected
`ensL1Client` under the Option B ladder (002 SF-1): bound-first on UR-carrying chains, L1 only after
definitive empty or when the bound chain has no UR.
- **ENS resolution starts on L1 (forward); reverse/forward may miss-fallback to L1 when opted in.**
The forward path uses the bound client when the bound chain carries an ENS Universal Resolver
(mainnet-bound, `coinType` 60); otherwise, **iff** the runtime wired an `ensL1Client`, it resolves
the name chain-scoped on L1 (`coinType = toCoinType(boundChainId)`) — the canonical `001` 1b path,
**not** miss-fallback. UR-carrying bound forward miss-fallback to mainnet L1 runs **only** when
`enableMainnetL1MissFallback: true` (003 SF-4). A bound network with no Universal Resolver **and** no
`ensL1Client` returns `UNSUPPORTED_NETWORK` **before any I/O**. The reverse path uses the same
`ensL1Client` under the Option B ladder (003 SF-3), gated by the same opt-in.
- **Opt-in default OFF (fund-safety).** Wiring `ensL1Client` does **not** enable miss-fallback.
Runtime profiles must pass `enableMainnetL1MissFallback: true` explicitly to permit L1 consult after
bound-empty / bound `NAME_NOT_FOUND`. Transport/gateway/timeout failures remain terminal — opt-in
relaxes **where** we look after a clean miss, not error discipline.
- **Chain-scoped addresses must be bound to their network.** When `provenance.scopedToNetworkId` is
present on a **forward** result (`coinType !== 60`), the resolved address is meaningful **only** on
that network. On **reverse**, `scopedToNetworkId` marks a **bound-local primary name** (network-local
Expand Down
Loading
Loading