Wissen
Sicherheits-Checkliste für ERC-20-Vesting-Verträge
Vesting-Mathematik, Widerruf, Admin-Schlüssel, Token-Transfers, Foundry-Invariantentests und Deployment-Prüfungen für sichere ERC-20-Vesting-Verträge.
Engineering-Team von sigmacode.io9 Min. Lesezeit
Auf dieser Seite (14)
- 1. Vesting-Mathematik
- 2. Begünstigte und Widerruf
- 3. Zugriffskontrolle und Admin-Schlüssel
- 4. Umgang mit Token-Transfers
- 5. Reentrancy und Aufrufreihenfolge
- 6. Zeitstempel
- 7. Events und Transparenz
- 8. Abwägungen bei der Upgradefähigkeit
- 9. Notfallmechanismen
- 10. Tests
- 11. Statische Analyse
- 12. Deployment und Verifizierung
- 13. Betriebliche Prüfungen
- Abschließender Hinweis
Vesting-Verträge wirken einfach: Token sperren, über die Zeit freigeben, fertig. In der Praxis halten sie jedoch über Jahre einen großen Teil des Token-Angebots eines Projekts, sie werden von Gründern, Investoren, Mitarbeitenden und Multisigs genutzt, und nach dem Deployment werden sie selten noch einmal angesehen. Ein kleiner Fehler in der Berechnung oder im Berechtigungsmodell bleibt über die gesamte Vesting-Laufzeit aktiv. Diese Checkliste fasst die Fragen zusammen, die wir stellen, wenn wir einen ERC-20-Vesting-Vertrag entwerfen oder prüfen – von der Arithmetik bis zum Deployment und zum laufenden Betrieb.
1. Vesting-Mathematik#
Der Kern jedes Vesting-Vertrags ist eine Funktion, die beantwortet: „Wie viele Token sind zum Zeitpunkt t gevestet?“ Fast jeder ernsthafte Vesting-Fehler steckt hier.
Linearer Zeitplan und Cliff#
- Definieren Sie den Zeitplan mit expliziten Parametern:
start,cliff,duration,totalAllocation. Vermeiden Sie implizite Werte, die beim Deployment ausblock.timestampabgeleitet werden. - Legen Sie fest, was der Cliff bedeutet. Übliche Modelle sind „nichts vor dem Cliff, danach linearer Nachholeffekt ab
start“ und „nichts vor dem Cliff, dann ein Einmalbetrag, danach linear“. Dokumentieren Sie das gewählte Modell in NatSpec und in den Tests. - Validieren Sie bei der Erstellung:
durationgrößer als null,cliffnicht nachstart + duration,totalAllocationgrößer als null, Begünstigter nicht die Nulladresse. - Nach
start + durationmuss der gevestete Betrag exakttotalAllocationentsprechen – nicht „ungefähr“.
Rundung#
- Erst multiplizieren, dann dividieren.
total * elapsed / durationist korrekt;total / duration * elapsedverliert bei jeder Freigabe unbemerkt Token. - Rundungen sollten immer zugunsten des Vertrags ausfallen: Der beanspruchbare Betrag des Begünstigten wird abgerundet, nie aufgerundet. Die letzte Freigabe am Ende des Zeitplans übernimmt verbleibende Rundungsreste.
- Prüfen Sie große Allokationen bei Token mit 18 Dezimalstellen auf Überläufe. Solidity 0.8.x bricht bei einem Überlauf ab, doch ein Revert in
vestedAmountkann jede Auszahlung blockieren. Verwenden SieMath.mulDivvon OpenZeppelin, wenn das Produkt groß werden kann.
Start in der Vergangenheit oder Zukunft#
- Ein
startin der Vergangenheit ist legitim (rückdatierte Mitarbeiterzuteilungen), bedeutet aber, dass sofort ein großer Betrag beanspruchbar ist. Das sollte eine bewusste, geprüfte Entscheidung sein und nicht die Folge eines falschen Parameters. - Ein
startweit in der Zukunft kann ein Tippfehler sein (Millisekunden statt Sekunden ist ein Klassiker). Ergänzen Sie Plausibilitätsgrenzen im Konstruktor oder in der Factory sowie im Deployment-Skript.
Eine kompakte Referenzimplementierung des Zeitplans:
// 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. Begünstigte und Widerruf#
- Wer darf auszahlen? Nur der Begünstigte oder jeder in dessen Namen? Es ist unproblematisch, wenn jeder
release()auslösen darf, solange die Token immer an den Begünstigten gehen; das erleichtert Automatisierung und die Wiederherstellung bei verlorenen Schlüsseln. - Kann der Begünstigte geändert werden? Falls ja, muss der aktuelle Begünstigte die Änderung anstoßen (idealerweise als zweistufige Übertragung mit Annahme), und es muss ein Event ausgegeben werden. Ist der Begünstigte ein Vertrag, muss er Token empfangen und verwenden können.
- Widerrufsmodell. Entscheiden Sie pro Zeitplan, ob er widerrufbar ist. Beim Widerruf sollte der bereits gevestete, aber noch nicht ausgezahlte Betrag für den Begünstigten beanspruchbar bleiben; nur der nicht gevestete Rest geht an das Treasury zurück. Der Widerruf bereits gevesteter Token ist ein Vertrauensproblem, nicht nur ein Codeproblem.
- Ein Widerruf muss endgültig sein. Ein widerrufener Zeitplan darf nicht erneut widerrufbar sein, darf nicht weiter vesten und darf dem Admin nicht erlauben, mehr als den nicht gevesteten Rest abzuziehen.
- Mehrere Zeitpläne pro Begünstigtem. Verwalten Sie Zeitpläne über eine ID und nicht allein über die Adresse, damit eine zweite Zuteilung die erste nicht überschreibt.
3. Zugriffskontrolle und Admin-Schlüssel#
- Listen Sie jede privilegierte Funktion auf: Zeitpläne anlegen, widerrufen, pausieren, Überschüsse abziehen, upgraden. Jede davon ist eine Angriffsfläche, falls der Schlüssel kompromittiert wird.
- Verwenden Sie
Ownable2StepoderAccessControlmit getrennten Rollen statt eines einzigen allmächtigen Owners. Die Rolle, die Zeitpläne anlegt, muss nicht dieselbe sein, die Mittel abziehen darf. - Halten Sie Admin-Rollen in einem Multisig und erwägen Sie einen Timelock für alles, was Token aus dem Vertrag bewegt.
- Der Admin darf niemals Token abziehen können, die Zeitplänen zugesagt sind. Führen Sie
totalCommittedmit und erlauben Sie für Überschüsse nur die Auszahlung vonbalance - totalCommitted. - Planen Sie den Endzustand: Können Admin-Rechte aufgegeben werden, sobald alle Zeitpläne angelegt sind? Weniger aktive Schlüssel bedeuten weniger Fehlerquellen.
4. Umgang mit Token-Transfers#
- Verwenden Sie OpenZeppelin
SafeERC20für jeden Transfer. Manche Token geben keinen booleschen Wert zurück, andere gebenfalsezurück, statt abzubrechen. - Fee-on-Transfer-Token. Wird der Vertrag mit einem Token finanziert, der eine Gebühr erhebt, erhält er weniger als den Nominalbetrag. Messen Sie den Saldo vor und nach der Finanzierung und verbuchen Sie den tatsächlich eingegangenen Betrag – oder lehnen Sie solche Token ausdrücklich ab.
- Rebasing-Token. Salden, die sich von selbst ändern, brechen die Annahme
balance == committed + surplus. Dokumentieren Sie entweder, dass Rebasing-Token nicht unterstützt werden, oder bauen Sie die Buchhaltung auf Anteilen auf. - Legen Sie die Token-Adresse als
immutablefest, wenn der Vertrag nur einen Token bedient. Beliebige Token-Adressen pro Zeitplan zu akzeptieren, vergrößert die Angriffsfläche erheblich. - Erlauben Sie niemals, den gevesteten Token über eine generische
recoverERC20-Funktion zu „retten“, ohne zugesagte Beträge abzuziehen.
5. Reentrancy und Aufrufreihenfolge#
- Befolgen Sie Checks-Effects-Interactions: Aktualisieren Sie
released, bevor SiesafeTransferaufrufen. - Versehen Sie
release,revokeund jede Auszahlungsfunktion mitnonReentrant. Hooks im Stil von ERC-777 oder ein bösartiger Token können in den Vertrag zurückrufen. - Halten Sie externe Aufrufe minimal. Ein Vesting-Vertrag hat keinen Grund, beliebige Adressen aufzurufen.
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. Zeitstempel#
- Verwenden Sie
block.timestamp, nicht Blocknummern. Blockzeiten unterscheiden sich zwischen Chains und ändern sich im Laufe der Zeit; derselbe Vertrag wird möglicherweise später auf einem L2 deployt. - Der Einfluss von Validatoren auf Zeitstempel beschränkt sich auf Sekunden. Für Zeitpläne in Monaten ist das irrelevant, doch bauen Sie keine Logik, die von sekundengenauer Präzision abhängt.
- Speichern Sie Zeitstempel als
uint64. Das reicht für jeden realistischen Zeitplan und lässt sich gut im Storage packen. - Testen Sie die Grenzen explizit: eine Sekunde vor dem Cliff, genau am Cliff, genau am Ende und lange nach dem Ende.
7. Events und Transparenz#
Jede Zustandsänderung sollte ein Event ausgeben: ScheduleCreated, TokensReleased, ScheduleRevoked, BeneficiaryChanged, Rollenänderungen und Pausen. Auf Events stützen sich Indexer, Dashboards und Ihr eigenes Support-Team. Geben Sie die Zeitplan-ID und die Beträge mit aus, nicht nur Adressen. Investoren und Mitarbeitende werden fragen, wie viel bereits gevestet ist – On-Chain-Events sind die glaubwürdigste Antwort darauf.
8. Abwägungen bei der Upgradefähigkeit#
| Option | Vorteil | Risiko |
|---|---|---|
| Unveränderlicher Vertrag | Stärkste Garantie für Begünstigte, einfacheres Audit | Fehler lassen sich nicht beheben; Migration erfordert neuen Vertrag und neue Mittel |
| Upgradefähiger Proxy | Fehler können gepatcht werden | Upgrade-Schlüssel kann jede Regel ändern, Fehler im Storage-Layout, größerer Audit-Umfang |
| Unveränderlich plus Factory | Jeder Satz an Zeitplänen ist isoliert, neue Versionen für neue Zuteilungen | Alte Instanzen behalten alte Fehler |
Für Vesting ist Unveränderlichkeit häufig die bessere Voreinstellung: Der eigentliche Zweck des Vertrags ist, dass niemand die Vereinbarung nachträglich ändern kann. Wenn Sie sich für einen Proxy entscheiden, legen Sie die Upgrade-Rolle hinter ein Multisig und einen Timelock, verwenden Sie Storage Gaps oder Namespaced Storage und führen Sie die Upgrade-Sicherheitsprüfungen von OpenZeppelin in der CI aus.
9. Notfallmechanismen#
- Eine Pause kann vor einem unbekannten Fehler schützen, doch eine Pause, die
releasedauerhaft blockiert, ist zugleich ein Mittel, um Begünstigte einzufrieren. Erwägen Sie, die maximale Dauer einer Pause zu begrenzen oder Auszahlungen auch dann zuzulassen, wenn nur das Anlegen neuer Zeitpläne pausiert ist. - Dokumentieren Sie, wer pausieren darf, unter welchen Bedingungen und wie die Community informiert wird.
- Vermeiden Sie Funktionen wie „im Notfall alles abziehen“. Ist eine solche Funktion unvermeidbar, muss sie hinter einem Timelock liegen und in der Dokumentation sichtbar sein, die Investoren lesen.
10. Tests#
Unit-Tests sind das Minimum. Bei Vesting-Verträgen bringen eigenschaftsbasierte Tests viel zusätzlichen Wert, denn die Mathematik muss für jeden Zeitpunkt und jeden Betrag stimmen.
- Unit-Tests: jeder Revert-Pfad, jeder Grenz-Zeitstempel, Widerruf vor dem Cliff, Widerruf nach vollständigem Vesting, mehrere Zeitpläne für denselben Begünstigten.
- Fuzz-Tests: zufällige Werte für
total,durationundtimestamp; prüfen Sie, dassvestedAmountmonoton ist undtotalnie überschreitet. - Invariantentests: Lassen Sie Foundry
release,revoke,createScheduleundvm.warpin zufälliger Reihenfolge aufrufen und prüfen Sie anschließend globale Eigenschaften.
Nützliche Invarianten:
- Die Summe der ausgezahlten Beträge pro Zeitplan überschreitet nie dessen Allokation.
- Der Token-Saldo des Vertrags ist immer mindestens
totalCommitted. - Ein widerrufener Zeitplan gewinnt danach nie mehr gevesteten Betrag hinzu.
// 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. Statische Analyse#
Führen Sie Slither bei jeder Änderung aus und behandeln Sie die Ergebnisse als Prüfliste, nicht als Bestanden/Nicht-bestanden-Signal. Achten Sie bei Vesting-Verträgen besonders auf Reentrancy-Befunde, ungeprüfte Transfers, gefährliche strikte Gleichheitsprüfungen auf Salden und fehlende Events.
slither . --filter-paths "lib|test" --exclude-dependencies
forge test --fuzz-runs 10000
forge coverage --report summary
Eine KI-gestützte Vorprüfung, etwa mit unserem Smart Contract AI Reviewer, ist ein weiterer schneller erster Durchgang, der verdächtige Muster hervorhebt, bevor ein Mensch den Code ansieht.
12. Deployment und Verifizierung#
- Skripten Sie das Deployment mit Foundry-Skripten statt mit manuellen Transaktionen. Parameter liegen in versionierten Konfigurationsdateien und werden wie Code geprüft.
- Deployen Sie zunächst mit exakt demselben Skript und denselben Parametern auf ein Testnet und führen Sie danach einen Probelauf auf einem Mainnet-Fork durch.
- Verifizieren Sie den Quellcode unmittelbar nach dem Deployment im Block-Explorer – mit derselben Compiler-Version und denselben Optimizer-Einstellungen.
- Prüfen Sie die Dezimalstellen doppelt: Eine Allokation von 1.000.000 Token mit 18 Dezimalstellen ist
1_000_000e18, nicht1_000_000. - Übertragen Sie die Ownership im selben Skript an das Multisig und bestätigen Sie, dass der Deployer-Schlüssel keine Rollen mehr hält.
13. Betriebliche Prüfungen#
- Gleichen Sie regelmäßig ab: Die Summe der Allokationen abzüglich der Auszahlungen sollte dem zugesagten Betrag und dem Vertragssaldo entsprechen.
- Überwachen Sie Events und lösen Sie Alarme bei unerwarteten Widerrufen, Rollenänderungen oder Pausen aus.
- Pflegen Sie eine öffentliche oder investorenseitige Übersicht der Zeitpläne, damit Fragen anhand von On-Chain-Daten beantwortet werden können.
- Üben Sie die zentralen Abläufe: Rotation von Multisig-Unterzeichnern, das Vorgehen, wenn ein Begünstigter den Zugang zu seiner Wallet verliert, und die Kommunikation im Fall einer Pause.
Abschließender Hinweis#
Eine Checkliste und eine automatisierte Prüfung decken viele Probleme frühzeitig auf, ersetzen jedoch kein unabhängiges Sicherheitsaudit. Bevor ein Vesting-Vertrag echte Werte verwaltet, sollte er von Personen geprüft werden, die ihn nicht geschrieben haben.
Wenn Sie sehen möchten, wie wir das in der Praxis angehen, werfen Sie einen Blick auf unseren Token-Suite-Showcase, der einen Vesting-Vertrag mit Tests enthält, oder erfahren Sie mehr über unsere Blockchain-Leistungen. Unser Team wird von einem Tech Lead mit mehr als 20 Jahren Erfahrung geleitet, und wir prüfen gerne Ihre Tokenomics oder Ihr Vesting-Design – nehmen Sie einfach Kontakt auf.