diff --git a/deployment/ccip/changeset/cs_accept_admin_role.go b/deployment/ccip/changeset/cs_accept_admin_role.go new file mode 100644 index 00000000000..c4aa80b5cbd --- /dev/null +++ b/deployment/ccip/changeset/cs_accept_admin_role.go @@ -0,0 +1,58 @@ +package changeset + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/token_admin_registry" +) + +var _ deployment.ChangeSet[TokenAdminRegistryChangesetConfig] = AcceptAdminRoleChangeset + +func validateAcceptAdminRole( + config token_admin_registry.TokenAdminRegistryTokenConfig, + sender common.Address, + externalAdmin common.Address, + symbol TokenSymbol, + chain deployment.Chain, +) error { + // We must be the pending administrator + if config.PendingAdministrator != sender { + return fmt.Errorf("unable to accept admin role for %s token on %s: %s is not the pending administrator (%s)", symbol, chain, sender, config.PendingAdministrator) + } + return nil +} + +// AcceptAdminRoleChangeset accepts admin rights for tokens on the token admin registry. +func AcceptAdminRoleChangeset(env deployment.Environment, c TokenAdminRegistryChangesetConfig) (deployment.ChangesetOutput, error) { + if err := c.Validate(env, validateAcceptAdminRole); err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("invalid TokenAdminRegistryChangesetConfig: %w", err) + } + state, err := LoadOnchainState(env) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to load onchain state: %w", err) + } + deployerGroup := NewDeployerGroup(env, state, c.MCMS) + + for chainSelector, tokenSymbolToPoolInfo := range c.Pools { + chain := env.Chains[chainSelector] + chainState := state.Chains[chainSelector] + opts, err := deployerGroup.GetDeployer(chainSelector) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to get deployer for %s", chain) + } + for symbol, poolInfo := range tokenSymbolToPoolInfo { + _, tokenAddress, err := poolInfo.GetPoolAndTokenAddress(env.GetContext(), symbol, chain, chainState) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to get state of %s token on chain %s: %w", symbol, chain, err) + } + _, err = chainState.TokenAdminRegistry.AcceptAdminRole(opts, tokenAddress) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create acceptAdminRole transaction for %s on %s registry: %w", symbol, chain, err) + } + } + } + + return deployerGroup.Enact("accept admin role for tokens on token admin registries") +} diff --git a/deployment/ccip/changeset/cs_accept_admin_role_test.go b/deployment/ccip/changeset/cs_accept_admin_role_test.go new file mode 100644 index 00000000000..8e69d5d9906 --- /dev/null +++ b/deployment/ccip/changeset/cs_accept_admin_role_test.go @@ -0,0 +1,216 @@ +package changeset_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +func TestAcceptAdminRoleChangeset_Validations(t *testing.T) { + t.Parallel() + + e, selectorA, _, tokens, timelockContracts := testhelpers.SetupTwoChainEnvironmentWithTokens(t, logger.TestLogger(t), true) + + e = testhelpers.DeployTestTokenPools(t, e, map[uint64]changeset.DeployTokenPoolInput{ + selectorA: { + Type: changeset.BurnMintTokenPool, + TokenAddress: tokens[selectorA].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + }, + }, true) + + mcmsConfig := &changeset.MCMSConfig{ + MinDelay: 0 * time.Second, + } + + tests := []struct { + Config changeset.TokenAdminRegistryChangesetConfig + ErrStr string + Msg string + }{ + { + Msg: "Chain selector is invalid", + Config: changeset.TokenAdminRegistryChangesetConfig{ + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + 0: map[changeset.TokenSymbol]changeset.TokenPoolInfo{}, + }, + }, + ErrStr: "failed to validate chain selector 0", + }, + { + Msg: "Chain selector doesn't exist in environment", + Config: changeset.TokenAdminRegistryChangesetConfig{ + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + 5009297550715157269: map[changeset.TokenSymbol]changeset.TokenPoolInfo{}, + }, + }, + ErrStr: "does not exist in environment", + }, + { + Msg: "Ownership validation failure", + Config: changeset.TokenAdminRegistryChangesetConfig{ + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{}, + }, + }, + ErrStr: "token admin registry failed ownership validation", + }, + { + Msg: "Invalid pool type", + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: "InvalidType", + Version: deployment.Version1_5_1, + }, + }, + }, + }, + ErrStr: "InvalidType is not a known token pool type", + }, + { + Msg: "Invalid pool version", + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_0_0, + }, + }, + }, + }, + ErrStr: "1.0.0 is not a known token pool version", + }, + { + Msg: "Not pending admin", + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + }, + }, + ErrStr: "is not the pending administrator", + }, + } + + for _, test := range tests { + t.Run(test.Msg, func(t *testing.T) { + _, err := commonchangeset.ApplyChangesets(t, e, timelockContracts, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(changeset.AcceptAdminRoleChangeset), + Config: test.Config, + }, + }) + require.Error(t, err) + require.ErrorContains(t, err, test.ErrStr) + }) + } +} + +func TestAcceptAdminRoleChangeset_Execution(t *testing.T) { + for _, mcmsConfig := range []*changeset.MCMSConfig{nil, &changeset.MCMSConfig{MinDelay: 0 * time.Second}} { + msg := "Accept admin role with MCMS" + if mcmsConfig == nil { + msg = "Accept admin role without MCMS" + } + + t.Run(msg, func(t *testing.T) { + e, selectorA, selectorB, tokens, timelockContracts := testhelpers.SetupTwoChainEnvironmentWithTokens(t, logger.TestLogger(t), mcmsConfig != nil) + + e = testhelpers.DeployTestTokenPools(t, e, map[uint64]changeset.DeployTokenPoolInput{ + selectorA: { + Type: changeset.BurnMintTokenPool, + TokenAddress: tokens[selectorA].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + }, + selectorB: { + Type: changeset.BurnMintTokenPool, + TokenAddress: tokens[selectorB].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + }, + }, mcmsConfig != nil) + + state, err := changeset.LoadOnchainState(e) + require.NoError(t, err) + + registryOnA := state.Chains[selectorA].TokenAdminRegistry + registryOnB := state.Chains[selectorB].TokenAdminRegistry + + e, err = commonchangeset.ApplyChangesets(t, e, timelockContracts, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(changeset.ProposeAdminRoleChangeset), + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + selectorB: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + }, + }, + }, + { + Changeset: commonchangeset.WrapChangeSet(changeset.AcceptAdminRoleChangeset), + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + selectorB: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + }, + }, + }, + }) + require.NoError(t, err) + + configOnA, err := registryOnA.GetTokenConfig(nil, tokens[selectorA].Address) + require.NoError(t, err) + if mcmsConfig != nil { + require.Equal(t, state.Chains[selectorA].Timelock.Address(), configOnA.Administrator) + } else { + require.Equal(t, e.Chains[selectorA].DeployerKey.From, configOnA.Administrator) + } + + configOnB, err := registryOnB.GetTokenConfig(nil, tokens[selectorB].Address) + require.NoError(t, err) + if mcmsConfig != nil { + require.Equal(t, state.Chains[selectorB].Timelock.Address(), configOnB.Administrator) + } else { + require.Equal(t, e.Chains[selectorB].DeployerKey.From, configOnB.Administrator) + } + }) + } +} diff --git a/deployment/ccip/changeset/cs_configure_token_admin_registry.go b/deployment/ccip/changeset/cs_configure_token_admin_registry.go deleted file mode 100644 index 7cb06ba063f..00000000000 --- a/deployment/ccip/changeset/cs_configure_token_admin_registry.go +++ /dev/null @@ -1,299 +0,0 @@ -package changeset - -import ( - "context" - "errors" - "fmt" - - "github.com/Masterminds/semver/v3" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - - "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" - - "github.com/smartcontractkit/chainlink/deployment" - commoncs "github.com/smartcontractkit/chainlink/deployment/common/changeset" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/token_admin_registry" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/token_pool" -) - -var _ deployment.ChangeSet[ConfigureTokenAdminRegistryConfig] = ConfigureTokenAdminRegistryChangeset - -// RegistryConfig defines a token and its state on the token admin registry -type RegistryConfig struct { - // Type is the type of the token pool. - Type deployment.ContractType - // Version is the version of the token pool. - Version semver.Version - // ExternalAdministrator is the address of a 3rd party token administrator that should be set on the registry. - ExternalAdministrator common.Address -} - -func (c RegistryConfig) Validate(ctx context.Context, chain deployment.Chain, state CCIPChainState, useMcms bool, tokenSymbol TokenSymbol) error { - // Ensure that the inputted type is known - if _, ok := tokenPoolTypes[c.Type]; !ok { - return fmt.Errorf("%s is not a known token pool type", c.Type) - } - - // Ensure that the inputted version is known - if _, ok := tokenPoolVersions[c.Version]; !ok { - return fmt.Errorf("%s is not a known token pool version", c.Version) - } - - // Ensure that a pool with given symbol, type and version is known to the environment - tokenPool, err := getTokenPoolFromSymbolTypeAndVersion(state, chain, tokenSymbol, c.Type, c.Version) - if err != nil { - return fmt.Errorf("failed to find token pool on %s with symbol %s, type %s, and version %s: %w", chain.String(), tokenSymbol, c.Type, c.Version, err) - } - - // Validate that the token admin registry is owned by the address that will be actioning the transactions (i.e. Timelock or deployer key) - if err := commoncs.ValidateOwnership(ctx, useMcms, chain.DeployerKey.From, state.Timelock.Address(), state.TokenAdminRegistry); err != nil { - return fmt.Errorf("token admin registry failed ownership validation on %s: %w", chain.String(), err) - } - - // Fetch information about the corresponding token and its state on the registry - token, err := tokenPool.GetToken(nil) - if err != nil { - return fmt.Errorf("failed to get token from pool with address %s on chain %s: %w", tokenPool.Address(), chain.String(), err) - } - tokenConfig, err := state.TokenAdminRegistry.GetTokenConfig(nil, token) - if err != nil { - return fmt.Errorf("failed to get %s config from registry on chain %s: %w", tokenSymbol, chain.String(), err) - } - - fromAddress := state.Timelock.Address() // "We" are either the Timelock or the deployer key - if !useMcms { - fromAddress = chain.DeployerKey.From - } - - // Running this changeset has possible motivations: we want to update the pool for a token or transfer the admin rights of a token. - // It doesn't really matter if we are doing one or both, so long as we are able to perform the action(s). - - // To perform these actions, we have to be the admin of the token. There are three ways this can happen: - // 1. We are already the admin of the token (no action) - // 2. We are the proposed admin of the token (just have to accept) - // 3. We can become the admin of the token (have to propose and accept), which requires us to be the owner of the registry and for the token to be admin-less. - // The following code checks these conditions. - - if tokenConfig.Administrator == fromAddress || tokenConfig.PendingAdministrator == fromAddress { - // We are already the administrator / pending administrator & will be able to perform any actions required - return nil - } - - // If we are not admin / pending admin, we must set ourselves as admin of the token, which requires two things to be true. - // 1. We own the token admin registry - // 2. An admin musn't exist yet - // We've already validated that we own the registry during ValidateOwnership, so we only need to check the 2nd condition - if tokenConfig.Administrator != utils.ZeroAddress { - return fmt.Errorf("unable to set %s as admin of %s token on %s: token already has an administrator (%s)", fromAddress, tokenSymbol, chain, tokenConfig.Administrator) - } - return nil -} - -// ConfigureTokenAdminRegistryConfig is the configuration for the ConfigureTokenAdminRegistry changeset. -type ConfigureTokenAdminRegistryConfig struct { - // MCMS defines the delay to use for Timelock (if absent, the changeset will attempt to use the deployer key). - MCMS *MCMSConfig - // RegistryUpdates defines the desired state of the registry on each given chain. - RegistryUpdates map[uint64]RegistryConfig - // TokenSymbol is the symbol of the token of interest. - TokenSymbol TokenSymbol -} - -func (c ConfigureTokenAdminRegistryConfig) Validate(env deployment.Environment) error { - if c.TokenSymbol == "" { - return errors.New("token symbol must be defined") - } - state, err := LoadOnchainState(env) - if err != nil { - return fmt.Errorf("failed to load onchain state: %w", err) - } - for chainSelector, registryUpdate := range c.RegistryUpdates { - err := deployment.IsValidChainSelector(chainSelector) - if err != nil { - return fmt.Errorf("failed to validate chain selector %d: %w", chainSelector, err) - } - chain, ok := env.Chains[chainSelector] - if !ok { - return fmt.Errorf("chain with selector %d does not exist in environment", chainSelector) - } - chainState, ok := state.Chains[chainSelector] - if !ok { - return fmt.Errorf("%s does not exist in state", chain.String()) - } - if tokenAdminRegistry := chainState.TokenAdminRegistry; tokenAdminRegistry == nil { - return fmt.Errorf("missing tokenAdminRegistry on %s", chain.String()) - } - if c.MCMS != nil { - if timelock := chainState.Timelock; timelock == nil { - return fmt.Errorf("missing timelock on %s", chain.String()) - } - if proposerMcm := chainState.ProposerMcm; proposerMcm == nil { - return fmt.Errorf("missing proposerMcm on %s", chain.String()) - } - } - if err := registryUpdate.Validate(env.GetContext(), chain, chainState, c.MCMS != nil, c.TokenSymbol); err != nil { - return fmt.Errorf("invalid pool update on %s: %w", chain.String(), err) - } - } - - return nil -} - -// ConfigureTokenAdminRegistryChangeset configures updates administrators and token pools on the TokenAdminRegistry. -func ConfigureTokenAdminRegistryChangeset(env deployment.Environment, c ConfigureTokenAdminRegistryConfig) (deployment.ChangesetOutput, error) { - if err := c.Validate(env); err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("invalid ConfigureTokenAdminRegistryConfig: %w", err) - } - state, err := LoadOnchainState(env) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to load onchain state: %w", err) - } - deployerGroup := NewDeployerGroup(env, state, c.MCMS) - - chainConfigs, err := getConfigurationsByChain(env, state, c, deployerGroup) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch configurations for each chain: %w", err) - } - - // Propose admin pass - for chainSelector := range c.RegistryUpdates { - cc := chainConfigs[chainSelector] - - if cc.TokenConfigOnRegistry.Administrator != cc.Sender && cc.TokenConfigOnRegistry.PendingAdministrator != cc.Sender { - _, err := cc.State.TokenAdminRegistry.ProposeAdministrator(cc.Opts, cc.TokenAddress, cc.Sender) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to create proposeAdministrator transaction for %s on %s registry: %w", c.TokenSymbol, cc.Chain, err) - } - } - } - - proposeAdminOutput, err := deployerGroup.Enact(fmt.Sprintf("propose admin for %s on token admin registries", c.TokenSymbol)) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("propose admin for %s on token admin registries: %w", c.TokenSymbol, err) - } - - // Accept admin pass - for chainSelector := range c.RegistryUpdates { - cc := chainConfigs[chainSelector] - - if cc.TokenConfigOnRegistry.Administrator != cc.Sender { - _, err := cc.State.TokenAdminRegistry.AcceptAdminRole(cc.Opts, cc.TokenAddress) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to create acceptAdminRole transaction for %s on %s registry: %w", c.TokenSymbol, cc.Chain, err) - } - } - } - - acceptAdminOutput, err := deployerGroup.Enact(fmt.Sprintf("accept admin rights for %s on token admin registries", c.TokenSymbol)) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to accept admin rights for %s on token admin registries: %w", c.TokenSymbol, err) - } - - // Configuration pass (set pool, transfer admin role to 3rd party) - for chainSelector, registryUpdate := range c.RegistryUpdates { - cc := chainConfigs[chainSelector] - - // Only set the pool if we need to - if cc.TokenConfigOnRegistry.TokenPool != cc.TokenPool.Address() { - _, err := cc.State.TokenAdminRegistry.SetPool(cc.Opts, cc.TokenAddress, cc.TokenPool.Address()) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to create setPool transaction for %s on %s registry: %w", c.TokenSymbol, cc.Chain, err) - } - } - - // Only set the administrator to an external address if we need to - if registryUpdate.ExternalAdministrator != cc.Sender { - _, err := cc.State.TokenAdminRegistry.TransferAdminRole(cc.Opts, cc.TokenAddress, registryUpdate.ExternalAdministrator) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to create transferAdminRole transaction for %s on %s registry: %w", c.TokenSymbol, cc.Chain, err) - } - } - } - - configurationOutput, err := deployerGroup.Enact(fmt.Sprintf("configure %s on token admin registries", c.TokenSymbol)) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to configure %s on token admin registries: %w", c.TokenSymbol, err) - } - - if c.MCMS != nil { - // Pre-allocate the proposal slice with the correct capacity - totalProposals := len(proposeAdminOutput.Proposals) + - len(acceptAdminOutput.Proposals) + - len(configurationOutput.Proposals) - proposals := make([]timelock.MCMSWithTimelockProposal, 0, totalProposals) - proposals = append(proposals, proposeAdminOutput.Proposals...) - proposals = append(proposals, acceptAdminOutput.Proposals...) - proposals = append(proposals, configurationOutput.Proposals...) - - return deployment.ChangesetOutput{ - Proposals: proposals, - }, nil - } - - return deployment.ChangesetOutput{}, nil -} - -// chainConfig defines the configuration needed to create operations on a chain -type chainConfig struct { - TokenPool *token_pool.TokenPool - TokenAddress common.Address - TokenConfigOnRegistry token_admin_registry.TokenAdminRegistryTokenConfig - Sender common.Address - State CCIPChainState - Chain deployment.Chain - Opts *bind.TransactOpts -} - -// getConfigurationsByChain fetches the configuration required to create operations for each chain -func getConfigurationsByChain( - env deployment.Environment, - state CCIPOnChainState, - c ConfigureTokenAdminRegistryConfig, - deployerGroup *DeployerGroup, -) (map[uint64]chainConfig, error) { - chainConfigs := make(map[uint64]chainConfig, len(c.RegistryUpdates)) - - for chainSelector, registryUpdate := range c.RegistryUpdates { - chain := env.Chains[chainSelector] - chainState := state.Chains[chainSelector] - - opts, err := deployerGroup.GetDeployer(chainSelector) - if err != nil { - return map[uint64]chainConfig{}, fmt.Errorf("failed to get deployer for %s", chain) - } - - tokenPool, err := getTokenPoolFromSymbolTypeAndVersion(chainState, chain, c.TokenSymbol, registryUpdate.Type, registryUpdate.Version) - if err != nil { - return map[uint64]chainConfig{}, fmt.Errorf("failed to find token pool on %s with symbol %s, type %s, and version %s: %w", chain.String(), c.TokenSymbol, registryUpdate.Type, registryUpdate.Version, err) - } - - token, err := tokenPool.GetToken(nil) - if err != nil { - return map[uint64]chainConfig{}, fmt.Errorf("failed to get token from address %s on chain %s: %w", tokenPool.Address(), chain.String(), err) - } - - tokenConfig, err := chainState.TokenAdminRegistry.GetTokenConfig(nil, token) - if err != nil { - return map[uint64]chainConfig{}, fmt.Errorf("failed to get %s config from registry on chain %s: %w", c.TokenSymbol, chain.String(), err) - } - - sender := chainState.Timelock.Address() - if c.MCMS == nil { - sender = chain.DeployerKey.From - } - - chainConfigs[chainSelector] = chainConfig{ - TokenPool: tokenPool, - TokenAddress: token, - TokenConfigOnRegistry: tokenConfig, - Sender: sender, - State: chainState, - Chain: chain, - Opts: opts, - } - } - - return chainConfigs, nil -} diff --git a/deployment/ccip/changeset/cs_configure_token_admin_registry_test.go b/deployment/ccip/changeset/cs_configure_token_admin_registry_test.go deleted file mode 100644 index 2b00df6380e..00000000000 --- a/deployment/ccip/changeset/cs_configure_token_admin_registry_test.go +++ /dev/null @@ -1,275 +0,0 @@ -package changeset_test - -import ( - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/require" - "go.uber.org/zap/zapcore" - - "github.com/smartcontractkit/chainlink/deployment" - "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" - "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" - commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" - "github.com/smartcontractkit/chainlink/deployment/environment/memory" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" - "github.com/smartcontractkit/chainlink/v2/core/logger" -) - -func TestValidateRegistryConfig(t *testing.T) { - t.Parallel() - - e, selectorA, _, tokens, timelockContracts := testhelpers.SetupTwoChainEnvironmentWithTokens(t, logger.TestLogger(t), true) - administrator := utils.RandomAddress() - - e = testhelpers.DeployTestTokenPools(t, e, map[uint64]changeset.DeployTokenPoolInput{ - selectorA: { - Type: changeset.BurnMintTokenPool, - TokenAddress: tokens[selectorA].Address, - LocalTokenDecimals: testhelpers.LocalTokenDecimals, - }, - }, true) - - // Deploy another token pool with force enabled - e = testhelpers.DeployTestTokenPools(t, e, map[uint64]changeset.DeployTokenPoolInput{ - selectorA: { - Type: changeset.BurnWithFromMintTokenPool, - TokenAddress: tokens[selectorA].Address, - LocalTokenDecimals: testhelpers.LocalTokenDecimals, - ForceDeployment: true, - }, - }, true) - - state, err := changeset.LoadOnchainState(e) - require.NoError(t, err) - - tokenAdminRegistry := state.Chains[selectorA].TokenAdminRegistry - - // We want to transfer the admin role of the pool to the deployer to get a validation failure in the last test - e, err = commonchangeset.ApplyChangesets(t, e, timelockContracts, []commonchangeset.ChangesetApplication{ - { - Changeset: commonchangeset.WrapChangeSet(changeset.ConfigureTokenAdminRegistryChangeset), - Config: changeset.ConfigureTokenAdminRegistryConfig{ - TokenSymbol: testhelpers.TestTokenSymbol, - MCMS: &changeset.MCMSConfig{ - MinDelay: 0 * time.Second, - }, - RegistryUpdates: map[uint64]changeset.RegistryConfig{ - selectorA: { - Type: changeset.BurnMintTokenPool, - Version: deployment.Version1_5_1, - ExternalAdministrator: e.Chains[selectorA].DeployerKey.From, - }, - }, - }, - }, - }) - require.NoError(t, err) - tx, err := tokenAdminRegistry.AcceptAdminRole(e.Chains[selectorA].DeployerKey, tokens[selectorA].Address) - require.NoError(t, err) - _, err = e.Chains[selectorA].Confirm(tx) - require.NoError(t, err) - - tests := []struct { - Msg string - UseMcms bool - TokenSymbol changeset.TokenSymbol - RegistryConfig changeset.RegistryConfig - ErrStr string - }{ - { - Msg: "Pool type is invalid", - RegistryConfig: changeset.RegistryConfig{}, - ErrStr: "is not a known token pool type", - }, - { - Msg: "Pool version is invalid", - RegistryConfig: changeset.RegistryConfig{ - Type: changeset.BurnWithFromMintTokenPool, - }, - ErrStr: "is not a known token pool version", - }, - { - Msg: "Pool not found", - TokenSymbol: "WRONG", - RegistryConfig: changeset.RegistryConfig{ - Type: changeset.BurnWithFromMintTokenPool, - Version: deployment.Version1_5_1, - }, - ErrStr: "failed to find token pool", - }, - { - Msg: "Token admin registry is not owned by required address", - TokenSymbol: testhelpers.TestTokenSymbol, - RegistryConfig: changeset.RegistryConfig{ - Type: changeset.BurnWithFromMintTokenPool, - Version: deployment.Version1_5_1, - }, - ErrStr: "token admin registry failed ownership validation", - }, - { - Msg: "Owner can't become admin of token", - TokenSymbol: testhelpers.TestTokenSymbol, - UseMcms: true, - RegistryConfig: changeset.RegistryConfig{ - Type: changeset.BurnWithFromMintTokenPool, - Version: deployment.Version1_5_1, - ExternalAdministrator: administrator, - }, - ErrStr: "token already has an administrator", - }, - } - - for _, test := range tests { - t.Run(test.Msg, func(t *testing.T) { - err := test.RegistryConfig.Validate(e.GetContext(), e.Chains[selectorA], state.Chains[selectorA], test.UseMcms, test.TokenSymbol) - require.Error(t, err) - require.ErrorContains(t, err, test.ErrStr) - }) - } -} - -func TestValidateConfigureTokenAdminRegistryConfig(t *testing.T) { - t.Parallel() - - lggr := logger.TestLogger(t) - e := memory.NewMemoryEnvironment(t, lggr, zapcore.InfoLevel, memory.MemoryEnvironmentConfig{ - Chains: 2, - }) - - tests := []struct { - TokenSymbol changeset.TokenSymbol - Input changeset.ConfigureTokenAdminRegistryConfig - ErrStr string - Msg string - }{ - { - Msg: "Token symbol is missing", - Input: changeset.ConfigureTokenAdminRegistryConfig{}, - ErrStr: "token symbol must be defined", - }, - { - Msg: "Chain selector is invalid", - Input: changeset.ConfigureTokenAdminRegistryConfig{ - TokenSymbol: testhelpers.TestTokenSymbol, - RegistryUpdates: map[uint64]changeset.RegistryConfig{ - 0: changeset.RegistryConfig{}, - }, - }, - ErrStr: "failed to validate chain selector 0", - }, - { - Msg: "Chain selector doesn't exist in environment", - Input: changeset.ConfigureTokenAdminRegistryConfig{ - TokenSymbol: testhelpers.TestTokenSymbol, - RegistryUpdates: map[uint64]changeset.RegistryConfig{ - 5009297550715157269: changeset.RegistryConfig{}, - }, - }, - ErrStr: "does not exist in environment", - }, - { - Msg: "Token admin registry is missing", - Input: changeset.ConfigureTokenAdminRegistryConfig{ - TokenSymbol: testhelpers.TestTokenSymbol, - RegistryUpdates: map[uint64]changeset.RegistryConfig{ - e.AllChainSelectors()[0]: changeset.RegistryConfig{}, - }, - }, - ErrStr: "missing tokenAdminRegistry", - }, - } - - for _, test := range tests { - t.Run(test.Msg, func(t *testing.T) { - err := test.Input.Validate(e) - require.Contains(t, err.Error(), test.ErrStr) - }) - } -} - -func TestConfigureTokenAdminRegistry(t *testing.T) { - t.Parallel() - - tests := []struct { - Administrator common.Address - MCMS *changeset.MCMSConfig - Msg string - }{ - { - Msg: "Configure with MCMS", - MCMS: &changeset.MCMSConfig{ - MinDelay: 0 * time.Second, - }, - }, - { - Msg: "Configure without MCMS", - }, - { - Msg: "Configure with MCMS & transfer", - MCMS: &changeset.MCMSConfig{ - MinDelay: 0 * time.Second, - }, - Administrator: utils.RandomAddress(), - }, - { - Msg: "Configure without MCMS & transfer", - Administrator: utils.RandomAddress(), - }, - } - - for _, test := range tests { - t.Run(test.Msg, func(t *testing.T) { - e, selectorA, _, tokens, timelockContracts := testhelpers.SetupTwoChainEnvironmentWithTokens(t, logger.TestLogger(t), test.MCMS != nil) - - e = testhelpers.DeployTestTokenPools(t, e, map[uint64]changeset.DeployTokenPoolInput{ - selectorA: { - Type: changeset.BurnMintTokenPool, - TokenAddress: tokens[selectorA].Address, - LocalTokenDecimals: testhelpers.LocalTokenDecimals, - }, - }, test.MCMS != nil) - - state, err := changeset.LoadOnchainState(e) - require.NoError(t, err) - - tokenAddress := tokens[selectorA].Address - timelockAddress := state.Chains[selectorA].Timelock.Address() - poolAddress := state.Chains[selectorA].BurnMintTokenPools[testhelpers.TestTokenSymbol][deployment.Version1_5_1].Address() - tokenAdminRegistry := state.Chains[selectorA].TokenAdminRegistry - deployerKey := e.Chains[selectorA].DeployerKey.From - - e, err = commonchangeset.ApplyChangesets(t, e, timelockContracts, []commonchangeset.ChangesetApplication{ - { - Changeset: commonchangeset.WrapChangeSet(changeset.ConfigureTokenAdminRegistryChangeset), - Config: changeset.ConfigureTokenAdminRegistryConfig{ - TokenSymbol: testhelpers.TestTokenSymbol, - MCMS: test.MCMS, - RegistryUpdates: map[uint64]changeset.RegistryConfig{ - selectorA: { - Type: changeset.BurnMintTokenPool, - Version: deployment.Version1_5_1, - ExternalAdministrator: test.Administrator, - }, - }, - }, - }, - }) - require.NoError(t, err) - - tokenConfig, err := tokenAdminRegistry.GetTokenConfig(nil, tokenAddress) - require.NoError(t, err) - - if test.Administrator != utils.ZeroAddress { - require.Equal(t, test.Administrator, tokenConfig.PendingAdministrator) - } - if test.MCMS == nil { - require.Equal(t, deployerKey, tokenConfig.Administrator) - } else { - require.Equal(t, timelockAddress, tokenConfig.Administrator) - } - require.Equal(t, poolAddress, tokenConfig.TokenPool) - }) - } -} diff --git a/deployment/ccip/changeset/cs_configure_token_pools.go b/deployment/ccip/changeset/cs_configure_token_pools.go index 737abed47b1..5913f102139 100644 --- a/deployment/ccip/changeset/cs_configure_token_pools.go +++ b/deployment/ccip/changeset/cs_configure_token_pools.go @@ -182,7 +182,7 @@ func ConfigureTokenPoolContractsChangeset(env deployment.Environment, c Configur if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to get deployer for %s", chain) } - err = configureTokenPool(opts, env.Chains, state, c, chainSelector) + err = configureTokenPool(env.GetContext(), opts, env.Chains, state, c, chainSelector) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to make operations to configure %s token pool on %s: %w", c.TokenSymbol, chain.String(), err) } @@ -194,6 +194,7 @@ func ConfigureTokenPoolContractsChangeset(env deployment.Environment, c Configur // configureTokenPool creates all transactions required to configure the desired token pool on a chain, // either applying the transactions with the deployer key or returning an MCMS proposal. func configureTokenPool( + ctx context.Context, opts *bind.TransactOpts, chains map[uint64]deployment.Chain, state CCIPOnChainState, @@ -202,7 +203,7 @@ func configureTokenPool( ) error { poolUpdate := config.PoolUpdates[chainSelector] chain := chains[chainSelector] - tokenPool, _, tokenConfig, err := getTokenStateFromPool(config.TokenSymbol, poolUpdate.Type, poolUpdate.Version, chain, state.Chains[chainSelector]) + tokenPool, _, tokenConfig, err := getTokenStateFromPool(ctx, config.TokenSymbol, poolUpdate.Type, poolUpdate.Version, chain, state.Chains[chainSelector]) if err != nil { return fmt.Errorf("failed to get token state from pool with address %s on %s: %w", tokenPool.Address(), chain.String(), err) } @@ -223,7 +224,7 @@ func configureTokenPool( } remoteChain := chains[remoteChainSelector] remotePoolUpdate := config.PoolUpdates[remoteChainSelector] - remoteTokenPool, remoteTokenAddress, remoteTokenConfig, err := getTokenStateFromPool(config.TokenSymbol, remotePoolUpdate.Type, remotePoolUpdate.Version, remoteChain, state.Chains[remoteChainSelector]) + remoteTokenPool, remoteTokenAddress, remoteTokenConfig, err := getTokenStateFromPool(ctx, config.TokenSymbol, remotePoolUpdate.Type, remotePoolUpdate.Version, remoteChain, state.Chains[remoteChainSelector]) if err != nil { return fmt.Errorf("failed to get token state from pool with address %s on %s: %w", tokenPool.Address(), chain.String(), err) } @@ -297,6 +298,7 @@ func configureTokenPool( // getTokenStateFromPool fetches the token config from the registry given the pool address func getTokenStateFromPool( + ctx context.Context, symbol TokenSymbol, poolType deployment.ContractType, version semver.Version, @@ -307,7 +309,7 @@ func getTokenStateFromPool( if err != nil { return nil, utils.ZeroAddress, token_admin_registry.TokenAdminRegistryTokenConfig{}, fmt.Errorf("failed to find token pool on %s with symbol %s, type %s, and version %s: %w", chain.String(), symbol, poolType, version, err) } - tokenAddress, err := tokenPool.GetToken(nil) + tokenAddress, err := tokenPool.GetToken(&bind.CallOpts{Context: ctx}) if err != nil { return nil, utils.ZeroAddress, token_admin_registry.TokenAdminRegistryTokenConfig{}, fmt.Errorf("failed to get token from pool with address %s on %s: %w", tokenPool.Address(), chain.String(), err) } diff --git a/deployment/ccip/changeset/cs_configure_token_pools_test.go b/deployment/ccip/changeset/cs_configure_token_pools_test.go index a2cf5e97459..2aa50bdc38a 100644 --- a/deployment/ccip/changeset/cs_configure_token_pools_test.go +++ b/deployment/ccip/changeset/cs_configure_token_pools_test.go @@ -456,18 +456,61 @@ func TestValidateConfigureTokenPoolContracts(t *testing.T) { }, }, { - Changeset: commonchangeset.WrapChangeSet(changeset.ConfigureTokenAdminRegistryChangeset), - Config: changeset.ConfigureTokenAdminRegistryConfig{ - TokenSymbol: testhelpers.TestTokenSymbol, - MCMS: mcmsConfig, - RegistryUpdates: map[uint64]changeset.RegistryConfig{ - selectorA: { - Type: changeset.LockReleaseTokenPool, - Version: deployment.Version1_5_1, + Changeset: commonchangeset.WrapChangeSet(changeset.ProposeAdminRoleChangeset), + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.LockReleaseTokenPool, + Version: deployment.Version1_5_1, + }, }, - selectorB: { - Type: changeset.LockReleaseTokenPool, - Version: deployment.Version1_5_1, + selectorB: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.LockReleaseTokenPool, + Version: deployment.Version1_5_1, + }, + }, + }, + }, + }, + { + Changeset: commonchangeset.WrapChangeSet(changeset.AcceptAdminRoleChangeset), + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.LockReleaseTokenPool, + Version: deployment.Version1_5_1, + }, + }, + selectorB: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.LockReleaseTokenPool, + Version: deployment.Version1_5_1, + }, + }, + }, + }, + }, + { + Changeset: commonchangeset.WrapChangeSet(changeset.SetPoolChangeset), + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.LockReleaseTokenPool, + Version: deployment.Version1_5_1, + }, + }, + selectorB: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.LockReleaseTokenPool, + Version: deployment.Version1_5_1, + }, }, }, }, diff --git a/deployment/ccip/changeset/cs_propose_admin_role.go b/deployment/ccip/changeset/cs_propose_admin_role.go new file mode 100644 index 00000000000..4609391dec8 --- /dev/null +++ b/deployment/ccip/changeset/cs_propose_admin_role.go @@ -0,0 +1,66 @@ +package changeset + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/token_admin_registry" +) + +var _ deployment.ChangeSet[TokenAdminRegistryChangesetConfig] = ProposeAdminRoleChangeset + +func validateProposeAdminRole( + config token_admin_registry.TokenAdminRegistryTokenConfig, + sender common.Address, + externalAdmin common.Address, + symbol TokenSymbol, + chain deployment.Chain, +) error { + // To propose ourselves as admin of the token, two things must be true. + // 1. We own the token admin registry + // 2. An admin does not exist exist yet + // We've already validated that we own the registry during ValidateOwnership, so we only need to check the 2nd condition + if config.Administrator != utils.ZeroAddress { + return fmt.Errorf("unable to propose %s as admin of %s token on %s: token already has an administrator (%s)", sender, symbol, chain, config.Administrator) + } + return nil +} + +// ProposeAdminRoleChangeset proposes admin rights for tokens on the token admin registry. +func ProposeAdminRoleChangeset(env deployment.Environment, c TokenAdminRegistryChangesetConfig) (deployment.ChangesetOutput, error) { + if err := c.Validate(env, validateProposeAdminRole); err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("invalid TokenAdminRegistryChangesetConfig: %w", err) + } + state, err := LoadOnchainState(env) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to load onchain state: %w", err) + } + deployerGroup := NewDeployerGroup(env, state, c.MCMS) + + for chainSelector, tokenSymbolToPoolInfo := range c.Pools { + chain := env.Chains[chainSelector] + chainState := state.Chains[chainSelector] + opts, err := deployerGroup.GetDeployer(chainSelector) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to get deployer for %s", chain) + } + sender := chainState.Timelock.Address() + if c.MCMS == nil { + sender = chain.DeployerKey.From + } + for symbol, poolInfo := range tokenSymbolToPoolInfo { + _, tokenAddress, err := poolInfo.GetPoolAndTokenAddress(env.GetContext(), symbol, chain, chainState) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to get state of %s token on chain %s: %w", symbol, chain, err) + } + _, err = chainState.TokenAdminRegistry.ProposeAdministrator(opts, tokenAddress, sender) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create proposeAdministrator transaction for %s on %s registry: %w", symbol, chain, err) + } + } + } + + return deployerGroup.Enact("propose admin role for tokens on token admin registries") +} diff --git a/deployment/ccip/changeset/cs_propose_admin_role_test.go b/deployment/ccip/changeset/cs_propose_admin_role_test.go new file mode 100644 index 00000000000..3be86e056a8 --- /dev/null +++ b/deployment/ccip/changeset/cs_propose_admin_role_test.go @@ -0,0 +1,229 @@ +package changeset_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +func TestProposeAdminRoleChangeset_Validations(t *testing.T) { + t.Parallel() + + e, selectorA, _, tokens, timelockContracts := testhelpers.SetupTwoChainEnvironmentWithTokens(t, logger.TestLogger(t), true) + + e = testhelpers.DeployTestTokenPools(t, e, map[uint64]changeset.DeployTokenPoolInput{ + selectorA: { + Type: changeset.BurnMintTokenPool, + TokenAddress: tokens[selectorA].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + }, + }, true) + + mcmsConfig := &changeset.MCMSConfig{ + MinDelay: 0 * time.Second, + } + + // We want an administrator to exist to force failure in the last test + e, err := commonchangeset.ApplyChangesets(t, e, timelockContracts, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(changeset.ProposeAdminRoleChangeset), + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + }, + }, + }, + { + Changeset: commonchangeset.WrapChangeSet(changeset.AcceptAdminRoleChangeset), + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + }, + }, + }, + }) + require.NoError(t, err) + + tests := []struct { + Config changeset.TokenAdminRegistryChangesetConfig + ErrStr string + Msg string + }{ + { + Msg: "Chain selector is invalid", + Config: changeset.TokenAdminRegistryChangesetConfig{ + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + 0: map[changeset.TokenSymbol]changeset.TokenPoolInfo{}, + }, + }, + ErrStr: "failed to validate chain selector 0", + }, + { + Msg: "Chain selector doesn't exist in environment", + Config: changeset.TokenAdminRegistryChangesetConfig{ + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + 5009297550715157269: map[changeset.TokenSymbol]changeset.TokenPoolInfo{}, + }, + }, + ErrStr: "does not exist in environment", + }, + { + Msg: "Ownership validation failure", + Config: changeset.TokenAdminRegistryChangesetConfig{ + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{}, + }, + }, + ErrStr: "token admin registry failed ownership validation", + }, + { + Msg: "Invalid pool type", + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: "InvalidType", + Version: deployment.Version1_5_1, + }, + }, + }, + }, + ErrStr: "InvalidType is not a known token pool type", + }, + { + Msg: "Invalid pool version", + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_0_0, + }, + }, + }, + }, + ErrStr: "1.0.0 is not a known token pool version", + }, + { + Msg: "Admin already exists", + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + }, + }, + ErrStr: "token already has an administrator", + }, + } + + for _, test := range tests { + t.Run(test.Msg, func(t *testing.T) { + _, err = commonchangeset.ApplyChangesets(t, e, timelockContracts, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(changeset.ProposeAdminRoleChangeset), + Config: test.Config, + }, + }) + require.Error(t, err) + require.ErrorContains(t, err, test.ErrStr) + }) + } +} + +func TestProposeAdminRoleChangeset_Execution(t *testing.T) { + for _, mcmsConfig := range []*changeset.MCMSConfig{nil, &changeset.MCMSConfig{MinDelay: 0 * time.Second}} { + msg := "Propose admin role with MCMS" + if mcmsConfig == nil { + msg = "Propose admin role without MCMS" + } + + t.Run(msg, func(t *testing.T) { + e, selectorA, selectorB, tokens, timelockContracts := testhelpers.SetupTwoChainEnvironmentWithTokens(t, logger.TestLogger(t), mcmsConfig != nil) + + e = testhelpers.DeployTestTokenPools(t, e, map[uint64]changeset.DeployTokenPoolInput{ + selectorA: { + Type: changeset.BurnMintTokenPool, + TokenAddress: tokens[selectorA].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + }, + selectorB: { + Type: changeset.BurnMintTokenPool, + TokenAddress: tokens[selectorB].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + }, + }, mcmsConfig != nil) + + state, err := changeset.LoadOnchainState(e) + require.NoError(t, err) + + registryOnA := state.Chains[selectorA].TokenAdminRegistry + registryOnB := state.Chains[selectorB].TokenAdminRegistry + + e, err = commonchangeset.ApplyChangesets(t, e, timelockContracts, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(changeset.ProposeAdminRoleChangeset), + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + selectorB: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + }, + }, + }, + }) + require.NoError(t, err) + + configOnA, err := registryOnA.GetTokenConfig(nil, tokens[selectorA].Address) + require.NoError(t, err) + if mcmsConfig != nil { + require.Equal(t, state.Chains[selectorA].Timelock.Address(), configOnA.PendingAdministrator) + } else { + require.Equal(t, e.Chains[selectorA].DeployerKey.From, configOnA.PendingAdministrator) + } + + configOnB, err := registryOnB.GetTokenConfig(nil, tokens[selectorB].Address) + require.NoError(t, err) + if mcmsConfig != nil { + require.Equal(t, state.Chains[selectorB].Timelock.Address(), configOnB.PendingAdministrator) + } else { + require.Equal(t, e.Chains[selectorB].DeployerKey.From, configOnB.PendingAdministrator) + } + }) + } +} diff --git a/deployment/ccip/changeset/cs_set_pool.go b/deployment/ccip/changeset/cs_set_pool.go new file mode 100644 index 00000000000..9ca87d98ab7 --- /dev/null +++ b/deployment/ccip/changeset/cs_set_pool.go @@ -0,0 +1,58 @@ +package changeset + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/token_admin_registry" +) + +var _ deployment.ChangeSet[TokenAdminRegistryChangesetConfig] = SetPoolChangeset + +func validateSetPool( + config token_admin_registry.TokenAdminRegistryTokenConfig, + sender common.Address, + externalAdmin common.Address, + symbol TokenSymbol, + chain deployment.Chain, +) error { + // We must be the administrator + if config.Administrator != sender { + return fmt.Errorf("unable to set pool for %s token on %s: %s is not the administrator (%s)", symbol, chain, sender, config.Administrator) + } + return nil +} + +// SetPoolChangeset sets pools for tokens on the token admin registry. +func SetPoolChangeset(env deployment.Environment, c TokenAdminRegistryChangesetConfig) (deployment.ChangesetOutput, error) { + if err := c.Validate(env, validateSetPool); err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("invalid TokenAdminRegistryChangesetConfig: %w", err) + } + state, err := LoadOnchainState(env) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to load onchain state: %w", err) + } + deployerGroup := NewDeployerGroup(env, state, c.MCMS) + + for chainSelector, tokenSymbolToPoolInfo := range c.Pools { + chain := env.Chains[chainSelector] + chainState := state.Chains[chainSelector] + opts, err := deployerGroup.GetDeployer(chainSelector) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to get deployer for %s", chain) + } + for symbol, poolInfo := range tokenSymbolToPoolInfo { + tokenPool, tokenAddress, err := poolInfo.GetPoolAndTokenAddress(env.GetContext(), symbol, chain, chainState) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to get state of %s token on chain %s: %w", symbol, chain, err) + } + _, err = chainState.TokenAdminRegistry.SetPool(opts, tokenAddress, tokenPool.Address()) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create setPool transaction for %s on %s registry: %w", symbol, chain, err) + } + } + } + + return deployerGroup.Enact("set pool for tokens on token admin registries") +} diff --git a/deployment/ccip/changeset/cs_set_pool_test.go b/deployment/ccip/changeset/cs_set_pool_test.go new file mode 100644 index 00000000000..b087405735d --- /dev/null +++ b/deployment/ccip/changeset/cs_set_pool_test.go @@ -0,0 +1,228 @@ +package changeset_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +func TestSetPoolChangeset_Validations(t *testing.T) { + t.Parallel() + + e, selectorA, _, tokens, timelockContracts := testhelpers.SetupTwoChainEnvironmentWithTokens(t, logger.TestLogger(t), true) + + e = testhelpers.DeployTestTokenPools(t, e, map[uint64]changeset.DeployTokenPoolInput{ + selectorA: { + Type: changeset.BurnMintTokenPool, + TokenAddress: tokens[selectorA].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + }, + }, true) + + mcmsConfig := &changeset.MCMSConfig{ + MinDelay: 0 * time.Second, + } + + tests := []struct { + Config changeset.TokenAdminRegistryChangesetConfig + ErrStr string + Msg string + }{ + { + Msg: "Chain selector is invalid", + Config: changeset.TokenAdminRegistryChangesetConfig{ + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + 0: map[changeset.TokenSymbol]changeset.TokenPoolInfo{}, + }, + }, + ErrStr: "failed to validate chain selector 0", + }, + { + Msg: "Chain selector doesn't exist in environment", + Config: changeset.TokenAdminRegistryChangesetConfig{ + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + 5009297550715157269: map[changeset.TokenSymbol]changeset.TokenPoolInfo{}, + }, + }, + ErrStr: "does not exist in environment", + }, + { + Msg: "Ownership validation failure", + Config: changeset.TokenAdminRegistryChangesetConfig{ + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{}, + }, + }, + ErrStr: "token admin registry failed ownership validation", + }, + { + Msg: "Invalid pool type", + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: "InvalidType", + Version: deployment.Version1_5_1, + }, + }, + }, + }, + ErrStr: "InvalidType is not a known token pool type", + }, + { + Msg: "Invalid pool version", + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_0_0, + }, + }, + }, + }, + ErrStr: "1.0.0 is not a known token pool version", + }, + { + Msg: "Not admin", + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + }, + }, + ErrStr: "is not the administrator", + }, + } + + for _, test := range tests { + t.Run(test.Msg, func(t *testing.T) { + _, err := commonchangeset.ApplyChangesets(t, e, timelockContracts, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(changeset.SetPoolChangeset), + Config: test.Config, + }, + }) + require.Error(t, err) + require.ErrorContains(t, err, test.ErrStr) + }) + } +} + +func TestSetPoolChangeset_Execution(t *testing.T) { + for _, mcmsConfig := range []*changeset.MCMSConfig{nil, &changeset.MCMSConfig{MinDelay: 0 * time.Second}} { + msg := "Set pool with MCMS" + if mcmsConfig == nil { + msg = "Set pool without MCMS" + } + + t.Run(msg, func(t *testing.T) { + e, selectorA, selectorB, tokens, timelockContracts := testhelpers.SetupTwoChainEnvironmentWithTokens(t, logger.TestLogger(t), mcmsConfig != nil) + + e = testhelpers.DeployTestTokenPools(t, e, map[uint64]changeset.DeployTokenPoolInput{ + selectorA: { + Type: changeset.BurnMintTokenPool, + TokenAddress: tokens[selectorA].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + }, + selectorB: { + Type: changeset.BurnMintTokenPool, + TokenAddress: tokens[selectorB].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + }, + }, mcmsConfig != nil) + + state, err := changeset.LoadOnchainState(e) + require.NoError(t, err) + + registryOnA := state.Chains[selectorA].TokenAdminRegistry + registryOnB := state.Chains[selectorB].TokenAdminRegistry + + e, err = commonchangeset.ApplyChangesets(t, e, timelockContracts, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(changeset.ProposeAdminRoleChangeset), + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + selectorB: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + }, + }, + }, + { + Changeset: commonchangeset.WrapChangeSet(changeset.AcceptAdminRoleChangeset), + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + selectorB: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + }, + }, + }, + { + Changeset: commonchangeset.WrapChangeSet(changeset.SetPoolChangeset), + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + selectorB: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + }, + }, + }, + }) + require.NoError(t, err) + + configOnA, err := registryOnA.GetTokenConfig(nil, tokens[selectorA].Address) + require.NoError(t, err) + require.Equal(t, state.Chains[selectorA].BurnMintTokenPools[testhelpers.TestTokenSymbol][deployment.Version1_5_1].Address(), configOnA.TokenPool) + + configOnB, err := registryOnB.GetTokenConfig(nil, tokens[selectorB].Address) + require.NoError(t, err) + require.Equal(t, state.Chains[selectorB].BurnMintTokenPools[testhelpers.TestTokenSymbol][deployment.Version1_5_1].Address(), configOnB.TokenPool) + }) + } +} diff --git a/deployment/ccip/changeset/cs_transfer_admin_role.go b/deployment/ccip/changeset/cs_transfer_admin_role.go new file mode 100644 index 00000000000..23937fbc00b --- /dev/null +++ b/deployment/ccip/changeset/cs_transfer_admin_role.go @@ -0,0 +1,63 @@ +package changeset + +import ( + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/token_admin_registry" +) + +var _ deployment.ChangeSet[TokenAdminRegistryChangesetConfig] = TransferAdminRoleChangeset + +func validateTransferAdminRole( + config token_admin_registry.TokenAdminRegistryTokenConfig, + sender common.Address, + externalAdmin common.Address, + symbol TokenSymbol, + chain deployment.Chain, +) error { + if externalAdmin == utils.ZeroAddress { + return errors.New("external admin must be defined") + } + // We must be the administrator + if config.Administrator != sender { + return fmt.Errorf("unable to transfer admin role for %s token on %s: %s is not the administrator (%s)", symbol, chain, sender, config.Administrator) + } + return nil +} + +// TransferAdminRoleChangeset transfers the admin role for tokens on the token admin registry to 3rd parties. +func TransferAdminRoleChangeset(env deployment.Environment, c TokenAdminRegistryChangesetConfig) (deployment.ChangesetOutput, error) { + if err := c.Validate(env, validateTransferAdminRole); err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("invalid TokenAdminRegistryChangesetConfig: %w", err) + } + state, err := LoadOnchainState(env) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to load onchain state: %w", err) + } + deployerGroup := NewDeployerGroup(env, state, c.MCMS) + + for chainSelector, tokenSymbolToPoolInfo := range c.Pools { + chain := env.Chains[chainSelector] + chainState := state.Chains[chainSelector] + opts, err := deployerGroup.GetDeployer(chainSelector) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to get deployer for %s", chain) + } + for symbol, poolInfo := range tokenSymbolToPoolInfo { + _, tokenAddress, err := poolInfo.GetPoolAndTokenAddress(env.GetContext(), symbol, chain, chainState) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to get state of %s token on chain %s: %w", symbol, chain, err) + } + _, err = chainState.TokenAdminRegistry.TransferAdminRole(opts, tokenAddress, poolInfo.ExternalAdmin) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create transferAdminRole transaction for %s on %s registry: %w", symbol, chain, err) + } + } + } + + return deployerGroup.Enact("transfer admin role for tokens on token admin registries") +} diff --git a/deployment/ccip/changeset/cs_transfer_admin_role_test.go b/deployment/ccip/changeset/cs_transfer_admin_role_test.go new file mode 100644 index 00000000000..03dabe0f135 --- /dev/null +++ b/deployment/ccip/changeset/cs_transfer_admin_role_test.go @@ -0,0 +1,249 @@ +package changeset_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +func TestTransferAdminRoleChangeset_Validations(t *testing.T) { + t.Parallel() + + e, selectorA, _, tokens, timelockContracts := testhelpers.SetupTwoChainEnvironmentWithTokens(t, logger.TestLogger(t), true) + + e = testhelpers.DeployTestTokenPools(t, e, map[uint64]changeset.DeployTokenPoolInput{ + selectorA: { + Type: changeset.BurnMintTokenPool, + TokenAddress: tokens[selectorA].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + }, + }, true) + + mcmsConfig := &changeset.MCMSConfig{ + MinDelay: 0 * time.Second, + } + + tests := []struct { + Config changeset.TokenAdminRegistryChangesetConfig + ErrStr string + Msg string + }{ + { + Msg: "Chain selector is invalid", + Config: changeset.TokenAdminRegistryChangesetConfig{ + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + 0: map[changeset.TokenSymbol]changeset.TokenPoolInfo{}, + }, + }, + ErrStr: "failed to validate chain selector 0", + }, + { + Msg: "Chain selector doesn't exist in environment", + Config: changeset.TokenAdminRegistryChangesetConfig{ + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + 5009297550715157269: map[changeset.TokenSymbol]changeset.TokenPoolInfo{}, + }, + }, + ErrStr: "does not exist in environment", + }, + { + Msg: "Ownership validation failure", + Config: changeset.TokenAdminRegistryChangesetConfig{ + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{}, + }, + }, + ErrStr: "token admin registry failed ownership validation", + }, + { + Msg: "Invalid pool type", + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: "InvalidType", + Version: deployment.Version1_5_1, + }, + }, + }, + }, + ErrStr: "InvalidType is not a known token pool type", + }, + { + Msg: "Invalid pool version", + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_0_0, + }, + }, + }, + }, + ErrStr: "1.0.0 is not a known token pool version", + }, + { + Msg: "External admin undefined", + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + }, + }, + ErrStr: "external admin must be defined", + }, + { + Msg: "Not admin", + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + ExternalAdmin: utils.RandomAddress(), + }, + }, + }, + }, + ErrStr: "is not the administrator", + }, + } + + for _, test := range tests { + t.Run(test.Msg, func(t *testing.T) { + _, err := commonchangeset.ApplyChangesets(t, e, timelockContracts, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(changeset.TransferAdminRoleChangeset), + Config: test.Config, + }, + }) + require.Error(t, err) + require.ErrorContains(t, err, test.ErrStr) + }) + } +} + +func TestTransferAdminRoleChangeset_Execution(t *testing.T) { + for _, mcmsConfig := range []*changeset.MCMSConfig{nil, &changeset.MCMSConfig{MinDelay: 0 * time.Second}} { + msg := "Transfer admin role with MCMS" + if mcmsConfig == nil { + msg = "Transfer admin role without MCMS" + } + + t.Run(msg, func(t *testing.T) { + e, selectorA, selectorB, tokens, timelockContracts := testhelpers.SetupTwoChainEnvironmentWithTokens(t, logger.TestLogger(t), mcmsConfig != nil) + externalAdminA := utils.RandomAddress() + externalAdminB := utils.RandomAddress() + + e = testhelpers.DeployTestTokenPools(t, e, map[uint64]changeset.DeployTokenPoolInput{ + selectorA: { + Type: changeset.BurnMintTokenPool, + TokenAddress: tokens[selectorA].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + }, + selectorB: { + Type: changeset.BurnMintTokenPool, + TokenAddress: tokens[selectorB].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + }, + }, mcmsConfig != nil) + + state, err := changeset.LoadOnchainState(e) + require.NoError(t, err) + + registryOnA := state.Chains[selectorA].TokenAdminRegistry + registryOnB := state.Chains[selectorB].TokenAdminRegistry + + e, err = commonchangeset.ApplyChangesets(t, e, timelockContracts, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(changeset.ProposeAdminRoleChangeset), + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + selectorB: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + }, + }, + }, + { + Changeset: commonchangeset.WrapChangeSet(changeset.AcceptAdminRoleChangeset), + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + selectorB: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + }, + }, + }, + }, + }, + { + Changeset: commonchangeset.WrapChangeSet(changeset.TransferAdminRoleChangeset), + Config: changeset.TokenAdminRegistryChangesetConfig{ + MCMS: mcmsConfig, + Pools: map[uint64]map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + selectorA: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + ExternalAdmin: externalAdminA, + }, + }, + selectorB: map[changeset.TokenSymbol]changeset.TokenPoolInfo{ + testhelpers.TestTokenSymbol: { + Type: changeset.BurnMintTokenPool, + Version: deployment.Version1_5_1, + ExternalAdmin: externalAdminB, + }, + }, + }, + }, + }, + }) + require.NoError(t, err) + + configOnA, err := registryOnA.GetTokenConfig(nil, tokens[selectorA].Address) + require.NoError(t, err) + require.Equal(t, externalAdminA, configOnA.PendingAdministrator) + + configOnB, err := registryOnB.GetTokenConfig(nil, tokens[selectorB].Address) + require.NoError(t, err) + require.Equal(t, externalAdminB, configOnB.PendingAdministrator) + }) + } +} diff --git a/deployment/ccip/changeset/deployer_group.go b/deployment/ccip/changeset/deployer_group.go index ed168d5f342..a552d6d9f9b 100644 --- a/deployment/ccip/changeset/deployer_group.go +++ b/deployment/ccip/changeset/deployer_group.go @@ -23,11 +23,10 @@ type MCMSConfig struct { } type DeployerGroup struct { - e deployment.Environment - state CCIPOnChainState - mcmConfig *MCMSConfig - transactions map[uint64][]*types.Transaction - mcmsOperationOffsets map[uint64]uint64 + e deployment.Environment + state CCIPOnChainState + mcmConfig *MCMSConfig + transactions map[uint64][]*types.Transaction } // DeployerGroup is an abstraction that lets developers write their changeset @@ -44,11 +43,10 @@ type DeployerGroup struct { // deployerGroup.Enact("Curse RMNRemote") func NewDeployerGroup(e deployment.Environment, state CCIPOnChainState, mcmConfig *MCMSConfig) *DeployerGroup { return &DeployerGroup{ - e: e, - mcmConfig: mcmConfig, - state: state, - transactions: make(map[uint64][]*types.Transaction), - mcmsOperationOffsets: make(map[uint64]uint64), + e: e, + mcmConfig: mcmConfig, + state: state, + transactions: make(map[uint64][]*types.Transaction), } } @@ -85,19 +83,18 @@ func (d *DeployerGroup) GetDeployer(chain uint64) (*bind.TransactOpts, error) { } oldSigner := sim.Signer - sim.Signer = func(a common.Address, t *types.Transaction) (*types.Transaction, error) { - // Fetch the starting nonce - var startingNonce *big.Int - if txOpts.Nonce != nil { - startingNonce = new(big.Int).Set(txOpts.Nonce) - } else { - nonce, err := d.e.Chains[chain].Client.PendingNonceAt(context.Background(), txOpts.From) - if err != nil { - return nil, fmt.Errorf("could not get nonce for deployer: %w", err) - } - startingNonce = new(big.Int).SetUint64(nonce) + var startingNonce *big.Int + if txOpts.Nonce != nil { + startingNonce = new(big.Int).Set(txOpts.Nonce) + } else { + nonce, err := d.e.Chains[chain].Client.PendingNonceAt(context.Background(), txOpts.From) + if err != nil { + return nil, fmt.Errorf("could not get nonce for deployer: %w", err) } + startingNonce = new(big.Int).SetUint64(nonce) + } + sim.Signer = func(a common.Address, t *types.Transaction) (*types.Transaction, error) { // Update the nonce to consider the transactions that have been sent sim.Nonce = big.NewInt(0).Add(startingNonce, big.NewInt(int64(len(d.transactions[chain]))+1)) @@ -112,8 +109,6 @@ func (d *DeployerGroup) GetDeployer(chain uint64) (*bind.TransactOpts, error) { } func (d *DeployerGroup) Enact(deploymentDescription string) (deployment.ChangesetOutput, error) { - defer d.reset() - if d.mcmConfig != nil { return d.enactMcms(deploymentDescription) } @@ -121,16 +116,6 @@ func (d *DeployerGroup) Enact(deploymentDescription string) (deployment.Changese return d.enactDeployer() } -func (d *DeployerGroup) reset() { - for selector := range d.transactions { - if _, ok := d.mcmsOperationOffsets[selector]; !ok { - d.mcmsOperationOffsets[selector] = 0 - } - d.mcmsOperationOffsets[selector] += 1 - } - d.transactions = make(map[uint64][]*types.Transaction) -} - func (d *DeployerGroup) enactMcms(deploymentDescription string) (deployment.ChangesetOutput, error) { batches := make([]timelock.BatchChainOperation, 0) for selector, txs := range d.transactions { @@ -152,10 +137,9 @@ func (d *DeployerGroup) enactMcms(deploymentDescription string) (deployment.Chan proposerMCMSes := BuildProposerPerChain(d.e, d.state) - prop, err := proposalutils.BuildProposalFromBatchesWithOffsets( + prop, err := proposalutils.BuildProposalFromBatches( timelocksPerChain, proposerMCMSes, - d.mcmsOperationOffsets, batches, deploymentDescription, d.mcmConfig.MinDelay, diff --git a/deployment/ccip/changeset/token_pools.go b/deployment/ccip/changeset/token_pools.go index c0da681c8ca..91f5c3cb674 100644 --- a/deployment/ccip/changeset/token_pools.go +++ b/deployment/ccip/changeset/token_pools.go @@ -1,6 +1,7 @@ package changeset import ( + "context" "fmt" "github.com/Masterminds/semver/v3" @@ -8,6 +9,9 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/chainlink/deployment" + commoncs "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/token_admin_registry" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/token_pool" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/erc20" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" @@ -26,6 +30,67 @@ var tokenPoolVersions map[semver.Version]struct{} = map[semver.Version]struct{}{ deployment.Version1_5_1: struct{}{}, } +// TokenPoolInfo defines the type & version of a token pool, along with an optional external administrator. +type TokenPoolInfo struct { + // Type is the type of the token pool. + Type deployment.ContractType + // Version is the version of the token pool. + Version semver.Version + // ExternalAdmin is the external administrator of the token pool on the registry. + ExternalAdmin common.Address +} + +func (t TokenPoolInfo) Validate() error { + // Ensure that the inputted type is known + if _, ok := tokenPoolTypes[t.Type]; !ok { + return fmt.Errorf("%s is not a known token pool type", t.Type) + } + + // Ensure that the inputted version is known + if _, ok := tokenPoolVersions[t.Version]; !ok { + return fmt.Errorf("%s is not a known token pool version", t.Version) + } + + return nil +} + +// GetConfigOnRegistry fetches the token's config on the token admin registry. +func (t TokenPoolInfo) GetConfigOnRegistry( + ctx context.Context, + symbol TokenSymbol, + chain deployment.Chain, + state CCIPChainState, +) (token_admin_registry.TokenAdminRegistryTokenConfig, error) { + _, tokenAddress, err := t.GetPoolAndTokenAddress(ctx, symbol, chain, state) + if err != nil { + return token_admin_registry.TokenAdminRegistryTokenConfig{}, fmt.Errorf("failed to get token pool and token address for %s token on %s: %w", symbol, chain, err) + } + tokenAdminRegistry := state.TokenAdminRegistry + tokenConfig, err := tokenAdminRegistry.GetTokenConfig(&bind.CallOpts{Context: ctx}, tokenAddress) + if err != nil { + return token_admin_registry.TokenAdminRegistryTokenConfig{}, fmt.Errorf("failed to get config of %s token with address %s from registry on %s: %w", symbol, tokenAddress, chain, err) + } + return tokenConfig, nil +} + +// GetPoolAndTokenAddress returns pool bindings and the token address. +func (t TokenPoolInfo) GetPoolAndTokenAddress( + ctx context.Context, + symbol TokenSymbol, + chain deployment.Chain, + state CCIPChainState, +) (*token_pool.TokenPool, common.Address, error) { + tokenPool, err := getTokenPoolFromSymbolTypeAndVersion(state, chain, symbol, t.Type, t.Version) + if err != nil { + return nil, utils.ZeroAddress, fmt.Errorf("failed to find token pool on %s with symbol %s, type %s, and version %s: %w", chain, symbol, t.Type, t.Version, err) + } + tokenAddress, err := tokenPool.GetToken(&bind.CallOpts{Context: ctx}) + if err != nil { + return nil, utils.ZeroAddress, fmt.Errorf("failed to get token from pool with address %s on %s: %w", tokenPool.Address(), chain, err) + } + return tokenPool, tokenAddress, nil +} + // tokenPool defines behavior common to all token pools. type tokenPool interface { GetToken(opts *bind.CallOpts) (common.Address, error) @@ -163,3 +228,79 @@ func getTokenPoolFromSymbolTypeAndVersion( return nil, fmt.Errorf("failed to find token pool with symbol %s, type %s, and version %s", symbol, poolType, version) } + +// TokenAdminRegistryChangesetConfig defines a config for all token admin registry actions. +type TokenAdminRegistryChangesetConfig struct { + // MCMS defines the delay to use for Timelock (if absent, the changeset will attempt to use the deployer key). + MCMS *MCMSConfig + // Pools defines the pools corresponding to the tokens we want to accept admin role for. + Pools map[uint64]map[TokenSymbol]TokenPoolInfo +} + +// validateTokenAdminRegistryChangeset validates all token admin registry changesets. +func (c TokenAdminRegistryChangesetConfig) Validate( + env deployment.Environment, + registryConfigCheck func( + config token_admin_registry.TokenAdminRegistryTokenConfig, + sender common.Address, + externalAdmin common.Address, + symbol TokenSymbol, + chain deployment.Chain, + ) error, +) error { + state, err := LoadOnchainState(env) + if err != nil { + return fmt.Errorf("failed to load onchain state: %w", err) + } + for chainSelector, symbolToPoolInfo := range c.Pools { + err := deployment.IsValidChainSelector(chainSelector) + if err != nil { + return fmt.Errorf("failed to validate chain selector %d: %w", chainSelector, err) + } + chain, ok := env.Chains[chainSelector] + if !ok { + return fmt.Errorf("chain with selector %d does not exist in environment", chainSelector) + } + chainState, ok := state.Chains[chainSelector] + if !ok { + return fmt.Errorf("%s does not exist in state", chain) + } + if tokenAdminRegistry := chainState.TokenAdminRegistry; tokenAdminRegistry == nil { + return fmt.Errorf("missing tokenAdminRegistry on %s", chain) + } + if c.MCMS != nil { + if timelock := chainState.Timelock; timelock == nil { + return fmt.Errorf("missing timelock on %s", chain) + } + if proposerMcm := chainState.ProposerMcm; proposerMcm == nil { + return fmt.Errorf("missing proposerMcm on %s", chain) + } + } + // Validate that the token admin registry is owned by the address that will be actioning the transactions (i.e. Timelock or deployer key) + if err := commoncs.ValidateOwnership(env.GetContext(), c.MCMS != nil, chain.DeployerKey.From, chainState.Timelock.Address(), chainState.TokenAdminRegistry); err != nil { + return fmt.Errorf("token admin registry failed ownership validation on %s: %w", chain, err) + } + for symbol, poolInfo := range symbolToPoolInfo { + if err := poolInfo.Validate(); err != nil { + return fmt.Errorf("failed to validate token pool info for %s token on chain %s: %w", symbol, chain, err) + } + + tokenConfigOnRegistry, err := poolInfo.GetConfigOnRegistry(env.GetContext(), symbol, chain, chainState) + if err != nil { + return fmt.Errorf("failed to get state of %s token on chain %s: %w", symbol, chain, err) + } + + fromAddress := chain.DeployerKey.From // "We" are either the deployer key or the timelock + if c.MCMS != nil { + fromAddress = chainState.Timelock.Address() + } + + err = registryConfigCheck(tokenConfigOnRegistry, fromAddress, poolInfo.ExternalAdmin, symbol, chain) + if err != nil { + return err + } + } + } + + return nil +}