Zum Inhalt springen

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. 1. Vesting-Mathematik
  2. 2. Begünstigte und Widerruf
  3. 3. Zugriffskontrolle und Admin-Schlüssel
  4. 4. Umgang mit Token-Transfers
  5. 5. Reentrancy und Aufrufreihenfolge
  6. 6. Zeitstempel
  7. 7. Events und Transparenz
  8. 8. Abwägungen bei der Upgradefähigkeit
  9. 9. Notfallmechanismen
  10. 10. Tests
  11. 11. Statische Analyse
  12. 12. Deployment und Verifizierung
  13. 13. Betriebliche Prüfungen
  14. 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 aus block.timestamp abgeleitet 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: duration größer als null, cliff nicht nach start + duration, totalAllocation größer als null, Begünstigter nicht die Nulladresse.
  • Nach start + duration muss der gevestete Betrag exakt totalAllocation entsprechen – nicht „ungefähr“.

Rundung#

  • Erst multiplizieren, dann dividieren. total * elapsed / duration ist korrekt; total / duration * elapsed verliert 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 vestedAmount kann jede Auszahlung blockieren. Verwenden Sie Math.mulDiv von OpenZeppelin, wenn das Produkt groß werden kann.

Start in der Vergangenheit oder Zukunft#

  • Ein start in 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 start weit 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:

solidity
// 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 Ownable2Step oder AccessControl mit 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 totalCommitted mit und erlauben Sie für Überschüsse nur die Auszahlung von balance - 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 SafeERC20 für jeden Transfer. Manche Token geben keinen booleschen Wert zurück, andere geben false zurü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 immutable fest, 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 Sie safeTransfer aufrufen.
  • Versehen Sie release, revoke und jede Auszahlungsfunktion mit nonReentrant. 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.
solidity
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#

OptionVorteilRisiko
Unveränderlicher VertragStärkste Garantie für Begünstigte, einfacheres AuditFehler lassen sich nicht beheben; Migration erfordert neuen Vertrag und neue Mittel
Upgradefähiger ProxyFehler können gepatcht werdenUpgrade-Schlüssel kann jede Regel ändern, Fehler im Storage-Layout, größerer Audit-Umfang
Unveränderlich plus FactoryJeder Satz an Zeitplänen ist isoliert, neue Versionen für neue ZuteilungenAlte 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 release dauerhaft 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, duration und timestamp; prüfen Sie, dass vestedAmount monoton ist und total nie überschreitet.
  • Invariantentests: Lassen Sie Foundry release, revoke, createSchedule und vm.warp in 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.
solidity
// 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.

bash
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, nicht 1_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.

Sie planen ein Projekt?

Erzählen Sie uns, was Sie vorhaben. Sie erhalten eine ehrliche Einschätzung, einen klaren Umfang und ein Angebot mit Festpreis oder Meilensteinen – meist innerhalb weniger Werktage.

Lieber zuerst schreiben? Nachricht schreiben