Skip to content

chore: review-only diff of the Paseo v1.0.0 upgrade (do not merge) - #332

Closed
re-gius wants to merge 38 commits into
masterfrom
dev/testnet-upgrades
Closed

re-gius wants to merge 38 commits into
masterfrom
dev/testnet-upgrades

Conversation

@re-gius

@re-gius re-gius commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

Description

Review only. Never merged. Keep open until the upgrade is declared and verified: this PR is the broadcast console.

Upgrades Paseo Asset Hub Next in place from v0.8.0 to v1.0.0. Adding a run:<Script> label triggers one gated step, which pauses on the testnet-upgrades environment until a reviewer approves it, then comments the outcome and removes its label. docs/PASEO-V100-RUNBOOK.md is the order and the operating manual. Campaign content was reviewed in #331.

# Label Upgrades
1 run:UpgradeRegistry DotnsRegistry
2 run:UpgradePopRules PopRules
3 run:UpgradePopResolverAndController DotnsPopResolver, then DotnsPopController
4 run:RedeployPopLens DotnsPopLens at 0x309C5ff21f9082A53211500c6f33cA2a21024Ae4; then commit the manifest artifact
5 run:DeclareRelease codehashes, then 1.0.0

Shipped contracts differ from master only by the branch's layout fixes from the v0.8.0 campaign, unchanged here: gaps sized to the live proxies (DotnsRegistrar, DotnsPopController, DotnsProtocolRegistry, UserStore), the retained whitelist slot in DotnsRegistrarController, and the retained AccessControl namespace in DotnsRegistrarController and DotnsNameWhitelist. Everything else is upgrade material: snapshots of the deployed code (from upgrades/paseo-nv2-v0.8.0), five step scripts, fork tests, the console and the runbook.

Type

  • Bug fix
  • Feature
  • Breaking change
  • Documentation
  • Chore
  • Refactor
  • Security

Scope

  • Registration
  • Resolver
  • Store
  • Proof of Personhood
  • Deployment scripts
  • Tests

Related Issues

Follows #312 (v0.8.0). Content merged via #331.

Fixes

Checklist

Code

  • Follows project style
  • forge build passes
  • forge test passes
  • No new compiler warnings

Testing

  • New tests added for changed behavior
  • Fuzz tests added where applicable
  • Invariant tests verified

Security

  • No new selfdestruct or delegatecall
  • Access control reviewed
  • No storage layout conflicts (for upgradeable contracts)

Documentation

  • NatSpec updated on changed interfaces
  • README updated if needed

Breaking Changes

  • No breaking changes
  • Breaking changes documented below

Breaking changes:

How to test

RPC_URL=https://eth-rpc-paseo-next.polkadot.io scripts/shell/verify-snapshots.sh
PASEO_FORK_RPC=https://eth-rpc-paseo-next.polkadot.io forge test --match-path 'test/fork/**

Notes

From step 1 on, CI's full verify-snapshots here fails by design; the console checks only each step's own contracts. Labelled deployment-record because the step 4 manifest commit lands on this branch.

sphamjoli and others added 30 commits September 9, 2026 00:55
Brings the numeric-namespace change onto the upgrade-tooling branch and adds the
PR-scoped upgrade scripts, pinned pre-upgrade snapshots, and fork tests that swap
the registry, PoP controller, PoP rules, and reverse resolver proxies in place
and redeploy the lens, so every address is kept. The fork suite issues a lite
username against live state to prove it lands as a subname beneath its numeric
container.
Brings the generic SubnodeUtils, the unified lite-node derivation, the registry
write returning the subnode, and the lens store-row and cold-path fixes onto the
in-place upgrade branch, so the swapped implementations carry them.
Brings the clarified `persist` documentation and the registry store-write tests
onto the in-place upgrade branch so it stays in step with the feature branch.
## Description

Upgrades the deployed proxies in place so the numeric-namespace change
reaches live state while
every address is kept. It swaps the registry, PoP controller, PoP rules,
and reverse resolver
implementations behind their existing proxies and redeploys the
immutable lens, repointing the
`popLens` key. Each proxy swap resolves its target from the on-disk
manifest and diffs the new
storage layout against a pinned pre-upgrade snapshot, failing closed if
a slot moves, shrinks, or
changes type.

Fork tests run each script against live Paseo Asset Hub state through
the ETH-RPC adapter. One test
per upgrade proves the swap keeps the address, the owner, and the state;
an end-to-end test issues a
lite username and checks it lands as a subname beneath its numeric
container, owned in the registry
rather than held as a token.

