ERC-20 на фронте: баланс, decimals, approve
ERC-20 — стандарт токенов. Учимся читать баланс с учётом decimals и понимать паттерн approve + transferFrom.
ERC-20 — стандарт взаимозаменяемых токенов: фиксированный набор функций (
balanceOf,transfer,approve,allowance,transferFrom), который реализуют тысячи токенов.
USDT, USDC, DAI, любой проектный токен — это ERC-20. Стандарт означает: один ABI подходит ко всем токенам. Научились работать с одним — работаете со всеми.
Баланс и decimals
balanceOf возвращает сырое число. Сколько это «настоящих» токенов — зависит от decimals. У большинства токенов 18, у USDC и USDT — 6. Игнорировать decimals = показать пользователю в миллион раз больше или меньше:
// сырой баланс и decimals читаются из контракта; здесь — чистый расчёт
function formatUnits(raw, decimals) {
const base = 10n ** BigInt(decimals);
const whole = raw / base;
const frac = (raw % base).toString().padStart(decimals, "0").replace(/0+$/, "");
return frac ? `${whole}.${frac}` : whole.toString();
}
console.log(formatUnits(1500000n, 6)); // USDC: 1.5
console.log(formatUnits(2500000000000000000n, 18)); // DAI: 2.5
console.log(formatUnits(1000000n, 18)); // 0.000000000001Вывод:
1.5 2.5 0.000000000001
Та же логика лежит в formatUnits/parseUnits из ethers — это обобщение formatEther на произвольные decimals.
Паттерн approve + transferFrom
Ключевая идея ERC-20, которую обязан понимать фронтендер. Прямой transfer переводит ваши токены сами. Но часто токены должен потратить другой контракт (биржа, стейкинг). Контракт не может взять ваши токены без разрешения. Поэтому:
- approve(spender, amount) — вы разрешаете контракту
spenderтратить доamountваших токенов. - transferFrom — теперь
spenderможет списать токены в пределах разрешения.
На фронте это значит две транзакции для одной операции «положить токены в пул»: сначала approve, потом основное действие. UX-ловушка: пользователь часто не понимает, зачем «два раза подтверждать».
// шаг 1: разрешить
const a = await token.approve(spenderAddress, amount);
await a.wait();
// шаг 2: spender (другой контракт) списывает
const b = await pool.deposit(amount); // внутри вызовет transferFromallowance — текущее разрешение
Перед approve полезно прочитать allowance(owner, spender) — сколько уже разрешено. Если разрешения хватает, второй approve не нужен (экономит газ и клик). Это стандартная проверка в DeFi-фронтах.
Как работает под капотом
approve записывает в контракт токена число: «адрес owner разрешил адресу spender тратить N». transferFrom при вызове проверяет это число, уменьшает его и переводит токены. Никакого «доступа к кошельку» — всё в рамках состояния контракта токена. Бесконечный approve (amount = 2^256-1) часто используют, чтобы не подтверждать каждый раз, но это риск безопасности: вредоносный контракт сможет вывести всё. Безопаснее approve ровно на нужную сумму.
Частые ошибки
- Не учитывать decimals. У USDC их 6, не 18 — баланс отобразится неверно.
- Делать approve, когда allowance уже достаточно. Лишняя транзакция и газ.
- Бездумный бесконечный approve. Удобно, но опасно; предлагайте approve на сумму.
Итоги
- ERC-20 — единый стандарт; один ABI на все токены.
- Всегда учитывайте
decimalsпри показе баланса. - Паттерн approve + transferFrom = две транзакции; проверяйте allowance заранее.