From 4ee2d8565090535af43b831befd99572ab51ceda Mon Sep 17 00:00:00 2001 From: Graham Goh Date: Sat, 30 Nov 2024 17:11:25 +1100 Subject: [PATCH] feat(deployment): add link token transfer changeset Added a new changeset in shared package which transfer the link token. JIRA: https://smartcontract-it.atlassian.net/browse/DPA-1314 --- .../common/changeset/transfer_link_token.go | 68 +++++++ .../changeset/transfer_link_token_test.go | 179 ++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 deployment/common/changeset/transfer_link_token.go create mode 100644 deployment/common/changeset/transfer_link_token_test.go diff --git a/deployment/common/changeset/transfer_link_token.go b/deployment/common/changeset/transfer_link_token.go new file mode 100644 index 00000000000..abc52a9b71e --- /dev/null +++ b/deployment/common/changeset/transfer_link_token.go @@ -0,0 +1,68 @@ +package changeset + +import ( + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/smartcontractkit/chainlink/deployment" +) + +var _ deployment.ChangeSet[TransferLinkTokenBatchConfig] = TransferLinkTokenBatch + +type TokenTransferer interface { + Transfer(opts *bind.TransactOpts, to common.Address, amount *big.Int) (*gethtypes.Transaction, error) +} + +type TransferLinkTokenConfig struct { + To common.Address + Amount *big.Int + Transferer TokenTransferer +} + +type TransferLinkTokenBatchConfig struct { + Transfers map[uint64]TransferLinkTokenConfig +} + +func (cfg TransferLinkTokenBatchConfig) Validate() error { + for k, c := range cfg.Transfers { + if err := deployment.IsValidChainSelector(k); err != nil { + return err + } + if c.To == (common.Address{}) { + return errors.New("to address must be set") + } + if c.Amount == nil || c.Amount.Sign() == -1 { + return errors.New("amount must be set and positive") + } + if c.Transferer == nil { + return errors.New("transferer must be set") + } + } + + return nil +} + +// TransferLinkTokenBatch transfers link token to the to address from the transferer in a batch. +func TransferLinkTokenBatch(e deployment.Environment, config TransferLinkTokenBatchConfig) (deployment.ChangesetOutput, error) { + if err := config.Validate(); err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("invalid transfer link token config: %v", err) + } + + for chainSelector, transferConfig := range config.Transfers { + chain, ok := e.Chains[chainSelector] + if !ok { + return deployment.ChangesetOutput{}, fmt.Errorf("chain not found in environment") + } + tx, err := transferConfig.Transferer.Transfer(chain.DeployerKey, transferConfig.To, transferConfig.Amount) + if _, err = deployment.ConfirmIfNoError(chain, tx, err); err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm transfer link token to %s: %v", transferConfig.To, err) + } + e.Logger.Infow("Transferred LINK", "to", transferConfig.To, "amount", transferConfig.Amount, "txHash", tx.Hash().Hex()) + } + return deployment.ChangesetOutput{}, nil +} diff --git a/deployment/common/changeset/transfer_link_token_test.go b/deployment/common/changeset/transfer_link_token_test.go new file mode 100644 index 00000000000..f993b498680 --- /dev/null +++ b/deployment/common/changeset/transfer_link_token_test.go @@ -0,0 +1,179 @@ +package changeset_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + chainselectors "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/common/types" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/link_token" +) + +func TestTransferLinkToken_Validate(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + config changeset.TransferLinkTokenBatchConfig + wantErr bool + wantErrMsg string + }{ + { + name: "valid config", + config: changeset.TransferLinkTokenBatchConfig{ + Transfers: map[uint64]changeset.TransferLinkTokenConfig{ + chainselectors.ETHEREUM_TESTNET_SEPOLIA.Selector: { + To: common.HexToAddress("0x1"), + Amount: big.NewInt(1), + Transferer: mockTokenTransferer{}, + }, + }, + }, + wantErr: false, + }, + { + name: "missing to address", + config: changeset.TransferLinkTokenBatchConfig{ + Transfers: map[uint64]changeset.TransferLinkTokenConfig{ + chainselectors.ETHEREUM_TESTNET_SEPOLIA.Selector: { + Amount: big.NewInt(1), + Transferer: mockTokenTransferer{}, + }, + }, + }, + wantErr: true, + wantErrMsg: "to address must be set", + }, + { + name: "missing amount", + config: changeset.TransferLinkTokenBatchConfig{ + Transfers: map[uint64]changeset.TransferLinkTokenConfig{ + chainselectors.ETHEREUM_TESTNET_SEPOLIA.Selector: { + To: common.HexToAddress("0x1"), + Transferer: mockTokenTransferer{}, + }, + }, + }, + wantErr: true, + wantErrMsg: "amount must be set and positive", + }, + { + name: "missing transferer", + config: changeset.TransferLinkTokenBatchConfig{ + Transfers: map[uint64]changeset.TransferLinkTokenConfig{ + chainselectors.ETHEREUM_TESTNET_SEPOLIA.Selector: { + To: common.HexToAddress("0x1"), + Amount: big.NewInt(1), + Transferer: nil, + }, + }, + }, + wantErr: true, + wantErrMsg: "transferer must be set", + }, + { + name: "invalid chain selector", + config: changeset.TransferLinkTokenBatchConfig{ + Transfers: map[uint64]changeset.TransferLinkTokenConfig{ + 0: { + To: common.HexToAddress("0x1"), + Amount: big.NewInt(1), + Transferer: mockTokenTransferer{}, + }, + }, + }, + wantErr: true, + wantErrMsg: "chain selector must be set", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + err := test.config.Validate() + if test.wantErr { + if test.wantErrMsg != "" { + assert.Contains(t, err.Error(), test.wantErrMsg) + } + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestTransferLinkToken(t *testing.T) { + t.Parallel() + + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 1, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + chainSelectorId := env.AllChainSelectors()[0] + + chain := env.Chains[chainSelectorId] + deployer := chain.DeployerKey + tokenContract, err := deployment.DeployContract(lggr, chain, env.ExistingAddresses, + func(chain deployment.Chain) deployment.ContractDeploy[*link_token.LinkToken] { + tokenAddress, tx, token, err2 := link_token.DeployLinkToken( + deployer, + chain.Client, + ) + return deployment.ContractDeploy[*link_token.LinkToken]{ + tokenAddress, token, tx, deployment.NewTypeAndVersion(types.LinkToken, deployment.Version1_0_0), err2, + } + }) + require.NoError(t, err) + + tx, err := tokenContract.Contract.GrantMintRole(deployer, deployer.From) + require.NoError(t, err) + _, err = chain.Confirm(tx) + + tx, err = tokenContract.Contract.Mint(deployer, deployer.From, big.NewInt(100)) + require.NoError(t, err) + _, err = chain.Confirm(tx) + require.NoError(t, err) + + receiver := common.HexToAddress("0x1") + _, err = changeset.TransferLinkTokenBatch(env, changeset.TransferLinkTokenBatchConfig{ + Transfers: map[uint64]changeset.TransferLinkTokenConfig{ + chainSelectorId: { + To: receiver, + Amount: big.NewInt(30), + Transferer: tokenContract.Contract, + }, + }, + }) + require.NoError(t, err) + + balance, err := tokenContract.Contract.BalanceOf(nil, deployer.From) + require.NoError(t, err) + require.Equal(t, big.NewInt(70), balance) + + balance, err = tokenContract.Contract.BalanceOf(nil, receiver) + require.NoError(t, err) + require.Equal(t, big.NewInt(30), balance) +} + +type mockTokenTransferer struct{} + +func (m mockTokenTransferer) Transfer(opts *bind.TransactOpts, + to common.Address, amount *big.Int) (*gethtypes.Transaction, error) { + return nil, nil +}