diff --git a/deployment/common/changeset/transfer_link_token.go b/deployment/common/changeset/transfer_link_token.go new file mode 100644 index 00000000000..e61dbecfb64 --- /dev/null +++ b/deployment/common/changeset/transfer_link_token.go @@ -0,0 +1,73 @@ +package changeset + +import ( + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/deployment" +) + +var _ deployment.ChangeSet[TransferLinkTokenConfig] = TransferLinkToken + +type Transfer struct { + To common.Address + Amount *big.Int +} + +type TransferLinkTokenConfig struct { + Transfers map[uint64]Transfer +} + +func (c TransferLinkTokenConfig) Validate() error { + for k, v := range c.Transfers { + if err := deployment.IsValidChainSelector(k); err != nil { + return err + } + + if v.To == (common.Address{}) { + return errors.New("to address must be set") + } + if v.Amount == nil || v.Amount.Sign() == -1 { + return errors.New("amount must be set and positive") + } + } + return nil +} + +// TransferLinkToken transfers link token to the to address on the chain identified by the chainSelector. +func TransferLinkToken(e deployment.Environment, config TransferLinkTokenConfig) (deployment.ChangesetOutput, error) { + if err := config.Validate(); err != nil { + return deployment.ChangesetOutput{}, err + } + + for chainSelector, transferConfig := range config.Transfers { + addresses, err := e.ExistingAddresses.AddressesForChain(chainSelector) + if err != nil { + return deployment.ChangesetOutput{}, err + } + + chain, ok := e.Chains[chainSelector] + if !ok { + return deployment.ChangesetOutput{}, fmt.Errorf("chain not found in environment") + } + + linkState, err := LoadLinkTokenState(chain, addresses) + if err != nil { + return deployment.ChangesetOutput{}, err + } + + tx, err := linkState.LinkToken.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(), + "chainSelector", chainSelector) + } + 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..6f7b589f62a --- /dev/null +++ b/deployment/common/changeset/transfer_link_token_test.go @@ -0,0 +1,138 @@ +package changeset_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + chainselectors "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + "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.TransferLinkTokenConfig + wantErr bool + wantErrMsg string + }{ + { + name: "valid config", + config: changeset.TransferLinkTokenConfig{ + Transfers: map[uint64]changeset.Transfer{ + chainselectors.ETHEREUM_TESTNET_SEPOLIA.Selector: { + To: common.HexToAddress("0x1"), + Amount: big.NewInt(1), + }, + }, + }, + wantErr: false, + }, + { + name: "missing to address", + config: changeset.TransferLinkTokenConfig{ + Transfers: map[uint64]changeset.Transfer{ + chainselectors.ETHEREUM_TESTNET_SEPOLIA.Selector: { + Amount: big.NewInt(1), + }, + }, + }, + wantErr: true, + wantErrMsg: "to address must be set", + }, + { + name: "missing amount", + config: changeset.TransferLinkTokenConfig{ + Transfers: map[uint64]changeset.Transfer{ + chainselectors.ETHEREUM_TESTNET_SEPOLIA.Selector: { + To: common.HexToAddress("0x1"), + }, + }, + }, + wantErr: true, + wantErrMsg: "amount must be set and positive", + }, + } + + 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.TransferLinkToken(env, + changeset.TransferLinkTokenConfig{ + Transfers: map[uint64]changeset.Transfer{ + chainSelectorId: { + To: receiver, + Amount: big.NewInt(30), + }, + }, + }) + 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) +}