Every upgrade script, pre-upgrade snapshot, and fork test in this change
is scoped to this pull
request and removed before merge, so the default branch keeps no upgrade
scaffolding and the fork
suite runs zero tests between upgrades.

This branch is based on the upgrade-tooling branch and carries the
numeric-namespace contract change
so the swapped implementations contain it.

Migration note: lite usernames issued before this upgrade were recorded
as atomic labels under the
top level. The upgraded code addresses a lite name at the
container-then-stem node, so a pre-upgrade
lite name is not read by the new path and its subname node can be issued
afresh. That overwrite is
accepted.

## Type

- [ ] Bug fix
- [x] Feature
- [x] Breaking change
- [ ] Documentation
- [ ] Chore
- [ ] Refactor
- [ ] Security

## Scope

- [x] Registration
- [x] Resolver
- [ ] Store
- [x] Proof of Personhood
- [x] Deployment scripts
- [x] Tests

## Related Issues

- #287

## Fixes

## Checklist

### Code

- [x] Follows project style
- [x] `forge build` passes
- [x] `forge test` passes
- [x] No new compiler warnings

### Testing

- [x] New tests added for changed behavior
- [ ] Fuzz tests added where applicable
- [ ] Invariant tests verified

### Security

- [x] No new `selfdestruct` or `delegatecall`
- [x] Access control reviewed
- [x] No storage layout conflicts (for upgradeable contracts)

### Documentation

- [x] NatSpec updated on changed interfaces
- [ ] README updated if needed

### Breaking Changes

- [ ] No breaking changes
- [x] Breaking changes documented below

**Breaking changes:**

- Lite usernames issued before this upgrade are addressed at a different
node afterwards and are not
migrated. Their subname node can be issued afresh, so a pre-upgrade lite
name is not preserved.

## How to test

Fork tests need the local ETH-RPC adapter on `paseo_local`.

```bash
bun run test:fork
```

## Notes

Upgrade scripts run through `scripts/deploy/upgrade.sh`, for example
`SCRIPT=UpgradeRegistry ACCOUNT_NAME=<keystore> RPC_URL=<network>
./scripts/deploy/upgrade.sh`.
Brings the dev branch up to tag v0.8.0 (3c8a9e9), so the upgrade artefacts sit
on top of the released code instead of a fork point 43 commits behind it. This
is step 1 of the Paseo in-place upgrade: #301 (writeNewLabel), #304 (protocol
version and codehash declarations), #298 (UUPS StoreFactory) and #305 (deferred
store-write gate) all arrive before any upgrade script is written against them.

The textual merge was conflict-free, but it left one semantic conflict:
DotnsRegistryOld calls StoreUtils.writeLabel, which #301 removed. The snapshot
has to keep reproducing the implementation deployed on chain, so it cannot move
to writeNewLabel. Resolved by adding contracts/utils/StoreUtilsOld.sol, a
verbatim PR-scoped snapshot of the library as deployed, and pointing the Old
registry at it. It is deleted along with every other *Old.sol before the upgrade
branch merges to master.

Outside *Old.sol, the contract diff against master is the four storage-layout
fixes this branch carries and nothing else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Description

Merges master at v0.8.0 (3c8a9e9) into the upgrade branch

## Type

- [ ] Bug fix
- [ ] Feature
- [ ] Breaking change
- [ ] Documentation
- [x] Chore
- [ ] Refactor
- [ ] Security

## Scope

- [ ] Registration
- [ ] Resolver
- [x] Store
- [ ] Proof of Personhood
- [x] Deployment scripts
- [ ] Tests

## Related Issues

## Fixes

## Checklist

### Code

- [x] Follows project style
- [x] `forge build` passes
- [x] `forge test` passes
- [x] No new compiler warnings

### Testing

- [ ] New tests added for changed behavior
- [ ] Fuzz tests added where applicable
- [ ] Invariant tests verified

### Security

- [x] No new `selfdestruct` or `delegatecall`
- [x] Access control reviewed
- [x] No storage layout conflicts (for upgradeable contracts)

### Documentation

- [x] NatSpec updated on changed interfaces
- [x] README updated if needed

### Breaking Changes

- [x] No breaking changes
- [ ] Breaking changes documented below

**Breaking changes:**

## How to test

```bash
forge test --mt <testName>
```

## Notes
The `*Old.sol` snapshots on this branch described implementations that were
replaced in place months ago. All four built to different bytecode from what
Paseo Asset Hub Next is running, and nothing noticed: the OpenZeppelin layout
diff compares the new implementation against whatever the snapshot happens to
be, so a stale reference produces an honest diff of the wrong pair, and a
change that lives in calldata leaves no trace in a layout at all.

