Přeskočit na hlavní obsah

Podrobný průvodce kontraktem ERC-20

solidity
erc-20
Začátečník
Ori Pomerantz
9. března 2021
24 minuta čtení

Úvod

Jedním z nejčastějších způsobů využití Etherea je vytvoření obchodovatelného tokenu pro skupinu, v podstatě jejich vlastní měny. Tyto tokeny obvykle dodržují standard ERC-20. Tento standard umožňuje psát nástroje, jako jsou pooly likvidity a peněženky, které fungují se všemi tokeny ERC-20. V tomto článku budeme analyzovat implementaci OpenZeppelin Solidity ERC20opens in a new tab a také definici rozhraníopens in a new tab.

Jedná se o anotovaný zdrojový kód. Chcete-li implementovat ERC-20, přečtěte si tento návodopens in a new tab.

Rozhraní

Účelem standardu, jako je ERC-20, je umožnit mnoho implementací tokenů, které jsou interoperabilní napříč aplikacemi, jako jsou peněženky a decentralizované burzy. K tomu vytvoříme rozhraníopens in a new tab. Jakýkoli kód, který potřebuje použít kontrakt tokenu, může použít stejné definice v rozhraní a být kompatibilní se všemi kontrakty tokenu, které jej používají, ať už je to peněženka, jako je MetaMask, dapp, jako je etherscan.io, nebo jiný kontrakt, jako je pool likvidity.

Ilustrace rozhraní ERC-20

Pokud jste zkušený programátor, pravděpodobně si pamatujete, že jste podobné konstrukce viděli v Javěopens in a new tab nebo dokonce v hlavičkových souborech Copens in a new tab.

Toto je definice rozhraní ERC-20opens in a new tab od OpenZeppelin. Jedná se o překlad lidsky čitelného standarduopens in a new tab do kódu v Solidity. Samozřejmě samotné rozhraní nedefinuje, jak se má něco dělat. To je vysvětleno ve zdrojovém kódu kontraktu níže.

 

1// SPDX-License-Identifier: MIT

Soubory v Solidity by měly obsahovat identifikátor licence. Seznam licencí naleznete zdeopens in a new tab. Pokud potřebujete jinou licenci, jednoduše to vysvětlete v komentářích.

 

1pragma solidity >=0.6.0 <0.8.0;

Jazyk Solidity se stále rychle vyvíjí a nové verze nemusí být kompatibilní se starým kódem (viz zdeopens in a new tab). Proto je dobré specifikovat nejen minimální verzi jazyka, ale také maximální verzi, nejnovější, se kterou jste kód testovali.

 

1/**
2 * @dev Rozhraní standardu ERC20, jak je definováno v EIP.
3 */

Značka @dev v komentáři je součástí formátu NatSpecopens in a new tab, který se používá k vytváření dokumentace ze zdrojového kódu.

 

