Salt la conținutul principal

Analiza contractului ERC-20

solidityerc-20
Începător
Ori Pomerantz
9 martie 2021
27 minute de citit minute read

Introducere

Ca mod de utilizare a lui Ethereum dintre cele mai frecvente, un grup creează un token de tranzacționare, care este într-un fel propria lui monedă. Aceste tokenuri respectă de obicei un standard, ERC-20. Acest standard face posibilă scrierea de instrumente, cum ar fi grupurile de lichidități și portofelele, care funcționează cu toate tokenurile ERC-20. Acest articol va analiza implementarea OpenZeppelin Solidity ERC20(opens in a new tab), cât și definirea interfeței(opens in a new tab).

Este vorba de un cod sursă adnotat. Dacă doriți să implementați ERC-20, citiți acest tutorial(opens in a new tab).

Interfața

Obiectivul unui standard cum este ERC-20 este să permită implementarea mai multor tokenuri interoperabile între aplicații, precum portofelele și schimburile descentralizate. Pentru a realiza aceasta, am creat o interfață(opens in a new tab). Orice cod care trebuie să utilizeze contractul de token poate folosi aceleași definiții din interfață și poate fi compatibil cu toate contractele de token care îl folosesc, fie că este vorba de un portofel precum MetaMask, o aplicaţie dapp cum ar fi etherscan.io sau un contract diferit, cum ar fi un fond de lichidități.

Ilustrare a interfeței ERC-20

Dacă sunteți un programator experimentat, probabil vă amintiți că ați văzut construcții similare în Java(opens in a new tab) sau chiar în anteturile fișierelor C(opens in a new tab).

Aceasta este o definiție a interfeței ERC-20(opens in a new tab) din OpenZeppelin. Este traducerea standardului lizibil de către om(opens in a new tab) în codul Solidity. Bineînțeles că interfața în sine nu definește modul cum se face ceva. Aceasta este explicată în codul sursă al contractului de mai jos.

1// SPDX-License-Identifier: MIT
Copiați

Fișierele Solidity ar trebui să includă un identificator de licență. Puteți vedea lista de licențe aici(opens in a new tab). În cazul în care aveți nevoie de o altă licență, e suficient să explicați în comentarii.

1pragma solidity >=0.6.0 <0.8.0;
Copiați

Limbajul Solidity continuă să evolueze rapid și probabil că noile versiuni nu vor fi compatibile cu codul vechi. (vedeți aici(opens in a new tab)). De aceea este bine să specificați nu numai o versiune minimă a limbajului, ci și versiunea maximă, cea mai recentă cu care ați testat codul.

1/**
2 * @dev Interface al standardului ERC20, așa cum este definit în EIP.
3 */
Copiați

@dev din comentariu face parte din formatul NatSpec(opens in a new tab) utilizat pentru a produce documentația din codul sursă.

