diff --git a/deployment/ccip/changeset/solana/cs_chain_contracts.go b/deployment/ccip/changeset/solana/cs_chain_contracts.go index a4cc5c11d9f..89a6be24ea5 100644 --- a/deployment/ccip/changeset/solana/cs_chain_contracts.go +++ b/deployment/ccip/changeset/solana/cs_chain_contracts.go @@ -33,6 +33,8 @@ type MCMSConfigSolana struct { RouterOwnedByTimelock bool FeeQuoterOwnedByTimelock bool OffRampOwnedByTimelock bool + // Assumes whatever token pool we're operating on + TokenPoolOwnedByTimelock bool } // HELPER FUNCTIONS diff --git a/deployment/ccip/changeset/solana/cs_chain_contracts_test.go b/deployment/ccip/changeset/solana/cs_chain_contracts_test.go index 8a4fc0579d8..5d9ecefaba1 100644 --- a/deployment/ccip/changeset/solana/cs_chain_contracts_test.go +++ b/deployment/ccip/changeset/solana/cs_chain_contracts_test.go @@ -8,14 +8,11 @@ import ( "github.com/gagliardetto/solana-go" "github.com/stretchr/testify/require" - solBaseTokenPool "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/base_token_pool" 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" - solTestTokenPool "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/test_token_pool" solCommonUtil "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/common" solState "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/state" - solTokenUtil "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/tokens" "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext" @@ -228,96 +225,6 @@ func TestDeployCCIPContracts(t *testing.T) { testhelpers.DeployCCIPContractsTest(t, 1) } -func TestAddTokenPool(t *testing.T) { - t.Parallel() - ctx := testcontext.Get(t) - tenv, _ := testhelpers.NewMemoryEnvironment(t, testhelpers.WithSolChains(1)) - - evmChain := tenv.Env.AllChainSelectors()[0] - solChain := tenv.Env.AllChainSelectorsSolana()[0] - e, newTokenAddress, err := deployToken(t, tenv.Env, solChain) - require.NoError(t, err) - state, err := ccipChangeset.LoadOnchainStateSolana(e) - require.NoError(t, err) - remoteConfig := solBaseTokenPool.RemoteConfig{ - PoolAddresses: []solTestTokenPool.RemoteAddress{{Address: []byte{1, 2, 3}}}, - TokenAddress: solTestTokenPool.RemoteAddress{Address: []byte{4, 5, 6}}, - Decimals: 9, - } - inboundConfig := solBaseTokenPool.RateLimitConfig{ - Enabled: true, - Capacity: uint64(1000), - Rate: 1, - } - outboundConfig := solBaseTokenPool.RateLimitConfig{ - Enabled: false, - Capacity: 0, - Rate: 0, - } - - tokenMap := map[deployment.ContractType]solana.PublicKey{ - ccipChangeset.SPL2022Tokens: newTokenAddress, - ccipChangeset.SPLTokens: state.SolChains[solChain].WSOL, - } - - type poolTestType struct { - poolType solTestTokenPool.PoolType - poolAddress solana.PublicKey - } - testCases := []poolTestType{ - { - poolType: solTestTokenPool.BurnAndMint_PoolType, - poolAddress: state.SolChains[solChain].BurnMintTokenPool, - }, - { - poolType: solTestTokenPool.LockAndRelease_PoolType, - poolAddress: state.SolChains[solChain].LockReleaseTokenPool, - }, - } - for _, testCase := range testCases { - for _, tokenAddress := range tokenMap { - e, err = commonchangeset.Apply(t, e, nil, - commonchangeset.Configure( - deployment.CreateLegacyChangeSet(ccipChangesetSolana.AddTokenPool), - ccipChangesetSolana.TokenPoolConfig{ - ChainSelector: solChain, - TokenPubKey: tokenAddress.String(), - PoolType: testCase.poolType, - // this works for testing, but if we really want some other authority we need to pass in a private key for signing purposes - Authority: tenv.Env.SolChains[solChain].DeployerKey.PublicKey().String(), - }, - ), - commonchangeset.Configure( - deployment.CreateLegacyChangeSet(ccipChangesetSolana.SetupTokenPoolForRemoteChain), - ccipChangesetSolana.RemoteChainTokenPoolConfig{ - SolChainSelector: solChain, - RemoteChainSelector: evmChain, - SolTokenPubKey: tokenAddress.String(), - RemoteConfig: remoteConfig, - InboundRateLimit: inboundConfig, - OutboundRateLimit: outboundConfig, - PoolType: testCase.poolType, - }, - ), - ) - require.NoError(t, err) - // test AddTokenPool results - configAccount := solTestTokenPool.State{} - poolConfigPDA, _ := solTokenUtil.TokenPoolConfigAddress(tokenAddress, testCase.poolAddress) - err = e.SolChains[solChain].GetAccountDataBorshInto(ctx, poolConfigPDA, &configAccount) - require.NoError(t, err) - require.Equal(t, tokenAddress, configAccount.Config.Mint) - // test SetupTokenPoolForRemoteChain results - remoteChainConfigPDA, _, _ := solTokenUtil.TokenPoolChainConfigPDA(evmChain, tokenAddress, testCase.poolAddress) - var remoteChainConfigAccount solTestTokenPool.ChainConfig - err = e.SolChains[solChain].GetAccountDataBorshInto(ctx, remoteChainConfigPDA, &remoteChainConfigAccount) - require.NoError(t, err) - require.Equal(t, uint8(9), remoteChainConfigAccount.Base.Remote.Decimals) - } - } - -} - func TestBilling(t *testing.T) { t.Parallel() tests := []struct { diff --git a/deployment/ccip/changeset/solana/cs_token_pool.go b/deployment/ccip/changeset/solana/cs_token_pool.go index 7d85ecfa4fb..965bd653c44 100644 --- a/deployment/ccip/changeset/solana/cs_token_pool.go +++ b/deployment/ccip/changeset/solana/cs_token_pool.go @@ -14,6 +14,8 @@ import ( solCommonUtil "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/common" solState "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/state" solTokenUtil "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/tokens" + "github.com/smartcontractkit/mcms" + mcmsTypes "github.com/smartcontractkit/mcms/types" "github.com/smartcontractkit/chainlink/deployment" ccipChangeset "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" @@ -185,6 +187,8 @@ type RemoteChainTokenPoolConfig struct { RemoteConfig solBaseTokenPool.RemoteConfig InboundRateLimit solBaseTokenPool.RateLimitConfig OutboundRateLimit solBaseTokenPool.RateLimitConfig + MCMSSolana *MCMSConfigSolana + IsEdit bool } func (cfg RemoteChainTokenPoolConfig) Validate(e deployment.Environment) error { @@ -231,8 +235,12 @@ func (cfg RemoteChainTokenPoolConfig) Validate(e deployment.Environment) error { if err != nil { return fmt.Errorf("failed to get token pool remote chain config pda (remoteSelector: %d, mint: %s, pool: %s): %w", cfg.RemoteChainSelector, tokenPubKey.String(), tokenPool.String(), err) } - if err := chain.GetAccountDataBorshInto(context.Background(), remoteChainConfigPDA, &remoteChainConfigAccount); err == nil { + err = chain.GetAccountDataBorshInto(context.Background(), remoteChainConfigPDA, &remoteChainConfigAccount) + + if !cfg.IsEdit && err == nil { return fmt.Errorf("remote chain config already exists for (remoteSelector: %d, mint: %s, pool: %s, type: %s)", cfg.RemoteChainSelector, tokenPubKey.String(), tokenPool.String(), cfg.PoolType) + } else if cfg.IsEdit && err != nil { + return fmt.Errorf("remote chain config not found for (remoteSelector: %d, mint: %s, pool: %s, type: %s): %w", cfg.RemoteChainSelector, tokenPubKey.String(), tokenPool.String(), cfg.PoolType, err) } return nil } @@ -278,17 +286,35 @@ func getInstructionsForBurnMint( poolConfigPDA, _ := solTokenUtil.TokenPoolConfigAddress(tokenPubKey, chainState.BurnMintTokenPool) remoteChainConfigPDA, _, _ := solTokenUtil.TokenPoolChainConfigPDA(cfg.RemoteChainSelector, tokenPubKey, chainState.BurnMintTokenPool) solBurnMintTokenPool.SetProgramID(chainState.BurnMintTokenPool) - ixConfigure, err := solBurnMintTokenPool.NewInitChainRemoteConfigInstruction( - cfg.RemoteChainSelector, - tokenPubKey, - cfg.RemoteConfig, - poolConfigPDA, - remoteChainConfigPDA, - chain.DeployerKey.PublicKey(), - solana.SystemProgramID, - ).ValidateAndBuild() - if err != nil { - return nil, fmt.Errorf("failed to generate instructions: %w", err) + ixns := make([]solana.Instruction, 0) + if !cfg.IsEdit { + ixConfigure, err := solBurnMintTokenPool.NewInitChainRemoteConfigInstruction( + cfg.RemoteChainSelector, + tokenPubKey, + cfg.RemoteConfig, + poolConfigPDA, + remoteChainConfigPDA, + chain.DeployerKey.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + if err != nil { + return nil, fmt.Errorf("failed to generate instructions: %w", err) + } + ixns = append(ixns, ixConfigure) + } else { + ixConfigure, err := solBurnMintTokenPool.NewEditChainRemoteConfigInstruction( + cfg.RemoteChainSelector, + tokenPubKey, + cfg.RemoteConfig, + poolConfigPDA, + remoteChainConfigPDA, + chain.DeployerKey.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + if err != nil { + return nil, fmt.Errorf("failed to generate instructions: %w", err) + } + ixns = append(ixns, ixConfigure) } ixRates, err := solBurnMintTokenPool.NewSetChainRateLimitInstruction( cfg.RemoteChainSelector, @@ -303,19 +329,23 @@ func getInstructionsForBurnMint( if err != nil { return nil, fmt.Errorf("failed to generate instructions: %w", err) } - ixAppend, err := solBurnMintTokenPool.NewAppendRemotePoolAddressesInstruction( - cfg.RemoteChainSelector, - tokenPubKey, - cfg.RemoteConfig.PoolAddresses, // i dont know why this is a list (is it for different types of pool of the same token?) - poolConfigPDA, - remoteChainConfigPDA, - chain.DeployerKey.PublicKey(), - solana.SystemProgramID, - ).ValidateAndBuild() - if err != nil { - return nil, fmt.Errorf("failed to generate instructions: %w", err) + ixns = append(ixns, ixRates) + if len(cfg.RemoteConfig.PoolAddresses) > 0 { + ixAppend, err := solBurnMintTokenPool.NewAppendRemotePoolAddressesInstruction( + cfg.RemoteChainSelector, + tokenPubKey, + cfg.RemoteConfig.PoolAddresses, // i dont know why this is a list (is it for different types of pool of the same token?) + poolConfigPDA, + remoteChainConfigPDA, + chain.DeployerKey.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + if err != nil { + return nil, fmt.Errorf("failed to generate instructions: %w", err) + } + ixns = append(ixns, ixAppend) } - return []solana.Instruction{ixConfigure, ixRates, ixAppend}, nil + return ixns, nil } func getInstructionsForLockRelease( @@ -327,17 +357,35 @@ func getInstructionsForLockRelease( poolConfigPDA, _ := solTokenUtil.TokenPoolConfigAddress(tokenPubKey, chainState.LockReleaseTokenPool) remoteChainConfigPDA, _, _ := solTokenUtil.TokenPoolChainConfigPDA(cfg.RemoteChainSelector, tokenPubKey, chainState.LockReleaseTokenPool) solLockReleaseTokenPool.SetProgramID(chainState.LockReleaseTokenPool) - ixConfigure, err := solLockReleaseTokenPool.NewInitChainRemoteConfigInstruction( - cfg.RemoteChainSelector, - tokenPubKey, - cfg.RemoteConfig, - poolConfigPDA, - remoteChainConfigPDA, - chain.DeployerKey.PublicKey(), - solana.SystemProgramID, - ).ValidateAndBuild() - if err != nil { - return nil, fmt.Errorf("failed to generate instructions: %w", err) + ixns := make([]solana.Instruction, 0) + if !cfg.IsEdit { + ixConfigure, err := solLockReleaseTokenPool.NewInitChainRemoteConfigInstruction( + cfg.RemoteChainSelector, + tokenPubKey, + cfg.RemoteConfig, + poolConfigPDA, + remoteChainConfigPDA, + chain.DeployerKey.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + if err != nil { + return nil, fmt.Errorf("failed to generate instructions: %w", err) + } + ixns = append(ixns, ixConfigure) + } else { + ixConfigure, err := solLockReleaseTokenPool.NewEditChainRemoteConfigInstruction( + cfg.RemoteChainSelector, + tokenPubKey, + cfg.RemoteConfig, + poolConfigPDA, + remoteChainConfigPDA, + chain.DeployerKey.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + if err != nil { + return nil, fmt.Errorf("failed to generate instructions: %w", err) + } + ixns = append(ixns, ixConfigure) } ixRates, err := solLockReleaseTokenPool.NewSetChainRateLimitInstruction( cfg.RemoteChainSelector, @@ -352,19 +400,23 @@ func getInstructionsForLockRelease( if err != nil { return nil, fmt.Errorf("failed to generate instructions: %w", err) } - ixAppend, err := solLockReleaseTokenPool.NewAppendRemotePoolAddressesInstruction( - cfg.RemoteChainSelector, - tokenPubKey, - cfg.RemoteConfig.PoolAddresses, // i dont know why this is a list (is it for different types of pool of the same token?) - poolConfigPDA, - remoteChainConfigPDA, - chain.DeployerKey.PublicKey(), - solana.SystemProgramID, - ).ValidateAndBuild() - if err != nil { - return nil, fmt.Errorf("failed to generate instructions: %w", err) + ixns = append(ixns, ixRates) + if len(cfg.RemoteConfig.PoolAddresses) > 0 { + ixAppend, err := solLockReleaseTokenPool.NewAppendRemotePoolAddressesInstruction( + cfg.RemoteChainSelector, + tokenPubKey, + cfg.RemoteConfig.PoolAddresses, // i dont know why this is a list (is it for different types of pool of the same token?) + poolConfigPDA, + remoteChainConfigPDA, + chain.DeployerKey.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + if err != nil { + return nil, fmt.Errorf("failed to generate instructions: %w", err) + } + ixns = append(ixns, ixAppend) } - return []solana.Instruction{ixConfigure, ixRates, ixAppend}, nil + return ixns, nil } // ADD TOKEN POOL LOOKUP TABLE @@ -518,3 +570,266 @@ func SetPool(e deployment.Environment, cfg SetPoolConfig) (deployment.ChangesetO e.Logger.Infow("Set pool config", "token_pubkey", tokenPubKey.String()) return deployment.ChangesetOutput{}, nil } + +type ConfigureTokenPoolAllowListConfig struct { + SolChainSelector uint64 + SolTokenPubKey string + PoolType solTestTokenPool.PoolType + Accounts []solana.PublicKey + Enabled bool + MCMSSolana *MCMSConfigSolana +} + +func (cfg ConfigureTokenPoolAllowListConfig) Validate(e deployment.Environment) error { + tokenPubKey := solana.MustPublicKeyFromBase58(cfg.SolTokenPubKey) + if err := commonValidation(e, cfg.SolChainSelector, tokenPubKey); err != nil { + return err + } + state, _ := ccipChangeset.LoadOnchainState(e) + chainState := state.SolChains[cfg.SolChainSelector] + chain := e.SolChains[cfg.SolChainSelector] + + if err := validatePoolDeployment(chainState, cfg.PoolType, cfg.SolChainSelector); err != nil { + return err + } + + var tokenPool solana.PublicKey + var poolConfigAccount interface{} + + switch cfg.PoolType { + case solTestTokenPool.BurnAndMint_PoolType: + tokenPool = chainState.BurnMintTokenPool + poolConfigAccount = solBurnMintTokenPool.State{} + case solTestTokenPool.LockAndRelease_PoolType: + tokenPool = chainState.LockReleaseTokenPool + poolConfigAccount = solLockReleaseTokenPool.State{} + default: + return fmt.Errorf("invalid pool type: %s", cfg.PoolType) + } + + // check if pool config exists + poolConfigPDA, err := solTokenUtil.TokenPoolConfigAddress(tokenPubKey, tokenPool) + if err != nil { + return fmt.Errorf("failed to get token pool config address (mint: %s, pool: %s): %w", tokenPubKey.String(), tokenPool.String(), err) + } + if err := chain.GetAccountDataBorshInto(context.Background(), poolConfigPDA, &poolConfigAccount); err != nil { + return fmt.Errorf("token pool config not found (mint: %s, pool: %s, type: %s): %w", tokenPubKey.String(), tokenPool.String(), cfg.PoolType, err) + } + return nil +} + +func ConfigureTokenPoolAllowList(e deployment.Environment, cfg ConfigureTokenPoolAllowListConfig) (deployment.ChangesetOutput, error) { + if err := cfg.Validate(e); err != nil { + return deployment.ChangesetOutput{}, err + } + + chain := e.SolChains[cfg.SolChainSelector] + state, _ := ccipChangeset.LoadOnchainState(e) + chainState := state.SolChains[cfg.SolChainSelector] + tokenPubKey := solana.MustPublicKeyFromBase58(cfg.SolTokenPubKey) + + var ix solana.Instruction + var err error + tokenPoolUsingMcms := cfg.MCMSSolana != nil && cfg.MCMSSolana.TokenPoolOwnedByTimelock + // validate ownership + var authority solana.PublicKey + var programID solana.PublicKey + var contractType deployment.ContractType + if tokenPoolUsingMcms { + authority, err = FetchTimelockSigner(e, cfg.SolChainSelector) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch timelock signer: %w", err) + } + } else { + authority = chain.DeployerKey.PublicKey() + } + switch cfg.PoolType { + case solTestTokenPool.BurnAndMint_PoolType: + tokenPubKey := solana.MustPublicKeyFromBase58(cfg.SolTokenPubKey) + poolConfigPDA, _ := solTokenUtil.TokenPoolConfigAddress(tokenPubKey, chainState.BurnMintTokenPool) + solBurnMintTokenPool.SetProgramID(chainState.BurnMintTokenPool) + programID = chainState.BurnMintTokenPool + contractType = ccipChangeset.BurnMintTokenPool + ix, err = solBurnMintTokenPool.NewConfigureAllowListInstruction( + cfg.Accounts, + cfg.Enabled, + poolConfigPDA, + authority, + solana.SystemProgramID, + ).ValidateAndBuild() + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate instructions: %w", err) + } + case solTestTokenPool.LockAndRelease_PoolType: + tokenPubKey := solana.MustPublicKeyFromBase58(cfg.SolTokenPubKey) + poolConfigPDA, _ := solTokenUtil.TokenPoolConfigAddress(tokenPubKey, chainState.LockReleaseTokenPool) + solLockReleaseTokenPool.SetProgramID(chainState.LockReleaseTokenPool) + programID = chainState.LockReleaseTokenPool + contractType = ccipChangeset.LockReleaseTokenPool + ix, err = solLockReleaseTokenPool.NewConfigureAllowListInstruction( + cfg.Accounts, + cfg.Enabled, + poolConfigPDA, + authority, + solana.SystemProgramID, + ).ValidateAndBuild() + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate instructions: %w", err) + } + default: + return deployment.ChangesetOutput{}, fmt.Errorf("invalid pool type: %s", cfg.PoolType) + } + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate instructions: %w", err) + } + if tokenPoolUsingMcms { + tx, err := BuildMCMSTxn(ix, programID.String(), contractType) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create transaction: %w", err) + } + proposal, err := BuildProposalsForTxns( + e, cfg.SolChainSelector, "proposal to ConfigureTokenPoolAllowList in Solana", cfg.MCMSSolana.MCMS.MinDelay, []mcmsTypes.Transaction{*tx}) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + return deployment.ChangesetOutput{ + MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, + }, nil + } + + err = chain.Confirm([]solana.Instruction{ix}) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm instructions: %w", err) + } + e.Logger.Infow("Configured token pool allowlist", "token_pubkey", tokenPubKey.String()) + return deployment.ChangesetOutput{}, nil +} + +type RemoveFromAllowListConfig struct { + SolChainSelector uint64 + SolTokenPubKey string + PoolType solTestTokenPool.PoolType + Accounts []solana.PublicKey + MCMSSolana *MCMSConfigSolana +} + +func (cfg RemoveFromAllowListConfig) Validate(e deployment.Environment) error { + tokenPubKey := solana.MustPublicKeyFromBase58(cfg.SolTokenPubKey) + if err := commonValidation(e, cfg.SolChainSelector, tokenPubKey); err != nil { + return err + } + state, _ := ccipChangeset.LoadOnchainState(e) + chainState := state.SolChains[cfg.SolChainSelector] + chain := e.SolChains[cfg.SolChainSelector] + + if err := validatePoolDeployment(chainState, cfg.PoolType, cfg.SolChainSelector); err != nil { + return err + } + + var tokenPool solana.PublicKey + var poolConfigAccount interface{} + + switch cfg.PoolType { + case solTestTokenPool.BurnAndMint_PoolType: + tokenPool = chainState.BurnMintTokenPool + poolConfigAccount = solBurnMintTokenPool.State{} + case solTestTokenPool.LockAndRelease_PoolType: + tokenPool = chainState.LockReleaseTokenPool + poolConfigAccount = solLockReleaseTokenPool.State{} + default: + return fmt.Errorf("invalid pool type: %s", cfg.PoolType) + } + + // check if pool config exists + poolConfigPDA, err := solTokenUtil.TokenPoolConfigAddress(tokenPubKey, tokenPool) + if err != nil { + return fmt.Errorf("failed to get token pool config address (mint: %s, pool: %s): %w", tokenPubKey.String(), tokenPool.String(), err) + } + if err := chain.GetAccountDataBorshInto(context.Background(), poolConfigPDA, &poolConfigAccount); err != nil { + return fmt.Errorf("token pool config not found (mint: %s, pool: %s, type: %s): %w", tokenPubKey.String(), tokenPool.String(), cfg.PoolType, err) + } + return nil +} + +func RemoveFromTokenPoolAllowList(e deployment.Environment, cfg RemoveFromAllowListConfig) (deployment.ChangesetOutput, error) { + if err := cfg.Validate(e); err != nil { + return deployment.ChangesetOutput{}, err + } + + chain := e.SolChains[cfg.SolChainSelector] + state, _ := ccipChangeset.LoadOnchainState(e) + chainState := state.SolChains[cfg.SolChainSelector] + tokenPubKey := solana.MustPublicKeyFromBase58(cfg.SolTokenPubKey) + + var ix solana.Instruction + var err error + tokenPoolUsingMcms := cfg.MCMSSolana != nil && cfg.MCMSSolana.TokenPoolOwnedByTimelock + // validate ownership + var authority solana.PublicKey + var programID solana.PublicKey + var contractType deployment.ContractType + if tokenPoolUsingMcms { + authority, err = FetchTimelockSigner(e, cfg.SolChainSelector) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch timelock signer: %w", err) + } + } else { + authority = chain.DeployerKey.PublicKey() + } + switch cfg.PoolType { + case solTestTokenPool.BurnAndMint_PoolType: + tokenPubKey := solana.MustPublicKeyFromBase58(cfg.SolTokenPubKey) + poolConfigPDA, _ := solTokenUtil.TokenPoolConfigAddress(tokenPubKey, chainState.BurnMintTokenPool) + solBurnMintTokenPool.SetProgramID(chainState.BurnMintTokenPool) + programID = chainState.BurnMintTokenPool + contractType = ccipChangeset.BurnMintTokenPool + ix, err = solBurnMintTokenPool.NewRemoveFromAllowListInstruction( + cfg.Accounts, + poolConfigPDA, + authority, + ).ValidateAndBuild() + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate instructions: %w", err) + } + case solTestTokenPool.LockAndRelease_PoolType: + tokenPubKey := solana.MustPublicKeyFromBase58(cfg.SolTokenPubKey) + poolConfigPDA, _ := solTokenUtil.TokenPoolConfigAddress(tokenPubKey, chainState.LockReleaseTokenPool) + solLockReleaseTokenPool.SetProgramID(chainState.LockReleaseTokenPool) + programID = chainState.LockReleaseTokenPool + contractType = ccipChangeset.LockReleaseTokenPool + ix, err = solLockReleaseTokenPool.NewRemoveFromAllowListInstruction( + cfg.Accounts, + poolConfigPDA, + authority, + ).ValidateAndBuild() + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate instructions: %w", err) + } + default: + return deployment.ChangesetOutput{}, fmt.Errorf("invalid pool type: %s", cfg.PoolType) + } + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate instructions: %w", err) + } + if tokenPoolUsingMcms { + tx, err := BuildMCMSTxn(ix, programID.String(), contractType) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create transaction: %w", err) + } + proposal, err := BuildProposalsForTxns( + e, cfg.SolChainSelector, "proposal to RemoveFromTokenPoolAllowList in Solana", cfg.MCMSSolana.MCMS.MinDelay, []mcmsTypes.Transaction{*tx}) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + return deployment.ChangesetOutput{ + MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, + }, nil + } + + err = chain.Confirm([]solana.Instruction{ix}) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm instructions: %w", err) + } + e.Logger.Infow("Configured token pool allowlist", "token_pubkey", tokenPubKey.String()) + return deployment.ChangesetOutput{}, nil +} diff --git a/deployment/ccip/changeset/solana/cs_token_pool_test.go b/deployment/ccip/changeset/solana/cs_token_pool_test.go new file mode 100644 index 00000000000..c67b5f9bd12 --- /dev/null +++ b/deployment/ccip/changeset/solana/cs_token_pool_test.go @@ -0,0 +1,197 @@ +package solana_test + +import ( + "testing" + "time" + + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" + + solBaseTokenPool "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/base_token_pool" + solTestTokenPool "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/test_token_pool" + solTokenUtil "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/tokens" + + "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext" + + ccipChangeset "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" + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" +) + +func TestAddTokenPool(t *testing.T) { + t.Parallel() + doTestTokenPool(t, false) +} + +// func TestAddTokenPoolMcms(t *testing.T) { +// t.Parallel() +// doTestTokenPool(t, true) +// } + +func doTestTokenPool(t *testing.T, mcms bool) { + ctx := testcontext.Get(t) + tenv, _ := testhelpers.NewMemoryEnvironment(t, testhelpers.WithSolChains(1)) + + evmChain := tenv.Env.AllChainSelectors()[0] + solChain := tenv.Env.AllChainSelectorsSolana()[0] + e, newTokenAddress, err := deployToken(t, tenv.Env, solChain) + require.NoError(t, err) + state, err := ccipChangeset.LoadOnchainStateSolana(e) + require.NoError(t, err) + mcmsConfigured := false + remoteConfig := solBaseTokenPool.RemoteConfig{ + PoolAddresses: []solTestTokenPool.RemoteAddress{{Address: []byte{1, 2, 3}}}, + TokenAddress: solTestTokenPool.RemoteAddress{Address: []byte{4, 5, 6}}, + Decimals: 9, + } + inboundConfig := solBaseTokenPool.RateLimitConfig{ + Enabled: true, + Capacity: uint64(1000), + Rate: 1, + } + outboundConfig := solBaseTokenPool.RateLimitConfig{ + Enabled: false, + Capacity: 0, + Rate: 0, + } + + tokenMap := map[deployment.ContractType]solana.PublicKey{ + ccipChangeset.SPL2022Tokens: newTokenAddress, + ccipChangeset.SPLTokens: state.SolChains[solChain].WSOL, + } + + type poolTestType struct { + poolType solTestTokenPool.PoolType + poolAddress solana.PublicKey + mcms bool + } + testCases := []poolTestType{ + { + poolType: solTestTokenPool.BurnAndMint_PoolType, + poolAddress: state.SolChains[solChain].BurnMintTokenPool, + }, + { + poolType: solTestTokenPool.LockAndRelease_PoolType, + poolAddress: state.SolChains[solChain].LockReleaseTokenPool, + }, + } + for _, testCase := range testCases { + for _, tokenAddress := range tokenMap { + e, err = commonchangeset.ApplyChangesetsV2(t, e, []commonchangeset.ConfiguredChangeSet{ + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.AddTokenPool), + ccipChangesetSolana.TokenPoolConfig{ + ChainSelector: solChain, + TokenPubKey: tokenAddress.String(), + PoolType: testCase.poolType, + // this works for testing, but if we really want some other authority we need to pass in a private key for signing purposes + Authority: tenv.Env.SolChains[solChain].DeployerKey.PublicKey().String(), + }, + ), + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.SetupTokenPoolForRemoteChain), + ccipChangesetSolana.RemoteChainTokenPoolConfig{ + SolChainSelector: solChain, + RemoteChainSelector: evmChain, + SolTokenPubKey: tokenAddress.String(), + RemoteConfig: remoteConfig, + InboundRateLimit: inboundConfig, + OutboundRateLimit: outboundConfig, + PoolType: testCase.poolType, + }, + ), + }, + ) + require.NoError(t, err) + // test AddTokenPool results + configAccount := solTestTokenPool.State{} + poolConfigPDA, _ := solTokenUtil.TokenPoolConfigAddress(tokenAddress, testCase.poolAddress) + err = e.SolChains[solChain].GetAccountDataBorshInto(ctx, poolConfigPDA, &configAccount) + require.NoError(t, err) + require.Equal(t, tokenAddress, configAccount.Config.Mint) + // test SetupTokenPoolForRemoteChain results + remoteChainConfigPDA, _, _ := solTokenUtil.TokenPoolChainConfigPDA(evmChain, tokenAddress, testCase.poolAddress) + var remoteChainConfigAccount solTestTokenPool.ChainConfig + err = e.SolChains[solChain].GetAccountDataBorshInto(ctx, remoteChainConfigPDA, &remoteChainConfigAccount) + require.NoError(t, err) + require.Equal(t, uint8(9), remoteChainConfigAccount.Base.Remote.Decimals) + + var mcmsConfig *ccipChangesetSolana.MCMSConfigSolana + if testCase.mcms && !mcmsConfigured { + _, _ = testhelpers.TransferOwnershipSolana(t, &e, solChain, true, true, true, true) + mcmsConfig = &ccipChangesetSolana.MCMSConfigSolana{ + MCMS: &ccipChangeset.MCMSConfig{ + MinDelay: 1 * time.Second, + }, + RouterOwnedByTimelock: true, + FeeQuoterOwnedByTimelock: true, + OffRampOwnedByTimelock: true, + } + require.NotNil(t, mcmsConfig) + mcmsConfigured = true + } + + allowedAccount1, _ := solana.NewRandomPrivateKey() + allowedAccount2, _ := solana.NewRandomPrivateKey() + + newRemoteConfig := solBaseTokenPool.RemoteConfig{ + PoolAddresses: []solTestTokenPool.RemoteAddress{{Address: []byte{7, 8, 9}}}, + TokenAddress: solTestTokenPool.RemoteAddress{Address: []byte{10, 11, 12}}, + Decimals: 9, + } + newOutboundConfig := solBaseTokenPool.RateLimitConfig{ + Enabled: true, + Capacity: uint64(1000), + Rate: 1, + } + newInboundConfig := solBaseTokenPool.RateLimitConfig{ + Enabled: false, + Capacity: 0, + Rate: 0, + } + + e, err = commonchangeset.ApplyChangesetsV2(t, e, []commonchangeset.ConfiguredChangeSet{ + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.ConfigureTokenPoolAllowList), + ccipChangesetSolana.ConfigureTokenPoolAllowListConfig{ + SolChainSelector: solChain, + SolTokenPubKey: tokenAddress.String(), + PoolType: testCase.poolType, + Accounts: []solana.PublicKey{allowedAccount1.PublicKey(), allowedAccount2.PublicKey()}, + Enabled: true, + MCMSSolana: mcmsConfig, + }, + ), + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.RemoveFromTokenPoolAllowList), + ccipChangesetSolana.RemoveFromAllowListConfig{ + SolChainSelector: solChain, + SolTokenPubKey: tokenAddress.String(), + PoolType: testCase.poolType, + Accounts: []solana.PublicKey{allowedAccount1.PublicKey(), allowedAccount2.PublicKey()}, + MCMSSolana: mcmsConfig, + }, + ), + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.SetupTokenPoolForRemoteChain), + ccipChangesetSolana.RemoteChainTokenPoolConfig{ + SolChainSelector: solChain, + RemoteChainSelector: evmChain, + SolTokenPubKey: tokenAddress.String(), + RemoteConfig: newRemoteConfig, + InboundRateLimit: newInboundConfig, + OutboundRateLimit: newOutboundConfig, + PoolType: testCase.poolType, + MCMSSolana: mcmsConfig, + IsEdit: true, + }, + ), + }, + ) + require.NoError(t, err) + } + } +}