Перейти до основного контенту

Огляд контракту ERC-20

Solidity
erc-20
Початківець
Ori Pomerantz
9 березня 2021 р.
24 читається за хвилину

Вступ

Одне з найпоширеніших застосувань Ethereum – це створення групою осіб торгового токена, у певному сенсі – своєї власної валюти. Ці токени зазвичай відповідають стандарту ERC-20. Цей стандарт дає змогу створювати інструменти, як-от пули ліквідності та гаманці, які працюють з усіма токенами ERC-20. У цій статті ми проаналізуємо реалізацію ERC-20 на Solidity від OpenZeppelin (opens in a new tab), а також визначення інтерфейсу (opens in a new tab).

Це анотований вихідний код. Якщо ви хочете реалізувати ERC-20, прочитайте цей посібник (opens in a new tab).

Інтерфейс

Мета стандарту, як-от ERC-20, — уможливити створення багатьох реалізацій токенів, які є сумісними з різними застосунками, наприклад, гаманцями та децентралізованими біржами. Для цього ми створюємо інтерфейс (opens in a new tab). Будь-який код, якому потрібно використовувати контракт токена, може використовувати однакові визначення в інтерфейсі та бути сумісним з усіма контрактами токенів, що його використовують, незалежно від того, чи це гаманець, як-от MetaMask, dapp, як-от etherscan.io, або інший контракт, наприклад пул ліквідності.

Ілюстрація інтерфейсу ERC-20

Якщо ви досвідчений програміст, ви, ймовірно, пам’ятаєте схожі конструкції в Java (opens in a new tab) або навіть у заголовкових файлах C (opens in a new tab).

Це визначення інтерфейсу ERC-20 (opens in a new tab) від OpenZeppelin. Це переклад зручного для читання стандарту (opens in a new tab) в код Solidity. Звісно, сам інтерфейс не визначає, як щось робити. Це пояснено у вихідному коді контракту нижче.

 

1// SPDX-License-Identifier: MIT

Файли Solidity повинні містити ідентифікатор ліцензії. Список ліцензій можна переглянути тут (opens in a new tab). Якщо вам потрібна інша ліцензія, просто поясніть це в коментарях.

 

1pragma solidity >=0.6.0 <0.8.0;

Мова Solidity все ще швидко розвивається, і нові версії можуть бути несумісними зі старим кодом (дивіться тут (opens in a new tab)). Тому рекомендується вказувати не тільки мінімальну версію мови, а й максимальну — останню, з якою ви тестували код.

 

1/**
2 * @dev Інтерфейс стандарту ERC20, як визначено в EIP.
3 */

Тег @dev у коментарі є частиною формату NatSpec (opens in a new tab), що використовується для створення документації з вихідного коду.

 

