diff --git a/mcms/changesets/firedrill.go b/mcms/changesets/firedrill.go new file mode 100644 index 0000000..df43592 --- /dev/null +++ b/mcms/changesets/firedrill.go @@ -0,0 +1,124 @@ +package changesets + +import ( + "errors" + "fmt" + + "github.com/smartcontractkit/mcms" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils" + fwops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + chainsel "github.com/smartcontractkit/chain-selectors" + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + + mcops "github.com/smartcontractkit/cld-changesets/mcms/operations" + evmstate "github.com/smartcontractkit/cld-changesets/pkg/family/evm" +) + +var _ cldf.ChangeSetV2[FireDrillConfig] = MCMSSignFireDrillChangeset{} + +// FireDrillConfig selects chains and MCMS timelock routing for a signing fire drill. +type FireDrillConfig struct { + TimelockCfg cldfproposalutils.TimelockConfig `json:"timelockCfg"` + Selectors []uint64 `json:"selectors,omitempty"` +} + +// MCMSSignFireDrillChangeset creates an MCMS signing fire-drill proposal with noop operations per chain. +// It exercises signing and execution pipelines without mutating on-chain configuration. +type MCMSSignFireDrillChangeset struct{} + +// ResolvedSelectors returns the chain selectors VerifyPreconditions and the fire-drill operation will use. +// When cfg.Selectors is empty, it defaults to every Solana chain in the environment followed by every EVM chain. +func (cfg FireDrillConfig) ResolvedSelectors(e cldf.Environment) []uint64 { + return cfg.resolvedSelectors(e) +} + +// VerifyPreconditions ensures each target chain exists and MCMS timelock state satisfies the configured action. +func (MCMSSignFireDrillChangeset) VerifyPreconditions(e cldf.Environment, cfg FireDrillConfig) error { + selectors := cfg.ResolvedSelectors(e) + if len(selectors) == 0 { + return errors.New("no chain selectors resolved for MCMS fire drill") + } + + for _, selector := range selectors { + family, err := chainsel.GetSelectorFamily(selector) + if err != nil { + return err + } + + switch family { + case chainsel.FamilyEVM: + ch, ok := e.BlockChains.EVMChains()[selector] + if !ok { + return fmt.Errorf("evm chain %d not found in environment", selector) + } + + addresses, err := e.ExistingAddresses.AddressesForChain(selector) //nolint:staticcheck // SA1019 + if err != nil { + return fmt.Errorf("addresses for chain %d: %w", selector, err) + } + + st, err := evmstate.MaybeLoadMCMSWithTimelockChainState(ch, addresses) + if err != nil { + return fmt.Errorf("load MCMS timelock state for chain %d: %w", selector, err) + } + + if err := cfg.TimelockCfg.Validate(ch, st); err != nil { + return fmt.Errorf("timelock config for chain %d: %w", selector, err) + } + + case chainsel.FamilySolana: + if _, ok := e.BlockChains.SolanaChains()[selector]; !ok { + return fmt.Errorf("solana chain %d not found in environment", selector) + } + + if err := cfg.TimelockCfg.ValidateSolana(e, selector); err != nil { + return fmt.Errorf("timelock config for chain %d: %w", selector, err) + } + + default: + return fmt.Errorf("unsupported chain family for selector %d", selector) + } + } + + return nil +} + +// Apply builds the fire-drill proposal via the operations API (with force execute for repeatable drills). +func (MCMSSignFireDrillChangeset) Apply(e cldf.Environment, cfg FireDrillConfig) (cldf.ChangesetOutput, error) { + deps := mcops.FireDrillDeps{Environment: e} + input := mcops.FireDrillInput{TimelockCfg: cfg.TimelockCfg, Selectors: cfg.Selectors} + + report, err := fwops.ExecuteOperation[mcops.FireDrillInput, mcops.FireDrillOutput, mcops.FireDrillDeps]( + e.OperationsBundle, + mcops.BuildMCMSFiredrillProposalOp, + deps, + input, + fwops.WithForceExecute[mcops.FireDrillInput, mcops.FireDrillDeps](), + ) + out := cldf.ChangesetOutput{ + Reports: []fwops.Report[any, any]{report.ToGenericReport()}, + } + if err != nil { + return out, err + } + + out.MCMSTimelockProposals = []mcms.TimelockProposal{report.Output.Proposal} + + return out, nil +} + +func (cfg FireDrillConfig) resolvedSelectors(e cldf.Environment) []uint64 { + if len(cfg.Selectors) > 0 { + return cfg.Selectors + } + solSelectors := e.BlockChains.ListChainSelectors(cldf_chain.WithFamily(chainsel.FamilySolana)) + evmSelectors := e.BlockChains.ListChainSelectors(cldf_chain.WithFamily(chainsel.FamilyEVM)) + out := make([]uint64, 0, len(solSelectors)+len(evmSelectors)) + out = append(out, solSelectors...) + out = append(out, evmSelectors...) + + return out +} diff --git a/mcms/changesets/firedrill_test.go b/mcms/changesets/firedrill_test.go new file mode 100644 index 0000000..d9e84bc --- /dev/null +++ b/mcms/changesets/firedrill_test.go @@ -0,0 +1,157 @@ +package changesets + +import ( + "testing" + + chainselectors "github.com/smartcontractkit/chain-selectors" + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + cldf_solana "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils" + mcmstypes "github.com/smartcontractkit/mcms/types" + "github.com/stretchr/testify/require" +) + +func TestMCMSSignFireDrillChangeset_VerifyPreconditions_NoChainsResolved(t *testing.T) { + t.Parallel() + + env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(nil)) + cfg := FireDrillConfig{TimelockCfg: cldfproposalutils.TimelockConfig{}} + + err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg) + require.ErrorContains(t, err, "no chain selectors resolved") +} + +func TestMCMSSignFireDrillChangeset_VerifyPreconditions_UnknownChain(t *testing.T) { + t.Parallel() + + sel := uint64(999991) + env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(nil)) + cfg := FireDrillConfig{ + TimelockCfg: cldfproposalutils.TimelockConfig{}, + Selectors: []uint64{sel}, + } + + err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg) + require.Error(t, err) + _, famErr := chainselectors.GetSelectorFamily(sel) + if famErr != nil { + require.ErrorContains(t, err, famErr.Error()) + } else { + require.ErrorContains(t, err, "not found in environment") + } +} + +func TestMCMSSignFireDrillChangeset_VerifyPreconditions_unsupportedChainFamily(t *testing.T) { + t.Parallel() + + sel := chainselectors.APTOS_MAINNET.Selector + env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(nil)) + cfg := FireDrillConfig{ + TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule}, + Selectors: []uint64{sel}, + } + + err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg) + require.ErrorContains(t, err, "unsupported chain family") +} + +func TestMCMSSignFireDrillChangeset_VerifyPreconditions_evmChainNotInEnvironment(t *testing.T) { + t.Parallel() + + evmSel := chainselectors.TEST_90000002.Selector + solSel := chainselectors.TEST_22222222222222222222222222222222222222222222.Selector + env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{ + solSel: cldf_solana.Chain{Selector: solSel}, + })) + cfg := FireDrillConfig{ + TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule}, + Selectors: []uint64{evmSel}, + } + + err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg) + require.ErrorContains(t, err, "evm chain") + require.ErrorContains(t, err, "not found in environment") +} + +func TestMCMSSignFireDrillChangeset_VerifyPreconditions_solanaChainNotInEnvironment(t *testing.T) { + t.Parallel() + + evmSel := chainselectors.TEST_90000002.Selector + solSel := chainselectors.TEST_22222222222222222222222222222222222222222222.Selector + env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{ + evmSel: cldf_evm.Chain{Selector: evmSel}, + })) + cfg := FireDrillConfig{ + TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule}, + Selectors: []uint64{solSel}, + } + + err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg) + require.ErrorContains(t, err, "solana chain") + require.ErrorContains(t, err, "not found in environment") +} + +func TestMCMSSignFireDrillChangeset_VerifyPreconditions_missingAddressBookEntry(t *testing.T) { + t.Parallel() + + evmSel := chainselectors.TEST_90000002.Selector + env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{ + evmSel: cldf_evm.Chain{Selector: evmSel}, + })) + cfg := FireDrillConfig{ + TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule}, + Selectors: []uint64{evmSel}, + } + + err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg) + require.ErrorContains(t, err, "addresses for chain") +} + +func TestFireDrillConfig_ResolvedSelectors_defaultOrderSolanaBeforeEVM(t *testing.T) { + t.Parallel() + + evmSel := chainselectors.TEST_90000002.Selector + solSel := chainselectors.TEST_22222222222222222222222222222222222222222222.Selector + env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{ + evmSel: cldf_evm.Chain{Selector: evmSel}, + solSel: cldf_solana.Chain{Selector: solSel}, + })) + + got := FireDrillConfig{TimelockCfg: cldfproposalutils.TimelockConfig{}}.ResolvedSelectors(env) + require.Equal(t, []uint64{solSel, evmSel}, got) +} + +func TestFireDrillConfig_ResolvedSelectors_explicitPreservesInputOrder(t *testing.T) { + t.Parallel() + + evmSel := chainselectors.TEST_90000002.Selector + solSel := chainselectors.TEST_22222222222222222222222222222222222222222222.Selector + env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{ + evmSel: cldf_evm.Chain{Selector: evmSel}, + solSel: cldf_solana.Chain{Selector: solSel}, + })) + + got := FireDrillConfig{ + TimelockCfg: cldfproposalutils.TimelockConfig{}, + Selectors: []uint64{evmSel, solSel}, + }.ResolvedSelectors(env) + require.Equal(t, []uint64{evmSel, solSel}, got) +} + +func TestMCMSSignFireDrillChangeset_Apply_returnsReportOnFailure(t *testing.T) { + t.Parallel() + + env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(nil)) + cfg := FireDrillConfig{ + TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule}, + } + + out, err := MCMSSignFireDrillChangeset{}.Apply(env, cfg) + require.ErrorContains(t, err, "no chain selectors resolved") + require.Len(t, out.Reports, 1) + require.Empty(t, out.MCMSTimelockProposals) + require.NotNil(t, out.Reports[0].Err) + require.ErrorContains(t, out.Reports[0].Err, "no chain selectors resolved") +} diff --git a/mcms/operations/firedrill.go b/mcms/operations/firedrill.go new file mode 100644 index 0000000..f916a8f --- /dev/null +++ b/mcms/operations/firedrill.go @@ -0,0 +1,204 @@ +package operations + +import ( + "errors" + "fmt" + "math/big" + + "github.com/Masterminds/semver/v3" + "github.com/gagliardetto/solana-go" + chainsel "github.com/smartcontractkit/chain-selectors" + mcmslib "github.com/smartcontractkit/mcms" + mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" + mcmssolanasdk "github.com/smartcontractkit/mcms/sdk/solana" + mcmstypes "github.com/smartcontractkit/mcms/types" + + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils" + fwops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + mcmscontract "github.com/smartcontractkit/cld-changesets/pkg/contract/mcms" + evmstate "github.com/smartcontractkit/cld-changesets/pkg/family/evm" + solanastate "github.com/smartcontractkit/cld-changesets/pkg/family/solana" +) + +// FireDrillInput is JSON-serializable input for the MCMS signing fire-drill proposal operation. +type FireDrillInput struct { + TimelockCfg cldfproposalutils.TimelockConfig `json:"timelockCfg"` + Selectors []uint64 `json:"selectors,omitempty"` +} + +// FireDrillDeps holds non-serializable dependencies for the fire-drill operation. +type FireDrillDeps struct { + Environment cldf.Environment +} + +// FireDrillOutput is the serializable result of building the fire-drill proposal. +type FireDrillOutput struct { + Proposal mcmslib.TimelockProposal `json:"proposal"` +} + +// BuildMCMSFiredrillProposalOp builds a noop MCMS timelock proposal covering the configured chains. +// Use [fwops.WithForceExecute] at the call site so repeated drills record fresh proposals under identical inputs. +var BuildMCMSFiredrillProposalOp = fwops.NewOperation( + "mcms-firedrill-proposal", + semver.MustParse("1.0.0"), + "Build noop MCMS timelock proposal for signing fire drills", + buildMCMSFiredrillProposal, +) + +func buildMCMSFiredrillProposal(_ fwops.Bundle, deps FireDrillDeps, input FireDrillInput) (FireDrillOutput, error) { + e := deps.Environment + + allSelectors := input.Selectors + if len(allSelectors) == 0 { + solSelectors := e.BlockChains.ListChainSelectors(cldf_chain.WithFamily(chainsel.FamilySolana)) + evmSelectors := e.BlockChains.ListChainSelectors(cldf_chain.WithFamily(chainsel.FamilyEVM)) + allSelectors = append(append(allSelectors, solSelectors...), evmSelectors...) + } + if len(allSelectors) == 0 { + return FireDrillOutput{}, errors.New("no chain selectors resolved for MCMS fire drill") + } + + operations := make([]mcmstypes.BatchOperation, 0, len(allSelectors)) + timelocks := make(map[uint64]string, len(allSelectors)) + mcmAddresses := make(map[uint64]string, len(allSelectors)) + + inspectors, inspErr := cldfproposalutils.McmsInspectors(e) + if inspErr != nil { + return FireDrillOutput{}, inspErr + } + + for _, selector := range allSelectors { + family, famErr := chainsel.GetSelectorFamily(selector) + if famErr != nil { + return FireDrillOutput{}, famErr + } + + switch family { + case chainsel.FamilyEVM: + evmChain, ok := e.BlockChains.EVMChains()[selector] + if !ok { + return FireDrillOutput{}, fmt.Errorf("evm chain %d not found in environment", selector) + } + + addresses, err := e.ExistingAddresses.AddressesForChain(selector) //nolint:staticcheck // SA1019: AddressBook compatibility + if err != nil { + return FireDrillOutput{}, err + } + + st, err := evmstate.MaybeLoadMCMSWithTimelockChainState(evmChain, addresses) + if err != nil { + return FireDrillOutput{}, err + } + + timelocks[selector] = st.Timelock.Address().String() + + mcmAddress, err := input.TimelockCfg.MCMBasedOnAction(st) + if err != nil { + return FireDrillOutput{}, err + } + + mcmAddresses[selector] = mcmAddress.Address().String() + + tx, err := buildNoOPEVM(st) + if err != nil { + return FireDrillOutput{}, err + } + + operations = append(operations, mcmstypes.BatchOperation{ + ChainSelector: mcmstypes.ChainSelector(selector), + Transactions: []mcmstypes.Transaction{tx}, + }) + + case chainsel.FamilySolana: + solChain, ok := e.BlockChains.SolanaChains()[selector] + if !ok { + return FireDrillOutput{}, fmt.Errorf("solana chain %d not found in environment", selector) + } + + addresses, err := e.ExistingAddresses.AddressesForChain(selector) //nolint:staticcheck // SA1019 + if err != nil { + return FireDrillOutput{}, err + } + + st, err := solanastate.MaybeLoadMCMSWithTimelockChainState(solChain, addresses) + if err != nil { + return FireDrillOutput{}, err + } + + timelocks[selector] = mcmssolanasdk.ContractAddress(st.TimelockProgram, mcmssolanasdk.PDASeed(st.TimelockSeed)) + + mcmAddr, err := input.TimelockCfg.MCMBasedOnActionSolana(st) + if err != nil { + return FireDrillOutput{}, err + } + + mcmAddresses[selector] = mcmAddr + + tx, err := buildNoOPSolana() + if err != nil { + return FireDrillOutput{}, err + } + + operations = append(operations, mcmstypes.BatchOperation{ + ChainSelector: mcmstypes.ChainSelector(selector), + Transactions: []mcmstypes.Transaction{tx}, + }) + + default: + return FireDrillOutput{}, fmt.Errorf("unsupported chain family for selector %d", selector) + } + } + + prop, err := mcmscontract.BuildProposalFromBatchesV2( + e, + timelocks, + mcmAddresses, + inspectors, + operations, + "firedrill proposal", + input.TimelockCfg, + ) + if err != nil { + return FireDrillOutput{}, err + } + + return FireDrillOutput{Proposal: *prop}, nil +} + +// buildNoOPEVM builds a dummy tx that transfers 0 to the RBACTimelock (receive path). +func buildNoOPEVM(st *evmstate.MCMSWithTimelockState) (mcmstypes.Transaction, error) { + if st == nil || st.Timelock == nil { + return mcmstypes.Transaction{}, errors.New("timelock binding is required for noop EVM fire drill transaction") + } + + return mcmsevmsdk.NewTransaction( + st.Timelock.Address(), + []byte{}, + big.NewInt(0), + "FireDrillNoop", + nil, + ), nil +} + +// buildNoOPSolana builds a dummy transaction that invokes the memo program. +func buildNoOPSolana() (mcmstypes.Transaction, error) { + contractID := solana.MemoProgramID + memo := []byte("noop") + + tx, err := mcmssolanasdk.NewTransaction( + contractID.String(), + memo, + big.NewInt(0), + []*solana.AccountMeta{}, + "Memo", + []string{}, + ) + if err != nil { + return mcmstypes.Transaction{}, err + } + + return tx, nil +} diff --git a/mcms/operations/firedrill_test.go b/mcms/operations/firedrill_test.go new file mode 100644 index 0000000..1b2e6a3 --- /dev/null +++ b/mcms/operations/firedrill_test.go @@ -0,0 +1,91 @@ +package operations + +import ( + "context" + "testing" + + chainsel "github.com/smartcontractkit/chain-selectors" + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils" + "github.com/smartcontractkit/chainlink-deployments-framework/offchain/ocr" + fwops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" + mcmstypes "github.com/smartcontractkit/mcms/types" + "github.com/stretchr/testify/require" + + evmstate "github.com/smartcontractkit/cld-changesets/pkg/family/evm" +) + +func testFireDrillEnv(t *testing.T, chains cldf_chain.BlockChains) cldf.Environment { + t.Helper() + + return *cldf.NewEnvironment( + "test", + logger.Test(t), + cldf.NewMemoryAddressBook(), + datastore.NewMemoryDataStore().Seal(), + nil, + nil, + func() context.Context { return t.Context() }, + ocr.OCRSecrets{}, + chains, + ) +} + +func TestBuildMCMSFiredrillProposalOp_noSelectorsResolved(t *testing.T) { + t.Parallel() + + env := testFireDrillEnv(t, cldf_chain.NewBlockChains(nil)) + input := FireDrillInput{TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule}} + + _, err := fwops.ExecuteOperation[FireDrillInput, FireDrillOutput, FireDrillDeps]( + env.OperationsBundle, + BuildMCMSFiredrillProposalOp, + FireDrillDeps{Environment: env}, + input, + fwops.WithForceExecute[FireDrillInput, FireDrillDeps](), + ) + require.ErrorContains(t, err, "no chain selectors resolved") +} + +func TestBuildMCMSFiredrillProposalOp_evmChainMissingFromEnvironment(t *testing.T) { + t.Parallel() + + evmSel := chainsel.TEST_90000002.Selector + env := testFireDrillEnv(t, cldf_chain.NewBlockChains(nil)) + input := FireDrillInput{ + TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule}, + Selectors: []uint64{evmSel}, + } + + _, err := fwops.ExecuteOperation[FireDrillInput, FireDrillOutput, FireDrillDeps]( + env.OperationsBundle, + BuildMCMSFiredrillProposalOp, + FireDrillDeps{Environment: env}, + input, + fwops.WithForceExecute[FireDrillInput, FireDrillDeps](), + ) + require.ErrorContains(t, err, "evm chain") + require.ErrorContains(t, err, "not found in environment") +} + +func TestBuildNoOPSolana(t *testing.T) { + t.Parallel() + + tx, err := buildNoOPSolana() + require.NoError(t, err) + require.Equal(t, "Memo", tx.ContractType) + require.NotEmpty(t, tx.To) +} + +func TestBuildNoOPEVM_requiresTimelockBinding(t *testing.T) { + t.Parallel() + + _, err := buildNoOPEVM(nil) + require.ErrorContains(t, err, "timelock binding is required") + + _, err = buildNoOPEVM(&evmstate.MCMSWithTimelockState{}) + require.ErrorContains(t, err, "timelock binding is required") +} diff --git a/pkg/contract/mcms/propose_test.go b/pkg/contract/mcms/propose_test.go new file mode 100644 index 0000000..db16bce --- /dev/null +++ b/pkg/contract/mcms/propose_test.go @@ -0,0 +1,57 @@ +package mcms + +import ( + "context" + "testing" + + chainsel "github.com/smartcontractkit/chain-selectors" + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils" + "github.com/smartcontractkit/chainlink-deployments-framework/offchain/ocr" + "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" + mcmstypes "github.com/smartcontractkit/mcms/types" + "github.com/stretchr/testify/require" +) + +func TestChainMetadata_Set_initializesNestedMap(t *testing.T) { + t.Parallel() + + m := make(ChainMetadata) + m.Set(42, "role", "proposer") + + require.Contains(t, m, uint64(42)) + require.Equal(t, "proposer", m[42]["role"]) +} + +func TestBuildProposalFromBatchesV2_emptyBatches(t *testing.T) { + t.Parallel() + + evmSel := chainsel.TEST_90000002.Selector + env := *cldf.NewEnvironment( + "test", + logger.Nop(), + cldf.NewMemoryAddressBook(), + datastore.NewMemoryDataStore().Seal(), + nil, + nil, + func() context.Context { return t.Context() }, + ocr.OCRSecrets{}, + cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{ + evmSel: cldf_evm.Chain{Selector: evmSel}, + }), + ) + + _, err := BuildProposalFromBatchesV2( + env, + map[uint64]string{evmSel: "0x1"}, + map[uint64]string{evmSel: "0x2"}, + nil, + nil, + "desc", + cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule}, + ) + require.ErrorContains(t, err, "no operations in batch") +} diff --git a/pkg/family/evm/proposal_adapter.go b/pkg/family/evm/proposal_adapter.go new file mode 100644 index 0000000..d3baab9 --- /dev/null +++ b/pkg/family/evm/proposal_adapter.go @@ -0,0 +1,20 @@ +package evm + +import ( + cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils" +) + +// TimelockContracts implements [cldfproposalutils.EVMMCMSWithTimelock] for MCMS timelock proposal helpers. +func (s *MCMSWithTimelockState) TimelockContracts() cldfproposalutils.MCMSWithTimelockContracts { + if s == nil { + return cldfproposalutils.MCMSWithTimelockContracts{} + } + + return cldfproposalutils.MCMSWithTimelockContracts{ + CancellerMcm: s.CancellerMcm, + BypasserMcm: s.BypasserMcm, + ProposerMcm: s.ProposerMcm, + Timelock: s.Timelock, + CallProxy: s.CallProxy, + } +} diff --git a/pkg/family/solana/proposal_adapter.go b/pkg/family/solana/proposal_adapter.go new file mode 100644 index 0000000..a5f27df --- /dev/null +++ b/pkg/family/solana/proposal_adapter.go @@ -0,0 +1,23 @@ +package solana + +import ( + mcmssolanasdk "github.com/smartcontractkit/mcms/sdk/solana" + + cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils" +) + +// TimelockPrograms implements [cldfproposalutils.SolanaMCMSWithTimelock] for MCMS timelock proposal helpers. +func (s *MCMSWithTimelockState) TimelockPrograms() cldfproposalutils.MCMSWithTimelockPrograms { + if s == nil || s.MCMSWithTimelockPrograms == nil { + return cldfproposalutils.MCMSWithTimelockPrograms{} + } + + p := s.MCMSWithTimelockPrograms + + return cldfproposalutils.MCMSWithTimelockPrograms{ + McmProgram: p.McmProgram, + ProposerMcmSeed: mcmssolanasdk.PDASeed(p.ProposerMcmSeed), + CancellerMcmSeed: mcmssolanasdk.PDASeed(p.CancellerMcmSeed), + BypasserMcmSeed: mcmssolanasdk.PDASeed(p.BypasserMcmSeed), + } +}