From 1a9a8caddac771516493f695dadd5132e4d6f758 Mon Sep 17 00:00:00 2001 From: tt-cll <141346969+tt-cll@users.noreply.github.com> Date: Thu, 27 Feb 2025 07:33:18 -0500 Subject: [PATCH] add more solana mcms support (#16555) * add timelock to remote chain * lint * re-add transfer * move to mcms helper * fix buffer bug * mcms validation * set authority * refactor * try to unify ci and local * lint * add update dest configs * add disable * cleanup * error message * v2 * fix test * add mcms to billing * refactor into helper * lint * add more mcms support * wip * wip * wip * set token authority * fix tests with/without mcms * lint * bug fix * lint --- .../solana/cs_chain_contracts_test.go | 394 +++++++++++------- .../ccip/changeset/solana/cs_deploy_chain.go | 47 ++- .../changeset/solana/cs_deploy_chain_test.go | 15 + .../ccip/changeset/solana/cs_set_ocr3.go | 68 ++- .../ccip/changeset/solana/cs_solana_token.go | 41 +- .../changeset/solana/cs_solana_token_test.go | 1 - .../solana/cs_token_admin_registry.go | 175 ++++++-- .../ccip/changeset/solana/cs_token_pool.go | 53 ++- 8 files changed, 598 insertions(+), 196 deletions(-) diff --git a/deployment/ccip/changeset/solana/cs_chain_contracts_test.go b/deployment/ccip/changeset/solana/cs_chain_contracts_test.go index 8a4fc0579d8..8b9022c455a 100644 --- a/deployment/ccip/changeset/solana/cs_chain_contracts_test.go +++ b/deployment/ccip/changeset/solana/cs_chain_contracts_test.go @@ -20,6 +20,7 @@ import ( "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext" ccipChangeset "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/globals" ccipChangesetSolana "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/solana" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/v1_6" @@ -228,6 +229,29 @@ func TestDeployCCIPContracts(t *testing.T) { testhelpers.DeployCCIPContractsTest(t, 1) } +func TestSetOcr3(t *testing.T) { + t.Parallel() + tenv, _ := testhelpers.NewMemoryEnvironment(t, testhelpers.WithSolChains(1)) + var err error + evmSelectors := tenv.Env.AllChainSelectors() + homeChainSel := evmSelectors[0] + solChainSelectors := tenv.Env.AllChainSelectorsSolana() + _, _ = testhelpers.TransferOwnershipSolana(t, &tenv.Env, solChainSelectors[0], true, true, true, true) + + tenv.Env, err = commonchangeset.ApplyChangesetsV2(t, tenv.Env, []commonchangeset.ConfiguredChangeSet{ + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.SetOCR3ConfigSolana), + v1_6.SetOCR3OffRampConfig{ + HomeChainSel: homeChainSel, + RemoteChainSels: solChainSelectors, + CCIPHomeConfigType: globals.ConfigTypeActive, + MCMS: &ccipChangeset.MCMSConfig{MinDelay: 1 * time.Second}, + }, + ), + }) + require.NoError(t, err) +} + func TestAddTokenPool(t *testing.T) { t.Parallel() ctx := testcontext.Get(t) @@ -325,11 +349,11 @@ func TestBilling(t *testing.T) { Mcms bool }{ { - Msg: "TestBilling with mcms", + Msg: "with mcms", Mcms: true, }, { - Msg: "TestBilling without mcms", + Msg: "without mcms", Mcms: false, }, } @@ -448,159 +472,245 @@ func TestBilling(t *testing.T) { func TestTokenAdminRegistry(t *testing.T) { t.Parallel() - ctx := testcontext.Get(t) - tenv, _ := testhelpers.NewMemoryEnvironment(t, testhelpers.WithSolChains(1)) - solChain := tenv.Env.AllChainSelectorsSolana()[0] - e, tokenAddress, err := deployToken(t, tenv.Env, solChain) - require.NoError(t, err) - state, err := ccipChangeset.LoadOnchainStateSolana(e) - require.NoError(t, err) - linkTokenAddress := state.SolChains[solChain].LinkToken + tests := []struct { + Msg string + Mcms bool + }{ + { + Msg: "with mcms", + Mcms: true, + }, + { + Msg: "without mcms", + Mcms: false, + }, + } - tokenAdminRegistryAdminPrivKey, _ := solana.NewRandomPrivateKey() + for _, test := range tests { + t.Run(test.Msg, func(t *testing.T) { + ctx := testcontext.Get(t) + tenv, _ := testhelpers.NewMemoryEnvironment(t, testhelpers.WithSolChains(1)) + solChain := tenv.Env.AllChainSelectorsSolana()[0] + e, tokenAddress, err := deployToken(t, tenv.Env, solChain) + require.NoError(t, err) + state, err := ccipChangeset.LoadOnchainStateSolana(e) + require.NoError(t, err) + linkTokenAddress := state.SolChains[solChain].LinkToken + newAdminNonTimelock, _ := solana.NewRandomPrivateKey() + newAdmin := newAdminNonTimelock.PublicKey() + newTokenAdmin := e.SolChains[solChain].DeployerKey.PublicKey() - e, err = commonchangeset.Apply(t, e, nil, - commonchangeset.Configure( - // register token admin registry for tokenAddress via admin instruction - deployment.CreateLegacyChangeSet(ccipChangesetSolana.RegisterTokenAdminRegistry), - ccipChangesetSolana.RegisterTokenAdminRegistryConfig{ - ChainSelector: solChain, - TokenPubKey: tokenAddress.String(), - TokenAdminRegistryAdmin: tokenAdminRegistryAdminPrivKey.PublicKey().String(), - RegisterType: ccipChangesetSolana.ViaGetCcipAdminInstruction, - }, - ), - commonchangeset.Configure( - // register token admin registry for linkToken via owner instruction - deployment.CreateLegacyChangeSet(ccipChangesetSolana.RegisterTokenAdminRegistry), - ccipChangesetSolana.RegisterTokenAdminRegistryConfig{ - ChainSelector: solChain, - TokenPubKey: linkTokenAddress.String(), - TokenAdminRegistryAdmin: tokenAdminRegistryAdminPrivKey.PublicKey().String(), - RegisterType: ccipChangesetSolana.ViaOwnerInstruction, - }, - ), - ) - require.NoError(t, err) + var mcmsConfig *ccipChangesetSolana.MCMSConfigSolana + if test.Mcms { + _, _ = testhelpers.TransferOwnershipSolana(t, &e, solChain, true, true, true, true) + mcmsConfig = &ccipChangesetSolana.MCMSConfigSolana{ + MCMS: &ccipChangeset.MCMSConfig{ + MinDelay: 1 * time.Second, + }, + RouterOwnedByTimelock: true, + FeeQuoterOwnedByTimelock: true, + OffRampOwnedByTimelock: true, + } + timelockSignerPDA, err := ccipChangesetSolana.FetchTimelockSigner(e, solChain) + require.NoError(t, err) + newAdmin = timelockSignerPDA + newTokenAdmin = timelockSignerPDA + } + timelockSignerPDA, err := ccipChangesetSolana.FetchTimelockSigner(e, solChain) + require.NoError(t, err) - tokenAdminRegistryPDA, _, _ := solState.FindTokenAdminRegistryPDA(tokenAddress, state.SolChains[solChain].Router) - var tokenAdminRegistryAccount solRouter.TokenAdminRegistry - err = e.SolChains[solChain].GetAccountDataBorshInto(ctx, tokenAdminRegistryPDA, &tokenAdminRegistryAccount) - require.NoError(t, err) - require.Equal(t, solana.PublicKey{}, tokenAdminRegistryAccount.Administrator) - // pending administrator should be the proposed admin key - require.Equal(t, tokenAdminRegistryAdminPrivKey.PublicKey(), tokenAdminRegistryAccount.PendingAdministrator) + e, err = commonchangeset.ApplyChangesetsV2(t, e, []commonchangeset.ConfiguredChangeSet{ + commonchangeset.Configure( + // register token admin registry for tokenAddress via admin instruction + deployment.CreateLegacyChangeSet(ccipChangesetSolana.RegisterTokenAdminRegistry), + ccipChangesetSolana.RegisterTokenAdminRegistryConfig{ + ChainSelector: solChain, + TokenPubKey: tokenAddress.String(), + TokenAdminRegistryAdmin: newAdmin.String(), + RegisterType: ccipChangesetSolana.ViaGetCcipAdminInstruction, + MCMSSolana: mcmsConfig, + }, + ), + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.SetTokenMintAuthority), + ccipChangesetSolana.SetTokenMintAuthorityConfig{ + ChainSelector: solChain, + TokenPubkey: linkTokenAddress, + NewAuthority: newTokenAdmin, + }, + ), + commonchangeset.Configure( + // register token admin registry for linkToken via owner instruction + deployment.CreateLegacyChangeSet(ccipChangesetSolana.RegisterTokenAdminRegistry), + ccipChangesetSolana.RegisterTokenAdminRegistryConfig{ + ChainSelector: solChain, + TokenPubKey: linkTokenAddress.String(), + TokenAdminRegistryAdmin: newAdmin.String(), + RegisterType: ccipChangesetSolana.ViaOwnerInstruction, + MCMSSolana: mcmsConfig, + }, + ), + }, + ) + require.NoError(t, err) - linkTokenAdminRegistryPDA, _, _ := solState.FindTokenAdminRegistryPDA(linkTokenAddress, state.SolChains[solChain].Router) - var linkTokenAdminRegistryAccount solRouter.TokenAdminRegistry - err = e.SolChains[solChain].GetAccountDataBorshInto(ctx, linkTokenAdminRegistryPDA, &linkTokenAdminRegistryAccount) - require.NoError(t, err) - require.Equal(t, tokenAdminRegistryAdminPrivKey.PublicKey(), linkTokenAdminRegistryAccount.PendingAdministrator) + tokenAdminRegistryPDA, _, _ := solState.FindTokenAdminRegistryPDA(tokenAddress, state.SolChains[solChain].Router) + var tokenAdminRegistryAccount solRouter.TokenAdminRegistry + err = e.SolChains[solChain].GetAccountDataBorshInto(ctx, tokenAdminRegistryPDA, &tokenAdminRegistryAccount) + require.NoError(t, err) + require.Equal(t, solana.PublicKey{}, tokenAdminRegistryAccount.Administrator) + // pending administrator should be the proposed admin key + require.Equal(t, newAdmin, tokenAdminRegistryAccount.PendingAdministrator) - e, err = commonchangeset.Apply(t, e, nil, - commonchangeset.Configure( - // accept admin role for tokenAddress - deployment.CreateLegacyChangeSet(ccipChangesetSolana.AcceptAdminRoleTokenAdminRegistry), - ccipChangesetSolana.AcceptAdminRoleTokenAdminRegistryConfig{ - ChainSelector: solChain, - TokenPubKey: tokenAddress.String(), - NewRegistryAdminPrivateKey: tokenAdminRegistryAdminPrivKey.String(), - }, - ), - ) - require.NoError(t, err) - err = e.SolChains[solChain].GetAccountDataBorshInto(ctx, tokenAdminRegistryPDA, &tokenAdminRegistryAccount) - require.NoError(t, err) - // confirm that the administrator is the deployer key - require.Equal(t, tokenAdminRegistryAdminPrivKey.PublicKey(), tokenAdminRegistryAccount.Administrator) - require.Equal(t, solana.PublicKey{}, tokenAdminRegistryAccount.PendingAdministrator) + linkTokenAdminRegistryPDA, _, _ := solState.FindTokenAdminRegistryPDA(linkTokenAddress, state.SolChains[solChain].Router) + var linkTokenAdminRegistryAccount solRouter.TokenAdminRegistry + err = e.SolChains[solChain].GetAccountDataBorshInto(ctx, linkTokenAdminRegistryPDA, &linkTokenAdminRegistryAccount) + require.NoError(t, err) + require.Equal(t, newAdmin, linkTokenAdminRegistryAccount.PendingAdministrator) - newTokenAdminRegistryAdminPrivKey, _ := solana.NewRandomPrivateKey() - e, err = commonchangeset.Apply(t, e, nil, - commonchangeset.Configure( - // transfer admin role for tokenAddress - deployment.CreateLegacyChangeSet(ccipChangesetSolana.TransferAdminRoleTokenAdminRegistry), - ccipChangesetSolana.TransferAdminRoleTokenAdminRegistryConfig{ - ChainSelector: solChain, - TokenPubKey: tokenAddress.String(), - NewRegistryAdminPublicKey: newTokenAdminRegistryAdminPrivKey.PublicKey().String(), - CurrentRegistryAdminPrivateKey: tokenAdminRegistryAdminPrivKey.String(), - }, - ), - ) - require.NoError(t, err) - err = e.SolChains[solChain].GetAccountDataBorshInto(ctx, tokenAdminRegistryPDA, &tokenAdminRegistryAccount) - require.NoError(t, err) - require.Equal(t, newTokenAdminRegistryAdminPrivKey.PublicKey(), tokenAdminRegistryAccount.PendingAdministrator) + // While we can assign the admin role arbitrarily regardless of mcms, we can only accept it as timelock + if test.Mcms { + e, err = commonchangeset.Apply(t, e, nil, + commonchangeset.Configure( + // accept admin role for tokenAddress + deployment.CreateLegacyChangeSet(ccipChangesetSolana.AcceptAdminRoleTokenAdminRegistry), + ccipChangesetSolana.AcceptAdminRoleTokenAdminRegistryConfig{ + ChainSelector: solChain, + TokenPubKey: tokenAddress.String(), + MCMSSolana: mcmsConfig, + }, + ), + ) + require.NoError(t, err) + err = e.SolChains[solChain].GetAccountDataBorshInto(ctx, tokenAdminRegistryPDA, &tokenAdminRegistryAccount) + require.NoError(t, err) + // confirm that the administrator is the deployer key + require.Equal(t, timelockSignerPDA, tokenAdminRegistryAccount.Administrator) + require.Equal(t, solana.PublicKey{}, tokenAdminRegistryAccount.PendingAdministrator) + + e, err = commonchangeset.ApplyChangesetsV2(t, e, []commonchangeset.ConfiguredChangeSet{ + commonchangeset.Configure( + // transfer admin role for tokenAddress + deployment.CreateLegacyChangeSet(ccipChangesetSolana.TransferAdminRoleTokenAdminRegistry), + ccipChangesetSolana.TransferAdminRoleTokenAdminRegistryConfig{ + ChainSelector: solChain, + TokenPubKey: tokenAddress.String(), + NewRegistryAdminPublicKey: newAdminNonTimelock.PublicKey().String(), + MCMSSolana: mcmsConfig, + }, + ), + }, + ) + require.NoError(t, err) + err = e.SolChains[solChain].GetAccountDataBorshInto(ctx, tokenAdminRegistryPDA, &tokenAdminRegistryAccount) + require.NoError(t, err) + require.Equal(t, newAdminNonTimelock.PublicKey(), tokenAdminRegistryAccount.PendingAdministrator) + } + }) + } } func TestPoolLookupTable(t *testing.T) { t.Parallel() - ctx := testcontext.Get(t) - tenv, _ := testhelpers.NewMemoryEnvironment(t, testhelpers.WithSolChains(1)) - solChain := tenv.Env.AllChainSelectorsSolana()[0] + tests := []struct { + Msg string + Mcms bool + }{ + { + Msg: "with mcms", + Mcms: true, + }, + { + Msg: "without mcms", + Mcms: false, + }, + } - e, tokenAddress, err := deployToken(t, tenv.Env, solChain) - require.NoError(t, err) - e, err = commonchangeset.Apply(t, e, nil, - commonchangeset.Configure( - // add token pool lookup table - deployment.CreateLegacyChangeSet(ccipChangesetSolana.AddTokenPoolLookupTable), - ccipChangesetSolana.TokenPoolLookupTableConfig{ - ChainSelector: solChain, - TokenPubKey: tokenAddress.String(), - }, - ), - ) - require.NoError(t, err) - state, err := ccipChangeset.LoadOnchainStateSolana(e) - require.NoError(t, err) - lookupTablePubKey := state.SolChains[solChain].TokenPoolLookupTable[tokenAddress] + for _, test := range tests { + t.Run(test.Msg, func(t *testing.T) { + ctx := testcontext.Get(t) + tenv, _ := testhelpers.NewMemoryEnvironment(t, testhelpers.WithSolChains(1)) + solChain := tenv.Env.AllChainSelectorsSolana()[0] - lookupTableEntries0, err := solCommonUtil.GetAddressLookupTable(ctx, e.SolChains[solChain].Client, lookupTablePubKey) - require.NoError(t, err) - require.Equal(t, lookupTablePubKey, lookupTableEntries0[0]) - require.Equal(t, tokenAddress, lookupTableEntries0[7]) + var mcmsConfig *ccipChangesetSolana.MCMSConfigSolana + newAdmin := tenv.Env.SolChains[solChain].DeployerKey.PublicKey() + if test.Mcms { + _, _ = testhelpers.TransferOwnershipSolana(t, &tenv.Env, solChain, true, true, true, true) + mcmsConfig = &ccipChangesetSolana.MCMSConfigSolana{ + MCMS: &ccipChangeset.MCMSConfig{ + MinDelay: 1 * time.Second, + }, + RouterOwnedByTimelock: true, + FeeQuoterOwnedByTimelock: true, + OffRampOwnedByTimelock: true, + } + timelockSignerPDA, err := ccipChangesetSolana.FetchTimelockSigner(tenv.Env, solChain) + require.NoError(t, err) + newAdmin = timelockSignerPDA + } - tokenAdminRegistryAdminPrivKey, _ := solana.NewRandomPrivateKey() + e, tokenAddress, err := deployToken(t, tenv.Env, solChain) + require.NoError(t, err) + e, err = commonchangeset.Apply(t, e, nil, + commonchangeset.Configure( + // add token pool lookup table + deployment.CreateLegacyChangeSet(ccipChangesetSolana.AddTokenPoolLookupTable), + ccipChangesetSolana.TokenPoolLookupTableConfig{ + ChainSelector: solChain, + TokenPubKey: tokenAddress.String(), + }, + ), + ) + require.NoError(t, err) + state, err := ccipChangeset.LoadOnchainStateSolana(e) + require.NoError(t, err) + lookupTablePubKey := state.SolChains[solChain].TokenPoolLookupTable[tokenAddress] - e, err = commonchangeset.Apply(t, e, nil, - commonchangeset.Configure( - // register token admin registry for linkToken via owner instruction - deployment.CreateLegacyChangeSet(ccipChangesetSolana.RegisterTokenAdminRegistry), - ccipChangesetSolana.RegisterTokenAdminRegistryConfig{ - ChainSelector: solChain, - TokenPubKey: tokenAddress.String(), - TokenAdminRegistryAdmin: tokenAdminRegistryAdminPrivKey.PublicKey().String(), - RegisterType: ccipChangesetSolana.ViaGetCcipAdminInstruction, - }, - ), - commonchangeset.Configure( - // accept admin role for tokenAddress - deployment.CreateLegacyChangeSet(ccipChangesetSolana.AcceptAdminRoleTokenAdminRegistry), - ccipChangesetSolana.AcceptAdminRoleTokenAdminRegistryConfig{ - ChainSelector: solChain, - TokenPubKey: tokenAddress.String(), - NewRegistryAdminPrivateKey: tokenAdminRegistryAdminPrivKey.String(), - }, - ), - commonchangeset.Configure( - // set pool -> this updates tokenAdminRegistryPDA, hence above changeset is required - deployment.CreateLegacyChangeSet(ccipChangesetSolana.SetPool), - ccipChangesetSolana.SetPoolConfig{ - ChainSelector: solChain, - TokenPubKey: tokenAddress.String(), - TokenAdminRegistryAdminPrivateKey: tokenAdminRegistryAdminPrivKey.String(), - WritableIndexes: []uint8{3, 4, 7}, - }, - ), - ) - require.NoError(t, err) - tokenAdminRegistry := solRouter.TokenAdminRegistry{} - tokenAdminRegistryPDA, _, _ := solState.FindTokenAdminRegistryPDA(tokenAddress, state.SolChains[solChain].Router) + lookupTableEntries0, err := solCommonUtil.GetAddressLookupTable(ctx, e.SolChains[solChain].Client, lookupTablePubKey) + require.NoError(t, err) + require.Equal(t, lookupTablePubKey, lookupTableEntries0[0]) + require.Equal(t, tokenAddress, lookupTableEntries0[7]) - err = e.SolChains[solChain].GetAccountDataBorshInto(ctx, tokenAdminRegistryPDA, &tokenAdminRegistry) - require.NoError(t, err) - require.Equal(t, tokenAdminRegistryAdminPrivKey.PublicKey(), tokenAdminRegistry.Administrator) - require.Equal(t, lookupTablePubKey, tokenAdminRegistry.LookupTable) + e, err = commonchangeset.Apply(t, e, nil, + commonchangeset.Configure( + // register token admin registry for linkToken via owner instruction + deployment.CreateLegacyChangeSet(ccipChangesetSolana.RegisterTokenAdminRegistry), + ccipChangesetSolana.RegisterTokenAdminRegistryConfig{ + ChainSelector: solChain, + TokenPubKey: tokenAddress.String(), + TokenAdminRegistryAdmin: newAdmin.String(), + RegisterType: ccipChangesetSolana.ViaGetCcipAdminInstruction, + MCMSSolana: mcmsConfig, + }, + ), + commonchangeset.Configure( + // accept admin role for tokenAddress + deployment.CreateLegacyChangeSet(ccipChangesetSolana.AcceptAdminRoleTokenAdminRegistry), + ccipChangesetSolana.AcceptAdminRoleTokenAdminRegistryConfig{ + ChainSelector: solChain, + TokenPubKey: tokenAddress.String(), + MCMSSolana: mcmsConfig, + }, + ), + commonchangeset.Configure( + // set pool -> this updates tokenAdminRegistryPDA, hence above changeset is required + deployment.CreateLegacyChangeSet(ccipChangesetSolana.SetPool), + ccipChangesetSolana.SetPoolConfig{ + ChainSelector: solChain, + TokenPubKey: tokenAddress.String(), + WritableIndexes: []uint8{3, 4, 7}, + MCMSSolana: mcmsConfig, + }, + ), + ) + require.NoError(t, err) + tokenAdminRegistry := solRouter.TokenAdminRegistry{} + tokenAdminRegistryPDA, _, _ := solState.FindTokenAdminRegistryPDA(tokenAddress, state.SolChains[solChain].Router) + + err = e.SolChains[solChain].GetAccountDataBorshInto(ctx, tokenAdminRegistryPDA, &tokenAdminRegistry) + require.NoError(t, err) + require.Equal(t, newAdmin, tokenAdminRegistry.Administrator) + require.Equal(t, lookupTablePubKey, tokenAdminRegistry.LookupTable) + }) + } } diff --git a/deployment/ccip/changeset/solana/cs_deploy_chain.go b/deployment/ccip/changeset/solana/cs_deploy_chain.go index 00260f10f1c..d9da9843168 100644 --- a/deployment/ccip/changeset/solana/cs_deploy_chain.go +++ b/deployment/ccip/changeset/solana/cs_deploy_chain.go @@ -899,6 +899,7 @@ func generateCloseBufferIxn( type SetFeeAggregatorConfig struct { ChainSelector uint64 FeeAggregator string + MCMSSolana *MCMSConfigSolana } func (cfg SetFeeAggregatorConfig) Validate(e deployment.Environment) error { @@ -916,6 +917,14 @@ func (cfg SetFeeAggregatorConfig) Validate(e deployment.Environment) error { return err } + if err := ValidateMCMSConfigSolana(e, cfg.ChainSelector, cfg.MCMSSolana); err != nil { + return err + } + routerUsingMCMS := cfg.MCMSSolana != nil && cfg.MCMSSolana.RouterOwnedByTimelock + if err := ccipChangeset.ValidateOwnershipSolana(&e, chain, routerUsingMCMS, chainState.Router, ccipChangeset.Router); err != nil { + return fmt.Errorf("failed to validate ownership: %w", err) + } + // Validate fee aggregator address is valid if _, err := solana.PublicKeyFromBase58(cfg.FeeAggregator); err != nil { return fmt.Errorf("invalid fee aggregator address: %w", err) @@ -939,28 +948,56 @@ func SetFeeAggregator(e deployment.Environment, cfg SetFeeAggregatorConfig) (dep feeAggregatorPubKey := solana.MustPublicKeyFromBase58(cfg.FeeAggregator) routerConfigPDA, _, _ := solState.FindConfigPDA(chainState.Router) + routerUsingMCMS := cfg.MCMSSolana != nil && cfg.MCMSSolana.RouterOwnedByTimelock solRouter.SetProgramID(chainState.Router) + var authority solana.PublicKey + var err error + if routerUsingMCMS { + authority, err = FetchTimelockSigner(e, cfg.ChainSelector) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch timelock signer: %w", err) + } + } else { + authority = e.SolChains[cfg.ChainSelector].DeployerKey.PublicKey() + } + instruction, err := solRouter.NewUpdateFeeAggregatorInstruction( feeAggregatorPubKey, routerConfigPDA, - chain.DeployerKey.PublicKey(), + authority, solana.SystemProgramID, ).ValidateAndBuild() if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to build instruction: %w", err) } - - if err := chain.Confirm([]solana.Instruction{instruction}); err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm instructions: %w", err) - } newAddresses := deployment.NewMemoryAddressBook() err = newAddresses.Save(cfg.ChainSelector, cfg.FeeAggregator, deployment.NewTypeAndVersion(ccipChangeset.FeeAggregator, deployment.Version1_0_0)) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to save address: %w", err) } + if routerUsingMCMS { + tx, err := BuildMCMSTxn(instruction, chainState.Router.String(), ccipChangeset.Router) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create transaction: %w", err) + } + proposal, err := BuildProposalsForTxns( + e, cfg.ChainSelector, "proposal to SetFeeAggregator in Solana", cfg.MCMSSolana.MCMS.MinDelay, []mcmsTypes.Transaction{*tx}) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + return deployment.ChangesetOutput{ + MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, + AddressBook: newAddresses, + }, nil + } + + if err := chain.Confirm([]solana.Instruction{instruction}); err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm instructions: %w", err) + } e.Logger.Infow("Set new fee aggregator", "chain", chain.String(), "fee_aggregator", feeAggregatorPubKey.String()) + return deployment.ChangesetOutput{ AddressBook: newAddresses, }, nil diff --git a/deployment/ccip/changeset/solana/cs_deploy_chain_test.go b/deployment/ccip/changeset/solana/cs_deploy_chain_test.go index 9d377838abe..a5f3010dff4 100644 --- a/deployment/ccip/changeset/solana/cs_deploy_chain_test.go +++ b/deployment/ccip/changeset/solana/cs_deploy_chain_test.go @@ -59,6 +59,8 @@ func TestDeployChainContractsChangesetSolana(t *testing.T) { feeAggregatorPrivKey, _ := solana.NewRandomPrivateKey() feeAggregatorPubKey := feeAggregatorPrivKey.PublicKey() + feeAggregatorPrivKey2, _ := solana.NewRandomPrivateKey() + feeAggregatorPubKey2 := feeAggregatorPrivKey2.PublicKey() ci := os.Getenv("CI") == "true" // we can't upgrade in place locally if we preload addresses so we have to change where we build // we also don't want to incur two builds in CI, so only do it locally @@ -207,6 +209,19 @@ func TestDeployChainContractsChangesetSolana(t *testing.T) { }, }, ), + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.SetFeeAggregator), + ccipChangesetSolana.SetFeeAggregatorConfig{ + ChainSelector: solChainSelectors[0], + FeeAggregator: feeAggregatorPubKey2.String(), + MCMSSolana: &ccipChangesetSolana.MCMSConfigSolana{ + MCMS: &ccipChangeset.MCMSConfig{ + MinDelay: 1 * time.Second, + }, + RouterOwnedByTimelock: true, + }, + }, + ), }) require.NoError(t, err) testhelpers.ValidateSolanaState(t, e, solChainSelectors) diff --git a/deployment/ccip/changeset/solana/cs_set_ocr3.go b/deployment/ccip/changeset/solana/cs_set_ocr3.go index 4eab1aca7bc..6403acafa80 100644 --- a/deployment/ccip/changeset/solana/cs_set_ocr3.go +++ b/deployment/ccip/changeset/solana/cs_set_ocr3.go @@ -5,6 +5,10 @@ import ( "github.com/gagliardetto/solana-go" chain_selectors "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/sdk" + mcmsSolana "github.com/smartcontractkit/mcms/sdk/solana" + mcmsTypes "github.com/smartcontractkit/mcms/types" solOffRamp "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_offramp" @@ -13,6 +17,8 @@ import ( ccipChangeset "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/internal" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/v1_6" + csState "github.com/smartcontractkit/chainlink/deployment/common/changeset/state" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" ) const ( @@ -49,8 +55,16 @@ func SetOCR3ConfigSolana(e deployment.Environment, cfg v1_6.SetOCR3OffRampConfig if chainFamily != chain_selectors.FamilySolana { return deployment.ChangesetOutput{}, fmt.Errorf("chain %d is not a solana chain", remote) } + chain := e.SolChains[remote] + if err := ccipChangeset.ValidateOwnershipSolana(&e, chain, cfg.MCMS != nil, state.SolChains[remote].OffRamp, ccipChangeset.OffRamp); err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to validate ownership: %w", err) + } } + timelocks := map[uint64]string{} + proposers := map[uint64]string{} + inspectors := map[uint64]sdk.Inspector{} + var batches []mcmsTypes.BatchOperation for _, remote := range cfg.RemoteChainSels { donID, err := internal.DonIDForChain( state.Chains[cfg.HomeChainSel].CapabilityRegistry, @@ -71,11 +85,31 @@ func SetOCR3ConfigSolana(e deployment.Environment, cfg v1_6.SetOCR3OffRampConfig e.Logger.Infof("OCR3 config already set on offramp for chain %d", remote) continue } + chain := e.SolChains[remote] + addresses, _ := e.ExistingAddresses.AddressesForChain(remote) + mcmState, _ := csState.MaybeLoadMCMSWithTimelockChainStateSolana(chain, addresses) + + timelocks[remote] = mcmsSolana.ContractAddress( + mcmState.TimelockProgram, + mcmsSolana.PDASeed(mcmState.TimelockSeed), + ) + proposers[remote] = mcmsSolana.ContractAddress(mcmState.McmProgram, mcmsSolana.PDASeed(mcmState.ProposerMcmSeed)) + inspectors[remote] = mcmsSolana.NewInspector(chain.Client) var instructions []solana.Instruction + var txns []mcmsTypes.Transaction offRampConfigPDA := state.SolChains[remote].OffRampConfigPDA offRampStatePDA := state.SolChains[remote].OffRampStatePDA solOffRamp.SetProgramID(state.SolChains[remote].OffRamp) + var authority solana.PublicKey + if cfg.MCMS != nil { + authority, err = FetchTimelockSigner(e, remote) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch timelock signer: %w", err) + } + } else { + authority = e.SolChains[remote].DeployerKey.PublicKey() + } for _, arg := range args { instruction, err := solOffRamp.NewSetOcrConfigInstruction( arg.OCRPluginType, @@ -88,18 +122,48 @@ func SetOCR3ConfigSolana(e deployment.Environment, cfg v1_6.SetOCR3OffRampConfig arg.Transmitters, offRampConfigPDA, offRampStatePDA, - e.SolChains[remote].DeployerKey.PublicKey(), + authority, ).ValidateAndBuild() if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate instructions: %w", err) } - instructions = append(instructions, instruction) + if cfg.MCMS == nil { + instructions = append(instructions, instruction) + } else { + tx, err := BuildMCMSTxn(instruction, state.SolChains[remote].OffRamp.String(), ccipChangeset.OffRamp) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create transaction: %w", err) + } + txns = append(txns, *tx) + } } if cfg.MCMS == nil { if err := e.SolChains[remote].Confirm(instructions); err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm instructions: %w", err) } + } else { + batches = append(batches, mcmsTypes.BatchOperation{ + ChainSelector: mcmsTypes.ChainSelector(remote), + Transactions: txns, + }) + } + } + if cfg.MCMS != nil { + proposal, err := proposalutils.BuildProposalFromBatchesV2( + e, + timelocks, + proposers, + inspectors, + batches, + "set ocr3 config for Solana", + cfg.MCMS.MinDelay, + ) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) } + return deployment.ChangesetOutput{ + MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, + }, nil } return deployment.ChangesetOutput{}, nil } diff --git a/deployment/ccip/changeset/solana/cs_solana_token.go b/deployment/ccip/changeset/solana/cs_solana_token.go index 9932887acc2..88cb3af7eb3 100644 --- a/deployment/ccip/changeset/solana/cs_solana_token.go +++ b/deployment/ccip/changeset/solana/cs_solana_token.go @@ -17,6 +17,7 @@ import ( var _ deployment.ChangeSet[DeploySolanaTokenConfig] = DeploySolanaToken var _ deployment.ChangeSet[MintSolanaTokenConfig] = MintSolanaToken var _ deployment.ChangeSet[CreateSolanaTokenATAConfig] = CreateSolanaTokenATA +var _ deployment.ChangeSet[SetTokenMintAuthorityConfig] = SetTokenMintAuthority // TODO: add option to set token mint authority by taking in its public key // might need to take authority private key if it needs to sign that @@ -166,8 +167,10 @@ type CreateSolanaTokenATAConfig struct { func CreateSolanaTokenATA(e deployment.Environment, cfg CreateSolanaTokenATAConfig) (deployment.ChangesetOutput, error) { chain := e.SolChains[cfg.ChainSelector] + state, _ := ccipChangeset.LoadOnchainState(e) + chainState := state.SolChains[cfg.ChainSelector] - tokenprogramID, err := GetTokenProgramID(cfg.TokenProgram) + tokenprogramID, err := chainState.TokenToTokenProgram(cfg.TokenPubkey) if err != nil { return deployment.ChangesetOutput{}, err } @@ -197,3 +200,39 @@ func CreateSolanaTokenATA(e deployment.Environment, cfg CreateSolanaTokenATAConf return deployment.ChangesetOutput{}, nil } + +type SetTokenMintAuthorityConfig struct { + ChainSelector uint64 + TokenPubkey solana.PublicKey + NewAuthority solana.PublicKey +} + +func SetTokenMintAuthority(e deployment.Environment, cfg SetTokenMintAuthorityConfig) (deployment.ChangesetOutput, error) { + chain := e.SolChains[cfg.ChainSelector] + state, _ := ccipChangeset.LoadOnchainState(e) + chainState := state.SolChains[cfg.ChainSelector] + + tokenprogramID, err := chainState.TokenToTokenProgram(cfg.TokenPubkey) + if err != nil { + return deployment.ChangesetOutput{}, err + } + + ix, err := solTokenUtil.SetTokenMintAuthority( + tokenprogramID, + cfg.NewAuthority, + cfg.TokenPubkey, + chain.DeployerKey.PublicKey(), + ) + if err != nil { + return deployment.ChangesetOutput{}, err + } + + // confirm instructions + if err = chain.Confirm([]solana.Instruction{ix}); err != nil { + e.Logger.Errorw("Failed to confirm instructions for ATA creation", "chain", chain.String(), "err", err) + return deployment.ChangesetOutput{}, err + } + e.Logger.Infow("Set token mint authority on", "chain", cfg.ChainSelector, "for token", cfg.TokenPubkey.String(), "newAuthority", cfg.NewAuthority.String()) + + return deployment.ChangesetOutput{}, nil +} diff --git a/deployment/ccip/changeset/solana/cs_solana_token_test.go b/deployment/ccip/changeset/solana/cs_solana_token_test.go index 4534bbfa5a2..4498ae39688 100644 --- a/deployment/ccip/changeset/solana/cs_solana_token_test.go +++ b/deployment/ccip/changeset/solana/cs_solana_token_test.go @@ -56,7 +56,6 @@ func TestSolanaTokenOps(t *testing.T) { changeset_solana.CreateSolanaTokenATAConfig{ ChainSelector: solChain1, TokenPubkey: tokenAddress, - TokenProgram: ccipChangeset.SPL2022Tokens, ATAList: []string{deployerKey.String(), testUserPubKey.String()}, }, ), diff --git a/deployment/ccip/changeset/solana/cs_token_admin_registry.go b/deployment/ccip/changeset/solana/cs_token_admin_registry.go index 550a7581325..65fb1b789e2 100644 --- a/deployment/ccip/changeset/solana/cs_token_admin_registry.go +++ b/deployment/ccip/changeset/solana/cs_token_admin_registry.go @@ -7,8 +7,10 @@ import ( "github.com/gagliardetto/solana-go" + "github.com/smartcontractkit/mcms" + mcmsTypes "github.com/smartcontractkit/mcms/types" + solRouter "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" - solCommonUtil "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/common" solState "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/state" "github.com/smartcontractkit/chainlink/deployment" @@ -27,6 +29,7 @@ type RegisterTokenAdminRegistryConfig struct { TokenPubKey string TokenAdminRegistryAdmin string RegisterType RegisterTokenAdminRegistryType + MCMSSolana *MCMSConfigSolana } func (cfg RegisterTokenAdminRegistryConfig) Validate(e deployment.Environment) error { @@ -48,6 +51,13 @@ func (cfg RegisterTokenAdminRegistryConfig) Validate(e deployment.Environment) e if err := validateRouterConfig(chain, chainState); err != nil { return err } + if err := ValidateMCMSConfigSolana(e, cfg.ChainSelector, cfg.MCMSSolana); err != nil { + return err + } + routerUsingMcms := cfg.MCMSSolana != nil && cfg.MCMSSolana.RouterOwnedByTimelock + if err := ccipChangeset.ValidateOwnershipSolana(&e, chain, routerUsingMcms, chainState.Router, ccipChangeset.Router); err != nil { + return fmt.Errorf("failed to validate ownership: %w", err) + } tokenAdminRegistryPDA, _, err := solState.FindTokenAdminRegistryPDA(tokenPubKey, chainState.Router) if err != nil { return fmt.Errorf("failed to find token admin registry pda (mint: %s, router: %s): %w", tokenPubKey.String(), chainState.Router.String(), err) @@ -74,6 +84,16 @@ func RegisterTokenAdminRegistry(e deployment.Environment, cfg RegisterTokenAdmin var instruction *solRouter.Instruction var err error + routerUsingMCMS := cfg.MCMSSolana != nil && cfg.MCMSSolana.RouterOwnedByTimelock + var authority solana.PublicKey + if routerUsingMCMS { + authority, err = FetchTimelockSigner(e, cfg.ChainSelector) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch timelock signer: %w", err) + } + } else { + authority = chain.DeployerKey.PublicKey() + } switch cfg.RegisterType { // the ccip admin signs and makes tokenAdminRegistryAdmin the authority of the tokenAdminRegistry PDA case ViaGetCcipAdminInstruction: @@ -82,7 +102,7 @@ func RegisterTokenAdminRegistry(e deployment.Environment, cfg RegisterTokenAdmin chainState.RouterConfigPDA, tokenAdminRegistryPDA, // this gets created tokenPubKey, - chain.DeployerKey.PublicKey(), // (ccip admin) + authority, solana.SystemProgramID, ).ValidateAndBuild() if err != nil { @@ -95,14 +115,28 @@ func RegisterTokenAdminRegistry(e deployment.Environment, cfg RegisterTokenAdmin chainState.RouterConfigPDA, tokenAdminRegistryPDA, // this gets created tokenPubKey, - chain.DeployerKey.PublicKey(), // (token mint authority) becomes the authority of the tokenAdminRegistry PDA + authority, // (token mint authority) becomes the authority of the tokenAdminRegistry PDA solana.SystemProgramID, ).ValidateAndBuild() if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate instructions: %w", err) } } - // if we want to have a different authority, we will need to add the corresponding singer here + if routerUsingMCMS { + tx, err := BuildMCMSTxn(instruction, chainState.Router.String(), ccipChangeset.Router) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create transaction: %w", err) + } + proposal, err := BuildProposalsForTxns( + e, cfg.ChainSelector, "proposal to RegisterTokenAdminRegistry in Solana", cfg.MCMSSolana.MCMS.MinDelay, []mcmsTypes.Transaction{*tx}) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + return deployment.ChangesetOutput{ + MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, + }, nil + } + // if we want to have a different authority, we will need to add the corresponding signer here // for now we are assuming both token owner and ccip admin will always be deployer key instructions := []solana.Instruction{instruction} if err := chain.Confirm(instructions); err != nil { @@ -113,10 +147,10 @@ func RegisterTokenAdminRegistry(e deployment.Environment, cfg RegisterTokenAdmin // TRANSFER AND ACCEPT TOKEN ADMIN REGISTRY type TransferAdminRoleTokenAdminRegistryConfig struct { - ChainSelector uint64 - TokenPubKey string - NewRegistryAdminPublicKey string - CurrentRegistryAdminPrivateKey string + ChainSelector uint64 + TokenPubKey string + NewRegistryAdminPublicKey string + MCMSSolana *MCMSConfigSolana } func (cfg TransferAdminRoleTokenAdminRegistryConfig) Validate(e deployment.Environment) error { @@ -124,23 +158,37 @@ func (cfg TransferAdminRoleTokenAdminRegistryConfig) Validate(e deployment.Envir if err := commonValidation(e, cfg.ChainSelector, tokenPubKey); err != nil { return err } + state, _ := ccipChangeset.LoadOnchainState(e) + chainState := state.SolChains[cfg.ChainSelector] + chain := e.SolChains[cfg.ChainSelector] + if err := validateRouterConfig(chain, chainState); err != nil { + return err + } + if err := ValidateMCMSConfigSolana(e, cfg.ChainSelector, cfg.MCMSSolana); err != nil { + return err + } + currentAdmin := chain.DeployerKey.PublicKey() + routerUsingMcms := cfg.MCMSSolana != nil && cfg.MCMSSolana.RouterOwnedByTimelock + var err error + if routerUsingMcms { + currentAdmin, err = FetchTimelockSigner(e, cfg.ChainSelector) + if err != nil { + return fmt.Errorf("failed to fetch timelock signer: %w", err) + } + } - currentRegistryAdminPrivateKey := solana.MustPrivateKeyFromBase58(cfg.CurrentRegistryAdminPrivateKey) newRegistryAdminPubKey := solana.MustPublicKeyFromBase58(cfg.NewRegistryAdminPublicKey) - if currentRegistryAdminPrivateKey.PublicKey().Equals(newRegistryAdminPubKey) { + if currentAdmin.Equals(newRegistryAdminPubKey) { return fmt.Errorf("new registry admin public key (%s) cannot be the same as current registry admin public key (%s) for token %s", newRegistryAdminPubKey.String(), - currentRegistryAdminPrivateKey.PublicKey().String(), + currentAdmin.String(), tokenPubKey.String(), ) } - state, _ := ccipChangeset.LoadOnchainState(e) - chainState := state.SolChains[cfg.ChainSelector] - chain := e.SolChains[cfg.ChainSelector] - if err := validateRouterConfig(chain, chainState); err != nil { - return err + if err := ccipChangeset.ValidateOwnershipSolana(&e, chain, routerUsingMcms, chainState.Router, ccipChangeset.Router); err != nil { + return fmt.Errorf("failed to validate ownership: %w", err) } tokenAdminRegistryPDA, _, err := solState.FindTokenAdminRegistryPDA(tokenPubKey, chainState.Router) if err != nil { @@ -157,7 +205,6 @@ func TransferAdminRoleTokenAdminRegistry(e deployment.Environment, cfg TransferA if err := cfg.Validate(e); err != nil { return deployment.ChangesetOutput{}, err } - chain := e.SolChains[cfg.ChainSelector] state, _ := ccipChangeset.LoadOnchainState(e) chainState := state.SolChains[cfg.ChainSelector] tokenPubKey := solana.MustPublicKeyFromBase58(cfg.TokenPubKey) @@ -165,22 +212,46 @@ func TransferAdminRoleTokenAdminRegistry(e deployment.Environment, cfg TransferA // verified tokenAdminRegistryPDA, _, _ := solState.FindTokenAdminRegistryPDA(tokenPubKey, chainState.Router) - currentRegistryAdminPrivateKey := solana.MustPrivateKeyFromBase58(cfg.CurrentRegistryAdminPrivateKey) newRegistryAdminPubKey := solana.MustPublicKeyFromBase58(cfg.NewRegistryAdminPublicKey) + routerUsingMCMS := cfg.MCMSSolana != nil && cfg.MCMSSolana.RouterOwnedByTimelock + var authority solana.PublicKey + var err error + if routerUsingMCMS { + authority, err = FetchTimelockSigner(e, cfg.ChainSelector) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch timelock signer: %w", err) + } + } else { + authority = e.SolChains[cfg.ChainSelector].DeployerKey.PublicKey() + } ix1, err := solRouter.NewTransferAdminRoleTokenAdminRegistryInstruction( newRegistryAdminPubKey, chainState.RouterConfigPDA, tokenAdminRegistryPDA, tokenPubKey, - currentRegistryAdminPrivateKey.PublicKey(), + authority, ).ValidateAndBuild() if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate instructions: %w", err) } - instructions := []solana.Instruction{ix1} + if routerUsingMCMS { + tx, err := BuildMCMSTxn(ix1, chainState.Router.String(), ccipChangeset.Router) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create transaction: %w", err) + } + proposal, err := BuildProposalsForTxns( + e, cfg.ChainSelector, "proposal to TransferAdminRoleTokenAdminRegistry in Solana", cfg.MCMSSolana.MCMS.MinDelay, []mcmsTypes.Transaction{*tx}) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + return deployment.ChangesetOutput{ + MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, + }, nil + } // the existing authority will have to sign the transfer - if err := chain.Confirm(instructions, solCommonUtil.AddSigners(currentRegistryAdminPrivateKey)); err != nil { + chain := e.SolChains[cfg.ChainSelector] + if err := chain.Confirm([]solana.Instruction{ix1}); err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm instructions: %w", err) } return deployment.ChangesetOutput{}, nil @@ -188,9 +259,9 @@ func TransferAdminRoleTokenAdminRegistry(e deployment.Environment, cfg TransferA // ACCEPT TOKEN ADMIN REGISTRY type AcceptAdminRoleTokenAdminRegistryConfig struct { - ChainSelector uint64 - TokenPubKey string - NewRegistryAdminPrivateKey string + ChainSelector uint64 + TokenPubKey string + MCMSSolana *MCMSConfigSolana } func (cfg AcceptAdminRoleTokenAdminRegistryConfig) Validate(e deployment.Environment) error { @@ -204,6 +275,21 @@ func (cfg AcceptAdminRoleTokenAdminRegistryConfig) Validate(e deployment.Environ if err := validateRouterConfig(chain, chainState); err != nil { return err } + if err := ValidateMCMSConfigSolana(e, cfg.ChainSelector, cfg.MCMSSolana); err != nil { + return err + } + routerUsingMcms := cfg.MCMSSolana != nil && cfg.MCMSSolana.RouterOwnedByTimelock + newAdmin := chain.DeployerKey.PublicKey() + var err error + if routerUsingMcms { + newAdmin, err = FetchTimelockSigner(e, cfg.ChainSelector) + if err != nil { + return fmt.Errorf("failed to fetch timelock signer: %w", err) + } + } + if err := ccipChangeset.ValidateOwnershipSolana(&e, chain, routerUsingMcms, chainState.Router, ccipChangeset.Router); err != nil { + return fmt.Errorf("failed to validate ownership: %w", err) + } tokenAdminRegistryPDA, _, err := solState.FindTokenAdminRegistryPDA(tokenPubKey, chainState.Router) if err != nil { return fmt.Errorf("failed to find token admin registry pda (mint: %s, router: %s): %w", tokenPubKey.String(), chainState.Router.String(), err) @@ -212,12 +298,9 @@ func (cfg AcceptAdminRoleTokenAdminRegistryConfig) Validate(e deployment.Environ if err := chain.GetAccountDataBorshInto(context.Background(), tokenAdminRegistryPDA, &tokenAdminRegistryAccount); err != nil { return fmt.Errorf("token admin registry not found for (mint: %s, router: %s), cannot accept admin role", tokenPubKey.String(), chainState.Router.String()) } - // check if accepting admin is the pending admin - newRegistryAdminPrivateKey := solana.MustPrivateKeyFromBase58(cfg.NewRegistryAdminPrivateKey) - newRegistryAdminPublicKey := newRegistryAdminPrivateKey.PublicKey() - if !tokenAdminRegistryAccount.PendingAdministrator.Equals(newRegistryAdminPublicKey) { + if !tokenAdminRegistryAccount.PendingAdministrator.Equals(newAdmin) { return fmt.Errorf("new admin public key (%s) does not match pending registry admin role (%s) for token %s", - newRegistryAdminPublicKey.String(), + newAdmin.String(), tokenAdminRegistryAccount.PendingAdministrator.String(), tokenPubKey.String(), ) @@ -233,24 +316,46 @@ func AcceptAdminRoleTokenAdminRegistry(e deployment.Environment, cfg AcceptAdmin state, _ := ccipChangeset.LoadOnchainState(e) chainState := state.SolChains[cfg.ChainSelector] tokenPubKey := solana.MustPublicKeyFromBase58(cfg.TokenPubKey) - newRegistryAdminPrivateKey := solana.MustPrivateKeyFromBase58(cfg.NewRegistryAdminPrivateKey) // verified tokenAdminRegistryPDA, _, _ := solState.FindTokenAdminRegistryPDA(tokenPubKey, chainState.Router) + routerUsingMCMS := cfg.MCMSSolana != nil && cfg.MCMSSolana.RouterOwnedByTimelock + var authority solana.PublicKey + var err error + if routerUsingMCMS { + authority, err = FetchTimelockSigner(e, cfg.ChainSelector) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch timelock signer: %w", err) + } + } else { + authority = chain.DeployerKey.PublicKey() + } ix1, err := solRouter.NewAcceptAdminRoleTokenAdminRegistryInstruction( chainState.RouterConfigPDA, tokenAdminRegistryPDA, tokenPubKey, - newRegistryAdminPrivateKey.PublicKey(), + authority, ).ValidateAndBuild() if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate instructions: %w", err) } - - instructions := []solana.Instruction{ix1} - // the new authority will have to sign the acceptance - if err := chain.Confirm(instructions, solCommonUtil.AddSigners(newRegistryAdminPrivateKey)); err != nil { + if routerUsingMCMS { + // We will only be able to accept the admin role if the pending admin is the timelock signer + tx, err := BuildMCMSTxn(ix1, chainState.Router.String(), ccipChangeset.Router) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create transaction: %w", err) + } + proposal, err := BuildProposalsForTxns( + e, cfg.ChainSelector, "proposal to AcceptAdminRoleTokenAdminRegistry in Solana", cfg.MCMSSolana.MCMS.MinDelay, []mcmsTypes.Transaction{*tx}) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + return deployment.ChangesetOutput{ + MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, + }, nil + } + if err := chain.Confirm([]solana.Instruction{ix1}); err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm instructions: %w", err) } return deployment.ChangesetOutput{}, nil diff --git a/deployment/ccip/changeset/solana/cs_token_pool.go b/deployment/ccip/changeset/solana/cs_token_pool.go index 7d85ecfa4fb..1d6d8b173f7 100644 --- a/deployment/ccip/changeset/solana/cs_token_pool.go +++ b/deployment/ccip/changeset/solana/cs_token_pool.go @@ -6,6 +6,9 @@ import ( "github.com/gagliardetto/solana-go" + "github.com/smartcontractkit/mcms" + mcmsTypes "github.com/smartcontractkit/mcms/types" + solBaseTokenPool "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/base_token_pool" solRouter "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" solBurnMintTokenPool "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/example_burnmint_token_pool" @@ -449,10 +452,10 @@ func AddTokenPoolLookupTable(e deployment.Environment, cfg TokenPoolLookupTableC } type SetPoolConfig struct { - ChainSelector uint64 - TokenPubKey string - TokenAdminRegistryAdminPrivateKey string - WritableIndexes []uint8 + ChainSelector uint64 + TokenPubKey string + WritableIndexes []uint8 + MCMSSolana *MCMSConfigSolana } func (cfg SetPoolConfig) Validate(e deployment.Environment) error { @@ -466,6 +469,13 @@ func (cfg SetPoolConfig) Validate(e deployment.Environment) error { if err := validateRouterConfig(chain, chainState); err != nil { return err } + if err := ValidateMCMSConfigSolana(e, cfg.ChainSelector, cfg.MCMSSolana); err != nil { + return err + } + routerUsingMcms := cfg.MCMSSolana != nil && cfg.MCMSSolana.RouterOwnedByTimelock + if err := ccipChangeset.ValidateOwnershipSolana(&e, chain, routerUsingMcms, chainState.Router, ccipChangeset.Router); err != nil { + return fmt.Errorf("failed to validate ownership: %w", err) + } tokenAdminRegistryPDA, _, err := solState.FindTokenAdminRegistryPDA(tokenPubKey, chainState.Router) if err != nil { return fmt.Errorf("failed to find token admin registry pda (mint: %s, router: %s): %w", tokenPubKey.String(), chainState.Router.String(), err) @@ -486,22 +496,31 @@ func SetPool(e deployment.Environment, cfg SetPoolConfig) (deployment.ChangesetO return deployment.ChangesetOutput{}, err } - chain := e.SolChains[cfg.ChainSelector] state, _ := ccipChangeset.LoadOnchainState(e) chainState := state.SolChains[cfg.ChainSelector] tokenPubKey := solana.MustPublicKeyFromBase58(cfg.TokenPubKey) routerConfigPDA, _, _ := solState.FindConfigPDA(chainState.Router) tokenAdminRegistryPDA, _, _ := solState.FindTokenAdminRegistryPDA(tokenPubKey, chainState.Router) - tokenAdminRegistryAdminPrivKey := solana.MustPrivateKeyFromBase58(cfg.TokenAdminRegistryAdminPrivateKey) lookupTablePubKey := chainState.TokenPoolLookupTable[tokenPubKey] + routerUsingMCMS := cfg.MCMSSolana != nil && cfg.MCMSSolana.RouterOwnedByTimelock + var authority solana.PublicKey + var err error + if routerUsingMCMS { + authority, err = FetchTimelockSigner(e, cfg.ChainSelector) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch timelock signer: %w", err) + } + } else { + authority = e.SolChains[cfg.ChainSelector].DeployerKey.PublicKey() + } base := solRouter.NewSetPoolInstruction( cfg.WritableIndexes, routerConfigPDA, tokenAdminRegistryPDA, tokenPubKey, lookupTablePubKey, - tokenAdminRegistryAdminPrivKey.PublicKey(), + authority, ) base.AccountMetaSlice = append(base.AccountMetaSlice, solana.Meta(lookupTablePubKey)) @@ -510,9 +529,23 @@ func SetPool(e deployment.Environment, cfg SetPoolConfig) (deployment.ChangesetO return deployment.ChangesetOutput{}, err } - instructions := []solana.Instruction{instruction} - err = chain.Confirm(instructions, solCommonUtil.AddSigners(tokenAdminRegistryAdminPrivKey)) - if err != nil { + if routerUsingMCMS { + tx, err := BuildMCMSTxn(instruction, chainState.Router.String(), ccipChangeset.Router) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create transaction: %w", err) + } + proposal, err := BuildProposalsForTxns( + e, cfg.ChainSelector, "proposal to RegisterTokenAdminRegistry in Solana", cfg.MCMSSolana.MCMS.MinDelay, []mcmsTypes.Transaction{*tx}) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + return deployment.ChangesetOutput{ + MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, + }, nil + } + + chain := e.SolChains[cfg.ChainSelector] + if err = chain.Confirm([]solana.Instruction{instruction}); err != nil { return deployment.ChangesetOutput{}, err } e.Logger.Infow("Set pool config", "token_pubkey", tokenPubKey.String())