1interface IERC20 {

Podle konvence začínají názvy rozhraní písmenem I.

 

1 /**
2 * @dev Vrací množství existujících tokenů.
3 */
4 function totalSupply() external view returns (uint256);

Tato funkce je externí (external), což znamená, že ji lze volat pouze z vnějšku kontraktuopens in a new tab. Vrací celkovou zásobu tokenů v kontraktu. Tato hodnota je vrácena pomocí nejběžnějšího typu v Ethereu, 256bitového celého čísla bez znaménka (256 bitů je nativní velikost slova EVM). Tato funkce je také view, což znamená, že nemění stav, takže ji lze provést na jednom uzlu, místo aby ji musel spouštět každý uzel v blockchainu. Tento druh funkce negeneruje transakci a nestojí žádné palivo.

Poznámka: Teoreticky by se mohlo zdát, že tvůrce kontraktu by mohl podvádět vrácením menší celkové zásoby, než je skutečná hodnota, takže by se každý token zdál cennější, než ve skutečnosti je. Tato obava však ignoruje skutečnou podstatu blockchainu. Vše, co se děje na blockchainu, může být ověřeno každým uzlem. Aby toho bylo dosaženo, je na každém uzlu k dispozici strojový kód a úložiště každého kontraktu. I když nejste povinni zveřejnit kód vašeho kontraktu v Solidity, nikdo by vás nebral vážně, pokud nezveřejníte zdrojový kód a verzi Solidity, se kterou byl zkompilován, aby mohl být ověřen oproti strojovému kódu, který jste poskytli. Podívejte se například na tento kontraktopens in a new tab.

 

1 /**
2 * @dev Vrací množství tokenů, které vlastní `account`.
3 */
4 function balanceOf(address account) external view returns (uint256);

Jak název napovídá, funkce balanceOf vrací zůstatek účtu. Účty Etherea jsou v Solidity identifikovány pomocí typu address, který obsahuje 160 bitů. Je také external a view.

 

1 /**
2 * @dev Přesune tokeny v množství `amount` z účtu volajícího na `recipient`.
3 *
4 * Vrací booleovskou hodnotu, která udává, zda operace proběhla úspěšně.
5 *
6 * Vyvolá událost {Transfer}.
7 */
8 function transfer(address recipient, uint256 amount) external returns (bool);

Funkce transfer převádí tokeny od volajícího na jinou adresu. To zahrnuje změnu stavu, takže to není view. Když uživatel zavolá tuto funkci, vytvoří se transakce, která stojí palivo. Rovněž vyvolá událost Transfer, aby informovala všechny na blockchainu o této události.

Funkce má dva typy výstupu pro dva různé typy volajících:

  • Uživatelé, kteří volají funkci přímo z uživatelského rozhraní. Uživatel obvykle odešle transakci a nečeká na odpověď, což může trvat neurčitou dobu. Uživatel může zjistit, co se stalo, vyhledáním potvrzení o transakci (které je identifikováno hašem transakce) nebo vyhledáním události Transfer.
  • Jiné kontrakty, které volají funkci jako součást celkové transakce. Tyto kontrakty dostanou výsledek okamžitě, protože běží ve stejné transakci, takže mohou použít návratovou hodnotu funkce.

Stejný typ výstupu je vytvořen ostatními funkcemi, které mění stav kontraktu.

 

Povolení umožňují účtu utratit některé tokeny, které patří jinému vlastníkovi. To je užitečné například pro kontrakty, které fungují jako prodejci. Kontrakty nemohou sledovat události, takže pokud by kupující převedl tokeny přímo na kontrakt prodejce, tento kontrakt by nevěděl, že mu bylo zaplaceno. Místo toho kupující povolí kontraktu prodejce utratit určitou částku a prodejce tuto částku převede. To se děje prostřednictvím funkce, kterou kontrakt prodejce volá, takže kontrakt prodejce může vědět, zda byl úspěšný.

1 /**
2 * @dev Vrací zbývající počet tokenů, které bude moci `spender`
3 * utratit jménem `owner` prostřednictvím {transferFrom}. Ve výchozím nastavení je
4 * tato hodnota nulová.
5 *
6 * Tato hodnota se mění při volání funkcí {approve} nebo {transferFrom}.
7 */
8 function allowance(address owner, address spender) external view returns (uint256);

Funkce allowance umožňuje komukoli dotázat se, jaké je povolení, které jedna adresa (owner) umožňuje utratit jiné adrese (spender).

 

1 /**
2 * @dev Nastaví `amount` jako povolenou částku pro `spender` pro tokeny volajícího.
3 *
4 * Vrací booleovskou hodnotu, která udává, zda operace proběhla úspěšně.
5 *
6 * DŮLEŽITÉ: Dejte si pozor, že změna povolení touto metodou s sebou nese riziko,
7 * že někdo může nešťastným pořadím transakcí využít jak staré, tak i nové povolení.
8 * Jedním z možných řešení, jak tento souběh zmírnit,
9 * je nejprve snížit povolení pro utrácejícího na 0 a poté nastavit
10 * požadovanou hodnotu:
11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
12 *
13 * Vyvolá událost {Approval}.
14 */
15 function approve(address spender, uint256 amount) external returns (bool);
Zobrazit vše

Funkce approve vytváří povolení. Nezapomeňte si přečíst zprávu o tom, jak může být zneužita. V Ethereu ovládáte pořadí svých vlastních transakcí, ale nemůžete ovládat pořadí, v jakém budou provedeny transakce jiných lidí, pokud neodešlete vlastní transakci až poté, co uvidíte, že transakce druhé strany proběhla.

 

1 /**
2 * @dev Přesune tokeny v množství `amount` z adresy `sender` na `recipient` pomocí
3 * mechanismu povolení. `amount` se poté odečte od povolení volajícího
4 * .
5 *
6 * Vrací booleovskou hodnotu, která udává, zda operace proběhla úspěšně.
7 *
8 * Vyvolá událost {Transfer}.
9 */
10 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
Zobrazit vše

Nakonec transferFrom použije utrácející k samotnému utracení povolené částky.

 

1
2 /**
3 * @dev Vyvolá se, když se tokeny v hodnotě `value` přesunou z jednoho účtu (`from`) na
4 * jiný (`to`).
5 *
6 * Všimněte si, že `value` může být nula.
7 */
8 event Transfer(address indexed from, address indexed to, uint256 value);
9
10 /**
11 * @dev Vyvolá se, když je povolení `spendera` pro `ownera` nastaveno
12 * voláním {approve}. `value` je nové povolení.
13 */
14 event Approval(address indexed owner, address indexed spender, uint256 value);
15}
Zobrazit vše