Regenerate all of them from the build that is actually deployed (c852004),
and prove it. `scripts/shell/verify-snapshots.sh` builds each snapshot and
compares runtime bytecode against the implementation behind the proxy, masking
only `UUPSUpgradeable.__self` and the trailing CBOR metadata. All twelve
upgraded proxies match byte for byte. `fork-tests.sh` runs it before the suite,
so the check cannot be skipped by anyone running the tests the normal way.

The snapshot set is larger than the twelve contracts because master has moved
26 contract files since the deployed build. A snapshot that imports the current
tree stops reproducing the deployed bytecode, and one that imports a snapshot
hands a renamed type to a signature expecting the current one. The set is
therefore closed over both: everything reachable that changed, plus everything
that reaches one of those. Sixteen unchanged interfaces and libraries stay
shared, unrenamed.

CI now runs on pull requests into `spha/**`. It previously triggered on master
alone, so a PR into this branch got lint and nothing else, which is how the
stale snapshots reached review in the first place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the gap it caught

The storage-layout diff only ran inside an upgrade script, which meant it only
ran when someone brought up the ETH-RPC adapter and ran the fork suite. Lift it
into a plain unit test, one case per proxy, so every push answers the question
the upgrade actually turns on: would `upgradeProxy` accept this layout change.

It found one on its first run. `DotnsProtocolRegistry` appends
`_expectedCodehash` and `_protocolVersion` for the declarations added in #304,
but left `__gap` at 50, so the gap starts two slots later than on the deployed
proxy and the contract's footprint grows. The validator rejects it, and any
field appended after the gap in a later release would land on a slot the live
proxy does not expect. Sized to 48, which keeps the 54-slot footprint. This is
a fifth storage-layout correction of the same family as the four already on
this branch, and like those it stays here per the branch policy.

The fix moves no bytecode: a gap reserves slots and emits no code, so the
implementation still builds byte-identical to the v0.8.0 tag. The release-drift
finding is unchanged, and `DotnsRegistrarController` remains the only proxy
whose deployed code will differ from the tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ation

Thirteen scripts and thirteen fork tests, paired one to one. Twelve swap a
proxy in place; the thirteenth migrates the store factory, which cannot be
swapped because what is deployed is a plain contract from an earlier release
and the current one puts the factory behind a proxy at a different address.

Each fork test reads live state through the deployed implementation, runs the
script's own internal path rather than reimplementing the upgrade, and reads
the same state back. Each asserts the implementation address actually changed,
without which a swap that silently did nothing would satisfy every
state-preservation assertion. The assertions are per contract: the registry
drives the new deferral gate against the live controller set, which a unit test
cannot reach; the registrar controller reads back the protocol registry pointer
that the retained slot exists to keep in place; the escrow asserts its redeem
window is both unchanged and non-zero, that being the field an upgrade has
dropped before.

`StoreFactoryMigrator` closes the migration without a release. It is the
shipped factory with four fields widened and a one-shot owner-only import,
swapped into the proxy for a single transaction and swapped back out, so the
deployment ends on the shipped implementation with the bindings in place. Both
directions are layout-diffed.

The holder list it needs cannot be read from the factory: the mapping has no
enumeration, and the list it does keep holds store addresses, which do not know
which user they belong to. Only `LabelStoreDeployed` carries the pairing, so
`scripts/shell/store-holders.sh` replays it and reconciles the result against
the factory's own count. It refuses to print a list it cannot account for,
which matters because the public gateway answers log queries with an empty
result rather than an error: a naive replay against it looks like it worked and
is empty. The count also moves, from 57 to 58 while this was being written, so
the list is read immediately before broadcasting and never reused.

`DeclareRelease` extends `WireDeployments` rather than restating its key list,
so a release that adds a key cannot be declared on a fresh deploy and silently
skipped on every upgrade.

Docs carry the parts that are not obvious from the code. CONTRIBUTING now says
the artefacts stay on a branch that never merges, and that a snapshot is of the
deployed code rather than of the previous release. DEPLOYMENTS and a folder
README record why this network's chain id does not identify it, why its
gateway's empty log results are a trap, and why `verify --tag` will report one
key as drift permanently. The README no longer claims a lite name's node cannot
collide with a subname, which stopped being true when lite names became
subnames.

The note stayed out of the manifest deliberately: the deploy pipeline parses
every key there as an address, so a text field fails it on first read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four things an AI review caught, the first of which was mine and the worst.

The migration was built on a false premise. I claimed the deployed factory had
no enumeration and that stores did not know their user, and wrote a log-replay
helper, a count-reconciliation guard and two fork tests around that. Both
claims are false: `getLabelStores(0, n)` answers on the live factory, and
`IDotnsStore.owner()` on a live store round-trips back through
`getLabelStore`. `importStores` now takes only the old factory and reads the
count, the list and each store's owner on chain, checking every pairing back
through the mapping before it writes. `store-holders.sh` is deleted.