1interface IERC20 {
Copiați

Conform convenției, numele interfeței încep cu I.

1 /**
2 * @dev dă ca rezultat numărul de tokenuri existente.
3 */
4 function totalSupply() external view returns (uint256);
Copiați

Această funcție este externă, adică nu poate fi apelată decât din afara contractului(opens in a new tab). Aceasta răspunde prin numărul total de tokenuri din contract. Valoarea aceasta rezultă folosind cel mai frecvent tip din Ethereum, nesemnat pe 256 de biți (256 de biți este dimensiunea cuvântului nativ din EVM). Această funcție este şi un view, ceea ce înseamnă că nu schimbă starea, așadar poate fi executată pe un singur nod în loc să fie executată de fiecare nod din blockchain. Acest tip de funcție nu generează o tranzacție și nu costă gaz.

Observaţie: Teoretic, creatorul contractului ar putea să pară că trișează răspunzând printr-o sumă totală mai mică decât valoarea reală, ceea ce ar face ca tokenul să pară mai valoros decât este în realitate. Dar dacă ne temem de acest lucru, înseamnă că nu ținem cont de adevărata natură a blockchain-ului. Tot ceea ce se întâmplă pe blockchain poate fi verificat de fiecare nod. Pentru a realiza acest lucru, fiecare cod de limbaj mașină al fiecărui contract și stocarea acestuia sunt disponibile pe fiecare nod. Deși nu sunteți obligat să publicați codul Solidity al contractului dvs., nimeni nu vă va lua în serios dacă nu publicați codul sursă și versiunea de Solidity cu care l-ați compilat, pentru a putea fi verificat în raport cu codul de limbaj mașină pe care l-ați furnizat. De exemplu, vedeți acest contract(opens in a new tab).

1 /**
2 * @dev răspunde prin numărul de tokenuri deținute de „account”.
3 */
4 function balanceOf(address account) external view returns (uint256);
Copiați

Așa cum indică și numele, balanceOf returnează soldul unui cont. Conturile Ethereum sunt identificate în Solidity cu ajutorul tipului address, care conține 160 de biți. Este de asemenea external și view.

1 /**
2 * @dev mută tokenurile „amount” din contul apelantului în „recipient”.
3 *
4 * Răspunde printr-o valoare booleană, indicând dacă operațiunea a reușit.
5 *
6 * Emite un eveniment {Transfer}.
7 */
8 function transfer(address recipient, uint256 amount) external returns (bool);
Copiați

Funcția transfer transferă un token de la apelant la o altă adresă. Pentru că aceasta implică o schimbare de stare, nu este un view. La apelarea acestei funcții de către un utilizator, se creează o tranzacție și un cost de gaz. Totodată, se emite un eveniment Transfer pentru a-i informa pe toți din blockchain despre eveniment.

Funcția produce două tipuri de rezultate pentru două tipuri diferite de apelanți:

  • Utilizatorii care apelează funcția direct dintr-o interfață utilizator. De obicei, utilizatorul trimite o tranzacție și nu așteaptă răspunsul, care ar putea să dureze o perioadă nedeterminată. Utilizatorul poate să vadă ce s-a întâmplat căutând chitanța tranzacției (identificată prin hash-ul tranzacției) sau evenimentul Transfer.
  • Alte contracte, care apelează funcția în cadrul unei tranzacții globale. Aceste contracte obțin rezultatul imediat, deoarece se execută în aceeași tranzacție, deci pot utiliza valoarea de răspouns a funcției.

Același tip de rezultat este produs de celelalte funcții care modifică starea contractului.

Alocațiile („Allowances”) permit unui cont să cheltuiască anumite tokenuri care aparțin unui alt proprietar. Această opțiune este utilă, de exemplu, pentru contractele care acționează ca vânzători. Contractele nu pot monitoriza evenimente, deci dacă un cumpărător transferă tokenuri direct către contractul vânzătorului, acesta nu ar ști că i s-a plătit. În schimb, cumpărătorul permite contractului de vânzare să cheltuiască o anumită sumă, iar vânzătorul transferă suma respectivă. Aceasta se realizează prin intermediul unei funcții apelate de contractul vânzătorului, deci acesta știe dacă operațiunea a reușit.

1 /**
2 * @dev Răspunde prin numărul de tokenuri pe care „spender”-ul mai are voie
3 * să le cheltuiască în numele „owner”-ului prin {transferFrom}. Acesta este implicit
4 * zero.
5 *
6 * Această valoare se modifică când se apelează {approve} sau {transferFrom}.
7 */
8 function allowance(address owner, address spender) external view returns (uint256);
Copiați

Prin funcția allowance, oricine poate interoga ce alocație permite o adresă (owner) a fi cheltuită de către o altă adresă (spender).

1 /**
2 * @dev Stabilește „amount” ca fiind alocația lui „spender” în raport cu tokenurile „owner”-ului.
3 *
4 * Răspunde printr-o valoare booleană, indicând dacă operațiunea a reușit.
5 *
6 * IMPORTANT: Atenţie la faptul că schimbarea unei alocaţii prin această metodă implică riscul
7 * ca cineva să poată folosi atât alocaţia veche, cât şi pe cea nouă, ordonând
8 * greşit tranzacţiile. O posibilă soluție pentru a atenua acest risc
9 * este de a reduce întâi la 0 alocația celui care cheltuiește și de a seta
10 * valoarea dorită după aceea:
11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
12 *
13 * Emite un eveniment {Approval}.
14 */
15 function approve(address spender, uint256 amount) external returns (bool);
Afișează tot
Copiați

Funcția approve creează o alocație. Nu uitați să citiți mesajul despre cum se poate abuza de aceasta. În Ethereum vă controlați ordinea propriilor tranzacții, dar nu puteți controla ordinea în care vor fi executate tranzacțiile altora decât dacă nu vă trimiteți propria tranzacție până când nu vedeți că a avut loc tranzacția celeilalte părți.

1 /**
2 * @dev Mută tokenurile „amount” din „expeditor” în „recipient” folosind mecanismul
3 * de alocare. „amount” este apoi dedusă din
4 * alocația apelantului.
5 *
6 * Răspunde printr-o valoare booleană, indicând dacă operațiunea a reușit.
7 *
8 * Emite un eveniment {Transfer}.
9 */
10 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
Afișează tot
Copiați

În sfârșit, transferFrom este folosită de cel care cheltuiește pentru a cheltui efectiv alocația.

1
2 /**
3 * @dev Este emis când tokenurile „value” sunt mutate dintr-un („from”) cont în
4 * („to”) altul.
5 *
6 * Țineți cont că „value” poate fi zero.
7 */
8 event Transfer(address indexed from, address indexed to, uint256 value);
9
10 /**
11 * @dev Emitted when the allowance of a `spender` for an `owner` is set by
12 * a call to {approve}. „value” este noua alocație.
13 */
14 event Approval(address indexed owner, address indexed spender, uint256 value);
15}
Afișează tot
Copiați

