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

[DPA-1564] fund mcm pdas changeset #16436

Merged
merged 11 commits into from
Feb 18, 2025
110 changes: 110 additions & 0 deletions deployment/common/changeset/solana/fund_mcm_pdas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package solana

import (
"errors"
"fmt"

"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"

"github.com/smartcontractkit/chainlink/deployment"
"github.com/smartcontractkit/chainlink/deployment/common/changeset/state"
)

var _ deployment.ChangeSetV2[FundMCMSignerConfig] = FundMCMSignersChangeset{}

type AmountsToTransfer struct {
ProposeMCM uint64
CancellerMCM uint64
BypasserMCM uint64
Timelock uint64
}
type FundMCMSignerConfig struct {
AmountsPerChain map[uint64]AmountsToTransfer
}

// FundMCMSignersChangeset is a changeset that funds the MCMS signers on each chain. It will find the
// signer PDA for the proposer, canceller and bypasser MCM as well as the timelock signer PDA and send the amount of
// SOL specified in the config to each of them.
type FundMCMSignersChangeset struct{}

// VerifyPreconditions checks if the deployer has enough SOL to fund the MCMS signers on each chain.
func (f FundMCMSignersChangeset) VerifyPreconditions(e deployment.Environment, config FundMCMSignerConfig) error {
// the number of accounts to fund per chain (bypasser, canceller, proposer, timelock)
for chainSelector, chainCfg := range config.AmountsPerChain {
solChain, ok := e.SolChains[chainSelector]
if !ok {
return fmt.Errorf("solana chain not found for selector %d", chainSelector)
}
addreses, err := e.ExistingAddresses.AddressesForChain(chainSelector)
if err != nil {
return fmt.Errorf("failed to get existing addresses: %w", err)
}
mcmState, err := state.MaybeLoadMCMSWithTimelockChainStateSolana(solChain, addreses)
if err != nil {
return fmt.Errorf("failed to load MCMS state: %w", err)
}
// Check if seeds are empty
if mcmState.ProposerMcmSeed == [32]byte{} || mcmState.TimelockSeed == [32]byte{} || mcmState.CancellerMcmSeed == [32]byte{} || mcmState.BypasserMcmSeed == [32]byte{} {
return errors.New("mcm/timelock seeds are empty, please deploy MCMS contracts first")
}
// Check if program IDs exists
if mcmState.McmProgram.IsZero() || mcmState.TimelockProgram.IsZero() {
return errors.New("mcm/timelock program IDs are empty, please deploy MCMS contracts first")
}
result, err := solChain.Client.GetBalance(e.GetContext(), solChain.DeployerKey.PublicKey(), rpc.CommitmentConfirmed)
if err != nil {
return fmt.Errorf("failed to get deployer balance: %w", err)
}
requiredAmount := chainCfg.ProposeMCM + chainCfg.CancellerMCM + chainCfg.BypasserMCM + chainCfg.Timelock
if result.Value < requiredAmount {
return fmt.Errorf("deployer balance is insufficient, required: %d, actual: %d", requiredAmount, result.Value)
}
}
return nil
}

