diff --git a/deployment/ccip/changeset/solana/cs_chain_contracts_test.go b/deployment/ccip/changeset/solana/cs_chain_contracts_test.go index 5d9ecefaba1..2c81f0e5232 100644 --- a/deployment/ccip/changeset/solana/cs_chain_contracts_test.go +++ b/deployment/ccip/changeset/solana/cs_chain_contracts_test.go @@ -65,7 +65,7 @@ func doTestAddRemoteChain(t *testing.T, e deployment.Environment, evmChain uint6 var mcmsConfig *ccipChangesetSolana.MCMSConfigSolana var err error if mcms { - _, _ = testhelpers.TransferOwnershipSolana(t, &e, solChain, true, true, true, true) + _, _ = testhelpers.TransferOwnershipSolana(t, &e, solChain, true, true, true, true, nil, nil) mcmsConfig = &ccipChangesetSolana.MCMSConfigSolana{ MCMS: &ccipChangeset.MCMSConfig{ MinDelay: 1 * time.Second, @@ -259,7 +259,7 @@ func TestBilling(t *testing.T) { bigNum.FillBytes(value[:]) var mcmsConfig *ccipChangesetSolana.MCMSConfigSolana if test.Mcms { - _, _ = testhelpers.TransferOwnershipSolana(t, &e, solChain, true, true, true, true) + _, _ = testhelpers.TransferOwnershipSolana(t, &e, solChain, true, true, true, true, nil, nil) mcmsConfig = &ccipChangesetSolana.MCMSConfigSolana{ MCMS: &ccipChangeset.MCMSConfig{ MinDelay: 1 * time.Second, diff --git a/deployment/ccip/changeset/solana/cs_deploy_chain_test.go b/deployment/ccip/changeset/solana/cs_deploy_chain_test.go index 9d377838abe..27c1cae6b29 100644 --- a/deployment/ccip/changeset/solana/cs_deploy_chain_test.go +++ b/deployment/ccip/changeset/solana/cs_deploy_chain_test.go @@ -145,7 +145,7 @@ func TestDeployChainContractsChangesetSolana(t *testing.T) { testhelpers.ValidateSolanaState(t, e, solChainSelectors) // Expensive to run in CI if !ci { - timelockSignerPDA, _ := testhelpers.TransferOwnershipSolana(t, &e, solChainSelectors[0], true, true, true, true) + timelockSignerPDA, _ := testhelpers.TransferOwnershipSolana(t, &e, solChainSelectors[0], true, true, true, true, nil, nil) upgradeAuthority := timelockSignerPDA state, err := changeset.LoadOnchainStateSolana(e) require.NoError(t, err) diff --git a/deployment/ccip/changeset/solana/cs_token_pool_test.go b/deployment/ccip/changeset/solana/cs_token_pool_test.go index 9026bbdd0c6..1af5f4f8d5d 100644 --- a/deployment/ccip/changeset/solana/cs_token_pool_test.go +++ b/deployment/ccip/changeset/solana/cs_token_pool_test.go @@ -159,7 +159,7 @@ func doTestTokenPool(t *testing.T, mcms bool) { var mcmsConfig *ccipChangesetSolana.MCMSConfigSolana if testCase.mcms && !mcmsConfigured { - _, _ = testhelpers.TransferOwnershipSolana(t, &e, solChain, true, true, true, true) + _, _ = testhelpers.TransferOwnershipSolana(t, &e, solChain, true, true, true, true, nil, nil) mcmsConfig = &ccipChangesetSolana.MCMSConfigSolana{ MCMS: &ccipChangeset.MCMSConfig{ MinDelay: 1 * time.Second, diff --git a/deployment/ccip/changeset/solana/ownership_transfer_helpers.go b/deployment/ccip/changeset/solana/ownership_transfer_helpers.go index 406846f5ee3..4fe632db687 100644 --- a/deployment/ccip/changeset/solana/ownership_transfer_helpers.go +++ b/deployment/ccip/changeset/solana/ownership_transfer_helpers.go @@ -8,6 +8,8 @@ import ( "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_offramp" "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" + burnmint "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/example_burnmint_token_pool" + lockrelease "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/example_lockrelease_token_pool" "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/fee_quoter" "github.com/smartcontractkit/chainlink/deployment" @@ -247,3 +249,121 @@ func transferOwnershipOffRamp( result = append(result, tx) return result, nil } + +// transferOwnershipLockMintTokenPools transfers ownership of the lock mint token pools. +func transferOwnershipBurnMintTokenPools( + ccipState state2.CCIPOnChainState, + tokenPools []solana.PublicKey, + chainSelector uint64, + solChain deployment.SolChain, + timelockProgramID solana.PublicKey, + timelockInstanceSeed state.PDASeed, +) ([]mcmsTypes.Transaction, error) { + var result []mcmsTypes.Transaction + + timelockSignerPDA := state.GetTimelockSignerPDA(timelockProgramID, timelockInstanceSeed) + state := ccipState.SolChains[chainSelector] + + // Build specialized closures + buildTransfer := func(proposedOwner, config, authority solana.PublicKey) (solana.Instruction, error) { + burnmint.SetProgramID(state.BurnMintTokenPool) + return burnmint.NewTransferOwnershipInstruction( + proposedOwner, config, authority, + ).ValidateAndBuild() + } + buildAccept := func(config, newOwnerAuthority solana.PublicKey) (solana.Instruction, error) { + burnmint.SetProgramID(state.BurnMintTokenPool) + // If the router has its own accept function, use that + ix, err := burnmint.NewAcceptOwnershipInstruction( + config, newOwnerAuthority, + ).ValidateAndBuild() + if err != nil { + return nil, err + } + for _, acc := range ix.Accounts() { + if acc.PublicKey == newOwnerAuthority { + acc.IsSigner = false + } + } + return ix, nil + } + + for _, tokenPoolConfigPDA := range tokenPools { + tx, err := transferAndWrapAcceptOwnership( + buildTransfer, + buildAccept, + state.BurnMintTokenPool, + timelockSignerPDA, // timelock PDA + tokenPoolConfigPDA, // config PDA + solChain.DeployerKey.PublicKey(), + solChain, + state2.BurnMintTokenPool, + ) + + if err != nil { + return nil, fmt.Errorf("failed to transfer burn-mint token pool ownership: %w", err) + } + + result = append(result, tx) + } + return result, nil +} + +// transferOwnershipLockReleaseTokenPools transfers ownership of the lock mint token pools. +func transferOwnershipLockReleaseTokenPools( + ccipState state2.CCIPOnChainState, + tokenPools []solana.PublicKey, + chainSelector uint64, + solChain deployment.SolChain, + timelockProgramID solana.PublicKey, + timelockInstanceSeed state.PDASeed, +) ([]mcmsTypes.Transaction, error) { + var result []mcmsTypes.Transaction + + timelockSignerPDA := state.GetTimelockSignerPDA(timelockProgramID, timelockInstanceSeed) + state := ccipState.SolChains[chainSelector] + + // Build specialized closures + buildTransfer := func(proposedOwner, config, authority solana.PublicKey) (solana.Instruction, error) { + lockrelease.SetProgramID(state.LockReleaseTokenPool) + return lockrelease.NewTransferOwnershipInstruction( + proposedOwner, config, authority, + ).ValidateAndBuild() + } + buildAccept := func(config, newOwnerAuthority solana.PublicKey) (solana.Instruction, error) { + lockrelease.SetProgramID(state.LockReleaseTokenPool) + // If the router has its own accept function, use that + ix, err := lockrelease.NewAcceptOwnershipInstruction( + config, newOwnerAuthority, + ).ValidateAndBuild() + if err != nil { + return nil, err + } + for _, acc := range ix.Accounts() { + if acc.PublicKey == newOwnerAuthority { + acc.IsSigner = false + } + } + return ix, nil + } + + for _, tokenPoolConfigPDA := range tokenPools { + tx, err := transferAndWrapAcceptOwnership( + buildTransfer, + buildAccept, + state.LockReleaseTokenPool, + timelockSignerPDA, // timelock PDA + tokenPoolConfigPDA, // config PDA + solChain.DeployerKey.PublicKey(), + solChain, + state2.LockReleaseTokenPool, + ) + + if err != nil { + return nil, fmt.Errorf("failed to transfer lock-release token pool ownership: %w", err) + } + + result = append(result, tx) + } + return result, nil +} diff --git a/deployment/ccip/changeset/solana/transfer_ccip_to_mcms_with_timelock.go b/deployment/ccip/changeset/solana/transfer_ccip_to_mcms_with_timelock.go index 07da0358659..31907b4671a 100644 --- a/deployment/ccip/changeset/solana/transfer_ccip_to_mcms_with_timelock.go +++ b/deployment/ccip/changeset/solana/transfer_ccip_to_mcms_with_timelock.go @@ -23,9 +23,11 @@ var _ deployment.ChangeSet[TransferCCIPToMCMSWithTimelockSolanaConfig] = Transfe // CCIPContractsToTransfer is a struct that represents the contracts we want to transfer. Each contract set to true will be transferred. type CCIPContractsToTransfer struct { - Router bool - FeeQuoter bool - OffRamp bool + Router bool + FeeQuoter bool + OffRamp bool + LockReleaseTokenPools []solana.PublicKey + BurnMintTokenPools []solana.PublicKey } type TransferCCIPToMCMSWithTimelockSolanaConfig struct { @@ -200,6 +202,41 @@ func TransferCCIPToMCMSWithTimelockSolana( Transactions: mcmsTxs, }) } + if len(contractsToTransfer.LockReleaseTokenPools) > 0 { + mcmsTxs, err := transferOwnershipLockReleaseTokenPools( + ccipState, + contractsToTransfer.LockReleaseTokenPools, + chainSelector, + solChain, + mcmState.TimelockProgram, + mcmState.TimelockSeed, + ) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to transfer ownership of lock-release token pools: %w", err) + } + batches = append(batches, mcmsTypes.BatchOperation{ + ChainSelector: mcmsTypes.ChainSelector(chainSelector), + Transactions: mcmsTxs, + }) + } + + if len(contractsToTransfer.BurnMintTokenPools) > 0 { + mcmsTxs, err := transferOwnershipBurnMintTokenPools( + ccipState, + contractsToTransfer.BurnMintTokenPools, + chainSelector, + solChain, + mcmState.TimelockProgram, + mcmState.TimelockSeed, + ) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to transfer ownership of burn-mint token pools: %w", err) + } + batches = append(batches, mcmsTypes.BatchOperation{ + ChainSelector: mcmsTypes.ChainSelector(chainSelector), + Transactions: mcmsTxs, + }) + } } proposal, err := proposalutils.BuildProposalFromBatchesV2( 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 b0c7e5e5721..ec617a0ed76 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 @@ -6,9 +6,15 @@ import ( "testing" "time" - "github.com/gagliardetto/solana-go" + solBinary "github.com/gagliardetto/binary" chainselectors "github.com/smartcontractkit/chain-selectors" mcmsSolana "github.com/smartcontractkit/mcms/sdk/solana" + + burnmint "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/example_burnmint_token_pool" + lockrelease "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/example_lockrelease_token_pool" + solTokenUtil "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/tokens" + + "github.com/gagliardetto/solana-go" "github.com/stretchr/testify/require" "go.uber.org/zap/zapcore" @@ -18,21 +24,17 @@ import ( "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/fee_quoter" "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/test_token_pool" - "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" - "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/v1_6" - "github.com/smartcontractkit/chainlink/v2/core/logger" - - solBinary "github.com/gagliardetto/binary" - "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/globals" solanachangesets "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" commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" - - "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/globals" "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" "github.com/smartcontractkit/chainlink/deployment/environment/memory" + "github.com/smartcontractkit/chainlink/v2/core/logger" ) // TODO: remove. These should be deployed as part of the test once deployment changesets are ready. @@ -270,6 +272,14 @@ func prepareEnvironmentForOwnershipTransfer(t *testing.T) (deployment.Environmen TokenDecimals: 9, }, ), + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(solanachangesets.DeploySolanaToken), + solanachangesets.DeploySolanaTokenConfig{ + ChainSelector: solChain1, + TokenProgramName: changeset.SPL2022Tokens, + TokenDecimals: 9, + }, + ), commonchangeset.Configure( deployment.CreateLegacyChangeSet(commonchangeset.DeployMCMSWithTimelockV2), map[uint64]commontypes.MCMSWithTimelockConfigV2{ @@ -288,17 +298,26 @@ func prepareEnvironmentForOwnershipTransfer(t *testing.T) (deployment.Environmen testhelpers.ValidateSolanaState(t, e, solChainSelectors) state, err := changeset.LoadOnchainStateSolana(e) require.NoError(t, err) - tokenAddress := state.SolChains[solChain1].SPL2022Tokens[0] + tokenAddressLockRelease := state.SolChains[solChain1].SPL2022Tokens[0] + tokenAddressBurnMint := state.SolChains[solChain1].SPL2022Tokens[1] e, err = commonchangeset.ApplyChangesets(t, e, nil, []commonchangeset.ConfiguredChangeSet{ commonchangeset.Configure( deployment.CreateLegacyChangeSet(solanachangesets.AddTokenPool), solanachangesets.TokenPoolConfig{ ChainSelector: solChain1, - TokenPubKey: tokenAddress.String(), + TokenPubKey: tokenAddressLockRelease.String(), PoolType: test_token_pool.LockAndRelease_PoolType, }, ), + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(solanachangesets.AddTokenPool), + solanachangesets.TokenPoolConfig{ + ChainSelector: solChain1, + TokenPubKey: tokenAddressBurnMint.String(), + PoolType: test_token_pool.BurnAndMint_PoolType, + }, + ), }) require.NoError(t, err) return e, state @@ -308,7 +327,23 @@ func TestTransferCCIPToMCMSWithTimelockSolana(t *testing.T) { e, state := prepareEnvironmentForOwnershipTransfer(t) solChain1 := e.AllChainSelectorsSolana()[0] solChain := e.SolChains[solChain1] - timelockSignerPDA, _ := testhelpers.TransferOwnershipSolana(t, &e, solChain1, false, true, true, true) + + tokenAddressLockRelease := state.SolChains[solChain1].SPL2022Tokens[0] + + tokenAddressBurnMint := state.SolChains[solChain1].SPL2022Tokens[1] + burnMintPoolConfigPDA, _ := solTokenUtil.TokenPoolConfigAddress(tokenAddressBurnMint, state.SolChains[solChain1].BurnMintTokenPool) + lockReleasePoolConfigPDA, _ := solTokenUtil.TokenPoolConfigAddress(tokenAddressLockRelease, state.SolChains[solChain1].LockReleaseTokenPool) + timelockSignerPDA, _ := testhelpers.TransferOwnershipSolana( + t, + &e, + solChain1, + false, + true, + true, + true, + []solana.PublicKey{burnMintPoolConfigPDA}, + []solana.PublicKey{lockReleasePoolConfigPDA}, + ) // 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, @@ -344,4 +379,22 @@ func TestTransferCCIPToMCMSWithTimelockSolana(t *testing.T) { 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") + + // (D) Check BurnMintTokenPools ownership: + require.Eventually(t, func() bool { + programData := burnmint.State{} + t.Logf("Checking BurnMintTokenPools ownership data. configPDA: %s", burnMintPoolConfigPDA.String()) + err := solChain.GetAccountDataBorshInto(ctx, burnMintPoolConfigPDA, &programData) + require.NoError(t, err) + return timelockSignerPDA.String() == programData.Config.Owner.String() + }, 30*time.Second, 5*time.Second, "BurnMintTokenPool owner was not changed to timelock signer PDA") + + // (E) Check LockReleaseTokenPools ownership: + require.Eventually(t, func() bool { + programData := lockrelease.State{} + t.Logf("Checking LockReleaseTokenPools ownership data. configPDA: %s", lockReleasePoolConfigPDA.String()) + err := solChain.GetAccountDataBorshInto(ctx, lockReleasePoolConfigPDA, &programData) + require.NoError(t, err) + return timelockSignerPDA.String() == programData.Config.Owner.String() + }, 30*time.Second, 5*time.Second, "LockReleaseTokenPool owner was not changed to timelock signer PDA") } diff --git a/deployment/ccip/changeset/testhelpers/test_helpers.go b/deployment/ccip/changeset/testhelpers/test_helpers.go index 98c85afb0cc..690beeff168 100644 --- a/deployment/ccip/changeset/testhelpers/test_helpers.go +++ b/deployment/ccip/changeset/testhelpers/test_helpers.go @@ -1636,7 +1636,11 @@ func TransferOwnershipSolana( e *deployment.Environment, solChain uint64, needTimelockDeployed bool, - transferRouter, transferFeeQuoter, transferOffRamp bool) (solana.PublicKey, solana.PublicKey) { + transferRouter, + transferFeeQuoter, + transferOffRamp bool, + burnMintTokenPools []solana.PublicKey, + lockReleaseTokenPools []solana.PublicKey) (timelockSignerPDA solana.PublicKey, mcmSignerPDA solana.PublicKey) { var err error if needTimelockDeployed { *e, err = commoncs.ApplyChangesetsV2(t, *e, []commoncs.ConfiguredChangeSet{ @@ -1662,8 +1666,8 @@ func TransferOwnershipSolana( // 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) + 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()) @@ -1676,9 +1680,11 @@ func TransferOwnershipSolana( MinDelay: 1 * time.Second, ContractsByChain: map[uint64]ccipChangeSetSolana.CCIPContractsToTransfer{ solChain: { - Router: transferRouter, - FeeQuoter: transferFeeQuoter, - OffRamp: transferOffRamp, + Router: transferRouter, + FeeQuoter: transferFeeQuoter, + OffRamp: transferOffRamp, + BurnMintTokenPools: burnMintTokenPools, + LockReleaseTokenPools: lockReleaseTokenPools, }, }, }, diff --git a/deployment/common/proposalutils/mcms_test_helpers.go b/deployment/common/proposalutils/mcms_test_helpers.go index 4e31011a1d9..bbe912b2ae6 100644 --- a/deployment/common/proposalutils/mcms_test_helpers.go +++ b/deployment/common/proposalutils/mcms_test_helpers.go @@ -320,7 +320,7 @@ func ExecuteMCMSTimelockProposalV2(t *testing.T, env deployment.Environment, tim if err != nil { return fmt.Errorf("[ExecuteMCMSTimelockProposalV2] Execute failed: %w", err) } - + t.Logf("[ExecuteMCMSTimelockProposalV2] Executed timelock operation index=%d on chain %d", i, uint64(op.ChainSelector)) family, err := chainsel.GetSelectorFamily(uint64(op.ChainSelector)) require.NoError(t, err)