Analiza contractului ERC-20
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.
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: MITCopiaț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 voie3 * să le cheltuiască în numele „owner”-ului prin {transferFrom}. Acesta este implicit4 * 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ă riscul7 * ca cineva să poată folosi atât alocaţia veche, cât şi pe cea nouă, ordonând8 * greşit tranzacţiile. O posibilă soluție pentru a atenua acest risc9 * este de a reduce întâi la 0 alocația celui care cheltuiește și de a seta10 * valoarea dorită după aceea:11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-26352472912 *13 * Emite un eveniment {Approval}.14 */15 function approve(address spender, uint256 amount) external returns (bool);Afișează totCopiaț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 mecanismul3 * de alocare. „amount” este apoi dedusă din4 * 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ă totCopiați
În sfârșit, transferFrom
este folosită de cel care cheltuiește pentru a cheltui efectiv alocația.
12 /**3 * @dev Este emis când tokenurile „value” sunt mutate dintr-un („from”) cont în4 * („to”) altul.5 *6 * Țineți cont că „value” poate fi zero.7 */8 event Transfer(address indexed from, address indexed to, uint256 value);910 /**11 * @dev Emitted when the allowance of a `spender` for an `owner` is set by12 * a call to {approve}. „value” este noua alocație.13 */14 event Approval(address indexed owner, address indexed spender, uint256 value);15}Afișează totCopiaț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: MIT2pragma 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:
12import "../../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 nostru9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[Cum10 * se implementează mecanismele de furnizare].11 *12 * Am urmat îndrumările generale ale OpenZeppelin: funcţiile se inversează în schimb13 * să răspundă prin mesaj de „fals” în caz de eşec. Totuși, acest comportament este convențional14 * ș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 doar18 * 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 setarea23 * alocațiilor. Vedeți {IERC20-approve}.24 */25Afișează totCopiaț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.
12 using SafeMath for uint256;3Copiaț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} cu3 * 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 timpul8 * construcției.9 */10 constructor (string memory name_, string memory symbol_) public {11 _name = name_;12 _symbol = symbol_;13 _decimals = 18;14 }Afișează totCopiaț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 }78 /**9 * @dev Returns the symbol of the token, usually a shorter version of the10 * name.11 */12 function symbol() public view returns (string memory) {13 return _symbol;14 }1516 /**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 trebui19 * 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 dintre22 * ether și wei. Aceasta este valoarea utilizată de {ERC20}, doar dacă nu este23 * apelată {_setupDecimals}.24 *25 * OBSERVAŢIE: Această informaţie este folosită doar pentru _display_ purposes:26 * nu afectează sub nici o formă aritmetica contractului, inclusiv27 * {IERC20-balanceOf} și {IERC20-transfer}.28 */29 function decimals() public view returns (uint8) {30 return _decimals;31 }Afișează totCopiaț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 contract | Costul gazului | |
---|---|---|---|
Memorie | Apel funcție | Citire/Scriere | Zeci sau sute (mai mare pe adrese de memorie mai înalte) |
Calldata | Apel funcție | Doar citire | Inutilizabilă ca tip de răspuns, este doar un tip de parametru al funcției |
Stocare | Până la schimbare | Citire/Scriere | Ridicat (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ă totCopiaț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 este5 * 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ţin12 * `amount`.13 */14 function transferFrom(address sender, address recipient, uint256 amount) public virtual15 override returns (bool) {16 _transfer(sender, recipient, amount);Afișează totCopiaț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 Alice | Nonce-ul lui Alice | Tranzacția lui Bill | Nonce-ul lui Bill | Alocația lui Bill | Venitul total a lui Bill de la Alice |
---|---|---|---|---|---|
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 |
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 Alice | Nonce-ul lui Alice | Tranzacția lui Bill | Nonce-ul lui Bill | Alocația lui Bill | Venitul total al lui Bill de la Alice |
---|---|---|---|---|---|
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:
Tranzacția lui Alice | Nonce-ul lui Alice | Tranzacția lui Bill | Nonce-ul lui Bill | Alocația lui Bill | Venitul total al lui Bill de la Alice |
---|---|---|---|---|---|
approve(Bill, 5) | 10 | 5 | 0 | ||
increaseAllowance(Bill, 5) | 11 | 5+5 = 10 | 0 | ||
transferFrom(Alice, Bill, 10) | 10,124 | 0 | 10 |
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 pentru5 * 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ă totCopiaț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ă.
12 /**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 pentru6 * 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ţin14 * `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ă totCopiaț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ă totCopiaț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);2Copiați
Există două moduri de folosire a acestui contract:
- Să fie folosit ca model pentru propriul dvs. cod
- 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ând2 * 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ă totCopiaț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ând3 * 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");1415 _beforeTokenTransfer(account, address(0), amount);1617 _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");1718 _allowances[owner][spender] = amount;Afișează totCopiaț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 }3Copiați
Modificarea variabilei „decimals”
123 /**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. Majoritatea7 * aplicațiilor care interacționează cu contractele tokenului nu se vor aștepta ca8 * {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ă totCopiaț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
12 /**3 * @dev Hook care este apelat înaintea oricărui transfer de tokenuri. Aceasta include4 * 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ă totCopiaț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ă deapprove
,transferFrom
,increaseAllowance
, șidecreaseAllowance
- 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: @nhsz(opens in a new tab), 18 februarie 2024