1interface IERC20 {

За домовленістю, назви інтерфейсів починаються з I.

 

1 /**
2 * @dev Повертає кількість існуючих токенів.
3 */
4 function totalSupply() external view returns (uint256);

Ця функція є external, тобто її можна викликати лише ззовні контракту (opens in a new tab). Вона повертає загальну кількість токенів у контракті. Це значення повертається з використанням найпоширенішого типу в Ethereum, беззнакові 256 біт (256 біт — це нативний розмір слова EVM). Ця функція також є view, що означає, що вона не змінює стан, тому її можна виконати на одному вузлі, а не змушувати кожен вузол у блокчейні її запускати. Функції такого типу не створюють транзакції й не вимагають витрат газу.

Примітка: теоретично може здатися, що творець контракту міг би шахраювати, повертаючи меншу загальну пропозицію, ніж реальна, через що кожен токен здаватиметься ціннішим, ніж він є насправді. Однак цей страх ігнорує справжню природу блокчейну. Усе, що відбувається в блокчейні, може бути перевірено кожним вузлом. Для цього машинний код і сховище кожного контракту доступні на кожному вузлі. Хоча від вас не вимагається публікувати код на Solidity для вашого контракту, ніхто не сприйме вас серйозно, якщо ви не опублікуєте вихідний код і версію Solidity, за допомогою якої його було скомпільовано, щоб його можна було звірити з наданим вами машинним кодом. Наприклад, дивіться цей контракт (opens in a new tab).

 

1 /**
2 * @dev Повертає кількість токенів, що належать `account`.
3 */
4 function balanceOf(address account) external view returns (uint256);

Як випливає з назви, balanceOf повертає баланс облікового запису. Облікові записи Ethereum ідентифікуються в Solidity за допомогою типу address, який містить 160 біт. Вона також є external і view.

 

1 /**
2 * @dev Переміщує токени в кількості `amount` з облікового запису викликаючого до `recipient`.
3 *
4 * Повертає логічне значення, що вказує на успішність операції.
5 *
6 * Викликає подію {Transfer}.
7 */
8 function transfer(address recipient, uint256 amount) external returns (bool);

Функція transfer переказує токени від викликаючого на іншу адресу. Це передбачає зміну стану, тому вона не є view. Коли користувач викликає цю функцію, вона створює транзакцію і вимагає витрат газу. Вона також викликає подію Transfer, щоб повідомити всіх у блокчейні про цю подію.

Функція має два типи виводу для двох різних типів викликаючих:

  • Користувачі, які викликають функцію безпосередньо з інтерфейсу користувача. Зазвичай користувач надсилає транзакцію і не чекає на відповідь, що може зайняти невизначений час. Користувач може побачити, що сталося, знайшовши квитанцію про транзакцію (яка ідентифікується хешем транзакції) або подію Transfer.
  • Інші контракти, які викликають функцію як частину загальної транзакції. Ці контракти отримують результат негайно, оскільки вони виконуються в одній транзакції, тому можуть використовувати значення, що повертається функцією.

Такий самий тип виводу створюється іншими функціями, що змінюють стан контракту.

 

Дозволи (allowances) дозволяють обліковому запису витрачати токени, що належать іншому власнику. Це корисно, наприклад, для контрактів, які діють як продавці. Контракти не можуть відстежувати події, тому якщо покупець перекаже токени на контракт продавця безпосередньо, цей контракт не дізнається про оплату. Натомість покупець дозволяє контракту продавця витратити певну суму, і продавець переказує цю суму. Це робиться через функцію, яку викликає контракт продавця, тому контракт продавця може знати, чи була операція успішною.

1 /**
2 * @dev Повертає залишкову кількість токенів, які `spender` зможе
3 * витратити від імені `owner` через {transferFrom}. За замовчуванням
4 * це нуль.
5 *
6 * Це значення змінюється, коли викликаються {approve} або {transferFrom}.
7 */
8 function allowance(address owner, address spender) external view returns (uint256);

Функція allowance дозволяє будь-кому запитати, який дозвіл одна адреса (owner) надала іншій адресі (spender) на витрату коштів.

 

1 /**
2 * @dev Встановлює `amount` як дозвіл для `spender` на токени викликаючого.
3 *
4 * Повертає логічне значення, що вказує на успішність операції.
5 *
6 * ВАЖЛИВО: пам'ятайте, що зміна дозволу цим методом несе ризик
7 * того, що хтось може використати і старий, і новий дозвіл через невдалий
8 * порядок транзакцій. Одним з можливих рішень для пом'якшення цієї умови
9 * змагання є спочатку зменшення дозволу для витрачаючого до 0, а потім встановлення
10 * бажаного значення:
11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
12 *
13 * Викликає подію {Approval}.
14 */
15 function approve(address spender, uint256 amount) external returns (bool);
Показати все

Функція approve створює дозвіл. Обов’язково прочитайте повідомлення про те, як цим можна зловживати. В Ethereum ви контролюєте порядок власних транзакцій, але не можете контролювати порядок, у якому виконуватимуться транзакції інших людей, якщо тільки ви не надсилатимете власну транзакцію, поки не побачите, що транзакція іншої сторони відбулася.

 

1 /**
2 * @dev Переміщує токени в кількості `amount` від `sender` до `recipient` з використанням
3 * механізму дозволів. Потім `amount` віднімається від
4 * дозволу викликаючого.
5 *
6 * Повертає логічне значення, що вказує на успішність операції.
7 *
8 * Викликає подію {Transfer}.
9 */
10 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
Показати все

Нарешті, transferFrom використовується витрачаючим для фактичної витрати дозволу.

 

1
2 /**
3 * @dev Викликається, коли токени в кількості `value` переміщуються з одного облікового запису (`from`)
4 * на інший (`to`).
5 *
6 * Зауважте, що `value` може дорівнювати нулю.
7 */
8 event Transfer(address indexed from, address indexed to, uint256 value);
9
10 /**
11 * @dev Викликається, коли дозвіл `spender` для `owner` встановлюється
12 * викликом {approve}. `value` — це новий дозвіл.
13 */
14 event Approval(address indexed owner, address indexed spender, uint256 value);
15}
Показати все

Ці події викликаються, коли змінюється стан контракту ERC-20.

Фактичний контракт