That also closes a race the old shape carried. The set was read off chain and
passed in, so a store created between the read and the import was silently left
unbound, and its holder would be handed an empty store on their next
registration. The count on this network moved while this branch was being
written. Reading inside the transaction removes the window, and a test covers
a binding created after the decision to migrate.

The script could not have been broadcast. It read the destination from the
manifest key `StoreFactory`, which still names the factory being migrated from,
and nothing deployed the replacement: the CREATE3 address for it is empty on
chain. Meanwhile the fork test quietly deployed the proxy itself, so the two
told different stories and the test passed. `MigrateStoreFactory` now deploys
the replacement through the pipeline's own helper, which also adopts one a
previous run left behind, and records it and its beacons in the manifest.

The success condition was never asserted. After a real migration every mint
resolves `STORE_FACTORY` through the protocol registry and calls
`ensureLabelStore`; nothing exercised that path. The fork test now upgrades the
protocol registry first, because the rewire declares a codehash that entrypoint
does not have yet, runs all four legs, and asserts a live holder resolves to
the store they already had with no extra store deployed.

Two smaller ones. `verify-snapshots.sh` ran only from `fork-tests.sh`, and CI
calls `forge test` directly, so the check this branch leans on was absent from
the one place reviewers see; it now runs in the fork job. `UserStore` added
`_protocolRegistry` without shrinking its gap, the same defect already fixed on
the protocol registry; it is harmless at zero user stores and would not stay
harmless through a beacon rotation.

`docs/PASEO-V080-RUNBOOK.md` writes down the broadcast order. Nothing in the
scripts enforces it and they are independently runnable by design, so the three
dependencies that fix the order live there: registry first because every
`version()` reads through it, the migration before the declaration because it
rewires a key the declaration records, and the declaration last because it
claims something about the whole deployment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d factory

Three findings from the second review, all correct.

The fork test could not have run the deploy leg it claims to exercise.
`_deployReplacement` reaches `_create3Factory()`, which falls back to reading
`DotnsProtocolRegistry` out of the manifest, and the manifest is only populated
by `initDeployment`, which `run` calls and the harness does not. The test would
have failed at CREATE3 resolution before reaching its assertion, and the unit
tests cannot see that. `_deployReplacement` now adopts the CREATE3 factory from
the protocol registry it was already handed, so the leg depends on no ambient
state and production and the test run the same path.

The script erased the only durable pointer to the factory it migrates away
from. The deploy reuses the `StoreFactory` label and the replacement mints its
own beacons, so after a successful run all three manifest entries described the
new deployment, while the imported stores stayed on the old beacons. Those
beacons answer to the old factory, and it is the only contract that can ever
rotate the implementation behind those stores. The outgoing addresses are now
written first, as `StoreFactoryLegacy`, `LabelStoreBeaconLegacy` and
`UserStoreBeaconLegacy`, and the runbook says which one a later rotation goes
through.

The resume claim was too strong. Adoption covers a run that died between the
deploy and the import, and nothing else: once the import has landed a second
run reverts on the first user it tries to bind, and if the proxy is still on
the migrator the adopt is refused, because the deployer requires the occupant
to delegate to the implementation that run deployed. The NatSpec said the step
was resumable; it now says what is actually true, and the runbook marks step 13
as the one step to inspect and continue rather than re-run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Description

Brings Paseo Asset Hub Next to the current release in place: 12 proxies
swapped, store factory migrated, release declared last. Only two shipped
contracts change (`DotnsProtocolRegistry`, new `StoreFactoryMigrator`);
the rest is upgrade tooling, snapshots and tests.

Two things were broken and passing every check:

- **All four `Old.sol` snapshots were stale**, describing
implementations replaced in place months ago, so every layout diff
compared the wrong pair. Nothing can catch that: the diff compares
whatever it is handed, and a calldata-only change leaves no trace in a
layout. Regenerated from `c8520046` (the build actually deployed) and
proved by `scripts/shell/verify-snapshots.sh`, which compares each
snapshot's bytecode against the implementation behind the proxy. All 12
match; `fork-tests.sh` runs it before the suite.
- **`DotnsProtocolRegistry` could not be upgraded onto the live proxy.**
#304 appended two fields but left `__gap` at 50, so the gap starts two
slots late and the validator rejects it. Now 48. Found by the new layout
suite, which moves the diff into a plain unit test so it runs on every
push. No bytecode change.