Tyto události jsou emitovány, když se změní stav kontraktu ERC-20.

Skutečný kontrakt

Toto je skutečný kontrakt, který implementuje standard ERC-20, převzato odtudopens in a new tab. Není určen k použití tak, jak je, ale můžete z něj děditopens in a new tab a rozšířit jej na něco použitelného.

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

 

Importní příkazy

Kromě výše uvedených definic rozhraní importuje definice kontraktu dva další soubory:

1
2import "../../GSN/Context.sol";
3import "./IERC20.sol";
4import "../../math/SafeMath.sol";
  • GSN/Context.sol jsou definice potřebné k použití OpenGSNopens in a new tab, systému, který umožňuje uživatelům bez etheru používat blockchain. Všimněte si, že se jedná o starou verzi, pokud chcete integrovat s OpenGSN, použijte tento návodopens in a new tab.
  • Knihovna SafeMathopens in a new tab, která zabraňuje aritmetickému přetečení/podtečení pro verze Solidity <0.8.0. V Solidity ≥0.8.0 aritmetické operace automaticky vracejí při přetečení/podtečení, takže SafeMath je zbytečná. Tento kontrakt používá SafeMath pro zpětnou kompatibilitu se staršími verzemi kompilátoru.

 

Tento komentář vysvětluje účel kontraktu.

1/**
2 * @dev Implementace rozhraní {IERC20}.
3 *
4 * Tato implementace je agnostická vůči způsobu vytváření tokenů. To znamená,
5 * že mechanismus dodávání musí být přidán v odvozeném kontraktu pomocí {_mint}.
6 * Obecný mechanismus viz {ERC20PresetMinterPauser}.
7 *
8 * TIP: Podrobný popis naleznete v našem průvodci
9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[Jak
10 * implementovat mechanismy dodávání].
11 *
12 * Dodržovali jsme obecné pokyny OpenZeppelin: funkce se při selhání vracejí, místo aby
13 * vracely `false`. Toto chování je nicméně konvenční
14 * a není v rozporu s očekáváními aplikací ERC20.
15 *
16 * Navíc se při volání {transferFrom} emituje událost {Approval}.
17 * To umožňuje aplikacím rekonstruovat povolení pro všechny účty jen
18 * nasloucháním zmíněných událostí. Jiné implementace EIP nemusí tyto události
19 * emitovat, protože to není vyžadováno specifikací.
20 *
21 * Nakonec byly přidány nestandardní funkce {decreaseAllowance} a {increaseAllowance},
22 * aby se zmírnily známé problémy kolem nastavování
23 * povolení. Viz {IERC20-approve}.
24 */
25
Zobrazit vše

Definice kontraktu

