diff --git a/deployment/README.md b/deployment/README.md index 9a7a52730..a5b38abd4 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -511,7 +511,7 @@ Production lane configure avoids mixed batches by using **Run 1 + Run 2** (below | Run | What | MCMS | Entry | |---|---|---|---| -| **Run 1** | CCIP core lanes (GlobalConfig, FeeQuoter, OnRamp/OffRamp, Executor) | `ccipOwner` | Generic `configure-chains-for-lanes-from-topology` → `adapters/chain_family_adapter.go` | +| **Run 1** | CCIP core lanes (GlobalConfig, FeeQuoter, OnRamp/OffRamp, Executor) + native fee token TAR registration (Amulet) | `ccipOwner` | Generic `configure-chains-for-lanes-from-topology` → `adapters/chain_family_adapter.go` | | **Run 2** | CommitteeVerifier only (`ApplyRemoteChainConfigUpdates`, signatures, allowlist) | `ccvOwner` | `changesets/configure_canton_committee_verifier_for_lanes.go` | Run 1: omit CV from Canton chain input. Run 2: custom chain-family registry delegates only to `sequences/configure_committee_verifier_for_lanes.go`. diff --git a/deployment/adapters/chain_family_adapter.go b/deployment/adapters/chain_family_adapter.go index a6d08ebae..60b9b107d 100644 --- a/deployment/adapters/chain_family_adapter.go +++ b/deployment/adapters/chain_family_adapter.go @@ -15,12 +15,14 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldfops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-canton/contracts" committeeverifierop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/committee_verifier" executorop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/executor" "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/fee_quoter" "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/global_config" "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/offramp" "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/onramp" + "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/token_admin_registry" "github.com/smartcontractkit/chainlink-canton/deployment/sequences" dsutil "github.com/smartcontractkit/chainlink-canton/deployment/utils/datastore" cantonmcms "github.com/smartcontractkit/chainlink-canton/deployment/utils/mcms" @@ -135,6 +137,43 @@ func (a *CantonChainFamilyAdapter) configureChainForLanes( return ccipseq.OnChainOutput{}, fmt.Errorf("resolve Canton native fee token instrument: %w", err) } + // Register native fee token (Amulet) TokenConfig in TAR once per lane configure run. + // Skipped when already on-ledger. Not inlined in proposal-driven core deploy because + // timelock-execute pre-resolves TAR before the deploy batch creates it. + tarRef, err := findContractRef( + ds, + input.ChainSelector, + datastore.ContractType(token_admin_registry.ContractType), + token_admin_registry.Version, + "", + ) + if err != nil { + return ccipseq.OnChainOutput{}, fmt.Errorf("resolve token admin registry: %w", err) + } + tarRaw, err := dsutil.GetRawInstanceAddressFromAddressRef(tarRef) + if err != nil { + return ccipseq.OnChainOutput{}, fmt.Errorf("resolve token admin registry raw address: %w", err) + } + ccipOwnerParty, err := resolveCcipOwnerParty(ds, input.ChainSelector) + if err != nil { + return ccipseq.OnChainOutput{}, fmt.Errorf("resolve ccipOwner party: %w", err) + } + mcmsEnabled := len(chain.Participants[0].ReadAsPartyIDs) > 0 + tarReport, err := cldfops.ExecuteSequence(b, sequences.RegisterNativeFeeTokenInTAR, chain, sequences.RegisterNativeFeeTokenInTARInput{ + TokenAdminRegistryInstanceAddress: contracts.HexToInstanceAddress(tarRef.Address), + TokenAdminRegistryRawInstanceAddress: tarRaw, + InstrumentId: nativeInstrument, + CcipOwnerParty: ccipOwnerParty, + TokenQualifier: string(nativeInstrument.Id), + ChainSelector: input.ChainSelector, + ProposalDriven: mcmsEnabled, + }) + if err != nil { + return ccipseq.OnChainOutput{}, fmt.Errorf("register native fee token in TAR: %w", err) + } + out.BatchOps = append(out.BatchOps, tarReport.Output.BatchOps...) + out.Addresses = append(out.Addresses, tarReport.Output.Addresses...) + for remoteSelector, remoteCfg := range input.RemoteChains { localExecutor, err := resolveContractRefByAddress( ds, diff --git a/deployment/changesets/register_native_fee_token.go b/deployment/changesets/register_native_fee_token.go index b189629ff..451573879 100644 --- a/deployment/changesets/register_native_fee_token.go +++ b/deployment/changesets/register_native_fee_token.go @@ -20,6 +20,9 @@ import ( // RegisterNativeFeeTokenInTARConfig registers the Canton native fee token (Amulet) in TAR. // Creates TokenConfig only — no token pool. InstrumentId is resolved from the validator // scan-proxy when admin/id are empty (requires ledger access at pipeline run time). +// +// New environments: this is also emitted automatically during lane configure Run 1. +// Keep this changeset for standalone registration or envs that ran lanes before that hook existed. type RegisterNativeFeeTokenInTARConfig struct { CCIPOwnerParty string `json:"ccipOwnerParty" yaml:"ccipOwnerParty"` MinDelay time.Duration `json:"minDelay,omitempty" yaml:"minDelay,omitempty"` diff --git a/deployment/sequences/deploy_chain_contracts_from_factory.go b/deployment/sequences/deploy_chain_contracts_from_factory.go index ac8f4a625..f4d0abcf8 100644 --- a/deployment/sequences/deploy_chain_contracts_from_factory.go +++ b/deployment/sequences/deploy_chain_contracts_from_factory.go @@ -49,10 +49,6 @@ var DeployChainContractsFromFactory = operations.NewSequence( if err != nil { return sequences.OnChainOutput{}, err } - nativeInstrumentID, err := resolveNativeInstrumentID(b, deps, input.NativeInstrumentId) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to resolve native fee token instrument: %w", err) - } factoryRawInstanceAddress, err := rawInstanceAddressFromAddressRef(input.FactoryAddressRef) if err != nil { return sequences.OnChainOutput{}, err @@ -105,19 +101,28 @@ var DeployChainContractsFromFactory = operations.NewSequence( tokenAdminRegistryRawInstanceAddress := tokenAdminRegistryInstanceID.RawInstanceAddress(ccipOwnerParty) addresses = append(addresses, newAddressRef(deps.ChainSelector(), tokenAdminRegistryRawInstanceAddress, token_admin_registry.ContractType, token_admin_registry.Version, "")) - feeTokenConfigOutputs, err := ensureNativeFeeTokenConfig( - b, - deps, - tokenAdminRegistryRawInstanceAddress.InstanceAddress(), - tokenAdminRegistryRawInstanceAddress, - input.CCIPOwnerParty, - nativeInstrumentID, - input.ProposalDriven, - ) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to ensure native fee token config: %w", err) + // Native fee token (Amulet) TAR registration is skipped in proposal-driven factory + // deploy: timelock-execute resolves TAR from ledger before the batch creates it. + // Registered during lane configure Run 1 (configureChainForLanes) after TAR exists. + if !input.ProposalDriven { + nativeInstrumentID, err := resolveNativeInstrumentID(b, deps, input.NativeInstrumentId) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to resolve native fee token instrument: %w", err) + } + feeTokenConfigOutputs, err := ensureNativeFeeTokenConfig( + b, + deps, + tokenAdminRegistryRawInstanceAddress.InstanceAddress(), + tokenAdminRegistryRawInstanceAddress, + input.CCIPOwnerParty, + nativeInstrumentID, + input.ProposalDriven, + ) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to ensure native fee token config: %w", err) + } + proposalOutputs = append(proposalOutputs, feeTokenConfigOutputs...) } - proposalOutputs = append(proposalOutputs, feeTokenConfigOutputs...) feeQuoterInstanceID, err := ensureInstanceID(input.FeeQuoterConfig.Template.InstanceId, "feequoter") if err != nil {