The store factory cannot be swapped: what is deployed is a plain
contract, and the release puts the factory behind a proxy elsewhere. Its
bindings are proxy storage with no export, so re-pointing
`STORE_FACTORY` at a fresh factory gives every existing holder a second
empty store and drops their labels from enumeration.
`StoreFactoryMigrator` avoids that without a release: the shipped
factory with four fields widened plus a one-shot owner-only import,
swapped in for one transaction and back out, both directions
layout-diffed.

## Type

- [ ] Bug fix
- [ ] Feature
- [ ] Breaking change
- [ ] Documentation
- [x] Chore
- [ ] Refactor
- [ ] Security

## Scope

- [ ] Registration
- [ ] Resolver
- [ ] Store
- [ ] Proof of Personhood
- [x] Deployment scripts
- [ ] Tests

## Related Issues

Follows #308 (master merged into this branch). Supersedes #306.

## Fixes

Fixes deployment upgrade to v0.8.0 on Paseo

## Checklist

### Code

- [ ] Follows project style
- [ ] `forge build` passes
- [ ] `forge test` passes
- [ ] No new compiler warnings

### Testing

- [ ] New tests added for changed behavior
- [ ] Fuzz tests added where applicable
- [ ] Invariant tests verified

### Security

- [ ] No new `selfdestruct` or `delegatecall`
- [ ] Access control reviewed
- [ ] No storage layout conflicts (for upgradeable contracts)

### Documentation

- [ ] NatSpec updated on changed interfaces
- [ ] README updated if needed

### Breaking Changes

- [ ] No breaking changes
- [ ] Breaking changes documented below

**Breaking changes:**

## How to test

```bash
forge test --match-path 'test/unit/upgrade/LayoutCompatibility.t.sol'
forge test --match-path 'test/unit/store/StoreFactoryMigrator.t.sol'
```

## Notes

**Permanent drift on `DotnsRegistrarController`.** Its retained slot
changes emitted slot constants, so the deployed code will differ from
the v0.8.0 tag forever. A tag build reads `protocolRegistry` as zero and
bricks the contract, so the branch build is the only deployable one.
`verify --tag` reports that one key as drift by design; a second one is
a real finding. Recorded in DEPLOYMENTS.md and
`deployments/paseo-assethub/README.md`.
  
**Holder list for the migration** cannot be read from the factory: no
enumeration, and the list it keeps holds stores, which do not know their
user. `scripts/shell/store-holders.sh` replays `LabelStoreDeployed` and
reconciles against the factory's count, refusing a list it cannot
account for. The public gateway returns empty for `eth_getLogs` instead
of erroring, so a naive replay looks like it worked. The count moved 57
to 58 mid-work, so read it immediately before broadcasting.
  
**Manifest deliberately untouched.** `initDeployment` parses every key
as an address, so a note field breaks the pipeline; it went in a sibling
README.
Prepares the rename of `spha/registrar-upgrade`. GitHub retargets open pull
requests and leaves a redirect, but it does not touch file contents, so these
four references would have gone stale silently.

The CI trigger is the one that matters. It matched `spha/**`, and a renamed
branch stops matching, which puts pull requests into it back to running lint
and nothing else. That is the gap that let a storage snapshot describing a
replaced implementation reach review in the first place. Now matched as
`dev/**`, by prefix rather than by name, so the next long-lived branch is
covered the day it is created instead of the day someone remembers this file.

The other three are prose: CONTRIBUTING on why the artefacts stay on a branch
that never merges, and DEPLOYMENTS and the manifest folder README on why this
network's deployed code matches no release tag.

Land this before the rename, not after. Nothing here touches `contracts/**`,
`test/**` or `**.sol`, so the test workflow's path filter skips this commit
under either branch name and no window opens either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
)

Follows the rename of `spha/registrar-upgrade` to
`dev/testnet-upgrades`.

GitHub retargets open pull requests and leaves a redirect, but it does
not touch file contents, so four references would have gone stale
silently.

The CI trigger is the one that matters. It matched `spha/**`, and the
renamed branch stops matching, which puts pull requests into it back to
running lint and nothing else. That is the gap that let a storage
snapshot describing a replaced implementation reach review in the first
place. Now matched as `dev/**`, by prefix rather than by name, so the
next long-lived branch is covered the day it is created.

The other three are prose: CONTRIBUTING on why the artefacts stay on a
branch that never merges, and DEPLOYMENTS plus the manifest folder
README on why this networks deployed code matches no release tag.

Nothing here touches `contracts/**`, `test/**` or `**.sol`, so the test
workflow path filter skips this commit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…uite