Це власне контракт, який реалізує стандарт ERC-20, взятий звідси (opens in a new tab). Він не призначений для використання як є, але ви можете наслідувати (opens in a new tab) його, щоб розширити до чогось придатного для використання.

1// SPDX-License-Identifier: MIT
2pragma solidity >=0.6.0 <0.8.0;

 

Інструкції імпорту

Окрім визначень інтерфейсу вище, визначення контракту імпортує два інші файли:

1
2import "../../GSN/Context.sol";
3import "./IERC20.sol";
4import "../../math/SafeMath.sol";
  • GSN/Context.sol — це визначення, необхідні для використання OpenGSN (opens in a new tab), системи, яка дозволяє користувачам без ether використовувати блокчейн. Зауважте, що це стара версія. Якщо ви хочете інтегруватися з OpenGSN, використовуйте цей посібник (opens in a new tab).
  • Бібліотека SafeMath (opens in a new tab), яка запобігає арифметичним переповненням/недостатнім заповненням для версій Solidity <0.8.0. У Solidity ≥0.8.0 арифметичні операції автоматично скасовуються при переповненні/недостатньому заповненні, що робить SafeMath непотрібною. Цей контракт використовує SafeMath для зворотної сумісності зі старими версіями компілятора.

 

Цей коментар пояснює мету контракту.

1/**
2 * @dev Реалізація інтерфейсу {IERC20}.
3 *
4 * Ця реалізація є агностичною до способу створення токенів. Це означає,
5 * що механізм надання має бути доданий у похідному контракті з використанням {_mint}.
6 * Загальний механізм дивіться в {ERC20PresetMinterPauser}.
7 *
8 * ПОРАДА: Для детального опису дивіться наш посібник
9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[Як
10 * реалізувати механізми надання].
11 *
12 * Ми дотримувалися загальних рекомендацій OpenZeppelin: функції скасовують операцію замість
13 * повернення `false` у разі збою. Ця поведінка є загальноприйнятою
14 * і не суперечить очікуванням застосунків ERC20.
15 *
16 * Додатково, подія {Approval} викликається при викликах {transferFrom}.
17 * Це дозволяє застосункам відтворювати дозвіл для всіх облікових записів,
18 * просто прослуховуючи зазначені події. Інші реалізації EIP можуть не викликати
19 * ці події, оскільки це не вимагається специфікацією.
20 *
21 * Нарешті, нестандартні функції {decreaseAllowance} і {increaseAllowance}
22 * були додані для пом'якшення відомих проблем, пов'язаних із встановленням
23 * дозволів. Дивіться {IERC20-approve}.
24 */
25
Показати все

Визначення контракту

