---
title: "Огляд контракту ERC-20"
description: "Що міститься в контракті ERC-20 від ОупенЗеппелін і чому воно там?"
author: "Орі Померанц"
lang: uk
tags: ["Solidity", "erc-20"]
skill: beginner
breadcrumb: "Огляд ERC-20"
published: 2021-03-09
---

## Вступ {#introduction}

Одне з найпоширеніших застосувань Етеріуму — це створення групою людей токена, яким можна торгувати, у певному сенсі їхньої власної валюти. Ці токени зазвичай відповідають стандарту [ERC-20](/developers/docs/standards/tokens/erc-20/). Цей стандарт дає змогу створювати інструменти, такі як пули ліквідності та гаманці, які працюють з усіма токенами ERC-20. У цій статті ми проаналізуємо [реалізацію ERC-20 на Solidity від ОупенЗеппелін](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol), а також [визначення інтерфейсу](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol).

Це анотований вихідний код. Якщо ви хочете реалізувати ERC-20, [прочитайте цей посібник](https://docs.openzeppelin.com/contracts/2.x/erc20-supply).

## Інтерфейс {#the-interface}

Мета такого стандарту, як ERC-20, полягає в тому, щоб дозволити багатьом реалізаціям токенів бути інтероперабельними в різних застосунках, таких як гаманці та децентралізовані біржі. Щоб досягти цього, ми створюємо [інтерфейс](https://www.geeksforgeeks.org/solidity/solidity-basics-of-interface/). Будь-який код, якому потрібно використовувати контракт токена, може використовувати ті самі визначення в інтерфейсі та бути сумісним з усіма контрактами токенів, які його використовують, незалежно від того, чи це гаманець, як-от МетаМаск, децентралізований застосунок (dapp), як-от Etherscan.io, або інший контракт, як-от пул ліквідності.

![Illustration of the ERC-20 interface](erc20_interface.png)

Якщо ви досвідчений програміст, ви, ймовірно, пам'ятаєте подібні конструкції в [Java](https://www.w3schools.com/java/java_interface.asp) або навіть у [заголовних файлах C](https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html).

Це визначення [інтерфейсу ERC-20](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol) від ОупенЗеппелін. Це переклад [зрозумілого для людини стандарту](https://eips.ethereum.org/EIPS/eip-20) у код Solidity. Звісно, сам інтерфейс не визначає, _як_ щось робити. Це пояснюється у вихідному коді контракту нижче.

&nbsp;

```solidity
// SPDX-License-Identifier: MIT
```

Файли Solidity повинні містити ідентифікатор ліцензії. [Ви можете переглянути список ліцензій тут](https://spdx.org/licenses/). Якщо вам потрібна інша ліцензія, просто поясніть це в коментарях.

&nbsp;

```solidity
pragma solidity >=0.6.0 <0.8.0;
```

Мова Solidity все ще швидко розвивається, і нові версії можуть бути несумісними зі старим кодом ([дивіться тут](https://docs.soliditylang.org/en/v0.7.0/070-breaking-changes.html)). Тому доцільно вказувати не лише мінімальну версію мови, але й максимальну — останню, з якою ви тестували код.

&nbsp;

```solidity
/**
 * @dev Інтерфейс стандарту ERC-20, як визначено в EIP.
 */
```

`@dev` у коментарі є частиною [формату NatSpec](https://docs.soliditylang.org/en/develop/natspec-format.html), який використовується для створення документації з вихідного коду.

&nbsp;

```solidity
interface IERC20 {
```

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

&nbsp;

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

Ця функція є `external`, що означає, що [її можна викликати лише ззовні контракту](https://docs.soliditylang.org/en/v0.7.0/cheatsheet.html#index-2). Вона повертає загальну пропозицію токенів у контракті. Це значення повертається з використанням найпоширенішого типу в Етеріумі — 256-бітного цілого числа без знака (256 біт — це власний розмір слова EVM). Ця функція також є `view`, що означає, що вона не змінює стан, тому її можна виконати на одному вузлі замість того, щоб кожен вузол у блокчейні запускав її. Такий тип функції не генерує транзакцію і не потребує витрат [газу](/developers/docs/gas/).

**Примітка:** Теоретично може здатися, що творець контракту може схитрувати, повернувши меншу загальну пропозицію, ніж реальне значення, через що кожен токен здаватиметься ціннішим, ніж він є насправді. Однак це побоювання ігнорує справжню природу блокчейну. Усе, що відбувається в блокчейні, може бути перевірено кожним вузлом. Для досягнення цього машинний код і сховище кожного контракту доступні на кожному вузлі. Хоча ви не зобов'язані публікувати код Solidity для свого контракту, ніхто не сприйматиме вас серйозно, якщо ви не опублікуєте вихідний код і версію Solidity, за допомогою якої він був скомпільований, щоб його можна було перевірити на відповідність наданому вами машинному коду.
Наприклад, подивіться [цей контракт](https://eth.blockscout.com/address/0xa530F85085C6FE2f866E7FdB716849714a89f4CD?tab=contract).

&nbsp;

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

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

&nbsp;

```solidity
    /**
     * @dev Переміщує `amount` токенів з акаунта викликаючого до `recipient`.
     *
     * Повертає логічне значення, що вказує, чи була операція успішною.
     *
     * Генерує подію {Transfer}.
     */
    function transfer(address recipient, uint256 amount) external returns (bool);
```

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

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

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

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

&nbsp;

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

```solidity
    /**
     * @dev Повертає кількість токенів, що залишилася, яку `spender` матиме
     * дозвіл витратити від імені `owner` за допомогою {transferFrom}. За замовчуванням
     * це нуль.
     *
     * Це значення змінюється, коли викликаються {approve} або {transferFrom}.
     */
    function allowance(address owner, address spender) external view returns (uint256);
```

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

&nbsp;

```solidity
    /**
     * @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` створює дозвіл. Обов'язково прочитайте повідомлення про те, як цим можна зловживати. В Етеріумі ви контролюєте порядок власних транзакцій, але ви не можете контролювати порядок, у якому будуть виконуватися транзакції інших людей, хіба що ви не надсилатимете власну транзакцію, доки не побачите, що транзакція іншої сторони відбулася.

&nbsp;

```solidity
    /**
     * @dev Переміщує `amount` токенів від `sender` до `recipient` за допомогою
     * механізму дозволів. Потім `amount` віднімається від дозволу
     * викликаючого.
     *
     * Повертає логічне значення, що вказує, чи була операція успішною.
     *
     * Генерує подію {Transfer}.
     */
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
```

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

&nbsp;

```solidity

    /**
     * @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.

## Фактичний контракт {#the-actual-contract}

Це фактичний контракт, який реалізує стандарт ERC-20, [взятий звідси](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol). Він не призначений для використання як є, але ви можете [успадкувати](https://www.tutorialspoint.com/solidity/solidity_inheritance.htm) його, щоб розширити до чогось придатного для використання.

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

&nbsp;

### Інструкції імпорту {#import-statements}

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

```solidity

import "../../GSN/Context.sol";
import "./IERC20.sol";
import "../../math/SafeMath.sol";
```

- `GSN/Context.sol` — це визначення, необхідні для використання [OpenGSN](https://opengsn.org/), системи, яка дозволяє користувачам без етеру використовувати блокчейн. Зверніть увагу, що це стара версія, якщо ви хочете інтегруватися з OpenGSN, [скористайтеся цим посібником](https://docs.opengsn.org/javascript-client/tutorial.html).
- [Бібліотека SafeMath](https://ethereumdev.io/using-safe-math-library-to-prevent-from-overflows/), яка запобігає арифметичним переповненням/втратам значущості для версій Solidity **&lt;0.8.0**. У Solidity ≥0.8.0 арифметичні операції автоматично скасовуються при переповненні/втраті значущості, що робить SafeMath непотрібною. Цей контракт використовує SafeMath для зворотної сумісності зі старішими версіями компілятора.

&nbsp;

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

```solidity
/**
 * @dev Реалізація інтерфейсу {IERC20}.
 *
 * Ця реалізація не залежить від способу створення токенів. Це означає,
 * що механізм постачання має бути доданий у похідний контракт за допомогою {_mint}.
 * Для загального механізму див. {ERC20PresetMinterPauser}.
 *
 * ПОРАДА: Для детального опису див. наш посібник
 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How
 * to implement supply mechanisms].
 *
 * Ми дотримувалися загальних рекомендацій ОупенЗеппелін: функції скасовуються (revert) замість
 * повернення `false` у разі невдачі. Тим не менш, ця поведінка є загальноприйнятою
 * і не суперечить очікуванням застосунків ERC-20.
 *
 * Крім того, подія {Approval} генерується при викликах {transferFrom}.
 * Це дозволяє застосункам відновлювати дозвіл для всіх акаунтів просто
 * прослуховуючи ці події. Інші реалізації EIP можуть не генерувати
 * ці події, оскільки це не вимагається специфікацією.
 *
 * Нарешті, нестандартні функції {decreaseAllowance} та {increaseAllowance}
 * були додані для пом'якшення добре відомих проблем, пов'язаних із встановленням
 * дозволів. Див. {IERC20-approve}.
 */

```

### Визначення контракту {#contract-definition}

```solidity
contract ERC20 is Context, IERC20 {
```

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

&nbsp;

```solidity

    using SafeMath for uint256;

```

Цей рядок приєднує бібліотеку `SafeMath` до типу `uint256`. Ви можете знайти цю бібліотеку [тут](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol).

### Визначення змінних {#variable-definitions}

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

Перші дві змінні є [відображеннями (mappings)](https://www.tutorialspoint.com/solidity/solidity_mappings.htm), що означає, що вони поводяться приблизно так само, як [асоціативні масиви](https://wikipedia.org/wiki/Associative_array), за винятком того, що ключами є числові значення. Сховище виділяється лише для записів, які мають значення, відмінні від значення за замовчуванням (нуля).

```solidity
    mapping (address => uint256) private _balances;
```

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

&nbsp;

```solidity
    mapping (address => mapping (address => uint256)) private _allowances;
```

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

&nbsp;

```solidity
    uint256 private _totalSupply;
```

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

&nbsp;

```solidity
    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**.

### Конструктор {#the-constructor}

```solidity
    /**
     * @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>_`.

### Функції інтерфейсу користувача {#user-interface-functions}

```solidity
    /**
     * @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 для читання, 20 тис. для запису)                                                             |

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

### Читання інформації про токен {#read-token-information}

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

```solidity
    /**
     * @dev Див. {IERC20-totalSupply}.
     */
    function totalSupply() public view override returns (uint256) {
        return _totalSupply;
    }
```

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

&nbsp;

```solidity
    /**
     * @dev Див. {IERC20-balanceOf}.
     */
    function balanceOf(address account) public view override returns (uint256) {
        return _balances[account];
    }
```

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

### Переказ токенів {#transfer-tokens}

```solidity
    /**
     * @dev Див. {IERC20-transfer}.
     *
     * Вимоги:
     *
     * - `recipient` не може бути нульовою адресою.
     * - викликаючий повинен мати баланс щонайменше `amount`.
     */
    function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
```

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

&nbsp;

```solidity
        _transfer(_msgSender(), recipient, amount);
        return true;
    }
```

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

Зазвичай у Solidity ми використовуємо `msg.sender` для відправника повідомлення. Однак це ламає [OpenGSN](https://opengsn.org/). Якщо ми хочемо дозволити транзакції без етеру з нашим токеном, нам потрібно використовувати `_msgSender()`. Вона повертає `msg.sender` для звичайних транзакцій, але для транзакцій без етеру повертає початкового підписанта, а не контракт, який ретранслював повідомлення.

### Функції дозволів {#allowance-functions}

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

#### Функція allowance {#allowance}

```solidity
    /**
     * @dev Див. {IERC20-allowance}.
     */
    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }
```

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

#### Функція approve {#approve}

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

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

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

&nbsp;

```solidity
        _approve(_msgSender(), spender, amount);
        return true;
    }
```

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

#### Функція transferFrom {#transferfrom}

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

```solidity
    /**
     * @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);
```

&nbsp;

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

```solidity
        _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,
             "ERC20: transfer amount exceeds allowance"));
        return true;
    }
```

#### Доповнення безпеки від ОупенЗеппелін {#openzeppelin-safety-additions}

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

Потім щось змінюється, і ціна Білла зростає до десяти токенів. Аліса, яка все ще хоче отримати послугу, надсилає транзакцію, яка встановлює дозвіл Білла на десять. У той момент, коли Білл бачить цю нову транзакцію в пулі транзакцій, він надсилає транзакцію, яка витрачає п'ять токенів Аліси та має набагато вищу ціну газу, щоб вона була видобута швидше. Таким чином Білл може витратити спочатку п'ять токенів, а потім, коли новий дозвіл Аліси буде видобуто, витратити ще десять на загальну суму п'ятнадцять токенів, що більше, ніж Аліса мала намір дозволити. Ця техніка називається [випередженням](https://consensysdiligence.github.io/smart-contract-best-practices/attacks/#front-running)

| Транзакція Аліси  | Нонс Аліси  | Транзакція Білла              | Нонс Білла | Дозвіл Білла     | Загальний дохід Білла від Аліси |
| ----------------- | ----------- | ----------------------------- | ---------- | ---------------- | ------------------------------- |
| 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 |

```solidity
    /**
     * @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`, вона не переповнюється так, як це робить звичайне додавання.

```solidity

    /**
     * @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;
    }
```

### Функції, які змінюють інформацію про токен {#functions-that-modify-token-information}

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

#### Функція _transfer {#transfer}

```solidity
    /**
     * @dev Переміщує токени `amount` від `sender` до `recipient`.
     *
     * Ця внутрішня функція еквівалентна {transfer}, і може бути використана для,
     * наприклад, реалізації автоматичних комісій у токенах, механізмів слешингу тощо.
     *
     * Генерує подію {Transfer}.
     *
     * Вимоги:
     *
     * - `sender` не може бути нульовою адресою.
     * - `recipient` не може бути нульовою адресою.
     * - `sender` повинен мати баланс щонайменше `amount`.
     */
    function _transfer(address sender, address recipient, uint256 amount) internal virtual {
```

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

&nbsp;

```solidity
        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");
```

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

&nbsp;

```solidity
        _beforeTokenTransfer(sender, recipient, amount);

```

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

1. Використовувати його як шаблон для власного коду
1. [Успадкувати від нього](https://www.bitdegree.org/learn/solidity-inheritance) та перевизначити лише ті функції, які вам потрібно змінити

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

Часто буває корисно виконувати функцію щоразу, коли токени переходять з рук у руки. Однак `_transfer` є дуже важливою функцією, і її можна написати небезпечно (див. нижче), тому краще її не перевизначати. Рішенням є `_beforeTokenTransfer`, [функція-хук](https://wikipedia.org/wiki/Hooking). Ви можете перевизначити цю функцію, і вона буде викликатися під час кожного переказу.

&nbsp;

```solidity
        _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");
        _balances[recipient] = _balances[recipient].add(amount);
```

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

&nbsp;

```solidity
        emit Transfer(sender, recipient, amount);
    }
```

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

#### Функції _mint та _burn {#mint-and-burn}

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

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

```solidity
    /** @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`, коли змінюється загальна кількість токенів.

&nbsp;

```solidity
    /**
     * @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 {#approve-2}

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

```solidity
    /**
     * @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;
```

&nbsp;

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

```solidity
        emit Approval(owner, spender, amount);
    }

```

### Зміна змінної Decimals {#modify-the-decimals-variable}

```solidity


    /**
     * @dev Встановлює {decimals} на значення, відмінне від значення за замовчуванням 18.
     *
     * ПОПЕРЕДЖЕННЯ: Цю функцію слід викликати лише з конструктора. Більшість
     * застосунків, які взаємодіють з контрактами токенів, не очікуватимуть,
     * що {decimals} коли-небудь зміниться, і можуть працювати неправильно, якщо це станеться.
     */
    function _setupDecimals(uint8 decimals_) internal {
        _decimals = decimals_;
    }
```

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

### Хуки {#hooks}

```solidity

    /**
     * @dev Хук, який викликається перед будь-яким переказом токенів. Це включає
     * карбування (minting) та спалювання (burning).
     *
     * Умови виклику:
     *
     * - коли `from` та `to` обидва не є нульовими, `amount` токенів ``from``
     * буде переказано до `to`.
     * - коли `from` дорівнює нулю, `amount` токенів буде викарбувано для `to`.
     * - коли `to` дорівнює нулю, `amount` токенів ``from`` буде спалено.
     * - `from` та `to` ніколи не дорівнюють нулю одночасно.
     *
     * Щоб дізнатися більше про хуки, перейдіть до xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }
}
```

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

## Висновок {#conclusion}

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

- _У блокчейні немає секретів_. Будь-яка інформація, до якої може отримати доступ смарт-контракт, доступна всьому світу.
- Ви можете контролювати порядок власних транзакцій, але не те, коли відбуваються транзакції інших людей. Це причина, чому зміна дозволу може бути небезпечною, оскільки це дозволяє витрачальнику витратити суму обох дозволів.
- Значення типу `uint256` переповнюються. Іншими словами, _0-1=2^256-1_. Якщо це не бажана поведінка, ви повинні перевірити це (або використати бібліотеку SafeMath, яка зробить це за вас). Зверніть увагу, що це змінилося в [Solidity 0.8.0](https://docs.soliditylang.org/en/breaking/080-breaking-changes.html).
- Робіть усі зміни стану певного типу в певному місці, оскільки це полегшує аудит. Це причина, чому ми маємо, наприклад, `_approve`, яка викликається `approve`, `transferFrom`, `increaseAllowance` та `decreaseAllowance`
- Зміни стану мають бути атомарними, без будь-яких інших дій посередині (як ви можете бачити в `_transfer`). Це пов'язано з тим, що під час зміни стану ви маєте неузгоджений стан. Наприклад, між часом, коли ви віднімаєте з балансу відправника, і часом, коли ви додаєте до балансу одержувача, існує менше токенів, ніж має бути. Цим потенційно можна зловживати, якщо між ними є операції, особливо виклики іншого контракту.

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

[Дивіться тут більше моїх робіт](https://cryptodocguy.pro/).