Aceste evenimente sunt emise în momentul în care se schimbă starea contractului ERC-20.

Contractul propriu-zis

Acesta este contractul efectiv care implementează standardul ERC-20, preluat de aici(opens in a new tab). Nu este destinat a fi utilizat ca atare, dar puteți moșteni(opens in a new tab) din el pentru a-l transforma în ceva utilizabil.

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

Declarații de import

Pe lângă definițiile interfețelor de mai sus, definiția contractului importă alte două fișiere:

1
2import "../../GSN/Context.sol";
3import "./IERC20.sol";
4import "../../math/SafeMath.sol";
Copiați
  • GSN/Context.sol sunt definițiile necesare pentru a utiliza OpenGSN(opens in a new tab), un sistem care permite utilizatorilor fără ether să utilizeze blockchain-ul. De reținut că aceasta este o versiune veche; dacă doriți să o integrați cu OpenGSN, utilizați acest tutorial(opens in a new tab).
  • Biblioteca SafeMath(opens in a new tab), folosită pentru a efectua adunări și scăderi fără depășirea întregului. Acest lucru este necesar pentru că altfel o persoană ar putea, având un token, să facă în așa fel încât să cheltuiască două şi să deţină apoi 2^256-1 tokenuri.

Acest comentariu explică scopul contractului.

1/**
2 * @dev Implementarea interfeței {IERC20}.
3 *
4 * Această implementare este agnostică privind modul în care sunt create tokenurile. Aceasta înseamnă
5* că trebuie adăugat un mecanism de furnizare într-un contract derivat care utilizează {_mint}.
6 * Pentru a vedea un mecanism generic, consultaţi {ERC20PresetMinterPauser}.
7 *
8 * RECOMANDARE: Pentru o scriere detaliată, consultați ghidul nostru
9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[Cum
10 * se implementează mecanismele de furnizare].
11 *
12 * Am urmat îndrumările generale ale OpenZeppelin: funcţiile se inversează în schimb
13 * să răspundă prin mesaj de „fals” în caz de eşec. Totuși, acest comportament este convențional
14 * și nu intră în conflict cu ce preconizează aplicațiile ERC20.
15 *
16 * În plus, este emis un eveniment {Approval} la apelurile către {transferFrom}.
17 * Acest lucru permite aplicațiilor să reconstruiască alocația pentru toate conturile doar
18 * ascultând evenimentele menționate. Alte implementări ale EIP-ului pot să nu emită
19 * aceste evenimente, deoarece specificația nu impune acest lucru.
20 *
21 * În sfârşit, funcţiile non-standard {decreaseAllowance} şi {increaseAllowance}
22 * au fost adăugate pentru a atenua problemele binecunoscute legate de setarea
23 * alocațiilor. Vedeți {IERC20-approve}.
24 */
25
Afișează tot
Copiați

Definiția contractului

