Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add token pool ownership transfer #16596

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions deployment/ccip/changeset/solana/cs_chain_contracts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion deployment/ccip/changeset/solana/cs_deploy_chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion deployment/ccip/changeset/solana/cs_token_pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
120 changes: 120 additions & 0 deletions deployment/ccip/changeset/solana/ownership_transfer_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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.
Expand Down Expand Up @@ -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{
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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")
}
18 changes: 12 additions & 6 deletions deployment/ccip/changeset/testhelpers/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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())
Expand All @@ -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,
},
},
},
Expand Down
Loading
Loading