Skip to content

Commit

Permalink
feat(deployment/mcms): update common changesets (#16249)
Browse files Browse the repository at this point in the history
- Added new v2 version of set_config_mcms to support new MCMS library
- Added new v2 version of transfer_to_mcms_with_timelock to support new MCMS library

JIRA: https://smartcontract-it.atlassian.net/browse/DPA-1361
  • Loading branch information
graham-chainlink authored Feb 7, 2025
1 parent 993e1c1 commit ba402ad
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 1 deletion.
78 changes: 77 additions & 1 deletion deployment/common/changeset/set_config_mcms.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ import (
"github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/mcms"
"github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock"
chain_selectors "github.com/smartcontractkit/chain-selectors"

"github.com/smartcontractkit/chainlink-common/pkg/logger"
mcmslib "github.com/smartcontractkit/mcms"
"github.com/smartcontractkit/mcms/sdk"
"github.com/smartcontractkit/mcms/sdk/evm"
mcmstypes "github.com/smartcontractkit/mcms/types"

"github.com/smartcontractkit/chainlink/deployment"
"github.com/smartcontractkit/chainlink/deployment/common/proposalutils"
Expand Down Expand Up @@ -157,6 +160,7 @@ func addTxsToProposalBatch(setConfigTxsChain setConfigTxs, chainSelector uint64,
}

// SetConfigMCMS sets the configuration of the MCMS contract on the chain identified by the chainSelector.
// Deprecated: Use SetConfigMCMSV2 instead.
func SetConfigMCMS(e deployment.Environment, cfg MCMSConfig) (deployment.ChangesetOutput, error) {
selectors := []uint64{}
lggr := e.Logger
Expand Down Expand Up @@ -206,3 +210,75 @@ func SetConfigMCMS(e deployment.Environment, cfg MCMSConfig) (deployment.Changes

return deployment.ChangesetOutput{}, nil
}

// SetConfigMCMSV2 is a reimplementation of SetConfigMCMS that uses the new MCMS library.
func SetConfigMCMSV2(e deployment.Environment, cfg MCMSConfig) (deployment.ChangesetOutput, error) {
selectors := []uint64{}
lggr := e.Logger
ctx := e.GetContext()
for chainSelector := range cfg.ConfigsPerChain {
selectors = append(selectors, chainSelector)
}
useMCMS := cfg.ProposalConfig != nil
err := cfg.Validate(e, selectors)
if err != nil {
return deployment.ChangesetOutput{}, err
}

var batches []mcmstypes.BatchOperation
timelockAddressesPerChain := map[uint64]string{}
inspectorPerChain := map[uint64]sdk.Inspector{}
proposerMcmsPerChain := map[uint64]string{}

mcmsStatePerChain, err := MaybeLoadMCMSWithTimelockState(e, selectors)
if err != nil {
return deployment.ChangesetOutput{}, err
}

for chainSelector, c := range cfg.ConfigsPerChain {
chain := e.Chains[chainSelector]
state := mcmsStatePerChain[chainSelector]
timelockAddressesPerChain[chainSelector] = state.Timelock.Address().Hex()
proposerMcmsPerChain[chainSelector] = state.ProposerMcm.Address().Hex()
inspectorPerChain[chainSelector] = evm.NewInspector(chain.Client)
setConfigTxsChain, err := setConfigPerRole(ctx, lggr, chain, c, state, useMCMS)
if err != nil {
return deployment.ChangesetOutput{}, err
}
if useMCMS {
batch := addTxsToProposalBatchV2(setConfigTxsChain, chainSelector, *state)
batches = append(batches, batch)
}
}

if useMCMS {
proposal, err := proposalutils.BuildProposalFromBatchesV2(e.GetContext(), timelockAddressesPerChain,
proposerMcmsPerChain, inspectorPerChain, batches, "Set config proposal", cfg.ProposalConfig.MinDelay)
if err != nil {
return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal from batch: %w", err)
}
lggr.Infow("SetConfigMCMS proposal created", "proposal", proposal)
return deployment.ChangesetOutput{MCMSTimelockProposals: []mcmslib.TimelockProposal{*proposal}}, nil
}

return deployment.ChangesetOutput{}, nil
}

func addTxsToProposalBatchV2(setConfigTxsChain setConfigTxs, chainSelector uint64, state MCMSWithTimelockState) mcmstypes.BatchOperation {
result := mcmstypes.BatchOperation{
ChainSelector: mcmstypes.ChainSelector(chainSelector),
Transactions: []mcmstypes.Transaction{},
}

result.Transactions = append(result.Transactions,
evm.NewTransaction(state.ProposerMcm.Address(),
setConfigTxsChain.proposerTx.Data(), big.NewInt(0), string(commontypes.ProposerManyChainMultisig), nil))

result.Transactions = append(result.Transactions, evm.NewTransaction(state.CancellerMcm.Address(),
setConfigTxsChain.cancellerTx.Data(), big.NewInt(0), string(commontypes.CancellerManyChainMultisig), nil))

result.Transactions = append(result.Transactions,
evm.NewTransaction(state.BypasserMcm.Address(),
setConfigTxsChain.bypasserTx.Data(), big.NewInt(0), string(commontypes.BypasserManyChainMultisig), nil))
return result
}
109 changes: 109 additions & 0 deletions deployment/common/changeset/set_config_mcms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,115 @@ func TestSetConfigMCMSVariants(t *testing.T) {
}
}

func TestSetConfigMCMSV2Variants(t *testing.T) {
// Add the timelock as a signer to check state changes
for _, tc := range []struct {
name string
changeSets func(mcmsState *commonchangeset.MCMSWithTimelockState, chainSel uint64, cfgProp, cfgCancel, cfgBypass config.Config) []commonchangeset.ChangesetApplication
}{
{
name: "MCMS disabled",
changeSets: func(mcmsState *commonchangeset.MCMSWithTimelockState, chainSel uint64, cfgProp, cfgCancel, cfgBypass config.Config) []commonchangeset.ChangesetApplication {
return []commonchangeset.ChangesetApplication{
{
Changeset: commonchangeset.WrapChangeSet(commonchangeset.SetConfigMCMSV2),
Config: commonchangeset.MCMSConfig{
ConfigsPerChain: map[uint64]commonchangeset.ConfigPerRole{
chainSel: {
Proposer: cfgProp,
Canceller: cfgCancel,
Bypasser: cfgBypass,
},
},
},
},
}
},
},
{
name: "MCMS enabled",
changeSets: func(mcmsState *commonchangeset.MCMSWithTimelockState, chainSel uint64, cfgProp, cfgCancel, cfgBypass config.Config) []commonchangeset.ChangesetApplication {
return []commonchangeset.ChangesetApplication{
{
Changeset: commonchangeset.WrapChangeSet(commonchangeset.TransferToMCMSWithTimelockV2),
Config: commonchangeset.TransferToMCMSWithTimelockConfig{
ContractsByChain: map[uint64][]common.Address{
chainSel: {mcmsState.ProposerMcm.Address(), mcmsState.BypasserMcm.Address(), mcmsState.CancellerMcm.Address()},
},
},
},
{
Changeset: commonchangeset.WrapChangeSet(commonchangeset.SetConfigMCMSV2),
Config: commonchangeset.MCMSConfig{
ProposalConfig: &commonchangeset.TimelockConfig{
MinDelay: 0,
},
ConfigsPerChain: map[uint64]commonchangeset.ConfigPerRole{
chainSel: {
Proposer: cfgProp,
Canceller: cfgCancel,
Bypasser: cfgBypass,
},
},
},
},
}
},
},
} {
t.Run(tc.name, func(t *testing.T) {
ctx := tests.Context(t)

env := setupSetConfigTestEnv(t)
chainSelector := env.AllChainSelectors()[0]
chain := env.Chains[chainSelector]
addrs, err := env.ExistingAddresses.AddressesForChain(chainSelector)
require.NoError(t, err)
require.Len(t, addrs, 6)

mcmsState, err := commonchangeset.MaybeLoadMCMSWithTimelockChainState(chain, addrs)
require.NoError(t, err)
timelockAddress := mcmsState.Timelock.Address()
cfgProposer := proposalutils.SingleGroupMCMS(t)
cfgProposer.Signers = append(cfgProposer.Signers, timelockAddress)
cfgProposer.Quorum = 2 // quorum should change to 2 out of 2 signers
timelockMap := map[uint64]*proposalutils.TimelockExecutionContracts{
chainSelector: {
Timelock: mcmsState.Timelock,
CallProxy: mcmsState.CallProxy,
},
}
cfgCanceller := proposalutils.SingleGroupMCMS(t)
cfgBypasser := proposalutils.SingleGroupMCMS(t)
cfgBypasser.Signers = append(cfgBypasser.Signers, timelockAddress)
cfgBypasser.Signers = append(cfgBypasser.Signers, mcmsState.ProposerMcm.Address())
cfgBypasser.Quorum = 3 // quorum should change to 3 out of 3 signers

// Set config on all 3 MCMS contracts
changesetsToApply := tc.changeSets(mcmsState, chainSelector, cfgProposer, cfgCanceller, cfgBypasser)
_, err = commonchangeset.ApplyChangesets(t, env, timelockMap, changesetsToApply)
require.NoError(t, err)

// Check new State
expected := cfgProposer.ToRawConfig()
opts := &bind.CallOpts{Context: ctx}
newConf, err := mcmsState.ProposerMcm.GetConfig(opts)
require.NoError(t, err)
require.Equal(t, expected, newConf)

expected = cfgBypasser.ToRawConfig()
newConf, err = mcmsState.BypasserMcm.GetConfig(opts)
require.NoError(t, err)
require.Equal(t, expected, newConf)

expected = cfgCanceller.ToRawConfig()
newConf, err = mcmsState.CancellerMcm.GetConfig(opts)
require.NoError(t, err)
require.Equal(t, expected, newConf)
})
}
}

func TestValidate(t *testing.T) {
env := setupSetConfigTestEnv(t)

Expand Down
71 changes: 71 additions & 0 deletions deployment/common/changeset/transfer_to_mcms_with_timelock.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package changeset

import (
"encoding/binary"
"encoding/json"
"fmt"
"math/big"
"time"
Expand All @@ -12,6 +13,10 @@ import (
owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers"
"github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/mcms"
"github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock"
mcmslib "github.com/smartcontractkit/mcms"
"github.com/smartcontractkit/mcms/sdk"
"github.com/smartcontractkit/mcms/sdk/evm"
mcmstypes "github.com/smartcontractkit/mcms/types"

"github.com/smartcontractkit/chainlink/deployment"
"github.com/smartcontractkit/chainlink/deployment/common/proposalutils"
Expand Down Expand Up @@ -84,6 +89,7 @@ var _ deployment.ChangeSet[TransferToMCMSWithTimelockConfig] = TransferToMCMSWit
// It assumes that DeployMCMSWithTimelock has already been run s.t.
// the timelock and mcmses exist on the chain and that the proposed addresses to transfer ownership
// are currently owned by the deployer key.
// Deprecated: Use TransferToMCMSWithTimelockV2 instead.
func TransferToMCMSWithTimelock(
e deployment.Environment,
cfg TransferToMCMSWithTimelockConfig,
Expand Down Expand Up @@ -144,6 +150,69 @@ func TransferToMCMSWithTimelock(
return deployment.ChangesetOutput{Proposals: []timelock.MCMSWithTimelockProposal{*proposal}}, nil
}

var _ deployment.ChangeSet[TransferToMCMSWithTimelockConfig] = TransferToMCMSWithTimelockV2

// TransferToMCMSWithTimelockV2 is a reimplementation of TransferToMCMSWithTimelock which uses the new MCMS library.
func TransferToMCMSWithTimelockV2(
e deployment.Environment,
cfg TransferToMCMSWithTimelockConfig,
) (deployment.ChangesetOutput, error) {
if err := cfg.Validate(e); err != nil {
return deployment.ChangesetOutput{}, err
}
batches := []mcmstypes.BatchOperation{}
timelockAddressByChain := make(map[uint64]string)
inspectorPerChain := map[uint64]sdk.Inspector{}
proposerAddressByChain := make(map[uint64]string)
for chainSelector, contracts := range cfg.ContractsByChain {
// Already validated that the timelock/proposer exists.
timelockAddr, _ := deployment.SearchAddressBook(e.ExistingAddresses, chainSelector, types.RBACTimelock)
proposerAddr, _ := deployment.SearchAddressBook(e.ExistingAddresses, chainSelector, types.ProposerManyChainMultisig)
timelockAddressByChain[chainSelector] = timelockAddr
proposerAddressByChain[chainSelector] = proposerAddr
inspectorPerChain[chainSelector] = evm.NewInspector(e.Chains[chainSelector].Client)

var ops []mcmstypes.Transaction
for _, contract := range contracts {
// Just using the ownership interface.
// Already validated is ownable.
owner, c, _ := LoadOwnableContract(contract, e.Chains[chainSelector].Client)
if owner.String() == timelockAddr {
// Already owned by timelock.
e.Logger.Infof("contract %s already owned by timelock", contract)
continue
}
tx, err := c.TransferOwnership(e.Chains[chainSelector].DeployerKey, common.HexToAddress(timelockAddr))
_, err = deployment.ConfirmIfNoError(e.Chains[chainSelector], tx, err)
if err != nil {
return deployment.ChangesetOutput{}, fmt.Errorf("failed to transfer ownership of contract %T: %w", contract, err)
}
tx, err = c.AcceptOwnership(deployment.SimTransactOpts())
if err != nil {
return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate accept ownership calldata of %s: %w", contract, err)
}
ops = append(ops, mcmstypes.Transaction{
To: contract.Hex(),
Data: tx.Data(),
AdditionalFields: json.RawMessage(`{"value": 0}`), // JSON-encoded `{"value": 0}`
})
}
batches = append(batches, mcmstypes.BatchOperation{
ChainSelector: mcmstypes.ChainSelector(chainSelector),
Transactions: ops,
})
}
proposal, err := proposalutils.BuildProposalFromBatchesV2(
e.GetContext(),
timelockAddressByChain, proposerAddressByChain, inspectorPerChain,
batches, "Transfer ownership to timelock", cfg.MinDelay)
if err != nil {
return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal from batch: %w, batches: %+v", err, batches)
}

return deployment.ChangesetOutput{MCMSTimelockProposals: []mcmslib.TimelockProposal{*proposal}}, nil
}

var _ deployment.ChangeSet[TransferToDeployerConfig] = TransferToDeployer

type TransferToDeployerConfig struct {
Expand Down Expand Up @@ -194,6 +263,7 @@ func TransferToDeployer(e deployment.Environment, cfg TransferToDeployerConfig)
if err != nil {
return deployment.ChangesetOutput{}, fmt.Errorf("error creating timelock executor proxy: %w", err)
}

tx, err = timelockExecutorProxy.ExecuteBatch(
e.Chains[cfg.ChainSel].DeployerKey, calls, [32]byte{}, salt)
if err != nil {
Expand All @@ -202,6 +272,7 @@ func TransferToDeployer(e deployment.Environment, cfg TransferToDeployerConfig)
if _, err = deployment.ConfirmIfNoErrorWithABI(e.Chains[cfg.ChainSel], tx, owner_helpers.RBACTimelockABI, err); err != nil {
return deployment.ChangesetOutput{}, err
}

e.Logger.Infof("executed transfer ownership to deployer key with tx %s", tx.Hash().Hex())

tx, err = ownable.AcceptOwnership(e.Chains[cfg.ChainSel].DeployerKey)
Expand Down
67 changes: 67 additions & 0 deletions deployment/common/changeset/transfer_to_mcms_with_timelock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,73 @@ func TestTransferToMCMSWithTimelock(t *testing.T) {
require.Equal(t, e.Chains[chain1].DeployerKey.From, o)
}

func TestTransferToMCMSWithTimelockV2(t *testing.T) {
lggr := logger.TestLogger(t)
e := memory.NewMemoryEnvironment(t, lggr, 0, memory.MemoryEnvironmentConfig{
Chains: 1,
Nodes: 1,
})
chain1 := e.AllChainSelectors()[0]
e, err := ApplyChangesets(t, e, nil, []ChangesetApplication{
{
Changeset: WrapChangeSet(DeployLinkToken),
Config: []uint64{chain1},
},
{
Changeset: WrapChangeSet(DeployMCMSWithTimelock),
Config: map[uint64]types.MCMSWithTimelockConfig{
chain1: proposalutils.SingleGroupTimelockConfig(t),
},
},
})
require.NoError(t, err)
addrs, err := e.ExistingAddresses.AddressesForChain(chain1)
require.NoError(t, err)
state, err := MaybeLoadMCMSWithTimelockChainState(e.Chains[chain1], addrs)
require.NoError(t, err)
link, err := MaybeLoadLinkTokenChainState(e.Chains[chain1], addrs)
require.NoError(t, err)
e, err = ApplyChangesets(t, e, map[uint64]*proposalutils.TimelockExecutionContracts{
chain1: {
Timelock: state.Timelock,
CallProxy: state.CallProxy,
},
}, []ChangesetApplication{
{
Changeset: WrapChangeSet(TransferToMCMSWithTimelockV2),
Config: TransferToMCMSWithTimelockConfig{
ContractsByChain: map[uint64][]common.Address{
chain1: {link.LinkToken.Address()},
},
MinDelay: 0,
},
},
})
require.NoError(t, err)
// We expect now that the link token is owned by the MCMS timelock.
link, err = MaybeLoadLinkTokenChainState(e.Chains[chain1], addrs)
require.NoError(t, err)
o, err := link.LinkToken.Owner(nil)
require.NoError(t, err)
require.Equal(t, state.Timelock.Address(), o)

// Try a rollback to the deployer.
e, err = ApplyChangesets(t, e, nil, []ChangesetApplication{
{
Changeset: WrapChangeSet(TransferToDeployer),
Config: TransferToDeployerConfig{
ContractAddress: link.LinkToken.Address(),
ChainSel: chain1,
},
},
})
require.NoError(t, err)

o, err = link.LinkToken.Owner(nil)
require.NoError(t, err)
require.Equal(t, e.Chains[chain1].DeployerKey.From, o)
}

func TestRenounceTimelockDeployerConfigValidate(t *testing.T) {
t.Parallel()
lggr := logger.TestLogger(t)
Expand Down

0 comments on commit ba402ad

Please sign in to comment.