İçeriğe atla

Yazılar

ERC-20 vesting sözleşmeleri için güvenlik kontrol listesi

Güvenli ERC-20 vesting sözleşmeleri için vesting matematiği, iptal, yönetici anahtarları, token transferleri, Foundry invariant testleri ve dağıtım kontrolleri.

sigmacode.io mühendislik ekibi9 dk okuma

Bu sayfada (14)
  1. 1. Vesting matematiği
  2. 2. Hak sahipleri ve iptal
  3. 3. Erişim kontrolü ve yönetici anahtarları
  4. 4. Token transferi işleme
  5. 5. Reentrancy ve çağrı sırası
  6. 6. Zaman damgaları
  7. 7. Event'ler ve şeffaflık
  8. 8. Yükseltilebilirlik ödünleşimleri
  9. 9. Acil durum kontrolleri
  10. 10. Test
  11. 11. Statik analiz
  12. 12. Dağıtım ve doğrulama
  13. 13. Operasyonel kontroller
  14. Son not

Vesting sözleşmeleri basit görünür: tokenları kilitleyin, zaman içinde serbest bırakın, bitti. Pratikte ise bir projenin arzının büyük bir bölümünü yıllarca elinde tutarlar; kurucular, yatırımcılar, çalışanlar ve multisig'ler tarafından kullanılırlar ve dağıtıldıktan sonra nadiren yeniden gözden geçirilirler. Matematikteki veya yetki modelindeki küçük bir hata, tüm vesting süresi boyunca canlıda kalır. Bu kontrol listesi, bir ERC-20 vesting sözleşmesini tasarlarken veya incelerken sorduğumuz soruları aritmetikten dağıtıma ve günlük operasyonlara kadar bir araya getiriyor.

1. Vesting matematiği#

Her vesting sözleşmesinin özü, "t anında kaç token vest edilmiştir?" sorusunu yanıtlayan tek bir fonksiyondur. Ciddi vesting hatalarının neredeyse tamamı burada yaşar.

Doğrusal takvim ve cliff#

  • Takvimi açık parametrelerle tanımlayın: start, cliff, duration, totalAllocation. Dağıtım anında block.timestamp'tan türetilen örtük değerlerden kaçının.
  • Cliff'in ne anlama geldiğine karar verin. Yaygın modeller "cliff'ten önce hiçbir şey, sonra start'tan itibaren doğrusal olarak yetişme" ve "cliff'ten önce hiçbir şey, sonra toplu bir ödeme, ardından doğrusal"dır. Seçilen modeli NatSpec'e ve testlere yazın.
  • Oluşturma anında doğrulama yapın: duration sıfırdan büyük, cliff start + duration'dan sonra değil, totalAllocation sıfırdan büyük, hak sahibi sıfır adresi değil.
  • start + duration sonrasında vest edilmiş miktar tam olarak totalAllocation'a eşit olmalıdır; "yaklaşık olarak" değil.

Yuvarlama#

  • Bölmeden önce çarpın. total * elapsed / duration doğrudur; total / duration * elapsed her serbest bırakmada sessizce token kaybettirir.
  • Yuvarlama her zaman sözleşmenin lehine olmalıdır: hak sahibinin talep edebileceği miktarı aşağı yuvarlayın, asla yukarı değil. Takvimin sonundaki son serbest bırakma, kalan tozu süpürür.
  • 18 ondalıklı tokenlarla büyük tahsislerde taşmayı kontrol edin. Solidity 0.8.x taşmada revert eder, ancak vestedAmount içindeki bir revert her talebi kilitleyebilir. Çarpım büyüyebiliyorsa OpenZeppelin'den Math.mulDiv kullanın.

Geçmişte veya gelecekte başlangıç#

  • Geçmişte bir start meşrudur (geriye dönük çalışan hibeleri), ancak büyük bir miktarın anında talep edilebilir olması anlamına gelir. Bunu açık ve gözden geçirilmiş bir karar haline getirin; yanlış bir parametrenin kazara sonucu değil.
  • Çok uzak gelecekte bir start bir yazım hatası olabilir (saniye yerine milisaniye klasik bir hatadır). Constructor'da veya factory'de ve dağıtım script'inde makullük sınırları ekleyin.

