Skip to main content

CCIP Allowlist Chain-Match Setup — Operator Runbook

When to run: Once per deployment before the first CCIP movement after the signed-recipient invariant + chain-match pin lands, and again whenever you add a new CCIP (source, destination) lane or a new source/destination address. Why this exists: The CCIP lane now enforces two fund-outflow safety gates on top of the shared address allowlist. Both are CCIP-only — CCTP, Gateway, and CEX lanes are unchanged.
  1. Chain-match pin (movement creation and dispatch). A CCIP source or destination address is allowed only if its AllowedAddress row carries the exact on-chain chain_id derived from the lane’s CCIP selector. Chain-agnostic rows (chain_id IS NULL, the dev-quickstart / admin default) no longer pass for CCIP. Dispatch-time is authoritative; creation-time is defense-in-depth so a misconfigured movement is rejected before approval instead of failing a node after funds are committed to the plan.
  2. Submit-time signed-artifact invariant (broadcast boundary). Before the sidecar broadcasts, QTG independently RLP-decodes the actual signed ccipSend transaction and asserts its router, destination chain selector, receiver, and token equal the trusted intent / binding. Any divergence fails the node closed — the sidecar never broadcasts. This is the first QTG-side cross-check of the sidecar-built receiver against the movement intent.
Executor surface: docs/reference/executors/bridges/ccip-bridge.md.

Quick checklist

  • MG_CCIP_CHAIN_IDS_JSON maps every CCIP selector you transact on (source and destination) to its EVM chain_id.
  • Every CCIP source/destination address has an AllowedAddress row with chain_family='evm' and chain_id set to the mapped value.
  • A test CCIP movement creates and dispatches past the address guard.

1. Configure the selector → chain_id map

MG_CCIP_CHAIN_IDS_JSON is a JSON object mapping each CCIP chain selector to its EVM chain_id. It already feeds the evm_finality probe; this slice makes it also the authority for the allowlist chain-match. The default is empty ({}), which fails closed. Testnet example (Ethereum Sepolia + Base Sepolia):
Mainnet example (subset — match to the selectors your lanes use):
Every selector that appears as a CCIP source_chain_selector or dest_chain_selector in your lane registry must be present. A missing selector fails the lane closed (see the error table below). Restart the API/worker processes after changing the value.

2. Chain-scope the allowlist rows

Every CCIP source and destination address needs an AllowedAddress row with:
  • chain_family = 'evm'
  • chain_id = the value the address’s selector maps to in MG_CCIP_CHAIN_IDS_JSON
  • is_active = true
Rows added via the dev-quickstart or the generic admin path default to chain_id IS NULL and will fail closed for CCIP. Backfill them with the correct chain_id (the same address may legitimately have multiple rows, one per chain it operates on). CCTP/Gateway rows can stay chain-NULL — they are unaffected.

3. Verify

  1. Create a CCIP movement. It must compile without ADDRESS_NOT_ALLOWED / CHAIN_ID_NOT_CONFIGURED.
  2. Approve + dispatch. The node must progress past the address guard (PREPARING/SUBMITTING), not land in FAILED with an address error.

Error-code → cause → fix

The observable node error_code is always the exception’s first arg (ccip_signed_artifact_mismatch for the submit-time guard, ADDRESS_NOT_ALLOWED / CHAIN_ID_NOT_CONFIGURED for the address guard). The precise discriminator is in detail.error_code and is also appended to the node’s error_detail column — e.g. error_code=ccip_signed_artifact_mismatch, error_detail="ccip_signed_artifact_mismatch (SIGNED_TARGET_MISMATCH)" — so the specific reason is visible directly from the failed node record (and the logs).

Recovery — submit-time signed-artifact mismatch

A signed-artifact mismatch means the transaction that would have been broadcast does not match the approved intent. On detection, QTG tombstones the intent and releases the nonce reservation atomically (mirroring the registry-version-drift terminal pattern) before failing the node — so no broadcast occurs and the reservation is freed. If the atomic tombstone affects 0 rows (the intent was not in SUBMITTING, or the reservation id no longer matches), the reservation may be stranded; a ccip_eager_tombstone_rowcount_0 warning is logged. Follow docs/runbooks/ccip-recover-cutover-and-residuals.md and let the stranded-row reaper reclaim it. Do not retry the same payload. A mismatch is evidence the sidecar or signer produced a tx that diverges from the intent; the correct action is to investigate that divergence, not to resubmit.

What is NOT affected

CCTP, Gateway, and CEX lanes retain chain-agnostic allowlist matching (require_chain_match=False). No MG_CCIP_CHAIN_IDS_JSON or chain_id backfill is required for them. This runbook applies to the CCIP lane only.