diff --git a/deployment/adapters/fastcurse_test.go b/deployment/adapters/fastcurse_test.go index 425350b8..eceb617e 100644 --- a/deployment/adapters/fastcurse_test.go +++ b/deployment/adapters/fastcurse_test.go @@ -38,6 +38,18 @@ func fastCurseTestBundle(t *testing.T) cld_ops.Bundle { ) } +func uncurseTestBundle(t *testing.T) cld_ops.Bundle { + t.Helper() + + registry := cld_ops.NewOperationRegistry(rmnops.UncurseChainOp.AsUntyped()) + return cld_ops.NewBundle( + t.Context, + logger.Test(t), + cld_ops.NewMemoryReporter(), + cld_ops.WithOperationRegistry(registry), + ) +} + func txAdditionalFields(t *testing.T, tx mcmstypes.Transaction) suisdk.AdditionalFields { t.Helper() @@ -139,3 +151,105 @@ func TestCurseAdapter_Curse_WithUpgradedPackage_PropagatesLatestPackageID(t *tes require.NoError(t, err) require.Equal(t, testLatestCCIPPackageID, latestPackageID) } + +func TestCurseAdapter_Uncurse_UsesOwnerCapSequence(t *testing.T) { + t.Parallel() + + selector := cselectors.SUI_TESTNET.Selector + adapter := adapters.NewCurseAdapter() + adapter.CCIPAddress = testCCIPPackageID + adapter.CCIPObjectRef = testCCIPObjectRef + adapter.CCIPOwnerCapObjectID = "0x3333333333333333333333333333333333333333333333333333333333333333" + + chains := cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{ + selector: cldfsui.Chain{ChainMetadata: cldfsui.ChainMetadata{Selector: selector}}, + }) + + report, err := cld_ops.ExecuteSequence( + uncurseTestBundle(t), + adapter.Uncurse(), + chains, + fastcurse.CurseInput{ + ChainSelector: selector, + Subjects: []fastcurse.Subject{fastcurse.GlobalCurseSubject()}, + }, + ) + require.NoError(t, err) + require.Len(t, report.Output.BatchOps, 1) + require.Len(t, report.Output.BatchOps[0].Transactions, 1) + + tx := report.Output.BatchOps[0].Transactions[0] + require.Equal(t, testCCIPPackageID, tx.To) + + fields := txAdditionalFields(t, tx) + require.Equal(t, "rmn_remote", fields.ModuleName) + require.Equal(t, "uncurse_multiple", fields.Function) +} + +func TestCurseAdapter_Uncurse_WithUpgradedPackage_PropagatesLatestPackageID(t *testing.T) { + t.Parallel() + + selector := cselectors.SUI_TESTNET.Selector + adapter := adapters.NewCurseAdapter() + adapter.CCIPAddress = testCCIPPackageID + adapter.LatestCCIPPackageID = testLatestCCIPPackageID + adapter.CCIPObjectRef = testCCIPObjectRef + adapter.CCIPOwnerCapObjectID = "0x3333333333333333333333333333333333333333333333333333333333333333" + + chains := cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{ + selector: cldfsui.Chain{ChainMetadata: cldfsui.ChainMetadata{Selector: selector}}, + }) + + report, err := cld_ops.ExecuteSequence( + uncurseTestBundle(t), + adapter.Uncurse(), + chains, + fastcurse.CurseInput{ + ChainSelector: selector, + Subjects: []fastcurse.Subject{fastcurse.GenericSelectorToSubject(cselectors.ETHEREUM_MAINNET.Selector)}, + }, + ) + require.NoError(t, err) + require.Len(t, report.Output.BatchOps, 1) + require.Len(t, report.Output.BatchOps[0].Transactions, 1) + + tx := report.Output.BatchOps[0].Transactions[0] + require.Equal(t, testCCIPPackageID, tx.To) + + latestPackageID, err := suideployutils.TransactionLatestPackageID(tx) + require.NoError(t, err) + require.Equal(t, testLatestCCIPPackageID, latestPackageID) +} + +func TestCurseAdapter_Uncurse_MultipleSubjects(t *testing.T) { + t.Parallel() + + selector := cselectors.SUI_TESTNET.Selector + adapter := adapters.NewCurseAdapter() + adapter.CCIPAddress = testCCIPPackageID + adapter.CCIPObjectRef = testCCIPObjectRef + adapter.CCIPOwnerCapObjectID = "0x3333333333333333333333333333333333333333333333333333333333333333" + + chains := cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{ + selector: cldfsui.Chain{ChainMetadata: cldfsui.ChainMetadata{Selector: selector}}, + }) + + subjects := []fastcurse.Subject{ + fastcurse.GenericSelectorToSubject(cselectors.ETHEREUM_MAINNET.Selector), + fastcurse.GenericSelectorToSubject(cselectors.POLYGON_MAINNET.Selector), + } + + report, err := cld_ops.ExecuteSequence( + uncurseTestBundle(t), + adapter.Uncurse(), + chains, + fastcurse.CurseInput{ + ChainSelector: selector, + Subjects: subjects, + }, + ) + require.NoError(t, err) + require.Len(t, report.Output.BatchOps, 1) + require.Equal(t, mcmstypes.ChainSelector(selector), report.Output.BatchOps[0].ChainSelector) + require.Len(t, report.Output.BatchOps[0].Transactions, 1) +} diff --git a/deployment/adapters/mcmsreader_test.go b/deployment/adapters/mcmsreader_test.go index a86b1d0d..28ed3885 100644 --- a/deployment/adapters/mcmsreader_test.go +++ b/deployment/adapters/mcmsreader_test.go @@ -27,6 +27,14 @@ const ( testFastMcmsOwnerCap = "0x5656565656565656565656565656565656565656565656565656565656565656" testFastMcmsTimelock = "0x6767676767676767676767676767676767676767676767676767676767676767" testFastMcmsDeployer = "0x7878787878787878787878787878787878787878787878787878787878787878" + + testSlowMcmsPackageID = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + testSlowMcmsState = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + testSlowMcmsRegistry = "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + testSlowMcmsAccount = "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + testSlowMcmsOwnerCap = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + testSlowMcmsTimelock = "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + testSlowMcmsDeployer = "0x0909090909090909090909090909090909090909090909090909090909090909" ) func TestMCMSReader_RMNMCMSQualifier_SelectsFastMCMS(t *testing.T) { @@ -71,3 +79,61 @@ func TestMCMSReader_RMNMCMSQualifier_SelectsFastMCMS(t *testing.T) { require.Equal(t, testFastMcmsState, mcmsRef.Address) require.Equal(t, selector, mcmsRef.ChainSelector) } + +func TestMCMSReader_EmptyQualifier_SelectsSlowMCMS(t *testing.T) { + t.Parallel() + + selector := cselectors.SUI_TESTNET.Selector + ab := cldf.NewMemoryAddressBook() + require.NoError(t, ab.Save(selector, testMcmsReaderCCIPPackageID, cldf.NewTypeAndVersion(deployment.SuiCCIPType, deployment.Version1_0_0))) + require.NoError(t, ab.Save(selector, testMcmsReaderCCIPObjectRef, cldf.NewTypeAndVersion(deployment.SuiCCIPObjectRefType, deployment.Version1_0_0))) + require.NoError(t, deployment.StoreMCMSInAddressBook(ab, selector, mcmsops.DeployMCMSSeqOutput{ + PackageId: testSlowMcmsPackageID, + Objects: mcmsops.DeployMCMSObjects{ + McmsMultisigStateObjectId: testSlowMcmsState, + McmsRegistryObjectId: testSlowMcmsRegistry, + McmsAccountStateObjectId: testSlowMcmsAccount, + McmsAccountOwnerCapObjectId: testSlowMcmsOwnerCap, + TimelockObjectId: testSlowMcmsTimelock, + McmsDeployerStateObjectId: testSlowMcmsDeployer, + }, + }, deployment.MCMSInstanceSlow)) + + env := cldf.Environment{ + ExistingAddresses: ab, + BlockChains: cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{ + selector: cldfsui.Chain{ChainMetadata: cldfsui.ChainMetadata{Selector: selector}}, + }), + } + + reader := &adapters.MCMSReader{} + + tests := []struct { + name string + qualifier string + }{ + {"empty qualifier", ""}, + {"CLLCCIP qualifier", "CLLCCIP"}, + {"any non-RMNMCMS qualifier", "SomeOtherQualifier"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + input := ccipmcms.Input{ + Qualifier: tt.qualifier, + TimelockAction: mcmstypes.TimelockActionBypass, + } + + timelockRef, err := reader.GetTimelockRef(env, selector, input) + require.NoError(t, err) + require.Equal(t, testSlowMcmsTimelock, timelockRef.Address) + require.Equal(t, selector, timelockRef.ChainSelector) + + mcmsRef, err := reader.GetMCMSRef(env, selector, input) + require.NoError(t, err) + require.Equal(t, testSlowMcmsState, mcmsRef.Address) + require.Equal(t, selector, mcmsRef.ChainSelector) + }) + } +} diff --git a/deployment/changesets/cs_mcms_init_ownership_transfer.go b/deployment/changesets/cs_mcms_init_ownership_transfer.go new file mode 100644 index 00000000..00af44dd --- /dev/null +++ b/deployment/changesets/cs_mcms_init_ownership_transfer.go @@ -0,0 +1,99 @@ +package changesets + +import ( + "fmt" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cld_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink-sui/bindings/bind" + "github.com/smartcontractkit/chainlink-sui/deployment" + sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" + mcmsops "github.com/smartcontractkit/chainlink-sui/deployment/ops/mcms" +) + +var _ cldf.ChangeSetV2[InitMCMSOwnershipTransferConfig] = InitMCMSOwnershipTransfer{} + +// InitMCMSOwnershipTransferConfig starts the MCMS self-ownership transfer flow by calling +// mcms_account::transfer_ownership_to_self. This is step 1 of 3; accept and execute follow +// via sui_accept_mcms_ownership_to_self and sui_execute_ownership_transfer. +type InitMCMSOwnershipTransferConfig struct { + ChainSelector uint64 `json:"chainSelector" yaml:"chainSelector"` + // IsFastCurse selects the fastcurse MCMS instance; otherwise the normal instance is used. + IsFastCurse bool `json:"isFastCurse,omitempty" yaml:"isFastCurse,omitempty"` +} + +type InitMCMSOwnershipTransfer struct{} + +// VerifyPreconditions implements deployment.ChangeSetV2. +func (i InitMCMSOwnershipTransfer) VerifyPreconditions(e cldf.Environment, config InitMCMSOwnershipTransferConfig) error { + if config.ChainSelector == 0 { + return fmt.Errorf("chainSelector is required") + } + return nil +} + +// Apply implements deployment.ChangeSetV2. +func (i InitMCMSOwnershipTransfer) Apply(e cldf.Environment, config InitMCMSOwnershipTransferConfig) (cldf.ChangesetOutput, error) { + suiState, err := deployment.LoadOnchainStatesui(e) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to load sui onchain state: %w", err) + } + + chainState, ok := suiState[config.ChainSelector] + if !ok { + return cldf.ChangesetOutput{}, fmt.Errorf("no Sui chain state for chain selector %d", config.ChainSelector) + } + + mcmsFields := chainState.MCMSState(config.IsFastCurse) + if mcmsFields.PackageID == "" { + return cldf.ChangesetOutput{}, fmt.Errorf("MCMS package ID not found for chain selector %d (isFastCurse=%v)", config.ChainSelector, config.IsFastCurse) + } + if mcmsFields.AccountOwnerCapObjectID == "" { + return cldf.ChangesetOutput{}, fmt.Errorf("MCMS account owner cap not found for chain selector %d (isFastCurse=%v)", config.ChainSelector, config.IsFastCurse) + } + if mcmsFields.AccountStateObjectID == "" { + return cldf.ChangesetOutput{}, fmt.Errorf("MCMS account state object not found for chain selector %d (isFastCurse=%v)", config.ChainSelector, config.IsFastCurse) + } + + suiChains := e.BlockChains.SuiChains() + suiChain, ok := suiChains[config.ChainSelector] + if !ok { + return cldf.ChangesetOutput{}, fmt.Errorf("no Sui chain found for chain selector %d", config.ChainSelector) + } + + deps := sui_ops.OpTxDeps{ + Client: suiChain.Client, + Signer: suiChain.Signer, + GetCallOpts: func() *bind.CallOpts { + b := uint64(400_000_000) + return &bind.CallOpts{ + WaitForExecution: true, + GasBudget: &b, + } + }, + SuiRPC: suiChain.URL, + } + + opInput := mcmsops.MCMSTransferOwnershipInput{ + McmsPackageID: mcmsFields.PackageID, + OwnerCap: mcmsFields.AccountOwnerCapObjectID, + AccountObjectID: mcmsFields.AccountStateObjectID, + } + + report, err := cld_ops.ExecuteOperation(e.OperationsBundle, mcmsops.MCMSTransferOwnershipOp, deps, opInput) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to init MCMS ownership transfer for chain %d: %w", config.ChainSelector, err) + } + + e.Logger.Infow("Initiated MCMS ownership transfer to self", + "chainSelector", config.ChainSelector, + "isFastCurse", config.IsFastCurse, + "mcmsPackageID", mcmsFields.PackageID, + "digest", report.Output.Digest, + ) + + return cldf.ChangesetOutput{ + Reports: []cld_ops.Report[any, any]{report.ToGenericReport()}, + }, nil +} diff --git a/deployment/changesets/cs_mcms_init_ownership_transfer_test.go b/deployment/changesets/cs_mcms_init_ownership_transfer_test.go new file mode 100644 index 00000000..178fd92b --- /dev/null +++ b/deployment/changesets/cs_mcms_init_ownership_transfer_test.go @@ -0,0 +1,23 @@ +package changesets + +import ( + "testing" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/stretchr/testify/require" +) + +func TestInitMCMSOwnershipTransfer_VerifyPreconditions(t *testing.T) { + t.Parallel() + + cs := InitMCMSOwnershipTransfer{} + + err := cs.VerifyPreconditions(cldf.Environment{}, InitMCMSOwnershipTransferConfig{ + ChainSelector: 17529533435026248318, + }) + require.NoError(t, err) + + err = cs.VerifyPreconditions(cldf.Environment{}, InitMCMSOwnershipTransferConfig{}) + require.Error(t, err) + require.Contains(t, err.Error(), "chainSelector is required") +}