diff --git a/chains/evm/deployment/fastcurse_test.go b/chains/evm/deployment/fastcurse_test.go index 596dcb805..cdbb5c07f 100644 --- a/chains/evm/deployment/fastcurse_test.go +++ b/chains/evm/deployment/fastcurse_test.go @@ -152,7 +152,7 @@ func TestFastCurse(t *testing.T) { evmDeployer := &adapters.EVMDeployer{} dReg := deploy.GetRegistry() dReg.RegisterDeployer(chainsel.FamilyEVM, deploy.MCMSVersion, evmDeployer) - cs := deploy.DeployMCMS(dReg) + cs := deploy.DeployMCMS(dReg, nil) evmChain1 := env.BlockChains.EVMChains()[chain1] evmChain2 := env.BlockChains.EVMChains()[chain2] output, err := cs.Apply(*env, deploy.MCMSDeploymentConfig{ @@ -459,7 +459,7 @@ func TestFastCurseGlobalCurseOnChain(t *testing.T) { evmDeployer := &adapters.EVMDeployer{} dReg := deploy.GetRegistry() dReg.RegisterDeployer(chainsel.FamilyEVM, deploy.MCMSVersion, evmDeployer) - cs := deploy.DeployMCMS(dReg) + cs := deploy.DeployMCMS(dReg, nil) mcmsChainInput := make(map[uint64]deploy.MCMSDeploymentConfigPerChain) for _, sel := range []uint64{chain1, chain2, chain3} { evmChain := env.BlockChains.EVMChains()[sel] diff --git a/chains/evm/deployment/v1_0_0/adapters/deployer_test.go b/chains/evm/deployment/v1_0_0/adapters/deployer_test.go index f6aa1b1d5..5ea0076f9 100644 --- a/chains/evm/deployment/v1_0_0/adapters/deployer_test.go +++ b/chains/evm/deployment/v1_0_0/adapters/deployer_test.go @@ -36,7 +36,7 @@ func TestDeployMCMS(t *testing.T) { evmDeployer := &adapters.EVMDeployer{} dReg := deployops.GetRegistry() dReg.RegisterDeployer(chainsel.FamilyEVM, deployops.MCMSVersion, evmDeployer) - cs := deployops.DeployMCMS(dReg) + cs := deployops.DeployMCMS(dReg, nil) output, err := cs.Apply(*env, deployops.MCMSDeploymentConfig{ AdapterVersion: semver.MustParse("1.0.0"), Chains: map[uint64]deployops.MCMSDeploymentConfigPerChain{ diff --git a/chains/evm/deployment/v1_0_0/adapters/transfer_ownership_test.go b/chains/evm/deployment/v1_0_0/adapters/transfer_ownership_test.go index 39390be67..bd976bdd2 100644 --- a/chains/evm/deployment/v1_0_0/adapters/transfer_ownership_test.go +++ b/chains/evm/deployment/v1_0_0/adapters/transfer_ownership_test.go @@ -49,7 +49,7 @@ func TestTransferOwnership(t *testing.T) { evmDeployer := &adapters.EVMDeployer{} dReg := deploy.GetRegistry() dReg.RegisterDeployer(chainsel.FamilyEVM, deploy.MCMSVersion, evmDeployer) - deployMCMS := deploy.DeployMCMS(dReg) + deployMCMS := deploy.DeployMCMS(dReg, nil) output, err := deployMCMS.Apply(*env, deploy.MCMSDeploymentConfig{ AdapterVersion: semver.MustParse("1.0.0"), Chains: map[uint64]deploy.MCMSDeploymentConfigPerChain{ diff --git a/chains/evm/deployment/v1_6_4/changesets/siloed_usdc_token_pool_deploy_test.go b/chains/evm/deployment/v1_6_4/changesets/siloed_usdc_token_pool_deploy_test.go index caaeb1574..873825d24 100644 --- a/chains/evm/deployment/v1_6_4/changesets/siloed_usdc_token_pool_deploy_test.go +++ b/chains/evm/deployment/v1_6_4/changesets/siloed_usdc_token_pool_deploy_test.go @@ -83,7 +83,7 @@ func TestSiloedUSDCTokenPoolDeployChangeset(t *testing.T) { evmDeployer := &adapters.EVMDeployer{} dReg := deploy.GetRegistry() dReg.RegisterDeployer(chain_selectors.FamilyEVM, deploy.MCMSVersion, evmDeployer) - cs := deploy.DeployMCMS(dReg) + cs := deploy.DeployMCMS(dReg, nil) output, err := cs.Apply(*e, deploy.MCMSDeploymentConfig{ AdapterVersion: semver.MustParse("1.0.0"), Chains: map[uint64]deploy.MCMSDeploymentConfigPerChain{ diff --git a/chains/evm/deployment/v1_6_4/changesets/update_lock_release_pool_addresses_test.go b/chains/evm/deployment/v1_6_4/changesets/update_lock_release_pool_addresses_test.go index 0c6c955dd..d4acc255d 100644 --- a/chains/evm/deployment/v1_6_4/changesets/update_lock_release_pool_addresses_test.go +++ b/chains/evm/deployment/v1_6_4/changesets/update_lock_release_pool_addresses_test.go @@ -124,7 +124,7 @@ func TestUpdateLockReleasePoolAddressesChangeset(t *testing.T) { evmDeployer := &adapters.EVMDeployer{} dReg := deploy.GetRegistry() dReg.RegisterDeployer(chain_selectors.FamilyEVM, deploy.MCMSVersion, evmDeployer) - cs := deploy.DeployMCMS(dReg) + cs := deploy.DeployMCMS(dReg, nil) output, err := cs.Apply(*e, deploy.MCMSDeploymentConfig{ AdapterVersion: semver.MustParse("1.0.0"), Chains: map[uint64]deploy.MCMSDeploymentConfigPerChain{ diff --git a/chains/evm/deployment/v1_6_4/changesets/usdc_token_pool_cctp_v2_deploy_test.go b/chains/evm/deployment/v1_6_4/changesets/usdc_token_pool_cctp_v2_deploy_test.go index 41b96f3a6..3c9113f82 100644 --- a/chains/evm/deployment/v1_6_4/changesets/usdc_token_pool_cctp_v2_deploy_test.go +++ b/chains/evm/deployment/v1_6_4/changesets/usdc_token_pool_cctp_v2_deploy_test.go @@ -109,7 +109,7 @@ func TestUSDCTokenPoolCCTPV2DeployChangeset(t *testing.T) { evmDeployer := &adapters.EVMDeployer{} dReg := deploy.GetRegistry() dReg.RegisterDeployer(chain_selectors.FamilyEVM, deploy.MCMSVersion, evmDeployer) - cs := deploy.DeployMCMS(dReg) + cs := deploy.DeployMCMS(dReg, nil) output, err := cs.Apply(*e, deploy.MCMSDeploymentConfig{ AdapterVersion: semver.MustParse("1.0.0"), Chains: map[uint64]deploy.MCMSDeploymentConfigPerChain{ diff --git a/chains/evm/deployment/v1_6_4/changesets/usdc_token_pool_deploy_test.go b/chains/evm/deployment/v1_6_4/changesets/usdc_token_pool_deploy_test.go index 1b0aada17..532d8e992 100644 --- a/chains/evm/deployment/v1_6_4/changesets/usdc_token_pool_deploy_test.go +++ b/chains/evm/deployment/v1_6_4/changesets/usdc_token_pool_deploy_test.go @@ -111,7 +111,7 @@ func TestUSDCTokenPoolDeployChangeset_NoExisting_MessageTransmitter_Proxy(t *tes evmDeployer := &adapters.EVMDeployer{} dReg := deploy.GetRegistry() dReg.RegisterDeployer(chain_selectors.FamilyEVM, deploy.MCMSVersion, evmDeployer) - cs := deploy.DeployMCMS(dReg) + cs := deploy.DeployMCMS(dReg, nil) output, err := cs.Apply(*e, deploy.MCMSDeploymentConfig{ AdapterVersion: semver.MustParse("1.0.0"), Chains: map[uint64]deploy.MCMSDeploymentConfigPerChain{ @@ -312,7 +312,7 @@ func TestUSDCTokenPoolDeployChangeset_Existing_MessageTransmitter_Proxy(t *testi evmDeployer := &adapters.EVMDeployer{} dReg := deploy.GetRegistry() dReg.RegisterDeployer(chain_selectors.FamilyEVM, deploy.MCMSVersion, evmDeployer) - cs := deploy.DeployMCMS(dReg) + cs := deploy.DeployMCMS(dReg, nil) output, err := cs.Apply(*e, deploy.MCMSDeploymentConfig{ AdapterVersion: semver.MustParse("1.0.0"), Chains: map[uint64]deploy.MCMSDeploymentConfigPerChain{ diff --git a/chains/evm/deployment/v1_6_4/changesets/usdc_token_pool_proxy_deploy_test.go b/chains/evm/deployment/v1_6_4/changesets/usdc_token_pool_proxy_deploy_test.go index 67fe9c551..e871f9f38 100644 --- a/chains/evm/deployment/v1_6_4/changesets/usdc_token_pool_proxy_deploy_test.go +++ b/chains/evm/deployment/v1_6_4/changesets/usdc_token_pool_proxy_deploy_test.go @@ -73,7 +73,7 @@ func TestDeployUSDCTokenPoolProxyChangeset(t *testing.T) { evmDeployer := &adapters.EVMDeployer{} dReg := deploy.GetRegistry() dReg.RegisterDeployer(chain_selectors.FamilyEVM, deploy.MCMSVersion, evmDeployer) - cs := deploy.DeployMCMS(dReg) + cs := deploy.DeployMCMS(dReg, nil) output, err := cs.Apply(*e, deploy.MCMSDeploymentConfig{ AdapterVersion: semver.MustParse("1.0.0"), Chains: map[uint64]deploy.MCMSDeploymentConfigPerChain{ diff --git a/chains/solana/deployment/utils/upgrade_authority.go b/chains/solana/deployment/utils/upgrade_authority.go new file mode 100644 index 000000000..4f62059fb --- /dev/null +++ b/chains/solana/deployment/utils/upgrade_authority.go @@ -0,0 +1,107 @@ +package utils + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + + "github.com/gagliardetto/solana-go" + solRpc "github.com/gagliardetto/solana-go/rpc" +) + +// UpgradeableLoaderState mirrors the Rust enum in the Solana SDK. +type UpgradeableLoaderState struct { + Type uint32 + Program *Program + ProgramData *ProgramData + Uninitialized bool +} + +// Program holds the address of the ProgramData account. +type Program struct { + ProgramData solana.PublicKey +} + +// ProgramData holds the optional UpgradeAuthority. +type ProgramData struct { + Slot uint64 + AuthorityOption uint32 // 0 = none, 1 = present + Authority solana.PublicKey +} + +func decodeUpgradeableLoaderState(data []byte) (*UpgradeableLoaderState, error) { + if len(data) < 4 { + return nil, errors.New("data too short") + } + state := &UpgradeableLoaderState{} + state.Type = binary.LittleEndian.Uint32(data[:4]) + + switch state.Type { + case 2: // Program + if len(data) < 36 { + return nil, errors.New("program data too short") + } + state.Program = &Program{ + ProgramData: solana.PublicKeyFromBytes(data[4:36]), + } + case 3: // ProgramData + slot := binary.LittleEndian.Uint64(data[4:12]) + opt := data[12] + var auth *solana.PublicKey + if opt == 1 { + if len(data) < 45 { + return nil, errors.New("missing authority pubkey") + } + pk := solana.PublicKeyFromBytes(data[13:45]) + auth = &pk + } + state.ProgramData = &ProgramData{ + Slot: slot, + AuthorityOption: uint32(opt), + } + if state.ProgramData.AuthorityOption == 1 { + state.ProgramData.Authority = *auth + } + default: + // other variants (Uninitialized, Buffer) are not needed here + } + return state, nil +} + +func getUpgradeableLoaderState(client *solRpc.Client, progPubkey solana.PublicKey) (*UpgradeableLoaderState, error) { + resp, err := client.GetAccountInfo(context.Background(), progPubkey) + if err != nil { + return nil, fmt.Errorf("failed to fetch program account: %w", err) + } + if resp.Value == nil { + return nil, errors.New("program account does not exist") + } + + state, err := decodeUpgradeableLoaderState(resp.Value.Data.GetBinary()) + if err != nil { + return nil, fmt.Errorf("decode error: %w", err) + } + return state, nil +} + +func GetUpgradeAuthority(client *solRpc.Client, progDataPubkey solana.PublicKey) (solana.PublicKey, error) { + data, err := GetSolProgramData(client, progDataPubkey) + if err != nil { + return solana.PublicKey{}, fmt.Errorf("failed to get program data for %s: %w", progDataPubkey.String(), err) + } + state, err := getUpgradeableLoaderState(client, data.Address) + if err != nil { + return solana.PublicKey{}, fmt.Errorf("failed to get upgrade authority for program data %s: %w", progDataPubkey.String(), err) + } + + if state.ProgramData == nil { + return solana.PublicKey{}, errors.New("unexpected state: no programdata") + } + + if state.ProgramData.AuthorityOption == 0 { + // No authority – the program is immutable + return solana.PublicKey{}, nil + } + return state.ProgramData.Authority, nil +} diff --git a/chains/solana/deployment/utils/utils.go b/chains/solana/deployment/utils/utils.go index 03abb6935..284f5b431 100644 --- a/chains/solana/deployment/utils/utils.go +++ b/chains/solana/deployment/utils/utils.go @@ -27,7 +27,7 @@ func GetSolProgramSize(chain cldf_solana.Chain, programID solana.PublicKey) (int return programBytes, nil } -func GetSolProgramData(chain cldf_solana.Chain, programID solana.PublicKey) (struct { +func GetSolProgramData(client *solrpc.Client, programID solana.PublicKey) (struct { DataType uint32 Address solana.PublicKey }, error) { @@ -35,7 +35,7 @@ func GetSolProgramData(chain cldf_solana.Chain, programID solana.PublicKey) (str DataType uint32 Address solana.PublicKey } - data, err := chain.Client.GetAccountInfoWithOpts(context.Background(), programID, &solrpc.GetAccountInfoOpts{ + data, err := client.GetAccountInfoWithOpts(context.Background(), programID, &solrpc.GetAccountInfoOpts{ Commitment: solrpc.CommitmentConfirmed, }) if err != nil { diff --git a/chains/solana/deployment/v1_6_0/operations/fee_quoter/fee_quoter.go b/chains/solana/deployment/v1_6_0/operations/fee_quoter/fee_quoter.go index 1c9510e45..f2661a0c7 100644 --- a/chains/solana/deployment/v1_6_0/operations/fee_quoter/fee_quoter.go +++ b/chains/solana/deployment/v1_6_0/operations/fee_quoter/fee_quoter.go @@ -55,7 +55,7 @@ var Initialize = operations.NewOperation( "Initializes the FeeQuoter 1.6.0 contract", func(b operations.Bundle, chain cldf_solana.Chain, input Params) (sequences.OnChainOutput, error) { fee_quoter.SetProgramID(input.FeeQuoter) - programData, err := utils.GetSolProgramData(chain, input.FeeQuoter) + programData, err := utils.GetSolProgramData(chain.Client, input.FeeQuoter) if err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to get program data: %w", err) } diff --git a/chains/solana/deployment/v1_6_0/operations/mcms/mcms.go b/chains/solana/deployment/v1_6_0/operations/mcms/mcms.go index 3afebb538..6057de4fd 100644 --- a/chains/solana/deployment/v1_6_0/operations/mcms/mcms.go +++ b/chains/solana/deployment/v1_6_0/operations/mcms/mcms.go @@ -160,6 +160,11 @@ type ( Qualifier string } + MCMOutput struct { + NewAddresses []cldf_datastore.AddressRef + BatchOps []types.BatchOperation + } + AddAccessInput struct { Role timelock.Role Accounts []solana.PublicKey @@ -270,7 +275,7 @@ func initializeAccessController( return nil } -func initMCM(b operations.Bundle, deps Deps, in InitMCMInput) ([]cldf_datastore.AddressRef, error) { +func initMCM(b operations.Bundle, deps Deps, in InitMCMInput) (MCMOutput, error) { mcm.SetProgramID(in.MCM) // Should be one of: // BypasserSeed @@ -292,7 +297,7 @@ func initMCM(b operations.Bundle, deps Deps, in InitMCMInput) ([]cldf_datastore. case utils.ProposerSeed: outType = cldf_datastore.ContractType(common_utils.ProposerManyChainMultisig) default: - return []cldf_datastore.AddressRef{}, fmt.Errorf("unsupported mcm contract type: %s", in.ContractType) + return MCMOutput{}, fmt.Errorf("unsupported mcm contract type: %s", in.ContractType) } var mcmSeed state.PDASeed @@ -303,18 +308,32 @@ func initMCM(b operations.Bundle, deps Deps, in InitMCMInput) ([]cldf_datastore. err := common.GetAccountDataBorshInto(b.GetContext(), deps.Chain.Client, mcmConfigPDA, rpc.CommitmentConfirmed, &data) if err == nil { b.Logger.Infow("mcm config already initialized, skipping initialization", "chain", deps.Chain.String()) - return []cldf_datastore.AddressRef{}, nil + return MCMOutput{}, nil } - return []cldf_datastore.AddressRef{}, fmt.Errorf("unable to read mcm ConfigPDA account config %q", mcmConfigPDA.String()) + return MCMOutput{}, fmt.Errorf("unable to read mcm ConfigPDA account config %q", mcmConfigPDA.String()) } b.Logger.Infow("mcm config not initialized, initializing", "chain", deps.Chain.String()) seed := randomSeed() b.Logger.Infow("generated MCM seed", "seed", string(seed[:])) - err := initializeMCM(b, deps, in.MCM, seed) + ixns, err := initializeMCM(b, deps, in.MCM, seed) if err != nil { - return []cldf_datastore.AddressRef{}, fmt.Errorf("failed to initialize mcm: %w", err) + return MCMOutput{}, fmt.Errorf("failed to initialize mcm: %w", err) + } + + var batches []types.BatchOperation + if len(ixns) > 0 { + batch, err := utils.BuildMCMSBatchOperation( + deps.Chain.Selector, + ixns, + in.MCM.String(), + in.ContractType.String(), + ) + if err != nil { + return MCMOutput{}, fmt.Errorf("failed to build timelock initialization batch operation: %w", err) + } + batches = append(batches, batch) } encodedAddress := mcms_solana.ContractAddress(in.MCM, mcms_solana.PDASeed(seed)) @@ -322,37 +341,39 @@ func initMCM(b operations.Bundle, deps Deps, in InitMCMInput) ([]cldf_datastore. configurer := mcms_solana.NewConfigurer(deps.Chain.Client, *deps.Chain.DeployerKey, types.ChainSelector(deps.Chain.ChainSelector())) tx, err := configurer.SetConfig(b.GetContext(), encodedAddress, &in.MCMConfig, false) if err != nil { - return []cldf_datastore.AddressRef{}, fmt.Errorf("failed to set config on mcm: %w", err) + return MCMOutput{}, fmt.Errorf("failed to set config on mcm: %w", err) } b.Logger.Infow("called SetConfig on MCM", "transaction", tx.Hash) - return []cldf_datastore.AddressRef{ - { - Address: mcms_solana.ContractAddress( - solana.MustPublicKeyFromBase58(in.MCM.String()), - mcms_solana.PDASeed([]byte(seed[:])), - ), - ChainSelector: deps.Chain.Selector, - Type: outType, - Qualifier: deps.Qualifier, - Version: common_utils.Version_1_6_0, - }, - { - Address: string(seed[:]), - ChainSelector: deps.Chain.Selector, - Type: cldf_datastore.ContractType(in.ContractType), - Qualifier: deps.Qualifier, - Version: common_utils.Version_1_6_0, - }, - }, nil + return MCMOutput{ + BatchOps: batches, + NewAddresses: []cldf_datastore.AddressRef{ + { + Address: mcms_solana.ContractAddress( + solana.MustPublicKeyFromBase58(in.MCM.String()), + mcms_solana.PDASeed([]byte(seed[:])), + ), + ChainSelector: deps.Chain.Selector, + Type: outType, + Qualifier: deps.Qualifier, + Version: common_utils.Version_1_6_0, + }, + { + Address: string(seed[:]), + ChainSelector: deps.Chain.Selector, + Type: cldf_datastore.ContractType(in.ContractType), + Qualifier: deps.Qualifier, + Version: common_utils.Version_1_6_0, + }, + }}, nil } -func initializeMCM(b operations.Bundle, deps Deps, mcmProgram solana.PublicKey, multisigID state.PDASeed) error { +func initializeMCM(b operations.Bundle, deps Deps, mcmProgram solana.PublicKey, multisigID state.PDASeed) ([]solana.Instruction, error) { var mcmConfig mcm.MultisigConfig err := deps.Chain.GetAccountDataBorshInto(b.GetContext(), state.GetMCMConfigPDA(mcmProgram, multisigID), &mcmConfig) if err == nil { b.Logger.Infow("MCM already initialized, skipping initialization", "chain", deps.Chain.String()) - return nil + return nil, nil } var programData struct { @@ -363,18 +384,23 @@ func initializeMCM(b operations.Bundle, deps Deps, mcmProgram solana.PublicKey, data, err := deps.Chain.Client.GetAccountInfoWithOpts(b.GetContext(), mcmProgram, opts) if err != nil { - return fmt.Errorf("failed to get mcm program account info: %w", err) + return nil, fmt.Errorf("failed to get mcm program account info: %w", err) } err = bin.UnmarshalBorsh(&programData, data.Bytes()) if err != nil { - return fmt.Errorf("failed to unmarshal program data: %w", err) + return nil, fmt.Errorf("failed to unmarshal program data: %w", err) + } + + upgradeAuthority, err := utils.GetUpgradeAuthority(deps.Chain.Client, mcmProgram) + if err != nil { + return nil, fmt.Errorf("failed to get upgrade authority: %w", err) } instruction, err := mcm.NewInitializeInstruction( deps.Chain.Selector, multisigID, state.GetMCMConfigPDA(mcmProgram, multisigID), - deps.Chain.DeployerKey.PublicKey(), + upgradeAuthority, solana.SystemProgramID, mcmProgram, programData.Address, @@ -382,18 +408,22 @@ func initializeMCM(b operations.Bundle, deps Deps, mcmProgram solana.PublicKey, state.GetMCMExpiringRootAndOpCountPDA(mcmProgram, multisigID), ).ValidateAndBuild() if err != nil { - return fmt.Errorf("failed to build instruction: %w", err) + return nil, fmt.Errorf("failed to build instruction: %w", err) } - err = deps.Chain.Confirm([]solana.Instruction{instruction}) - if err != nil { - return fmt.Errorf("failed to confirm instructions: %w", err) + if upgradeAuthority == deps.Chain.DeployerKey.PublicKey() { + err = deps.Chain.Confirm([]solana.Instruction{instruction}) + if err != nil { + return nil, fmt.Errorf("failed to confirm instructions: %w", err) + } + } else { + b.Logger.Infow("skipping confirm of initialize instruction as upgrade authority is not deployer key ", "chain", deps.Chain.String()) + return []solana.Instruction{instruction}, nil } - - return nil + return nil, nil } -func initTimelock(b operations.Bundle, deps Deps, in InitTimelockInput) ([]cldf_datastore.AddressRef, error) { +func initTimelock(b operations.Bundle, deps Deps, in InitTimelockInput) (MCMOutput, error) { timelock.SetProgramID(in.Timelock) // Should be one of: // RBACTimelockSeed @@ -413,9 +443,9 @@ func initTimelock(b operations.Bundle, deps Deps, in InitTimelockInput) ([]cldf_ err := deps.Chain.GetAccountDataBorshInto(b.GetContext(), timelockConfigPDA, &timelockConfig) if err == nil { b.Logger.Infow("timelock config already initialized, skipping initialization", "chain", deps.Chain.String()) - return []cldf_datastore.AddressRef{}, nil + return MCMOutput{}, nil } - return []cldf_datastore.AddressRef{}, fmt.Errorf("unable to read timelock ConfigPDA account config %s", timelockConfigPDA.String()) + return MCMOutput{}, fmt.Errorf("unable to read timelock ConfigPDA account config %s", timelockConfigPDA.String()) } b.Logger.Infow("timelock config not initialized, initializing", "chain", deps.Chain.String()) @@ -423,34 +453,49 @@ func initTimelock(b operations.Bundle, deps Deps, in InitTimelockInput) ([]cldf_ seed := randomSeed() b.Logger.Infow("generated Timelock seed", "seed", string(seed[:])) - err := initializeTimelock(b, deps, in.Timelock, seed, in.MinDelay) + ixns, err := initializeTimelock(b, deps, in.Timelock, seed, in.MinDelay) if err != nil { - return []cldf_datastore.AddressRef{}, fmt.Errorf("failed to initialize timelock: %w", err) - } - - return []cldf_datastore.AddressRef{ - { - Address: mcms_solana.ContractAddress( - solana.MustPublicKeyFromBase58(in.Timelock.String()), - mcms_solana.PDASeed([]byte(seed[:])), - ), - ChainSelector: deps.Chain.Selector, - Type: cldf_datastore.ContractType(common_utils.RBACTimelock), - Qualifier: deps.Qualifier, - Version: common_utils.Version_1_6_0, - }, - { - Address: string(seed[:]), - ChainSelector: deps.Chain.Selector, - Type: cldf_datastore.ContractType(in.ContractType), - Qualifier: deps.Qualifier, - Version: common_utils.Version_1_6_0, - }, - }, nil + return MCMOutput{}, fmt.Errorf("failed to initialize timelock: %w", err) + } + var batches []types.BatchOperation + if len(ixns) > 0 { + batch, err := utils.BuildMCMSBatchOperation( + deps.Chain.Selector, + ixns, + in.Timelock.String(), + in.ContractType.String(), + ) + if err != nil { + return MCMOutput{}, fmt.Errorf("failed to build timelock initialization batch operation: %w", err) + } + batches = append(batches, batch) + } + + return MCMOutput{ + BatchOps: batches, + NewAddresses: []cldf_datastore.AddressRef{ + { + Address: mcms_solana.ContractAddress( + solana.MustPublicKeyFromBase58(in.Timelock.String()), + mcms_solana.PDASeed([]byte(seed[:])), + ), + ChainSelector: deps.Chain.Selector, + Type: cldf_datastore.ContractType(common_utils.RBACTimelock), + Qualifier: deps.Qualifier, + Version: common_utils.Version_1_6_0, + }, + { + Address: string(seed[:]), + ChainSelector: deps.Chain.Selector, + Type: cldf_datastore.ContractType(in.ContractType), + Qualifier: deps.Qualifier, + Version: common_utils.Version_1_6_0, + }, + }}, nil } func initializeTimelock(b operations.Bundle, deps Deps, timelockProgram solana.PublicKey, - timelockID state.PDASeed, minDelay *big.Int) error { + timelockID state.PDASeed, minDelay *big.Int) ([]solana.Instruction, error) { if minDelay == nil { minDelay = big.NewInt(0) } @@ -460,7 +505,7 @@ func initializeTimelock(b operations.Bundle, deps Deps, timelockProgram solana.P &timelockConfig) if err == nil { b.Logger.Infow("Timelock already initialized, skipping initialization", "chain", deps.Chain.String()) - return nil + return nil, nil } var programData struct { @@ -471,11 +516,11 @@ func initializeTimelock(b operations.Bundle, deps Deps, timelockProgram solana.P data, err := deps.Chain.Client.GetAccountInfoWithOpts(b.GetContext(), timelockProgram, opts) if err != nil { - return fmt.Errorf("failed to get timelock program account info: %w", err) + return nil, fmt.Errorf("failed to get timelock program account info: %w", err) } err = bin.UnmarshalBorsh(&programData, data.Bytes()) if err != nil { - return fmt.Errorf("failed to unmarshal program data: %w", err) + return nil, fmt.Errorf("failed to unmarshal program data: %w", err) } accessControllerProgram := datastore.GetAddressRef( @@ -514,11 +559,16 @@ func initializeTimelock(b operations.Bundle, deps Deps, timelockProgram solana.P deps.Qualifier, ) + upgradeAuthority, err := utils.GetUpgradeAuthority(deps.Chain.Client, timelockProgram) + if err != nil { + return nil, fmt.Errorf("failed to get upgrade authority: %w", err) + } + instruction, err := timelock.NewInitializeInstruction( timelockID, minDelay.Uint64(), state.GetTimelockConfigPDA(timelockProgram, timelockID), - deps.Chain.DeployerKey.PublicKey(), + upgradeAuthority, solana.SystemProgramID, timelockProgram, programData.Address, @@ -529,18 +579,23 @@ func initializeTimelock(b operations.Bundle, deps Deps, timelockProgram solana.P solana.MustPublicKeyFromBase58(bypasserAccount.Address), ).ValidateAndBuild() if err != nil { - return fmt.Errorf("failed to build instruction: %w", err) + return nil, fmt.Errorf("failed to build instruction: %w", err) } - err = deps.Chain.Confirm([]solana.Instruction{instruction}) - if err != nil { - return fmt.Errorf("failed to confirm instructions: %w", err) + if upgradeAuthority == deps.Chain.DeployerKey.PublicKey() { + err = deps.Chain.Confirm([]solana.Instruction{instruction}) + if err != nil { + return nil, fmt.Errorf("failed to confirm instructions: %w", err) + } + } else { + b.Logger.Infow("skipping confirm of initialize instruction as upgrade authority is not deployer key") + return []solana.Instruction{instruction}, nil } - return nil + return nil, nil } -func addAccess(b operations.Bundle, deps Deps, in AddAccessInput) (cldf_datastore.AddressRef, error) { +func addAccess(b operations.Bundle, deps Deps, in AddAccessInput) (MCMOutput, error) { accessControllerProgram := datastore.GetAddressRef( deps.ExistingAddresses, deps.Chain.Selector, @@ -571,7 +626,7 @@ func addAccess(b operations.Bundle, deps Deps, in AddAccessInput) (cldf_datastor case timelock.Bypasser_Role: roleAccessController = utils.BypasserAccessControllerAccount default: - return cldf_datastore.AddressRef{}, fmt.Errorf("unknown role: %d", in.Role) + return MCMOutput{}, fmt.Errorf("unknown role: %d", in.Role) } roleAccount := datastore.GetAddressRef( deps.ExistingAddresses, @@ -580,13 +635,18 @@ func addAccess(b operations.Bundle, deps Deps, in AddAccessInput) (cldf_datastor common_utils.Version_1_6_0, in.Qualifier, ) + upgradeAuthority, err := utils.GetUpgradeAuthority(deps.Chain.Client, id) + if err != nil { + return MCMOutput{}, fmt.Errorf("failed to get upgrade authority: %w", err) + } + instructionBuilder := timelock.NewBatchAddAccessInstruction([32]uint8( state.PDASeed([]byte(seed[:]))), in.Role, timelockConfigPDA, solana.MustPublicKeyFromBase58(accessControllerProgram.Address), solana.MustPublicKeyFromBase58(roleAccount.Address), - deps.Chain.DeployerKey.PublicKey()) + upgradeAuthority) for _, account := range in.Accounts { instructionBuilder.Append(solana.Meta(account)) @@ -594,14 +654,29 @@ func addAccess(b operations.Bundle, deps Deps, in AddAccessInput) (cldf_datastor instruction, err := instructionBuilder.ValidateAndBuild() if err != nil { - return cldf_datastore.AddressRef{}, fmt.Errorf("failed to build BatchAddAccess instruction: %w", err) + return MCMOutput{}, fmt.Errorf("failed to build BatchAddAccess instruction: %w", err) + } + + if upgradeAuthority != deps.Chain.DeployerKey.PublicKey() { + batch, err := utils.BuildMCMSBatchOperation( + deps.Chain.Selector, + []solana.Instruction{instruction}, + id.String(), + utils.TimelockProgramType.String(), + ) + if err != nil { + return MCMOutput{}, fmt.Errorf("failed to build timelock initialization batch operation: %w", err) + } + return MCMOutput{ + BatchOps: []types.BatchOperation{batch}, + }, nil } err = deps.Chain.Confirm([]solana.Instruction{instruction}) if err != nil { - return cldf_datastore.AddressRef{}, fmt.Errorf("failed to confirm BatchAddAccess instruction: %w", err) + return MCMOutput{}, fmt.Errorf("failed to confirm BatchAddAccess instruction: %w", err) } - return cldf_datastore.AddressRef{}, nil + return MCMOutput{}, nil } func transferToTimelockSolanaOp(b operations.Bundle, deps Deps, in TransferToTimelockInput) ([]types.BatchOperation, error) { @@ -617,7 +692,7 @@ func transferToTimelockSolanaOp(b operations.Bundle, deps Deps, in TransferToTim contract.Seed, in.NewOwner, contract.OwnerPDA, - solChain.DeployerKey.PublicKey()) + in.CurrentOwner) if err != nil { return out, fmt.Errorf("failed to create transfer ownership instruction: %w", err) } diff --git a/chains/solana/deployment/v1_6_0/operations/offramp/offramp.go b/chains/solana/deployment/v1_6_0/operations/offramp/offramp.go index 8ebed3419..b27134065 100644 --- a/chains/solana/deployment/v1_6_0/operations/offramp/offramp.go +++ b/chains/solana/deployment/v1_6_0/operations/offramp/offramp.go @@ -72,7 +72,7 @@ var Initialize = operations.NewOperation( "Initializes the OffRamp 1.6.0 contract", func(b operations.Bundle, chain cldf_solana.Chain, input Params) (sequences.OnChainOutput, error) { ccip_offramp.SetProgramID(input.OffRamp) - programData, err := utils.GetSolProgramData(chain, input.OffRamp) + programData, err := utils.GetSolProgramData(chain.Client, input.OffRamp) if err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to get program data: %w", err) } @@ -125,7 +125,7 @@ var InitializeConfig = operations.NewOperation( "Initializes the config of the OffRamp 1.6.0 contract", func(b operations.Bundle, chain cldf_solana.Chain, input Params) (sequences.OnChainOutput, error) { ccip_offramp.SetProgramID(input.OffRamp) - programData, err := utils.GetSolProgramData(chain, input.OffRamp) + programData, err := utils.GetSolProgramData(chain.Client, input.OffRamp) if err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to get program data: %w", err) } diff --git a/chains/solana/deployment/v1_6_0/operations/rmn_remote/rmn_remote.go b/chains/solana/deployment/v1_6_0/operations/rmn_remote/rmn_remote.go index 064350b0a..2e93bf986 100644 --- a/chains/solana/deployment/v1_6_0/operations/rmn_remote/rmn_remote.go +++ b/chains/solana/deployment/v1_6_0/operations/rmn_remote/rmn_remote.go @@ -59,7 +59,7 @@ var Initialize = operations.NewOperation( "Initializes the RMNRemote 1.6.0 contract", func(b operations.Bundle, chain cldf_solana.Chain, input Params) (sequences.OnChainOutput, error) { rmn_remote.SetProgramID(input.RMNRemote) - programData, err := utils.GetSolProgramData(chain, input.RMNRemote) + programData, err := utils.GetSolProgramData(chain.Client, input.RMNRemote) if err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to get program data: %w", err) } diff --git a/chains/solana/deployment/v1_6_0/operations/router/router.go b/chains/solana/deployment/v1_6_0/operations/router/router.go index 84b308307..3654cd2e0 100644 --- a/chains/solana/deployment/v1_6_0/operations/router/router.go +++ b/chains/solana/deployment/v1_6_0/operations/router/router.go @@ -57,7 +57,7 @@ var Initialize = operations.NewOperation( "Initializes the Router 1.6.0 contract", func(b operations.Bundle, chain cldf_solana.Chain, input Params) (sequences.OnChainOutput, error) { ccip_router.SetProgramID(input.Router) - programData, err := utils.GetSolProgramData(chain, input.Router) + programData, err := utils.GetSolProgramData(chain.Client, input.Router) if err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to get program data: %w", err) } diff --git a/chains/solana/deployment/v1_6_0/sequences/mcms.go b/chains/solana/deployment/v1_6_0/sequences/mcms.go index 85d242136..90dccc05b 100644 --- a/chains/solana/deployment/v1_6_0/sequences/mcms.go +++ b/chains/solana/deployment/v1_6_0/sequences/mcms.go @@ -75,21 +75,25 @@ func (d *SolanaAdapter) DeployMCMS() *operations.Sequence[ccipapi.MCMSDeployment if err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to initialize MCMs: %w", err) } - output.Addresses = append(output.Addresses, initMcmRef...) - deps.ExistingAddresses = append(deps.ExistingAddresses, initMcmRef...) + output.Addresses = append(output.Addresses, initMcmRef.NewAddresses...) + output.BatchOps = append(output.BatchOps, initMcmRef.BatchOps...) + deps.ExistingAddresses = append(deps.ExistingAddresses, initMcmRef.NewAddresses...) initTimelockRef, err := initTimelock(b, deps, in.TimelockMinDelay, timelockAddress) if err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to initialize Timelock: %w", err) } - output.Addresses = append(output.Addresses, initTimelockRef...) - deps.ExistingAddresses = append(deps.ExistingAddresses, initTimelockRef...) + output.Addresses = append(output.Addresses, initTimelockRef.NewAddresses...) + output.BatchOps = append(output.BatchOps, initTimelockRef.BatchOps...) + deps.ExistingAddresses = append(deps.ExistingAddresses, initTimelockRef.NewAddresses...) // roles - err = setupRoles(b, deps, mcmAddress) + setupRolesOutput, err := setupRoles(b, deps, mcmAddress) if err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to setup roles in Timelock: %w", err) } + output.Addresses = append(output.Addresses, setupRolesOutput.NewAddresses...) + output.BatchOps = append(output.BatchOps, setupRolesOutput.BatchOps...) return output, err }, @@ -121,8 +125,8 @@ func initAccessController(b operations.Bundle, deps mcmsops.Deps, accessControll return refs, nil } -func initMCM(b operations.Bundle, deps mcmsops.Deps, cfg ccipapi.MCMSDeploymentConfigPerChain, mcmAddress solana.PublicKey) ([]cldf_datastore.AddressRef, error) { - var refs []cldf_datastore.AddressRef +func initMCM(b operations.Bundle, deps mcmsops.Deps, cfg ccipapi.MCMSDeploymentConfigPerChain, mcmAddress solana.PublicKey) (mcmsops.MCMOutput, error) { + var output mcmsops.MCMOutput configs := []struct { ctype cldf_deployment.ContractType cfg types.Config @@ -151,14 +155,15 @@ func initMCM(b operations.Bundle, deps mcmsops.Deps, cfg ccipapi.MCMSDeploymentC Qualifier: deps.Qualifier, // used for testing purposes }) if err != nil { - return nil, fmt.Errorf("failed to init config type:%q, err:%w", cfg.ctype, err) + return mcmsops.MCMOutput{}, fmt.Errorf("failed to init config type:%q, err:%w", cfg.ctype, err) } - refs = append(refs, ref.Output...) + output.NewAddresses = append(output.NewAddresses, ref.Output.NewAddresses...) + output.BatchOps = append(output.BatchOps, ref.Output.BatchOps...) } - return refs, nil + return output, nil } -func initTimelock(b operations.Bundle, deps mcmsops.Deps, minDelay *big.Int, timelockAddress solana.PublicKey) ([]cldf_datastore.AddressRef, error) { +func initTimelock(b operations.Bundle, deps mcmsops.Deps, minDelay *big.Int, timelockAddress solana.PublicKey) (mcmsops.MCMOutput, error) { ref, err := operations.ExecuteOperation(b, mcmsops.InitTimelockOp, deps, mcmsops.InitTimelockInput{ ContractType: utils.RBACTimelockSeed, ChainSel: deps.Chain.ChainSelector(), @@ -167,12 +172,13 @@ func initTimelock(b operations.Bundle, deps mcmsops.Deps, minDelay *big.Int, tim Qualifier: deps.Qualifier, // used for testing purposes }) if err != nil { - return nil, fmt.Errorf("failed to init timelock: %w", err) + return mcmsops.MCMOutput{}, fmt.Errorf("failed to init timelock: %w", err) } return ref.Output, nil } -func setupRoles(b operations.Bundle, deps mcmsops.Deps, mcmProgram solana.PublicKey) error { +func setupRoles(b operations.Bundle, deps mcmsops.Deps, mcmProgram solana.PublicKey) (mcmsops.MCMOutput, error) { + var output mcmsops.MCMOutput proposerRef := datastore.GetAddressRef( deps.ExistingAddresses, deps.Chain.ChainSelector(), @@ -219,16 +225,18 @@ func setupRoles(b operations.Bundle, deps mcmsops.Deps, mcmProgram solana.Public }, } for _, role := range roles { - _, err := operations.ExecuteOperation(b, mcmsops.AddAccessOp, deps, mcmsops.AddAccessInput{ + out, err := operations.ExecuteOperation(b, mcmsops.AddAccessOp, deps, mcmsops.AddAccessInput{ Role: role.role, Accounts: role.pdas, Qualifier: deps.Qualifier, // used for testing purposes }) if err != nil { - return fmt.Errorf("failed to add access for role %d: %w", role.role, err) + return mcmsops.MCMOutput{}, fmt.Errorf("failed to add access for role %d: %w", role.role, err) } + output.NewAddresses = append(output.NewAddresses, out.Output.NewAddresses...) + output.BatchOps = append(output.BatchOps, out.Output.BatchOps...) } - return nil + return output, nil } // assume refs are in the order returned by GetAllMCMS diff --git a/deployment/deploy/mcms.go b/deployment/deploy/mcms.go index e12595120..b7bb57a2f 100644 --- a/deployment/deploy/mcms.go +++ b/deployment/deploy/mcms.go @@ -10,6 +10,7 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + mcms_types "github.com/smartcontractkit/mcms/types" mcmstypes "github.com/smartcontractkit/mcms/types" "github.com/smartcontractkit/chainlink-ccip/deployment/utils/changesets" @@ -37,11 +38,11 @@ type MCMSDeploymentConfigPerChainWithAddress struct { ExistingAddresses []datastore.AddressRef } -func DeployMCMS(deployerReg *DeployerRegistry) cldf.ChangeSetV2[MCMSDeploymentConfig] { - return cldf.CreateChangeSet(deployMCMSApply(deployerReg), deployMCMSVerify(deployerReg)) +func DeployMCMS(deployerReg *DeployerRegistry, mcmsRegistry *changesets.MCMSReaderRegistry) cldf.ChangeSetV2[MCMSDeploymentConfig] { + return cldf.CreateChangeSet(deployMCMSApply(deployerReg, mcmsRegistry), deployMCMSVerify(deployerReg, mcmsRegistry)) } -func deployMCMSVerify(_ *DeployerRegistry) func(cldf.Environment, MCMSDeploymentConfig) error { +func deployMCMSVerify(_ *DeployerRegistry, _ *changesets.MCMSReaderRegistry) func(cldf.Environment, MCMSDeploymentConfig) error { return func(e cldf.Environment, cfg MCMSDeploymentConfig) error { // TODO: implement if cfg.AdapterVersion == nil { @@ -51,9 +52,10 @@ func deployMCMSVerify(_ *DeployerRegistry) func(cldf.Environment, MCMSDeployment } } -func deployMCMSApply(d *DeployerRegistry) func(cldf.Environment, MCMSDeploymentConfig) (cldf.ChangesetOutput, error) { +func deployMCMSApply(d *DeployerRegistry, mcmsRegistry *changesets.MCMSReaderRegistry) func(cldf.Environment, MCMSDeploymentConfig) (cldf.ChangesetOutput, error) { return func(e cldf.Environment, cfg MCMSDeploymentConfig) (cldf.ChangesetOutput, error) { reports := make([]cldf_ops.Report[any, any], 0) + batchOps := make([]mcms_types.BatchOperation, 0) ds := datastore.NewMemoryDataStore() for selector, mcmsCfg := range cfg.Chains { family, err := chain_selectors.GetSelectorFamily(selector) @@ -83,12 +85,14 @@ func deployMCMSApply(d *DeployerRegistry) func(cldf.Environment, MCMSDeploymentC return cldf.ChangesetOutput{}, fmt.Errorf("failed to add %s %s with address %v on chain with selector %d to datastore: %w", r.Type, r.Version, r, r.ChainSelector, err) } } + batchOps = append(batchOps, deployReport.Output.BatchOps...) reports = append(reports, deployReport.ExecutionReports...) } - return changesets.NewOutputBuilder(e, nil). + return changesets.NewOutputBuilder(e, mcmsRegistry). WithReports(reports). WithDataStore(ds). + WithBatchOps(batchOps). Build(mcms.Input{}) // for deployment, we don't need an MCMS proposal } } diff --git a/integration-tests/deployment/fastcurse_test.go b/integration-tests/deployment/fastcurse_test.go index 66668d1ea..5b9b5c665 100644 --- a/integration-tests/deployment/fastcurse_test.go +++ b/integration-tests/deployment/fastcurse_test.go @@ -192,7 +192,7 @@ func TestFastCurseSolanaAndEVM(t *testing.T) { // deploy mcms evmDeployer := &adapters.EVMDeployer{} dReg.RegisterDeployer(chainsel.FamilyEVM, deploy.MCMSVersion, evmDeployer) - cs := deploy.DeployMCMS(dReg) + cs := deploy.DeployMCMS(dReg, nil) evmChain1 := env.BlockChains.EVMChains()[chain1] evmChain2 := env.BlockChains.EVMChains()[chain2] output, err := cs.Apply(*env, deploy.MCMSDeploymentConfig{ diff --git a/integration-tests/deployment/helpers.go b/integration-tests/deployment/helpers.go index c3f4cc6a5..661a52298 100644 --- a/integration-tests/deployment/helpers.go +++ b/integration-tests/deployment/helpers.go @@ -42,7 +42,7 @@ func DeployMCMS(t *testing.T, e *cldf_deployment.Environment, selector uint64, q } dReg := mcmsapi.GetRegistry() version := semver.MustParse("1.6.0") - cs := mcmsapi.DeployMCMS(dReg) + cs := mcmsapi.DeployMCMS(dReg, nil) for _, qualifier := range qualifiers { output, err := cs.Apply(*e, mcmsapi.MCMSDeploymentConfig{ AdapterVersion: version,