// Apply funds the MCMS signers on each chain.
func (f FundMCMSignersChangeset) Apply(e deployment.Environment, config FundMCMSignerConfig) (deployment.ChangesetOutput, error) {
for chainSelector, cfgAmounts := range config.AmountsPerChain {
solChain := e.SolChains[chainSelector]
addreses, err := e.ExistingAddresses.AddressesForChain(chainSelector)
if err != nil {
return deployment.ChangesetOutput{}, fmt.Errorf("failed to get existing addresses: %w", err)
}
mcmState, err := state.MaybeLoadMCMSWithTimelockChainStateSolana(solChain, addreses)
if err != nil {
return deployment.ChangesetOutput{}, fmt.Errorf("failed to load MCMS state: %w", err)
}

err = FundFromDeployerKey(
solChain,
[]solana.PublicKey{state.GetTimelockSignerPDA(mcmState.TimelockProgram, mcmState.TimelockSeed)},
cfgAmounts.Timelock)
if err != nil {
return deployment.ChangesetOutput{}, fmt.Errorf("failed to fund timelock signer on chain %d: %w", chainSelector, err)
}
err = FundFromDeployerKey(
solChain,
[]solana.PublicKey{state.GetMCMSignerPDA(mcmState.McmProgram, mcmState.ProposerMcmSeed)},
cfgAmounts.ProposeMCM)
if err != nil {
return deployment.ChangesetOutput{}, fmt.Errorf("failed to fund MCMS proposer on chain %d: %w", chainSelector, err)
}
err = FundFromDeployerKey(
solChain,
[]solana.PublicKey{state.GetMCMSignerPDA(mcmState.McmProgram, mcmState.CancellerMcmSeed)},
cfgAmounts.CancellerMCM)
if err != nil {
return deployment.ChangesetOutput{}, fmt.Errorf("failed to fund MCMS canceller on chain %d: %w", chainSelector, err)
}
err = FundFromDeployerKey(
solChain,
[]solana.PublicKey{state.GetMCMSignerPDA(mcmState.McmProgram, mcmState.BypasserMcmSeed)},
cfgAmounts.BypasserMCM)
if err != nil {
return deployment.ChangesetOutput{}, fmt.Errorf("failed to fund mcm bypasser on chain %d: %w", chainSelector, err)
}
}
return deployment.ChangesetOutput{}, nil
}
282 changes: 282 additions & 0 deletions deployment/common/changeset/solana/fund_mcm_pdas_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
package solana_test

import (
"fmt"
"testing"

"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
chainselectors "github.com/smartcontractkit/chain-selectors"
mcmsSolana "github.com/smartcontractkit/mcms/sdk/solana"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zapcore"

"github.com/smartcontractkit/chainlink/deployment"
"github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers"
"github.com/smartcontractkit/chainlink/deployment/common/changeset"
commonSolana "github.com/smartcontractkit/chainlink/deployment/common/changeset/solana"
"github.com/smartcontractkit/chainlink/deployment/common/changeset/state"
"github.com/smartcontractkit/chainlink/deployment/common/proposalutils"
"github.com/smartcontractkit/chainlink/deployment/common/types"
"github.com/smartcontractkit/chainlink/deployment/environment/memory"
"github.com/smartcontractkit/chainlink/v2/core/logger"
)

// setupFundingTestEnv deploys all required contracts for the funding test
func setupFundingTestEnv(t *testing.T) deployment.Environment {
lggr := logger.TestLogger(t)
cfg := memory.MemoryEnvironmentConfig{
Nodes: 1,
SolChains: 1,
}
env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg)
chainSelector := env.AllChainSelectorsSolana()[0]

config := proposalutils.SingleGroupTimelockConfigV2(t)
testhelpers.SavePreloadedSolAddresses(t, env, chainSelector)
// Initialize the address book with a dummy address to avoid deploy precondition errors.
err := env.ExistingAddresses.Save(chainSelector, "dummyAddress", deployment.TypeAndVersion{Type: "dummy", Version: deployment.Version1_0_0})
require.NoError(t, err)

// Deploy MCMS and Timelock
env, err = changeset.Apply(t, env, nil,
changeset.Configure(
deployment.CreateLegacyChangeSet(changeset.DeployMCMSWithTimelockV2),
map[uint64]types.MCMSWithTimelockConfigV2{
chainSelector: config,
},
),
)
require.NoError(t, err)

return env
}