1contract ERC20 is Context, IERC20 {

Tento řádek specifikuje dědičnost, v tomto případě z IERC20 z výše uvedeného a Context, pro OpenGSN.

 

1
2 using SafeMath for uint256;
3

Tento řádek připojuje knihovnu SafeMath k typu uint256. Tuto knihovnu najdete zdeopens in a new tab.

Definice proměnných

Tyto definice specifikují stavové proměnné kontraktu. Tyto proměnné jsou deklarovány jako soukromé (private), ale to pouze znamená, že je nemohou číst jiné kontrakty na blockchainu. Na blockchainu neexistují žádná tajemství, software na každém uzlu má stav každého kontraktu v každém bloku. Podle konvence se stavové proměnné pojmenovávají _<něco>.

První dvě proměnné jsou mapováníopens in a new tab, což znamená, že se chovají zhruba stejně jako asociativní poleopens in a new tab, s výjimkou toho, že klíče jsou číselné hodnoty. Úložiště je přiděleno pouze pro položky, které mají hodnoty odlišné od výchozí (nula).

1 mapping (address => uint256) private _balances;

První mapování, _balances, jsou adresy a jejich příslušné zůstatky tohoto tokenu. Pro přístup k zůstatku použijte tuto syntaxi: _balances[<adresa>].

 

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

Tato proměnná, _allowances, ukládá povolení vysvětlená dříve. První index je vlastníkem tokenů a druhý je kontrakt s povolením. Pro přístup k částce, kterou může adresa A utratit z účtu adresy B, použijte _allowances[B][A].

 

1 uint256 private _totalSupply;

Jak název napovídá, tato proměnná sleduje celkovou zásobu tokenů.

 

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

Tyto tři proměnné se používají ke zlepšení čitelnosti. První dvě jsou samovysvětlující, ale _decimals není.

Na jedné straně Ethereum nemá proměnné s plovoucí desetinnou čárkou nebo zlomkové proměnné. Na druhé straně, lidé rádi dělí tokeny. Jedním z důvodů, proč se lidé usadili na zlatě jako měně, bylo to, že bylo těžké vrátit drobné, když si někdo chtěl koupit hodnotu kachny v kravě.

Řešením je sledovat celá čísla, ale počítat místo skutečného tokenu zlomkový token, který je téměř bezcenný. V případě etheru se zlomkový token nazývá wei a 10^18 wei se rovná jednomu ETH. V době psaní tohoto článku je 10 000 000 000 000 wei přibližně jeden americký nebo eurový cent.

Aplikace potřebují vědět, jak zobrazit zůstatek tokenu. Pokud má uživatel 3 141 000 000 000 000 000 wei, je to 3,14 ETH? 31,41 ETH? 3 141 ETH? V případě etheru je definováno 10^18 wei na ETH, ale pro váš token můžete zvolit jinou hodnotu. Pokud dělení tokenu nemá smysl, můžete použít hodnotu _decimals nula. Chcete-li použít stejný standard jako ETH, použijte hodnotu 18.

Konstruktor

1 /**
2 * @dev Nastaví hodnoty pro {name} a {symbol}, inicializuje {decimals} s
3 * výchozí hodnotou 18.
4 *
5 * Chcete-li vybrat jinou hodnotu pro {decimals}, použijte {_setupDecimals}.
6 *
7 * Všechny tři z těchto hodnot jsou neměnné: lze je nastavit pouze jednou během
8 * konstrukce.
9 */
10 constructor (string memory name_, string memory symbol_) public {
11 // V Solidity ≥0.7.0, 'public' je implicitní a může být vynechán.
12
13 _name = name_;
14 _symbol = symbol_;
15 _decimals = 18;
16 }
Zobrazit vše

Konstruktor se volá při prvním vytvoření kontraktu. Podle konvence jsou parametry funkce pojmenovány <něco>_.

Funkce uživatelského rozhraní

1 /**
2 * @dev Vrací název tokenu.
3 */
4 function name() public view returns (string memory) {
5 return _name;
6 }
7
8 /**
9 * @dev Vrací symbol tokenu, obvykle kratší verzi
10 * názvu.
11 */
12 function symbol() public view returns (string memory) {
13 return _symbol;
14 }
15
16 /**
17 * @dev Vrací počet desetinných míst použitých k získání jeho uživatelské reprezentace.
18 * Například, pokud se `decimals` rovná `2`, měl by být zůstatek `505` tokenů
19 * zobrazen uživateli jako `5,05` (`505 / 10 ** 2`).
20 *
21 * Tokeny obvykle volí hodnotu 18, napodobující vztah mezi
22 * etherem a wei. Toto je hodnota, kterou {ERC20} používá, pokud není
23 * volána {_setupDecimals}.
24 *
25 * POZNÁMKA: Tato informace se používá pouze pro účely _zobrazení_: v
26 * žádném případě neovlivňuje žádnou aritmetiku kontraktu, včetně
27 * {IERC20-balanceOf} a {IERC20-transfer}.
28 */
29 function decimals() public view returns (uint8) {
30 return _decimals;
31 }
Zobrazit vše

Tyto funkce, name, symbol a decimals, pomáhají uživatelským rozhraním poznat váš kontrakt, aby ho mohly správně zobrazit.

Návratový typ je string memory, což znamená návrat řetězce, který je uložen v paměti. Proměnné, jako jsou řetězce, mohou být uloženy na třech místech:

ŽivotnostPřístup ke kontraktuNáklady na palivo
PaměťVolání funkceČtení/zápisDesítky nebo stovky (vyšší pro vyšší umístění)
CalldataVolání funkcePouze pro čteníNelze použít jako návratový typ, pouze jako typ parametru funkce
ÚložištěDokud se nezměníČtení/zápisVysoké (800 pro čtení, 20k pro zápis)

V tomto případě je memory nejlepší volbou.

Čtení informací o tokenu

Jedná se o funkce, které poskytují informace o tokenu, buď o celkové zásobě, nebo o zůstatku na účtu.

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

Funkce totalSupply vrací celkovou zásobu tokenů.

 

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

Přečtěte si zůstatek na účtu. Všimněte si, že kdokoli může získat zůstatek na účtu kohokoli jiného. Nemá smysl se snažit tuto informaci skrývat, protože je stejně dostupná na každém uzlu. Na blockchainu neexistují žádná tajemství.

Převod tokenů

1 /**
2 * @dev Viz {IERC20-transfer}.
3 *
4 * Požadavky:
5 *
6 * - `recipient` nemůže být nulová adresa.
7 * - volající musí mít zůstatek alespoň `amount`.
8 */
9 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
Zobrazit vše

Funkce transfer se volá k převodu tokenů z účtu odesílatele na jiný. Všimněte si, že i když vrací booleovskou hodnotu, tato hodnota je vždy true. Pokud se převod nepodaří, kontrakt vrátí volání zpět.

 

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

Funkce _transfer dělá skutečnou práci. Jedná se o soukromou funkci, kterou mohou volat pouze jiné funkce kontraktu. Podle konvence jsou soukromé funkce pojmenovány _<něco>, stejně jako stavové proměnné.

Normálně v Solidity používáme msg.sender pro odesílatele zprávy. To však narušuje OpenGSNopens in a new tab. Chceme-li s naším tokenem povolit transakce bez etheru, musíme použít _msgSender(). Pro běžné transakce vrací msg.sender, ale pro transakce bez etheru vrací původního podepisujícího a ne kontrakt, který zprávu předal.

Funkce povolení

Jedná se o funkce, které implementují funkcionalitu povolení: allowance, approve, transferFrom a _approve. Kromě toho implementace OpenZeppelin jde nad rámec základního standardu a obsahuje některé funkce, které zlepšují bezpečnost: increaseAllowance a decreaseAllowance.

Funkce povolení

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

Funkce allowance umožňuje každému zkontrolovat jakékoli povolení.

Funkce schválení

1 /**
2 * @dev Viz {IERC20-approve}.
3 *
4 * Požadavky:
5 *
6 * - `spender` nemůže být nulová adresa.
7 */
8 function approve(address spender, uint256 amount) public virtual override returns (bool) {

Tato funkce se volá pro vytvoření povolení. Je podobná výše uvedené funkci transfer:

  • Funkce pouze volá interní funkci (v tomto případě _approve), která provádí skutečnou práci.
  • Funkce buď vrátí true (pokud je úspěšná), nebo se vrátí (pokud ne).

 

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

Používáme interní funkce, abychom minimalizovali počet míst, kde dochází ke změnám stavu. Každá funkce, která mění stav, je potenciálním bezpečnostním rizikem, které je třeba zkontrolovat z hlediska bezpečnosti. Tímto způsobem máme menší šanci, že se spleteme.

Funkce transferFrom

Toto je funkce, kterou volá utrácející, aby utratil povolenou částku. To vyžaduje dvě operace: převést utracenou částku a snížit povolenou částku o tuto částku.

1 /**
2 * @dev Viz {IERC20-transferFrom}.
3 *
4 * Vyvolá událost {Approval} udávající aktualizované povolení. To není
5 * vyžadováno EIP. Viz poznámka na začátku {ERC20}.
6 *
7 * Požadavky:
8 *
9 * - `sender` a `recipient` nemohou být nulová adresa.
10 * - `sender` musí mít zůstatek alespoň `amount`.
11 * - volající musí mít povolení pro tokeny ``sender`` alespoň
12 * `amount`.
13 */
14 function transferFrom(address sender, address recipient, uint256 amount) public virtual
15 override returns (bool) {
16 _transfer(sender, recipient, amount);
Zobrazit vše

 

Volání funkce a.sub(b, "zpráva") provádí dvě věci. Nejprve vypočítá a-b, což je nové povolení. Zadruhé zkontroluje, zda tento výsledek není záporný. Pokud je záporný, volání se vrátí s poskytnutou zprávou. Všimněte si, že když se volání vrátí, veškeré předchozí zpracování během tohoto volání se ignoruje, takže nemusíme vracet _transfer.

1 _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,
2 "ERC20: transfer amount exceeds allowance"));
3 return true;
4 }