Takvimin kompakt bir referans implementasyonu:

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. Hak sahipleri ve iptal#

  • Kim talep edebilir? Yalnızca hak sahibi mi, yoksa hak sahibi adına herkes mi? Tokenlar her zaman hak sahibine gittiği sürece release()'i herkesin tetiklemesine izin vermek sorun değildir; kayıp anahtar kurtarmasına ve otomasyona yardımcı olur.
  • Hak sahibi değişebilir mi? Evetse, mevcut hak sahibinin başlatmasını şart koşun (ideal olarak kabul adımlı iki aşamalı bir transfer) ve bir event yayınlayın. Sözleşme olan bir hak sahibi tokenları alabilmeli ve kullanabilmelidir.
  • İptal modeli. Takvim bazında iptal edilebilir olup olmadığına karar verin. İptalde, vest edilmiş ancak serbest bırakılmamış miktar hak sahibi tarafından talep edilebilir kalmalıdır; yalnızca vest edilmemiş kalan kısım hazineye döner. Vest edilmiş tokenları iptal etmek bir kod sorunu değil, bir güven sorunudur.
  • İptal kesin olmalıdır. İptal edilmiş bir takvim ikinci kez iptal edilememeli, vest etmeye devam etmemeli ve yöneticinin vest edilmemiş kalandan fazlasını çekmesine izin vermemelidir.
  • Hak sahibi başına birden fazla takvim. Takvimleri yalnızca adrese göre değil, bir id'ye göre anahtarlayın; böylece ikinci bir hibe ilkinin üzerine yazmaz.

3. Erişim kontrolü ve yönetici anahtarları#

  • Her ayrıcalıklı fonksiyonu listeleyin: takvim oluşturma, iptal, duraklatma, fazlayı çekme, yükseltme. Anahtar ele geçirilirse her biri bir saldırı yüzeyidir.
  • Tek ve her şeye yetkili bir sahip yerine ayrı rollere sahip Ownable2Step veya AccessControl kullanın. Takvim oluşturan rolün fon çekebilen rol olması gerekmez.
  • Yönetici rollerini bir multisig'de tutun ve sözleşmeden token çıkaran her işlem için bir timelock düşünün.
  • Yönetici, takvimlere bağlanmış tokenları asla çekememelidir. totalCommitted'i izleyin ve fazla için yalnızca balance - totalCommitted çekimine izin verin.
  • Son durum için plan yapın: tüm takvimler oluşturulduktan sonra yönetici haklarından feragat edilebilir mi? Daha az canlı anahtar, daha az hata modu demektir.

4. Token transferi işleme#

  • Her transfer için OpenZeppelin SafeERC20 kullanın. Bazı tokenlar boolean döndürmez; bazıları ise revert etmek yerine false döndürür.
  • Transfer ücreti alan tokenlar. Sözleşme ücret alan bir tokenla fonlanıyorsa, nominal miktardan daha azını alır. Fonlamadan önce ve sonra bakiyeyi ölçün ve gerçekten gelen miktarı kaydedin ya da bu tür tokenları açıkça reddedin.
  • Rebase tokenları. Kendi kendine değişen bakiyeler balance == committed + surplus varsayımını bozar. Ya rebase tokenlarının desteklenmediğini belgeleyin ya da muhasebeyi paylar üzerinden tasarlayın.
  • Sözleşme tek bir tokena hizmet veriyorsa token adresini immutable olarak sabitleyin. Takvim başına keyfi token adresleri kabul etmek saldırı yüzeyini önemli ölçüde genişletir.
  • Vest edilen tokenın, bağlanmış miktarlar düşülmeden genel bir recoverERC20 fonksiyonuyla "kurtarılmasına" asla izin vermeyin.