1contract ERC20 is Context, IERC20 {

Цей рядок визначає наслідування, у цьому випадку від IERC20 вище та Context для OpenGSN.

 

1
2 using SafeMath for uint256;
3

Цей рядок прикріплює бібліотеку SafeMath до типу uint256. Ви можете знайти цю бібліотеку тут (opens in a new tab).

Визначення змінних

Ці визначення вказують на змінні стану контракту. Ці змінні оголошені як private, але це означає лише те, що інші контракти в блокчейні не можуть їх читати. У блокчейні немає секретів, програмне забезпечення на кожному вузлі має стан кожного контракту в кожному блоці. За домовленістю, змінні стану називаються _<something>.

Перші дві змінні є відображеннями (mappings) (opens in a new tab), тобто вони поводяться приблизно так само, як асоціативні масиви (opens in a new tab), за винятком того, що ключі є числовими значеннями. Сховище виділяється лише для записів, які мають значення, відмінні від стандартного (нуль).

1 mapping (address => uint256) private _balances;

Перше відображення, _balances, — це адреси та їхні відповідні баланси цього токена. Щоб отримати доступ до балансу, використовуйте такий синтаксис: _balances[<address>].

 

1 mapping (address => mapping (address => uint256)) private _allowances;

Ця змінна, _allowances, зберігає дозволи, пояснені раніше. Перший індекс — це власник токенів, а другий — контракт із дозволом. Щоб отримати доступ до суми, яку адреса А може витратити з облікового запису адреси Б, використовуйте _allowances[B][A].

 

1 uint256 private _totalSupply;

Як випливає з назви, ця змінна відстежує загальну пропозицію токенів.

 

1 string private _name;
2 string private _symbol;
3 uint8 private _decimals;

Ці три змінні використовуються для покращення читабельності. Перші дві зрозумілі самі по собі, а _decimals — ні.

З одного боку, в Ethereum немає змінних із плаваючою комою або дробових змінних. З іншого боку, людям подобається можливість ділити токени. Однією з причин, чому люди обрали золото як валюту, було те, що було важко давати решту, коли хтось хотів купити щось вартістю частини корови.

Рішення полягає в тому, щоб відстежувати цілі числа, але рахувати не справжній токен, а дробовий токен, який майже нічого не вартий. У випадку ether дробовий токен називається 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? 3141 ETH? У випадку ether визначено, що 10^18 wei дорівнює одному ETH, але для вашого токена ви можете вибрати інше значення. Якщо ділення токена не має сенсу, ви можете використовувати значення _decimals, що дорівнює нулю. Якщо ви хочете використовувати той самий стандарт, що і для ETH, використовуйте значення 18.

Конструктор

1 /**
2 * @dev Встановлює значення для {name} та {symbol}, ініціалізує {decimals}
3 * стандартним значенням 18.
4 *
5 * Щоб вибрати інше значення для {decimals}, використовуйте {_setupDecimals}.
6 *
7 * Усі три ці значення є незмінними: їх можна встановити лише один раз під час
8 * створення.
9 */
10 constructor (string memory name_, string memory symbol_) public {
11 // In Solidity ≥0.7.0, 'public' is implicit and can be omitted.
12
13 _name = name_;
14 _symbol = symbol_;
15 _decimals = 18;
16 }
Показати все

Конструктор викликається під час першого створення контракту. За домовленістю, параметри функцій називаються <something>_.

Функції інтерфейсу користувача

1 /**
2 * @dev Повертає назву токена.
3 */
4 function name() public view returns (string memory) {
5 return _name;
6 }
7
8 /**
9 * @dev Повертає символ токена, зазвичай скорочену версію
10 * назви.
11 */
12 function symbol() public view returns (string memory) {
13 return _symbol;
14 }
15
16 /**
17 * @dev Повертає кількість десяткових знаків, що використовуються для представлення користувачеві.
18 * Наприклад, якщо `decimals` дорівнює `2`, баланс `505` токенів
19 * має відображатися користувачеві як `5,05` (`505 / 10 ** 2`).
20 *
21 * Токени зазвичай обирають значення 18, імітуючи зв'язок між
22 * ether і wei. Це значення, яке використовує {ERC20}, якщо не викликано {_setupDecimals}.
23 *
24 * ПРИМІТКА: Ця інформація використовується лише для _відображення_: вона
25 * жодним чином не впливає на арифметику контракту, включно з
26 * {IERC20-balanceOf} і {IERC20-transfer}.
27 */
28 function decimals() public view returns (uint8) {
29 return _decimals;
30 }
Показати все

Ці функції, name, symbol і decimals, допомагають інтерфейсам користувача дізнатися про ваш контракт, щоб вони могли правильно його відобразити.

Тип повернення — string memory, що означає повернення рядка, що зберігається в пам'яті. Змінні, як-от рядки, можуть зберігатися в трьох місцях:

Час життяДоступ до контрактуВартість газу
Пам’ятьВиклик функціїЧитання/ЗаписДесятки або сотні (більше для вищих рівнів)
CalldataВиклик функціїЛише для читанняНе можна використовувати як тип, що повертається, тільки як тип параметра функції
СховищеДо зміниЧитання/ЗаписВисока (800 для читання, 20 тис. для запису)

У цьому випадку, memory є найкращим вибором.

Читання інформації про токен

Це функції, які надають інформацію про токен, або загальну пропозицію, або баланс облікового запису.

1 /**
2 * @dev Див. {IERC20-totalSupply}.
3 */
4 function totalSupply() public view override returns (uint256) {
5 return _totalSupply;
6 }

Функція totalSupply повертає загальну пропозицію токенів.

 

1 /**
2 * @dev Див. {IERC20-balanceOf}.
3 */
4 function balanceOf(address account) public view override returns (uint256) {
5 return _balances[account];
6 }

Читання балансу облікового запису. Зауважте, що будь-кому дозволено отримувати баланс чужого облікового запису. Немає сенсу намагатися приховати цю інформацію, оскільки вона все одно доступна на кожному вузлі. У блокчейні немає секретів.

Переказ токенів

1 /**
2 * @dev Див. {IERC20-transfer}.
3 *
4 * Вимоги:
5 *
6 * - `recipient` не може бути нульовою адресою.
7 * - викликаючий повинен мати баланс не менше `amount`.
8 */
9 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
Показати все

Функція transfer викликається для переказу токенів з облікового запису відправника на інший. Зауважте, що хоча вона повертає логічне значення, це значення завжди true. Якщо переказ не вдається, контракт скасовує виклик.

 

1 _transfer(_msgSender(), recipient, amount);
2 return true;
3 }