1contract ERC20 is Context, IERC20 {
Copiați

This line specifies the inheritance, in this case from IERC20 from above and Context, for OpenGSN.

1
2 using SafeMath for uint256;
3
Copiați

Această linie atașează biblioteca SafeMath la tipul uint256. Puteți găsi această bibliotecă aici(opens in a new tab).

Definițiile variabilelor

Aceste definiții specifică variabilele de stare ale contractului. Aceste variabile sunt declarate private, dar aceasta înseamnă doar că alte contracte de pe blockchain nu le pot citi. Nu există secrete pe blockchain, software-ul fiecărui nod are starea fiecărui contract la fiecare bloc. În mod convențional, variabilele de stare se numesc _<something>.

Primele două variabile sunt mappings(opens in a new tab), care înseamnă că se comportă aproximativ ca matricele asociative(opens in a new tab), mai puțin faptul că aceste chei sunt valori numerice. Spațiul de stocare este alocat numai datelor introduse care au valori diferite de valoarea implicită (zero).

1 mapping (address => uint256) private _balances;
Copiați

Prima variabilă de mapare este _balances și reprezintă adresele și soldurile respective ale acestui token. Pentru a accesa soldul, utilizați această sintaxă: _balances[<address>].

1 mapping (address => mapping (address => uint256)) private _allowances;
Copiați

Această variabilă, _allowances, stochează alocațiile explicate anterior. Primul index este proprietarul tokenurilor, iar al doilea este contractul cu alocația. Pentru accesarea sumei pe care adresa A o poate cheltui din contul adresei B, utilizați _allowances[B][A].

1 uint256 private _totalSupply;
Copiați

Așa cum sugerează și numele, această variabilă ține evidența stocului total de tokenuri.

1 string private _name;
2 string private _symbol;
3 uint8 private _decimals;
Copiați

Aceste trei variabile sunt utilizate pentru îmbunătățirea lizibilității. Primele două se explică de la sine, dar _decimals nu.

Pe de-o parte, Ethereum nu are variabile în virgulă mobilă sau fracționare. Pe de altă parte, oamenilor le place să poată împărți tokenurile. Un motiv pentru care oamenii au ales aurul ca monedă a fost pentru că era greu de făcut rost de mărunţiş când cineva voia să cumpere o raţă, dar avea numai valoarea pentru o vacă.

Soluția este de a ține evidența valorilor întregi, dar la numărătoare, în loc de a lua în considerare tokenul real, să considerăm un token fracționar, care aproape că nu are valoare. În cazul ether-ului, tokenul fracționar se numește wei, iar 10^18 wei sunt echivalenţi cu un ETH. În momentul scrierii, 10.000.000.000.000.000.000 de wei echivalau cu circa un cent american sau un eurocent.

Este necesar ca aplicațiile să știe cum să afișeze soldul tokenurilor. Dacă un utilizator are 3,141,000,000,000,000,000,000,000,000 wei, aceasta înseamnă 3,14 ETH? 31,41 ETH? 3.141 ETH? În cazul ether-ului, un ETH se definește ca 10^18 wei, dar pentru tokenul dvs. puteți selecta o valoare diferită. Dacă nu are sens să împărţim tokenul, puteți folosi o valoare cu _decimals de zero. Dacă doriți să utilizați același standard ca ETH, utilizați valoarea 18.

Constructorul

1 /**
2 * @dev setează valorile pentru {name} și {symbol}, inițializează {decimals} cu
3 * o valoare implicită de 18.
4 *
5 * Pentru a selecta o altă valoare pentru {decimals}, utilizați {_setupDecimals}.
6 *
7 * Toate cele trei valori sunt imuabile: ele pot fi setate o singură dată în timpul
8 * construcției.
9 */
10 constructor (string memory name_, string memory symbol_) public {
11 _name = name_;
12 _symbol = symbol_;
13 _decimals = 18;
14 }
Afișează tot
Copiați

Constructorul este apelat la crearea pentru prima dată a contractului. În mod convențional, variabilele de stare se numesc _<something>_.

Funcțiile interfeţei cu utilizatorul

1 /**
2 * @dev răspunde prin numele tokenului.
3 */
4 function name() public view returns (string memory) {
5 return _name;
6 }
7
8 /**
9 * @dev Returns the symbol of the token, usually a shorter version of the
10 * name.
11 */
12 function symbol() public view returns (string memory) {
13 return _symbol;
14 }
15
16 /**
17 * @dev Returns the number of decimals used to get its user representation.
18 * De exemplu, dacă `decimals` este egal cu `2`, un sold de `505` tokenuri ar trebui
19 * să fie afișat unui utilizator ca `5,05` (`505 / 10 ** 2`).
20 *
21 * Tokenurile optează de obicei pentru o valoare de 18, imitând relația dintre
22 * ether și wei. Aceasta este valoarea utilizată de {ERC20}, doar dacă nu este
23 * apelată {_setupDecimals}.
24 *
25 * OBSERVAŢIE: Această informaţie este folosită doar pentru _display_ purposes:
26 * nu afectează sub nici o formă aritmetica contractului, inclusiv
27 * {IERC20-balanceOf} și {IERC20-transfer}.
28 */
29 function decimals() public view returns (uint8) {
30 return _decimals;
31 }
Afișează tot
Copiați

Aceste funcții, name, symbol și decimals, ajută interfețele cu utilizatorul să știe despre contractul dvs. în așa fel încât să-l poată afișa corect.

Tipul de răspuns este string memory, ceea ce înseamnă că răspunde printr-un șir care este stocat în memorie. Variabile, cum ar fi șirurile de caractere, pot fi stocate în trei locații:

Durată de viațăAcces la contractCostul gazului
MemorieApel funcțieCitire/ScriereZeci sau sute (mai mare pe adrese de memorie mai înalte)
CalldataApel funcțieDoar citireInutilizabilă ca tip de răspuns, este doar un tip de parametru al funcției
StocarePână la schimbareCitire/ScriereRidicat (800 pentru citire, 20k pentru scriere)

În acest caz, memoria este cea mai bună alegere.

Citirea informațiilor tokenului

Acestea sunt funcții care furnizează informații despre token, fie că este vorba de cantitatea totală, fie de soldul unui cont.

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

Funcția totalSupply răspunde prin cantitatea totală de tokenuri.

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

Citirea soldului unui cont. Rețineți că oricine are permisiunea de a obține soldul contului altcuiva. Nu are nici un sens să ascundeți această informație, pentru că ea este oricum disponibilă pe fiecare nod. Nu există secrete pe blockchain.

Transferul de tokenuri

1 /**
2 * @dev See {IERC20-transfer}.
3 *
4 * Cerințe:
5 *
6 * - „recipient” nu poate să fie adresa zero.
7 * - apelantul trebuie să aibă un sold de cel puţin `amount`.
8 */
9 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
Afișează tot
Copiați

Funcția transfer este apelată pentru a transfera tokenuri din contul expeditorului către un alt cont. Rețineți că, deși valoarea de răspuns este o valoare de tip boolean, aceasta este întotdeauna adevărată. Dacă transferul eșuează, contractul returnează apelul.

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

Funcția _transfer este cea care efectuează cu adevărat munca. Este o funcție privată care nu poate fi apelată decât de alte funcții de contract. Prin convenție, funcțiile private se numesc _<something>, ca și variabilele de stare.

În mod normal, în Solidity folosim msg.sender pentru expeditorul mesajului. Totuși, acest lucru întrerupe OpenGSN(opens in a new tab). Dacă vrem să permitem tranzacții fără ether cu tokenul nostru, trebuie să folosim _msgSender(). Acesta răspunde cu msg.sender pentru tranzacțiile normale, însă pentru cele fără ether răspunde cu semnatarul original, și nu cu contractul care a transmis mesajul.

Funcțiile de alocație

Acestea sunt funcțiile care implementează funcționalitatea alocației: allowance, approve, transferFrom și _approve. În plus, implementarea OpenZeppelin depășește standardul de bază prin includerea unor functionalităţi care îmbunătățesc securitatea: increaseAllowance și decreaseAllowance.

Funcția „allowance”

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

Funcția allowance permite tuturor să verifice orice alocație.

Funcția „approve”

1 /**
2 * @dev See {IERC20-approve}.
3 *
4 * Cerințe:
5 *
6 * - „spender" nu poate să fie adresa zero.
7 */
8 function approve(address spender, uint256 amount) public virtual override returns (bool) {
Copiați

Această funcție este apelată pentru a crea o alocație. Este similară cu funcția transfer de mai sus:

  • Funcția nu face decât să apeleze o funcție internă (în acest caz, _approve) care face adevărata muncă.
  • Funcția răspunde prin true (în caz de succes) sau revine (în caz de eșec).
1 _approve(_msgSender(), spender, amount);
2 return true;
3 }
Copiați

Folosim funcții interne pentru a minimiza numărul de locuri unde au loc schimbări de stare. Orice funcție care schimbă starea reprezintă un risc potențial și trebuie auditată pe motive de securitate. În acest fel minimizăm șansele de a greşi.

Funcția „transferFrom”

Aceasta este funcția pe care spenderul o apelează pentru a cheltui o alocație. Aceasta necesită două operațiuni: transferul sumei care se cheltuiește și reducerea alocației cu aceeași sumă.

1 /**
2 * @dev See {IERC20-transferFrom}.
3 *
4 * Emite un eveniment de {Approval}, indicând alocaţia actualizată. Aceasta nu este
5 * o cerinţă pentru EIP. A se vedea observaţia de la începutul {ERC20}.
6 *
7 * Cerințe:
8 *
9 * - „sender” şi „recipient” nu pot fi adresa zero.
10 * - `apelantul trebuie să aibă un sold de cel puţin `amount`.
11 * - apelantul trebuie să aibă alocaţia pentru tokenurile `sender``-ului, de cel puţin
12 * `amount`.
13 */
14 function transferFrom(address sender, address recipient, uint256 amount) public virtual
15 override returns (bool) {
16 _transfer(sender, recipient, amount);
Afișează tot
Copiați

Funcția de apel a.sub(b, "message") face două lucruri. În primul rând, calculează a-b, care este noua alocație. În al doilea rând, verifică dacă rezultatul găsit nu este negativ. Dacă este negativ, apelul este returnat cu mesajul furnizat. Rețineți că atunci când un apel este returnat, orice procesare efectuată anterior pe durata apelului este ignorată, deci nu este nevoie să anulăm _transfer.

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

Suplimente de siguranță OpenZeppelin

Este periculos să setați o valoare diferită de zero la o altă valoare diferită de zero, deoarece nu controlați decât ordinea propriilor tranzacții, și nu pe cea a celorlalți. Să ne imaginăm că avem doi utilizatori, Alice, care este naivă, și Bill, care este necinstit. Alice vrea ca Bill să-i facă un serviciu despre care crede că valorează cinci tokenuri - de aceea îi dă lui Bill o alocație de cinci tokenuri.

În acel moment intervine ceva și prețul cerut de Bill crește la zece tokenuri. Alice, care vrea serviciul chiar și la acest preț, îi trimite o tranzacție care stabilește alocația lui Bill la zece. Imediat ce Bill vede noua tranzacție în fondul de tranzacții, trimite o tranzacție care cheltuiește cele cinci tokenuri ale lui Alice la un preț mai mare de gaz, pentru a fi minată mai repede. Astfel, Bill poate să cheltuiască primele cinci jetoane, iar odată ce noua alocație a lui Alice este minată, cheltuiește alte zece, pentru un preţ total de cincisprezece jtokenuri, mai mult decât era intenţia lui Alice să autorizeze. This technique is called front-running(opens in a new tab)

Tranzacția lui AliceNonce-ul lui AliceTranzacția lui BillNonce-ul lui BillAlocația lui BillVenitul total a lui Bill de la Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
approve(Bill, 10)11105
transferFrom(Alice, Bill, 10)10,124015

Pentru a evita această problemă, aceste două funcții (increaseAllowance și decreaseAllowance) vă permit să modificați alocația cu o anumită valoare. Deci dacă Bill a cheltuit deja cinci tokenuri, el va putea să cheltuiască numai încă alte cinci. Depending on the timing, there are two ways this can work, both of which end with Bill only getting ten tokens:

A:

Tranzacția lui AliceNonce-ul lui AliceTranzacția lui BillNonce-ul lui BillAlocația lui BillVenitul total al lui Bill de la Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
increaseAllowance(Bill, 5)110+5 = 55
transferFrom(Alice, Bill, 5)10,124010

B:

Tranzacția lui AliceNonce-ul lui AliceTranzacția lui BillNonce-ul lui BillAlocația lui BillVenitul total al lui Bill de la Alice
approve(Bill, 5)1050
increaseAllowance(Bill, 5)115+5 = 100
transferFrom(Alice, Bill, 10)10,124010
1 /**
2 * @dev Creşte în mod atomic alocaţia acordată pentru `spender` de către apelant.
3 *
4 * Aceasta este o alternativă la {approve} care poate fi utilizată ca atenuare pentru
5 * probleme descrise în {IERC20-approve}.
6 *
7 * Emite un eveniment de {Approval}, indicând alocaţia actualizată.
8 *
9 * Cerințe:
10 *
11 * - „spender" nu poate să fie adresa zero.
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 }
Afișează tot
Copiați

Funcția a.add(b) este o adunare sigură („safe add”). În cazul improbabil că a+b>=2^256, nu va executa o buclă, așa cum face o adunare normală.

1
2 /**
3 * @dev Creşte în mod atomic alocaţia acordată pentru `spender` de către apelant.
4 *
5 * Aceasta este o alternativă la {approve} care poate fi utilizată ca atenuare pentru
6 * probleme descrise în {IERC20-approve}.
7 *
8 * Emite un eveniment de {Approval}, indicând alocaţia actualizată.
9 *
10 * Cerințe:
11 *
12 * - „spender" nu poate să fie adresa zero.
13 * - `spender` trebuie să aibă o alocaţie pentru apelant de cel puţin
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 }
Afișează tot
Copiați

Funcții care modifică informațiile despre tokenuri

Există patru funcții care efeectuează cu adevărat munca: _transfer, _mint, _burn, și _approve.

Funcția _transfer {#_transfer}

1 /**
2 * @dev Mută tokenurile `amount` de la `expeditor` la `recipient`.
3 *
4 * Această funcție internă este echivalentă cu {transfer}, și poate fi utilizată,
5 * de exemplu, pentru a stabili alocații automate, pentru anumite subsisteme etc.
6 *
7 * Emite un eveniment {Transfer}.
8 *
9 * Cerințe:
10 *
11 * - „sender" nu poate să fie adresa zero.
12 * - „recipient” nu poate să fie adresa zero.
13 * - `sender` trebuie să aibă un sold de cel puţin `amount`.
14 */
15 function _transfer(address sender, address recipient, uint256 amount) internal virtual {
Afișează tot
Copiați

Această funcție, _transfer, transferă tokenuri dintr-un cont în altul. Este apelată atât de funcția transfer (pentru transferuri din contul propriu al expeditorului), cât și de funcția transferFrom (pentru utilizarea alocațiilor la efectuarea de transferuri din contul altcuiva).

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

Nimeni nu posedă de fapt adresa zero în Ethereum ( adică nimeni nu cunoaște o cheie privată a cărei cheie publică asociată se transformă în adresa zero). Atunci când cineva folosește această adresă este un bug de software - deci tranzacția eșuează dacă folosim adresa zero ca expeditor sau destinatar.

1 _beforeTokenTransfer(sender, recipient, amount);
2
Copiați

Există două moduri de folosire a acestui contract:

  1. Să fie folosit ca model pentru propriul dvs. cod
  2. Să moșteniți din el(opens in a new tab) și să schimbați numai acele funcții pe care trebuie să le modificați

A doua metodă este mult mai bună, întrucât codul OpenZeppelin ERC-20 a fost deja auditat și s-a dovedit a fi sigur. Când utilizați moștenirea, este clar care sunt funcțiile de modificat și pentru a avea încredere în contractul dvs., persoanele care se ocupă trebuie să auditeze numai aceste funcții.

Este adesea util să executați o funcție de fiecare dată când se schimbă proprietarul tokenurilor. Totuși,_transfer este o funcție foarte importantă și este posibil să fie redactată în mod nesecurizat (a se vedea mai jos), de aceea este mai bine să nu o suprascrieţi. Soluția este _beforeTokenTransfer, o funcție „hook”(opens in a new tab). Puteți suprascrie această funcție, dar ea va fi apelată la fiecare transfer.

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

Acestea sunt liniile care execută practic transferul. Observați că nu există nimic între ele și că scădem valoarea transferată de la expeditor înainte de a o adăuga la destinatar. Acest lucru este important, deoarece dacă ar fi existat între aceştia un apel la un alt contract, putea fi folosit pentru a frauda acest contract. Astfel, transferul este atomic, nimic nu se poate întâmpla în timp ce se petrece.

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

În cele din urmă, va emite evenimentul Transfer. Evenimentele nu sunt accesibile contractelor inteligente, dar codul care rulează în afara blockchain-ului poate să detecteze prin ascultare evenimente și să reacționeze la ele. De exemplu, un portofel poate monitoriza când proprietarul obține mai multe tokenuri.

Funcțiile _mint și _burn {#_mint-and-_burn}

Cele două funcții (_mint și _burn) modifică numărul total de tokenuri furnizate. Deoarece acestea sunt interne, nu există nicio funcție care să le apeleze în acest contract, iar ele sunt utile numai dacă moșteniți din contract și adăugați propria logică prin care să decideți în ce condiții să emiteţi noi tokenuri sau să le ardeți pe cele existente.

NOTĂ: Fiecare jeton ERC-20 are propria sa logică de activitate, care dictează gestionarea tokenului. De exemplu, un contract ce furnizează un număr fix ar putea apela _mint(emiterea) numai în constructor și nu ar putea niciodată apela _burn(arderea). Un contract care vinde tokenuri va apela _mint(emiterea) când este plătit și probabil va apela _burn(arderea) la un moment dat pentru a evita inflația.

1 /** @dev Creează tokenuri în valoare de `amount` și le atribuie în `account`, crescând
2 * cantitatea totală furnizată.
3 *
4 * Emite un eveniment {Transfer} cu `from` setat la adresa zero.
5 *
6 * Cerințe:
7 *
8 * - „to” nu poate să fie adresa zero.
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 }
Afișează tot
Copiați

Aveţi grijă să actualizați _totalSupply atunci când se modifică numărul de tokenuri.

1 /**
2 * @dev Distruge `amount` de tokenuri din `account`, reducând
3 * cantitatea totală furnizată.
4 *
5 * Emite un eveniment {Transfer} cu `to` setat la adresa zero.
6 *
7 * Cerințe:
8 *
9 * - „account” nu poate să fie adresa zero.
10 * - `cont` trebuie să aibă cel puţin tokenuri în valoare de `amount`.
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 }
Afișează tot

Funcția _burn este identică aproape cu _mint, cu excepția faptului că funcționează în sens invers.

Funcția _approve {#_approve}

Aceasta este funcția care specifică de fapt alocațiile. Rețineți că permite unui proprietar să specifice o alocație mai mare decât soldul curent al proprietarului. Acest lucru este în regulă, deoarece soldul este verificat în momentul transferului, când ar putea fi diferit de cel de la momentul în care a fost creată alocația.

1 /**
2 * @dev Stabilește „amount” ca fiind alocația lui „spender” în raport cu tokenurile „owner”-ului.
3 *
4 * Această funcție internă este echivalentă cu „approve” și poate fi utilizată,
5 * de exemplu, pentru a stabili alocații automate pentru anumite subsisteme etc.
6 *
7 * Emite un eveniment {Approval}.
8 *
9 * Cerințe:
10 *
11 * - „owner” nu poate să fie adresa zero.
12 * - „spender” nu poate să fie adresa zero.
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;
Afișează tot
Copiați

Emite un eveniment Approval. În funcție de felul cum este scrisă aplicația, contractul spenderului poate să fie anunţat despre aprobare fie de către proprietar, fie de serverul care ascultă aceste evenimente.

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

Modificarea variabilei „decimals”

1
2
3 /**
4 * @dev Definește {decimals} la o altă valoare decât cea implicită de 18.
5 *
6 * AVERTISMENT: Această funcție trebuie apelată numai din constructor. Majoritatea
7 * aplicațiilor care interacționează cu contractele tokenului nu se vor aștepta ca
8 * {decimals} să se schimbe vreodată și ar putea să funcționeze incorect dacă s-ar întâmpla acest lucru.
9 */
10 function _setupDecimals(uint8 decimals_) internal {
11 _decimals = decimals_;
12 }
Afișează tot
Copiați

Această funcție modifică variabila _decimals, care este utilizată pentru a indica interfeței cu utilizatorul cum să interpreteze valorile. Ar trebui să o apelați din constructor. Ar fi necinstit să fie apelată în orice moment ulterior, iar aplicațiile nu sunt concepute să o gestioneze.

Hook-uri

1
2 /**
3 * @dev Hook care este apelat înaintea oricărui transfer de tokenuri. Aceasta include
4 * emiterea și arderea.
5 *
6 * Condiţii de apelare:
7 *
8 * - când `from` şi `to` sunt amândouă diferite de zero, `amount` din token-urile `from`
9 * vor fi transferate la `to`.
10 * - când `from` este zero, vor fi emise tokenuri în valoare de `amount` pentru `to`.
11 * - când `to` este zero, se va arde un `amount` din tokenurile `from`.
12 * - `from` şi `to` nu sunt niciodată ambele zero.
13 *
14 * Pentru a afla mai multe despre hook-uri, consultaţi xref:ROOT:extending-contracts.adoc#using-hooks[Folosind Hooks].
15 */
16 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }
17}
Afișează tot
Copiați

Aceasta este funcția „hook” care va fi apelată în timpul transferurilor. It is empty here, but if you need it to do something you just override it.

Concluzie

În recapitulare, iată câteva dintre cele mai importante idei din acest contract (după părerea mea - părerea dvs. ar putea să fie diferită):

  • Nu există secrete pe blockchain. Orice informație pe care o poate accesa un contract inteligent este disponibilă tuturor.
  • Puteți controla ordinea propriilor tranzacții, dar nu și momentul în care au loc tranzacțiile altora. Acesta este motivul pentru care schimbarea unei alocații poate fi periculoasă, deoarece permite spenderului să cheltuiască ambele alocații.
  • Valorile de tip uint256 formează bucle. Cu alte cuvinte, 0-1=2^256-1. Dacă acesta nu este comportamentul dorit, trebuie să verificați acest lucru (sau să folosiți bibliotecile SafeMath care o vor face pentru dvs.). De reținut că aceasta s-a schimbat în Solidity 0.8.0(opens in a new tab).
  • Efectuați toate schimbările de stare de un anumit tip într-un anumit loc, deoarece facilitează auditul. Acesta este motivul pentru care avem, de exemplu, _approve, care este apelată de approve, transferFrom, increaseAllowance, și decreaseAllowance
  • Schimbările de stare ar trebui să fie atomice, fără nicio intervenție în timp ce au loc (așa cum se poate vedea în _transfer). Aceasta deoarece în timpul unei schimbări de stare, aveți o stare instabilă. De exemplu, între momentul în care deduceți din soldul expeditorului și cel în care adăugați în soldul destinatarului, există mai puține tokenuri decât ar trebui să existe. Se poate abuza de această situație dacă ar exista operații între aceste momente, în special apeluri către un alt contract.

Iar acum, odată ce ați văzut cum este scris un contract OpenZeppelin ERC-20 și mai ales cum este securizat, porniţi să vă scrieţi propriile contracte și aplicații securizate.

Ultima modificare: @nicklcanada(opens in a new tab), 15 august 2023

A fost util acest tutorial?