Članci
Bezbednosna kontrolna lista za ERC-20 vesting ugovore
Matematika vestinga, opoziv, administratorski ključevi, prenosi tokena, Foundry testovi invarijanti i provere postavljanja za bezbedne ERC-20 vesting ugovore.
Inženjerski tim sigmacode.io9 min čitanja
Na ovoj stranici (14)
- 1. Matematika vestinga
- 2. Korisnici i opoziv
- 3. Kontrola pristupa i administratorski ključevi
- 4. Rukovanje prenosima tokena
- 5. Reentrancy i redosled poziva
- 6. Vremenske oznake
- 7. Događaji i transparentnost
- 8. Kompromisi nadogradivosti
- 9. Mehanizmi za hitne slučajeve
- 10. Testiranje
- 11. Statička analiza
- 12. Postavljanje i verifikacija
- 13. Operativne provere
- Završna napomena
Ugovori za vesting deluju jednostavno: zaključaš tokene, oslobađaš ih tokom vremena i gotovo. U praksi oni godinama drže veliki deo ponude tokena nekog projekta, koriste ih osnivači, investitori, zaposleni i multisig novčanici, a nakon postavljanja retko ih iko ponovo pogleda. Mala greška u proračunu ili u modelu ovlašćenja ostaje aktivna tokom celog perioda vestinga. Ova kontrolna lista okuplja pitanja koja postavljamo kada dizajniramo ili pregledamo ERC-20 vesting ugovor, od aritmetike do postavljanja i svakodnevnog rada.
1. Matematika vestinga#
Srž svakog vesting ugovora je jedna funkcija koja odgovara na pitanje „koliko je tokena vestirano u trenutku t?”. Gotovo svaka ozbiljna greška u vestingu nalazi se upravo ovde.
Linearni raspored i cliff#
- Definišite raspored eksplicitnim parametrima:
start,cliff,duration,totalAllocation. Izbegavajte implicitne vrednosti izvedene izblock.timestampu trenutku postavljanja. - Odlučite šta cliff znači. Uobičajeni modeli su „ništa pre clifa, zatim linearno nadoknađivanje od
start” i „ništa pre clifa, zatim jednokratni iznos, pa linearno”. Izabrani model zapišite u NatSpec i u testove. - Proverite pri kreiranju:
durationveći od nule,cliffne poslestart + duration,totalAllocationveći od nule, korisnik nije nulta adresa. - Posle
start + durationvestirani iznos mora biti tačnototalAllocation, a ne „otprilike”.
Zaokruživanje#
- Prvo množite, pa delite.
total * elapsed / durationje ispravno;total / duration * elapsedpri svakom oslobađanju neprimetno gubi tokene. - Zaokruživanje uvek treba da ide u korist ugovora: iznos koji korisnik može da preuzme zaokružuje se naniže, nikada naviše. Poslednje oslobađanje na kraju rasporeda preuzima preostale ostatke.
- Proverite prekoračenje kod velikih alokacija sa tokenima od 18 decimala. Solidity 0.8.x prekida izvršavanje pri prekoračenju, ali revert unutar
vestedAmountmože da blokira svako preuzimanje. KoristiteMath.mulDiviz OpenZeppelina ako proizvod može da postane veliki.
Početak u prošlosti ili budućnosti#
startu prošlosti je legitiman (retroaktivne dodele zaposlenima), ali znači da je veliki iznos odmah dostupan. Neka to bude izričita, proverena odluka, a ne slučajnost zbog pogrešnog parametra.startdaleko u budućnosti može biti greška u kucanju (milisekunde umesto sekundi su klasičan primer). Dodajte granice razumnosti u konstruktor ili factory, kao i u skriptu za postavljanje.
Sažeta referentna implementacija rasporeda:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
function vestedAmount(
uint256 total,
uint64 start,
uint64 cliff,
uint64 duration,
uint64 timestamp
) pure returns (uint256) {
if (timestamp < cliff) return 0;
if (timestamp >= start + duration) return total;
// Multiply before divide; mulDiv avoids intermediate overflow.
return Math.mulDiv(total, timestamp - start, duration);
}
2. Korisnici i opoziv#
- Ko može da preuzme tokene? Samo korisnik ili bilo ko u njegovo ime? Dozvoliti bilo kome da pokrene
release()je u redu sve dok tokeni uvek idu korisniku; to olakšava automatizaciju i oporavak kod izgubljenog ključa. - Da li se korisnik može promeniti? Ako može, promenu mora da pokrene trenutni korisnik (idealno kao prenos u dva koraka sa prihvatanjem) i mora se emitovati događaj. Ako je korisnik ugovor, mora moći da prima i koristi tokene.
- Model opoziva. Za svaki raspored odlučite da li je opoziv moguć. Pri opozivu već vestirani, a neoslobođeni iznos treba da ostane dostupan korisniku; samo nevestirani ostatak vraća se u treasury. Opoziv već vestiranih tokena je problem poverenja, a ne samo koda.
- Opoziv mora biti konačan. Opozvani raspored ne sme moći da se opozove dvaput, ne sme da nastavi da vestira i ne sme administratoru da omogući povlačenje više od nevestiranog ostatka.
- Više rasporeda po korisniku. Rasporede vodite po ID-u, a ne samo po adresi, kako druga dodela ne bi prepisala prvu.
3. Kontrola pristupa i administratorski ključevi#
- Popišite svaku privilegovanu funkciju: kreiranje rasporeda, opoziv, pauziranje, povlačenje viška, nadogradnju. Svaka od njih je površina napada ako je ključ kompromitovan.
- Koristite
Ownable2StepiliAccessControlsa odvojenim ulogama umesto jednog svemoćnog vlasnika. Uloga koja kreira rasporede ne mora biti ista kao uloga koja može da povlači sredstva. - Administratorske uloge držite u multisig novčaniku i razmotrite timelock za sve što premešta tokene iz ugovora.
- Administrator nikada ne sme moći da povuče tokene koji su obećani rasporedima. Vodite
totalCommittedi za višak dozvolite samo povlačenje iznosabalance - totalCommitted. - Planirajte završno stanje: da li se administratorska prava mogu odreći kada su svi rasporedi kreirani? Manje aktivnih ključeva znači manje mogućih kvarova.
4. Rukovanje prenosima tokena#
- Za svaki prenos koristite OpenZeppelin
SafeERC20. Neki tokeni ne vraćaju boolean, a drugi vraćajufalseumesto da prekinu izvršavanje. - Tokeni sa naknadom pri prenosu. Ako se ugovor finansira tokenom koji naplaćuje naknadu, primiće manje od nominalnog iznosa. Izmerite stanje pre i posle finansiranja i evidentirajte ono što je stvarno stiglo ili takve tokene izričito odbijte.
- Rebasing tokeni. Stanja koja se menjaju sama od sebe narušavaju pretpostavku
balance == committed + surplus. Ili dokumentujte da rebasing tokeni nisu podržani ili računovodstvo zasnujte na udelima. - Ako ugovor služi jednom tokenu, adresu tokena postavite kao
immutable. Prihvatanje proizvoljnih adresa tokena po rasporedu znatno povećava površinu napada. - Nikada ne dozvolite „spasavanje” vestiranog tokena generičkom funkcijom
recoverERC20bez oduzimanja obećanih iznosa.
5. Reentrancy i redosled poziva#
- Pratite obrazac checks-effects-interactions: ažurirajte
releasedpre pozivasafeTransfer. - Dodajte
nonReentrantnarelease,revokei svaku funkciju za povlačenje. Hookovi u stilu ERC-777 ili zlonameran token mogu ponovo da pozovu ugovor. - Spoljne pozive svedite na minimum. Vesting ugovor nema razloga da poziva proizvoljne adrese.
function release(uint256 scheduleId) external nonReentrant {
Schedule storage s = schedules[scheduleId];
uint256 amount = _releasable(s);
require(amount > 0, "Vesting: nothing to release");
s.released += amount; // effects first
totalCommitted -= amount;
token.safeTransfer(s.beneficiary, amount); // interaction last
emit TokensReleased(scheduleId, s.beneficiary, amount);
}
6. Vremenske oznake#
- Koristite
block.timestamp, a ne brojeve blokova. Vremena blokova razlikuju se među lancima i menjaju se tokom vremena; isti ugovor će možda kasnije biti postavljen na L2 mrežu. - Uticaj validatora na vremenske oznake ograničen je na sekunde. Za rasporede koji se mere mesecima to je nevažno, ali ne gradite logiku koja zavisi od preciznosti na nivou sekunde.
- Vremenske oznake čuvajte kao
uint64. To je dovoljno za svaki realan raspored i dobro se pakuje u storage. - Granice testirajte izričito: sekundu pre clifa, tačno na cliffu, tačno na kraju i dugo posle kraja.
7. Događaji i transparentnost#
Svaka promena stanja treba da emituje događaj: ScheduleCreated, TokensReleased, ScheduleRevoked, BeneficiaryChanged, promene uloga i pauze. Na događaje se oslanjaju indekseri, kontrolne table i vaš sopstveni tim za podršku. Uključite ID rasporeda i iznose, a ne samo adrese. Investitori i zaposleni će pitati koliko je vestirano, a događaji na lancu su najverodostojniji odgovor.
8. Kompromisi nadogradivosti#
| Opcija | Prednost | Rizik |
|---|---|---|
| Nepromenljiv ugovor | Najjača garancija za korisnike, jednostavniji audit | Greške se ne mogu ispraviti; migracija zahteva novi ugovor i sredstva |
| Nadogradivi proxy | Greške se mogu zakrpiti | Ključ za nadogradnju može da promeni svako pravilo, greške u rasporedu storagea, veći obim audita |
| Nepromenljiv ugovor uz factory | Svaki skup rasporeda je izolovan, nove verzije za nove dodele | Stare instance zadržavaju stare greške |
Za vesting je nepromenljivost često bolji podrazumevani izbor: cela svrha ugovora je da niko naknadno ne može da promeni dogovor. Ako izaberete proxy, ulogu za nadogradnju stavite iza multisiga i timelocka, koristite storage gaps ili namespaced storage i u CI-ju pokrećite OpenZeppelinove provere bezbednosti nadogradnje.
9. Mehanizmi za hitne slučajeve#
- Pauza može da zaštiti od nepoznate greške, ali pauza koja trajno blokira
releaseistovremeno je način da se korisnici zamrznu. Razmotrite ograničavanje trajanja pauze ili dozvoljavanje oslobađanja čak i dok je pauzirano samo kreiranje novih rasporeda. - Dokumentujte ko može da pauzira, pod kojim uslovima i kako će zajednica biti obaveštena.
- Izbegavajte funkcije tipa „u hitnom slučaju povuci sve”. Ako je takva funkcija neizbežna, mora biti iza timelocka i vidljiva u dokumentaciji koju čitaju investitori.
10. Testiranje#
Jedinični testovi su minimum. Kod vesting ugovora testovi zasnovani na svojstvima donose veliku dodatnu vrednost jer matematika mora da važi za svako vreme i svaki iznos.
- Jedinični testovi: svaka putanja do reverta, svaka granična vremenska oznaka, opoziv pre clifa, opoziv posle potpunog vestinga, više rasporeda za istog korisnika.
- Fuzz testovi: nasumične vrednosti
total,durationitimestamp; proverite da jevestedAmountmonoton i da nikada ne premašujetotal. - Testovi invarijanti: neka Foundry poziva
release,revoke,createScheduleivm.warpnasumičnim redosledom, a zatim proverite globalna svojstva.
Korisne invarijante:
- Zbir oslobođenih iznosa po rasporedu nikada ne premašuje njegovu alokaciju.
- Stanje tokena u ugovoru je uvek najmanje
totalCommitted. - Opozvani raspored posle toga nikada ne dobija dodatni vestirani iznos.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
contract VestingInvariants is Test {
VestingHandler handler;
function setUp() public {
handler = new VestingHandler(); // deploys token + vesting, exposes bounded actions
targetContract(address(handler));
}
function invariant_releasedNeverExceedsAllocation() public view {
uint256 n = handler.vesting().scheduleCount();
for (uint256 i; i < n; i++) {
(, uint256 total, uint256 released) = handler.vesting().scheduleInfo(i);
assertLe(released, total);
}
}
function invariant_balanceCoversCommitments() public view {
assertGe(
handler.token().balanceOf(address(handler.vesting())),
handler.vesting().totalCommitted()
);
}
}
11. Statička analiza#
Slither pokrenite pri svakoj promeni i njegove rezultate tretirajte kao listu za pregled, a ne kao signal prolaza ili pada. Kod vesting ugovora obratite pažnju na nalaze vezane za reentrancy, neproverene prenose, opasne stroge jednakosti nad stanjima i događaje koji nedostaju.
slither . --filter-paths "lib|test" --exclude-dependencies
forge test --fuzz-runs 10000
forge coverage --report summary
Predpregled uz pomoć veštačke inteligencije, na primer naš Smart Contract AI Reviewer, još je jedan brz prvi prolaz koji ističe sumnjive obrasce pre nego što kod pogleda čovek.
12. Postavljanje i verifikacija#
- Postavljanje skriptujte Foundry skriptama, a ne ručnim transakcijama. Parametri se nalaze u konfiguracionim datotekama pod kontrolom verzija i pregledaju se kao kod.
- Prvo postavite ugovor na testnu mrežu sa potpuno istom skriptom i parametrima, a zatim izvršite probni rad na forku glavne mreže.
- Izvorni kod verifikujte na block exploreru odmah posle postavljanja, sa istom verzijom kompajlera i istim podešavanjima optimizatora.
- Dvaput proverite decimale: alokacija od 1.000.000 tokena sa 18 decimala iznosi
1_000_000e18, a ne1_000_000. - Vlasništvo prenesite na multisig u istoj skripti i potvrdite da ključ kojim je obavljeno postavljanje više nema nijednu ulogu.
13. Operativne provere#
- Redovno usklađujte: zbir alokacija rasporeda umanjen za oslobođene iznose treba da odgovara obećanom iznosu i stanju ugovora.
- Pratite događaje i podesite upozorenja za neočekivane opozive, promene uloga ili pauze.
- Održavajte javni pregled rasporeda ili pregled za investitore, kako bi se na pitanja moglo odgovoriti podacima sa lanca.
- Uvežbajte ključne postupke: rotaciju potpisnika multisiga, šta uraditi ako korisnik izgubi pristup novčaniku i kako bi se komunicirala pauza.
Završna napomena#
Kontrolna lista i automatizovani pregled rano otkrivaju mnoge probleme, ali nisu zamena za nezavisni bezbednosni audit. Pre nego što vesting ugovor počne da upravlja stvarnom vrednošću, neka ga pregledaju ljudi koji ga nisu napisali.
Ako želite da vidite kako to radimo u praksi, pogledajte naš prikaz token suitea, koji uključuje vesting ugovor sa testovima, ili saznajte više o našim blockchain uslugama. Naš tim vodi tehnički lider sa više od 20 godina iskustva i rado ćemo pregledati vašu tokenomiku ili dizajn vestinga, samo nam se javite.