diff --git a/deployment/ccip/changeset/solana/cs_add_remote_chain.go b/deployment/ccip/changeset/solana/cs_add_remote_chain.go index fcf8ffe1465..e12d69badc0 100644 --- a/deployment/ccip/changeset/solana/cs_add_remote_chain.go +++ b/deployment/ccip/changeset/solana/cs_add_remote_chain.go @@ -2,13 +2,18 @@ package solana import ( "context" - // "errors" + "fmt" "strconv" "github.com/ethereum/go-ethereum/common" "github.com/gagliardetto/solana-go" + "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" solRouter "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" solFeeQuoter "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/fee_quoter" @@ -17,8 +22,9 @@ import ( "github.com/smartcontractkit/chainlink/deployment" ccipChangeset "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" - commoncs "github.com/smartcontractkit/chainlink/deployment/common/changeset" - commonState "github.com/smartcontractkit/chainlink/deployment/common/changeset/state" + + "github.com/smartcontractkit/chainlink/deployment/common/changeset/state" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" ) // ADD REMOTE CHAIN @@ -29,6 +35,11 @@ type AddRemoteChainToSolanaConfig struct { // Disallow mixing MCMS/non-MCMS per chain for simplicity. // (can still be achieved by calling this function multiple times) MCMS *ccipChangeset.MCMSConfig + // Public key of program authorities. Depending on when this changeset is called, some may be under + // the control of the deployer, and some may be under the control of the timelock. (e.g. during new offramp deploy) + RouterAuthority solana.PublicKey + FeeQuoterAuthority solana.PublicKey + OffRampAuthority solana.PublicKey } type RemoteChainConfigSolana struct { @@ -55,19 +66,23 @@ func (cfg AddRemoteChainToSolanaConfig) Validate(e deployment.Environment) error if err := validateOffRampConfig(chain, chainState); err != nil { return err } + if err := ValidateMCMSConfig(e, cfg.ChainSelector, cfg.MCMS); err != nil { + return err + } + routerUsingMCMS := cfg.MCMS != nil && !cfg.RouterAuthority.IsZero() + feeQuoterUsingMCMS := cfg.MCMS != nil && !cfg.FeeQuoterAuthority.IsZero() + offRampUsingMCMS := cfg.MCMS != nil && !cfg.OffRampAuthority.IsZero() chain, ok := e.SolChains[cfg.ChainSelector] if !ok { return fmt.Errorf("chain %d not found in environment", cfg.ChainSelector) } - addresses, err := e.ExistingAddresses.AddressesForChain(cfg.ChainSelector) - if err != nil { - return err + if err := ccipChangeset.ValidateOwnershipSolana(&e, chain, routerUsingMCMS, e.SolChains[cfg.ChainSelector].DeployerKey.PublicKey(), chainState.Router, ccipChangeset.Router); err != nil { + return fmt.Errorf("failed to validate ownership: %w", err) } - mcmState, err := commonState.MaybeLoadMCMSWithTimelockChainStateSolana(chain, addresses) - if err != nil { - return fmt.Errorf("error loading MCMS state for chain %d: %w", cfg.ChainSelector, err) + if err := ccipChangeset.ValidateOwnershipSolana(&e, chain, feeQuoterUsingMCMS, e.SolChains[cfg.ChainSelector].DeployerKey.PublicKey(), chainState.FeeQuoter, ccipChangeset.FeeQuoter); err != nil { + return fmt.Errorf("failed to validate ownership: %w", err) } - if err := commoncs.ValidateOwnershipSolana(e.GetContext(), cfg.MCMS != nil, e.SolChains[cfg.ChainSelector].DeployerKey.PublicKey(), mcmState.TimelockProgram, mcmState.TimelockSeed, chainState.Router); err != nil { + if err := ccipChangeset.ValidateOwnershipSolana(&e, chain, offRampUsingMCMS, e.SolChains[cfg.ChainSelector].DeployerKey.PublicKey(), chainState.OffRamp, ccipChangeset.OffRamp); err != nil { return fmt.Errorf("failed to validate ownership: %w", err) } var routerConfigAccount solRouter.Config @@ -110,23 +125,66 @@ func AddRemoteChainToSolana(e deployment.Environment, cfg AddRemoteChainToSolana } ab := deployment.NewMemoryAddressBook() - err = doAddRemoteChainToSolana(e, s, cfg.ChainSelector, cfg.UpdatesByChain, ab) + txns, err := doAddRemoteChainToSolana(e, s, cfg, ab) if err != nil { return deployment.ChangesetOutput{AddressBook: ab}, err } + + // create proposals for ixns + if len(txns) > 0 { + timelocks := map[uint64]string{} + proposers := map[uint64]string{} + inspectors := map[uint64]sdk.Inspector{} + batches := make([]mcmsTypes.BatchOperation, 0) + chain := e.SolChains[cfg.ChainSelector] + addresses, _ := e.ExistingAddresses.AddressesForChain(cfg.ChainSelector) + mcmState, _ := state.MaybeLoadMCMSWithTimelockChainStateSolana(chain, addresses) + + timelocks[cfg.ChainSelector] = mcmsSolana.ContractAddress( + mcmState.TimelockProgram, + mcmsSolana.PDASeed(mcmState.TimelockSeed), + ) + proposers[cfg.ChainSelector] = mcmsSolana.ContractAddress(mcmState.McmProgram, mcmsSolana.PDASeed(mcmState.ProposerMcmSeed)) + inspectors[cfg.ChainSelector] = mcmsSolana.NewInspector(chain.Client) + batches = append(batches, mcmsTypes.BatchOperation{ + ChainSelector: mcmsTypes.ChainSelector(cfg.ChainSelector), + Transactions: txns, + }) + proposal, err := proposalutils.BuildProposalFromBatchesV2( + e.GetContext(), + timelocks, + proposers, + inspectors, + batches, + "proposal to add remote chains to 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}, + AddressBook: ab, + }, nil + } return deployment.ChangesetOutput{AddressBook: ab}, nil } func doAddRemoteChainToSolana( e deployment.Environment, s ccipChangeset.CCIPOnChainState, - chainSel uint64, - updates map[uint64]RemoteChainConfigSolana, - ab deployment.AddressBook) error { + cfg AddRemoteChainToSolanaConfig, + ab deployment.AddressBook) ([]mcmsTypes.Transaction, error) { + txns := make([]mcmsTypes.Transaction, 0) + ixns := make([]solana.Instruction, 0) + chainSel := cfg.ChainSelector + updates := cfg.UpdatesByChain chain := e.SolChains[chainSel] ccipRouterID := s.SolChains[chainSel].Router feeQuoterID := s.SolChains[chainSel].FeeQuoter offRampID := s.SolChains[chainSel].OffRamp + routerUsingMCMS := cfg.MCMS != nil && !cfg.RouterAuthority.IsZero() + feeQuoterUsingMCMS := cfg.MCMS != nil && !cfg.FeeQuoterAuthority.IsZero() + offRampUsingMCMS := cfg.MCMS != nil && !cfg.OffRampAuthority.IsZero() lookUpTableEntries := make([]solana.PublicKey, 0) for remoteChainSel, update := range updates { @@ -149,16 +207,31 @@ func doAddRemoteChainToSolana( ) solRouter.SetProgramID(ccipRouterID) + var authority solana.PublicKey + if routerUsingMCMS { + authority = cfg.RouterAuthority + } else { + authority = chain.DeployerKey.PublicKey() + } routerIx, err := solRouter.NewAddChainSelectorInstruction( remoteChainSel, update.RouterDestinationConfig, routerRemoteStatePDA, s.SolChains[chainSel].RouterConfigPDA, - chain.DeployerKey.PublicKey(), + authority, solana.SystemProgramID, ).ValidateAndBuild() if err != nil { - return fmt.Errorf("failed to generate instructions: %w", err) + return txns, fmt.Errorf("failed to generate instructions: %w", err) + } + if routerUsingMCMS { + tx, err := BuildMCMSTxn(routerIx, ccipRouterID.String(), ccipChangeset.Router) + if err != nil { + return txns, fmt.Errorf("failed to create transaction: %w", err) + } + txns = append(txns, *tx) + } else { + ixns = append(ixns, routerIx) } routerOfframpIx, err := solRouter.NewAddOfframpInstruction( @@ -166,24 +239,47 @@ func doAddRemoteChainToSolana( offRampID, allowedOffRampRemotePDA, s.SolChains[chainSel].RouterConfigPDA, - chain.DeployerKey.PublicKey(), + authority, solana.SystemProgramID, ).ValidateAndBuild() if err != nil { - return fmt.Errorf("failed to generate instructions: %w", err) + return txns, fmt.Errorf("failed to generate instructions: %w", err) + } + if routerUsingMCMS { + tx, err := BuildMCMSTxn(routerOfframpIx, ccipRouterID.String(), ccipChangeset.Router) + if err != nil { + return txns, fmt.Errorf("failed to create transaction: %w", err) + } + txns = append(txns, *tx) + } else { + ixns = append(ixns, routerOfframpIx) } solFeeQuoter.SetProgramID(feeQuoterID) + if feeQuoterUsingMCMS { + authority = cfg.RouterAuthority + } else { + authority = chain.DeployerKey.PublicKey() + } feeQuoterIx, err := solFeeQuoter.NewAddDestChainInstruction( remoteChainSel, update.FeeQuoterDestinationConfig, s.SolChains[chainSel].FeeQuoterConfigPDA, fqRemoteChainPDA, - chain.DeployerKey.PublicKey(), + authority, solana.SystemProgramID, ).ValidateAndBuild() if err != nil { - return fmt.Errorf("failed to generate instructions: %w", err) + return txns, fmt.Errorf("failed to generate instructions: %w", err) + } + if feeQuoterUsingMCMS { + tx, err := BuildMCMSTxn(feeQuoterIx, feeQuoterID.String(), ccipChangeset.FeeQuoter) + if err != nil { + return txns, fmt.Errorf("failed to create transaction: %w", err) + } + txns = append(txns, *tx) + } else { + ixns = append(ixns, feeQuoterIx) } solOffRamp.SetProgramID(offRampID) @@ -191,22 +287,37 @@ func doAddRemoteChainToSolana( OnRamp: [2][64]byte{onRampBytes, [64]byte{}}, IsEnabled: update.EnabledAsSource, } + if offRampUsingMCMS { + authority = cfg.RouterAuthority + } else { + authority = chain.DeployerKey.PublicKey() + } offRampIx, err := solOffRamp.NewAddSourceChainInstruction( remoteChainSel, validSourceChainConfig, offRampRemoteStatePDA, s.SolChains[chainSel].OffRampConfigPDA, - chain.DeployerKey.PublicKey(), + authority, solana.SystemProgramID, ).ValidateAndBuild() if err != nil { - return fmt.Errorf("failed to generate instructions: %w", err) + return txns, fmt.Errorf("failed to generate instructions: %w", err) } - - err = chain.Confirm([]solana.Instruction{routerIx, routerOfframpIx, feeQuoterIx, offRampIx}) - if err != nil { - return fmt.Errorf("failed to confirm instructions: %w", err) + if offRampUsingMCMS { + tx, err := BuildMCMSTxn(offRampIx, offRampID.String(), ccipChangeset.OffRamp) + if err != nil { + return txns, fmt.Errorf("failed to create transaction: %w", err) + } + txns = append(txns, *tx) + } else { + ixns = append(ixns, offRampIx) + } + if len(ixns) > 0 { + err = chain.Confirm(ixns) + if err != nil { + return txns, fmt.Errorf("failed to confirm instructions: %w", err) + } } tv := deployment.NewTypeAndVersion(ccipChangeset.RemoteDest, deployment.Version1_0_0) @@ -214,20 +325,20 @@ func doAddRemoteChainToSolana( tv.AddLabel(remoteChainSelStr) err = ab.Save(chainSel, routerRemoteStatePDA.String(), tv) if err != nil { - return fmt.Errorf("failed to save dest chain state to address book: %w", err) + return txns, fmt.Errorf("failed to save dest chain state to address book: %w", err) } tv = deployment.NewTypeAndVersion(ccipChangeset.RemoteSource, deployment.Version1_0_0) tv.AddLabel(remoteChainSelStr) err = ab.Save(chainSel, allowedOffRampRemotePDA.String(), tv) if err != nil { - return fmt.Errorf("failed to save source chain state to address book: %w", err) + return txns, fmt.Errorf("failed to save source chain state to address book: %w", err) } } addressLookupTable, err := ccipChangeset.FetchOfframpLookupTable(e.GetContext(), chain, offRampID) if err != nil { - return fmt.Errorf("failed to get offramp reference addresses: %w", err) + return txns, fmt.Errorf("failed to get offramp reference addresses: %w", err) } if err := solCommonUtil.ExtendLookupTable( @@ -237,8 +348,8 @@ func doAddRemoteChainToSolana( *chain.DeployerKey, lookUpTableEntries, ); err != nil { - return fmt.Errorf("failed to extend lookup table: %w", err) + return txns, fmt.Errorf("failed to extend lookup table: %w", err) } - return nil + return txns, nil } diff --git a/deployment/ccip/changeset/solana/cs_build_solana.go b/deployment/ccip/changeset/solana/cs_build_solana.go index 4430d511ae5..c8d2c6bdabc 100644 --- a/deployment/ccip/changeset/solana/cs_build_solana.go +++ b/deployment/ccip/changeset/solana/cs_build_solana.go @@ -6,10 +6,12 @@ import ( "os" "os/exec" "path/filepath" + "regexp" chainsel "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/chainlink/deployment" + cs "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" ) var _ deployment.ChangeSet[BuildSolanaConfig] = BuildSolanaChangeset @@ -22,6 +24,13 @@ const ( deployDir = "chains/solana/contracts/target/deploy" ) +// Map program names to their Rust file paths (relative to the Anchor project root) +// Needed for upgrades in place +var programToFileMap = map[deployment.ContractType]string{ + cs.Router: "programs/ccip-router/src/lib.rs", + cs.FeeQuoter: "programs/fee-quoter/src/lib.rs", +} + // Run a command in a specific directory func runCommand(command string, args []string, workDir string) (string, error) { cmd := exec.Command(command, args...) @@ -37,8 +46,14 @@ func runCommand(command string, args []string, workDir string) (string, error) { } // Clone and checkout the specific revision of the repo -func cloneRepo(e deployment.Environment, revision string) error { +func cloneRepo(e deployment.Environment, revision string, forceClean bool) error { // Check if the repository already exists + if forceClean { + e.Logger.Debugw("Cleaning repository", "dir", cloneDir) + if err := os.RemoveAll(cloneDir); err != nil { + return fmt.Errorf("failed to clean repository: %w", err) + } + } if _, err := os.Stat(filepath.Join(cloneDir, ".git")); err == nil { e.Logger.Debugw("Repository already exists, discarding local changes and updating", "dir", cloneDir) @@ -83,6 +98,32 @@ func replaceKeys(e deployment.Environment) error { return nil } +func replaceKeysForUpgrade(e deployment.Environment, keys map[deployment.ContractType]string) error { + e.Logger.Debug("Replacing keys in Rust files...") + for program, key := range keys { + programStr := string(program) + filePath, exists := programToFileMap[program] + if !exists { + return fmt.Errorf("no file path found for program %s", programStr) + } + + fullPath := filepath.Join(cloneDir, anchorDir, filePath) + content, err := os.ReadFile(fullPath) + if err != nil { + return fmt.Errorf("failed to read file %s: %w", fullPath, err) + } + + // Replace declare_id!("..."); with the new key + updatedContent := regexp.MustCompile(`declare_id!\(".*?"\);`).ReplaceAllString(string(content), fmt.Sprintf(`declare_id!("%s");`, key)) + err = os.WriteFile(fullPath, []byte(updatedContent), 0600) + if err != nil { + return fmt.Errorf("failed to write updated keys to file %s: %w", fullPath, err) + } + e.Logger.Debugw("Updated key for program %s in file %s\n", programStr, filePath) + } + return nil +} + func copyFile(srcFile string, destDir string) error { output, err := runCommand("cp", []string{srcFile, destDir}, ".") if err != nil { @@ -108,6 +149,9 @@ type BuildSolanaConfig struct { DestinationDir string CleanDestinationDir bool CreateDestinationDir bool + // Forces re-clone of git directory. Useful for forcing regeneration of keys + CleanGitDir bool + UpgradeKeys map[deployment.ContractType]string } func BuildSolanaChangeset(e deployment.Environment, config BuildSolanaConfig) (deployment.ChangesetOutput, error) { @@ -124,14 +168,21 @@ func BuildSolanaChangeset(e deployment.Environment, config BuildSolanaConfig) (d } // Clone the repository - if err := cloneRepo(e, config.GitCommitSha); err != nil { + if err := cloneRepo(e, config.GitCommitSha, config.CleanGitDir); err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("error cloning repo: %w", err) } + // Replace keys in Rust files using anchor keys sync if err := replaceKeys(e); err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("error replacing keys: %w", err) } + // Replace keys in Rust files for upgrade by replacing the declare_id!() macro explicitly + // We need to do this so the keys will match the existing deployed program + if err := replaceKeysForUpgrade(e, config.UpgradeKeys); err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("error replacing keys for upgrade: %w", err) + } + // Build the project with Anchor if err := buildProject(e); err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("error building project: %w", err) diff --git a/deployment/ccip/changeset/solana/cs_chain_contracts_test.go b/deployment/ccip/changeset/solana/cs_chain_contracts_test.go index 5132bbec813..b35f0aab139 100644 --- a/deployment/ccip/changeset/solana/cs_chain_contracts_test.go +++ b/deployment/ccip/changeset/solana/cs_chain_contracts_test.go @@ -3,6 +3,7 @@ package solana_test import ( "math/big" "testing" + "time" "github.com/gagliardetto/solana-go" "github.com/stretchr/testify/require" @@ -53,6 +54,7 @@ func TestAddRemoteChain(t *testing.T) { tenv, _ := testhelpers.NewMemoryEnvironment(t, testhelpers.WithSolChains(1)) evmChain := tenv.Env.AllChainSelectors()[0] + evmChain2 := tenv.Env.AllChainSelectors()[1] solChain := tenv.Env.AllChainSelectorsSolana()[0] _, err := ccipChangeset.LoadOnchainStateSolana(tenv.Env) @@ -114,6 +116,72 @@ func TestAddRemoteChain(t *testing.T) { require.NoError(t, err, "failed to get account info") require.Equal(t, solFeeQuoter.TimestampedPackedU224{}, destChainFqAccount.State.UsdPerUnitGas) require.True(t, destChainFqAccount.Config.IsEnabled) + + timelockSignerPDA, _ := testhelpers.TransferOwnershipSolana(t, &tenv.Env, solChain, true, true, true, true) + + tenv.Env, err = commonchangeset.ApplyChangesetsV2(t, tenv.Env, + []commonchangeset.ConfiguredChangeSet{ + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(v1_6.UpdateOnRampsDestsChangeset), + v1_6.UpdateOnRampDestsConfig{ + UpdatesByChain: map[uint64]map[uint64]v1_6.OnRampDestinationUpdate{ + evmChain2: { + solChain: { + IsEnabled: true, + TestRouter: false, + AllowListEnabled: false, + }, + }, + }, + }, + ), + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.AddRemoteChainToSolana), + ccipChangesetSolana.AddRemoteChainToSolanaConfig{ + ChainSelector: solChain, + UpdatesByChain: map[uint64]ccipChangesetSolana.RemoteChainConfigSolana{ + evmChain2: { + EnabledAsSource: true, + RouterDestinationConfig: solRouter.DestChainConfig{}, + FeeQuoterDestinationConfig: solFeeQuoter.DestChainConfig{ + IsEnabled: true, + DefaultTxGasLimit: 200000, + MaxPerMsgGasLimit: 3000000, + MaxDataBytes: 30000, + MaxNumberOfTokensPerMsg: 5, + DefaultTokenDestGasOverhead: 5000, + // bytes4(keccak256("CCIP ChainFamilySelector EVM")) + // TODO: do a similar test for other chain families + // https://smartcontract-it.atlassian.net/browse/INTAUTO-438 + ChainFamilySelector: [4]uint8{40, 18, 213, 44}, + }, + }, + }, + MCMS: &ccipChangeset.MCMSConfig{ + MinDelay: 1 * time.Second, + }, + RouterAuthority: timelockSignerPDA, + FeeQuoterAuthority: timelockSignerPDA, + OffRampAuthority: timelockSignerPDA, + }, + ), + }, + ) + + require.NoError(t, err) + + state, err = ccipChangeset.LoadOnchainStateSolana(tenv.Env) + require.NoError(t, err) + + evmDestChainStatePDA = state.SolChains[solChain].DestChainStatePDAs[evmChain2] + err = tenv.Env.SolChains[solChain].GetAccountDataBorshInto(ctx, evmDestChainStatePDA, &destChainStateAccount) + require.NoError(t, err) + + fqEvmDestChainPDA, _, _ = solState.FindFqDestChainPDA(evmChain2, state.SolChains[solChain].FeeQuoter) + err = tenv.Env.SolChains[solChain].GetAccountDataBorshInto(ctx, fqEvmDestChainPDA, &destChainFqAccount) + require.NoError(t, err, "failed to get account info") + require.Equal(t, solFeeQuoter.TimestampedPackedU224{}, destChainFqAccount.State.UsdPerUnitGas) + require.True(t, destChainFqAccount.Config.IsEnabled) } func TestDeployCCIPContracts(t *testing.T) { diff --git a/deployment/ccip/changeset/solana/cs_deploy_chain.go b/deployment/ccip/changeset/solana/cs_deploy_chain.go index 95cbb45ca0a..ffe4cb10c89 100644 --- a/deployment/ccip/changeset/solana/cs_deploy_chain.go +++ b/deployment/ccip/changeset/solana/cs_deploy_chain.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "math" - "math/big" "github.com/Masterminds/semver/v3" "github.com/gagliardetto/solana-go" @@ -42,6 +41,16 @@ const ( var _ deployment.ChangeSet[DeployChainContractsConfig] = DeployChainContractsChangeset +func getTypeToProgramDeployName() map[deployment.ContractType]string { + return map[deployment.ContractType]string{ + ccipChangeset.Router: RouterProgramName, + ccipChangeset.OffRamp: OffRampProgramName, + ccipChangeset.FeeQuoter: FeeQuoterProgramName, + ccipChangeset.BurnMintTokenPool: BurnMintTokenPool, + ccipChangeset.LockReleaseTokenPool: LockReleaseTokenPool, + } +} + type DeployChainContractsConfig struct { HomeChainSelector uint64 ContractParamsPerChain map[uint64]ChainContractParams @@ -70,20 +79,22 @@ type UpgradeConfig struct { // SpillAddress and UpgradeAuthority must be set SpillAddress solana.PublicKey UpgradeAuthority solana.PublicKey - MCMS *ccipChangeset.MCMSConfig + // MCMS config must be set for upgrades and offramp redploys (to configure the fee quoter after redeploy) + MCMS *ccipChangeset.MCMSConfig } func (cfg UpgradeConfig) Validate(e deployment.Environment, chainSelector uint64) error { if cfg.NewFeeQuoterVersion == nil && cfg.NewRouterVersion == nil && cfg.NewOffRampVersion == nil { return nil } - if cfg.NewFeeQuoterVersion != nil || cfg.NewRouterVersion != nil { - if cfg.SpillAddress.IsZero() { - return errors.New("spill address must be set for fee quoter and router upgrades") - } - if cfg.UpgradeAuthority.IsZero() { - return errors.New("upgrade authority must be set for fee quoter and router upgrades") - } + if cfg.MCMS == nil { + return errors.New("MCMS config must be set for upgrades") + } + if cfg.SpillAddress.IsZero() { + return errors.New("spill address must be set for fee quoter and router upgrades") + } + if cfg.UpgradeAuthority.IsZero() { + return errors.New("upgrade authority must be set for fee quoter and router upgrades") } return ValidateMCMSConfig(e, chainSelector, cfg.MCMS) } @@ -152,7 +163,7 @@ func DeployChainContractsChangeset(e deployment.Environment, c DeployChainContra e.Logger.Errorw("Failed to deploy CCIP contracts", "err", err, "newAddresses", newAddresses) return deployment.ChangesetOutput{}, err } - // create proposals for ixns + // create proposals for txns if len(mcmsTxs) > 0 { batches = append(batches, mcmsTypes.BatchOperation{ ChainSelector: mcmsTypes.ChainSelector(chainSel), @@ -161,7 +172,7 @@ func DeployChainContractsChangeset(e deployment.Environment, c DeployChainContra } } - if c.UpgradeConfig.MCMS != nil { + if len(batches) > 0 { proposal, err := proposalutils.BuildProposalFromBatchesV2( e.GetContext(), timelocks, @@ -376,18 +387,18 @@ func deployChainContractsSolana( config DeployChainContractsConfig, ) ([]mcmsTypes.Transaction, error) { // we may need to gather instructions and submit them as part of MCMS - ixns := make([]mcmsTypes.Transaction, 0) + txns := make([]mcmsTypes.Transaction, 0) state, err := ccipChangeset.LoadOnchainStateSolana(e) if err != nil { e.Logger.Errorw("Failed to load existing onchain state", "err", err) - return ixns, err + return txns, err } chainState, chainExists := state.SolChains[chain.Selector] if !chainExists { - return ixns, fmt.Errorf("chain %s not found in existing state, deploy the link token first", chain.String()) + return txns, fmt.Errorf("chain %s not found in existing state, deploy the link token first", chain.String()) } if chainState.LinkToken.IsZero() { - return ixns, fmt.Errorf("failed to get link token address for chain %s", chain.String()) + return txns, fmt.Errorf("failed to get link token address for chain %s", chain.String()) } params := config.ContractParamsPerChain[chain.Selector] @@ -396,98 +407,18 @@ func deployChainContractsSolana( var feeQuoterAddress solana.PublicKey //nolint:gocritic // this is a false positive, we need to check if the address is zero if chainState.FeeQuoter.IsZero() { - feeQuoterAddress, err = DeployAndMaybeSaveToAddressBook(e, chain, ab, FeeQuoterProgramName, deployment.Version1_0_0, false) + feeQuoterAddress, err = DeployAndMaybeSaveToAddressBook(e, chain, ab, ccipChangeset.FeeQuoter, deployment.Version1_0_0, false) if err != nil { - return ixns, fmt.Errorf("failed to deploy program: %w", err) + return txns, fmt.Errorf("failed to deploy program: %w", err) } } else if config.UpgradeConfig.NewFeeQuoterVersion != nil { // fee quoter updated in place - bufferProgram, err := DeployAndMaybeSaveToAddressBook(e, chain, ab, FeeQuoterProgramName, *config.UpgradeConfig.NewFeeQuoterVersion, true) - if err != nil { - return ixns, fmt.Errorf("failed to deploy program: %w", err) - } - if err := setUpgradeAuthority(&e, &chain, bufferProgram, chain.DeployerKey, config.UpgradeConfig.UpgradeAuthority.ToPointer(), true); err != nil { - return ixns, fmt.Errorf("failed to set upgrade authority: %w", err) - } - extendIxn, err := generateExtendIxn( - &e, - chain, - chainState.FeeQuoter, - bufferProgram, - config.UpgradeConfig.SpillAddress, - ) - if err != nil { - return ixns, fmt.Errorf("failed to generate extend instruction: %w", err) - } - upgradeIxn, err := generateUpgradeIxn( - &e, - chainState.FeeQuoter, - bufferProgram, - config.UpgradeConfig.SpillAddress, - config.UpgradeConfig.UpgradeAuthority, - ) - if err != nil { - return ixns, fmt.Errorf("failed to generate upgrade instruction: %w", err) - } - closeIxn, err := generateCloseBufferIxn( - &e, - bufferProgram, - config.UpgradeConfig.SpillAddress, - config.UpgradeConfig.UpgradeAuthority, - ) - if err != nil { - return ixns, fmt.Errorf("failed to generate close buffer instruction: %w", err) - } feeQuoterAddress = chainState.FeeQuoter - upgradeData, err := upgradeIxn.Data() - if err != nil { - return ixns, fmt.Errorf("failed to extract upgrade data: %w", err) - } - upgradeTx, err := mcmsSolana.NewTransaction( - solana.BPFLoaderUpgradeableProgramID.String(), - upgradeData, - big.NewInt(0), // e.g. value - upgradeIxn.Accounts(), // pass along needed accounts - string(ccipChangeset.FeeQuoter), // some string identifying the target - []string{}, // any relevant metadata - ) + newTxns, err := generateUpgradeTxns(e, chain, ab, config, config.UpgradeConfig.NewFeeQuoterVersion, chainState.FeeQuoter, ccipChangeset.FeeQuoter) if err != nil { - return ixns, fmt.Errorf("failed to create upgrade transaction: %w", err) + return txns, fmt.Errorf("failed to generate upgrade txns: %w", err) } - closeData, err := closeIxn.Data() - if err != nil { - return ixns, fmt.Errorf("failed to extract close data: %w", err) - } - closeTx, err := mcmsSolana.NewTransaction( - solana.BPFLoaderUpgradeableProgramID.String(), - closeData, - big.NewInt(0), // e.g. value - closeIxn.Accounts(), // pass along needed accounts - string(ccipChangeset.FeeQuoter), // some string identifying the target - []string{}, // any relevant metadata - ) - if err != nil { - return ixns, fmt.Errorf("failed to create close transaction: %w", err) - } - if extendIxn != nil { - extendData, err := extendIxn.Data() - if err != nil { - return ixns, fmt.Errorf("failed to extract extend data: %w", err) - } - extendTx, err := mcmsSolana.NewTransaction( - solana.BPFLoaderUpgradeableProgramID.String(), - extendData, - big.NewInt(0), // e.g. value - extendIxn.Accounts(), // pass along needed accounts - string(ccipChangeset.FeeQuoter), // some string identifying the target - []string{}, // any relevant metadata - ) - if err != nil { - return ixns, fmt.Errorf("failed to create extend transaction: %w", err) - } - ixns = append(ixns, extendTx) - } - ixns = append(ixns, upgradeTx, closeTx) + txns = append(txns, newTxns...) } else { e.Logger.Infow("Using existing fee quoter", "addr", chainState.FeeQuoter.String()) feeQuoterAddress = chainState.FeeQuoter @@ -499,98 +430,18 @@ func deployChainContractsSolana( //nolint:gocritic // this is a false positive, we need to check if the address is zero if chainState.Router.IsZero() { // deploy router - ccipRouterProgram, err = DeployAndMaybeSaveToAddressBook(e, chain, ab, RouterProgramName, deployment.Version1_0_0, false) + ccipRouterProgram, err = DeployAndMaybeSaveToAddressBook(e, chain, ab, ccipChangeset.Router, deployment.Version1_0_0, false) if err != nil { - return ixns, fmt.Errorf("failed to deploy program: %w", err) + return txns, fmt.Errorf("failed to deploy program: %w", err) } } else if config.UpgradeConfig.NewRouterVersion != nil { // router updated in place - bufferProgram, err := DeployAndMaybeSaveToAddressBook(e, chain, ab, RouterProgramName, *config.UpgradeConfig.NewRouterVersion, true) - if err != nil { - return ixns, fmt.Errorf("failed to deploy program: %w", err) - } - if err := setUpgradeAuthority(&e, &chain, bufferProgram, chain.DeployerKey, config.UpgradeConfig.UpgradeAuthority.ToPointer(), true); err != nil { - return ixns, fmt.Errorf("failed to set upgrade authority: %w", err) - } - upgradeIxn, err := generateUpgradeIxn( - &e, - chainState.Router, - bufferProgram, - config.UpgradeConfig.SpillAddress, - config.UpgradeConfig.UpgradeAuthority, - ) - if err != nil { - return ixns, fmt.Errorf("failed to generate upgrade instruction: %w", err) - } - extendIxn, err := generateExtendIxn( - &e, - chain, - chainState.Router, - bufferProgram, - config.UpgradeConfig.SpillAddress, - ) - if err != nil { - return ixns, fmt.Errorf("failed to generate extend instruction: %w", err) - } - closeIxn, err := generateCloseBufferIxn( - &e, - bufferProgram, - config.UpgradeConfig.SpillAddress, - config.UpgradeConfig.UpgradeAuthority, - ) - if err != nil { - return ixns, fmt.Errorf("failed to generate close buffer instruction: %w", err) - } ccipRouterProgram = chainState.Router - upgradeData, err := upgradeIxn.Data() - if err != nil { - return ixns, fmt.Errorf("failed to extract upgrade data: %w", err) - } - upgradeTx, err := mcmsSolana.NewTransaction( - solana.BPFLoaderUpgradeableProgramID.String(), - upgradeData, - big.NewInt(0), // e.g. value - upgradeIxn.Accounts(), // pass along needed accounts - string(ccipChangeset.Router), // some string identifying the target - []string{}, // any relevant metadata - ) - if err != nil { - return ixns, fmt.Errorf("failed to create upgrade transaction: %w", err) - } - closeData, err := closeIxn.Data() + newTxns, err := generateUpgradeTxns(e, chain, ab, config, config.UpgradeConfig.NewRouterVersion, chainState.Router, ccipChangeset.Router) if err != nil { - return ixns, fmt.Errorf("failed to extract close data: %w", err) + return txns, fmt.Errorf("failed to generate upgrade txns: %w", err) } - closeTx, err := mcmsSolana.NewTransaction( - solana.BPFLoaderUpgradeableProgramID.String(), - closeData, - big.NewInt(0), // e.g. value - closeIxn.Accounts(), // pass along needed accounts - string(ccipChangeset.Router), // some string identifying the target - []string{}, // any relevant metadata - ) - if err != nil { - return ixns, fmt.Errorf("failed to create close transaction: %w", err) - } - if extendIxn != nil { - extendData, err := extendIxn.Data() - if err != nil { - return ixns, fmt.Errorf("failed to extract extend data: %w", err) - } - extendTx, err := mcmsSolana.NewTransaction( - solana.BPFLoaderUpgradeableProgramID.String(), - extendData, - big.NewInt(0), // e.g. value - extendIxn.Accounts(), // pass along needed accounts - string(ccipChangeset.Router), // some string identifying the target - []string{}, // any relevant metadata - ) - if err != nil { - return ixns, fmt.Errorf("failed to create extend transaction: %w", err) - } - ixns = append(ixns, extendTx) - } - ixns = append(ixns, upgradeTx, closeTx) + txns = append(txns, newTxns...) } else { e.Logger.Infow("Using existing router", "addr", chainState.Router.String()) ccipRouterProgram = chainState.Router @@ -607,22 +458,22 @@ func deployChainContractsSolana( //nolint:gocritic // this is a false positive, we need to check if the address is zero if chainState.OffRamp.IsZero() { // deploy offramp - offRampAddress, err = DeployAndMaybeSaveToAddressBook(e, chain, ab, OffRampProgramName, deployment.Version1_0_0, false) + offRampAddress, err = DeployAndMaybeSaveToAddressBook(e, chain, ab, ccipChangeset.OffRamp, deployment.Version1_0_0, false) if err != nil { - return ixns, fmt.Errorf("failed to deploy program: %w", err) + return txns, fmt.Errorf("failed to deploy program: %w", err) } } else if config.UpgradeConfig.NewOffRampVersion != nil { tv := deployment.NewTypeAndVersion(ccipChangeset.OffRamp, *config.UpgradeConfig.NewOffRampVersion) existingAddresses, err := e.ExistingAddresses.AddressesForChain(chain.Selector) if err != nil { - return ixns, fmt.Errorf("failed to get existing addresses: %w", err) + return txns, fmt.Errorf("failed to get existing addresses: %w", err) } offRampAddress = ccipChangeset.FindSolanaAddress(tv, existingAddresses) if offRampAddress.IsZero() { // deploy offramp, not upgraded in place so upgrade is false - offRampAddress, err = DeployAndMaybeSaveToAddressBook(e, chain, ab, OffRampProgramName, *config.UpgradeConfig.NewOffRampVersion, false) + offRampAddress, err = DeployAndMaybeSaveToAddressBook(e, chain, ab, ccipChangeset.OffRamp, *config.UpgradeConfig.NewOffRampVersion, false) if err != nil { - return ixns, fmt.Errorf("failed to deploy program: %w", err) + return txns, fmt.Errorf("failed to deploy program: %w", err) } } @@ -634,28 +485,17 @@ func deployChainContractsSolana( offRampBillingSignerPDA, fqAllowedPriceUpdaterOfframpPDA, feeQuoterConfigPDA, - chain.DeployerKey.PublicKey(), + config.UpgradeConfig.UpgradeAuthority, solana.SystemProgramID, ).ValidateAndBuild() if err != nil { - return ixns, fmt.Errorf("failed to build instruction: %w", err) + return txns, fmt.Errorf("failed to build instruction: %w", err) } - priceUpdaterData, err := priceUpdaterix.Data() - if err != nil { - return ixns, fmt.Errorf("failed to extract price updater data: %w", err) - } - priceUpdaterTx, err := mcmsSolana.NewTransaction( - feeQuoterAddress.String(), - priceUpdaterData, - big.NewInt(0), // e.g. value - priceUpdaterix.Accounts(), // pass along needed accounts - string(ccipChangeset.OffRamp), // some string identifying the target - []string{}, // any relevant metadata - ) + priceUpdaterTx, err := BuildMCMSTxn(priceUpdaterix, feeQuoterAddress.String(), ccipChangeset.FeeQuoter) if err != nil { - return ixns, fmt.Errorf("failed to create price updater transaction: %w", err) + return txns, fmt.Errorf("failed to create price updater transaction: %w", err) } - ixns = append(ixns, priceUpdaterTx) + txns = append(txns, *priceUpdaterTx) } else { e.Logger.Infow("Using existing offramp", "addr", chainState.OffRamp.String()) offRampAddress = chainState.OffRamp @@ -668,7 +508,7 @@ func deployChainContractsSolana( err = chain.GetAccountDataBorshInto(e.GetContext(), feeQuoterConfigPDA, &fqConfig) if err != nil { if err2 := initializeFeeQuoter(e, chain, ccipRouterProgram, chainState.LinkToken, feeQuoterAddress, offRampAddress, params.FeeQuoterParams); err2 != nil { - return ixns, err2 + return txns, err2 } } else { e.Logger.Infow("Fee quoter already initialized, skipping initialization", "chain", chain.String()) @@ -681,7 +521,7 @@ func deployChainContractsSolana( err = chain.GetAccountDataBorshInto(e.GetContext(), routerConfigPDA, &routerConfigAccount) if err != nil { if err2 := initializeRouter(e, chain, ccipRouterProgram, chainState.LinkToken, feeQuoterAddress); err2 != nil { - return ixns, err2 + return txns, err2 } } else { e.Logger.Infow("Router already initialized, skipping initialization", "chain", chain.String()) @@ -707,10 +547,10 @@ func deployChainContractsSolana( solana.SPLAssociatedTokenAccountProgramID, }) if err2 != nil { - return ixns, fmt.Errorf("failed to create address lookup table: %w", err) + return txns, fmt.Errorf("failed to create address lookup table: %w", err) } if err2 := initializeOffRamp(e, chain, ccipRouterProgram, feeQuoterAddress, offRampAddress, table, params.OffRampParams); err2 != nil { - return ixns, err2 + return txns, err2 } // Initializing a new offramp means we need a new lookup table and need to fully populate it needFQinLookupTable = true @@ -732,9 +572,9 @@ func deployChainContractsSolana( var burnMintTokenPool solana.PublicKey if chainState.BurnMintTokenPool.IsZero() { - burnMintTokenPool, err = DeployAndMaybeSaveToAddressBook(e, chain, ab, BurnMintTokenPool, deployment.Version1_0_0, false) + burnMintTokenPool, err = DeployAndMaybeSaveToAddressBook(e, chain, ab, ccipChangeset.BurnMintTokenPool, deployment.Version1_0_0, false) if err != nil { - return ixns, fmt.Errorf("failed to deploy program: %w", err) + return txns, fmt.Errorf("failed to deploy program: %w", err) } needTokenPoolinLookupTable = true } else { @@ -744,9 +584,9 @@ func deployChainContractsSolana( var lockReleaseTokenPool solana.PublicKey if chainState.LockReleaseTokenPool.IsZero() { - lockReleaseTokenPool, err = DeployAndMaybeSaveToAddressBook(e, chain, ab, LockReleaseTokenPool, deployment.Version1_0_0, false) + lockReleaseTokenPool, err = DeployAndMaybeSaveToAddressBook(e, chain, ab, ccipChangeset.LockReleaseTokenPool, deployment.Version1_0_0, false) if err != nil { - return ixns, fmt.Errorf("failed to deploy program: %w", err) + return txns, fmt.Errorf("failed to deploy program: %w", err) } needTokenPoolinLookupTable = true } else { @@ -758,7 +598,7 @@ func deployChainContractsSolana( if err := AddBillingToken( e, chain, chainState, billingConfig, ); err != nil { - return ixns, err + return txns, err } } @@ -801,11 +641,11 @@ func deployChainContractsSolana( if len(lookupTableKeys) > 0 { addressLookupTable, err := ccipChangeset.FetchOfframpLookupTable(e.GetContext(), chain, offRampAddress) if err != nil { - return ixns, fmt.Errorf("failed to get offramp reference addresses: %w", err) + return txns, fmt.Errorf("failed to get offramp reference addresses: %w", err) } e.Logger.Debugw("Populating lookup table", "lookupTable", addressLookupTable.String(), "keys", lookupTableKeys) if err := solCommonUtil.ExtendLookupTable(e.GetContext(), chain.Client, addressLookupTable, *chain.DeployerKey, lookupTableKeys); err != nil { - return ixns, fmt.Errorf("failed to extend lookup table: %w", err) + return txns, fmt.Errorf("failed to extend lookup table: %w", err) } } @@ -814,12 +654,77 @@ func deployChainContractsSolana( e.Logger.Infow("Setting upgrade authority", "newUpgradeAuthority", config.NewUpgradeAuthority.String()) for _, programID := range []solana.PublicKey{ccipRouterProgram, feeQuoterAddress} { if err := setUpgradeAuthority(&e, &chain, programID, chain.DeployerKey, config.NewUpgradeAuthority, false); err != nil { - return ixns, fmt.Errorf("failed to set upgrade authority: %w", err) + return txns, fmt.Errorf("failed to set upgrade authority: %w", err) } } } - return ixns, nil + return txns, nil +} + +func generateUpgradeTxns( + e deployment.Environment, + chain deployment.SolChain, + ab deployment.AddressBook, + config DeployChainContractsConfig, + newVersion *semver.Version, + programID solana.PublicKey, + contractType deployment.ContractType, +) ([]mcmsTypes.Transaction, error) { + txns := make([]mcmsTypes.Transaction, 0) + bufferProgram, err := DeployAndMaybeSaveToAddressBook(e, chain, ab, contractType, *newVersion, true) + if err != nil { + return txns, fmt.Errorf("failed to deploy program: %w", err) + } + if err := setUpgradeAuthority(&e, &chain, bufferProgram, chain.DeployerKey, config.UpgradeConfig.UpgradeAuthority.ToPointer(), true); err != nil { + return txns, fmt.Errorf("failed to set upgrade authority: %w", err) + } + extendIxn, err := generateExtendIxn( + &e, + chain, + programID, + bufferProgram, + config.UpgradeConfig.SpillAddress, + ) + if err != nil { + return txns, fmt.Errorf("failed to generate extend instruction: %w", err) + } + upgradeIxn, err := generateUpgradeIxn( + &e, + programID, + bufferProgram, + config.UpgradeConfig.SpillAddress, + config.UpgradeConfig.UpgradeAuthority, + ) + if err != nil { + return txns, fmt.Errorf("failed to generate upgrade instruction: %w", err) + } + closeIxn, err := generateCloseBufferIxn( + &e, + bufferProgram, + config.UpgradeConfig.SpillAddress, + config.UpgradeConfig.UpgradeAuthority, + ) + if err != nil { + return txns, fmt.Errorf("failed to generate close buffer instruction: %w", err) + } + upgradeTx, err := BuildMCMSTxn(upgradeIxn, solana.BPFLoaderUpgradeableProgramID.String(), contractType) + if err != nil { + return txns, fmt.Errorf("failed to create upgrade transaction: %w", err) + } + closeTx, err := BuildMCMSTxn(closeIxn, solana.BPFLoaderUpgradeableProgramID.String(), contractType) + if err != nil { + return txns, fmt.Errorf("failed to create close transaction: %w", err) + } + if extendIxn != nil { + extendTx, err := BuildMCMSTxn(extendIxn, solana.BPFLoaderUpgradeableProgramID.String(), contractType) + if err != nil { + return txns, fmt.Errorf("failed to create extend transaction: %w", err) + } + txns = append(txns, *extendTx) + } + txns = append(txns, *upgradeTx, *closeTx) + return txns, nil } // DeployAndMaybeSaveToAddressBook deploys a program to the Solana chain and saves it to the address book @@ -828,30 +733,20 @@ func DeployAndMaybeSaveToAddressBook( e deployment.Environment, chain deployment.SolChain, ab deployment.AddressBook, - programName string, + contractType deployment.ContractType, version semver.Version, isUpgrade bool) (solana.PublicKey, error) { + programName := getTypeToProgramDeployName()[contractType] programID, err := chain.DeployProgram(e.Logger, programName, isUpgrade) if err != nil { return solana.PublicKey{}, fmt.Errorf("failed to deploy program: %w", err) } address := solana.MustPublicKeyFromBase58(programID) - programNameToType := map[string]deployment.ContractType{ - RouterProgramName: ccipChangeset.Router, - OffRampProgramName: ccipChangeset.OffRamp, - FeeQuoterProgramName: ccipChangeset.FeeQuoter, - BurnMintTokenPool: ccipChangeset.BurnMintTokenPool, - LockReleaseTokenPool: ccipChangeset.LockReleaseTokenPool, - } - programType, ok := programNameToType[programName] - if !ok { - return solana.PublicKey{}, fmt.Errorf("unknown program name: %s", programName) - } - e.Logger.Infow("Deployed program", "Program", programType, "addr", programID, "chain", chain.String(), "isUpgrade", isUpgrade) + e.Logger.Infow("Deployed program", "Program", contractType, "addr", programID, "chain", chain.String(), "isUpgrade", isUpgrade) if !isUpgrade { - tv := deployment.NewTypeAndVersion(programType, version) + tv := deployment.NewTypeAndVersion(contractType, version) err = ab.Save(chain.Selector, programID, tv) if err != nil { return solana.PublicKey{}, fmt.Errorf("failed to save address: %w", err) @@ -916,7 +811,7 @@ func generateUpgradeIxn( solana.NewAccountMeta(spillAddress, true, false), // Spill account (writable) solana.NewAccountMeta(solana.SysVarRentPubkey, false, false), // System program solana.NewAccountMeta(solana.SysVarClockPubkey, false, false), // System program - solana.NewAccountMeta(upgradeAuthority, false, false), // Current upgrade authority (signer) + solana.NewAccountMeta(upgradeAuthority, false, true), // Current upgrade authority (signer) } instruction := solana.NewInstruction( @@ -967,7 +862,7 @@ func generateExtendIxn( solana.NewAccountMeta(programDataAccount, true, false), // Program data account (writable) solana.NewAccountMeta(programID, true, false), // Program account (writable) solana.NewAccountMeta(solana.SystemProgramID, false, false), // System program - solana.NewAccountMeta(payer, true, false), // Payer for rent + solana.NewAccountMeta(payer, true, true), // Payer for rent } ixn := solana.NewInstruction( @@ -988,7 +883,7 @@ func generateCloseBufferIxn( keys := solana.AccountMetaSlice{ solana.NewAccountMeta(bufferAddress, true, false), solana.NewAccountMeta(recipient, true, false), - solana.NewAccountMeta(upgradeAuthority, false, false), + solana.NewAccountMeta(upgradeAuthority, false, true), } instruction := solana.NewInstruction( diff --git a/deployment/ccip/changeset/solana/cs_deploy_chain_test.go b/deployment/ccip/changeset/solana/cs_deploy_chain_test.go index 4800df91f50..55803335097 100644 --- a/deployment/ccip/changeset/solana/cs_deploy_chain_test.go +++ b/deployment/ccip/changeset/solana/cs_deploy_chain_test.go @@ -1,7 +1,6 @@ package solana_test import ( - "math/big" "os" "testing" "time" @@ -13,11 +12,12 @@ import ( solBinary "github.com/gagliardetto/binary" "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" ccipChangeset "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" + cs "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" 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" - commonState "github.com/smartcontractkit/chainlink/deployment/common/changeset/state" "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" "github.com/smartcontractkit/chainlink/deployment/environment/memory" "github.com/smartcontractkit/chainlink/v2/core/logger" @@ -60,11 +60,26 @@ func TestDeployChainContractsChangesetSolana(t *testing.T) { feeAggregatorPrivKey, _ := solana.NewRandomPrivateKey() feeAggregatorPubKey := feeAggregatorPrivKey.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 if ci { testhelpers.SavePreloadedSolAddresses(t, e, solChainSelectors[0]) + } else { + e, err = commonchangeset.ApplyChangesetsV2(t, e, []commonchangeset.ConfiguredChangeSet{ + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.BuildSolanaChangeset), + ccipChangesetSolana.BuildSolanaConfig{ + ChainSelector: solChainSelectors[0], + GitCommitSha: "3da552ac9d30b821310718b8b67e6a298335a485", + DestinationDir: e.SolChains[solChainSelectors[0]].ProgramsPath, + CleanDestinationDir: true, + }, + ), + }) + require.NoError(t, err) } - e, err = commonchangeset.Apply(t, e, nil, + e, err = commonchangeset.ApplyChangesetsV2(t, e, []commonchangeset.ConfiguredChangeSet{ commonchangeset.Configure( deployment.CreateLegacyChangeSet(v1_6.DeployHomeChainChangeset), v1_6.DeployHomeChainConfig{ @@ -103,179 +118,136 @@ func TestDeployChainContractsChangesetSolana(t *testing.T) { }, ), commonchangeset.Configure( - deployment.CreateLegacyChangeSet(commonchangeset.DeployMCMSWithTimelockV2), - map[uint64]commontypes.MCMSWithTimelockConfigV2{ - solChainSelectors[0]: { - Canceller: proposalutils.SingleGroupMCMSV2(t), - Proposer: proposalutils.SingleGroupMCMSV2(t), - Bypasser: proposalutils.SingleGroupMCMSV2(t), - TimelockMinDelay: big.NewInt(0), + deployment.CreateLegacyChangeSet(ccipChangesetSolana.DeployChainContractsChangeset), + ccipChangesetSolana.DeployChainContractsConfig{ + HomeChainSelector: homeChainSel, + ContractParamsPerChain: map[uint64]ccipChangesetSolana.ChainContractParams{ + solChainSelectors[0]: { + FeeQuoterParams: ccipChangesetSolana.FeeQuoterParams{ + DefaultMaxFeeJuelsPerMsg: solBinary.Uint128{Lo: 300000000, Hi: 0, Endianness: nil}, + }, + OffRampParams: ccipChangesetSolana.OffRampParams{ + EnableExecutionAfter: int64(globals.PermissionLessExecutionThreshold.Seconds()), + }, + }, }, }, ), - ) - require.NoError(t, err) - addresses, err := e.ExistingAddresses.AddressesForChain(solChainSelectors[0]) - require.NoError(t, err) - mcmState, err := commonState.MaybeLoadMCMSWithTimelockChainStateSolana(e.SolChains[solChainSelectors[0]], addresses) + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.SetFeeAggregator), + ccipChangesetSolana.SetFeeAggregatorConfig{ + ChainSelector: solChainSelectors[0], + FeeAggregator: feeAggregatorPubKey.String(), + }, + ), + }) require.NoError(t, err) - - // Fund signer PDAs for timelock and mcm - // If we don't fund, execute() calls will fail with "no funds" errors. - timelockSignerPDA := commonState.GetTimelockSignerPDA(mcmState.TimelockProgram, mcmState.TimelockSeed) - mcmSignerPDA := commonState.GetMCMSignerPDA(mcmState.McmProgram, mcmState.ProposerMcmSeed) - memory.FundSolanaAccounts(e.GetContext(), t, []solana.PublicKey{timelockSignerPDA, mcmSignerPDA}, - 100, e.SolChains[solChainSelectors[0]].Client) - t.Logf("funded timelock signer PDA: %s", timelockSignerPDA.String()) - t.Logf("funded mcm signer PDA: %s", mcmSignerPDA.String()) + testhelpers.ValidateSolanaState(t, e, solChainSelectors) + timelockSignerPDA, _ := testhelpers.TransferOwnershipSolana(t, &e, solChainSelectors[0], true, true, true, true) upgradeAuthority := timelockSignerPDA + state, err := changeset.LoadOnchainStateSolana(e) + require.NoError(t, err) - // we can't upgrade in place locally so we have to change where we build - buildCs := commonchangeset.Configure( - deployment.CreateLegacyChangeSet(ccipChangesetSolana.BuildSolanaChangeset), - ccipChangesetSolana.BuildSolanaConfig{ - ChainSelector: solChainSelectors[0], - GitCommitSha: "0863d8fed5fbada9f352f33c405e1753cbb7d72c", - DestinationDir: e.SolChains[solChainSelectors[0]].ProgramsPath, - CleanDestinationDir: true, - }, - ) - deployCs := commonchangeset.Configure( - deployment.CreateLegacyChangeSet(ccipChangesetSolana.DeployChainContractsChangeset), - ccipChangesetSolana.DeployChainContractsConfig{ - HomeChainSelector: homeChainSel, - ContractParamsPerChain: map[uint64]ccipChangesetSolana.ChainContractParams{ - solChainSelectors[0]: { - FeeQuoterParams: ccipChangesetSolana.FeeQuoterParams{ - DefaultMaxFeeJuelsPerMsg: solBinary.Uint128{Lo: 300000000, Hi: 0, Endianness: nil}, - }, - OffRampParams: ccipChangesetSolana.OffRampParams{ - EnableExecutionAfter: int64(globals.PermissionLessExecutionThreshold.Seconds()), + e, err = commonchangeset.ApplyChangesetsV2(t, e, []commonchangeset.ConfiguredChangeSet{ + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.DeployChainContractsChangeset), + ccipChangesetSolana.DeployChainContractsConfig{ + HomeChainSelector: homeChainSel, + ContractParamsPerChain: map[uint64]ccipChangesetSolana.ChainContractParams{ + solChainSelectors[0]: { + FeeQuoterParams: ccipChangesetSolana.FeeQuoterParams{ + DefaultMaxFeeJuelsPerMsg: solBinary.Uint128{Lo: 300000000, Hi: 0, Endianness: nil}, + }, + OffRampParams: ccipChangesetSolana.OffRampParams{ + EnableExecutionAfter: int64(globals.PermissionLessExecutionThreshold.Seconds()), + }, }, }, + NewUpgradeAuthority: &upgradeAuthority, }, - }, - ) - // set the fee aggregator address - feeAggregatorCs := commonchangeset.Configure( - deployment.CreateLegacyChangeSet(ccipChangesetSolana.SetFeeAggregator), - ccipChangesetSolana.SetFeeAggregatorConfig{ - ChainSelector: solChainSelectors[0], - FeeAggregator: feeAggregatorPubKey.String(), - }, - ) - transferOwnershipCs := commonchangeset.Configure( - deployment.CreateLegacyChangeSet(ccipChangesetSolana.TransferCCIPToMCMSWithTimelockSolana), - ccipChangesetSolana.TransferCCIPToMCMSWithTimelockSolanaConfig{ - MinDelay: 1 * time.Second, - ContractsByChain: map[uint64]ccipChangesetSolana.CCIPContractsToTransfer{ - solChainSelectors[0]: { - Router: true, - FeeQuoter: true, - OffRamp: true, + ), + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.BuildSolanaChangeset), + ccipChangesetSolana.BuildSolanaConfig{ + ChainSelector: solChainSelectors[0], + GitCommitSha: "0863d8fed5fbada9f352f33c405e1753cbb7d72c", + DestinationDir: e.SolChains[solChainSelectors[0]].ProgramsPath, + CleanDestinationDir: true, + CleanGitDir: true, + UpgradeKeys: map[deployment.ContractType]string{ + cs.Router: state.SolChains[solChainSelectors[0]].Router.String(), + cs.FeeQuoter: state.SolChains[solChainSelectors[0]].FeeQuoter.String(), }, }, - }, - ) - // make sure idempotency works and setting the upgrade authority - upgradeAuthorityCs := commonchangeset.Configure( - deployment.CreateLegacyChangeSet(ccipChangesetSolana.DeployChainContractsChangeset), - ccipChangesetSolana.DeployChainContractsConfig{ - HomeChainSelector: homeChainSel, - ContractParamsPerChain: map[uint64]ccipChangesetSolana.ChainContractParams{ - solChainSelectors[0]: { - FeeQuoterParams: ccipChangesetSolana.FeeQuoterParams{ - DefaultMaxFeeJuelsPerMsg: solBinary.Uint128{Lo: 300000000, Hi: 0, Endianness: nil}, - }, - OffRampParams: ccipChangesetSolana.OffRampParams{ - EnableExecutionAfter: int64(globals.PermissionLessExecutionThreshold.Seconds()), + ), + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.DeployChainContractsChangeset), + ccipChangesetSolana.DeployChainContractsConfig{ + HomeChainSelector: homeChainSel, + ContractParamsPerChain: map[uint64]ccipChangesetSolana.ChainContractParams{ + solChainSelectors[0]: { + FeeQuoterParams: ccipChangesetSolana.FeeQuoterParams{ + DefaultMaxFeeJuelsPerMsg: solBinary.Uint128{Lo: 300000000, Hi: 0, Endianness: nil}, + }, + OffRampParams: ccipChangesetSolana.OffRampParams{ + EnableExecutionAfter: int64(globals.PermissionLessExecutionThreshold.Seconds()), + }, }, }, - }, - NewUpgradeAuthority: &upgradeAuthority, - }, - ) - upgradeCs := commonchangeset.Configure( - deployment.CreateLegacyChangeSet(ccipChangesetSolana.DeployChainContractsChangeset), - ccipChangesetSolana.DeployChainContractsConfig{ - HomeChainSelector: homeChainSel, - ContractParamsPerChain: map[uint64]ccipChangesetSolana.ChainContractParams{ - solChainSelectors[0]: { - FeeQuoterParams: ccipChangesetSolana.FeeQuoterParams{ - DefaultMaxFeeJuelsPerMsg: solBinary.Uint128{Lo: 300000000, Hi: 0, Endianness: nil}, - }, - OffRampParams: ccipChangesetSolana.OffRampParams{ - EnableExecutionAfter: int64(globals.PermissionLessExecutionThreshold.Seconds()), + UpgradeConfig: ccipChangesetSolana.UpgradeConfig{ + NewFeeQuoterVersion: &deployment.Version1_1_0, + NewRouterVersion: &deployment.Version1_1_0, + UpgradeAuthority: upgradeAuthority, + SpillAddress: upgradeAuthority, + MCMS: &ccipChangeset.MCMSConfig{ + MinDelay: 1 * time.Second, }, }, }, - UpgradeConfig: ccipChangesetSolana.UpgradeConfig{ - NewFeeQuoterVersion: &deployment.Version1_1_0, - NewRouterVersion: &deployment.Version1_1_0, - UpgradeAuthority: upgradeAuthority, - SpillAddress: upgradeAuthority, - MCMS: &ccipChangeset.MCMSConfig{ - MinDelay: 1 * time.Second, - }, - }, - }, - ) - // because we cannot upgrade in place locally, we can't redeploy offramp - offRampCs := commonchangeset.Configure( - deployment.CreateLegacyChangeSet(ccipChangesetSolana.DeployChainContractsChangeset), - ccipChangesetSolana.DeployChainContractsConfig{ - HomeChainSelector: homeChainSel, - ContractParamsPerChain: map[uint64]ccipChangesetSolana.ChainContractParams{ - solChainSelectors[0]: { - FeeQuoterParams: ccipChangesetSolana.FeeQuoterParams{ - DefaultMaxFeeJuelsPerMsg: solBinary.Uint128{Lo: 300000000, Hi: 0, Endianness: nil}, + ), + }) + require.NoError(t, err) + testhelpers.ValidateSolanaState(t, e, solChainSelectors) + state, err = changeset.LoadOnchainStateSolana(e) + require.NoError(t, err) + oldOffRampAddress := state.SolChains[solChainSelectors[0]].OffRamp + // add a second offramp address + e, err = commonchangeset.ApplyChangesetsV2(t, e, []commonchangeset.ConfiguredChangeSet{ + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.DeployChainContractsChangeset), + ccipChangesetSolana.DeployChainContractsConfig{ + HomeChainSelector: homeChainSel, + ContractParamsPerChain: map[uint64]ccipChangesetSolana.ChainContractParams{ + solChainSelectors[0]: { + FeeQuoterParams: ccipChangesetSolana.FeeQuoterParams{ + DefaultMaxFeeJuelsPerMsg: solBinary.Uint128{Lo: 300000000, Hi: 0, Endianness: nil}, + }, + OffRampParams: ccipChangesetSolana.OffRampParams{ + EnableExecutionAfter: int64(globals.PermissionLessExecutionThreshold.Seconds()), + }, }, - OffRampParams: ccipChangesetSolana.OffRampParams{ - EnableExecutionAfter: int64(globals.PermissionLessExecutionThreshold.Seconds()), + }, + UpgradeConfig: ccipChangesetSolana.UpgradeConfig{ + NewOffRampVersion: &deployment.Version1_1_0, + UpgradeAuthority: upgradeAuthority, + SpillAddress: upgradeAuthority, + MCMS: &ccipChangeset.MCMSConfig{ + MinDelay: 1 * time.Second, }, }, }, - UpgradeConfig: ccipChangesetSolana.UpgradeConfig{ - NewOffRampVersion: &deployment.Version1_1_0, - }, - }, - ) - if ci { - e, err = commonchangeset.ApplyChangesetsV2(t, e, []commonchangeset.ConfiguredChangeSet{ - deployCs, - feeAggregatorCs, - upgradeAuthorityCs, - transferOwnershipCs, - }) - require.NoError(t, err) - state, err := ccipChangeset.LoadOnchainStateSolana(e) - require.NoError(t, err) - oldOffRampAddress := state.SolChains[solChainSelectors[0]].OffRamp - // add a second offramp address - e, err = commonchangeset.ApplyChangesetsV2(t, e, []commonchangeset.ConfiguredChangeSet{ - buildCs, - upgradeCs, - offRampCs, - }) - require.NoError(t, err) - // verify the offramp address is different - state, err = ccipChangeset.LoadOnchainStateSolana(e) - require.NoError(t, err) - newOffRampAddress := state.SolChains[solChainSelectors[0]].OffRamp - require.NotEqual(t, oldOffRampAddress, newOffRampAddress) - } else { - e, err = commonchangeset.ApplyChangesetsV2(t, e, []commonchangeset.ConfiguredChangeSet{ - buildCs, - deployCs, - feeAggregatorCs, - upgradeAuthorityCs, - upgradeCs, - }) - } + ), + }) + require.NoError(t, err) + // verify the offramp address is different + state, err = changeset.LoadOnchainStateSolana(e) require.NoError(t, err) + newOffRampAddress := state.SolChains[solChainSelectors[0]].OffRamp + require.NotEqual(t, oldOffRampAddress, newOffRampAddress) + // Verify router and fee quoter upgraded in place // and offramp had 2nd address added - addresses, err = e.ExistingAddresses.AddressesForChain(solChainSelectors[0]) + addresses, err := e.ExistingAddresses.AddressesForChain(solChainSelectors[0]) require.NoError(t, err) numRouters := 0 numFeeQuoters := 0 @@ -293,11 +265,7 @@ func TestDeployChainContractsChangesetSolana(t *testing.T) { } require.Equal(t, 1, numRouters) require.Equal(t, 1, numFeeQuoters) - if ci { - require.Equal(t, 2, numOffRamps) - } else { - require.Equal(t, 1, numOffRamps) - } + require.Equal(t, 2, numOffRamps) require.NoError(t, err) // solana verification testhelpers.ValidateSolanaState(t, e, solChainSelectors) diff --git a/deployment/ccip/changeset/solana/ownership_transfer_helpers.go b/deployment/ccip/changeset/solana/ownership_transfer_helpers.go index 094c3d3bc75..406846f5ee3 100644 --- a/deployment/ccip/changeset/solana/ownership_transfer_helpers.go +++ b/deployment/ccip/changeset/solana/ownership_transfer_helpers.go @@ -2,10 +2,8 @@ package solana import ( "fmt" - "math/big" "github.com/gagliardetto/solana-go" - mcmsSolana "github.com/smartcontractkit/mcms/sdk/solana" mcmsTypes "github.com/smartcontractkit/mcms/types" "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_offramp" @@ -60,25 +58,14 @@ func transferAndWrapAcceptOwnership( if err != nil { return mcmsTypes.Transaction{}, fmt.Errorf("%s: failed to create accept ownership instruction: %w", label, err) } - acceptData, err := ixAccept.Data() - if err != nil { - return mcmsTypes.Transaction{}, fmt.Errorf("%s: failed to extract accept data: %w", label, err) - } // 4. Wrap in MCMS transaction - mcmsTx, err := mcmsSolana.NewTransaction( - programID.String(), - acceptData, - big.NewInt(0), // e.g. value - ixAccept.Accounts(), // pass along needed accounts - string(label), // some string identifying the target - []string{}, // any relevant metadata - ) + mcmsTx, err := BuildMCMSTxn(ixAccept, programID.String(), label) if err != nil { return mcmsTypes.Transaction{}, fmt.Errorf("%s: failed to create MCMS transaction: %w", label, err) } - return mcmsTx, nil + return *mcmsTx, nil } // transferOwnershipRouter transfers ownership of the router to the timelock. diff --git a/deployment/ccip/changeset/solana/transfer_ccip_to_mcms_with_timelock_test.go b/deployment/ccip/changeset/solana/transfer_ccip_to_mcms_with_timelock_test.go index 94f34753ba6..986a06d90f8 100644 --- a/deployment/ccip/changeset/solana/transfer_ccip_to_mcms_with_timelock_test.go +++ b/deployment/ccip/changeset/solana/transfer_ccip_to_mcms_with_timelock_test.go @@ -30,7 +30,6 @@ import ( commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/globals" - commonState "github.com/smartcontractkit/chainlink/deployment/common/changeset/state" "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" "github.com/smartcontractkit/chainlink/deployment/environment/memory" @@ -310,37 +309,7 @@ func TestTransferCCIPToMCMSWithTimelockSolana(t *testing.T) { e, state := prepareEnvironmentForOwnershipTransfer(t) solChain1 := e.AllChainSelectorsSolana()[0] solChain := e.SolChains[solChain1] - // tokenAddress := state.SolChains[solChain1].SPL2022Tokens[0] - addresses, err := e.ExistingAddresses.AddressesForChain(solChain1) - require.NoError(t, err) - mcmState, err := commonState.MaybeLoadMCMSWithTimelockChainStateSolana(e.SolChains[solChain1], addresses) - require.NoError(t, err) - - // Fund signer PDAs for timelock and mcm - // If we don't fund, execute() calls will fail with "no funds" errors. - timelockSignerPDA := commonState.GetTimelockSignerPDA(mcmState.TimelockProgram, mcmState.TimelockSeed) - mcmSignerPDA := commonState.GetMCMSignerPDA(mcmState.McmProgram, mcmState.ProposerMcmSeed) - memory.FundSolanaAccounts(e.GetContext(), t, []solana.PublicKey{timelockSignerPDA, mcmSignerPDA}, - 100, solChain.Client) - t.Logf("funded timelock signer PDA: %s", timelockSignerPDA.String()) - t.Logf("funded mcm signer PDA: %s", mcmSignerPDA.String()) - // Apply transfer ownership changeset - e, err = commonchangeset.ApplyChangesetsV2(t, e, []commonchangeset.ConfiguredChangeSet{ - commonchangeset.Configure( - deployment.CreateLegacyChangeSet(solanachangesets.TransferCCIPToMCMSWithTimelockSolana), - solanachangesets.TransferCCIPToMCMSWithTimelockSolanaConfig{ - MinDelay: 1 * time.Second, - ContractsByChain: map[uint64]solanachangesets.CCIPContractsToTransfer{ - solChain1: { - Router: true, - FeeQuoter: true, - OffRamp: true, - }, - }, - }, - ), - }) - require.NoError(t, err) + timelockSignerPDA, _ := testhelpers.TransferOwnershipSolana(t, &e, solChain1, false, true, true, true) // 5. Now verify on-chain that each contract’s “config account” authority is the Timelock PDA. // Typically, each contract has its own config account: RouterConfigPDA, FeeQuoterConfigPDA, @@ -352,7 +321,8 @@ func TestTransferCCIPToMCMSWithTimelockSolana(t *testing.T) { routerConfigPDA := state.SolChains[solChain1].RouterConfigPDA t.Logf("Checking Router Config PDA ownership data configPDA: %s", routerConfigPDA.String()) programData := ccip_router.Config{} - err = solChain.GetAccountDataBorshInto(ctx, routerConfigPDA, &programData) + err := solChain.GetAccountDataBorshInto(ctx, routerConfigPDA, &programData) + require.NoError(t, err) return timelockSignerPDA.String() == programData.Owner.String() }, 30*time.Second, 5*time.Second, "Router config PDA owner was not changed to timelock signer PDA") @@ -361,7 +331,7 @@ func TestTransferCCIPToMCMSWithTimelockSolana(t *testing.T) { feeQuoterConfigPDA := state.SolChains[solChain1].FeeQuoterConfigPDA t.Logf("Checking Fee Quoter PDA ownership data configPDA: %s", feeQuoterConfigPDA.String()) programData := fee_quoter.Config{} - err = solChain.GetAccountDataBorshInto(ctx, feeQuoterConfigPDA, &programData) + err := solChain.GetAccountDataBorshInto(ctx, feeQuoterConfigPDA, &programData) require.NoError(t, err) return timelockSignerPDA.String() == programData.Owner.String() }, 30*time.Second, 5*time.Second, "Fee Quoter config PDA owner was not changed to timelock signer PDA") @@ -371,7 +341,7 @@ func TestTransferCCIPToMCMSWithTimelockSolana(t *testing.T) { offRampConfigPDA := state.SolChains[solChain1].OffRampConfigPDA programData := ccip_offramp.Config{} t.Logf("Checking Off Ramp PDA ownership data configPDA: %s", offRampConfigPDA.String()) - err = solChain.GetAccountDataBorshInto(ctx, offRampConfigPDA, &programData) + err := solChain.GetAccountDataBorshInto(ctx, offRampConfigPDA, &programData) require.NoError(t, err) return timelockSignerPDA.String() == programData.Owner.String() }, 30*time.Second, 5*time.Second, "OffRamp config PDA owner was not changed to timelock signer PDA") diff --git a/deployment/ccip/changeset/solana/utils.go b/deployment/ccip/changeset/solana/utils.go index ac571187a2d..47107b7d11c 100644 --- a/deployment/ccip/changeset/solana/utils.go +++ b/deployment/ccip/changeset/solana/utils.go @@ -2,8 +2,11 @@ package solana import ( "fmt" + "math/big" + "github.com/gagliardetto/solana-go" mcmsSolana "github.com/smartcontractkit/mcms/sdk/solana" + mcmsTypes "github.com/smartcontractkit/mcms/types" "github.com/smartcontractkit/chainlink/deployment" cs "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" @@ -33,3 +36,27 @@ func ValidateMCMSConfig(e deployment.Environment, chainSelector uint64, mcms *cs } return nil } + +func BuildMCMSTxn(ixn solana.Instruction, programID string, contractType deployment.ContractType) (*mcmsTypes.Transaction, error) { + data, err := ixn.Data() + if err != nil { + return nil, fmt.Errorf("failed to extract data: %w", err) + } + for _, account := range ixn.Accounts() { + if account.IsSigner { + account.IsSigner = false + } + } + tx, err := mcmsSolana.NewTransaction( + programID, + data, + big.NewInt(0), // e.g. value + ixn.Accounts(), // pass along needed accounts + string(contractType), // some string identifying the target + []string{}, // any relevant metadata + ) + if err != nil { + return nil, fmt.Errorf("failed to create transaction: %w", err) + } + return &tx, nil +} diff --git a/deployment/ccip/changeset/solana_state.go b/deployment/ccip/changeset/solana_state.go index e48f5165677..a5ed3a93671 100644 --- a/deployment/ccip/changeset/solana_state.go +++ b/deployment/ccip/changeset/solana_state.go @@ -12,9 +12,14 @@ import ( solState "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/state" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_offramp" solOffRamp "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_offramp" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/fee_quoter" "github.com/smartcontractkit/chainlink/deployment" + commoncs "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/common/changeset/state" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" ) @@ -237,3 +242,58 @@ func FindSolanaAddress(tv deployment.TypeAndVersion, addresses map[string]deploy } return solana.PublicKey{} } + +func ValidateOwnershipSolana( + e *deployment.Environment, + chain deployment.SolChain, + mcms bool, + deployerKey solana.PublicKey, + programID solana.PublicKey, + contractType deployment.ContractType, +) error { + addresses, err := e.ExistingAddresses.AddressesForChain(chain.Selector) + if err != nil { + return fmt.Errorf("failed to get existing addresses: %w", err) + } + mcmState, err := state.MaybeLoadMCMSWithTimelockChainStateSolana(chain, addresses) + if err != nil { + return fmt.Errorf("failed to load MCMS with timelock chain state: %w", err) + } + timelockSignerPDA := state.GetTimelockSignerPDA(mcmState.TimelockProgram, mcmState.TimelockSeed) + config, _, err := solState.FindConfigPDA(programID) + if err != nil { + return fmt.Errorf("failed to find config PDA: %w", err) + } + switch contractType { + case Router: + programData := ccip_router.Config{} + err = chain.GetAccountDataBorshInto(e.GetContext(), config, &programData) + if err != nil { + return fmt.Errorf("failed to get account data: %w", err) + } + if err := commoncs.ValidateOwnershipSolanaCommon(mcms, deployerKey, timelockSignerPDA, programData.Owner); err != nil { + return fmt.Errorf("failed to validate ownership for router: %w", err) + } + case OffRamp: + programData := ccip_offramp.Config{} + err = chain.GetAccountDataBorshInto(e.GetContext(), config, &programData) + if err != nil { + return fmt.Errorf("failed to get account data: %w", err) + } + if err := commoncs.ValidateOwnershipSolanaCommon(mcms, deployerKey, timelockSignerPDA, programData.Owner); err != nil { + return fmt.Errorf("failed to validate ownership for offramp: %w", err) + } + case FeeQuoter: + programData := fee_quoter.Config{} + err = chain.GetAccountDataBorshInto(e.GetContext(), config, &programData) + if err != nil { + return fmt.Errorf("failed to get account data: %w", err) + } + if err := commoncs.ValidateOwnershipSolanaCommon(mcms, deployerKey, timelockSignerPDA, programData.Owner); err != nil { + return fmt.Errorf("failed to validate ownership for feequoter: %w", err) + } + default: + return fmt.Errorf("unsupported contract type: %s", contractType) + } + return nil +} diff --git a/deployment/ccip/changeset/testhelpers/test_helpers.go b/deployment/ccip/changeset/testhelpers/test_helpers.go index 33683f0c217..45e3a2a8b88 100644 --- a/deployment/ccip/changeset/testhelpers/test_helpers.go +++ b/deployment/ccip/changeset/testhelpers/test_helpers.go @@ -23,6 +23,8 @@ import ( ccipChangeSetSolana "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/solana" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/v1_6" commoncs "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/common/changeset/state" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/v1_6_0/fee_quoter" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" @@ -1630,6 +1632,63 @@ func FindReceiverTargetAccount(receiverID solana.PublicKey) solana.PublicKey { return receiverTargetAccount } +func TransferOwnershipSolana( + t *testing.T, + e *deployment.Environment, + solChain uint64, + needTimelockDeployed bool, + transferRouter, transferFeeQuoter, transferOffRamp bool) (solana.PublicKey, solana.PublicKey) { + var err error + if needTimelockDeployed { + *e, err = commoncs.ApplyChangesetsV2(t, *e, []commoncs.ConfiguredChangeSet{ + commoncs.Configure( + deployment.CreateLegacyChangeSet(commoncs.DeployMCMSWithTimelockV2), + map[uint64]commontypes.MCMSWithTimelockConfigV2{ + solChain: { + Canceller: proposalutils.SingleGroupMCMSV2(t), + Proposer: proposalutils.SingleGroupMCMSV2(t), + Bypasser: proposalutils.SingleGroupMCMSV2(t), + TimelockMinDelay: big.NewInt(0), + }, + }, + ), + }) + require.NoError(t, err) + } + + addresses, err := e.ExistingAddresses.AddressesForChain(solChain) + require.NoError(t, err) + mcmState, err := state.MaybeLoadMCMSWithTimelockChainStateSolana(e.SolChains[solChain], addresses) + require.NoError(t, err) + + // Fund signer PDAs for timelock and mcm + // If we don't fund, execute() calls will fail with "no funds" errors. + timelockSignerPDA := state.GetTimelockSignerPDA(mcmState.TimelockProgram, mcmState.TimelockSeed) + mcmSignerPDA := state.GetMCMSignerPDA(mcmState.McmProgram, mcmState.ProposerMcmSeed) + memory.FundSolanaAccounts(e.GetContext(), t, []solana.PublicKey{timelockSignerPDA, mcmSignerPDA}, + 100, e.SolChains[solChain].Client) + t.Logf("funded timelock signer PDA: %s", timelockSignerPDA.String()) + t.Logf("funded mcm signer PDA: %s", mcmSignerPDA.String()) + // Apply transfer ownership changeset + *e, err = commoncs.ApplyChangesetsV2(t, *e, []commoncs.ConfiguredChangeSet{ + commoncs.Configure( + deployment.CreateLegacyChangeSet(ccipChangeSetSolana.TransferCCIPToMCMSWithTimelockSolana), + ccipChangeSetSolana.TransferCCIPToMCMSWithTimelockSolanaConfig{ + MinDelay: 1 * time.Second, + ContractsByChain: map[uint64]ccipChangeSetSolana.CCIPContractsToTransfer{ + solChain: { + Router: transferRouter, + FeeQuoter: transferFeeQuoter, + OffRamp: transferOffRamp, + }, + }, + }, + ), + }) + require.NoError(t, err) + return timelockSignerPDA, mcmSignerPDA +} + func GenTestTransferOwnershipConfig( e DeployedEnv, chains []uint64, diff --git a/deployment/ccip/changeset/v1_6/cs_chain_contracts.go b/deployment/ccip/changeset/v1_6/cs_chain_contracts.go index fdb66bf6087..0d0f6c5f959 100644 --- a/deployment/ccip/changeset/v1_6/cs_chain_contracts.go +++ b/deployment/ccip/changeset/v1_6/cs_chain_contracts.go @@ -20,7 +20,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" - commonState "github.com/smartcontractkit/chainlink/deployment/common/changeset/state" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/v1_6_0/fee_quoter" "github.com/smartcontractkit/chainlink/deployment" @@ -1422,24 +1421,12 @@ func (c SetOCR3OffRampConfig) validateRemoteChain(e *deployment.Environment, sta } switch family { case chain_selectors.FamilySolana: - chain, ok := e.SolChains[chainSelector] - if !ok { - return fmt.Errorf("chain %d not found in environment", chainSelector) - } chainState, ok := state.SolChains[chainSelector] if !ok { return fmt.Errorf("remote chain %d not found in onchain state", chainSelector) } - addresses, err := e.ExistingAddresses.AddressesForChain(chainSelector) - if err != nil { - return err - } - mcmState, err := commonState.MaybeLoadMCMSWithTimelockChainStateSolana(chain, addresses) - if err != nil { - return fmt.Errorf("error loading MCMS state for chain %d: %w", chainSelector, err) - } - if err := commoncs.ValidateOwnershipSolana(e.GetContext(), c.MCMS != nil, e.SolChains[chainSelector].DeployerKey.PublicKey(), mcmState.TimelockProgram, mcmState.TimelockSeed, chainState.Router); err != nil { - return err + if chainState.OffRamp.IsZero() { + return fmt.Errorf("missing OffRamp for chain %d", chainSelector) } case chain_selectors.FamilyEVM: chainState, ok := state.Chains[chainSelector] diff --git a/deployment/common/changeset/deploy_mcms_with_timelock.go b/deployment/common/changeset/deploy_mcms_with_timelock.go index cfe77a23c08..4ec3a140b82 100644 --- a/deployment/common/changeset/deploy_mcms_with_timelock.go +++ b/deployment/common/changeset/deploy_mcms_with_timelock.go @@ -13,7 +13,6 @@ import ( "github.com/smartcontractkit/chainlink/deployment/common/changeset/internal" evminternal "github.com/smartcontractkit/chainlink/deployment/common/changeset/internal/evm" solanainternal "github.com/smartcontractkit/chainlink/deployment/common/changeset/internal/solana" - "github.com/smartcontractkit/chainlink/deployment/common/changeset/state" "github.com/smartcontractkit/chainlink/deployment/common/types" ) @@ -82,11 +81,15 @@ func ValidateOwnership(ctx context.Context, mcms bool, deployerKey, timelock com return nil } -// TODO: SOLANA_CCIP -func ValidateOwnershipSolana( - ctx context.Context, mcms bool, deployerKey, timelock solana.PublicKey, timelockSeed state.PDASeed, - ccipRouter solana.PublicKey, -) error { - // TODO: implement +func ValidateOwnershipSolanaCommon(mcms bool, deployerKey solana.PublicKey, timelockSignerPDA solana.PublicKey, programOwner solana.PublicKey) error { + if !mcms { + if deployerKey.String() != programOwner.String() { + return fmt.Errorf("deployer key %s does not match owner %s", deployerKey.String(), programOwner.String()) + } + } else { + if timelockSignerPDA.String() != programOwner.String() { + return fmt.Errorf("timelock signer PDA %s does not match owner %s", timelockSignerPDA.String(), programOwner.String()) + } + } return nil }