Uvidi
Sigurnosna kontrolna lista za ERC-20 vesting ugovore
Matematika vestinga, opoziv, administratorski ključevi, rukovanje tokenima, Foundry testovi invarijanti i provjere pri implementaciji za sigurne 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 transferima tokena
- 5. Reentrancy i redoslijed 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. Implementacija i verifikacija
- 13. Operativne provjere
- Završna napomena
Ugovori za vesting izgledaju jednostavno: tokeni se zaključaju, oslobađaju se tokom vremena i to je sve. U praksi oni godinama drže veliki dio ponude tokena nekog projekta, diraju ih osnivači, investitori, zaposlenici i multisig novčanici, a nakon implementacije rijetko ih ko ponovo pogleda. Mala greška u izračunu ili u modelu ovlasti ostaje aktivna tokom cijelog perioda vestinga. Ova kontrolna lista okuplja pitanja koja postavljamo kada dizajniramo ili pregledamo ERC-20 vesting ugovor, od aritmetike do implementacije 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 ovdje.
Linearni raspored i cliff#
- Definišite raspored eksplicitnim parametrima:
start,cliff,duration,totalAllocation. Izbjegavajte implicitne vrijednosti izvedene izblock.timestampu trenutku implementacije. - Odlučite šta cliff znači. Uobičajeni modeli su „ništa prije clifa, zatim linearno nadoknađivanje od
start" i „ništa prije clifa, zatim jednokratni iznos, pa linearno". Odabrani model zapišite u NatSpec i u testove. - Provjerite pri kreiranju:
durationveći od nule,cliffne poslijestart + duration,totalAllocationveći od nule, korisnik nije nulta adresa. - Poslije
start + durationvestirani iznos mora biti tačnototalAllocation, a ne „otprilike".
Zaokruživanje#
- Prvo množite, pa dijelite.
total * elapsed / durationje ispravno;total / duration * elapsedpri svakom oslobađanju neprimjetno gubi tokene. - Zaokruživanje uvijek treba ići u korist ugovora: iznos koji korisnik može preuzeti zaokružuje se prema dolje, nikada prema gore. Posljednje oslobađanje na kraju rasporeda preuzima preostale ostatke.
- Provjerite preljev kod velikih alokacija sa tokenima od 18 decimala. Solidity 0.8.x prekida izvršavanje pri preljevu, ali revert unutar
vestedAmountmože blokirati svako preuzimanje. KoristiteMath.mulDiviz OpenZeppelina ako proizvod može postati velik.
Početak u prošlosti ili budućnosti#
startu prošlosti je legitiman (retroaktivne dodjele zaposlenicima), ali znači da je veliki iznos odmah dostupan. Neka to bude izričita, provjerena odluka, a ne slučajnost zbog pogrešnog parametra.startdaleko u budućnosti može biti tipfeler (milisekunde umjesto sekundi su klasičan primjer). Dodajte granice razumnosti u konstruktor ili factory te u skriptu za implementaciju.
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 preuzeti tokene? Samo korisnik ili bilo ko u njegovo ime? Dopustiti bilo kome da pokrene
release()je u redu sve dok tokeni uvijek idu korisniku; to olakšava automatizaciju i oporavak pri izgubljenom ključu. - Može li se korisnik promijeniti? Ako može, promjenu mora pokrenuti trenutni korisnik (idealno kao transfer u dva koraka sa prihvatanjem) i mora se emitirati događaj. Ako je korisnik ugovor, mora moći primati i koristiti tokene.
- Model opoziva. Za svaki raspored odlučite je li opoziv moguć. Pri opozivu već vestirani, a neoslobođeni iznos treba ostati dostupan korisniku; samo nevestirani ostatak vraća se u treasury. Opoziv već vestiranih tokena je problem povjerenja, a ne samo koda.
- Opoziv mora biti konačan. Opozvani raspored ne smije se moći opozvati dvaput, ne smije nastaviti vestirati i ne smije administratoru omogućiti povlačenje više od nevestiranog ostatka.
- Više rasporeda po korisniku. Rasporede vodite po ID-u, a ne samo po adresi, kako druga dodjela ne bi prebrisala 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 predstavlja površinu napada ako je ključ kompromitovan.
- Koristite
Ownable2StepiliAccessControlsa odvojenim ulogama umjesto jednog svemogućeg vlasnika. Uloga koja kreira rasporede ne mora biti ista kao uloga koja može povlačiti sredstva. - Administratorske uloge držite u multisig novčaniku i razmislite o timelocku za sve što premješta tokene iz ugovora.
- Administrator nikada ne smije moći povući tokene koji su obećani rasporedima. Vodite
totalCommittedi za višak dopustite samo povlačenje iznosabalance - totalCommitted. - Planirajte završno stanje: mogu li se administratorska prava odreći kada su svi rasporedi kreirani? Manje aktivnih ključeva znači manje mogućih kvarova.
4. Rukovanje transferima tokena#
- Za svaki transfer koristite OpenZeppelin
SafeERC20. Neki tokeni ne vraćaju boolean, a drugi vraćajufalseumjesto da prekinu izvršavanje. - Tokeni sa naknadom pri transferu. Ako se ugovor finansira tokenom koji naplaćuje naknadu, primit će manje od nominalnog iznosa. Izmjerite stanje prije i poslije finansiranja i evidentirajte ono što je stvarno stiglo ili takve tokene izričito odbijte.
- Rebasing tokeni. Stanja koja se mijenjaju sama od sebe narušavaju pretpostavku
balance == committed + surplus. Ili dokumentirajte da rebasing tokeni nisu podržani ili računovodstvo zasnivajte na udjelima. - 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 dopustite „spašavanje" vestiranog tokena generičkom funkcijom
recoverERC20bez oduzimanja obećanih iznosa.
5. Reentrancy i redoslijed poziva#
- Slijedite obrazac checks-effects-interactions: ažurirajte
releasedprije pozivasafeTransfer. - Dodajte
nonReentrantnarelease,revokei svaku funkciju za povlačenje. Hookovi u stilu ERC-777 ili zlonamjerni token mogu ponovo pozvati ugovor. - Vanjske pozive svedite na minimum. Vesting ugovor nema razloga pozivati 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 mijenjaju se tokom vremena; isti ugovor možda će kasnije biti implementiran na L2 mreži. - Uticaj validatora na vremenske oznake ograničen je na sekunde. Za rasporede koji se mjere mjesecima to je nevažno, ali ne gradite logiku koja zavisi od preciznosti na nivou sekunde.
- Vremenske oznake pohranjujte kao
uint64. To je dovoljno za svaki realan raspored i dobro se pakuje u storage. - Granice testirajte izričito: sekundu prije clifa, tačno na cliffu, tačno na kraju i dugo poslije kraja.
7. Događaji i transparentnost#
Svaka promjena stanja treba emitirati događaj: ScheduleCreated, TokensReleased, ScheduleRevoked, BeneficiaryChanged, promjene uloga i pauze. Na događaje se oslanjaju indekseri, nadzorne ploče i vaš vlastiti tim za podršku. Uključite ID rasporeda i iznose, a ne samo adrese. Investitori i zaposlenici pitat će koliko je vestirano, a događaji na lancu su najvjerodostojniji odgovor.
8. Kompromisi nadogradivosti#
| Opcija | Prednost | Rizik |
|---|---|---|
| Nepromjenjiv ugovor | Najjača garancija za korisnike, jednostavniji audit | Greške se ne mogu ispraviti; migracija zahtijeva novi ugovor i sredstva |
| Nadogradivi proxy | Greške se mogu zakrpati | Ključ za nadogradnju može promijeniti svako pravilo, greške u rasporedu storagea, veći opseg audita |
| Nepromjenjiv ugovor uz factory | Svaki skup rasporeda je izoliran, nove verzije za nove dodjele | Stare instance zadržavaju stare greške |
Za vesting je nepromjenjivost često bolji zadani izbor: cijela svrha ugovora jeste da niko naknadno ne može promijeniti dogovor. Ako odaberete proxy, ulogu za nadogradnju stavite iza multisiga i timelocka, koristite storage gaps ili namespaced storage te u CI-ju pokrećite OpenZeppelinove provjere sigurnosti nadogradnje.
9. Mehanizmi za hitne slučajeve#
- Pauza može zaštititi od nepoznate greške, ali pauza koja trajno blokira
releaseujedno je način da se korisnici zamrznu. Razmislite o ograničavanju trajanja pauze ili o dopuštanju oslobađanja čak i dok je pauzirano samo kreiranje novih rasporeda. - Dokumentirajte ko može pauzirati, pod kojim uslovima i kako će zajednica biti obaviještena.
- Izbjegavajte funkcije tipa „u hitnom slučaju povuci sve". Ako je takva funkcija neizbjež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 dodanu vrijednost jer matematika mora vrijediti za svako vrijeme i svaki iznos.
- Jedinični testovi: svaki put do reverta, svaka granična vremenska oznaka, opoziv prije clifa, opoziv nakon potpunog vestinga, više rasporeda za istog korisnika.
- Fuzz testovi: nasumične vrijednosti
total,durationitimestamp; provjerite da jevestedAmountmonoton i da nikada ne premašujetotal. - Testovi invarijanti: neka Foundry poziva
release,revoke,createScheduleivm.warpnasumičnim redoslijedom, a zatim provjerite globalna svojstva.
Korisne invarijante:
- Zbir oslobođenih iznosa po rasporedu nikada ne premašuje njegovu alokaciju.
- Stanje tokena u ugovoru uvijek je najmanje
totalCommitted. - Opozvani raspored poslije 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 promjeni i njegove rezultate tretirajte kao listu za pregled, a ne kao signal prolaza ili pada. Kod vesting ugovora obratite pažnju na nalaze vezane uz reentrancy, neprovjerene transfere, opasne stroge jednakosti nad stanjima i nedostajuće događaje.
slither . --filter-paths "lib|test" --exclude-dependencies
forge test --fuzz-runs 10000
forge coverage --report summary
Predpregled uz pomoć umjetne inteligencije, na primjer naš Smart Contract AI Reviewer, još je jedan brzi prvi prolaz koji ističe sumnjive obrasce prije nego što kod pogleda čovjek.
12. Implementacija i verifikacija#
- Implementaciju skriptirajte Foundry skriptama, a ne ručnim transakcijama. Parametri se nalaze u konfiguracijskim fajlovima pod kontrolom verzija i pregledaju se kao kod.
- Prvo implementirajte na testnu mrežu s potpuno istom skriptom i parametrima, a zatim napravite probni rad na forku glavne mreže.
- Izvorni kod verificirajte na block exploreru odmah nakon implementacije, sa istom verzijom kompajlera i istim postavkama optimizatora.
- Dvaput provjerite 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 obavljena implementacija više nema nijednu ulogu.
13. Operativne provjere#
- Redovno usklađujte: zbir alokacija rasporeda umanjen za oslobođene iznose treba odgovarati obećanom iznosu i stanju ugovora.
- Pratite događaje i postavite upozorenja za neočekivane opozive, promjene uloga ili pauze.
- Održavajte javni pregled rasporeda ili pregled za investitore, kako bi se na pitanja moglo odgovoriti podacima sa lanca.
- Uvježbajte ključne postupke: rotaciju potpisnika multisiga, šta učiniti ako korisnik izgubi pristup novčaniku i kako bi se saopćila pauza.
Završna napomena#
Kontrolna lista i automatizirani pregled rano otkrivaju mnoge probleme, ali nisu zamjena za nezavisni sigurnosni audit. Prije nego što vesting ugovor počne upravljati stvarnom vrijednošću, neka ga pregledaju ljudi koji ga nisu napisali.
Ako želite vidjeti 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 tech lead sa više od 20 godina iskustva i rado ćemo pregledati vašu tokenomiku ili dizajn vestinga — samo nam se javite.