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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ GROUP_DIR := $(if $(GROUP),$(GROUP)/,)
# still apply.
DEPLOY_VARS := TOKEN_NAME TOKEN_SYMBOL TOKEN_DECIMALS TOKEN_MAX_SUPPLY TOKEN_PRE_MINT \
TOKEN_PRE_MINT_RECIPIENT CCIP_ADMIN_ADDRESS ROLES_RECIPIENT TOKEN TOKEN_POOL LOCK_BOX DECIMALS \
POOL_HOOKS AUTHORIZED_CALLERS FORCE_REDEPLOY
POOL_HOOKS AUTHORIZED_CALLERS FORCE_REDEPLOY REANCHOR
$(foreach v,$(DEPLOY_VARS),$(if $(strip $($(v))),$(eval export $(v))))

# Preflight per-call inputs, forwarded to the forge script the same conditional way as DEPLOY_VARS
Expand Down Expand Up @@ -327,7 +327,7 @@ deploy-new-chain: tools ## Guided deploy: add-chain -> deploy-token -> deploy-po
# exit-remap note as sync-check: the 0/1/2 contract lives in `script/config/roles-check.sh`; CI calls
# the script directly, `make roles-check` is pass/fail only.

snapshot-chain: tools ## Backfill the declared roles{} authority block FROM chain (CHAIN= required; GROUP= scopes to one token group; opt: TOKEN= TOKEN_POOL= TAR= SCAN_FROM_BLOCK=)
snapshot-chain: tools ## Backfill the declared roles{} authority block FROM chain (CHAIN= required; GROUP= scopes to one token group; opt: TOKEN= TOKEN_POOL= TAR= SCAN_FROM_BLOCK= REANCHOR=true)
$(if $(CHAIN),,$(error CHAIN is required: make snapshot-chain CHAIN=<name>))
$(require-chain-config)
FOUNDRY_PROFILE=sync PROJECT_GROUP="$(GROUP)" forge script script/config/SnapshotChain.s.sol $(call evm-version-flag,$(CHAIN)) --sig "run(string)" "$(CHAIN)"
Expand Down
2 changes: 1 addition & 1 deletion docs/config-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ engine never assumes one:
"token": {
"address": "0xa1f7882a...", // the token this block describes (the snapshot/audit anchor;
// make doctor WARNs when it diverges from addresses.active.token
// - re-anchor after a repoint with make snapshot-chain)
// - a plain snapshot-chain refuses; re-anchor with REANCHOR=true)
"type": "crosschain", // crosschain | burnmint | factory | byo - selects the admin model
"ccipAdmin": "0xGov...", // getCCIPAdmin() - the TAR registration authority (one-step, owner-gated)
"defaultAdmin": "0xGov...", // crosschain only: defaultAdmin() (single-holder, two-step)
Expand Down
2 changes: 1 addition & 1 deletion docs/deployed-addresses.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Two terms, kept distinct throughout these docs:
| Run-time divergence notice | **Warner** | When an env override differs from `active.<role>`, a broadcasting script prints both values + the exact `make adopt-token …` to reconcile |
| `make doctor` TAR rung | **Warner** | Compares `active.tokenPool` against the on-chain TokenAdminRegistry and WARNs on divergence |
| `make doctor` registry rung | **Warner** | WARNs when `deployments{}` holds more than one token pool while `active.tokenPool` points at one (the multi-token ambiguity), naming a token `GROUP=<g>` as the durable fix and the `{CHAIN}_TOKEN_POOL` override as the one-off |
| `make doctor` roles rung | **Warner** | WARNs when a `roles.token/pool.address` anchor diverges from `addresses.active.<role>` (a repoint after the snapshot) - re-anchor with `make snapshot-chain` |
| `make doctor` roles rung | **Warner** | WARNs when a `roles.token/pool.address` anchor diverges from `addresses.active.<role>` (a repoint after the snapshot). A plain `make snapshot-chain` REFUSES here; re-anchor with `REANCHOR=true make snapshot-chain CHAIN=<name>` |

**Env overrides are READ-ONLY inputs: an env-driven run never writes the store.** An override changes only
what a single run resolves; to make a value the durable default, adopt it (`make adopt-token`). The
Expand Down
2 changes: 1 addition & 1 deletion docs/primitives/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -1123,7 +1123,7 @@
},
{
"target": "snapshot-chain",
"help": "Backfill the declared roles{} authority block FROM chain (CHAIN= required; GROUP= scopes to one token group; opt: TOKEN= TOKEN_POOL= TAR= SCAN_FROM_BLOCK=)"
"help": "Backfill the declared roles{} authority block FROM chain (CHAIN= required; GROUP= scopes to one token group; opt: TOKEN= TOKEN_POOL= TAR= SCAN_FROM_BLOCK= REANCHOR=true)"
},
{
"target": "roles-check",
Expand Down
16 changes: 16 additions & 0 deletions docs/roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,22 @@ flowchart TD
non-blocking) keep surfacing the drift as a `[FAIL]`/`::warning::` until it is reconciled one way or the
other. Reconciling means the two agree again - either the chain was fixed or the declaration was.

