approve, transferFrom и allowance
approve + transferFrom — это «доверенность» на токены: вы разрешаете другому адресу списывать с вашего баланса до определённой суммы.
Без этого паттерна не было бы ни Uniswap, ни стейкинга. Но именно он породил классическую уязвимость гонки allowance.
Прямой transfer двигает токены сами. Но что, если контракт-биржа должен забрать ваши токены по вашей команде? Вы не можете отдать ему приватный ключ. Решение — двухшаговый паттерн: сначала вы вызываете approve(spender, amount), разрешая spender тратить до amount ваших токенов; затем spender вызывает transferFrom(you, recipient, amount) и списывает в пределах разрешения. Текущее разрешение хранится в allowance[owner][spender].
ШАГ 1: alice.approve(dex, 50)
allowance[alice][dex] = 50
ШАГ 2: dex.transferFrom(alice, pool, 50)
проверка: allowance[alice][dex] >= 50 ?
balanceOf[alice] -= 50
balanceOf[pool] += 50
allowance[alice][dex] -= 50 -> 0
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract MiniToken {
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
return true;
}
function transferFrom(address from, address to, uint256 amount)
external returns (bool)
{
uint256 allowed = allowance[from][msg.sender];
require(allowed >= amount, "allowance too low");
require(balanceOf[from] >= amount, "balance too low");
if (allowed != type(uint256).max) {
allowance[from][msg.sender] = allowed - amount; // эффект
}
balanceOf[from] -= amount;
balanceOf[to] += amount;
return true;
}
}
Как работает под капотом (EVM/газ)
allowance — это вложенный маппинг «владелец → спендер → сумма». При transferFrom спендер (msg.sender) обязан иметь достаточное разрешение, иначе revert. Многие токены не уменьшают allowance, если оно равно type(uint256).max — это «бесконечное разрешение», экономит газ на повторных списаниях, но повышает риски. Каждое из этих изменений — запись в storage, поэтому за approve и transferFrom платят газ.
# Та же логика на Python: approve + transferFrom + allowance
from collections import defaultdict
balance_of = defaultdict(int, {"alice": 100})
allowance = defaultdict(lambda: defaultdict(int))
def approve(owner, spender, amount):
allowance[owner][spender] = amount
def transfer_from(spender, frm, to, amount):
assert allowance[frm][spender] >= amount, "REVERT: allowance too low"
assert balance_of[frm] >= amount, "REVERT: balance too low"
allowance[frm][spender] -= amount # эффект до перевода
balance_of[frm] -= amount
balance_of[to] += amount
approve("alice", "dex", 50) # шаг 1
transfer_from("dex", "alice", "pool", 50) # шаг 2
print("alice:", balance_of["alice"], "pool:", balance_of["pool"])
print("осталось разрешения:", allowance["alice"]["dex"]) # 0
«Та же логика на Python ▶». Спендер списывает только в рамках выданного разрешения, и оно уменьшается на потраченную сумму — точно как в ERC-20.
Частые ошибки и уязвимости
- Гонка allowance: если менять разрешение со старого ненулевого на новое одним
approve, спендер может в момент смены списать и старую, и новую сумму. Классический способ снизить риск — сначала обнулить (approve(spender, 0)), затем выставить новое значение; современные токены добавляютincreaseAllowance/decreaseAllowance. - Выдавать «бесконечное» разрешение (
max) недоверенным контрактам — при их взломе утекут все ваши токены. - Забывать уменьшать allowance в
transferFrom— спендер сможет списать многократно.
Best practices
- Минимизируйте разрешения: выдавайте ровно столько, сколько нужно операции, а не
max. - Регулярно отзывайте старые approve у контрактов, которыми не пользуетесь.
- В коде — порядок «проверка → уменьшение allowance → перевод» (эффекты до взаимодействий).
Итоги
approve/transferFrom — это контролируемая доверенность через allowance, основа DeFi. Но она несёт риск гонки разрешений и опасность «бесконечных» approve. Дальше — невзаимозаменяемые токены ERC-721 (NFT).