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. Vesting matematiği
- 2. Hak sahipleri ve iptal
- 3. Erişim kontrolü ve yönetici anahtarları
- 4. Token transferi işleme
- 5. Reentrancy ve çağrı sırası
- 6. Zaman damgaları
- 7. Event'ler ve şeffaflık
- 8. Yükseltilebilirlik ödünleşimleri
- 9. Acil durum kontrolleri
- 10. Test
- 11. Statik analiz
- 12. Dağıtım ve doğrulama
- 13. Operasyonel kontroller
- 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ındablock.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:
durationsıfırdan büyük,cliffstart + duration'dan sonra değil,totalAllocationsıfırdan büyük, hak sahibi sıfır adresi değil. start + durationsonrasında vest edilmiş miktar tam olaraktotalAllocation'a eşit olmalıdır; "yaklaşık olarak" değil.
Yuvarlama#
- Bölmeden önce çarpın.
total * elapsed / durationdoğrudur;total / duration * elapsedher 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
vestedAmountiçindeki bir revert her talebi kilitleyebilir. Çarpım büyüyebiliyorsa OpenZeppelin'denMath.mulDivkullanın.
Geçmişte veya gelecekte başlangıç#
- Geçmişte bir
startmeş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
startbir 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:
// 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
Ownable2StepveyaAccessControlkullanı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ızcabalance - 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
SafeERC20kullanın. Bazı tokenlar boolean döndürmez; bazıları ise revert etmek yerinefalsedö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 + surplusvarsayı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
immutableolarak 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
recoverERC20fonksiyonuyla "kurtarılmasına" asla izin vermeyin.
5. Reentrancy ve çağrı sırası#
- Checks-effects-interactions düzenini izleyin:
safeTransfer'ı çağırmadan öncereleased'i güncelleyin. release,revokeve tüm çekim fonksiyonlarınanonReentrantekleyin. 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.
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.timestampkullanı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ı
uint64olarak 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çenek | Avantaj | Risk |
|---|---|---|
| Değişmez sözleşme | Hak sahipleri için en güçlü garanti, daha basit denetim | Hatalar düzeltilemez; migrasyon yeni bir sözleşme ve fon gerektirir |
| Yükseltilebilir proxy | Hatalar yamanabilir | Yükseltme anahtarı her kuralı değiştirebilir, storage layout hataları, daha geniş denetim kapsamı |
| Değişmez + factory | Her takvim kümesi izoledir, yeni hibeler için yeni sürümler | Eski ö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,durationvetimestamp;vestedAmount'un monoton olduğunu ve aslatotal'ı aşmadığını doğrulayın. - Invariant testleri: Foundry'nin
release,revoke,createSchedulevevm.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
totalCommittedkadardır. - İptal edilmiş bir takvim sonradan asla vest edilmiş miktar kazanmaz.
// 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.
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_000değ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.