Bezpečnostní doplňky OpenZeppelin

Je nebezpečné nastavit nenulové povolení na jinou nenulovou hodnotu, protože ovládáte pouze pořadí svých vlastních transakcí, nikoli transakcí kohokoli jiného. Představte si, že máte dva uživatele, Alici, která je naivní, a Billa, který je nečestný. Alice chce od Billa nějakou službu, která si myslí, že stojí pět tokenů – takže dává Billovi povolení na pět tokenů.

Pak se něco změní a Billova cena stoupne na deset tokenů. Alice, která stále chce službu, pošle transakci, která nastaví Billovo povolení na deset. V okamžiku, kdy Bill uvidí tuto novou transakci v poolu transakcí, pošle transakci, která utratí Aliciných pět tokenů a má mnohem vyšší cenu paliva, takže bude vytěžena rychleji. Tímto způsobem může Bill nejprve utratit pět tokenů a poté, jakmile je vytěženo nové povolení Alice, utratit dalších deset za celkovou cenu patnácti tokenů, což je více, než Alice zamýšlela autorizovat. Tato technika se nazývá front-runningopens in a new tab

Transakce AliceNonce AliceTransakce BillaNonce BillaBillovo povoleníBillův celkový příjem od Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
approve(Bill, 10)11105
transferFrom(Alice, Bill, 10)10,124015

