Разбор контракта ERC-20
Введение
Одно из самых распространенных применений Эфириума — создание группой людей торгуемого токена, в некотором смысле их собственной валюты. Эти токены обычно следуют стандарту ERC-20. Этот стандарт позволяет создавать инструменты, такие как пулы ликвидности и кошельки, которые работают со всеми токенами ERC-20. В этой статье мы проанализируем реализацию ERC20 на Solidity от ОпенЗеппелин (opens in a new tab), а также определение интерфейса (opens in a new tab).
Это аннотированный исходный код. Если вы хотите реализовать ERC-20, прочтите это руководство (opens in a new tab).
Интерфейс
Цель стандарта, такого как ERC-20, — позволить множеству реализаций токенов быть интероперабельными в различных приложениях, таких как кошельки и децентрализованные биржи. Для достижения этого мы создаем интерфейс (opens in a new tab). Любой код, которому необходимо использовать контракт токена, может использовать те же определения в интерфейсе и быть совместимым со всеми контрактами токенов, которые его используют, будь то кошелек, такой как МетаМаск, децентрализованное приложение (dapp), такое как Etherscan, или другой контракт, такой как пул ликвидности.
Если вы опытный программист, вы, вероятно, помните подобные конструкции в Java (opens in a new tab) или даже в заголовочных файлах C (opens in a new tab).
Это определение интерфейса ERC-20 (opens in a new tab) от ОпенЗеппелин. Это перевод человекочитаемого стандарта (opens in a new tab) в код Solidity. Конечно, сам интерфейс не определяет, как что-либо делать. Это объясняется в исходном коде контракта ниже.
// SPDX-License-Identifier: MIT
Файлы Solidity должны включать идентификатор лицензии. Вы можете посмотреть список лицензий здесь (opens in a new tab). Если вам нужна другая лицензия, просто объясните это в комментариях.
pragma solidity >=0.6.0 <0.8.0;
Язык Solidity все еще быстро развивается, и новые версии могут быть несовместимы со старым кодом (см. здесь (opens in a new tab)). Поэтому хорошей идеей будет указать не только минимальную версию языка, но и максимальную — последнюю, с которой вы тестировали код.
/**
* @dev Интерфейс стандарта ERC-20, как определено в EIP.
*/
@dev в комментарии является частью формата NatSpec (opens in a new tab), используемого для создания документации из исходного кода.
interface IERC20 {
По соглашению имена интерфейсов начинаются с I.
/**
* @dev Возвращает количество существующих токенов.
*/
function totalSupply() external view returns (uint256);
Эта функция является external, что означает, что ее можно вызвать только извне контракта (opens in a new tab).
Она возвращает общее предложение токенов в контракте. Это значение возвращается с использованием самого распространенного типа в Эфириуме — беззнакового 256-битного целого числа (256 бит — это собственный размер слова EVM). Эта функция также является view, что означает, что она не изменяет состояние, поэтому ее можно выполнить на одном узле, вместо того чтобы каждый узел в блокчейне запускал ее. Такая функция не генерирует транзакцию и не требует затрат газа.
Примечание: В теории может показаться, что создатель контракта может сжульничать, вернув меньшее общее предложение, чем реальное значение, из-за чего каждый токен будет казаться более ценным, чем он есть на самом деле. Однако это опасение игнорирует истинную природу блокчейна. Все, что происходит в блокчейне, может быть проверено каждым узлом. Для достижения этого машинный код и хранилище каждого контракта доступны на каждом узле. Хотя вы не обязаны публиковать код Solidity для вашего контракта, никто не воспримет вас всерьез, если вы не опубликуете исходный код и версию Solidity, с которой он был скомпилирован, чтобы его можно было сверить с предоставленным вами машинным кодом. Например, посмотрите этот контракт (opens in a new tab).
/**
* @dev Возвращает количество токенов, принадлежащих Аккаунту `account`.
*/
function balanceOf(address account) external view returns (uint256);
Как следует из названия, balanceOf возвращает баланс аккаунта. Аккаунты Эфириума идентифицируются в Solidity с помощью типа address, который содержит 160 бит.
Она также является external и view.
/**
* @dev Перемещает `amount` токенов с Аккаунта вызывающего на `recipient`.
*
* Возвращает логическое значение, указывающее, была ли операция успешной.
*
* Генерирует событие {Transfer}.
*/
function transfer(address recipient, uint256 amount) external returns (bool);
Функция transfer переводит токены от вызывающего к другому адресу. Это влечет за собой изменение состояния, поэтому она не является view.
Когда пользователь вызывает эту функцию, она создает транзакцию и требует затрат газа. Она также генерирует событие Transfer, чтобы проинформировать всех в блокчейне об этом событии.
Функция имеет два типа вывода для двух разных типов вызывающих:
- Пользователи, которые вызывают функцию напрямую из пользовательского интерфейса. Обычно пользователь отправляет транзакцию и не ждет ответа, что может занять неопределенное количество времени. Пользователь может увидеть, что произошло, посмотрев квитанцию транзакции (которая идентифицируется через хеш транзакции) или поискав событие
Transfer. - Другие контракты, которые вызывают функцию как часть общей транзакции. Эти контракты получают результат немедленно, потому что они выполняются в той же транзакции, поэтому они могут использовать возвращаемое значение функции.
Такой же тип вывода создается другими функциями, которые изменяют состояние контракта.
Разрешения позволяют аккаунту тратить некоторые токены, принадлежащие другому владельцу. Это полезно, например, для контрактов, которые выступают в роли продавцов. Контракты не могут отслеживать события, поэтому, если бы покупатель перевел токены напрямую на контракт продавца, этот контракт не узнал бы, что ему заплатили. Вместо этого покупатель разрешает контракту продавца потратить определенную сумму, и продавец переводит эту сумму. Это делается через функцию, которую вызывает контракт продавца, чтобы контракт продавца мог узнать, была ли операция успешной.
/**
* @dev Возвращает оставшееся количество токенов, которое `spender` сможет
* потратить от имени `owner` через {transferFrom}. По умолчанию это
* ноль.
*
* Это значение изменяется при вызове {approve} или {transferFrom}.
*/
function allowance(address owner, address spender) external view returns (uint256);
Функция allowance позволяет любому запросить и увидеть, какое разрешение один адрес (owner) дает другому адресу (spender) на трату.
/**
* @dev Устанавливает `amount` в качестве разрешения для `spender` на токены вызывающего.
*
* Возвращает логическое значение, указывающее, была ли операция успешной.
*
* ВАЖНО: Имейте в виду, что изменение разрешения с помощью этого метода несет риск
* того, что кто-то может использовать как старое, так и новое разрешение из-за неудачного
* порядка транзакций. Одно из возможных решений для смягчения этого состояния
* гонки — сначала уменьшить разрешение для spender до 0, а затем установить
* желаемое значение:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Генерирует событие {Approval}.
*/
function approve(address spender, uint256 amount) external returns (bool);
Функция approve создает разрешение. Обязательно прочтите сообщение о том, как этим можно злоупотребить. В Эфириуме вы контролируете порядок своих собственных транзакций, но вы не можете контролировать порядок, в котором будут выполняться транзакции других людей, если только вы не отправите свою собственную транзакцию после того, как увидите, что транзакция другой стороны уже произошла.
/**
* @dev Перемещает `amount` токенов от `sender` к `recipient` с использованием
* механизма разрешения. Затем `amount` вычитается из разрешения
* вызывающего.
*
* Возвращает логическое значение, указывающее, была ли операция успешной.
*
* Генерирует событие {Transfer}.
*/
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
Наконец, transferFrom используется тратящим для фактической траты разрешения.
/**
* @dev Генерируется, когда `value` токенов перемещаются с одного Аккаунта (`from`) на
* другой (`to`).
*
* Обратите внимание, что `value` может быть равно нулю.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Генерируется, когда разрешение для `spender` от `owner` устанавливается
* вызовом {approve}. `value` — это новое разрешение.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}
Эти события генерируются при изменении состояния контракта ERC-20.
Сам контракт
Это сам контракт, который реализует стандарт ERC-20, взятый отсюда (opens in a new tab). Он не предназначен для использования как есть, но вы можете унаследовать (opens in a new tab) его, чтобы расширить до чего-то пригодного для использования.
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.8.0;
Инструкции импорта
В дополнение к определениям интерфейса выше, определение контракта импортирует два других файла:
import "../../GSN/Context.sol";
import "./IERC20.sol";
import "../../math/SafeMath.sol";
GSN/Context.sol— это определения, необходимые для использования OpenGSN (opens in a new tab), системы, которая позволяет пользователям без эфира использовать блокчейн. Обратите внимание, что это старая версия, если вы хотите интегрироваться с OpenGSN, используйте это руководство (opens in a new tab).- Библиотека SafeMath (opens in a new tab), которая предотвращает арифметические переполнения для версий Solidity <0.8.0. В Solidity ≥0.8.0 арифметические операции автоматически вызывают откат при переполнении, что делает SafeMath ненужной. Этот контракт использует SafeMath для обратной совместимости со старыми версиями компилятора.
Этот комментарий объясняет назначение контракта.
/**
* @dev Реализация интерфейса {IERC20}.
*
* Эта реализация не зависит от способа создания токенов. Это означает,
* что механизм эмиссии должен быть добавлен в производный контракт с использованием {_mint}.
* Для общего механизма см. {ERC20PresetMinterPauser}.
*
* СОВЕТ: Для подробного описания см. наше руководство
* https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[Как
* реализовать механизмы эмиссии].
*
* Мы следовали общим рекомендациям ОпенЗеппелин: функции откатываются вместо
* возврата `false` при сбое. Тем не менее, это поведение является общепринятым
* и не противоречит ожиданиям приложений ERC-20.
*
* Кроме того, событие {Approval} генерируется при вызовах {transferFrom}.
* Это позволяет приложениям восстанавливать разрешение для всех Аккаунтов просто
* путем прослушивания указанных событий. Другие реализации EIP могут не генерировать
* эти события, так как это не требуется спецификацией.
*
* Наконец, были добавлены нестандартные функции {decreaseAllowance} и {increaseAllowance}
* для смягчения хорошо известных проблем, связанных с установкой
* разрешений. См. {IERC20-approve}.
*/
Определение контракта
contract ERC20 is Context, IERC20 {
Эта строка указывает наследование, в данном случае от IERC20 из кода выше и Context для OpenGSN.
using SafeMath for uint256;
Эта строка прикрепляет библиотеку SafeMath к типу uint256. Вы можете найти эту библиотеку здесь (opens in a new tab).
Определения переменных
Эти определения задают переменные состояния контракта. Эти переменные объявлены как private, но это означает лишь то, что другие контракты в блокчейне не могут их прочитать. В блокчейне нет секретов, программное обеспечение на каждом узле имеет состояние каждого контракта в каждом блоке. По соглашению переменные состояния называются _<something>.
Первые две переменные — это сопоставления (mappings) (opens in a new tab), что означает, что они ведут себя примерно так же, как ассоциативные массивы (opens in a new tab), за исключением того, что ключи являются числовыми значениями. Хранилище выделяется только для записей, значения которых отличаются от значений по умолчанию (нуля).
mapping (address => uint256) private _balances;
Первое сопоставление, _balances, — это адреса и их соответствующие балансы этого токена. Для доступа к балансу используйте этот синтаксис: _balances[<address>].
mapping (address => mapping (address => uint256)) private _allowances;
Эта переменная, _allowances, хранит разрешения, объясненные ранее. Первый индекс — это владелец токенов, а второй — контракт с разрешением. Чтобы получить доступ к сумме, которую адрес A может потратить со счета адреса B, используйте _allowances[B][A].
uint256 private _totalSupply;
Как следует из названия, эта переменная отслеживает общее предложение токенов.
string private _name;
string private _symbol;
uint8 private _decimals;
Эти три переменные используются для улучшения читаемости. Первые две говорят сами за себя, но _decimals — нет.
С одной стороны, в Эфириуме нет переменных с плавающей запятой или дробных переменных. С другой стороны, людям нравится иметь возможность делить токены. Одной из причин, по которой люди остановились на золоте в качестве валюты, было то, что было трудно дать сдачу, когда кто-то хотел купить часть коровы по цене утки.
Решение состоит в том, чтобы отслеживать целые числа, но считать вместо реального токена дробный токен, который почти ничего не стоит. В случае с эфиром дробный токен называется Wei, и 10^18 Wei равны одному ETH. На момент написания статьи 10 000 000 000 000 Wei — это примерно один цент США или евро.
Приложениям нужно знать, как отображать баланс токенов. Если у пользователя есть 3 141 000 000 000 000 000 Wei, это 3.14 ETH? 31.41 ETH? 3 141 ETH? В случае с эфиром определено 10^18 Wei на ETH, но для вашего токена вы можете выбрать другое значение. Если деление токена не имеет смысла, вы можете использовать значение _decimals, равное нулю. Если вы хотите использовать тот же стандарт, что и ETH, используйте значение 18.
Конструктор
/**
* @dev Устанавливает значения для {name} и {symbol}, инициализирует {decimals}
* значением по умолчанию 18.
*
* Чтобы выбрать другое значение для {decimals}, используйте {_setupDecimals}.
*
* Все три эти значения неизменяемы: они могут быть установлены только один раз во время
* работы конструктора.
*/
constructor (string memory name_, string memory symbol_) public {
// В Solidity ≥0.7.0 'public' подразумевается и может быть опущено.
_name = name_;
_symbol = symbol_;
_decimals = 18;
}
Конструктор вызывается при первом создании контракта. По соглашению параметры функции называются <something>_.
Функции пользовательского интерфейса
/**
* @dev Возвращает имя токена.
*/
function name() public view returns (string memory) {
return _name;
}
/**
* @dev Возвращает символ токена, обычно более короткую версию
* имени.
*/
function symbol() public view returns (string memory) {
return _symbol;
}
/**
* @dev Возвращает количество десятичных знаков, используемых для получения его пользовательского представления.
* Например, если `decimals` равно `2`, баланс в `505` токенов должен
* отображаться пользователю как `5,05` (`505 / 10 ** 2`).
*
* Токены обычно выбирают значение 18, имитируя соотношение между
* эфиром и Wei. Это значение, которое использует {ERC20}, если не вызван
* {_setupDecimals}.
*
* ПРИМЕЧАНИЕ: Эта информация используется только для целей _отображения_: она
* никоим образом не влияет на арифметику контракта, включая
* {IERC20-balanceOf} и {IERC20-transfer}.
*/
function decimals() public view returns (uint8) {
return _decimals;
}
Эти функции, name, symbol и decimals, помогают пользовательским интерфейсам узнать о вашем контракте, чтобы они могли правильно его отображать.
Тип возвращаемого значения — string memory, что означает возврат строки, хранящейся в памяти. Переменные, такие как строки, могут храниться в трех местах:
| Время жизни | Доступ контракта | Затраты газа | |
|---|---|---|---|
| Память | Вызов функции | Чтение/Запись | Десятки или сотни (выше для более высоких позиций) |
| Данные вызова | Вызов функции | Только чтение | Не может использоваться как тип возвращаемого значения, только как тип параметра функции |
| Хранилище | До изменения | Чтение/Запись | Высокие (800 за чтение, 20k за запись) |
В данном случае memory — лучший выбор.
Чтение информации о токене
Это функции, которые предоставляют информацию о токене: либо общее предложение, либо баланс аккаунта.
/**
* @dev См. {IERC20-totalSupply}.
*/
function totalSupply() public view override returns (uint256) {
return _totalSupply;
}
Функция totalSupply возвращает общее предложение токенов.
/**
* @dev См. {IERC20-balanceOf}.
*/
function balanceOf(address account) public view override returns (uint256) {
return _balances[account];
}
Чтение баланса аккаунта. Обратите внимание, что любому разрешено получать баланс чужого аккаунта. Нет смысла пытаться скрыть эту информацию, потому что она в любом случае доступна на каждом узле. В блокчейне нет секретов.
Перевод токенов
/**
* @dev См. {IERC20-transfer}.
*
* Требования:
*
* - `recipient` не может быть нулевым адресом.
* - вызывающий должен иметь баланс не менее `amount`.
*/
function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
Функция transfer вызывается для перевода токенов со счета отправителя на другой. Обратите внимание, что хотя она возвращает логическое значение, это значение всегда true. Если перевод не удается, контракт откатывает вызов.
_transfer(_msgSender(), recipient, amount);
return true;
}
Функция _transfer выполняет фактическую работу. Это приватная функция, которая может быть вызвана только другими функциями контракта. По соглашению приватные функции называются _<something>, так же как и переменные состояния.
Обычно в Solidity мы используем msg.sender для отправителя сообщения. Однако это ломает OpenGSN (opens in a new tab). Если мы хотим разрешить безэфирные транзакции с нашим токеном, нам нужно использовать _msgSender(). Она возвращает msg.sender для обычных транзакций, но для безэфирных возвращает исходного подписанта, а не контракт, который ретранслировал сообщение.
Функции разрешений
Это функции, которые реализуют функциональность разрешений: allowance, approve, transferFrom и _approve. Кроме того, реализация ОпенЗеппелин выходит за рамки базового стандарта и включает некоторые функции, повышающие безопасность: increaseAllowance и decreaseAllowance.
Функция allowance
/**
* @dev См. {IERC20-allowance}.
*/
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}
Функция allowance позволяет каждому проверить любое разрешение.
Функция approve
/**
* @dev См. {IERC20-approve}.
*
* Требования:
*
* - `spender` не может быть нулевым адресом.
*/
function approve(address spender, uint256 amount) public virtual override returns (bool) {
Эта функция вызывается для создания разрешения. Она похожа на функцию transfer выше:
- Функция просто вызывает внутреннюю функцию (в данном случае
_approve), которая выполняет реальную работу. - Функция либо возвращает
true(в случае успеха), либо откатывается (если нет).
_approve(_msgSender(), spender, amount);
return true;
}
Мы используем внутренние функции, чтобы минимизировать количество мест, где происходят изменения состояния. Любая функция, изменяющая состояние, является потенциальным риском для безопасности, который необходимо проверять. Таким образом, у нас меньше шансов ошибиться.
Функция transferFrom
Это функция, которую вызывает тратящий, чтобы потратить разрешение. Это требует двух операций: перевести потраченную сумму и уменьшить разрешение на эту сумму.
/**
* @dev См. {IERC20-transferFrom}.
*
* Генерирует событие {Approval}, указывающее на обновленное разрешение. Это не
* требуется EIP. См. примечание в начале {ERC20}.
*
* Требования:
*
* - `sender` и `recipient` не могут быть нулевым адресом.
* - `sender` должен иметь баланс не менее `amount`.
* - вызывающий должен иметь разрешение на токены ``sender`` в размере не менее
* `amount`.
*/
function transferFrom(address sender, address recipient, uint256 amount) public virtual
override returns (bool) {
_transfer(sender, recipient, amount);
Вызов функции a.sub(b, "message") делает две вещи. Во-первых, он вычисляет a-b, что является новым разрешением. Во-вторых, он проверяет, что этот результат не отрицательный. Если он отрицательный, вызов откатывается с предоставленным сообщением. Обратите внимание, что когда вызов откатывается, любая обработка, выполненная ранее во время этого вызова, игнорируется, поэтому нам не нужно отменять _transfer.
_approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,
"ERC20: transfer amount exceeds allowance"));
return true;
}
Дополнения безопасности ОпенЗеппелин
Опасно устанавливать ненулевое разрешение на другое ненулевое значение, потому что вы контролируете только порядок своих собственных транзакций, а не чьих-либо еще. Представьте, что у вас есть два пользователя: наивная Алиса и нечестный Билл. Алиса хочет получить от Билла какую-то услугу, которая, по ее мнению, стоит пять токенов, поэтому она дает Биллу разрешение на пять токенов.
Затем что-то меняется, и цена Билла возрастает до десяти токенов. Алиса, которая все еще хочет получить услугу, отправляет транзакцию, которая устанавливает разрешение Билла на десять. В тот момент, когда Билл видит эту новую транзакцию в пуле транзакций, он отправляет транзакцию, которая тратит пять токенов Алисы и имеет гораздо более высокую цену газа, чтобы она была добыта быстрее. Таким образом, Билл может сначала потратить пять токенов, а затем, как только новое разрешение Алисы будет добыто, потратить еще десять на общую сумму пятнадцать токенов, что больше, чем Алиса собиралась разрешить. Этот метод называется фронтраннинг (opens in a new tab).
| Транзакция Алисы | Нонс Алисы | Транзакция Билла | Нонс Билла | Разрешение Билла | Общий доход Билла от Алисы |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
| approve(Bill, 10) | 11 | 10 | 5 | ||
| transferFrom(Alice, Bill, 10) | 10,124 | 0 | 15 |
Чтобы избежать этой проблемы, эти две функции (increaseAllowance и decreaseAllowance) позволяют вам изменять разрешение на определенную сумму. Таким образом, если Билл уже потратил пять токенов, он сможет потратить только еще пять. В зависимости от времени, есть два способа, как это может сработать, и оба заканчиваются тем, что Билл получает только десять токенов:
A:
| Транзакция Алисы | Нонс Алисы | Транзакция Билла | Нонс Билла | Разрешение Билла | Общий доход Билла от Алисы |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
| increaseAllowance(Bill, 5) | 11 | 0+5 = 5 | 5 | ||
| transferFrom(Alice, Bill, 5) | 10,124 | 0 | 10 |
B:
| Транзакция Алисы | Нонс Алисы | Транзакция Билла | Нонс Билла | Разрешение Билла | Общий доход Билла от Алисы |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| increaseAllowance(Bill, 5) | 11 | 5+5 = 10 | 0 | ||
| transferFrom(Alice, Bill, 10) | 10,124 | 0 | 10 |
/**
* @dev Атомарно увеличивает разрешение, предоставленное `spender` вызывающим.
*
* Это альтернатива {approve}, которая может использоваться для смягчения
* проблем, описанных в {IERC20-approve}.
*
* Генерирует событие {Approval}, указывающее на обновленное разрешение.
*
* Требования:
*
* - `spender` не может быть нулевым адресом.
*/
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
_approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue));
return true;
}
Функция a.add(b) — это безопасное сложение. В маловероятном случае, когда a+b>=2^256, она не вызывает переполнение с возвратом к нулю, как это делает обычное сложение.
/**
* @dev Атомарно уменьшает разрешение, предоставленное `spender` вызывающим.
*
* Это альтернатива {approve}, которая может использоваться для смягчения
* проблем, описанных в {IERC20-approve}.
*
* Генерирует событие {Approval}, указывающее на обновленное разрешение.
*
* Требования:
*
* - `spender` не может быть нулевым адресом.
* - `spender` должен иметь разрешение от вызывающего в размере не менее
* `subtractedValue`.
*/
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
_approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue,
"ERC20: decreased allowance below zero"));
return true;
}
Функции, изменяющие информацию о токене
Это четыре функции, которые выполняют фактическую работу: _transfer, _mint, _burn и _approve.
Функция _transfer
/**
* @dev Перемещает токены в количестве `amount` от `sender` к `recipient`.
*
* Эта внутренняя функция эквивалентна {transfer} и может использоваться,
* например, для реализации автоматических комиссий в токенах, механизмов слэшинга и т. д.
*
* Генерирует событие {Transfer}.
*
* Требования:
*
* - `sender` не может быть нулевым адресом.
* - `recipient` не может быть нулевым адресом.
* - `sender` должен иметь баланс не менее `amount`.
*/
function _transfer(address sender, address recipient, uint256 amount) internal virtual {
Эта функция, _transfer, переводит токены с одного аккаунта на другой. Она вызывается как transfer (для переводов с собственного счета отправителя), так и transferFrom (для использования разрешений на перевод с чужого счета).
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
Никто на самом деле не владеет нулевым адресом в Эфириуме (то есть никто не знает приватный ключ, соответствующий открытый ключ которого преобразуется в нулевой адрес). Когда люди используют этот адрес, это обычно программная ошибка, поэтому мы завершаем работу с ошибкой, если нулевой адрес используется в качестве отправителя или получателя.
_beforeTokenTransfer(sender, recipient, amount);
Есть два способа использовать этот контракт:
- Использовать его как шаблон для вашего собственного кода
- Унаследовать от него (opens in a new tab) и переопределить только те функции, которые вам нужно изменить
Второй метод намного лучше, потому что код ERC-20 от ОпенЗеппелин уже прошел аудит и доказал свою безопасность. Когда вы используете наследование, понятно, какие функции вы изменяете, и чтобы доверять вашему контракту, людям нужно проверить только эти конкретные функции.
Часто бывает полезно выполнять функцию каждый раз, когда токены переходят из рук в руки. Однако _transfer — очень важная функция, и ее можно написать небезопасно (см. ниже), поэтому лучше ее не переопределять. Решением является _beforeTokenTransfer, функция-хук (opens in a new tab). Вы можете переопределить эту функцию, и она будет вызываться при каждом переводе.
_balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");
_balances[recipient] = _balances[recipient].add(amount);
Это строки, которые фактически выполняют перевод. Обратите внимание, что между ними ничего нет, и что мы вычитаем переведенную сумму у отправителя перед тем, как добавить ее получателю. Это важно, потому что если бы посередине был вызов другого контракта, его можно было бы использовать для обмана этого контракта. Таким образом, перевод является атомарным, ничего не может произойти в его середине.
emit Transfer(sender, recipient, amount);
}
Наконец, генерируется событие Transfer. События недоступны для смарт-контрактов, но код, работающий вне блокчейна, может прослушивать события и реагировать на них. Например, кошелек может отслеживать, когда владелец получает больше токенов.
Функции _mint и _burn
Эти две функции (_mint и _burn) изменяют общее предложение токенов.
Они являются внутренними, и в этом контракте нет функции, которая их вызывает, поэтому они полезны только в том случае, если вы наследуете контракт и добавляете свою собственную логику, чтобы решить, при каких условиях чеканить новые токены или сжигать существующие.
ПРИМЕЧАНИЕ: Каждый токен ERC-20 имеет свою собственную бизнес-логику, которая диктует управление токенами.
Например, контракт с фиксированным предложением может вызывать _mint только в конструкторе и никогда не вызывать _burn. Контракт, который продает токены, будет вызывать _mint при оплате и, предположительно, вызывать _burn в какой-то момент, чтобы избежать неконтролируемой инфляции.
/** @dev Создает `amount` токенов и назначает их Аккаунту `account`, увеличивая
* общее предложение.
*
* Генерирует событие {Transfer} с `from`, установленным в нулевой адрес.
*
* Требования:
*
* - `to` не может быть нулевым адресом.
*/
function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: mint to the zero address");
_beforeTokenTransfer(address(0), account, amount);
_totalSupply = _totalSupply.add(amount);
_balances[account] = _balances[account].add(amount);
emit Transfer(address(0), account, amount);
}
Обязательно обновляйте _totalSupply при изменении общего количества токенов.
/**
* @dev Уничтожает `amount` токенов с Аккаунта `account`, уменьшая
* общее предложение.
*
* Генерирует событие {Transfer} с `to`, установленным в нулевой адрес.
*
* Требования:
*
* - `account` не может быть нулевым адресом.
* - Аккаунт `account` должен иметь не менее `amount` токенов.
*/
function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");
_beforeTokenTransfer(account, address(0), amount);
_balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance");
_totalSupply = _totalSupply.sub(amount);
emit Transfer(account, address(0), amount);
}
Функция _burn почти идентична _mint, за исключением того, что она работает в обратном направлении.
Функция _approve
Это функция, которая фактически задает разрешения. Обратите внимание, что она позволяет владельцу указать разрешение, которое превышает текущий баланс владельца. Это нормально, потому что баланс проверяется во время перевода, когда он может отличаться от баланса на момент создания разрешения.
/**
* @dev Устанавливает `amount` в качестве разрешения для `spender` на токены `owner`.
*
* Эта внутренняя функция эквивалентна `approve` и может использоваться,
* например, для установки автоматических разрешений для определенных подсистем и т. д.
*
* Генерирует событие {Approval}.
*
* Требования:
*
* - `owner` не может быть нулевым адресом.
* - `spender` не может быть нулевым адресом.
*/
function _approve(address owner, address spender, uint256 amount) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = amount;
Генерируется событие Approval. В зависимости от того, как написано приложение, контракту тратящего может быть сообщено об одобрении либо владельцем, либо сервером, который прослушивает эти события.
emit Approval(owner, spender, amount);
}
Изменение переменной decimals
/**
* @dev Устанавливает {decimals} в значение, отличное от значения по умолчанию 18.
*
* ПРЕДУПРЕЖДЕНИЕ: Эту функцию следует вызывать только из конструктора. Большинство
* приложений, взаимодействующих с контрактами токенов, не ожидают,
* что {decimals} когда-либо изменится, и могут работать некорректно, если это произойдет.
*/
function _setupDecimals(uint8 decimals_) internal {
_decimals = decimals_;
}
Эта функция изменяет переменную _decimals, которая используется для того, чтобы сообщить пользовательским интерфейсам, как интерпретировать сумму. Вы должны вызывать ее из конструктора. Было бы нечестно вызывать ее в любой последующий момент, и приложения не предназначены для обработки этого.
Хуки
/**
* @dev Хук, который вызывается перед любым переводом токенов. Это включает
* чеканку и сжигание.
*
* Условия вызова:
*
* - когда `from` и `to` оба не равны нулю, `amount` токенов ``from``
* будет переведено на `to`.
* - когда `from` равно нулю, `amount` токенов будет отчеканено для `to`.
* - когда `to` равно нулю, `amount` токенов ``from`` будет сожжено.
* - `from` и `to` никогда не равны нулю одновременно.
*
* Чтобы узнать больше о хуках, перейдите к xref:ROOT:extending-contracts.adoc#using-hooks[Использование хуков].
*/
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }
}
Это функция-хук, которая вызывается во время переводов. Здесь она пуста, но если вам нужно, чтобы она что-то делала, вы просто переопределяете ее.
Заключение
Для повторения, вот некоторые из самых важных идей в этом контракте (на мой взгляд, ваше мнение может отличаться):
- В блокчейне нет секретов. Любая информация, к которой может получить доступ смарт-контракт, доступна всему миру.
- Вы можете контролировать порядок своих собственных транзакций, но не то, когда происходят транзакции других людей. По этой причине изменение разрешения может быть опасным, так как оно позволяет тратящему потратить сумму обоих разрешений.
- Значения типа
uint256переполняются с возвратом к нулю. Другими словами, 0-1=2^256-1. Если это нежелательное поведение, вы должны проверять его (или использовать библиотеку SafeMath, которая делает это за вас). Обратите внимание, что это изменилось в Solidity 0.8.0 (opens in a new tab). - Выполняйте все изменения состояния определенного типа в определенном месте, потому что это упрощает аудит. По этой причине у нас есть, например,
_approve, которая вызываетсяapprove,transferFrom,increaseAllowanceиdecreaseAllowance. - Изменения состояния должны быть атомарными, без каких-либо других действий в их середине (как вы можете видеть в
_transfer). Это связано с тем, что во время изменения состояния у вас есть несогласованное состояние. Например, между моментом, когда вы вычитаете из баланса отправителя, и моментом, когда вы добавляете к балансу получателя, существует меньше токенов, чем должно быть. Этим потенциально можно злоупотребить, если между ними есть операции, особенно вызовы другого контракта.
Теперь, когда вы увидели, как написан контракт ERC-20 от ОпенЗеппелин, и особенно как он сделан более безопасным, идите и пишите свои собственные безопасные контракты и приложения.