`DeclareRelease` would have aborted on Paseo Asset Hub Next, at step 14, after
every swap and the store migration had already been broadcast. The registry's
self-reference key resolves to the zero address there, and `_verifyDeployment`
asserts it matches the manifest. Sixteen of the seventeen keys are wired and
agree; that one is a hole a fresh deploy never has, left by a network wired
before the key existed.

`_wireMissingKeys` sets keys that are unset and leaves every other key exactly
as it is. A key pointing somewhere unexpected still fails verification, because
that is drift, and repairing it here would make the check that follows
tautological and hide the thing it exists to surface.

The fork suite has now been run, for the first time. All 15 tests pass against
live state, including the three for the store migration, which could not have
run at all before the CREATE3 resolution fix in the previous commit: they would
have died resolving the factory out of a manifest the harness never loaded.

They went unrun because they needed Docker. The adapter only translates for the
same node the hosted endpoint already fronts, so `PASEO_FORK_RPC` now overrides
the fork endpoint and the whole suite runs in under a minute with no container.
The default stays on the local alias, so CI and anyone mid-deployment are not
silently pointed at a public gateway. `fork-tests.sh` and the runbook carry the
invocation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The deployment key's custody includes a laptop belonging to someone who has
left, so it is treated as exposed. Rotation was queued as a follow-up for
after v0.8.0; an exposed owner key makes that ordering backwards, and it is
now the step everything else waits on.

Rotation, not redeployment, because ownership is a storage field: every
address, registry key, host destination, SDK pin and codehash declaration is
untouched, and the only thing that changes is which key the onlyOwner gates
answer to. `RotateOwnership.s.sol` transfers the fourteen owned contracts,
probed on chain rather than assumed: twelve proxies, the cost-model registry,
and the store factory, plus the outgoing factory once the migration has
recorded it. The lens, the pricing helper and Multicall3 expose no owner in
the deployed release, and the CREATE3 factory answers to a different key on
purpose.

The inventory is checked from both ends. Contracts that must be owned
hard-fail on any surprise, every transfer is verified by readback because the
one-step variant makes a wrong destination permanent, and the run ends by
scanning the whole manifest for anything owner-answering that the lists
missed, so a contract added by a later release stops the rotation instead of
surviving it. Re-running after a partial failure skips what already moved,
which is what lets the old key finish an interrupted run; a completed one
re-runs as a loud no-op.

The fork test proves the three facts the step exists for, against live state:
every contract answers to the fresh key at its unchanged address, the old key
can no longer drive an upgrade script, and the fresh key can. The script
parses the manifest it reads itself instead of the deployer's ambient field,
which is the lesson from the migration's unreachable deploy leg applied before
the test could find it again.

The runbook gains the step and its aftermath: swap the signer before step 1,
sweep the old account's gas, and delete both stored copies of the old key,
this repository's and the organisation's, which the deployment records show
control the same account.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…master

The dispatch button only exists for workflows on the default branch, and
nothing that signs with the owner key is going there. A pull_request workflow
is taken from the PR itself, so this file lives on the dev branch alone and the
open review PR into master becomes the broadcast console: add `run:<Script>`,
the run pauses on the `paseo-upgrade` environment until a reviewer approves,
one script broadcasts through the local ETH-RPC adapter, the record and the
manifest land as artifacts, the outcome is commented, and the label is removed
so re-adding it is the retry.

The run pins the PR's head commit at label time, not the merge ref: master
merged into this branch is not the code that was reviewed or fork-tested, and
pinning means a push after labelling cannot change what an approval executes.
Runs are serialised and never cancelled, because two broadcasts would race
nonces and a queued step must not be dropped by an eager second label.

Key handling is asymmetric on purpose. Step 0 signs with the repository-level
secret, because its value cannot be read and therefore cannot be moved into an
environment; that exposure is time-boxed by being exactly what step 0
eliminates. The fresh key is entered as an environment secret under the same
name, shadowing the repository one for every gated step after, and the
repository copy is then deleted. The snapshot check runs automatically for
step 0 only: one broadcast later the snapshots stop matching the chain by
design, so anything else would be a gate that pretends.

The manifest cannot be pushed back from the runner: this branch requires
signed commits, which a runner cannot produce, so step 13's manifest change is
committed by the operator from the artifact. The runbook carries the label
loop, the environment setup, and where the manual steps sit between labels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 0 exists because the deployment key must be treated as leaked: copies
have spread across sources the project does not control. How that came to be
adds nothing a reader of this repository needs, so the script header and the
runbook now say only what operationally matters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he network

