diff --git a/legacy/mcms/changesets/transfer_ownership_solana.go b/legacy/mcms/changesets/transfer_ownership_solana.go new file mode 100644 index 0000000..4bebccd --- /dev/null +++ b/legacy/mcms/changesets/transfer_ownership_solana.go @@ -0,0 +1,553 @@ +package changesets + +import ( + "fmt" + "maps" + "slices" + + "github.com/Masterminds/semver/v3" + "github.com/gagliardetto/solana-go" + "github.com/mr-tron/base58" + "github.com/smartcontractkit/mcms" + mcmssdk "github.com/smartcontractkit/mcms/sdk" + mcmssolanasdk "github.com/smartcontractkit/mcms/sdk/solana" + mcmstypes "github.com/smartcontractkit/mcms/types" + + accessControllerBindings "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/v0_1_1/access_controller" + mcmBindings "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/v0_1_1/mcm" + + proposeutils "github.com/smartcontractkit/cld-changesets/legacy/mcms/proposeutils" + solstate "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana" + cldfutil "github.com/smartcontractkit/cld-changesets/pkg/cldfutil" + pdasol "github.com/smartcontractkit/cld-changesets/pkg/family/solana" + + cldfsol "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + mcmscontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/mcms" + cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +const maxAcceptInstructionsPerBatch = 5 + +// TransferToTimelockSolanaConfig holds the configuration for an ownership transfer changeset +type TransferToTimelockSolanaConfig struct { + ContractsByChain map[uint64][]OwnableContract + MCMSCfg cldfproposalutils.TimelockConfig +} + +type OwnableContract struct { + ProgramID solana.PublicKey + Seed solstate.PDASeed + OwnerPDA solana.PublicKey + Type cldf.ContractType +} + +// TransferToTimelockSolana transfers a set of Solana "contracts" to the Timelock +// signer PDA. +// The "transfer ownership" instructions are immediately sent and +// confirmed onchain. The "accept ownership" instructions are added to an MCMS +// timelock proposal that should be executed using the standard MCMS workflows. +type TransferToTimelockSolana struct{} + +func (t *TransferToTimelockSolana) VerifyPreconditions( + env cldf.Environment, config TransferToTimelockSolanaConfig, +) error { + for chainSelector, contracts := range config.ContractsByChain { + err := addressBookContains(envAddressBook(env), chainSelector, + mcmscontracts.RBACTimelockProgram, + mcmscontracts.RBACTimelock, + mcmscontracts.ManyChainMultisigProgram, + mcmscontracts.ProposerManyChainMultisig, + ) + if err != nil { + return err + } + + for _, contract := range contracts { + exists, err := cldf.AddressBookContains(envAddressBook(env), chainSelector, contract.ProgramID.String()) + if err != nil { + return fmt.Errorf("failed to search address book for program id: %w", err) + } + if !exists { + return fmt.Errorf("program id %s not found in address book", contract.ProgramID.String()) + } + + if (contract.Seed == solstate.PDASeed{}) { + continue + } + + exists, err = cldf.AddressBookContains(envAddressBook(env), chainSelector, base58.Encode(contract.Seed[:])) + if err != nil { + return fmt.Errorf("failed to search address book for seed (%s): %w", base58.Encode(contract.Seed[:]), err) + } + if !exists { + address := solanaAddress(contract.ProgramID, mcmssolanasdk.PDASeed(contract.Seed)) + exists, err = cldf.AddressBookContains(envAddressBook(env), chainSelector, address) + if err != nil { + return fmt.Errorf("failed to search address book for seed (%s): %w", address, err) + } + } + if !exists { + exists, err = cldf.AddressBookContains(envAddressBook(env), chainSelector, string(contract.Seed[:])) + if err != nil { + return fmt.Errorf("failed to search address book for seed (%s): %w", string(contract.Seed[:]), err) + } + } + if !exists { + return fmt.Errorf("seed %s not found in address book", base58.Encode(contract.Seed[:])) + } + } + } + + return nil +} + +func (t *TransferToTimelockSolana) Apply( + env cldf.Environment, cfg TransferToTimelockSolanaConfig, +) (cldf.ChangesetOutput, error) { + solChains := env.BlockChains.SolanaChains() + mcmsState, err := solstate.MaybeLoadMCMSWithTimelockState(env, slices.Collect(maps.Keys(solChains))) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to load onchain state: %w", err) + } + + batches := []mcmstypes.BatchOperation{} + timelocks := map[uint64]string{} + proposers := map[uint64]string{} + inspectors := map[uint64]mcmssdk.Inspector{} + var out cldf.ChangesetOutput + for chainSelector, contractsToTransfer := range cfg.ContractsByChain { + solChain, ok := solChains[chainSelector] + if !ok { + return cldf.ChangesetOutput{}, fmt.Errorf("solana chain not found in environment (selector: %v)", chainSelector) + } + chainState, ok := mcmsState[chainSelector] + if !ok { + return cldf.ChangesetOutput{}, fmt.Errorf("chain state not found for selector: %v", chainSelector) + } + + inspectors[chainSelector] = mcmssolanasdk.NewInspector(solChain.Client) + timelocks[chainSelector] = solanaAddress(chainState.TimelockProgram, mcmssolanasdk.PDASeed(chainState.TimelockSeed)) + proposers[chainSelector] = solanaAddress(chainState.McmProgram, mcmssolanasdk.PDASeed(chainState.ProposerMcmSeed)) + + for _, contract := range contractsToTransfer { + execOut, execErr := operations.ExecuteOperation(env.OperationsBundle, + operations.NewOperation( + "transfer-ownership", + &cldfutil.Version1_0_0, + "transfers ownership of contracts to mcms", + TransferToTimelockSolanaOp, + ), + Deps{ + Env: env, + State: chainState, + Chain: solChain, + }, + TransferToTimelockInput{ + Contract: contract, + MCMSCfg: cfg.MCMSCfg, + }, + ) + if execErr != nil { + return out, execErr + } + + batches = append(batches, execOut.Output.Batches...) + } + } + + // create timelock proposal with accept transactions + proposal, err := proposeutils.BuildProposalFromBatchesV2(env, timelocks, proposers, inspectors, + batches, "proposal to transfer ownership of contracts to timelock", cfg.MCMSCfg) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + env.Logger.Debugw("created timelock proposal", "# batches", len(batches)) + + return cldf.ChangesetOutput{MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}}, nil +} + +// ContractConfig defines the configuration for a contract ownership transfer +type ContractConfig struct { + ContractType datastore.ContractType + StateType datastore.ContractType + OperationID string + Description string +} + +// TransferOwnershipRequest represents a generic ownership transfer request +type TransferOwnershipRequest struct { + ChainSel uint64 + CurrentOwner, ProposedOwner solana.PublicKey + Version string + Qualifier string + MCMSCfg cldfproposalutils.TimelockConfig + ContractConfig ContractConfig +} + +// genericTransferOwnership handles the common ownership transfer logic +func GenericTransferOwnership(env cldf.Environment, req *TransferOwnershipRequest) (cldf.ChangesetOutput, error) { + var out cldf.ChangesetOutput + version := semver.MustParse(req.Version) + + // Build address references + contractStateRef := datastore.NewAddressRefKey(req.ChainSel, req.ContractConfig.StateType, version, req.Qualifier) + contractRef := datastore.NewAddressRefKey(req.ChainSel, req.ContractConfig.ContractType, version, req.Qualifier) + + // Get contract addresses + contract, err := env.DataStore.Addresses().Get(contractRef) + if err != nil { + return out, fmt.Errorf("failed to get contract address: %w", err) + } + + contractState, err := env.DataStore.Addresses().Get(contractStateRef) + if err != nil { + return out, fmt.Errorf("failed to get contract state address: %w", err) + } + + // Load MCMS state + mcmsState, err := solstate.MaybeLoadMCMSWithTimelockChainStateV2( + env.DataStore.Addresses().Filter(datastore.AddressRefByChainSelector(req.ChainSel))) + if err != nil { + return out, err + } + + solChain := env.BlockChains.SolanaChains()[req.ChainSel] + + // Execute the transfer operation + execOut, err := operations.ExecuteOperation(env.OperationsBundle, + operations.NewOperation( + req.ContractConfig.OperationID, + version, + req.ContractConfig.Description, + TransferToTimelockSolanaOp, + ), + Deps{ + Env: env, + State: mcmsState, + Chain: solChain, + }, + TransferToTimelockInput{ + Contract: OwnableContract{ + Type: cldf.ContractType(req.ContractConfig.ContractType), + ProgramID: solana.MustPublicKeyFromBase58(contract.Address), + OwnerPDA: solana.MustPublicKeyFromBase58(contractState.Address), + }, + MCMSCfg: req.MCMSCfg, + }, + ) + if err != nil { + return out, err + } + + // Build proposal maps + timelocks := map[uint64]string{} + proposers := map[uint64]string{} + inspectors := map[uint64]mcmssdk.Inspector{} + + inspectors[req.ChainSel] = mcmssolanasdk.NewInspector(solChain.Client) + timelocks[req.ChainSel] = mcmssolanasdk.ContractAddress(mcmsState.TimelockProgram, mcmssolanasdk.PDASeed(mcmsState.TimelockSeed)) + proposers[req.ChainSel] = mcmssolanasdk.ContractAddress(mcmsState.McmProgram, mcmssolanasdk.PDASeed(mcmsState.ProposerMcmSeed)) + + // Create timelock proposal + proposal, err := proposeutils.BuildProposalFromBatchesV2(env, timelocks, proposers, inspectors, + execOut.Output.Batches, fmt.Sprintf("proposal to transfer ownership of %s to timelock", req.ContractConfig.ContractType), req.MCMSCfg) + if err != nil { + return out, fmt.Errorf("failed to build proposal: %w", err) + } + env.Logger.Debugw("created timelock proposal", "# batches", len(execOut.Output.Batches)) + + out.MCMSTimelockProposals = []mcms.TimelockProposal{*proposal} + + return out, nil +} + +// genericVerifyPreconditions handles the common precondition verification logic +func GenericVerifyPreconditions(env cldf.Environment, chainSel uint64, version, qualifier string, contractType datastore.ContractType) error { + // Validate version + if _, err := semver.NewVersion(version); err != nil { + return err + } + + // Check if chain exists + if _, ok := env.BlockChains.SolanaChains()[chainSel]; !ok { + return fmt.Errorf("solana chain not found for chain selector %d", chainSel) + } + + // Verify contract exists + v := semver.MustParse(version) + contractKey := datastore.NewAddressRefKey(chainSel, contractType, v, qualifier) + if _, err := env.DataStore.Addresses().Get(contractKey); err != nil { + return fmt.Errorf("failed to get %s for chain selector %d: %w", contractType, chainSel, err) + } + + return nil +} + +type ( + Deps struct { + Env cldf.Environment + State *solstate.MCMSWithTimelockState + Chain cldfsol.Chain + } + + TransferToTimelockInput struct { + Contract OwnableContract + MCMSCfg cldfproposalutils.TimelockConfig + } + + TransferToTimelockOutput struct { + Batches []mcmstypes.BatchOperation + } +) + +func TransferToTimelockSolanaOp(b operations.Bundle, deps Deps, in TransferToTimelockInput) (TransferToTimelockOutput, error) { + var out TransferToTimelockOutput + + solChain := deps.Chain + chainState := deps.State + + batches := []mcmstypes.BatchOperation{} + timelocks := map[uint64]string{} + proposers := map[uint64]string{} + inspectors := map[uint64]mcmssdk.Inspector{} + instructions := make([]solana.Instruction, 0, 1) + chainSelector := solChain.ChainSelector() + + timelocks[chainSelector] = solanaAddress(chainState.TimelockProgram, mcmssolanasdk.PDASeed(chainState.TimelockSeed)) + proposers[chainSelector] = solanaAddress(chainState.McmProgram, mcmssolanasdk.PDASeed(chainState.ProposerMcmSeed)) + inspectors[chainSelector] = mcmssolanasdk.NewInspector(solChain.Client) + + timelockSignerPDA := pdasol.GetTimelockSignerPDA(chainState.TimelockProgram, chainState.TimelockSeed) + + transactions := make([]mcmstypes.Transaction, 0, 1) + contract := in.Contract + transferInstruction, err := transferOwnershipInstruction(contract.ProgramID, contract.Seed, timelockSignerPDA, + contract.OwnerPDA, solChain.DeployerKey.PublicKey()) + if err != nil { + return out, fmt.Errorf("failed to create transfer ownership instruction: %w", err) + } + instructions = append(instructions, transferInstruction) + + acceptMCMSTransaction, err := acceptMCMSTransaction(contract, timelockSignerPDA) + if err != nil { + return out, fmt.Errorf("failed to create accept ownership mcms transaction: %w", err) + } + transactions = append(transactions, acceptMCMSTransaction) + + // FIXME: remove the chunking logic once we have custom CU limit support in MCMS + for chunk := range slices.Chunk(transactions, maxAcceptInstructionsPerBatch) { + batches = append(batches, mcmstypes.BatchOperation{ + ChainSelector: mcmstypes.ChainSelector(chainSelector), + Transactions: chunk, + }) + b.Logger.Debugw("added BatchOperation with accept ownwership instructions", + "# transactions", len(transactions), "chain", chainSelector) + } + + for _, instruction := range instructions { + b.Logger.Debugw("confirming solana transfer ownership instruction", "instruction", instruction.ProgramID()) + err = solChain.Confirm([]solana.Instruction{instruction}) + if err != nil { + return out, fmt.Errorf("failed to confirm instruction: %w", err) + } + } + out.Batches = append(out.Batches, batches...) + + return out, nil +} + +type TransferMCMSToTimelockSolanaConfig struct { + Chains []uint64 + MCMSCfg cldfproposalutils.TimelockConfig +} + +// TransferMCMSToTimelockSolana transfers set MCMS "contracts" to the timelock +// signer PDA. It relies on the TransferToTimelockSolana changeset and merely +// adds the MCM, Timelock and AccessController contracts found in the address +// book to the list of contracts to transfer. +type TransferMCMSToTimelockSolana struct{} + +func (t TransferMCMSToTimelockSolana) VerifyPreconditions( + env cldf.Environment, config TransferMCMSToTimelockSolanaConfig, +) error { + for _, chainSelector := range config.Chains { + err := addressBookContains(envAddressBook(env), chainSelector, + mcmscontracts.RBACTimelockProgram, + mcmscontracts.RBACTimelock, + mcmscontracts.ManyChainMultisigProgram, + mcmscontracts.ProposerManyChainMultisig, + ) + if err != nil { + return err + } + } + + return nil +} + +func (t TransferMCMSToTimelockSolana) Apply( + env cldf.Environment, cfg TransferMCMSToTimelockSolanaConfig, +) (cldf.ChangesetOutput, error) { + mcmsState, err := solstate.MaybeLoadMCMSWithTimelockState(env, cfg.Chains) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to load mcms state: %w", err) + } + + contracts := map[uint64][]OwnableContract{} + for chainSelector, chainState := range mcmsState { + chainContracts := []OwnableContract{ + { + ProgramID: chainState.McmProgram, + Seed: chainState.ProposerMcmSeed, + OwnerPDA: pdasol.GetMCMConfigPDA(chainState.McmProgram, chainState.ProposerMcmSeed), + Type: mcmscontracts.ProposerManyChainMultisig, + }, + { + ProgramID: chainState.McmProgram, + Seed: chainState.CancellerMcmSeed, + OwnerPDA: pdasol.GetMCMConfigPDA(chainState.McmProgram, chainState.CancellerMcmSeed), + Type: mcmscontracts.CancellerManyChainMultisig, + }, + { + ProgramID: chainState.McmProgram, + Seed: chainState.BypasserMcmSeed, + OwnerPDA: pdasol.GetMCMConfigPDA(chainState.McmProgram, chainState.BypasserMcmSeed), + Type: mcmscontracts.BypasserManyChainMultisig, + }, + { + ProgramID: chainState.TimelockProgram, + Seed: chainState.TimelockSeed, + OwnerPDA: pdasol.GetTimelockConfigPDA(chainState.TimelockProgram, chainState.TimelockSeed), + Type: mcmscontracts.RBACTimelock, + }, + { + ProgramID: chainState.AccessControllerProgram, + OwnerPDA: chainState.ProposerAccessControllerAccount, + Type: mcmscontracts.ProposerAccessControllerAccount, + }, + { + ProgramID: chainState.AccessControllerProgram, + OwnerPDA: chainState.ExecutorAccessControllerAccount, + Type: mcmscontracts.ExecutorAccessControllerAccount, + }, + { + ProgramID: chainState.AccessControllerProgram, + OwnerPDA: chainState.CancellerAccessControllerAccount, + Type: mcmscontracts.CancellerAccessControllerAccount, + }, + { + ProgramID: chainState.AccessControllerProgram, + OwnerPDA: chainState.BypasserAccessControllerAccount, + Type: mcmscontracts.BypasserAccessControllerAccount, + }, + } + + contracts[chainSelector] = chainContracts + } + + return new(TransferToTimelockSolana).Apply(env, TransferToTimelockSolanaConfig{ + ContractsByChain: contracts, + MCMSCfg: cfg.MCMSCfg, + }) +} + +func transferOwnershipInstruction( + programID solana.PublicKey, seed solstate.PDASeed, proposedOwner, ownerPDA, auth solana.PublicKey, +) (solana.Instruction, error) { + if (seed == solstate.PDASeed{}) { + return newSeedlessTransferOwnershipInstruction(programID, proposedOwner, ownerPDA, auth) + } + + return newSeededTransferOwnershipInstruction(programID, seed, proposedOwner, ownerPDA, auth) +} + +func acceptMCMSTransaction( + contract OwnableContract, + authority solana.PublicKey, +) (mcmstypes.Transaction, error) { + acceptInstruction, err := acceptOwnershipInstruction(contract.ProgramID, contract.Seed, contract.OwnerPDA, authority) + if err != nil { + return mcmstypes.Transaction{}, fmt.Errorf("failed to build accept ownership instruction: %w", err) + } + acceptMCMSTx, err := mcmssolanasdk.NewTransactionFromInstruction(acceptInstruction, string(contract.Type), []string{}) + if err != nil { + return mcmstypes.Transaction{}, fmt.Errorf("failed to build mcms transaction from accept ownership instruction: %w", err) + } + + return acceptMCMSTx, nil +} + +func acceptOwnershipInstruction(programID solana.PublicKey, seed solstate.PDASeed, ownerPDA, auth solana.PublicKey, +) (solana.Instruction, error) { + if (seed == solstate.PDASeed{}) { + return newSeedlessAcceptOwnershipInstruction(programID, ownerPDA, auth) + } + + return newSeededAcceptOwnershipInstruction(programID, seed, ownerPDA, auth) +} + +func newSeededTransferOwnershipInstruction( + programID solana.PublicKey, seed solstate.PDASeed, proposedOwner, config, authority solana.PublicKey, +) (solana.Instruction, error) { + ix, err := mcmBindings.NewTransferOwnershipInstruction(seed, proposedOwner, config, authority).ValidateAndBuild() + return &seededInstruction{ix, programID}, err +} + +func newSeededAcceptOwnershipInstruction( + programID solana.PublicKey, seed solstate.PDASeed, config, authority solana.PublicKey, +) (solana.Instruction, error) { + ix, err := mcmBindings.NewAcceptOwnershipInstruction(seed, config, authority).ValidateAndBuild() + return &seededInstruction{ix, programID}, err +} + +func newSeedlessTransferOwnershipInstruction( + programID, proposedOwner, config, authority solana.PublicKey, +) (solana.Instruction, error) { + ix, err := accessControllerBindings.NewTransferOwnershipInstruction(proposedOwner, config, authority).ValidateAndBuild() + return &seedlessInstruction{ix, programID}, err +} + +func newSeedlessAcceptOwnershipInstruction( + programID, config, authority solana.PublicKey, +) (solana.Instruction, error) { + ix, err := accessControllerBindings.NewAcceptOwnershipInstruction(config, authority).ValidateAndBuild() + return &seedlessInstruction{ix, programID}, err +} + +type seedlessInstruction struct { + *accessControllerBindings.Instruction + programID solana.PublicKey +} + +func (s *seedlessInstruction) ProgramID() solana.PublicKey { + return s.programID +} + +type seededInstruction struct { + *mcmBindings.Instruction + programID solana.PublicKey +} + +func (s *seededInstruction) ProgramID() solana.PublicKey { + return s.programID +} + +func addressBookContains(addressBook cldf.AddressBook, chainSelector uint64, ctypes ...cldf.ContractType) error { + for _, ctype := range ctypes { + _, err := cldf.SearchAddressBook(addressBook, chainSelector, ctype) + if err != nil { + return fmt.Errorf("address book does not contain a %s contract for chain %d", ctype, chainSelector) + } + } + + return nil +} + +var solanaAddress = mcmssolanasdk.ContractAddress + +func envAddressBook(env cldf.Environment) cldf.AddressBook { + return env.ExistingAddresses //nolint:staticcheck // SA1019 until Solana MCMS preconditions use DataStore +} diff --git a/legacy/mcms/changesets/transfer_ownership_solana_test.go b/legacy/mcms/changesets/transfer_ownership_solana_test.go new file mode 100644 index 0000000..a1875b2 --- /dev/null +++ b/legacy/mcms/changesets/transfer_ownership_solana_test.go @@ -0,0 +1,131 @@ +package changesets_test + +import ( + "crypto/ecdsa" + "testing" + "time" + + "github.com/gagliardetto/solana-go" + chainselectors "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/quarantine" + "github.com/stretchr/testify/require" + + mcmschangesets "github.com/smartcontractkit/cld-changesets/legacy/mcms/changesets" + solstate "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana" + soltestutils "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana/testutils" + pdasol "github.com/smartcontractkit/cld-changesets/pkg/family/solana" + + accessControllerBindings "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/v0_1_1/access_controller" + mcmBindings "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/v0_1_1/mcm" + timelockBindings "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/v0_1_1/timelock" + + 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" + cldftesthelpers "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils/testhelpers" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime" + "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" +) + +func setupTransferOwnershipSolanaTest(t *testing.T) (*runtime.Runtime, uint64) { + t.Helper() + selector := chainselectors.TEST_22222222222222222222222222222222222222222222.Selector + programsPath, programIDs, ab := soltestutils.PreloadMCMS(t, selector) + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithSolanaContainer(t, []uint64{selector}, programsPath, programIDs), + environment.WithAddressBook(ab), + environment.WithLogger(logger.Test(t)), + )) + require.NoError(t, err) + + err = rt.Exec( + runtime.ChangesetTask(cldf.CreateLegacyChangeSet(mcmschangesets.DeployMCMSWithTimelockV2), map[uint64]cldfproposalutils.MCMSWithTimelockConfig{ + selector: cldftesthelpers.SingleGroupTimelockConfig(t), + }), + ) + require.NoError(t, err) + + return rt, selector +} + +func TestTransferToMCMSToTimelockSolana(t *testing.T) { + t.Parallel() + + quarantine.Flaky(t, "DX-1773") + + rt, selector := setupTransferOwnershipSolanaTest(t) + + addresses, err := rt.State().AddressBook.AddressesForChain(selector) + require.NoError(t, err) + + chain := rt.Environment().BlockChains.SolanaChains()[selector] + + mcmsState, err := solstate.MaybeLoadMCMSWithTimelockChainState(chain, addresses) + require.NoError(t, err) + + soltestutils.FundSignerPDAs(t, chain, mcmsState) + + deployer := rt.Environment().BlockChains.SolanaChains()[selector].DeployerKey.PublicKey() + assertTransferOwnershipSolanaOwner(t, chain, mcmsState, deployer) + + err = rt.Exec( + runtime.ChangesetTask(&mcmschangesets.TransferMCMSToTimelockSolana{}, mcmschangesets.TransferMCMSToTimelockSolanaConfig{ + Chains: []uint64{selector}, + MCMSCfg: cldfproposalutils.TimelockConfig{MinDelay: 1 * time.Second}, + }), + runtime.SignAndExecuteProposalsTask([]*ecdsa.PrivateKey{cldftesthelpers.TestXXXMCMSSigner}), + ) + require.NoError(t, err) + + timelockSignerPDA := pdasol.GetTimelockSignerPDA(mcmsState.TimelockProgram, mcmsState.TimelockSeed) + assertTransferOwnershipSolanaOwner(t, chain, mcmsState, timelockSignerPDA) +} + +func assertTransferOwnershipSolanaOwner( + t *testing.T, chain cldf_solana.Chain, mcmsState *solstate.MCMSWithTimelockState, owner solana.PublicKey, +) { + t.Helper() + + assertMCMOwner(t, owner, pdasol.GetMCMConfigPDA(mcmsState.McmProgram, mcmsState.ProposerMcmSeed), chain) + assertMCMOwner(t, owner, pdasol.GetMCMConfigPDA(mcmsState.McmProgram, mcmsState.CancellerMcmSeed), chain) + assertMCMOwner(t, owner, pdasol.GetMCMConfigPDA(mcmsState.McmProgram, mcmsState.BypasserMcmSeed), chain) + assertTimelockOwner(t, owner, pdasol.GetTimelockConfigPDA(mcmsState.TimelockProgram, mcmsState.TimelockSeed), chain) + assertAccessControllerOwner(t, owner, mcmsState.ProposerAccessControllerAccount, chain) + assertAccessControllerOwner(t, owner, mcmsState.ExecutorAccessControllerAccount, chain) + assertAccessControllerOwner(t, owner, mcmsState.CancellerAccessControllerAccount, chain) + assertAccessControllerOwner(t, owner, mcmsState.BypasserAccessControllerAccount, chain) +} + +func assertMCMOwner( + t *testing.T, want solana.PublicKey, configPDA solana.PublicKey, chain cldf_solana.Chain, +) { + t.Helper() + + var config mcmBindings.MultisigConfig + err := chain.GetAccountDataBorshInto(t.Context(), configPDA, &config) + require.NoError(t, err) + require.Equal(t, want, config.Owner) +} + +func assertTimelockOwner( + t *testing.T, want solana.PublicKey, configPDA solana.PublicKey, chain cldf_solana.Chain, +) { + t.Helper() + + var config timelockBindings.Config + err := chain.GetAccountDataBorshInto(t.Context(), configPDA, &config) + require.NoError(t, err) + require.Equal(t, want, config.Owner) +} + +func assertAccessControllerOwner( + t *testing.T, want solana.PublicKey, account solana.PublicKey, chain cldf_solana.Chain, +) { + t.Helper() + + var config accessControllerBindings.AccessController + err := chain.GetAccountDataBorshInto(t.Context(), account, &config) + require.NoError(t, err) + require.Equal(t, want, config.Owner) +}