func TestFundMCMSignersChangeset_VerifyPreconditions(t *testing.T) {
lggr := logger.TestLogger(t)
validEnv := memory.NewMemoryEnvironment(t, lggr, zapcore.InfoLevel, memory.MemoryEnvironmentConfig{SolChains: 1})
validEnv.SolChains[chainselectors.SOLANA_DEVNET.Selector] = deployment.SolChain{}
validSolChainSelector := validEnv.AllChainSelectorsSolana()[0]

timelockID := mcmsSolana.ContractAddress(
solana.NewWallet().PublicKey(),
[32]byte{'t', 'e', 's', 't'},
)
mcmDummyProgram := solana.NewWallet().PublicKey()
mcmsProposerID := mcmsSolana.ContractAddress(
mcmDummyProgram,
[32]byte{'t', 'e', 's', 't', '1'},
)

mcmsCancellerID := mcmsSolana.ContractAddress(
mcmDummyProgram,
[32]byte{'t', 'e', 's', 't', '2'},
)

mcmsBypasserID := mcmsSolana.ContractAddress(
mcmDummyProgram,
[32]byte{'t', 'e', 's', 't', '3'},
)
err := validEnv.ExistingAddresses.Save(validSolChainSelector, timelockID, deployment.TypeAndVersion{
Type: types.RBACTimelock,
Version: deployment.Version1_0_0,
})
require.NoError(t, err)
err = validEnv.ExistingAddresses.Save(validSolChainSelector, mcmsProposerID, deployment.TypeAndVersion{
Type: types.ProposerManyChainMultisig,
Version: deployment.Version1_0_0,
})
require.NoError(t, err)
err = validEnv.ExistingAddresses.Save(validSolChainSelector, mcmsCancellerID, deployment.TypeAndVersion{
Type: types.CancellerManyChainMultisig,
Version: deployment.Version1_0_0,
})
require.NoError(t, err)
err = validEnv.ExistingAddresses.Save(validSolChainSelector, mcmsBypasserID, deployment.TypeAndVersion{
Type: types.BypasserManyChainMultisig,
Version: deployment.Version1_0_0,
})
require.NoError(t, err)
mcmsProposerIDEmpty := mcmsSolana.ContractAddress(
mcmDummyProgram,
[32]byte{},
)

// Create an environment that simulates a chain where the MCMS contracts have not been deployed,
// e.g. missing the required addresses so that the state loader returns empty seeds.
noMCMSEnv := memory.NewMemoryEnvironment(t, lggr, zapcore.InfoLevel, memory.MemoryEnvironmentConfig{
Chains: 0,
SolChains: 1,
Nodes: 1,
})
noMCMSEnv.SolChains[chainselectors.SOLANA_DEVNET.Selector] = deployment.SolChain{}
err = noMCMSEnv.ExistingAddresses.Save(chainselectors.SOLANA_DEVNET.Selector, mcmsProposerIDEmpty, deployment.TypeAndVersion{
Type: types.BypasserManyChainMultisig,
Version: deployment.Version1_0_0,
})
require.NoError(t, err)

// Create an environment with a Solana chain that has an invalid (zero) underlying chain.
invalidSolChainEnv := memory.NewMemoryEnvironment(t, lggr, zapcore.InfoLevel, memory.MemoryEnvironmentConfig{
Chains: 0,
SolChains: 0,
Nodes: 1,
})
invalidSolChainEnv.SolChains[validSolChainSelector] = deployment.SolChain{}

tests := []struct {
name string
env deployment.Environment
config commonSolana.FundMCMSignerConfig
expectedError string
}{
{
name: "All preconditions satisfied",
env: validEnv,
config: commonSolana.FundMCMSignerConfig{
AmountsPerChain: map[uint64]commonSolana.AmountsToTransfer{validSolChainSelector: {
ProposeMCM: 100,
CancellerMCM: 100,
BypasserMCM: 100,
Timelock: 100,
}},
},
expectedError: "",
},
{
name: "No Solana chains found in environment",
env: memory.NewMemoryEnvironment(t, lggr, zapcore.InfoLevel, memory.MemoryEnvironmentConfig{
Bootstraps: 1,
Chains: 1,
SolChains: 0,
Nodes: 1,
}),
config: commonSolana.FundMCMSignerConfig{
AmountsPerChain: map[uint64]commonSolana.AmountsToTransfer{validSolChainSelector: {
ProposeMCM: 100,
CancellerMCM: 100,
BypasserMCM: 100,
Timelock: 100,
}},
},
expectedError: fmt.Sprintf("solana chain not found for selector %d", validSolChainSelector),
},
{
name: "Chain selector not found in environment",
env: validEnv,
config: commonSolana.FundMCMSignerConfig{AmountsPerChain: map[uint64]commonSolana.AmountsToTransfer{99999: {
ProposeMCM: 100,
CancellerMCM: 100,
BypasserMCM: 100,
Timelock: 100,
}}},
expectedError: "solana chain not found for selector 99999",
},
{
name: "MCMS contracts not deployed (empty seeds)",
env: noMCMSEnv,
config: commonSolana.FundMCMSignerConfig{
AmountsPerChain: map[uint64]commonSolana.AmountsToTransfer{chainselectors.SOLANA_DEVNET.Selector: {
ProposeMCM: 100,
CancellerMCM: 100,
BypasserMCM: 100,
Timelock: 100,
}},
},
expectedError: "mcm/timelock seeds are empty, please deploy MCMS contracts first",
},
{
name: "Insufficient deployer balance",
env: validEnv,
config: commonSolana.FundMCMSignerConfig{
AmountsPerChain: map[uint64]commonSolana.AmountsToTransfer{validSolChainSelector: {
ProposeMCM: 9999999999999999999,
CancellerMCM: 9999999999999999999,
BypasserMCM: 9999999999999999999,
Timelock: 9999999999999999999,
}},
},
expectedError: "deployer balance is insufficient",
},
{
name: "Invalid Solana chain in environment",
env: invalidSolChainEnv,
config: commonSolana.FundMCMSignerConfig{
AmountsPerChain: map[uint64]commonSolana.AmountsToTransfer{validSolChainSelector: {
ProposeMCM: 100,
CancellerMCM: 100,
BypasserMCM: 100,
Timelock: 100,
}},
},
expectedError: "failed to get existing addresses: chain selector 12463857294658392847: chain not found",
},
}

cs := commonSolana.FundMCMSignersChangeset{}

for _, tt := range tests {
tt := tt // capture range variable
t.Run(tt.name, func(t *testing.T) {
err := cs.VerifyPreconditions(tt.env, tt.config)
if tt.expectedError == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedError)
}
})
}
}