One key owns every testnet deployment stamped from these releases, and the
environment protects the key, so `paseo-upgrade` was the wrong scope: a
previewnet campaign would either duplicate the secret into a second
environment or borrow a gate named after a different network. Renamed to
`testnet-upgrades`, matching the branch. A network with its own key gets its
own environment, mainnet above all.

Renamed before the environment exists, because the workflow string is what a
first label auto-creates when it finds no match, unprotected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The new owner was to be funded by faucet or teleport before step 0, and the
first attempt showed why that path deserves to die: the funds landed on the
sibling Asset Hub, where a mapped account has no revive behind it and
therefore no key that can ever spend it. Meanwhile the funding source was
sitting in plain sight, the old account holds 954 on the target chain, and the
step 0 run is the one signer that balance has, since the old key cannot be
extracted for a manual transfer.

So the rotation sweeps. After every owner has moved and been read back, the
old account sends its balance minus a two-unit gas buffer to the new owner.
One approved run now strips the leaked key of authority and of money together,
instead of leaving the money for a race with whoever else holds the key. The
buffer keeps a re-run able to pay its own fees; what it leaves behind is dust
on a powerless account.

The fork test asserts the transfer against the live balance, whatever it is on
the day: the property is that nearly all of it moves, not any particular
number. Runbook step 0 loses its funding instruction and gains the warning the
mistake earned: never send to a mapped address on a chain without revive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Committed as 644, which nothing noticed until the broadcast workflow invoked
it directly and died on permission denied, one step before the snapshot check
and several before anything could sign. `fork-tests.sh` never tripped on it
because a script sourced or run through bash does not consult its mode; the
one caller that executes it by path is the workflow, and now every script any
caller executes by path carries 755.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The adapter opens its RPC socket before it has subscribed to the node and
filled its block cache. In that window, calls that pass straight through
succeed, and anything touching the block index answers "Ethereum block not
found", which is exactly the split the first rotation attempt fell into: the
snapshot verification sailed through and the forge script died on its opening
block fetch. Nothing broadcast, verified on chain afterwards, which is the
fail-safe doing its job, but a readiness check that only proves the socket is
open is a readiness check for the wrong thing.

Ready now means the latest block resolves with a body and its number advances
while we watch, which proves the subscription is live and the cache filling.
Every caller shares the same script, so the fork suite gains the same patience.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Forty-odd sequential calls, each single-shot, so one hiccup anywhere failed
the whole check, and upstream of step 0 that means a wasted label-approve
cycle. Three attempts with a short backoff absorb the blips; a real outage
still fails, as it must.

The broadcast leg stays deliberately retry-free. Under --slow a failure leaves
a clean prefix on chain, every script re-runs into that state by design, and
the relabel puts the human approval back in the loop. An automatic retry
around a broadcaster trades that legibility for the chance of double-driving
partial state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
forge pins its fork to a block hash it fetched moments earlier, and with
--eth-pruning 1 the adapter forgets that hash as soon as the next block lands,
which on an advancing chain is immediately. The rotation run died at startup
on "failed to get block hash", before simulation, chain untouched: fourth
firing, fourth failure, still zero transactions, and this one was predicted as
plan B two failures ago.

A thousand blocks is minutes of history, ample for any broadcast and trivial
for a runner. The fork tests never tripped on the one-block window because a
test fork tolerates what a script pipeline pins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@re-gius re-gius added the run:UpgradeRegistry Broadcast UpgradeRegistry on Paseo (gated) label Oct 6, 2026
@re-gius
re-gius deployed to testnet-upgrades October 6, 2026 11:40 — with GitHub Actions Active
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

UpgradeRegistry: success. Run. Broadcast record and manifest attached as artifacts. Re-add run:UpgradeRegistry to retry.

@github-actions github-actions Bot removed the run:UpgradeRegistry Broadcast UpgradeRegistry on Paseo (gated) label Oct 6, 2026
@re-gius re-gius added the run:UpgradePopRules Broadcast UpgradePopRules on Paseo (gated) label Oct 6, 2026
@re-gius
re-gius deployed to testnet-upgrades October 6, 2026 11:49 — with GitHub Actions Active
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

UpgradePopRules: success. Run. Broadcast record and manifest attached as artifacts. Re-add run:UpgradePopRules to retry.

@github-actions github-actions Bot removed the run:UpgradePopRules Broadcast UpgradePopRules on Paseo (gated) label Oct 6, 2026
@re-gius re-gius added the run:UpgradePopResolverAndController Broadcast UpgradePopResolverAndController on Paseo (gated) label Oct 6, 2026
@re-gius
re-gius deployed to testnet-upgrades October 6, 2026 12:00 — with GitHub Actions Active
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

