refactor: chain config and registry as git-tracked data with API sync, drift check, and chain doctor#5
Conversation
|
👋 aelmanaa, thanks for creating this pull request! To help reviewers, please consider creating future PRs as drafts first. This allows you to self-review and make any final changes before notifying the team. Once you're ready, you can mark it as "Ready for review" to request feedback. Thanks! |
5a20041 to
da5f128
Compare
da5f128 to
521ecda
Compare
There was a problem hiding this comment.
We're already writing/storing deployed addresses in script/deployments folder which is gitignored, so can't we utilize that itself, instead of duplicating?
There was a problem hiding this comment.
Good catch, it really was a double write. Each deploy script recorded the address twice on adjacent lines, once through DeploymentUtils.save* and again through the registry record. I've now folded both into a single DeploymentRecorder call, so it writes the ledger file (in your save* format, which stays untouched) and the registry entry together, and the two can no longer drift apart.
I did keep the registry as its own store rather than reading straight from script/deployments/, mainly because those files can't really act as the resolution source. Nothing reads them back since they're write only, they're gitignored so a fresh clone or a CI run starts with none of them, and they're keyed by chain name instead of the chainId that resolution relies on, with no marker for which entry is the live one. Both resolution and the redeploy guard need a single current state entry they can read, upsert, and delete, which append only timestamped files just can't give us.
…doctor, and a single-writer address registry
Chain metadata lives in git-tracked config/chains/<name>.json, read by
HelperConfig via ChainConfig.sol; the ccip{} address block is synced from
the CCIP REST API v2 with a field-by-field drift check and a layered
chain doctor.
The config file basename and lowercase `name` are the canonical CCIP
selectorName from the chain-selectors registry (the same key the REST API,
CLD, Atlas, the directory URL leaf, and ccip-cli all share), e.g.
ethereum-testnet-sepolia rather than the bespoke ethereum-sepolia. The sync
still joins on the numeric chainSelector (the immutable machine key), and now
also validates that the config name equals the API selectorName. Unlike the
chainId guard this works for non-EVM chains too, whose config carries a
placeholder chainId "0" the chainId check cannot verify — closing that gap
at add-chain time and in the doctor.
Unify the deployed-address write path and re-key the registry (schema v2).
Every deploy script previously wrote the same address twice, through two
adjacent, independently-maintained calls: DeploymentUtils.save* (the detailed
timestamped ledger) and RegistryWriter.record (the resolution registry). Two
stores, two formats, nothing keeping them in sync. Collapse them into a single
writer, script/utils/DeploymentRecorder.s.sol: one call per artifact emits the
ledger file (format byte-for-byte unchanged) AND upserts the registry, so the
two can no longer drift.
Reshape addresses/<chainId>.json to schema v2: an `active` map of per-role
pointers (token/tokenPool/lockBox/poolHooks) that HelperConfig resolves with
zero export, plus a `deployments` map of uniquely-named, versioned entries
(e.g. BnM-T_BurnMintTokenPool_2.0.0). The redeploy guard keys on the unique
deployments name, so a BurnMint and a LockRelease pool for one token — or an
old and a new pool version — coexist without collision. This is required by
the pool-migration flow, where the old and new pool must both be resolvable
across the setPool cutover. RegistryWriter.read resolves active.<role> with a
legacy fallback to the pre-v2 flat top-level key, and never reverts on a
concurrently-written (empty/partial) file. After each pool deploy, the script
asserts the on-chain typeAndVersion matches the version composed into the key.
Wire the previously dead registry read path: HelperConfig now resolves lockBox
and poolHooks through the same inline > {CHAIN}_ env > registry ladder as
token/tokenPool (getDeployedLockBox / getDeployedPoolHooks), so all four
artifacts resolve with no manual export. The allowlist, authorized-callers,
pool-deploy, and lockbox-operation scripts fall back to the getters; the
lockbox operations still revert with a clear message when nothing resolves.
Add a doctor rung reconciling the registry's active.tokenPool against the pool
wired in the on-chain TokenAdminRegistry: active is what this repo last
deployed, the TAR is what CCIP routes through, and they legitimately diverge
mid-migration — so divergence is reported as a WARN, never a FAIL.
The addresses/ registry is gitignored and local to the deploy machine; its
integrity comes from the single-writer recorder, not from git history (unlike
the git-tracked config/chains/*.json). Docs and the schema example are updated
to schema v2, the per-artifact keying table, the one-writer model, and the
active-vs-wired distinction.
521ecda to
133f056
Compare
What improves for the user
Before: chain metadata lives in hardcoded Solidity (
HelperConfig), deployed addresses are manualexportenv vars, and adding a chain means editing source code. Keeping router/registry addresses current requires hand-editing constants with no drift detection.After: chain config is git-tracked JSON under
config/chains/, synced from the live CCIP API; deployed token/pool/lockbox addresses are recorded automatically inaddresses/<chainId>.jsonand resolved by scripts without manual exports (env vars remain as overrides). Adding a chain is a config edit, not a code change —make discover→make add-chain→make doctor. The visible README command syntax is unchanged; the operational model is auditable, API-backed, and drift-aware.What this PR does
config/chains/*.json— git-tracked per-chain CCIP metadata (router, registry, RMN, fee tokens, selectors, family). The file basename and lowercasenameare the canonical CCIPselectorNamefrom the chain-selectors registry (the same key the REST API, CLD, Atlas, the directory URL leaf, and ccip-cli share), e.g.ethereum-testnet-sepoliarather than a bespokeethereum-sepolia. Six chains committed as examples; new chains are discovered automatically by scanning the directory.src/config/ChainConfig.sol+CcipApiSource.sol+IConfigSource.sol— typed config loading, API fetch/transform, and identity cross-checks. The sync joins on the numericchainSelector(immutable machine key) and additionally validates that the confignameequals the APIselectorName. Unlike the chainId guard, the name check also covers non-EVM chains (whose config carries a placeholderchainId"0"the chainId check cannot verify) — closing that gap atadd-chaintime and in the doctor.script/HelperConfig.s.solrewrite — dynamic chain discovery fromconfig/chains/instead of hardcoded per-chain getters; name/selector resolution now data-driven.addresses/<chainId>.jsonregistry —src/utils/RegistryWriter.solrecords deploy outputs; deploy scripts (DeployToken,DeployBurnMintTokenPool,DeployLockReleaseTokenPool,DeployERC20LockBox,DeployAdvancedPoolHooks) write addresses automatically. Subsequent scripts resolve from registry; env vars are overrides only.script/config/SyncCcipConfig.s.sol+VerifyChain.s.sol— forge scripts behind sync (API → JSON) and chain doctor (layered verification: schema, identity, drift, RPC, on-chain code, registry warnings).Makefilegolden path —discover,add-chain,sync,sync-preview,sync-all,sync-check,doctor,fmt-configwrapping the underlyingforge script/ bash commands documented in README..github/workflows/config-drift.yml— scheduled (weekly) +workflow_dispatchdrift check viascript/config/sync-check.sh. Not a PR gate — surfaces upstream CCIP address rotations; exit contract 0 clean / 1 drift / 2 API unreachable (flake → warn-and-pass)..env.example— newdocs/config-architecture.md(how config, sync, registry, and doctor fit together) anddocs/config-schema.md(per-field schema reference), plus the README prerequisites table, "Adding a New Chain" guide, "Configuration and Address Data" section, and the Which command when table below.Which command when
make discover FILTER=<term>make add-chain CHAIN=<name> SELECTOR=<sel>, thenmake doctor CHAIN=<name>make sync-check(CI/automation:bash script/config/sync-check.shfor the 0/1/2 exit codes)make sync-preview CHAIN=<name>make sync CHAIN=<name>/make sync-allmake doctor CHAIN=<name>forge scriptrunmake fmt-configdoctorandsync-checklayer rather than overlap:doctoris the deep single-chain health check for a human;sync-checkis the fleet-wide drift verdict for routine use and CI.Test coverage
New suite under
test/config/(27 tests):HelperConfigGoldenData.t.sol(7) — config loading matches committed JSONSyncFixtureTransform.t.sol(5) — API response → config JSON transformSyncToolingRules.t.sol(5) — sync script invariantsRegistryResolution.t.sol(1) +RegistryGuard.t.sol(7) — address registry read/write/guard rulesDynamicChainDiscovery.t.sol(2) — new chains discovered fromconfig/chains/without Solidity editsAcceptance checklist
make discover,make add-chain,make doctorgolden path documented and exercisedaddresses/<chainId>.jsonHelperConfigresolves chains dynamically fromconfig/chains/name/basename equals the CCIP APIselectorName(validated for EVM and non-EVM chains)make sync-checkexit contract: 0 clean / 1 drift / 2 API unreachablejq --indent 2 -S .)forge build,forge fmt --check,forge lint,forge testgreenTest plan
forge buildforge fmt --checkforge lintforge testmake sync-check(local)make doctor CHAIN=ethereum-testnet-sepolia(local)Out of scope: scripted lane-wiring golden path for
ApplyChainUpdates(follow-up); action-layer rollout to configure/hooks/operations scripts (separate PR).