Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 40 additions & 26 deletions legacy/mcms/changesets/deploy_mcms_with_timelock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,31 +37,35 @@ import (
"github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment"
"github.com/smartcontractkit/chainlink-deployments-framework/engine/test/onchain"
"github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime"

cldfchangesetutil "github.com/smartcontractkit/cld-changesets/pkg/cldfutil/changeset"
)

func TestGrantRoleInTimeLock(t *testing.T) {
t.Parallel()

// Initialize a runtime with a single EVM chain with one additional account
//
// The additional account will be used later on to replace the deployer key
selector := chain_selectors.TEST_90000001.Selector
env, err := environment.New(t.Context(),
rt, err := runtime.New(t.Context(), runtime.WithEnvOpts(
environment.WithEVMSimulatedWithConfig(t, []uint64{selector}, onchain.EVMSimLoaderConfig{
NumAdditionalAccounts: 1,
}),
)
environment.WithLogger(logger.Test(t)),
))
require.NoError(t, err)

// deploy the MCMS with timelock contracts
configuredChangeset := cldfchangesetutil.Configure(
cldf.CreateLegacyChangeSet(mcmschangesets.DeployMCMSWithTimelockV2),
map[uint64]cldfproposalutils.MCMSWithTimelockConfig{
// Deploy MCMS with timelock contracts
err = rt.Exec(
runtime.ChangesetTask(cldf.CreateLegacyChangeSet(mcmschangesets.DeployMCMSWithTimelockV2), map[uint64]cldfproposalutils.MCMSWithTimelockConfig{
selector: cldftesthelpers.SingleGroupTimelockConfig(t),
},
}),
)
updatedEnv, err := cldfchangesetutil.Apply(t, *env, configuredChangeset)
require.NoError(t, err)
mcmsState, err := evmstate.MaybeLoadMCMSWithTimelockState(updatedEnv, []uint64{selector})

// Get the environment from the runtime because we need to make changes to it
env := rt.Environment()

mcmsState, err := evmstate.MaybeLoadMCMSWithTimelockState(env, []uint64{selector})
require.NoError(t, err)

// change the environment to remove proposer from the timelock, so that we can deploy new proposer
Expand All @@ -70,12 +74,12 @@ func TestGrantRoleInTimeLock(t *testing.T) {
ab := cldf.NewMemoryAddressBook()
require.NoError(t, ab.Save(selector, existingProposer.Address().String(),
cldf.NewTypeAndVersion(mcmscontracts.ProposerManyChainMultisig, semvers.V1_0_0)))
require.NoError(t, updatedEnv.ExistingAddresses.Remove(ab)) //nolint:staticcheck // test removes legacy AddressBook entry while verifying DataStore migration behavior
require.NoError(t, env.ExistingAddresses.Remove(ab)) //nolint:staticcheck // test removes legacy AddressBook entry while verifying DataStore migration behavior

// remove from DataStore since deployment now uses DataStore
// Since DataStore is immutable, create a new one without the proposer
newDataStore := datastore.NewMemoryDataStore()
refs, err := updatedEnv.DataStore.Addresses().Fetch()
refs, err := env.DataStore.Addresses().Fetch()
require.NoError(t, err)

// Copy all address refs except the proposer we want to remove
Expand All @@ -90,36 +94,46 @@ func TestGrantRoleInTimeLock(t *testing.T) {
}

// Replace the DataStore in the environment
updatedEnv.DataStore = newDataStore.Seal()
env.DataStore = newDataStore.Seal()

// change the deployer key, so that we can deploy proposer with a new key
// the new deployer key will not be admin of the timelock
// we can test granting roles through proposal
evmChains := updatedEnv.BlockChains.EVMChains()
evmChains := env.BlockChains.EVMChains()
chain := evmChains[selector]
chain.DeployerKey = evmChains[selector].Users[0]
chain.DeployerKey = evmChains[selector].Users[0] // Changes it to the additional account
Comment thread
jkongie marked this conversation as resolved.
evmChains[selector] = chain

// Initialize a runtime again with the new environment so we can execute the changeset against
// the new environment
rt = runtime.NewFromEnvironment(env)

// now deploy MCMS again so that only the proposer is new
updatedEnv, err = cldfchangesetutil.Apply(t, updatedEnv, configuredChangeset)
require.NoError(t, err)
mcmsState, err = evmstate.MaybeLoadMCMSWithTimelockState(updatedEnv, []uint64{selector})
err = rt.Exec(
runtime.ChangesetTask(cldf.CreateLegacyChangeSet(mcmschangesets.DeployMCMSWithTimelockV2), map[uint64]cldfproposalutils.MCMSWithTimelockConfig{
selector: cldftesthelpers.SingleGroupTimelockConfig(t),
}),
)
require.NoError(t, err)

mcmsState, err = evmstate.MaybeLoadMCMSWithTimelockState(rt.Environment(), []uint64{selector})
require.NoError(t, err)
Comment thread
jkongie marked this conversation as resolved.
require.NotEqual(t, existingProposer.Address(), mcmsState[selector].ProposerMcm.Address())
updatedEnv, err = cldfchangesetutil.Apply(t, updatedEnv, cldfchangesetutil.Configure(
mcmschangesets.GrantRoleInTimeLock,
mcmschangesets.GrantRoleInput{

err = rt.Exec(
runtime.ChangesetTask(mcmschangesets.GrantRoleInTimeLock, mcmschangesets.GrantRoleInput{
ExistingProposerByChain: map[uint64]common.Address{
selector: existingProposer.Address(),
},
MCMS: &cldfproposalutils.TimelockConfig{MinDelay: 0},
},
))
}),
)
Comment thread
jkongie marked this conversation as resolved.
require.NoError(t, err)
mcmsState, err = evmstate.MaybeLoadMCMSWithTimelockState(updatedEnv, []uint64{selector})

mcmsState, err = evmstate.MaybeLoadMCMSWithTimelockState(rt.Environment(), []uint64{selector})
require.NoError(t, err)

evmTimelockInspector := mcmsevmsdk.NewTimelockInspector(updatedEnv.BlockChains.EVMChains()[selector].Client)
evmTimelockInspector := mcmsevmsdk.NewTimelockInspector(rt.Environment().BlockChains.EVMChains()[selector].Client)

proposers, err := evmTimelockInspector.GetProposers(t.Context(), mcmsState[selector].Timelock.Address().Hex())
require.NoError(t, err)
Expand Down
181 changes: 2 additions & 179 deletions pkg/cldfutil/changeset/test_helpers.go
Original file line number Diff line number Diff line change
@@ -1,195 +1,18 @@
package changeset

import (
"fmt"
"math/big"
"testing"

mapset "github.com/deckarep/golang-set/v2"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"

cldftesthelpers "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils/testhelpers"

evmstate "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/evm"

"github.com/smartcontractkit/chainlink-evm/pkg/utils"

cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"

"github.com/smartcontractkit/chainlink-deployments-framework/operations"

mcmsTypes "github.com/smartcontractkit/mcms/types"

"github.com/smartcontractkit/chainlink-deployments-framework/datastore"

cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
)

type ConfiguredChangeSet interface {
Apply(e cldf.Environment) (cldf.ChangesetOutput, error)
}

func Configure[C any](
changeset cldf.ChangeSetV2[C],
config C,
) ConfiguredChangeSet {
return configuredChangeSetImpl[C]{
changeset: changeset,
config: config,
}
}

type configuredChangeSetImpl[C any] struct {
changeset cldf.ChangeSetV2[C]
config C
}

func (ca configuredChangeSetImpl[C]) Apply(e cldf.Environment) (cldf.ChangesetOutput, error) {
err := ca.changeset.VerifyPreconditions(e, ca.config)
if err != nil {
return cldf.ChangesetOutput{}, err
}

return ca.changeset.Apply(e, ca.config)
}

// Apply applies the changeset applications to the environment and returns the updated environment. This is the
// variadic function equivalent of ApplyChangesets, but allowing you to simply pass in one or more changesets as
// parameters at the end of the function. e.g. `changeset.Apply(t, e, nil, configuredCS1, configuredCS2)` etc.
func Apply(t *testing.T, e cldf.Environment, first ConfiguredChangeSet, rest ...ConfiguredChangeSet) (cldf.Environment, error) {
t.Helper()

env, _, err := ApplyChangesets(t, e, append([]ConfiguredChangeSet{first}, rest...))

return env, err
}

type applyChangesetOptions struct {
realBackend bool
}

type ApplyChangesetsOptions func(*applyChangesetOptions) *applyChangesetOptions

func WithRealBackend() ApplyChangesetsOptions {
return func(o *applyChangesetOptions) *applyChangesetOptions {
o.realBackend = true
return o
}
}

// ApplyChangesets applies the changeset applications to the environment and returns the updated environment.
func ApplyChangesets(t *testing.T, e cldf.Environment, changesetApplications []ConfiguredChangeSet, opts ...ApplyChangesetsOptions) (cldf.Environment, []cldf.ChangesetOutput, error) {
t.Helper()

opt := applyChangesetOptions{}
for _, o := range opts {
opt = *o(&opt)
}

currentEnv := e
outputs := make([]cldf.ChangesetOutput, 0, len(changesetApplications))
for i, csa := range changesetApplications {
out, err := csa.Apply(currentEnv)
if err != nil {
return e, nil, fmt.Errorf("failed to apply changeset at index %d: %w", i, err)
}
outputs = append(outputs, out)
var addresses cldf.AddressBook
if out.AddressBook != nil { //nolint:staticcheck // legacy AddressBook is still supported by test helper while changesets migrate to DataStore
addresses = out.AddressBook //nolint:staticcheck // legacy AddressBook is still supported by test helper while changesets migrate to DataStore
if err = addresses.Merge(currentEnv.ExistingAddresses); err != nil { //nolint:staticcheck // merge legacy addresses for compatibility
return e, nil, fmt.Errorf("failed to merge address book: %w", err)
}
} else {
addresses = currentEnv.ExistingAddresses //nolint:staticcheck // preserve legacy AddressBook state in test helper
}

// Collect expected DataStore state after changeset is applied
var ds datastore.DataStore
if out.DataStore != nil {
ds1 := datastore.NewMemoryDataStore()
// New Addresses
if err = ds1.Merge(out.DataStore.Seal()); err != nil {
return e, nil, fmt.Errorf("failed to merge new addresses into datastore: %w", err)
}
// Existing Addresses
err = ds1.Merge(currentEnv.DataStore)
if err != nil {
return e, nil, fmt.Errorf("failed to merge current addresses into datastore: %w", err)
}
ds = ds1.Seal()
} else {
ds = currentEnv.DataStore
}

if out.Jobs != nil { //nolint:revive,staticcheck // we want the empty block as documentation
// do nothing, as these jobs auto-accept.
}

// Updated environment may be required before executing proposals when proposals involve new addresses
// Ex. changesets[0] deploys MCMS, changesets[1] generates a proposal with the new MCMS addresses
currentEnv = cldf.Environment{
Name: e.Name,
Logger: e.Logger,
ExistingAddresses: addresses, //nolint:staticcheck // preserve legacy AddressBook state in test helper
DataStore: ds,
NodeIDs: e.NodeIDs,
Offchain: e.Offchain,
OCRSecrets: e.OCRSecrets,
GetContext: e.GetContext,
OperationsBundle: operations.NewBundle(e.GetContext, e.Logger, operations.NewMemoryReporter()), // to ensure that each migration is run in a clean environment
BlockChains: e.BlockChains,
}

if out.MCMSTimelockProposals != nil {
for _, prop := range out.MCMSTimelockProposals {
chains := mapset.NewSet[uint64]()
for _, op := range prop.Operations {
chains.Add(uint64(op.ChainSelector))
}

// We need to supply a salt override, otherwise the validUntil timestamp will be used to generate the salt.
// In tests, validUntil is not always guaranteed to produce a unique operation ID because proposals often get generated within the same second.
// This has been a cause of flakiness in the past (caused an AlreadyScheduled error).
saltOverride := utils.RandomHash()
prop.SaltOverride = &saltOverride

p := cldftesthelpers.SignMCMSTimelockProposal(t, currentEnv, &prop, opt.realBackend)
err = cldftesthelpers.ExecuteMCMSProposalV2(t, currentEnv, p)
if err != nil {
return cldf.Environment{}, nil, err
}
if prop.Action != mcmsTypes.TimelockActionSchedule {
// We don't need to execute the proposal if it's not a schedule action
// because the proposal is already executed in the previous step.
return currentEnv, outputs, nil
}
err = cldftesthelpers.ExecuteMCMSTimelockProposalV2(t, currentEnv, &prop)
if err != nil {
return cldf.Environment{}, nil, err
}
}
}
if out.MCMSProposals != nil {
for _, prop := range out.MCMSProposals {
chains := mapset.NewSet[uint64]()
for _, op := range prop.Operations {
chains.Add(uint64(op.ChainSelector))
}

p := cldftesthelpers.SignMCMSProposal(t, currentEnv, &prop)
err = cldftesthelpers.ExecuteMCMSProposalV2(t, currentEnv, p)
if err != nil {
return cldf.Environment{}, nil, err
}
}
}
}

return currentEnv, outputs, nil
}
evmstate "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/evm"
)

func MustFundAddressWithLink(t *testing.T, e cldf.Environment, chain cldf_evm.Chain, to common.Address, amount int64) {
t.Helper()
Expand Down
15 changes: 6 additions & 9 deletions pkg/family/evm/operations/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ import (
"github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment"
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
"github.com/smartcontractkit/chainlink-deployments-framework/operations/optest"

cldfchangesetutil "github.com/smartcontractkit/cld-changesets/pkg/cldfutil/changeset"
)

func TestCloneTransactOptsWithGas(t *testing.T) {
Expand Down Expand Up @@ -201,19 +199,18 @@ func TestAddEVMCallSequenceToCSOutput_ProposalCombination(t *testing.T) {

// Deploy MCMS+Timelock to both chains. Real deployments are required because
// AddEVMCallSequenceToCSOutput → BuildProposalFromBatchesV2 reads OpCount from
env, err := cldfchangesetutil.Apply(t, rt.Environment(), cldfchangesetutil.Configure(
cldf.CreateLegacyChangeSet(mcmschangesets.DeployMCMSWithTimelockV2),
map[uint64]cldfproposalutils.MCMSWithTimelockConfig{
err = rt.Exec(
runtime.ChangesetTask(cldf.CreateLegacyChangeSet(mcmschangesets.DeployMCMSWithTimelockV2), map[uint64]cldfproposalutils.MCMSWithTimelockConfig{
selector1: cldftesthelpers.SingleGroupTimelockConfig(t),
selector2: cldftesthelpers.SingleGroupTimelockConfig(t),
},
))
}),
)
require.NoError(t, err)

// Build the MCMS state map directly from the EVM state package — this is the
// product-agnostic, MCMS-only equivalent of CCIP's stateview.LoadOnchainState
// + chainState.EVMMCMSStateByChain().
mcmsStateByChain := loadEVMMCMSStateByChain(t, env, selectors)
mcmsStateByChain := loadEVMMCMSStateByChain(t, rt.Environment(), selectors)

// Two pre-existing proposals (one per chain) to exercise the aggregation path.
existingProposal1 := mcmslib.TimelockProposal{
Expand Down Expand Up @@ -282,7 +279,7 @@ func TestAddEVMCallSequenceToCSOutput_ProposalCombination(t *testing.T) {
}

result, err := opsevm.AddEVMCallSequenceToCSOutput(
env,
rt.Environment(),
csOutput,
seqReport,
nil,
Expand Down
Loading