UpgradePopResolverAndController: success. Run. Broadcast record and manifest attached as artifacts. Re-add run:UpgradePopResolverAndController to retry.

@github-actions github-actions Bot removed the run:UpgradePopResolverAndController Broadcast UpgradePopResolverAndController on Paseo (gated) label Oct 6, 2026
@re-gius re-gius added the run:RedeployPopLens Broadcast RedeployPopLens on Paseo (gated) label Oct 6, 2026
@re-gius
re-gius deployed to testnet-upgrades October 6, 2026 12:10 — with GitHub Actions Active
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

RedeployPopLens: success. Run. Broadcast record and manifest attached as artifacts. Re-add run:RedeployPopLens to retry.

@github-actions github-actions Bot removed the run:RedeployPopLens Broadcast RedeployPopLens on Paseo (gated) label Oct 6, 2026
@re-gius re-gius added the run:DeclareRelease Broadcast DeclareRelease on Paseo (gated) label Oct 6, 2026
@re-gius
re-gius deployed to testnet-upgrades October 6, 2026 12:16 — with GitHub Actions Active
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

DeclareRelease: success. Run. Broadcast record and manifest attached as artifacts. Re-add run:DeclareRelease to retry.

@github-actions github-actions Bot removed the run:DeclareRelease Broadcast DeclareRelease on Paseo (gated) label Oct 6, 2026
@re-gius

re-gius commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

Closing without merging: the v1.0.0 upgrade of Paseo Asset Hub Next is complete.

# Step Run Result
1 UpgradeRegistry 37458010657 DotnsRegistry 0x0d4708a5… to 0x059bea24…
2 UpgradePopRules 37459001970 PopRules 0xef403ba3… to 0xdf1800ba…
3 UpgradePopResolverAndController 37460188525 DotnsPopResolver to 0xeb29adfd… (#1452672), DotnsPopController to 0xf8901566… (#1452676)
4 RedeployPopLens 37461441844 popLens to 0x309C5ff21f9082A53211500c6f33cA2a21024Ae4, manifest in 6e983db
5 DeclareRelease 37462133947 protocolVersion = 1.0.0

Each step was verified on chain before the next label: implementation moved and runs the branch build, owner unchanged, live records and pricing identical before and after. release-metadata.mjs verify --tag v1.0.0 reports paseo-assethub matches the chain.

The console workflow is disabled again and the campaign is tagged upgrades/paseo-nv2-v1.0.0.

@re-gius re-gius closed this Oct 6, 2026
github-actions Bot added a commit that referenced this pull request Oct 6, 2026
github-actions Bot added a commit that referenced this pull request Oct 6, 2026
re-gius added a commit that referenced this pull request Oct 6, 2026
## Description

Paseo Asset Hub Next was upgraded in place from v0.8.0 to v1.0.0 on
2026-10-06 (content #331, broadcast via #332). The four upgraded
contracts kept their proxies; the only address that moved is the PoP
lens, redeployed at a release-scoped CREATE3 salt.

| Key | Before | After |
|---|---|---|
| `DotnsPopLens` | `0xAE374b07c7e6f473CBa21d57e36AC15C631Abc51` |
`0x309C5ff21f9082A53211500c6f33cA2a21024Ae4` |

This brings master's manifest in line with the chain.

## Type

- [ ] Bug fix
- [ ] Feature
- [ ] Breaking change
- [ ] Documentation
- [x] Chore
- [ ] Refactor
- [ ] Security

## Scope

- [ ] Registration
- [ ] Resolver
- [ ] Store
- [ ] Proof of Personhood
- [x] Deployment scripts
- [ ] Tests

## Related Issues

See #331, #332

## Fixes

## Checklist

### Code

- [x] Follows project style
- [ ] `forge build` passes
- [ ] `forge test` passes
- [ ] No new compiler warnings

### Testing

- [ ] New tests added for changed behavior
- [ ] Fuzz tests added where applicable
- [ ] Invariant tests verified

### Security

- [x] No new `selfdestruct` or `delegatecall`
- [x] Access control reviewed
- [x] No storage layout conflicts (for upgradeable contracts)

### Documentation

- [ ] NatSpec updated on changed interfaces
- [ ] README updated if needed

### Breaking Changes

- [x] No breaking changes
- [ ] Breaking changes documented below

**Breaking changes:**

## How to test

```bash
node scripts/js/release-metadata.mjs verify --network paseo-assethub \
  --rpc https://eth-rpc-paseo-next.polkadot.io --tag v1.0.0
```

## Notes

This branch was successfully deployed

1 active deployment
testnet-upgrades — 6e983db7 Deployed Oct 6, 2026 by re-gius via run:DeclareRelease #26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants