feat: add multi chain upgrade script - #136
Merged
Merged
Conversation
Changes to gas cost
🧾 Summary (20% most significant diffs)
Full diff report 👇
|
LCOV of commit
|
Replace the decommissioned protocol-api WMHolders/WMHoldersL2 resolvers with zero-indexer's per-(chain, account) wm_earner table (is_earning), resolved over Hasura by probing the wm_earner / indexer_wm_earner field names. - enrich the balance column on-chain via balanceOf: Alchemy for supported networks, verified public RPCs (citrea, mantra, 0g, fluent, moca, plume) for the rest so they get balances without an Alchemy key - auto-load .env at startup, with shell-exported values taking precedence - sort CSV rows by balance descending (cosmetic; every consumer re-sorts by address)
Make earners/*.csv the single source of truth for the wM v1->v2 migration earner set and derive the on-chain list from it. - get-earners.ts: de-duplicate accounts (wm_earner can return an account in multiple rows) and write a header-only CSV for zero-earner networks. - generate-earners-array.ts: read earners/*.csv instead of protocol-api, emit a get<Network>Earners() per network (empty array when no earners) and a getEarners(chainId) dispatcher mapping each chain id to its function. - EarnersAddresses.sol: regenerated for all 16 zero-indexer networks. - DeployUpgradeMainnet: resolve earners via the generated dispatcher. - package.json: add generate-earners-array script; prettier now covers script/.
- get-earners.ts: de-duplicate accounts by address (wm_earner can return an account in multiple rows, which would break the strictly-ascending invariant), and add monad (chain 143) now that zero-indexer covers it. - generate-earners-array.ts: mirror the monad chain id in the dispatcher map and drop the stale optimism entry. - EarnersAddresses.sol: regenerated for the current 16-network CSV set.
…ication - DeployConfig.sol: per-chain config (governance roles, migration admin, excess destination) keyed by chain id; reverts for unconfigured chains. - DeployUpgrade.s.sol: generic across chains — shared constants, earners from the EarnersAddresses dispatcher, migrator addresses derived from the deployer's current nonce. Replaces DeployUpgradeMainnet.s.sol; no deployer/nonce pinning since the migration admin performs the migration via migrate(migrator). - Makefile: per-network deploy-upgrade-* targets passing the right verifier per chain (etherscan vs blockscout); drop invalid --show-standard-json-input. - foundry.toml: Etherscan V2 endpoints and per-network rpc/verifier config. chore(earners): remove zero-indexer issue notes from the repo
Adds `npm run get-holders`, the holder-side counterpart to `get-earners`: writes `holders/<network>.csv` (`address,balance`) for all 16 wM networks. zero-indexer has no wM balance reducer (`indexer.holder` covers only stablecoin-type contracts), so the candidate set is the DISTINCT participants of wM's Transfer dyn tables — the same derivation zero-indexer's own `wm-earner-seed.ts` uses. Both wM types are probed (`dyn_wrapped_m_token_transfer` for L2s, `dyn_stateful_wrapped_m_token_transfer` for Ethereum), across Hasura's two naming conventions and the `from`/`to` vs `sender`/`recipient` column pairs, since neither is guaranteed. Transfer participants include addresses that have since gone to zero, so candidates are enriched with on-chain `balanceOf` and filtered to > 0. An unreadable balance is kept with an empty column rather than dropped, so a missing RPC degrades to a candidate list instead of an empty holder set that would be indistinguishable from a chain with no holders. Extracts the chain tables, RPC routing, GraphQL client and CSV formatting shared by both scripts into `script/wm-common.ts` — the network list is load-bearing and must change in exactly one place. Also destroys the JsonRpcProvider after use: an unreachable RPC left ethers retrying network detection on a timer forever, keeping the event loop alive so the script never exited even once every CSV had been written. This affected get-earners too; it rarely surfaced because most earner sets are empty.
PierrickGT
force-pushed
the
proto-976-prepare-wrappedm-v2-migration
branch
from
July 21, 2026 09:31
2f446ca to
e32f010
Compare
Sepolia is the testnet for rehearsing the wM v2 upgrade, so it needs an earner set. zero-indexer doesn't index Sepolia (it explicitly skips it), so `wm_earner` has no rows there and the usual Hasura path would emit an empty CSV — silently dropping the accounts actually earning on-chain. Adds an on-chain fallback: for networks in ONCHAIN_EARNER_NETWORKS (currently just sepolia), `get-earners` derives the earner set directly from the wM contract's StartedEarning / StoppedEarning logs — the same events zero-indexer's reducer consumes — replayed in (block, logIndex) order, keeping the accounts left in the earning state. Balances are still enriched via `balanceOf`. A missing RPC is a hard error here: unlike balances (which degrade to empty), the address set is the whole point of the export. Wires sepolia (chain 11155111, eth-sepolia) into the shared CHAIN_IDS / NETWORKS / ALCHEMY_NETWORKS, the generator's CHAIN_IDS, and regenerates EarnersAddresses.sol so the upgrade's getEarners(11155111) dispatcher resolves its 4 earners. Note: performing the upgrade on Sepolia additionally needs a DeployConfig.get(11155111) branch with its governance addresses, which reverts UnsupportedChainId until added.
Adds `npm run verify-earners -- <network> <before|after|compare>` for the
chains the upgrade touches (sepolia, base, arbitrum, ethereum). `before` and
`after` snapshot each earner's on-chain state at one pinned block; `compare`
diffs them and exits non-zero on any violation, so it can gate the upgrade.
The earner set comes from the StartedEarning / StoppedEarning replay, which a
cross-check against the indexer showed reproduces its set exactly on ethereum
and arbitrum. A snapshot also diffs that set against the committed
`earners/<network>.csv`, so a stale migration list surfaces before the upgrade.
What is compared, and why:
- balanceOf is the balance invariant. It returns the STORED balance; yield
accrues into `accruedYieldOf`, not into it, so it does not drift block to
block. `balanceWithYieldOf` does drift with the index and is deliberately
not compared, to avoid false diffs from ordinary yield accrual.
- isEarning must survive unchanged. Both it and balanceOf are readable on v1
and v2, so they diff cleanly across the version boundary.
- earningPrincipal cannot be diffed: it is v2-only and reverts on v1. It is
instead predicted. The migrator derives it as
getPrincipalAmountRoundedDown(balance, lastIndex) from v1 state, so the
before snapshot reads v1's `lastIndex` out of storage slot 6 and records the
exact value v2 must end up holding. Validated against live v1 state on
ethereum, arbitrum and sepolia: the principal derived that way reproduces
the contract's own `accruedYieldOf` to the digit.
A null principal after the upgrade means the function still reverts, i.e. the
proxy was likely never upgraded. Where no prediction exists (two v2 snapshots)
it falls back to flagging a zero principal only where the balance implies a
non-zero one — zero is CORRECT for an earner holding nothing, and 5 of the 25
ethereum earners sit at zero, so a naive non-zero rule would have reported five
false positives on the first mainnet run.
Extracts the earner event-replay into `wm-common.fetchOnChainEarners` (with a
`toBlock` argument for pinned snapshots) so get-earners and the verifier share
one implementation. Snapshots are gitignored: they are regenerated at upgrade
time, not source.
Wires Nexus (chain 3946) into the earner/holder tooling — CHAIN_IDS, NETWORKS, its public RPC, and the generated getNexusEarners() dispatcher — and adds an on-chain holder path for the chains zero-indexer doesn't cover. get-holders previously sourced candidates only from the indexer's Transfer dyn tables, so a non-indexed chain (sepolia, nexus) got an empty file despite real holders. It now derives candidates for NON_INDEXED_NETWORKS from the wM Transfer logs on-chain, mirroring the earner scan; the log-scan helper is generalized to take topics and shared between both. The "not indexed" set is hoisted to wm-common so get-earners and get-holders agree on it. Fixes a silent-wrong-data bug on Nexus: its public RPC mishandles ethers' JSON-RPC request batching, returning EMPTY results for batched eth_getLogs rather than erroring — a batched full scan returns 0 logs where an unbatched one returns the real 4. Providers are now created through a wmProvider() helper that disables batching for NO_BATCH_NETWORKS (nexus); Alchemy chains keep batching. Nexus result cross-checked against Blockscout (independent of the RPC): 1 holder (0x77bab32f…, 499999) and 0 earners (only EarningEnabled, no per-account StartedEarning). Its earner CSV is correctly header-only. Also adds the sepolia holder export produced by the same on-chain path.
toninorair
approved these changes
Jul 29, 2026
Deploy the v2 implementation + migrator and post `migrate(migrator)` to the Safe read from the proxy's own `migrationAdmin()`, via the Safe Transaction Service. The batch is simulated against a prank of the Safe first, so a broken call fails locally instead of reaching the signers. `MultiSigBatchBase` comes from `common` rather than a vendored copy, so safe-utils resolves through `common`'s own submodule — hence the pinned remappings. `ffi = true` is required: safe-utils shells out to post the proposal. Narrow the upgrade targets to the chains actually being upgraded. Base, Arbitrum and Ethereum go through the Safe proposal; Sepolia keeps the deploy-only `DeployUpgrade` path because its migration admin is an EOA. Plasma and the other deprecated chains lose their targets entirely.
PierrickGT
marked this pull request as ready for review
July 30, 2026 07:59
The Safe's on-chain nonce only advances on execution, so proposing at `nonce()` collides with anything already queued and unexecuted — the two proposals become mutually exclusive rather than sequential. Read `SAFE_NONCE` to queue behind them instead, and log the nonce actually used. Keep this copy in `script/` rather than reusing `common`'s: the `nonce` argument on `proposeTransactionsWithSignature` landed in safe-utils v0.0.22 and `common` pins v0.0.19, so safe-utils returns as a top-level submodule with the remappings pointing back at it. Also fund the pranked Safe in `_simulateBatch`, since `isolate` mode runs each top-level call as its own transaction and makes the Safe pay for gas.
…trum Fill in the three chains the v2 upgrade runs on, from the upgrade plan's "Roles and Addresses" table. The table is not per-chain, so all three share one branch: the MXON Safe `0xf7298F04…` as `migrationAdmin` + `admin`, `0x235D1149…` as `excessDestination` + `excessManager`, and MXON `0x4F1cf244…` as `freezeManager`, `pauser` and `forcedTransferManager`. This replaces the mainnet placeholders carried over from the original script (`0x4311697…`, the Vault, and `0xF2f1ACbe…` on every role) — none of which matched the agreed set. Sepolia keeps its admin EOA, as the rehearsal chain. Two things the struct cannot express, both needing a follow-up: - The table lists a second freeze manager, Predicate `0x363c256D368277BBFaf6EaF65beE123a7AdbA464`. The migrator grants exactly one, so `admin` must call `grantRole(FREEZE_MANAGER_ROLE, ...)` for it after the migration. - `0xf7298F04…` is a Safe v1.4.1 with threshold 1 of 2 on all three chains, and it holds the immutable `migrationAdmin` alongside `DEFAULT_ADMIN_ROLE`, which can grant and revoke every other role.
`0x87F220C2c8026fb45AC3c2834599670A5d2F4872` and `0xAA818CE02375e4E65e76E59b968AAe168bB14c41` started earning since the last export, taking Ethereum from 25 to 27. Balances refreshed on both Ethereum and Arbitrum; the balance column is cosmetic (every consumer re-sorts by address), only the address set feeds the migration. The list is verified against full on-chain history rather than the indexer alone: 27 `StartedEarning` events, 27 distinct accounts, zero `StoppedEarning` ever, `isEarning == true` for all 27, and their balances sum exactly to `totalEarningSupply`. All 286 addresses in holders/ethereum.csv were scanned for earners outside that set — none. DO NOT regenerate this list from the indexer without re-verifying. zero-indexer under-reports Ethereum by four earners: 0x569D7dccBF6923350521ecBC28A555A500c4f0Ec 0x9F6d1a62bf268Aa05a1218CFc89C69833D2d2a70 0xB50A1f651A5ACb2679c8f679D782c728f3702E53 0xD48e565561416dE59DA1050ED70b8d75e8eF28f9 They have no `wm_earner` row at all, so `get-earners` silently drops them. The seed's candidate set is the DISTINCT participants of the Transfer table, and these four have never sent or received wM, so the seed cannot see them; their `StartedEarning` events (blocks 21.1M-24.5M) predate the reducer, so it never saw them either. Each holds a zero balance, which is why they are invisible to the supply arithmetic — but zero balance is not the same as not earning, and omitting them from the migration would silently strip their earning status. Arbitrum's upgrade already showed zero-balance earners migrate correctly, with a derived principal of 0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.