func TestFundMCMSignersChangeset_Apply(t *testing.T) {
env := setupFundingTestEnv(t)
cfgAmounts := commonSolana.AmountsToTransfer{
ProposeMCM: 100 * solana.LAMPORTS_PER_SOL,
CancellerMCM: 350 * solana.LAMPORTS_PER_SOL,
BypasserMCM: 75 * solana.LAMPORTS_PER_SOL,
Timelock: 83 * solana.LAMPORTS_PER_SOL,
}
amountsPerChain := make(map[uint64]commonSolana.AmountsToTransfer)
for chainSelector := range env.SolChains {
amountsPerChain[chainSelector] = cfgAmounts
}
config := commonSolana.FundMCMSignerConfig{
AmountsPerChain: amountsPerChain,
}

changesetInstance := commonSolana.FundMCMSignersChangeset{}

env, err := changeset.ApplyChangesetsV2(t, env, []changeset.ConfiguredChangeSet{
changeset.Configure(changesetInstance, config),
})
require.NoError(t, err)

chainSelector := env.AllChainSelectorsSolana()[0]
solChain := env.SolChains[chainSelector]
addresses, err := env.ExistingAddresses.AddressesForChain(chainSelector)
require.NoError(t, err)

// Check balances of MCM Signer PDAS
mcmState, err := state.MaybeLoadMCMSWithTimelockChainStateSolana(solChain, addresses)
require.NoError(t, err)

accounts := []solana.PublicKey{
state.GetTimelockSignerPDA(mcmState.TimelockProgram, mcmState.TimelockSeed),
state.GetMCMSignerPDA(mcmState.McmProgram, mcmState.ProposerMcmSeed),
state.GetMCMSignerPDA(mcmState.McmProgram, mcmState.CancellerMcmSeed),
state.GetMCMSignerPDA(mcmState.McmProgram, mcmState.BypasserMcmSeed),
}
var balances []uint64
for _, account := range accounts {
balance, err := solChain.Client.GetBalance(env.GetContext(), account, rpc.CommitmentConfirmed)
require.NoError(t, err)
t.Logf("Account: %s, Balance: %d", account, balance.Value)
balances = append(balances, balance.Value)
}

require.Equal(t, cfgAmounts.Timelock, balances[0])
require.Equal(t, cfgAmounts.ProposeMCM, balances[1])
require.Equal(t, cfgAmounts.CancellerMCM, balances[2])
require.Equal(t, cfgAmounts.BypasserMCM, balances[3])
}
Loading
Loading