Abyste se tomuto problému vyhnuli, tyto dvě funkce (increaseAllowance a decreaseAllowance) vám umožňují upravit povolenou částku o určitou částku. Takže pokud Bill už utratil pět tokenů, bude moci utratit jen dalších pět. V závislosti na načasování existují dva způsoby, jak to může fungovat, oba z nichž končí tím, že Bill dostane pouze deset tokenů:

A:

Transakce AliceNonce AliceTransakce BillaNonce BillaBillovo povoleníBillův celkový příjem od Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
increaseAllowance(Bill, 5)110+5 = 55
transferFrom(Alice, Bill, 5)10,124010

B:

Transakce AliceNonce AliceTransakce BillaNonce BillaBillovo povoleníBillův celkový příjem od Alice
approve(Bill, 5)1050
increaseAllowance(Bill, 5)115+5 = 100
transferFrom(Alice, Bill, 10)10,124010
1 /**
2 * @dev Atomicky zvyšuje povolení udělené `spenderu` volajícím.
3 *
4 * Toto je alternativa k {approve}, kterou lze použít jako zmírnění pro
5 * problémy popsané v {IERC20-approve}.
6 *
7 * Vyvolá událost {Approval} udávající aktualizované povolení.
8 *
9 * Požadavky:
10 *
11 * - `spender` nemůže být nulová adresa.
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 }
Zobrazit vše

Funkce a.add(b) je bezpečné sčítání. V nepravděpodobném případě, že a+b>=2^256, se neobtočí jako normální sčítání.

1
2 /**
3 * @dev Atomicky snižuje povolení udělené `spenderu` volajícím.
4 *
5 * Toto je alternativa k {approve}, kterou lze použít jako zmírnění pro
6 * problémy popsané v {IERC20-approve}.
7 *
8 * Vyvolá událost {Approval} udávající aktualizované povolení.
9 *
10 * Požadavky:
11 *
12 * - `spender` nemůže být nulová adresa.
13 * - `spender` musí mít povolení pro volajícího alespoň
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: decreased allowance below zero"));
19 return true;
20 }
Zobrazit vše

Funkce, které upravují informace o tokenu

Toto jsou čtyři funkce, které provádějí skutečnou práci: _transfer, _mint, _burn a _approve.

Funkce _transfer

1 /**
2 * @dev Přesune tokeny v množství `amount` od `odesílatele` k `příjemci`.
3 *
4 * Tato interní funkce je ekvivalentní {transfer} a může být použita
5 * například k implementaci automatických poplatků za tokeny, mechanismů slashing atd.
6 *
7 * Vyvolá událost {Transfer}.
8 *
9 * Požadavky:
10 *
11 * - `sender` nemůže být nulová adresa.
12 * - `recipient` nemůže být nulová adresa.
13 * - `sender` musí mít zůstatek alespoň `amount`.
14 */
15 function _transfer(address sender, address recipient, uint256 amount) internal virtual {
Zobrazit vše

Tato funkce, _transfer, převádí tokeny z jednoho účtu na druhý. Volá se jak transfer (pro převody z vlastního účtu odesílatele), tak transferFrom (pro použití povolení k převodu z cizího účtu).

 

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

Nikdo ve skutečnosti nevlastní adresu nula v Ethereu (to znamená, že nikdo nezná soukromý klíč, jehož odpovídající veřejný klíč je transformován na nulovou adresu). Když lidé používají tuto adresu, obvykle se jedná o softwarovou chybu – takže selžeme, pokud je jako odesílatel nebo příjemce použita nulová adresa.

 

1 _beforeTokenTransfer(sender, recipient, amount);
2

Existují dva způsoby, jak použít tento kontrakt:

  1. Použijte jej jako šablonu pro svůj vlastní kód
  2. Dědit z nějopens in a new tab a přepsat pouze ty funkce, které potřebujete upravit

Druhá metoda je mnohem lepší, protože kód OpenZeppelin ERC-20 již byl auditován a prokázal se jako bezpečný. Když používáte dědičnost, je jasné, jaké funkce upravujete, a aby lidé důvěřovali vašemu kontraktu, stačí jim auditovat pouze tyto konkrétní funkce.

Často je užitečné provést funkci pokaždé, když tokeny změní majitele. Nicméně _transfer je velmi důležitá funkce a je možné ji napsat nezabezpečeně (viz níže), takže je nejlepší ji nepřepisovat. Řešením je _beforeTokenTransfer, funkce hookopens in a new tab. Tuto funkci můžete přepsat a bude volána při každém převodu.

 

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

Toto jsou řádky, které skutečně provádějí převod. Všimněte si, že mezi nimi nic není a že odečítáme převedenou částku od odesílatele před jejím přičtením k příjemci. To je důležité, protože kdyby uprostřed bylo volání jiného kontraktu, mohlo by být použito k podvádění tohoto kontraktu. Tímto způsobem je převod atomický, uprostřed se nemůže nic stát.

 

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

Nakonec vyvoláme událost Transfer. Události nejsou přístupné chytrým kontraktům, ale kód spuštěný mimo blockchain může naslouchat událostem a reagovat na ně. Například peněženka může sledovat, kdy majitel získá více tokenů.

Funkce _mint a _burn

Tyto dvě funkce (_mint a _burn) upravují celkovou zásobu tokenů. Jsou interní a v tomto kontraktu je nevolá žádná funkce, takže jsou užitečné pouze v případě, že z kontraktu dědíte a přidáte si vlastní logiku k rozhodnutí, za jakých podmínek se mají nové tokeny razit nebo stávající pálit.

POZNÁMKA: Každý token ERC-20 má svou vlastní obchodní logiku, která určuje správu tokenů. Například kontrakt s pevnou zásobou může volat _mint pouze v konstruktoru a nikdy nevolat _burn. Kontrakt, který prodává tokeny, volá _mint, když je zaplaceno, a pravděpodobně v určitém okamžiku volá _burn, aby se zabránilo nekontrolovatelné inflaci.

1 /** @dev Vytvoří `amount` tokenů a přiřadí je `účtu`, čímž se zvýší
2 * celková zásoba.
3 *
4 * Vyvolá událost {Transfer} s `from` nastaveným na nulovou adresu.
5 *
6 * Požadavky:
7 *
8 * - `to` nemůže být nulová adresa.
9 */
10 function _mint(address account, uint256 amount) internal virtual {
11 require(account != address(0), "ERC20: mint to the zero address");
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 }
Zobrazit vše

Nezapomeňte aktualizovat _totalSupply, když se změní celkový počet tokenů.

 

1 /**
2 * @dev Zničí `amount` tokenů z `účtu`, čímž se sníží
3 * celková zásoba.
4 *
5 * Vyvolá událost {Transfer} s `to` nastaveným na nulovou adresu.
6 *
7 * Požadavky:
8 *
9 * - `účet` nemůže být nulová adresa.
10 * - `účet` musí mít alespoň `amount` tokenů.
11 */
12 function _burn(address account, uint256 amount) internal virtual {
13 require(account != address(0), "ERC20: burn from the zero address");
14
15 _beforeTokenTransfer(account, address(0), amount);
16
17 _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance");
18 _totalSupply = _totalSupply.sub(amount);
19 emit Transfer(account, address(0), amount);
20 }
Zobrazit vše

Funkce _burn je téměř identická s _mint, jen jde opačným směrem.

Funkce _approve

Toto je funkce, která skutečně specifikuje povolení. Všimněte si, že umožňuje vlastníkovi specifikovat povolení, které je vyšší než aktuální zůstatek vlastníka. To je v pořádku, protože zůstatek se kontroluje v okamžiku převodu, kdy se může lišit od zůstatku v době vytvoření povolení .

1 /**
2 * @dev Nastaví `amount` jako povolení `spenderu` pro tokeny `vlastníka`.
3 *
4 * Tato interní funkce je ekvivalentní `approve` a může být použita
5 * například k nastavení automatických povolení pro určité subsystémy atd.
6 *
7 * Vyvolá událost {Approval}.
8 *
9 * Požadavky:
10 *
11 * - `owner` nemůže být nulová adresa.
12 * - `spender` nemůže být nulová adresa.
13 */
14 function _approve(address owner, address spender, uint256 amount) internal virtual {
15 require(owner != address(0), "ERC20: approve from the zero address");
16 require(spender != address(0), "ERC20: approve to the zero address");
17
18 _allowances[owner][spender] = amount;
Zobrazit vše

 

Vyvolat událost Approval. V závislosti na tom, jak je aplikace napsána, může být kontraktu utrácejícího o schválení sděleno buď vlastníkem, nebo serverem, který naslouchá těmto událostem.

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

Upravit proměnnou Decimals

1
2
3 /**
4 * @dev Nastaví {decimals} na jinou hodnotu než výchozí 18.
5 *
6 * VAROVÁNÍ: Tuto funkci byste měli volat pouze z konstruktoru. Většina
7 * aplikací, které interagují s tokenovými kontrakty, neočekává,
8 * že se {decimals} někdy změní, a mohou fungovat nesprávně, pokud ano.
9 */
10 function _setupDecimals(uint8 decimals_) internal {
11 _decimals = decimals_;
12 }
Zobrazit vše

Tato funkce upravuje proměnnou _decimals, která se používá k tomu, aby uživatelským rozhraním sdělila, jak interpretovat částku. Měli byste ji volat z konstruktoru. Bylo by nečestné volat ji v jakémkoli následujícím bodě a aplikace nejsou navrženy tak, aby to zvládly.

Háčky

1
2 /**
3 * @dev Záchytný bod (hook), který se volá před jakýmkoli převodem tokenů. To zahrnuje
4 * ražbu a pálení.
5 *
6 * Podmínky volání:
7 *
8 * – když `from` a `to` jsou obě nenulové, bude převeden `amount` tokenů z adresy ``from``
9 * na adresu `to`.
10 * – když je `from` nulové, bude pro `to` vyraženo `amount` tokenů.
11 * – když je `to` nulové, `amount` tokenů z adresy ``from`` bude spáleno.
12 * – `from` a `to` nejsou nikdy obě nulové.
13 *
14 * Více informací o záchytných bodech (hooks) najdete v xref:ROOT:extending-contracts.adoc#using-hooks[Používání záchytných bodů].
15 */
16 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }
17}
Zobrazit vše

