Skip to content

[WIP]USDC migration post validation script#1965

Draft
0xsuryansh wants to merge 1 commit intomainfrom
cctp_migration_post_validation
Draft

[WIP]USDC migration post validation script#1965
0xsuryansh wants to merge 1 commit intomainfrom
cctp_migration_post_validation

Conversation

@0xsuryansh
Copy link
Copy Markdown
Member

No description provided.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 9, 2026

👋 0xsuryansh, 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!

@0xsuryansh 0xsuryansh changed the title USDC migration post validation script [WIP]USDC migration post validation script Apr 9, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a read-only changeset and basic tests to validate the post-migration on-chain state for the USDC CLD rollout across EVM chains (token admin registry active pool, USDCTokenPoolProxy routing/config, remote lane config, and Ethereum home-chain liquidity migration invariants).

Changes:

  • Introduces ValidateUSDCCLDRollout changeset with config schema, precondition validation, and on-chain reads/assertions.
  • Adds home-chain liquidity migration checks (hybrid lock-release mode, lockbox auth, locked/balance derived expectations).
  • Adds unit tests covering key VerifyPreconditions failures and an Apply missing-chain failure.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
ccv/chains/evm/deployment/v1_7_0/changesets/validate_usdc_cld_rollout.go New post-validation changeset implementing config validation + on-chain assertions for USDC CLD rollout.
ccv/chains/evm/deployment/v1_7_0/changesets/validate_usdc_cld_rollout_test.go New tests for precondition validation and basic apply error behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

verifyHexAddress(v, fmt.Sprintf("chains[%d].TokenAdminRegistry", chainSelector), chainCfg.TokenAdminRegistry, true)
verifyHexAddress(v, fmt.Sprintf("chains[%d].ExpectedTokenPool", chainSelector), chainCfg.ExpectedTokenPool, false)
verifyHexAddress(v, fmt.Sprintf("chains[%d].USDCTokenPoolProxy", chainSelector), chainCfg.USDCTokenPoolProxy, false)
verifyProxyPools(v, fmt.Sprintf("chains[%d].ExpectedProxyPools", chainSelector), chainCfg.ExpectedProxyPools)
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VerifyPreconditions allows RemoteChains / ExpectedProxyPools to be configured even when USDCTokenPoolProxy is empty. In that case Apply will silently skip all proxy/remote validations (it returns early when USDCTokenPoolProxy == ""). Consider enforcing that USDCTokenPoolProxy is required whenever RemoteChains is non-empty or ExpectedProxyPools is non-nil (or alternatively error if those fields are set while proxy is empty).

Suggested change
verifyProxyPools(v, fmt.Sprintf("chains[%d].ExpectedProxyPools", chainSelector), chainCfg.ExpectedProxyPools)
verifyProxyPools(v, fmt.Sprintf("chains[%d].ExpectedProxyPools", chainSelector), chainCfg.ExpectedProxyPools)
if chainCfg.USDCTokenPoolProxy == "" {
if chainCfg.ExpectedProxyPools != nil {
v.addf("chains[%d].USDCTokenPoolProxy: required when ExpectedProxyPools is configured", chainSelector)
}
if len(chainCfg.RemoteChains) > 0 {
v.addf("chains[%d].USDCTokenPoolProxy: required when RemoteChains is configured", chainSelector)
}
}

Copilot uses AI. Check for mistakes.
)
}