### `REANCHOR=true` - the repoint case

The runbook above reconciles the role HOLDERS. A repoint moves the SUBJECT: redeploy a token or pool
under the same group and `roles.<x>.address` still names the replaced contract while
`addresses.active.*` names the live one. `snapshot-chain` refuses that divergence rather than pick a
side - following the anchor would re-declare roles for a contract nobody uses, and following `active`
would let any redeploy silently re-point what the audit reconciles.

```bash
REANCHOR=true make snapshot-chain CHAIN=<name> # move the anchor to addresses.active.*
```

Use it only when the repoint was deliberate; if it was not, point the store back instead. The refusal
names both addresses, so you can tell which record is the wrong one. The anchor still outranks
`TOKEN`/`TOKEN_POOL`, which stay the way to snapshot a contract on a chain that has no declaration yet.

### `setDynamicConfig` router-preservation footgun

`rateLimitAdmin` and `feeAdmin` are both set through `setDynamicConfig(router, rateLimitAdmin,
Expand Down
26 changes: 26 additions & 0 deletions docs/troubleshooting/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,32 @@ command, the fix, and a self-verify. Start from the error you see.
redeploy, rewire the `TokenAdminRegistry` with `SetPool` (the registry still points at the old pool).
- **Verify.** `make doctor CHAIN=<name>` shows the registry pointing at the intended pool.

## A setup script reverts naming an admin address you never configured

`Admin of token doesn't match the expected admin address` from `ClaimAndAcceptAdmin`, or
`OnlyAdministrator(0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38, <token>)` from `SetPool`, on a token
whose `getCCIPAdmin()` and `owner()` both read back as your keystore account.