Функція _transfer виконує фактичну роботу. Це приватна функція, яку можуть викликати лише інші функції контракту. За домовленістю, приватні функції називаються _<something>, так само як і змінні стану.

Зазвичай у Solidity ми використовуємо msg.sender для позначення відправника повідомлення. Однак це порушує роботу OpenGSN (opens in a new tab). Якщо ми хочемо дозволити транзакції без ether з нашим токеном, ми повинні використовувати _msgSender(). Вона повертає msg.sender для звичайних транзакцій, але для транзакцій без ether повертає початкового підписанта, а не контракт, що передав повідомлення.

Функції дозволу

Це функції, які реалізують функціонал дозволів: allowance, approve, transferFrom та _approve. Крім того, реалізація OpenZeppelin виходить за рамки базового стандарту, включаючи деякі функції, що покращують безпеку: increaseAllowance та decreaseAllowance.

Функція allowance

1 /**
2 * @dev Див. {IERC20-allowance}.
3 */
4 function allowance(address owner, address spender) public view virtual override returns (uint256) {
5 return _allowances[owner][spender];
6 }

Функція allowance дозволяє будь-кому перевірити будь-який дозвіл.

Функція approve

1 /**
2 * @dev Див. {IERC20-approve}.
3 *
4 * Вимоги:
5 *
6 * - `spender` не може бути нульовою адресою.
7 */
8 function approve(address spender, uint256 amount) public virtual override returns (bool) {

Ця функція викликається для створення дозволу. Вона схожа на функцію transfer вище:

  • Функція просто викликає внутрішню функцію (у цьому випадку _approve), яка виконує реальну роботу.
  • Функція або повертає true (якщо успішно), або скасовує операцію (якщо ні).

 

1 _approve(_msgSender(), spender, amount);
2 return true;
3 }

Ми використовуємо внутрішні функції, щоб мінімізувати кількість місць, де відбуваються зміни стану. Будь-яка функція, що змінює стан, є потенційним ризиком для безпеки, який потребує аудиту. Таким чином, у нас менше шансів помилитися.

Функція transferFrom

Це функція, яку викликає витрачаючий для використання дозволу. Це вимагає двох операцій: переказати суму, що витрачається, і зменшити дозвіл на цю суму.

1 /**
2 * @dev Див. {IERC20-transferFrom}.
3 *
4 * Викликає подію {Approval}, що вказує на оновлений дозвіл. Це не
5 * вимагається EIP. Дивіться примітку на початку {ERC20}.
6 *
7 * Вимоги:
8 *
9 * - `sender` і `recipient` не можуть бути нульовими адресами.
10 * - `sender` повинен мати баланс не менше `amount`.
11 * - викликаючий повинен мати дозвіл на токени ``sender`'s` не менше
12 * `amount`.
13 */
14 function transferFrom(address sender, address recipient, uint256 amount) public virtual
15 override returns (bool) {
16 _transfer(sender, recipient, amount);
Показати все

 

Виклик функції a.sub(b, "message") виконує дві дії. По-перше, він обчислює a-b, що є новим дозволом. По-друге, він перевіряє, що цей результат не є від'ємним. Якщо він від’ємний, виклик скасовується з наданим повідомленням. Зауважте, що коли виклик скасовується, будь-яка обробка, виконана раніше під час цього виклику, ігнорується, тому нам не потрібно скасовувати _transfer.

1 _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,
2 "ERC20: сума переказу перевищує дозвіл"));
3 return true;
4 }

Доповнення безпеки від OpenZeppelin

Небезпечно встановлювати ненульовий дозвіл на інше ненульове значення, оскільки ви контролюєте лише порядок власних транзакцій, а не чужих. Уявіть, що у вас є два користувачі: Аліса, яка є наївною, і Білл, який є нечесним. Аліса хоче отримати послугу від Білла, яка, на її думку, коштує п'ять токенів, тому вона дає Біллу дозвіл на п'ять токенів.

Потім щось змінюється, і ціна Білла зростає до десяти токенів. Аліса, яка все ще хоче отримати послугу, надсилає транзакцію, яка встановлює дозвіл для Білла на десять. Щойно Білл бачить цю нову транзакцію в пулі транзакцій, він надсилає транзакцію, яка витрачає п'ять токенів Аліси і має набагато вищу ціну газу, щоб її було швидше видобуто. Таким чином Білл може спочатку витратити п'ять токенів, а потім, коли новий дозвіл Аліси буде видобуто, витратити ще десять, загалом п'ятнадцять токенів, більше, ніж Аліса мала намір дозволити. Ця техніка називається випередженням (front-running) (opens in a new tab)

