From 44d09d5c6476a95199eb991b991af18bd71a16a4 Mon Sep 17 00:00:00 2001 From: Terry Tata Date: Wed, 26 Feb 2025 18:43:49 -0500 Subject: [PATCH] liquidity --- .../ccip/changeset/solana/cs_token_pool.go | 222 +++++++++++++++++- .../changeset/solana/cs_token_pool_test.go | 112 ++++++++- ...ransfer_ccip_to_mcms_with_timelock_test.go | 1 - .../changeset/testhelpers/test_helpers.go | 1 - 4 files changed, 325 insertions(+), 11 deletions(-) diff --git a/deployment/ccip/changeset/solana/cs_token_pool.go b/deployment/ccip/changeset/solana/cs_token_pool.go index 965bd653c44..1edead96c0e 100644 --- a/deployment/ccip/changeset/solana/cs_token_pool.go +++ b/deployment/ccip/changeset/solana/cs_token_pool.go @@ -6,6 +6,9 @@ import ( "github.com/gagliardetto/solana-go" + "github.com/smartcontractkit/mcms" + mcmsTypes "github.com/smartcontractkit/mcms/types" + solBaseTokenPool "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/base_token_pool" solRouter "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" solBurnMintTokenPool "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/example_burnmint_token_pool" @@ -14,8 +17,6 @@ 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" @@ -43,7 +44,6 @@ func validatePoolDeployment(s ccipChangeset.SolCCIPChainState, poolType solTestT type TokenPoolConfig struct { ChainSelector uint64 PoolType solTestTokenPool.PoolType - Authority string TokenPubKey string } @@ -95,7 +95,6 @@ func AddTokenPool(e deployment.Environment, cfg TokenPoolConfig) (deployment.Cha chain := e.SolChains[cfg.ChainSelector] state, _ := ccipChangeset.LoadOnchainState(e) chainState := state.SolChains[cfg.ChainSelector] - authorityPubKey := solana.MustPublicKeyFromBase58(cfg.Authority) tokenPubKey := solana.MustPublicKeyFromBase58(cfg.TokenPubKey) tokenPool := solana.PublicKey{} @@ -132,7 +131,7 @@ func AddTokenPool(e deployment.Environment, cfg TokenPoolConfig) (deployment.Cha chainState.Router, poolConfigPDA, tokenPubKey, - authorityPubKey, // this is assumed to be chain.DeployerKey for now (owner of token pool) + chain.DeployerKey.PublicKey(), // this is assumed to be chain.DeployerKey for now (owner of token pool) solana.SystemProgramID, ).ValidateAndBuild() case solTestTokenPool.LockAndRelease_PoolType: @@ -141,7 +140,7 @@ func AddTokenPool(e deployment.Environment, cfg TokenPoolConfig) (deployment.Cha chainState.Router, poolConfigPDA, tokenPubKey, - authorityPubKey, // this is assumed to be chain.DeployerKey for now (owner of token pool) + chain.DeployerKey.PublicKey(), // this is assumed to be chain.DeployerKey for now (owner of token pool) solana.SystemProgramID, ).ValidateAndBuild() default: @@ -159,7 +158,7 @@ func AddTokenPool(e deployment.Environment, cfg TokenPoolConfig) (deployment.Cha tokenprogramID, poolSigner, tokenPubKey, - authorityPubKey, + chain.DeployerKey.PublicKey(), ) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate instructions: %w", err) @@ -833,3 +832,212 @@ func RemoveFromTokenPoolAllowList(e deployment.Environment, cfg RemoveFromAllowL e.Logger.Infow("Configured token pool allowlist", "token_pubkey", tokenPubKey.String()) return deployment.ChangesetOutput{}, nil } + +type LockReleaseLiquidityOpsConfig struct { + SolChainSelector uint64 + SolTokenPubKey string + SetCfg *SetLiquidityConfig + LiquidityCfg *LiquidityConfig + MCMSSolana *MCMSConfigSolana +} + +type SetLiquidityConfig struct { + Enabled bool +} +type LiquidityOperation int + +const ( + Provide LiquidityOperation = iota + Withdraw +) + +type LiquidityConfig struct { + Amount uint64 + RemoteTokenAccount solana.PublicKey + Type LiquidityOperation +} + +func (cfg LockReleaseLiquidityOpsConfig) 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, solTestTokenPool.LockAndRelease_PoolType, cfg.SolChainSelector); err != nil { + return err + } + + tokenPool := chainState.LockReleaseTokenPool + poolConfigAccount := solLockReleaseTokenPool.State{} + + // 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(), tokenPool, err) + } + return nil +} + +func LockReleaseLiquidityOps(e deployment.Environment, cfg LockReleaseLiquidityOpsConfig) (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] + tokenPool := chainState.LockReleaseTokenPool + + var err error + tokenPoolUsingMcms := cfg.MCMSSolana != nil && cfg.MCMSSolana.TokenPoolOwnedByTimelock + // validate ownership + var authority solana.PublicKey + + solLockReleaseTokenPool.SetProgramID(tokenPool) + programID := tokenPool + contractType := ccipChangeset.LockReleaseTokenPool + tokenPubKey := solana.MustPublicKeyFromBase58(cfg.SolTokenPubKey) + poolConfigPDA, _ := solTokenUtil.TokenPoolConfigAddress(tokenPubKey, tokenPool) + 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() + } + ixns := make([]solana.Instruction, 0) + if cfg.SetCfg != nil { + ix, err := solLockReleaseTokenPool.NewSetCanAcceptLiquidityInstruction( + cfg.SetCfg.Enabled, + poolConfigPDA, + authority, + ).ValidateAndBuild() + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate instructions: %w", err) + } + ixns = append(ixns, ix) + } + if cfg.LiquidityCfg != nil { + tokenProgram, _ := chainState.TokenToTokenProgram(tokenPubKey) + poolSigner, _ := solTokenUtil.TokenPoolSignerAddress(tokenPubKey, tokenPool) + poolConfigAccount := solLockReleaseTokenPool.State{} + _ = chain.GetAccountDataBorshInto(context.Background(), poolConfigPDA, &poolConfigAccount) + switch cfg.LiquidityCfg.Type { + case Provide: + ix, err := solLockReleaseTokenPool.NewProvideLiquidityInstruction( + cfg.LiquidityCfg.Amount, + poolConfigPDA, + tokenProgram, + tokenPubKey, + poolSigner, + poolConfigAccount.Config.PoolTokenAccount, + cfg.LiquidityCfg.RemoteTokenAccount, + authority, + ).ValidateAndBuild() + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate instructions: %w", err) + } + ixns = append(ixns, ix) + case Withdraw: + ix, err := solLockReleaseTokenPool.NewWithdrawLiquidityInstruction( + cfg.LiquidityCfg.Amount, + poolConfigPDA, + tokenProgram, + tokenPubKey, + poolSigner, + poolConfigAccount.Config.PoolTokenAccount, + cfg.LiquidityCfg.RemoteTokenAccount, + authority, + ).ValidateAndBuild() + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate instructions: %w", err) + } + ixns = append(ixns, ix) + } + } + + if tokenPoolUsingMcms { + txns := make([]mcmsTypes.Transaction, 0) + for _, ixn := range ixns { + tx, err := BuildMCMSTxn(ixn, programID.String(), contractType) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create transaction: %w", err) + } + txns = append(txns, *tx) + } + proposal, err := BuildProposalsForTxns( + e, cfg.SolChainSelector, "proposal to RemoveFromTokenPoolAllowList in Solana", cfg.MCMSSolana.MCMS.MinDelay, txns) + 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(ixns) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm instructions: %w", err) + } + return deployment.ChangesetOutput{}, nil +} + +type TokenApproveCheckedConfig struct { + Amount uint64 + Decimals uint8 + ChainSelector uint64 + TokenPubKey string + PoolType solTestTokenPool.PoolType + SourceATA solana.PublicKey +} + +func TokenApproveChecked(e deployment.Environment, cfg TokenApproveCheckedConfig) (deployment.ChangesetOutput, error) { + chain := e.SolChains[cfg.ChainSelector] + state, _ := ccipChangeset.LoadOnchainState(e) + chainState := state.SolChains[cfg.ChainSelector] + + tokenPubKey := solana.MustPublicKeyFromBase58(cfg.TokenPubKey) + tokenPool := solana.PublicKey{} + + if cfg.PoolType == solTestTokenPool.BurnAndMint_PoolType { + tokenPool = chainState.BurnMintTokenPool + solBurnMintTokenPool.SetProgramID(tokenPool) + } else if cfg.PoolType == solTestTokenPool.LockAndRelease_PoolType { + tokenPool = chainState.LockReleaseTokenPool + solLockReleaseTokenPool.SetProgramID(tokenPool) + } + + // verified + tokenprogramID, _ := chainState.TokenToTokenProgram(tokenPubKey) + poolSigner, _ := solTokenUtil.TokenPoolSignerAddress(tokenPubKey, tokenPool) + + ix, err := solTokenUtil.TokenApproveChecked( + cfg.Amount, + cfg.Decimals, + tokenprogramID, + cfg.SourceATA, + tokenPubKey, + poolSigner, + chain.DeployerKey.PublicKey(), + solana.PublicKeySlice{}, + ) + if err != nil { + return deployment.ChangesetOutput{}, err + } + + // confirm instructions + if err = chain.Confirm([]solana.Instruction{ix}); err != nil { + e.Logger.Errorw("Failed to confirm instructions for TokenApproveChecked", "chain", chain.String(), "err", err) + return deployment.ChangesetOutput{}, err + } + e.Logger.Infow("TokenApproveChecked on", "chain", cfg.ChainSelector, "for token", 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 index c67b5f9bd12..fd2e10d67c8 100644 --- a/deployment/ccip/changeset/solana/cs_token_pool_test.go +++ b/deployment/ccip/changeset/solana/cs_token_pool_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/gagliardetto/solana-go" + solRpc "github.com/gagliardetto/solana-go/rpc" "github.com/stretchr/testify/require" solBaseTokenPool "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/base_token_pool" @@ -15,6 +16,7 @@ import ( ccipChangeset "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" ccipChangesetSolana "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/solana" + changeset_solana "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/solana" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" "github.com/smartcontractkit/chainlink/deployment" @@ -41,6 +43,44 @@ func doTestTokenPool(t *testing.T, mcms bool) { require.NoError(t, err) state, err := ccipChangeset.LoadOnchainStateSolana(e) require.NoError(t, err) + // MintTo does not support native tokens + deployerKey := e.SolChains[solChain].DeployerKey.PublicKey() + testUser, _ := solana.NewRandomPrivateKey() + testUserPubKey := testUser.PublicKey() + e, err = commonchangeset.ApplyChangesetsV2(t, e, []commonchangeset.ConfiguredChangeSet{ + commonchangeset.Configure( + // deployer creates ATA for itself and testUser + deployment.CreateLegacyChangeSet(ccipChangesetSolana.CreateSolanaTokenATA), + ccipChangesetSolana.CreateSolanaTokenATAConfig{ + ChainSelector: solChain, + TokenPubkey: newTokenAddress, + TokenProgram: ccipChangeset.SPL2022Tokens, + ATAList: []string{deployerKey.String(), testUserPubKey.String()}, + }, + ), + commonchangeset.Configure( + // deployer mints token to itself and testUser + deployment.CreateLegacyChangeSet(changeset_solana.MintSolanaToken), + ccipChangesetSolana.MintSolanaTokenConfig{ + ChainSelector: solChain, + TokenPubkey: newTokenAddress.String(), + AmountToAddress: map[string]uint64{ + deployerKey.String(): uint64(1000), + testUserPubKey.String(): uint64(1000), + }, + }, + ), + }, + ) + require.NoError(t, err) + testUserATA, _, err := solTokenUtil.FindAssociatedTokenAddress(solana.Token2022ProgramID, newTokenAddress, testUserPubKey) + require.NoError(t, err) + deployerATA, _, err := solTokenUtil.FindAssociatedTokenAddress( + solana.Token2022ProgramID, + newTokenAddress, + e.SolChains[solChain].DeployerKey.PublicKey(), + ) + require.NoError(t, err) mcmsConfigured := false remoteConfig := solBaseTokenPool.RemoteConfig{ PoolAddresses: []solTestTokenPool.RemoteAddress{{Address: []byte{1, 2, 3}}}, @@ -87,8 +127,6 @@ func doTestTokenPool(t *testing.T, mcms bool) { 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( @@ -192,6 +230,76 @@ func doTestTokenPool(t *testing.T, mcms bool) { }, ) require.NoError(t, err) + if testCase.poolType == solTestTokenPool.LockAndRelease_PoolType && tokenAddress == newTokenAddress { + e, err = commonchangeset.ApplyChangesetsV2(t, e, []commonchangeset.ConfiguredChangeSet{ + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.TokenApproveChecked), + ccipChangesetSolana.TokenApproveCheckedConfig{ + Amount: 100, + Decimals: 9, + ChainSelector: solChain, + TokenPubKey: tokenAddress.String(), + PoolType: testCase.poolType, + SourceATA: deployerATA, + }, + ), + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.LockReleaseLiquidityOps), + ccipChangesetSolana.LockReleaseLiquidityOpsConfig{ + SolChainSelector: solChain, + SolTokenPubKey: tokenAddress.String(), + SetCfg: &ccipChangesetSolana.SetLiquidityConfig{ + Enabled: true, + }, + MCMSSolana: mcmsConfig, + }, + ), + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.LockReleaseLiquidityOps), + ccipChangesetSolana.LockReleaseLiquidityOpsConfig{ + SolChainSelector: solChain, + SolTokenPubKey: tokenAddress.String(), + LiquidityCfg: &ccipChangesetSolana.LiquidityConfig{ + Amount: 100, + RemoteTokenAccount: deployerATA, + Type: ccipChangesetSolana.Provide, + }, + MCMSSolana: mcmsConfig, + }, + ), + commonchangeset.Configure( + deployment.CreateLegacyChangeSet(ccipChangesetSolana.LockReleaseLiquidityOps), + ccipChangesetSolana.LockReleaseLiquidityOpsConfig{ + SolChainSelector: solChain, + SolTokenPubKey: tokenAddress.String(), + LiquidityCfg: &ccipChangesetSolana.LiquidityConfig{ + Amount: 50, + RemoteTokenAccount: testUserATA, + Type: ccipChangesetSolana.Withdraw, + }, + MCMSSolana: mcmsConfig, + }, + ), + }, + ) + require.NoError(t, err) + outDec, outVal, err := solTokenUtil.TokenBalance(e.GetContext(), e.SolChains[solChain].Client, deployerATA, solRpc.CommitmentConfirmed) + require.NoError(t, err) + require.Equal(t, int(900), outVal) + require.Equal(t, 9, int(outDec)) + + outDec, outVal, err = solTokenUtil.TokenBalance(e.GetContext(), e.SolChains[solChain].Client, testUserATA, solRpc.CommitmentConfirmed) + require.NoError(t, err) + require.Equal(t, int(1050), outVal) + require.Equal(t, 9, int(outDec)) + + err = e.SolChains[solChain].GetAccountDataBorshInto(ctx, poolConfigPDA, &configAccount) + require.NoError(t, err) + outDec, outVal, err = solTokenUtil.TokenBalance(e.GetContext(), e.SolChains[solChain].Client, configAccount.Config.PoolTokenAccount, solRpc.CommitmentConfirmed) + require.NoError(t, err) + require.Equal(t, int(50), outVal) + require.Equal(t, 9, int(outDec)) + } } } } 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 986a06d90f8..b0c7e5e5721 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 @@ -297,7 +297,6 @@ func prepareEnvironmentForOwnershipTransfer(t *testing.T) (deployment.Environmen ChainSelector: solChain1, TokenPubKey: tokenAddress.String(), PoolType: test_token_pool.LockAndRelease_PoolType, - Authority: e.SolChains[solChain1].DeployerKey.PublicKey().String(), }, ), }) diff --git a/deployment/ccip/changeset/testhelpers/test_helpers.go b/deployment/ccip/changeset/testhelpers/test_helpers.go index 45e3a2a8b88..98c85afb0cc 100644 --- a/deployment/ccip/changeset/testhelpers/test_helpers.go +++ b/deployment/ccip/changeset/testhelpers/test_helpers.go @@ -909,7 +909,6 @@ func DeployTransferableTokenSolana( ChainSelector: solChainSel, TokenPubKey: solTokenAddress.String(), PoolType: solTestTokenPool.BurnAndMint_PoolType, - Authority: solDeployerKey.String(), }, ), )