китаев.tech

ERC-20: transfer

Как отправить ERC-20 токены через ethers v6: подготовить amount через parseUnits, подписать transfer и дождаться подтверждения транзакции.

transfer отправляет токены

transfer(to, amount) — функция ERC-20, которая переводит токены с адреса отправителя на адрес получателя.

Это write-вызов. Он меняет State Ethereum, поэтому нужен signer, ETH для gas и подтверждённая транзакция.


Отправляем ERC-20 в локальной сети

Создайте проект:

bash
mkdir erc20-transfer-example
cd erc20-transfer-example

npm init -y
npm pkg set type="module"
npm install ethers solc tsx
npm install --save-dev hardhat typescript @types/node
mkdir contracts scripts

Создайте файл hardhat.config.ts:

typescript
import { defineConfig } from "hardhat/config";

export default defineConfig({
  solidity: {
    version: "0.8.20",
  },
});

Создайте файл contracts/DemoToken.sol:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract DemoToken {
    string public name = "Demo Token";
    string public symbol = "DEMO";
    uint8 public decimals = 18;
    uint256 public totalSupply;

    mapping(address => uint256) public balanceOf;

    event Transfer(address indexed from, address indexed to, uint256 value);

    constructor(uint256 initialSupply) {
        totalSupply = initialSupply;
        balanceOf[msg.sender] = initialSupply;
        emit Transfer(address(0), msg.sender, initialSupply);
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        require(to != address(0), "transfer to zero address");
        require(balanceOf[msg.sender] >= amount, "insufficient balance");

        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;

        emit Transfer(msg.sender, to, amount);
        return true;
    }
}

Создайте файл scripts/transfer-token.ts:

typescript
import { readFileSync } from "node:fs";
import { ethers } from "ethers";
import solc from "solc";

const source = readFileSync("contracts/DemoToken.sol", "utf8");

const input = {
  language: "Solidity",
  sources: {
    "DemoToken.sol": {
      content: source,
    },
  },
  settings: {
    outputSelection: {
      "*": {
        "*": ["abi", "evm.bytecode.object"],
      },
    },
  },
};

const output = JSON.parse(solc.compile(JSON.stringify(input)));
const contract = output.contracts["DemoToken.sol"].DemoToken;

const abi = contract.abi;
const bytecode = `0x${contract.evm.bytecode.object}`;

const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");

// Первый тестовый аккаунт Hardhat. Не используйте этот ключ в mainnet.
const sender = new ethers.Wallet(
  "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
  provider,
);

// Второй тестовый аккаунт Hardhat.
const recipient = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8";

const factory = new ethers.ContractFactory(abi, bytecode, sender);
const initialSupply = ethers.parseUnits("1000", 18);
const token = await factory.deploy(initialSupply);

await token.waitForDeployment();

const tokenAddress = await token.getAddress();
const decimals = await token.decimals();
const symbol = await token.symbol();

const senderBalanceBefore = await token.balanceOf(sender.address);
const recipientBalanceBefore = await token.balanceOf(recipient);

console.log("Token address:", tokenAddress);
console.log("Sender before:", ethers.formatUnits(senderBalanceBefore, decimals), symbol);
console.log(
  "Recipient before:",
  ethers.formatUnits(recipientBalanceBefore, decimals),
  symbol,
);

const amount = ethers.parseUnits("12.5", decimals);
const tx = await token.transfer(recipient, amount);

console.log("Transfer hash:", tx.hash);

const receipt = await tx.wait();

if (!receipt || receipt.status !== 1) {
  throw new Error("Транзакция transfer() не была успешно выполнена");
}

const senderBalanceAfter = await token.balanceOf(sender.address);
const recipientBalanceAfter = await token.balanceOf(recipient);

console.log("Block:", receipt.blockNumber);
console.log("Gas used:", receipt.gasUsed.toString());
console.log("Sender after:", ethers.formatUnits(senderBalanceAfter, decimals), symbol);
console.log(
  "Recipient after:",
  ethers.formatUnits(recipientBalanceAfter, decimals),
  symbol,
);

Запустите локальную сеть Hardhat в отдельном терминале:

bash
npx hardhat node

Во втором терминале выполните скрипт:

bash
npx tsx scripts/transfer-token.ts

Скрипт задеплоит тестовый ERC-20, отправит 12.5 DEMO на второй аккаунт и прочитает балансы после подтверждения.


Как это работает

new ethers.Wallet(privateKey, provider) — создаёт signer. Только signer может подписать transfer.

ethers.parseUnits("1000", 18) — переводит 1000 DEMO в raw-значение для контракта.

new ethers.ContractFactory(abi, bytecode, sender) — деплоит токен от имени отправителя.

token.balanceOf(sender.address) — читает баланс без транзакции и без gas.

ethers.parseUnits("12.5", decimals) — готовит amount в минимальных единицах токена.

token.transfer(recipient, amount) — отправляет write-транзакцию в ERC-20 контракт.

tx.wait() — ждёт, пока транзакцию включат в блок и вернут receipt.

receipt.status !== 1 — проверяет, что EVM не откатила выполнение.


transfer списывает токены с msg.sender

ERC-20 transfer всегда берёт токены с адреса, который подписал транзакцию.

В примере это sender, потому что контракт подключён к signer:

typescript
const token = await factory.deploy(initialSupply);
const tx = await token.transfer(recipient, amount);

Если подключить тот же контракт к другому signer, transfer попытается списать токены уже с другого адреса. Для перевода чужих токенов нужен не transfer, а связка approve + transferFrom.


Почему нужен parseUnits

Контракт не принимает "12.5" как число токенов.

ERC-20 работает с uint256, поэтому в транзакцию нужно передать raw-значение:

text
12.5 DEMO при decimals = 18
= 12500000000000000000

ethers.parseUnits("12.5", decimals) делает это преобразование без потери точности.


Частые ошибки

Передавать 12.5 как JavaScript number → используйте строку и ethers.parseUnits("12.5", decimals).

Думать, что tx.hash означает успешный перевод → hash означает только отправку транзакции. Проверяйте результат через tx.wait() и receipt.status.

Вызывать transfer через provider → provider умеет читать сеть, но не подписывает транзакции. Для transfer нужен signer.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.