Транзакція АлісиNonce АлісиТранзакція БіллаNonce БіллаДозвіл для БіллаЗагальний дохід Білла від Аліси
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
approve(Bill, 10)11105
transferFrom(Alice, Bill, 10)10,124015

Щоб уникнути цієї проблеми, ці дві функції (increaseAllowance та decreaseAllowance) дозволяють вам змінювати дозвіл на певну суму. Тож якщо Білл уже витратив п'ять токенів, він зможе витратити ще п'ять. Залежно від часу, є два способи, як це може спрацювати, і обидва закінчуються тим, що Білл отримує лише десять токенів:

A:

Транзакція АлісиNonce АлісиТранзакція БіллаNonce БіллаДозвіл для БіллаЗагальний дохід Білла від Аліси
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
increaseAllowance(Bill, 5)110+5 = 55
transferFrom(Alice, Bill, 5)10,124010

B:

Транзакція АлісиNonce АлісиТранзакція БіллаNonce БіллаДозвіл для БіллаЗагальний дохід Білла від Аліси
approve(Bill, 5)1050
increaseAllowance(Bill, 5)115+5 = 100
transferFrom(Alice, Bill, 10)10,124010
1 /**
2 * @dev Атомарно збільшує дозвіл, наданий `spender` викликаючим.
3 *
4 * Це альтернатива {approve}, яку можна використовувати для пом'якшення
5 * проблем, описаних у {IERC20-approve}.
6 *
7 * Викликає подію {Approval}, що вказує на оновлений дозвіл.
8 *
9 * Вимоги:
10 *
11 * - `spender` не може бути нульовою адресою.
12 */
13 function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
14 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue));
15 return true;
16 }
Показати все

Функція a.add(b) — це безпечне додавання. У малоймовірному випадку, що a+b>=2^256, вона не переноситься так, як звичайне додавання.

1
2 /**
3 * @dev Атомарно зменшує дозвіл, наданий `spender` викликаючим.
4 *
5 * Це альтернатива {approve}, яку можна використовувати для пом'якшення
6 * проблем, описаних у {IERC20-approve}.
7 *
8 * Викликає подію {Approval}, що вказує на оновлений дозвіл.
9 *
10 * Вимоги:
11 *
12 * - `spender` не може бути нульовою адресою.
13 * - `spender` повинен мати дозвіл для викликаючого не менше
14 * `subtractedValue`.
15 */
16 function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
17 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue,
18 "ERC20: зменшено дозвіл нижче нуля"));
19 return true;
20 }
Показати все

Функції, що змінюють інформацію про токен

Це чотири функції, які виконують фактичну роботу: _transfer, _mint, _burn та _approve.

Функція _transfer

1 /**
2 * @dev Переміщує токени в кількості `amount` від `sender` до `recipient`.
3 *
4 * Ця внутрішня функція еквівалентна {transfer} і може використовуватися для,
5 * наприклад, реалізації автоматичних комісій за токени, механізмів слешингу тощо.
6 *
7 * Викликає подію {Transfer}.
8 *
9 * Вимоги:
10 *
11 * - `sender` не може бути нульовою адресою.
12 * - `recipient` не може бути нульовою адресою.
13 * - `sender` повинен мати баланс не менше `amount`.
14 */
15 function _transfer(address sender, address recipient, uint256 amount) internal virtual {
Показати все

Ця функція, _transfer, переказує токени з одного облікового запису на інший. Її викликають як transfer (для переказів з власного облікового запису відправника), так і transferFrom (для використання дозволів для переказу з чужого облікового запису).

 

1 require(sender != address(0), "ERC20: переказ із нульової адреси");
2 require(recipient != address(0), "ERC20: переказ на нульову адресу");

Ніхто насправді не володіє нульовою адресою в Ethereum (тобто ніхто не знає приватного ключа, відповідний публічний ключ якого перетворюється на нульову адресу). Коли люди використовують цю адресу, це зазвичай помилка програмного забезпечення, тому ми викликаємо збій, якщо нульова адреса використовується як відправник або одержувач.

 

1 _beforeTokenTransfer(sender, recipient, amount);
2

Є два способи використання цього контракту:

  1. Використовуйте його як шаблон для власного коду
  2. Успадковуйте від нього (opens in a new tab) і перевизначайте лише ті функції, які вам потрібно змінити

Другий метод набагато кращий, оскільки код OpenZeppelin ERC-20 вже пройшов аудит і довів свою безпечність. Коли ви використовуєте успадкування, чітко видно, які функції ви змінюєте, і щоб довіряти вашому контракту, людям потрібно лише перевірити ці конкретні функції.

Часто буває корисно виконувати функцію щоразу, коли токени переходять з рук в руки. Однак,_transfer — це дуже важлива функція, і її можна написати небезпечно (див. нижче), тому краще її не перевизначати. Рішенням є _beforeTokenTransfer, функція- перехоплювач (hook function) (opens in a new tab). Ви можете перевизначити цю функцію, і вона буде викликатися при кожному переказі.

 

1 _balances[sender] = _balances[sender].sub(amount, "ERC20: сума переказу перевищує баланс");
2 _balances[recipient] = _balances[recipient].add(amount);

Це рядки, які фактично виконують переказ. Зауважте, що між ними нічого немає, і що ми віднімаємо переказану суму від відправника перед тим, як додати її одержувачу. Це важливо, оскільки якби посередині був виклик іншого контракту, його можна було б використати для обману цього контракту. Таким чином, переказ є атомарним, і нічого не може статися посередині.

 

1 emit Transfer(sender, recipient, amount);
2 }