5. Reentrancy ve çağrı sırası#

  • Checks-effects-interactions düzenini izleyin: safeTransfer'ı çağırmadan önce released'i güncelleyin.
  • release, revoke ve tüm çekim fonksiyonlarına nonReentrant ekleyin. ERC-777 tarzı hook'lar veya kötü niyetli bir token sözleşmeye geri çağrı yapabilir.
  • Dış çağrıları minimumda tutun. Bir vesting sözleşmesinin keyfi adresleri çağırmak için hiçbir nedeni yoktur.
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. Zaman damgaları#

  • Blok numaraları yerine block.timestamp kullanın. Blok süreleri zincirler arasında farklılık gösterir ve zamanla değişir; aynı sözleşme daha sonra bir L2'de dağıtılabilir.
  • Doğrulayıcıların zaman damgaları üzerindeki etkisi saniyelerle sınırlıdır. Aylarla ölçülen takvimler için bu önemsizdir, ancak saniye düzeyinde hassasiyete dayanan mantık kurmayın.
  • Zaman damgalarını uint64 olarak saklayın. Gerçekçi her takvim için yeterlidir ve storage'da iyi paketlenir.
  • Sınırları açıkça test edin: cliff'ten bir saniye önce, tam cliff anında, tam bitişte ve bitişten uzun süre sonra.

7. Event'ler ve şeffaflık#

Her durum değişikliği bir event yayınlamalıdır: ScheduleCreated, TokensReleased, ScheduleRevoked, BeneficiaryChanged, rol değişiklikleri ve duraklatmalar. Event'ler; indeksleyicilerin, panellerin ve kendi destek ekibinizin güvendiği şeydir. Yalnızca adresleri değil, takvim id'sini ve miktarları da ekleyin. Yatırımcılar ve çalışanlar ne kadarın vest edildiğini soracaktır ve zincir üstü event'ler en inandırıcı yanıttır.

8. Yükseltilebilirlik ödünleşimleri#

SeçenekAvantajRisk
Değişmez sözleşmeHak sahipleri için en güçlü garanti, daha basit denetimHatalar düzeltilemez; migrasyon yeni bir sözleşme ve fon gerektirir
Yükseltilebilir proxyHatalar yamanabilirYükseltme anahtarı her kuralı değiştirebilir, storage layout hataları, daha geniş denetim kapsamı
Değişmez + factoryHer takvim kümesi izoledir, yeni hibeler için yeni sürümlerEski örnekler eski hatalarını korur

Vesting için değişmezlik genellikle daha iyi varsayılandır: sözleşmenin tüm amacı, kimsenin anlaşmayı sonradan değiştirememesidir. Proxy seçerseniz yükseltme rolünü bir multisig ve timelock arkasına koyun, storage gap'leri veya isim alanlı storage kullanın ve OpenZeppelin yükseltme güvenliği kontrollerini CI'da çalıştırın.

9. Acil durum kontrolleri#

  • Bir duraklatma bilinmeyen bir hataya karşı koruyabilir, ancak release'i sonsuza dek engelleyen bir duraklatma da hak sahiplerini dondurmanın bir yoludur. Duraklatmanın ne kadar sürebileceğini sınırlamayı veya yeni takvim oluşturma duraklatılmışken bile serbest bırakmalara izin vermeyi düşünün.
  • Kimin, hangi koşullarda duraklatabileceğini ve topluluğun nasıl bilgilendirileceğini belgeleyin.
  • "Acil durumda her şeyi çek" fonksiyonlarından kaçının. Biri kaçınılmazsa bir timelock arkasında olmalı ve yatırımcıların okuduğu dokümantasyonda görünür olmalıdır.

10. Test#

Birim testleri asgari düzeydir. Vesting sözleşmelerinde özellik tabanlı testler büyük değer katar, çünkü matematik her zaman ve her miktar için geçerli olmalıdır.

  • Birim testleri: her revert yolu, her sınır zaman damgası, cliff'ten önce iptal, tam vesting sonrası iptal, aynı hak sahibi için birden fazla takvim.
  • Fuzz testleri: rastgele total, duration ve timestamp; vestedAmount'un monoton olduğunu ve asla total'ı aşmadığını doğrulayın.
  • Invariant testleri: Foundry'nin release, revoke, createSchedule ve vm.warp'ı rastgele sırada çağırmasına izin verin, sonra global özellikleri kontrol edin.