Toto je funkce háku, která se má volat během převodů. Zde je prázdná, ale pokud potřebujete, aby něco dělala, stačí ji přepsat.

Závěr

Pro přehled, zde jsou některé z nejdůležitějších myšlenek v tomto kontraktu (podle mého názoru se váš může lišit):

  • Na blockchainu neexistují žádná tajemství. Jakékoli informace, ke kterým má chytrý kontrakt přístup, jsou dostupné celému světu.
  • Můžete ovládat pořadí svých vlastních transakcí, ale ne to, kdy se uskuteční transakce jiných lidí. To je důvod, proč může být změna povolení nebezpečná, protože umožňuje utrácejícímu utratit součet obou povolení.
  • Hodnoty typu uint256 se obtáčejí. Jinými slovy, 0-1=2^256-1. Pokud to není požadované chování, musíte to zkontrolovat (nebo použít knihovnu SafeMath, která to udělá za vás). Všimněte si, že se to změnilo v Solidity 0.8.0opens in a new tab.
  • Provádějte všechny změny stavu určitého typu na určitém místě, protože to usnadňuje auditování. To je důvod, proč máme například _approve, které je voláno approve, transferFrom, increaseAllowance a decreaseAllowance
  • Změny stavu by měly být atomické, bez jakékoli jiné akce uprostřed (jak můžete vidět v _transfer). Je to proto, že během změny stavu máte nekonzistentní stav. Například mezi dobou, kdy odečtete ze zůstatku odesílatele, a dobou, kdy přičtete k zůstatku příjemce, existuje méně tokenů, než by mělo být. To by mohlo být potenciálně zneužito, pokud mezi nimi probíhají operace, zejména volání jiného kontraktu.

Nyní, když jste viděli, jak je napsán kontrakt OpenZeppelin ERC-20 a zejména jak je zabezpečen, jděte a pište své vlastní bezpečné kontrakty a aplikace.

Více z mé práce najdete zdeopens in a new tab.

Stránka naposledy aktualizována: 22. října 2025

Byl tento tutoriál užitečný?