Нарешті, викличте подію Transfer. Події недоступні для смарт-контрактів, але код, що виконується поза блокчейном, може прослуховувати події та реагувати на них. Наприклад, гаманець може відстежувати, коли власник отримує більше токенів.

Функції _mint та _burn

Ці дві функції (_mint і _burn) змінюють загальну пропозицію токенів. Вони є внутрішніми, і в цьому контракті немає функції, яка їх викликає, тому вони корисні лише в тому випадку, якщо ви успадковуєте контракт і додаєте власну логіку, щоб вирішити, за яких умов карбувати нові токени або спалювати наявні.

ПРИМІТКА: Кожен токен ERC-20 має власну бізнес-логіку, яка диктує управління токенами. Наприклад, контракт із фіксованою пропозицією може викликати _mint лише в конструкторі і ніколи не викликати _burn. Контракт, що продає токени, викликатиме _mint під час оплати і, ймовірно, в якийсь момент викличе _burn, щоб уникнути неконтрольованої інфляції.

1 /** @dev Створює токени в кількості `amount` і призначає їх `account`, збільшуючи
2 * загальну пропозицію.
3 *
4 * Викликає подію {Transfer} з `from`, встановленим на нульову адресу.
5 *
6 * Вимоги:
7 *
8 * - `to` не може бути нульовою адресою.
9 */
10 function _mint(address account, uint256 amount) internal virtual {
11 require(account != address(0), "ERC20: карбування на нульову адресу");
12 _beforeTokenTransfer(address(0), account, amount);
13 _totalSupply = _totalSupply.add(amount);
14 _balances[account] = _balances[account].add(amount);
15 emit Transfer(address(0), account, amount);
16 }
Показати все

Не забудьте оновити _totalSupply, коли зміниться загальна кількість токенів.

 

1 /**
2 * @dev Знищує токени в кількості `amount` з `account`, зменшуючи
3 * загальну пропозицію.
4 *
5 * Викликає подію {Transfer} з `to`, встановленим на нульову адресу.
6 *
7 * Вимоги:
8 *
9 * - `account` не може бути нульовою адресою.
10 * - `account` повинен мати не менше `amount` токенів.
11 */
12 function _burn(address account, uint256 amount) internal virtual {
13 require(account != address(0), "ERC20: спалювання з нульової адреси");
14
15 _beforeTokenTransfer(account, address(0), amount);
16
17 _balances[account] = _balances[account].sub(amount, "ERC20: сума спалювання перевищує баланс");
18 _totalSupply = _totalSupply.sub(amount);
19 emit Transfer(account, address(0), amount);
20 }
Показати все

Функція _burn майже ідентична _mint, за винятком того, що вона працює в протилежному напрямку.

Функція _approve

Це функція, яка фактично визначає дозволи. Зауважте, що вона дозволяє власнику вказати дозвіл, що перевищує поточний баланс власника. Це нормально, оскільки баланс перевіряється під час переказу, коли він може відрізнятися від балансу на момент створення дозволу.