Yararlı invariant'lar:

  • Takvim başına serbest bırakılan miktarların toplamı asla tahsisini aşmaz.
  • Sözleşmenin token bakiyesi her zaman en az totalCommitted kadardır.
  • İptal edilmiş bir takvim sonradan asla vest edilmiş miktar kazanmaz.
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. Statik analiz#

Her değişiklikte Slither'ı çalıştırın ve çıktısını bir geç/kal sinyali olarak değil, bir inceleme kuyruğu olarak ele alın. Vesting sözleşmelerinde reentrancy bulgularına, kontrol edilmemiş transferlere, bakiyelerde tehlikeli katı eşitliklere ve eksik event'lere dikkat edin.

bash
slither . --filter-paths "lib|test" --exclude-dependencies
forge test --fuzz-runs 10000
forge coverage --report summary

Smart Contract AI Reviewer aracımız gibi yapay zekâ destekli bir ön inceleme, bir insan koda bakmadan önce şüpheli kalıpları öne çıkaran hızlı bir ilk geçiştir.

12. Dağıtım ve doğrulama#

  • Dağıtımı manuel işlemlerle değil, Foundry script'leriyle gerçekleştirin. Parametreler versiyon kontrolüne alınmış yapılandırma dosyalarında yaşar ve kod gibi gözden geçirilir.
  • Önce tamamen aynı script ve parametrelerle bir testnet'e dağıtın, sonra bir mainnet fork'unda deneme çalıştırması yapın.
  • Dağıtımdan hemen sonra kaynak kodunu blok gezgininde, aynı derleyici sürümü ve optimizer ayarlarıyla doğrulayın.
  • Ondalıkları iki kez kontrol edin: 18 ondalıklı 1.000.000 tokenlık bir tahsis 1_000_000 değil, 1_000_000e18'dir.
  • Aynı script'te sahipliği multisig'e devredin ve dağıtıcı anahtarının kalan hiçbir rol tutmadığını doğrulayın.

13. Operasyonel kontroller#

  • Düzenli olarak mutabakat yapın: takvim tahsislerinin toplamı eksi serbest bırakmalar, bağlanmış miktarla ve sözleşme bakiyesiyle eşleşmelidir.
  • Event'leri izleyin ve beklenmedik iptallerde, rol değişikliklerinde veya duraklatmalarda alarm verin.
  • Soruların zincir üstü verilerden yanıtlanabilmesi için takvimlerin kamuya açık veya yatırımcıya dönük bir özetini tutun.
  • Kritik prosedürleri prova edin: multisig imzacı rotasyonu, bir hak sahibinin cüzdanına erişimi kaybetmesi durumunda ne olacağı ve bir duraklatmanın nasıl duyurulacağı.

Son not#

Bir kontrol listesi ve otomatik bir inceleme birçok sorunu erken yakalar, ancak bağımsız bir güvenlik denetiminin yerini tutmaz. Bir vesting sözleşmesi gerçek değer taşımadan önce, onu yazmamış kişiler tarafından incelenmesini sağlayın.

Bunu pratikte nasıl ele aldığımızı görmek isterseniz, testleriyle birlikte bir vesting sözleşmesi içeren token suite vitrinimize göz atın veya blockchain hizmetlerimiz hakkında daha fazla bilgi edinin. Ekibimiz 20+ yıllık deneyime sahip bir tech lead tarafından yönetiliyor ve tokenomikinizi ya da vesting tasarımınızı incelemekten memnuniyet duyarız: iletişime geçmeniz yeterli.

Aklınızda bir proje mi var?

Ne geliştirdiğinizi anlatın. İş günlerinde bir saat içinde kıdemli bir mühendis yanıt verir — ardından dürüst bir değerlendirme, net bir kapsam ve genellikle birkaç iş günü içinde sabit fiyatlı veya aşamalı bir teklif alırsınız.

Önce yazmayı mı tercih edersiniz? Bize yazın

Doğrudan Ing. Ismet Mesic ile konuşursunuz, Tech lead.