- **Diagnosis.** The passphrase never reached Foundry, so the keystore stayed locked and the simulation
ran as Foundry's default sender `0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38`. It is the same fallback
described under [the keystore password](../workflows/greenfield-deploy.md#keystore-password-non-interactive-runs),
seen from the other end: the revert names the token or the registry, never the signer, so the error
does not read like an authentication problem. Foundry prompts only when it can reach a terminal, so a
run with stdin closed (`</dev/null`, CI) takes the fallback silently.
- **Fix.** Supply the passphrase - `--password ''` for a passwordless keystore, `--password-file` or
`ETH_PASSWORD=<file>` otherwise. `--sender` is not needed; the account resolves once the keystore
unlocks.
```bash
forge script script/setup/ClaimAndAcceptAdmin.s.sol --rpc-url "$ETHEREUM_SEPOLIA_RPC_URL" \
--account "$KEYSTORE_NAME" --password '' --broadcast
```
Do not take Foundry's own hint here (`You seem to be using Foundry's default sender. Be sure to set
your own --sender`) at face value: `--sender` fixes which account the run simulates as, but the
keystore is still locked, so a non-interactive broadcast then dies at signing with `Device not
configured (os error 6)` - the passphrase is the fix in both cases.
- **Verify.** `cast call <TokenAdminRegistry> "getTokenConfig(address)((address,address,address))" <token>`
returns your account as the administrator.

## Inbound transfer reverts after removing a remote pool

- **Diagnosis.** Removing a remote pool leaves the chain supported with zero pools, so inbound
Expand Down
63 changes: 60 additions & 3 deletions script/config/VerifyChain.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ contract ChainProbe {
contract VerifyChain is Script {
uint256 private s_fails;
uint256 private s_warns;
// The last WARN text, kept so a unit test can assert WHICH warning fired. Two shapes of the same
// rung can both emit exactly one WARN, so a count alone cannot tell them apart, and `console.log`
// is not capturable in-process.
string private s_lastWarn;
uint256 private s_skips; // unverified-gap skips only (see _skipUnverified); designed skips do not count
bool private s_forked;
ChainProbe private s_probe;
Expand All @@ -274,6 +278,7 @@ contract VerifyChain is Script {

function _warn(string memory msg_) private {
s_warns++;
s_lastWarn = msg_;
console.log(string.concat("[WARN] ", msg_));
}

Expand Down Expand Up @@ -1380,17 +1385,29 @@ contract VerifyChain is Script {
/// @notice Test hook: runs ONLY the roles anchor-drift check (`_warnAnchorDrift` for the `token` and
/// `pool` anchors) for `name` against its project store, returning `(fails, warns)`. Lets a UNIT test
/// (no RPC, no auditor) assert the WARN-not-FAIL contract: a declared `roles.<x>.address` anchor that
/// diverges from `addresses.active.<role>` emits exactly one WARN naming both + `make snapshot-chain`,
/// diverges from `addresses.active.<role>` emits exactly one WARN naming both + `REANCHOR=true make snapshot-chain`,
/// while a matching anchor, an absent anchor, or a store with no active pointer stays silent. Not used
/// by any production path - the production caller is `_checkRoles` (behind the roles-block + RPC gates).
function warnAnchorDriftForTest(string memory name) public returns (uint256 failsOut, uint256 warnsOut) {
// `_warn`/`_fail` accumulate: without this reset a second call on the same instance carries the
// first store's tally and message, so an assertion lands on the earlier fixture, not this one.
s_fails = 0;
s_warns = 0;
s_lastWarn = "";
s_probe = new ChainProbe();
string memory projectJson = _readProject(name);
_warnAnchorDrift(name, projectJson, ".roles.token.address", "token", "roles.token.address");
_warnAnchorDrift(name, projectJson, ".roles.pool.address", "tokenPool", "roles.pool.address");
return (s_fails, s_warns);
}

/// @notice TEST-ONLY hook: the text of the last WARN emitted. A count cannot distinguish two
/// warnings from the same rung - a zero anchor and a diverging anchor both emit exactly one - so a
/// test that only counts passes whichever branch fired.
function lastWarnForTest() public view returns (string memory) {
return s_lastWarn;
}

/// @notice TEST-ONLY hook: runs the multi-pool ambiguity check for `name` against its project
/// store, returning `(fails, warns)`. Lets a UNIT test assert the WARN-not-FAIL contract: two or
/// more `deployments{}` token pools emit exactly one WARN; zero or one pool stays silent. Not used
Expand Down Expand Up @@ -1461,6 +1478,15 @@ contract VerifyChain is Script {

/// @dev WARN (never FAIL) when a declared roles anchor differs from the store's `active.<role>`.
/// Silent when the anchor is absent or the store has no active pointer (nothing to reconcile).
///
/// Two shapes warn with their OWN remedy rather than the drift one. A malformed anchor is read
/// through a try/catch because this rung runs OUTSIDE the guard that wraps the auditor, so a raw
/// parse abort here takes the whole doctor down - no verdict, no exit code - on a store the
/// snapshot half refuses cleanly. A zero anchor is not a usable declaration: `RolesAuditor` counts
/// a present-but-zero key as declared, gates every check behind a non-zero token, and then reports
/// "reconciles clean" having audited nothing, so it has to be named. Neither may quote the drift
/// remedy: a plain `snapshot-chain` resolves past a zero anchor rather than refusing it, and it
/// refuses a malformed one for a different reason than staleness.
function _warnAnchorDrift(
string memory name,
string memory projectJson,
Expand All @@ -1469,8 +1495,37 @@ contract VerifyChain is Script {
string memory label
) private {
if (!vm.keyExistsJson(projectJson, anchorPath)) return;
address anchor = vm.parseJsonAddress(projectJson, anchorPath);
address anchor;
try vm.parseJsonAddress(projectJson, anchorPath) returns (address a) {
anchor = a;
} catch {
_warn(
string.concat(
"roles: ",
label,
" is not an address - the audit cannot resolve what it is meant to reconcile.",
" Set it to a deployed address or remove the key, then: make snapshot-chain CHAIN=",
name
)
);
return;
}
address active = RegistryWriter._read(name, role);
if (anchor == address(0)) {
_warn(
string.concat(
"roles: ",
label,
" is the zero address, so every role check under it SKIPs and the roles rung reports",
" clean having audited nothing. A plain snapshot-chain does not refuse a zero anchor - it",
" re-resolves from addresses.active.",
role,
", and errors when the store has none: make snapshot-chain CHAIN=",
name
)
);
return;
}
if (active == address(0) || active == anchor) return;
_warn(
string.concat(
Expand All @@ -1482,7 +1537,9 @@ contract VerifyChain is Script {
role,
" ",
vm.toString(active),
" - the audit reconciles the anchored value; re-anchor after a repoint: make snapshot-chain CHAIN=",
" - the audit reconciles the ANCHORED value, so it is auditing the replaced contract.",
" A plain snapshot-chain will NOT fix this: it refuses a stale anchor rather than guess",
" which record is right. Re-anchor with: REANCHOR=true make snapshot-chain CHAIN=",
name
)
);
Expand Down
Loading
Loading