1 /**
2 * @dev Встановлює `amount` як дозвіл для `spender` на токени `owner`.
3 *
4 * Ця внутрішня функція еквівалентна `approve` і може використовуватися,
5 * наприклад, для встановлення автоматичних дозволів для певних підсистем тощо.
6 *
7 * Викликає подію {Approval}.
8 *
9 * Вимоги:
10 *
11 * - `owner` не може бути нульовою адресою.
12 * - `spender` не може бути нульовою адресою.
13 */
14 function _approve(address owner, address spender, uint256 amount) internal virtual {
15 require(owner != address(0), "ERC20: затвердження з нульової адреси");
16 require(spender != address(0), "ERC20: затвердження для нульової адреси");
17
18 _allowances[owner][spender] = amount;
Показати все

 

Викличте подію Approval. Залежно від того, як написаний застосунок, контракт витрачаючого може бути повідомлений про затвердження або власником, або сервером, який прослуховує ці події.

1 emit Approval(owner, spender, amount);
2 }
3

Зміна змінної decimals

1
2
3 /**
4 * @dev Встановлює {decimals} на значення, відмінне від стандартного 18.
5 *
6 * ПОПЕРЕДЖЕННЯ: Цю функцію слід викликати лише з конструктора. Більшість
7 * застосунків, які взаємодіють з контрактами токенів, не очікують,
8 * що {decimals} колись зміниться, і можуть працювати неправильно, якщо це станеться.
9 */
10 function _setupDecimals(uint8 decimals_) internal {
11 _decimals = decimals_;
12 }
Показати все

Ця функція змінює змінну _decimals, яка використовується для того, щоб повідомити інтерфейсу користувача, як інтерпретувати суму. Її слід викликати з конструктора. Було б нечесно викликати її в будь-який наступний момент, і застосунки не призначені для обробки такої зміни.

Хуки

1
2 /**
3 * @dev Перехоплювач, який викликається перед будь-яким переказом токенів. Це включає
4 * карбування та спалювання.
5 *
6 * Умови виклику:
7 *
8 * - коли `from` і `to` обидва ненульові, токени в кількості `amount` з ``from``'s
9 * будуть переказані до `to`.
10 * - коли `from` дорівнює нулю, токени в кількості `amount` будуть викарбувані для `to`.
11 * - коли `to` дорівнює нулю, токени ``from`` в кількості `amount` будуть спалені.
12 * - `from` і `to` ніколи не є одночасно нульовими.
13 *
14 * Щоб дізнатися більше про перехоплювачі, перейдіть до xref:ROOT:extending-contracts.adoc#using-hooks[Використання перехоплювачів].
15 */
16 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }
17}
Показати все

Це функція-перехоплювач, яка викликається під час переказів. Тут вона порожня, але якщо вам потрібно, щоб вона щось робила, ви просто перевизначаєте її.

Висновок

Для повторення, ось деякі з найважливіших ідей цього контракту (на мою думку, ваша може відрізнятися):

  • На блокчейні немає секретів. Будь-яка інформація, до якої має доступ смарт-контракт, доступна всьому світу.
  • Ви можете контролювати порядок власних транзакцій, але не те, коли відбуваються транзакції інших людей. Це причина, чому зміна дозволу може бути небезпечною, оскільки це дозволяє витрачаючому витратити суму обох дозволів.
  • Значення типу uint256 переповнюються. Іншими словами, 0-1=2^256-1. Якщо це небажана поведінка, вам доведеться перевіряти це (або використовувати бібліотеку SafeMath, яка робить це за вас). Зауважте, що це змінилося в Solidity 0.8.0 (opens in a new tab).
  • Виконуйте всі зміни стану певного типу в певному місці, оскільки це полегшує аудит. Це причина, чому ми маємо, наприклад, _approve, який викликається approve, transferFrom, increaseAllowance та decreaseAllowance
  • Зміни стану мають бути атомарними, без будь-яких інших дій посередині (як ви можете бачити в _transfer). Це тому, що під час зміни стану ви маєте неузгоджений стан. Наприклад, між моментом, коли ви віднімаєте з балансу відправника, і моментом, коли ви додаєте до балансу одержувача, існує менше токенів, ніж повинно бути. Цим можна потенційно зловживати, якщо між ними є операції, особливо виклики до іншого контракту.

Тепер, коли ви побачили, як написаний контракт OpenZeppelin ERC-20, і особливо, як він зроблений більш безпечним, ідіть і пишіть власні безпечні контракти та застосунки.

Більше моїх робіт дивіться тут (opens in a new tab).

Останні оновлення сторінки: 3 березня 2026 р.

Чи була ця інструкція корисною?