if cfg.USDCTokenPoolProxy == "" {
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateChainState returns immediately when USDCTokenPoolProxy is empty, which can lead to false-green runs when the caller provided ExpectedProxyPools and/or RemoteChains expectations but forgot to set the proxy address. Instead of returning silently, consider adding a validation error when proxy-dependent checks are configured (or move this enforcement into VerifyPreconditions).

Suggested change
if cfg.USDCTokenPoolProxy == "" {
if cfg.USDCTokenPoolProxy == "" {
if len(cfg.ExpectedProxyPools) > 0 || len(cfg.RemoteChains) > 0 {
v.addf(
"chains[%d]: USDCTokenPoolProxy is required when ExpectedProxyPools or RemoteChains are configured",
chain.Selector,
)
}

Copilot uses AI. Check for mistakes.
cfg ValidateUSDCCLDRolloutRemoteChainConfig,
v *validationCollector,
) {
if cfg.ExpectedMechanism == "" {
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateRemoteChainState bails out when ExpectedMechanism is empty, which also skips validating ExpectedRemoteToken / ExpectedRemotePools even if those fields are set in config. Either (a) validate those fields independently of ExpectedMechanism, or (b) add a precondition that ExpectedRemoteToken/ExpectedRemotePools must be empty unless ExpectedMechanism is set, to avoid silently ignoring configured expectations.

Suggested change
if cfg.ExpectedMechanism == "" {
if cfg.ExpectedMechanism == "" {
if cfg.ExpectedRemoteToken != (common.Address{}) {
v.addf("chains[%d].remoteChains[%d]: expectedRemoteToken requires expectedMechanism to be set", chainSelector, remoteSelector)
}
if len(cfg.ExpectedRemotePools) > 0 {
v.addf("chains[%d].remoteChains[%d]: expectedRemotePools requires expectedMechanism to be set", chainSelector, remoteSelector)
}

Copilot uses AI. Check for mistakes.
Comment on lines +260 to +281
v.addf("chains[%d].remoteChains[%d]: failed to read proxy mechanism: %v", chainSelector, remoteSelector, err)
return
}

expectedMechanism, err := mechanismUint8(cfg.ExpectedMechanism)
if err != nil {
v.addf("chains[%d].remoteChains[%d]: %v", chainSelector, remoteSelector, err)
return
}
if mechanism != expectedMechanism {
v.addf("chains[%d].remoteChains[%d]: mechanism mismatch, expected %s got %d", chainSelector, remoteSelector, cfg.ExpectedMechanism, mechanism)
}

if supported, err := proxy.IsSupportedChain(callOpts, remoteSelector); err != nil {
v.addf("chains[%d].remoteChains[%d]: failed to check proxy supported chain: %v", chainSelector, remoteSelector, err)
} else if !supported {
v.addf("chains[%d].remoteChains[%d]: proxy does not report the chain as supported", chainSelector, remoteSelector)
}

backingPool := backingPoolForMechanism(pools, cfg.ExpectedMechanism)
if backingPool == (common.Address{}) {
v.addf("chains[%d].remoteChains[%d]: proxy backing pool for mechanism %s is not configured", chainSelector, remoteSelector, cfg.ExpectedMechanism)
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error paths in remote-chain validation use chains[%d].remoteChains[%d] (lowercase remoteChains), while VerifyPreconditions uses chains[%d].RemoteChains[...]. Similarly, home-chain liquidity errors use homeChainLiquidity.checks[...] in some places. Aligning these field names/casing across all validation errors will make failures easier to map back to config keys.

Suggested change
v.addf("chains[%d].remoteChains[%d]: failed to read proxy mechanism: %v", chainSelector, remoteSelector, err)
return
}
expectedMechanism, err := mechanismUint8(cfg.ExpectedMechanism)
if err != nil {
v.addf("chains[%d].remoteChains[%d]: %v", chainSelector, remoteSelector, err)
return
}
if mechanism != expectedMechanism {
v.addf("chains[%d].remoteChains[%d]: mechanism mismatch, expected %s got %d", chainSelector, remoteSelector, cfg.ExpectedMechanism, mechanism)
}
if supported, err := proxy.IsSupportedChain(callOpts, remoteSelector); err != nil {
v.addf("chains[%d].remoteChains[%d]: failed to check proxy supported chain: %v", chainSelector, remoteSelector, err)
} else if !supported {
v.addf("chains[%d].remoteChains[%d]: proxy does not report the chain as supported", chainSelector, remoteSelector)
}
backingPool := backingPoolForMechanism(pools, cfg.ExpectedMechanism)
if backingPool == (common.Address{}) {
v.addf("chains[%d].remoteChains[%d]: proxy backing pool for mechanism %s is not configured", chainSelector, remoteSelector, cfg.ExpectedMechanism)
v.addf("chains[%d].RemoteChains[%d]: failed to read proxy mechanism: %v", chainSelector, remoteSelector, err)
return
}
expectedMechanism, err := mechanismUint8(cfg.ExpectedMechanism)
if err != nil {
v.addf("chains[%d].RemoteChains[%d]: %v", chainSelector, remoteSelector, err)
return
}
if mechanism != expectedMechanism {
v.addf("chains[%d].RemoteChains[%d]: mechanism mismatch, expected %s got %d", chainSelector, remoteSelector, cfg.ExpectedMechanism, mechanism)
}
if supported, err := proxy.IsSupportedChain(callOpts, remoteSelector); err != nil {
v.addf("chains[%d].RemoteChains[%d]: failed to check proxy supported chain: %v", chainSelector, remoteSelector, err)
} else if !supported {
v.addf("chains[%d].RemoteChains[%d]: proxy does not report the chain as supported", chainSelector, remoteSelector)
}
backingPool := backingPoolForMechanism(pools, cfg.ExpectedMechanism)
if backingPool == (common.Address{}) {
v.addf("chains[%d].RemoteChains[%d]: proxy backing pool for mechanism %s is not configured", chainSelector, remoteSelector, cfg.ExpectedMechanism)

Copilot uses AI. Check for mistakes.
}

for chainSelector, chainCfg := range cfg.Chains {
verifyEVMChainSelector(v, chainSelector, "chains")
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verifyEVMChainSelector is called with field name "chains" for the top-level chain selector (no per-chain context). If multiple chains are provided, an error like chains: invalid chain selector ... is ambiguous. Consider passing a selector-specific field name (e.g., chains[<selector>]) for clearer error messages.

Suggested change
verifyEVMChainSelector(v, chainSelector, "chains")
verifyEVMChainSelector(v, chainSelector, fmt.Sprintf("chains[%d]", chainSelector))

Copilot uses AI. Check for mistakes.
Comment on lines +535 to +572
func deriveExpectedHybridLocked(check ValidateUSDCCLDRolloutLiquidityLaneCheck) (*big.Int, error) {
if check.ExpectedHybridLocked != "" {
return parseBigInt(check.ExpectedHybridLocked)
}
if check.PreHybridLocked == "" || check.ExpectedWithdrawAmount == "" {
return nil, nil
}
pre, err := parseBigInt(check.PreHybridLocked)
if err != nil {
return nil, fmt.Errorf("invalid PreHybridLocked %q: %w", check.PreHybridLocked, err)
}
withdraw, err := parseBigInt(check.ExpectedWithdrawAmount)
if err != nil {
return nil, fmt.Errorf("invalid ExpectedWithdrawAmount %q: %w", check.ExpectedWithdrawAmount, err)
}
if pre.Cmp(withdraw) < 0 {
return nil, fmt.Errorf("pre hybrid locked amount %s is smaller than expected withdraw amount %s", pre.String(), withdraw.String())
}
return new(big.Int).Sub(pre, withdraw), nil
}

func deriveExpectedLockBoxBalance(check ValidateUSDCCLDRolloutLiquidityLaneCheck) (*big.Int, error) {
if check.ExpectedLockBoxBalance != "" {
return parseBigInt(check.ExpectedLockBoxBalance)
}
if check.PreLockBoxBalance == "" || check.ExpectedWithdrawAmount == "" {
return nil, nil
}
pre, err := parseBigInt(check.PreLockBoxBalance)
if err != nil {
return nil, fmt.Errorf("invalid PreLockBoxBalance %q: %w", check.PreLockBoxBalance, err)
}
withdraw, err := parseBigInt(check.ExpectedWithdrawAmount)
if err != nil {
return nil, fmt.Errorf("invalid ExpectedWithdrawAmount %q: %w", check.ExpectedWithdrawAmount, err)
}
return new(big.Int).Add(pre, withdraw), nil
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The arithmetic helpers deriveExpectedHybridLocked / deriveExpectedLockBoxBalance introduce non-trivial validation logic (explicit expected value vs derived value, and erroring when withdraw > pre). Adding focused unit tests for these functions (including boundary cases like equal amounts and withdraw greater than pre) would reduce the risk of incorrect post-validation results.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants