diff --git a/e2e/tests/aptos/timelock_cancel.go b/e2e/tests/aptos/timelock_cancel.go index 48edff542..56786dec5 100644 --- a/e2e/tests/aptos/timelock_cancel.go +++ b/e2e/tests/aptos/timelock_cancel.go @@ -17,6 +17,7 @@ import ( mcmslib "github.com/smartcontractkit/mcms" chainwrappermocks "github.com/smartcontractkit/mcms/chainwrappers/mocks" + e2ecommon "github.com/smartcontractkit/mcms/e2e/tests/common" "github.com/smartcontractkit/mcms/internal/testutils" "github.com/smartcontractkit/mcms/types" ) @@ -38,15 +39,15 @@ func (a *TestSuite) TestTimelock_Cancel() { accessor.EXPECT().AptosSigner(uint64(a.ChainSelector)).Return(a.deployerAccount, true).Maybe() phase := 0 - mcmslib.RunScheduleAndCancelTest(a.T(), mcmslib.ScheduleAndCancelTestHooks{ - Setup: func(ctx context.Context, t *testing.T) (mcmslib.ScheduleAndCancelTestEnv, error) { + e2ecommon.RunScheduleAndCancelTest(a.T(), e2ecommon.ScheduleAndCancelTestHooks{ + Setup: func(ctx context.Context, t *testing.T) (e2ecommon.ScheduleAndCancelTestEnv, error) { t.Helper() transaction, txErr := a.buildAcceptOwnershipTransaction() if txErr != nil { - return mcmslib.ScheduleAndCancelTestEnv{}, txErr + return e2ecommon.ScheduleAndCancelTestEnv{}, txErr } - return mcmslib.ScheduleAndCancelTestEnv{ + return e2ecommon.ScheduleAndCancelTestEnv{ Proposal: mcmslib.TimelockProposal{ BaseProposal: mcmslib.BaseProposal{ Version: "v1", @@ -112,7 +113,7 @@ func (a *TestSuite) TestTimelock_Cancel() { require.NoError(t, err) require.True(t, data.Success, data.VmStatus) }, - AssertExtraAfterCancel: func(ctx context.Context, t *testing.T, env *mcmslib.ScheduleAndCancelTestEnv) { + AssertExtraAfterCancel: func(ctx context.Context, t *testing.T, env *e2ecommon.ScheduleAndCancelTestEnv) { t.Helper() // After the cancel proposal is executed, verify that the ownership transfer // was not accepted — the owner should still be the deployer (from TransferOwnershipToSelf). diff --git a/e2e/tests/common/timelock_schedule_cancel.go b/e2e/tests/common/timelock_schedule_cancel.go new file mode 100644 index 000000000..1b75fcff5 --- /dev/null +++ b/e2e/tests/common/timelock_schedule_cancel.go @@ -0,0 +1,352 @@ +//go:build e2e + +package common + +import ( + "context" + "crypto/ecdsa" + "fmt" + "slices" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/chainwrappers" + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/types" +) + +type RoleConfig struct { + Keys []*ecdsa.PrivateKey + Config *types.Config +} + +func NewRoleConfig(t *testing.T, count int, quorum uint8) RoleConfig { + t.Helper() + + keys := make([]*ecdsa.PrivateKey, count) + signers := make([]common.Address, count) + for i := range count { + key, err := crypto.GenerateKey() + require.NoError(t, err) + keys[i] = key + signers[i] = crypto.PubkeyToAddress(key.PublicKey) + } + slices.SortFunc(signers, func(a, b common.Address) int { return a.Cmp(b) }) + + return RoleConfig{ + Keys: keys, + Config: &types.Config{ + Quorum: quorum, + Signers: signers, + }, + } +} + +func (r RoleConfig) Sign(t *testing.T, signable *mcms.Signable) { + t.Helper() + + require.LessOrEqual(t, int(r.Config.Quorum), len(r.Keys)) + for i := range int(r.Config.Quorum) { + _, err := signable.SignAndAppend(mcms.NewPrivateKeySigner(r.Keys[i])) + require.NoError(t, err) + } +} + +type ScheduleAndCancelTestEnv struct { + Proposal mcms.TimelockProposal + Chains chainwrappers.ChainAccessor +} + +type ScheduleAndCancelTestHooks struct { + Setup func(ctx context.Context, t *testing.T) (ScheduleAndCancelTestEnv, error) + Sign func(t *testing.T, signable *mcms.Signable) + + PrepareConvertedProposal func(t *testing.T, proposal *mcms.Proposal) + + DeriveCancellationMetadata func(t *testing.T, selector types.ChainSelector, scheduleMetadata types.ChainMetadata) (types.ChainMetadata, error) + + WaitForTransaction func(ctx context.Context, t *testing.T, tx types.TransactionResult) + + AssertExtraAfterCancel func(ctx context.Context, t *testing.T, env *ScheduleAndCancelTestEnv) +} + +func RunScheduleAndCancelTest(t *testing.T, hooks ScheduleAndCancelTestHooks) { + t.Helper() + + require.NotNil(t, hooks.Setup, "ScheduleAndCancelTestHooks.Setup must not be nil") + require.NotNil(t, hooks.Sign, "ScheduleAndCancelTestHooks.Sign must not be nil") + + ctx := t.Context() + + env, err := hooks.Setup(ctx, t) + require.NoError(t, err) + + scheduleInspectors := runTimelockLifecycleProposal(ctx, t, &env.Proposal, env.Chains, hooks) + + tExecutors, err := chainwrappers.BuildTimelockExecutors(env.Chains, env.Proposal.ChainMetadata, env.Proposal.Action) + require.NoError(t, err) + + tExecutable, err := mcms.NewTimelockExecutable(ctx, &env.Proposal, tExecutors) + require.NoError(t, err) + + for opIdx := range env.Proposal.Operations { + assertTimelockOperationState(t, ctx, &env.Proposal, tExecutable, tExecutors, opIdx, true, true, false, false) + assertOperationPendingState(t, tExecutable, &env.Proposal, opIdx) + assertOperationNotReadyState(t, tExecutable, &env.Proposal, opIdx) + assertOperationNotDoneState(t, tExecutable, &env.Proposal, opIdx) + } + + cancelProposal, err := deriveCancellationProposal(ctx, t, &env.Proposal, scheduleInspectors, hooks) + require.NoError(t, err) + + runTimelockLifecycleProposal(ctx, t, &cancelProposal, env.Chains, hooks) + + for opIdx := range env.Proposal.Operations { + assertTimelockOperationState(t, ctx, &env.Proposal, tExecutable, tExecutors, opIdx, false, false, false, false) + assertOperationNotPendingState(t, tExecutable, &env.Proposal, opIdx) + assertOperationNotReadyState(t, tExecutable, &env.Proposal, opIdx) + assertOperationNotDoneState(t, tExecutable, &env.Proposal, opIdx) + } + + if hooks.AssertExtraAfterCancel != nil { + hooks.AssertExtraAfterCancel(ctx, t, &env) + } +} + +func runTimelockLifecycleProposal( + ctx context.Context, + t *testing.T, + proposal *mcms.TimelockProposal, + chains chainwrappers.ChainAccessor, + hooks ScheduleAndCancelTestHooks, +) map[types.ChainSelector]sdk.Inspector { + t.Helper() + + converters, err := chainwrappers.BuildConverters(proposal.ChainMetadata) + require.NoError(t, err) + + mcmsProposal, _, err := proposal.Convert(ctx, converters) + require.NoError(t, err) + + if hooks.PrepareConvertedProposal != nil { + hooks.PrepareConvertedProposal(t, &mcmsProposal) + } + + inspectors, err := chainwrappers.BuildInspectors(chains, mcmsProposal.ChainMetadata, proposal.Action) + require.NoError(t, err) + + signable, err := mcms.NewSignable(&mcmsProposal, inspectors) //nolint:contextcheck //OPT-400 + require.NoError(t, err) + + require.NoError(t, signable.ValidateConfigs(ctx)) + + hooks.Sign(t, signable) + + quorumMet, err := signable.ValidateSignatures(ctx) + require.NoError(t, err) + require.True(t, quorumMet) + + encoders, err := mcmsProposal.GetEncoders() //nolint:contextcheck //OPT-400 + require.NoError(t, err) + + executors, err := chainwrappers.BuildExecutors(chains, mcmsProposal.ChainMetadata, encoders, proposal.Action) + require.NoError(t, err) + + executable, err := mcms.NewExecutable(&mcmsProposal, executors) //nolint:contextcheck //OPT-400 + require.NoError(t, err) + + tree, err := mcmsProposal.MerkleTree() //nolint:contextcheck //OPT-400 + require.NoError(t, err) + + for _, selector := range proposalChainSelectors(proposal.Operations) { + tx, setRootErr := executable.SetRoot(ctx, selector) + require.NoError(t, setRootErr) + require.NotEmpty(t, tx.Hash) + waitForTransaction(ctx, t, hooks, tx) + + root, validUntil, rootErr := inspectors[selector].GetRoot(ctx, proposal.ChainMetadata[selector].MCMAddress) + require.NoError(t, rootErr) + require.Equal(t, common.Hash([32]byte(tree.Root.Bytes())), root) + require.Equal(t, proposal.ValidUntil, validUntil) + } + + for opIdx := range mcmsProposal.Operations { + tx, execErr := executable.Execute(ctx, opIdx) + require.NoError(t, execErr) + require.NotEmpty(t, tx.Hash) + waitForTransaction(ctx, t, hooks, tx) + } + + assertOpCounts(t, ctx, proposal, inspectors) + + return inspectors +} + +func deriveCancellationProposal( + ctx context.Context, + t *testing.T, + schedule *mcms.TimelockProposal, + inspectors map[types.ChainSelector]sdk.Inspector, + hooks ScheduleAndCancelTestHooks, +) (mcms.TimelockProposal, error) { + t.Helper() + + cancellerMetadata := make(map[types.ChainSelector]types.ChainMetadata, len(schedule.ChainMetadata)) + for selector, metadata := range schedule.ChainMetadata { + opCount, err := inspectors[selector].GetOpCount(ctx, metadata.MCMAddress) + if err != nil { + return mcms.TimelockProposal{}, err + } + + next := metadata + next.StartingOpCount = opCount + + if hooks.DeriveCancellationMetadata != nil { + var err error + next, err = hooks.DeriveCancellationMetadata(t, selector, next) + if err != nil { + return mcms.TimelockProposal{}, fmt.Errorf("deriving cancellation metadata for selector %d: %w", selector, err) + } + } + + cancellerMetadata[selector] = next + } + + return schedule.DeriveCancellationProposal(cancellerMetadata) +} + +func assertOpCounts( + t *testing.T, + ctx context.Context, + proposal *mcms.TimelockProposal, + inspectors map[types.ChainSelector]sdk.Inspector, +) { + t.Helper() + + counts, err := proposal.OperationCounts(ctx) + require.NoError(t, err) + + for selector, metadata := range proposal.ChainMetadata { + opCount, getErr := inspectors[selector].GetOpCount(ctx, metadata.MCMAddress) + require.NoError(t, getErr) + require.Equal(t, metadata.StartingOpCount+counts[selector], opCount) + } +} + +func waitForTransaction( + ctx context.Context, + t *testing.T, + hooks ScheduleAndCancelTestHooks, + tx types.TransactionResult, +) { + t.Helper() + + if hooks.WaitForTransaction != nil { + hooks.WaitForTransaction(ctx, t, tx) + } +} + +func proposalChainSelectors(ops []types.BatchOperation) []types.ChainSelector { + selectors := make([]types.ChainSelector, 0, len(ops)) + seen := make(map[types.ChainSelector]struct{}, len(ops)) + for _, op := range ops { + if _, ok := seen[op.ChainSelector]; ok { + continue + } + seen[op.ChainSelector] = struct{}{} + selectors = append(selectors, op.ChainSelector) + } + + return selectors +} + +func assertOperationPendingState(t *testing.T, tExecutable *mcms.TimelockExecutable, proposal *mcms.TimelockProposal, opIdx int) { + t.Helper() + + ctx := t.Context() + require.NoError(t, tExecutable.IsOperationPending(ctx, opIdx)) + selector := proposal.Operations[opIdx].ChainSelector + require.NoError(t, tExecutable.IsChainPending(ctx, selector)) +} + +func assertTimelockOperationState( + t *testing.T, + ctx context.Context, + proposal *mcms.TimelockProposal, + tExecutable *mcms.TimelockExecutable, + executors map[types.ChainSelector]sdk.TimelockExecutor, + opIdx int, + wantIsOperation bool, + wantPending bool, + wantReady bool, + wantDone bool, +) { + t.Helper() + + op := proposal.Operations[opIdx] + selector := op.ChainSelector + timelockAddress := proposal.TimelockAddresses[selector] + + opID, err := tExecutable.GetOpID(ctx, opIdx, op, selector) + require.NoError(t, err) + + gotIsOperation, err := executors[selector].IsOperation(ctx, timelockAddress, opID) + require.NoError(t, err) + require.Equal(t, wantIsOperation, gotIsOperation) + + if !wantIsOperation && !gotIsOperation { + return + } + + gotPending, err := executors[selector].IsOperationPending(ctx, timelockAddress, opID) + require.NoError(t, err) + require.Equal(t, wantPending, gotPending) + + gotReady, err := executors[selector].IsOperationReady(ctx, timelockAddress, opID) + require.NoError(t, err) + require.Equal(t, wantReady, gotReady) + + gotDone, err := executors[selector].IsOperationDone(ctx, timelockAddress, opID) + require.NoError(t, err) + require.Equal(t, wantDone, gotDone) +} + +func assertOperationNotPendingState(t *testing.T, tExecutable *mcms.TimelockExecutable, proposal *mcms.TimelockProposal, opIdx int) { + t.Helper() + + ctx := t.Context() + err := tExecutable.IsOperationPending(ctx, opIdx) + var opErr *mcms.OperationNotPendingError + require.ErrorAs(t, err, &opErr) + require.Equal(t, opIdx, opErr.OpIndex) + selector := proposal.Operations[opIdx].ChainSelector + require.ErrorAs(t, tExecutable.IsChainPending(ctx, selector), &opErr) +} + +func assertOperationNotReadyState(t *testing.T, tExecutable *mcms.TimelockExecutable, proposal *mcms.TimelockProposal, opIdx int) { + t.Helper() + + ctx := t.Context() + err := tExecutable.IsOperationReady(ctx, opIdx) + var opErr *mcms.OperationNotReadyError + require.ErrorAs(t, err, &opErr) + require.Equal(t, opIdx, opErr.OpIndex) + selector := proposal.Operations[opIdx].ChainSelector + require.ErrorAs(t, tExecutable.IsChainReady(ctx, selector), &opErr) +} + +func assertOperationNotDoneState(t *testing.T, tExecutable *mcms.TimelockExecutable, proposal *mcms.TimelockProposal, opIdx int) { + t.Helper() + + ctx := t.Context() + err := tExecutable.IsOperationDone(ctx, opIdx) + var opErr *mcms.OperationNotDoneError + require.ErrorAs(t, err, &opErr) + require.Equal(t, opIdx, opErr.OpIndex) + selector := proposal.Operations[opIdx].ChainSelector + require.ErrorAs(t, tExecutable.IsChainDone(ctx, selector), &opErr) +} diff --git a/e2e/tests/evm/timelock_schedule_cancel.go b/e2e/tests/evm/timelock_schedule_cancel.go new file mode 100644 index 000000000..08474fd0b --- /dev/null +++ b/e2e/tests/evm/timelock_schedule_cancel.go @@ -0,0 +1,111 @@ +//go:build e2e + +package evme2e + +import ( + "context" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/mcms" + chainwrappermocks "github.com/smartcontractkit/mcms/chainwrappers/mocks" + e2ecommon "github.com/smartcontractkit/mcms/e2e/tests/common" + testutils "github.com/smartcontractkit/mcms/e2e/utils" + "github.com/smartcontractkit/mcms/sdk/evm" + "github.com/smartcontractkit/mcms/sdk/evm/bindings" + mcmtypes "github.com/smartcontractkit/mcms/types" +) + +func (s *ExecutionTestSuite) TestScheduleAndCancelProposal() { + ctx := s.T().Context() + + target := s.signerAddresses[0] + role, err := s.ChainA.timelockContract.EXECUTORROLE(&bind.CallOpts{Context: ctx}) + s.Require().NoError(err) + + hasRole, err := s.ChainA.timelockContract.HasRole(&bind.CallOpts{Context: ctx}, role, target) + s.Require().NoError(err) + s.Require().False(hasRole, "target should not already have executor role") + + inspector := evm.NewInspector(s.ClientA) + opCount, err := inspector.GetOpCount(ctx, s.ChainA.mcmsContract.Address().Hex()) + s.Require().NoError(err) + + timelockABI, err := bindings.RBACTimelockMetaData.GetAbi() + s.Require().NoError(err) + grantRoleData, err := timelockABI.Pack("grantRole", role, target) + s.Require().NoError(err) + + accessor := chainwrappermocks.NewChainAccessor(s.T()) + accessor.EXPECT().EVMClient(uint64(s.ChainA.chainSelector)).Return(s.ClientA, true).Maybe() + accessor.EXPECT().EVMSigner(uint64(s.ChainA.chainSelector)).Return(s.ChainA.auth, true).Maybe() + + signer := testutils.ParsePrivateKey(s.Settings.PrivateKeys[1]) + e2ecommon.RunScheduleAndCancelTest(s.T(), e2ecommon.ScheduleAndCancelTestHooks{ + Setup: func(ctx context.Context, t *testing.T) (e2ecommon.ScheduleAndCancelTestEnv, error) { + t.Helper() + + return e2ecommon.ScheduleAndCancelTestEnv{ + Proposal: mcms.TimelockProposal{ + BaseProposal: mcms.BaseProposal{ + Version: "v1", + Kind: mcmtypes.KindTimelockProposal, + Description: "Cancels scheduled EVM timelock role grant", + ValidUntil: uint32(time.Now().Add(24 * time.Hour).Unix()), + OverridePreviousRoot: false, + Signatures: []mcmtypes.Signature{}, + ChainMetadata: map[mcmtypes.ChainSelector]mcmtypes.ChainMetadata{ + s.ChainA.chainSelector: { + StartingOpCount: opCount, + MCMAddress: s.ChainA.mcmsContract.Address().Hex(), + }, + }, + }, + Action: mcmtypes.TimelockActionSchedule, + Delay: mcmtypes.MustParseDuration("5m"), + TimelockAddresses: map[mcmtypes.ChainSelector]string{ + s.ChainA.chainSelector: s.ChainA.timelockContract.Address().Hex(), + }, + Operations: []mcmtypes.BatchOperation{ + { + ChainSelector: s.ChainA.chainSelector, + Transactions: []mcmtypes.Transaction{ + evm.NewTransaction( + common.HexToAddress(s.ChainA.timelockContract.Address().Hex()), + grantRoleData, + big.NewInt(0), + "RBACTimelock", + []string{"RBACTimelock", "GrantRole"}, + ), + }, + }, + }, + }, + Chains: accessor, + }, nil + }, + Sign: func(t *testing.T, signable *mcms.Signable) { + t.Helper() + _, err := signable.SignAndAppend(mcms.NewPrivateKeySigner(signer)) + require.NoError(t, err) + }, + WaitForTransaction: func(ctx context.Context, t *testing.T, tx mcmtypes.TransactionResult) { + t.Helper() + receipt, err := testutils.WaitMinedWithTxHash(ctx, s.ClientA, common.HexToHash(tx.Hash)) + require.NoError(t, err) + require.Equal(t, gethtypes.ReceiptStatusSuccessful, receipt.Status) + }, + AssertExtraAfterCancel: func(ctx context.Context, t *testing.T, env *e2ecommon.ScheduleAndCancelTestEnv) { + t.Helper() + hasRole, err := s.ChainA.timelockContract.HasRole(&bind.CallOpts{Context: ctx}, role, target) + require.NoError(t, err) + require.False(t, hasRole, "target should not receive role after cancellation") + }, + }) +} diff --git a/e2e/tests/solana/timelock_schedule_cancel.go b/e2e/tests/solana/timelock_schedule_cancel.go new file mode 100644 index 000000000..c0cab8913 --- /dev/null +++ b/e2e/tests/solana/timelock_schedule_cancel.go @@ -0,0 +1,123 @@ +//go:build e2e + +package solanae2e + +import ( + "context" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" + + cpistub "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/v0_1_1/external_program_cpi_stub" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/v0_1_1/timelock" + + "github.com/smartcontractkit/mcms" + chainwrappermocks "github.com/smartcontractkit/mcms/chainwrappers/mocks" + e2ecommon "github.com/smartcontractkit/mcms/e2e/tests/common" + e2esolanautils "github.com/smartcontractkit/mcms/e2e/utils/solana" + solanasdk "github.com/smartcontractkit/mcms/sdk/solana" + "github.com/smartcontractkit/mcms/types" +) + +var testPDASeedTimelockScheduleCancel = [32]byte{'t', 'e', 's', 't', '-', 's', 'c', 'h', 'e', 'd', 'c', 'a', 'n', 'c', 'e', 'l'} + +func (s *TestSuite) TestScheduleAndCancelProposal() { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + s.T().Cleanup(cancel) + + wallet, err := solana.PrivateKeyFromBase58(privateKey) + s.Require().NoError(err) + + s.SetupMCM(testPDASeedTimelockScheduleCancel) + s.SetupTimelock(testPDASeedTimelockScheduleCancel, 5*time.Minute) + + mcmAddress := solanasdk.ContractAddress(s.MCMProgramID, testPDASeedTimelockScheduleCancel) + timelockAddress := solanasdk.ContractAddress(s.TimelockProgramID, testPDASeedTimelockScheduleCancel) + + mcmSignerPDA, err := solanasdk.FindSignerPDA(s.MCMProgramID, testPDASeedTimelockScheduleCancel) + s.Require().NoError(err) + timelockSignerPDA, err := solanasdk.FindTimelockSignerPDA(s.TimelockProgramID, testPDASeedTimelockScheduleCancel) + s.Require().NoError(err) + e2esolanautils.FundAccounts(s.T(), []solana.PublicKey{mcmSignerPDA, timelockSignerPDA}, 1, s.SolanaClient) + + s.AssignRoleToAccounts(ctx, testPDASeedTimelockScheduleCancel, wallet, []solana.PublicKey{mcmSignerPDA}, timelock.Proposer_Role) + s.AssignRoleToAccounts(ctx, testPDASeedTimelockScheduleCancel, wallet, []solana.PublicKey{mcmSignerPDA}, timelock.Canceller_Role) + + signer := NewEVMTestAccount(s.T()) + configurer := solanasdk.NewConfigurer(s.SolanaClient, wallet, s.ChainSelector) + _, err = configurer.SetConfig(ctx, mcmAddress, &types.Config{ + Quorum: 1, + Signers: []common.Address{signer.Address}, + }, true) + s.Require().NoError(err) + + inspector := solanasdk.NewInspector(s.SolanaClient) + opCount, err := inspector.GetOpCount(ctx, mcmAddress) + s.Require().NoError(err) + + metadata, err := solanasdk.NewChainMetadata( + opCount, + s.MCMProgramID, + testPDASeedTimelockScheduleCancel, + s.Roles[timelock.Proposer_Role].AccessController.PublicKey(), + s.Roles[timelock.Canceller_Role].AccessController.PublicKey(), + s.Roles[timelock.Bypasser_Role].AccessController.PublicKey(), + ) + s.Require().NoError(err) + + emptyInstruction, err := cpistub.NewEmptyInstruction().ValidateAndBuild() + s.Require().NoError(err) + transaction, err := solanasdk.NewTransactionFromInstruction(emptyInstruction, "CPIStub", []string{"cpi-stub-empty"}) + s.Require().NoError(err) + + accessor := chainwrappermocks.NewChainAccessor(s.T()) + accessor.EXPECT().SolanaClient(uint64(s.ChainSelector)).Return(s.SolanaClient, true).Maybe() + accessor.EXPECT().SolanaSigner(uint64(s.ChainSelector)).Return(&wallet, true).Maybe() + + e2ecommon.RunScheduleAndCancelTest(s.T(), e2ecommon.ScheduleAndCancelTestHooks{ + Setup: func(ctx context.Context, t *testing.T) (e2ecommon.ScheduleAndCancelTestEnv, error) { + t.Helper() + + return e2ecommon.ScheduleAndCancelTestEnv{ + Proposal: mcms.TimelockProposal{ + BaseProposal: mcms.BaseProposal{ + Version: "v1", + Kind: types.KindTimelockProposal, + Description: "Cancels scheduled Solana timelock CPI stub call", + ValidUntil: uint32(time.Now().Add(24 * time.Hour).Unix()), + OverridePreviousRoot: false, + Signatures: []types.Signature{}, + ChainMetadata: map[types.ChainSelector]types.ChainMetadata{ + s.ChainSelector: metadata, + }, + }, + Action: types.TimelockActionSchedule, + Delay: types.MustParseDuration("5m"), + TimelockAddresses: map[types.ChainSelector]string{ + s.ChainSelector: timelockAddress, + }, + Operations: []types.BatchOperation{ + { + ChainSelector: s.ChainSelector, + Transactions: []types.Transaction{transaction}, + }, + }, + }, + Chains: accessor, + }, nil + }, + Sign: func(t *testing.T, signable *mcms.Signable) { + t.Helper() + _, err := signable.SignAndAppend(mcms.NewPrivateKeySigner(signer.PrivateKey)) + require.NoError(t, err) + }, + WaitForTransaction: func(ctx context.Context, t *testing.T, tx types.TransactionResult) { + t.Helper() + _, err := solana.SignatureFromBase58(tx.Hash) + require.NoError(t, err) + }, + }) +} diff --git a/e2e/tests/ton/executable.go b/e2e/tests/ton/executable.go index 33877e0ae..e6d94788c 100644 --- a/e2e/tests/ton/executable.go +++ b/e2e/tests/ton/executable.go @@ -31,6 +31,7 @@ import ( mcmslib "github.com/smartcontractkit/mcms" chainwrappermocks "github.com/smartcontractkit/mcms/chainwrappers/mocks" e2e "github.com/smartcontractkit/mcms/e2e/tests" + e2ecommon "github.com/smartcontractkit/mcms/e2e/tests/common" "github.com/smartcontractkit/mcms/internal/testutils" "github.com/smartcontractkit/mcms/sdk" "github.com/smartcontractkit/mcms/sdk/evm" @@ -257,8 +258,8 @@ func (s *ExecutionTestSuite) TestScheduleAndCancelProposal() { accessor.EXPECT().TonClient(uint64(s.ChainA)).Return(s.TonClient, true).Maybe() accessor.EXPECT().TonSigner(uint64(s.ChainA)).Return(s.wallet, true).Maybe() - mcmslib.RunScheduleAndCancelTest(s.T(), mcmslib.ScheduleAndCancelTestHooks{ - Setup: func(ctx context.Context, t *testing.T) (mcmslib.ScheduleAndCancelTestEnv, error) { + e2ecommon.RunScheduleAndCancelTest(s.T(), e2ecommon.ScheduleAndCancelTestHooks{ + Setup: func(ctx context.Context, t *testing.T) (e2ecommon.ScheduleAndCancelTestEnv, error) { t.Helper() transactions := make([]types.Transaction, 0, len(tt.targetRoles)) for i, role := range tt.targetRoles { @@ -268,7 +269,7 @@ func (s *ExecutionTestSuite) TestScheduleAndCancelProposal() { Account: grantee, }) if grantErr != nil { - return mcmslib.ScheduleAndCancelTestEnv{}, grantErr + return e2ecommon.ScheduleAndCancelTestEnv{}, grantErr } tx, txErr := mcmston.NewTransaction( @@ -281,13 +282,13 @@ func (s *ExecutionTestSuite) TestScheduleAndCancelProposal() { []string{bindings.ShortTimelock, "GrantRole"}, ) if txErr != nil { - return mcmslib.ScheduleAndCancelTestEnv{}, txErr + return e2ecommon.ScheduleAndCancelTestEnv{}, txErr } transactions = append(transactions, tx) } - return mcmslib.ScheduleAndCancelTestEnv{ + return e2ecommon.ScheduleAndCancelTestEnv{ Proposal: mcmslib.TimelockProposal{ BaseProposal: mcmslib.BaseProposal{ Version: "v1", @@ -331,7 +332,7 @@ func (s *ExecutionTestSuite) TestScheduleAndCancelProposal() { require.NotNil(t, rawTx) require.NoError(t, tracetracking.WaitForTrace(ctx, s.TonClient, rawTx)) }, - AssertExtraAfterCancel: func(ctx context.Context, t *testing.T, env *mcmslib.ScheduleAndCancelTestEnv) { + AssertExtraAfterCancel: func(ctx context.Context, t *testing.T, env *e2ecommon.ScheduleAndCancelTestEnv) { t.Helper() inspectorT := mcmston.NewTimelockInspector(s.TonClient) for _, role := range tt.targetRoles { diff --git a/test_runner_timelock_schedule_cancel.go b/test_runner_timelock_schedule_cancel_test.go similarity index 100% rename from test_runner_timelock_schedule_cancel.go rename to test_runner_timelock_schedule_cancel_test.go