آنچه در این مقاله میخوانید [پنهانسازی]
اگر به دنبال ساخت یک قرارداد زمانبندی آزادسازی توکن هستید، در این راهنما به صورت عملی پیاده سازی، استقرار و تست Token Vesting با سالیدیتی را یاد می گیرید. با یک الگوی امن، قابل توسعه و ساده پیش می رویم تا بتوانید برای تیم، سرمایه گذاران یا جامعه خود برنامه آزادسازی شفاف و غیرمتمرکز بسازید.
چرا به وستینگ نیاز داریم و چه مساله ای حل می کند
وستینگ جلوی آزادسازی یکجای توکن ها را می گیرد و با زمانبندی مشخص آن ها را قابل برداشت می کند. این کار تعهد تیم را بالا می برد، فشار فروش را کاهش می دهد و اعتماد سرمایه گذاران را تقویت می کند. در عمل ما می خواهیم به شکلی غیرقابل تغییر و قابل حسابرسی تعیین کنیم چه کسی، چه مقدار و در چه زمانی توکن دریافت کند.
مفاهیم کلیدی و سناریوهای متداول
پیش از کدنویسی، اصطلاحات پایه را یکسان کنیم تا در طراحی دچار ابهام نشویم.
- ذینفع: آدرس دریافت کننده توکن های آزاد شده.
- شروع: زمان آغاز محاسبه آزادسازی.
- دوره انتظار یا کلیف: بازه ای که در آن هیچ توکنی آزاد نمی شود.
- مدت: کل زمان بندی از شروع تا پایان آزادسازی کامل.
- قابل فسخ بودن: اجازه مالک برای متوقف کردن برنامه و بازپس گیری بخش آزاد نشده.
- آزاد شده و قابل آزادسازی: مقدار توکنی که تاکنون آزاد شده و مقداری که اکنون می توان برداشت کرد.
انتخاب نوع برنامه آزادسازی
به صورت عمومی سه الگو بیشتر استفاده می شود. جدول زیر مقایسه کوتاهی برای انتخاب درست ارائه می کند.
| روش | توضیح |
|---|---|
| خطی | آزادسازی یکنواخت از شروع تا پایان پس از کلیف |
| کلیف + خطی | هیچ آزادسازی تا پایان کلیف، سپس خطی تا انتها |
| مرحله ای | آزادسازی در چند مایلستون مشخص با درصدهای از پیش تعیین شده |
طراحی قرارداد: حالت ها، رویدادها و توابع
برای سادگی یک قرارداد تک ذینفع می سازیم که قابل توسعه به نسخه های چند ذینفع است. حالت های اصلی شامل آدرس توکن، آدرس ذینفع، زمان شروع، کلیف، مدت، قابلیت فسخ، مقدار کل تامین شده و مقدار آزاد شده است. رویدادها برای شفافیت: تامین موجودی، آزادسازی و فسخ. توابع کلیدی: تامین موجودی، محاسبه مقدار آزاد شده، برداشت توسط ذینفع و فسخ توسط مالک.
پیاده سازی قرارداد در Solidity
کد زیر یک الگوی مینیمال، امن و خوانا از قرارداد وستینگ خطی را نشان می دهد. الگو به راحتی قابل تبدیل به چند ذینفع یا چند برنامه موازی است.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract TokenVesting {
address public immutable owner;
IERC20 public immutable token;
address public beneficiary;
uint64 public start;
uint64 public cliff;
uint64 public duration;
bool public revocable;
bool public revoked;
uint256 public totalAllocated;
uint256 public released;
event Funded(uint256 amount);
event Released(uint256 amount);
event Revoked(uint256 refund);
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
modifier onlyBeneficiary() {
require(msg.sender == beneficiary, "not beneficiary");
_;
}
constructor(
address token_,
address beneficiary_,
uint64 start_,
uint64 cliffDuration_,
uint64 duration_,
bool revocable_
) {
require(token_ != address(0), "token zero");
require(beneficiary_ != address(0), "beneficiary zero");
require(duration_ > 0, "duration zero");
require(cliffDuration_ <= duration_, "cliff too long"); owner = msg.sender; token = IERC20(token_); beneficiary = beneficiary_; start = start_; cliff = start_ + cliffDuration_; duration = duration_; revocable = revocable_; } function fund(uint256 amount) external onlyOwner { require(amount > 0, "amount zero");
totalAllocated += amount;
require(token.transferFrom(msg.sender, address(this), amount), "transferFrom failed");
emit Funded(amount);
}
function vestedAmount(uint64 timestamp) public view returns (uint256) {
if (timestamp < cliff) {
return 0;
}
if (revoked) {
uint64 t = timestamp < block.timestamp ? timestamp : uint64(block.timestamp);
// پس از فسخ، مقدار کل موثر همان مقدار قابل آزادسازی تا لحظه فسخ است
}
if (timestamp >= start + duration) {
return totalAllocated;
}
uint256 elapsed = uint256(timestamp - start);
return (totalAllocated * elapsed) / duration;
}
function releasable() public view returns (uint256) {
uint256 vested = vestedAmount(uint64(block.timestamp));
if (vested <= released) return 0;
return vested - released;
}
function release() external onlyBeneficiary {
uint256 amount = releasable();
require(amount > 0, "nothing to release");
released += amount;
require(token.transfer(beneficiary, amount), "transfer failed");
emit Released(amount);
}
function revoke() external onlyOwner {
require(revocable, "not revocable");
require(!revoked, "already revoked");
revoked = true;
uint256 vestedNow = vestedAmount(uint64(block.timestamp));
uint256 refund = totalAllocated - vestedNow;
// محدود کردن سقف کل به مقدار vested برای جلوگیری از آزادسازی بیش از حد در آینده
totalAllocated = vestedNow;
if (refund > 0) {
require(token.transfer(owner, refund), "refund failed");
}
emit Revoked(refund);
}
}
نکات کلیدی کد و نحوه گسترش آن
الگوریتم vestedAmount آزادسازی خطی را محاسبه می کند و کلیف را لحاظ می کند. تابع fund با transferFrom کار می کند، پس باید قبل از آن در توکن، مجوز کافی به قرارداد بدهید. release تنها برای ذینفع مجاز است تا الگوی pull پرداخت رعایت شود و ریسک حملات کاهش یابد. برای پشتیبانی از چند ذینفع می توانید به جای متغیرهای تکی، یک mapping از شناسه برنامه به ساختاری شامل پارامترها و وضعیت بسازید یا از یک فکتوری برای ایجاد چند قرارداد استفاده کنید.
مراحل استقرار و راه اندازی در شبکه آزمایشی
- انتخاب شبکه و پارامترها: شبکه مورد نظر، آدرس توکن، آدرس ذینفع، زمان شروع، طول کلیف، مدت و قابلیت فسخ را تعیین کنید.
- کامپایل با نسخه 0.8.20 یا بالاتر: تنظیمات کامپایلر را روی بهینه ساز مناسب قرار دهید تا گس کمتری مصرف شود.
- استقرار قرارداد: با ابزارهایی مانند Foundry، Hardhat یا Remix قرارداد را دیپلوی کنید.
- تامین موجودی: از طریق تابع fund مقدار کل برنامه را با transferFrom به قرارداد منتقل کنید.
- تایید: موجودی قرارداد و مقادیر پارامترها را بررسی کنید و رویداد Funded را در مرورگر بلاکچین ببینید.
- برداشت های دوره ای: ذینفع هر زمان که releasable بزرگتر از صفر باشد می تواند release را صدا بزند.
- فسخ در صورت نیاز: اگر revocable فعال است، مالک می تواند revoke را فراخوانی کند تا مقدار آزاد نشده بازگردد.
ادغام با ERC20 و ملاحظات توکن
قرارداد از یک واسط ساده IERC20 استفاده می کند که با بیشتر توکن های استاندارد سازگار است. در صورت کار با توکن های غیر استاندارد مطمئن شوید transfer و transferFrom مقدار بولی بر می گردانند. اگر توکن شما دارای مکانیزم کارمزد است، totalAllocated را بر اساس مقدار واقعی دریافتی تنظیم کنید تا محاسبات دقیق بماند.
آزمون پذیری و سناریوهای ضروری
برای اطمینان از صحت منطق، چند سناریو را پوشش دهید: پیش از کلیف مقدار قابل آزادسازی صفر باشد؛ در لحظه های میانی نسبت آزادسازی دقیق باشد؛ پس از مدت کل، تمام موجودی آزاد شده باشد؛ در صورت فسخ، تنها مقدار آزاد نشده بازگردد و آزادسازی آتی متوقف شود؛ و در صورت نبود موجودی کافی، تامین یا انتقال به درستی مدیریت شود.
بهترین روش های امنیتی
- اصل کمینه اختیارات: فقط ذینفع اجازه release و فقط مالک اجازه fund و revoke را داشته باشد.
- الگوی pull پرداخت: به جای ارسال خودکار، اجازه برداشت به ذینفع داده شود.
- بررسی بازگشت توابع انتقال: مقدار بازگشتی transfer و transferFrom را چک کنید.
- اجتناب از وابستگی به بلاک تایم دقیق: زمان ها تقریبی هستند، بنابراین چند ثانیه عدم قطعیت را در تست ها لحاظ کنید.
- ثبت رویدادها: برای ردگیری آن چین، همه تغییرات مهم را لاگ کنید.
خطاهای متداول و روش رفع آن
- فراموشی approve پیش از fund: ابتدا در قرارداد توکن به آدرس وستینگ مجوز دهید.
- کلیف بزرگتر از مدت: شرط های ورودی را بررسی و در constructor محدود کنید.
- تامین مرحله ای: اگر چند بار fund می کنید، تصور کنید totalAllocated جمع می شود و فرمول آزادسازی باید این جمع را پوشش دهد.
- توکن های با کارمزد: به جای مقدار درخواستی، مقدار دریافتی را مبنا قرار دهید یا از توکن های بدون کارمزد استفاده کنید.
گسترش به چند ذینفع و چند برنامه
برای پروژه های بزرگ، می توانید یک ساختار Program بسازید که شامل beneficiary، start، cliff، duration، revocable، totalAllocated، released و revoked باشد و سپس mapping از شناسه به Program نگه دارید. توابعی مانند createProgram، fundProgram، releaseProgram و revokeProgram اضافه می شود. این الگو مدیریت یکجا را ساده و هزینه گس را کاهش می دهد، هرچند پیچیدگی تست بیشتر می شود.
بهینه سازی گس و نگه داری بلندمدت
از انواع uint64 برای زمان ها استفاده کنید تا اندازه ذخیره سازی کاهش یابد. متغیرهای مرتبط را طوری چیدمان کنید که در یک اسلات قرار بگیرند. متغیرهای ثابت مانند owner و token را immutable تعریف کنید تا خواندن ارزان تر شود. برای ارتقاپذیری، به جای پروکسی اگر نیاز نیست از سادگی قرارداد نگذرید؛ سادگی خود یک مزیت امنیتی است.
مثال استفاده عملی و گردش کار تیمی
فرض کنید تیم شما 1 میلیون توکن برای اعضا در نظر گرفته است. برای هر نفر یک قرارداد یا یک برنامه در قرارداد چندذینفعه تعریف کنید: شروع امروز، کلیف 6 ماه و مدت 24 ماه. مالک مقدار کل هر برنامه را تامین می کند. ذینفع هر چند ماه یک بار release را فراخوانی می کند و در صورت ترک همکاری پیش از پایان، با revoke بخش آزاد نشده باز می گردد. این الگو برای سرمایه گذاران خصوصی نیز کارآمد است.
نکته های تست در محیط های توسعه
در Foundry از warp برای جهش زمان و در Hardhat از helpers مانند time.increase استفاده کنید. مقادیر مرزی دقیقا در لحظه پایان کلیف یا پایان مدت را بررسی کنید. رویدادها را تایید و موجودی های قبل و بعد از release و revoke را مقایسه کنید تا خطاهای حسابداری زودتر مشخص شوند.
کار با رابط کاربری و اتوماسیون
می توانید یک داشبورد ساده بسازید که releasable را نمایش دهد و با یک دکمه release را انجام دهد. برای اتوماسیون، رباتی بنویسید که در بازه های زمانی مشخص مقدار قابل آزادسازی را چک و اعلان ارسال کند. توجه کنید که عملیات مالی را به عهده ذینفع بگذارید تا الگوی pull حفظ شود.
اصلاحات پیشنهادی و ویژگی های پیشرفته
اگر به انعطاف بیشتری نیاز دارید، افزودن توقف اضطراری، تغییر ذینفع با تایید دو طرف یا پشتیبانی از چند توکن را در نظر بگیرید. برای الگوهای مرحله ای، یک آرایه از مایلستون ها با درصد تجمعی پیاده سازی کنید و مراقب جمع درصدها و ترتیب زمانی باشید. این توسعه ها را تنها در صورتی اضافه کنید که نیاز واقعی وجود دارد تا پیچیدگی بی مورد وارد نشود.
جمع بندی
با پیاده سازی یک قرارداد شفاف و ساده، می توانید آزادسازی منظم توکن ها را بدون نیاز به اعتماد به فرد ثالث مدیریت کنید. الگوی ارائه شده پایه ای محکم برای کاربردهای واقعی است و به سادگی به چند ذینفع، برنامه های موازی و ویژگی های پیشرفته قابل گسترش است. اگر در حال ساخت برنامه زمانبندی برای تیم یا سرمایه گذاران هستید، استفاده از Token Vesting با سالیدیتی روشی قابل اتکا و مقرون به صرفه است که به کمک تست و حسابرسی مناسب، ریسک شما را به حداقل می رساند.






