Building a custom vault
يمكنك كتابة vault خاص بك، عقد يستقبل إيرادات توكن CheesePad ضريبي ويفعل بها ما تصممه: يقسّمها، أو يدفقها، أو يقفلها، أو يعيد الشراء، أو يتبرع. لست بحاجة إلى وضع أي شيء في قائمة سماح ولا تلمس عقود CheesePad نفسها. تشحن عقدين، vault ومصنع vault، ويوجّه مُنشئ توكن إطلاقه إلى مصنعك. إذا اجتاز مصنعك فحوص الإطلاق، فسيُنشر الـ vault الخاص بك ويُوصَّل تلقائيًا.
أسرع طريق هو توسيع العقدين الأساسيين اللذين توفرهما CheesePad، VaultBase وVaultFactoryBase،
اللذين يتعاملان مع القوالب المتكررة (boilerplate) ويتركان لك الأجزاء المهمة.
المحتويات
- النموذج الذهني
- تواصل الإطلاق
- توسيع العقدين الأساسيين
- ثلاث قواعد لاستقبال الإيرادات
- المخططان
- وضع الإيرادات الأصلية مقابل ERC20
- مثال أدنى كامل
- قائمة تحقق قبل الشحن
- مطبّات
- تطبيقات مرجعية
النموذج الذهني
tax on every buy/sell
trader ──swap──▶ tax token ──▶ settles tax ──splits revenue──▶ YOUR VAULT
│
you decide what happens here
(distribute / claim / lock / ...)
- تُجمَع ضريبة التوكن وتُسوّى على دفعات، وليس في كل صفقة.
- عندما تُسوّى، تُرسَل حصة مستلم الرسوم إلى أي عنوان يكون مستلم رسوم التوكن. في إطلاق vault، ذلك العنوان هو الـ vault الخاص بك.
- تصل الإيرادات كـ BNB أصلي إذا كان توكن التسعير الخاص بالبيع هو WBNB، أو كـ ERC20 التسعير خلاف ذلك. يُخبَر الـ vault الخاص بك بأيّهما عند الإنشاء.
عقدان، وظيفتان:
| Contract | Job |
|---|---|
| Vault | يحتفظ بالإيرادات، وينفّذ منطق الدفع الخاص بك (distribute، claim، ...). واحد لكل إطلاق. |
| Vault factory | يتحقق من إطلاق وينشر vault جديدًا له. مصنع واحد يخدم إطلاقات كثيرة. |
تواصل الإطلاق
عندما يطلق مُنشئ بمصنعك (createMicroSaleTaxTokenWithVaultV2، أو مكافئ Melt Pool
createBondingCurveTaxTokenWithVaultV2)، تجري CheesePad هذه الفحوص بمبدأ الفشل الآمن. أي فشل يُلغي
الإطلاق بأكمله:
factory.diamond()تُرجع عنوان الـ launchpad، مثبتةً أن مصنعك يستهدف المنصة الصحيحة.factory.supportsQuoteToken(quoteToken)تُرجع true.factory.supportsLaunchRevision(1)تُرجع true.factory.reviewLaunch(payload)تُرجع(true, "")، وهي دالة الاعتراض (veto) الخاصة بك.factory.createVault(quoteToken, creator, vaultData)تنشر الـ vault.isNativeRevenue()الخاصة بالـ vault تساويquoteToken == WBNB. عدم التطابق يُلغي التنفيذ.- يربط الـ launchpad الـ vault كمستلم رسوم التوكن ويستدعي
vault.bind(token)مرة واحدة.
بعد هذا، يصبح الـ vault الخاص بك حيًا ويستقبل الإيرادات. أنت لا تستدعي CheesePad أبدًا؛ بل هي تستدعيك.
توسيع العقدين الأساسيين
الـ vault: وسّع VaultBase
يمنحك VaultBase، مجانًا:
| Provided | What it is |
|---|---|
diamond, quoteToken, creator, isNativeRevenue | سياق إطلاق ثابت (immutable) |
token, bound | التوكن الضريبي المربوط، يحدده الـ launchpad مرة واحدة عبر bind |
bind(token) | للـ launchpad فقط، لمرة واحدة؛ لا تتجاوزه (override) |
receive() | يقبل الإيرادات الأصلية فقط عندما تكون isNativeRevenue، ويرفض الأصل الخطأ |
onRevenue(amount) | إشارة أفضل-جهد من التوكن عند تسويته؛ تُصدر RevenueReceived |
_revenueBalance() | رصيد إيراداتك الحالي، بالأصل الصحيح |
_pay(payee, amount) | يدفع بالأصل الصحيح؛ يُرجع false بدلًا من فشل التنفيذ، ويتحمّل التوكنات التي لا تُرجع قيمة مثل USDT |
platform() | عنوان دعامة المنصة الاحتياطية، إذا اخترت الاشتراك فيها |
أنت تنفّذ ثلاث دوال (من IVault):
function distribute() external; // permissionless payout poke
function description() external view returns (string memory); // one-liner for listings
function vaultUISchema() external view returns (VaultUISchema memory); // see The two schemas
يأخذ المُنشئ (constructor) سياق الإطلاق:
constructor(address _diamond, address _quoteToken, address _creator, bool _isNativeRevenue)
المصنع: وسّع VaultFactoryBase
يتعامل VaultFactoryBase مع سباكة المراجعة (revision) وحارس الـ launchpad-فقط. أنت تنفّذ:
function _reviewV1(LaunchDataV1 memory d) internal view returns (bool ok, string memory reason);
function createVault(address quoteToken, address creator, bytes calldata vaultData)
external onlyDiamond returns (address vault);
function supportsQuoteToken(address quoteToken) external view returns (bool);
function vaultDataSchema() external view returns (VaultDataSchema memory);
_reviewV1 هي اعتراضك: تستقبل معاملات الإطلاق (LaunchDataV1) وتُرجع (false, reason) لحظر إطلاق
لا تريد خدمته. وهي لا تستقبل vaultData، لذا فأنت تتحقق من معاملات الإطلاق ومعاملات كل vault
بشكل مستقل.
ثلاث قواعد لاستقبال الإيرادات
تُسلَّم الإيرادات إلى الـ vault الخاص بك أثناء كون التوكن في منتصف التسوية. اكسر هذه ويمكنك أن تعطّل كل تحويل مفروض عليه الضريبة للتوكن:
- يجب أن تكون
receive()وonRevenue()رخيصتين وللتراكم فقط. مسك دفاتر وأحداث فقط. لا مبادلات، ولا استدعاءات خارجية ذات أثر. - لا تُعِد الاستدعاء إلى داخل التوكن أبدًا من
receive()أوonRevenue(). قراءته في منتصف التسوية قد تفشل أو تعيد الدخول (reenter). - قم بكل عمل الدفع في معاملة منفصلة. استدعاء
distribute()مفتوح بلا إذن، أو سحبclaim(). لا تدفع الأموال أبدًا أثناء استقبال الإيرادات.
لهذا يتراكم كل vault في CheesePad عند الاستقبال ويدفع لاحقًا.
المخططان
الـ vault والمصنع الخاصان بك يصفان نفسيهما ذاتيًا. تقرأ واجهة مستخدم CheesePad مخططين وتعرض نموذج إنشائك وواجهة الـ vault الخاصة بك بشكل عام، دون أي كود خاص بالـ vault الخاص بك.
vaultDataSchema() على المصنع: نموذج الإنشاء
يصف كيفية ترميز vaultData بـ ABI، حتى تتمكن واجهة المستخدم من عرض نموذج الإطلاق وترميز مدخلات
المُنشئ.
function vaultDataSchema() external pure returns (VaultDataSchema memory s) {
s.description = "Fixed payee list; revenue splits by bps";
s.isArray = false; // vaultData = abi.encode(tuple); true => tuple[]
s.fields = new Field[](2);
s.fields[0] = Field("payees", FieldType.Address, true, 0, "Recipient addresses");
s.fields[1] = Field("bps", FieldType.Uint16, true, 0, "Basis points per payee (sum 10000)");
}
ثم تفكّ createVault الخاصة بك ترميز vaultData بالترتيب والأنواع بالضبط التي يعلنها المخطط:
(address[] memory payees, uint16[] memory bps) = abi.decode(vaultData, (address[], uint16[]));
vaultUISchema() على الـ vault: الإجراءات والحالة القابلة للقراءة
قائمة من الدوال. كل واحدة إما إجراء كتابة (تعرض واجهة المستخدم زرًا ونموذج إدخال) أو حقل قراءة
(isWrite = false؛ تستدعيها واجهة المستخدم وتعرض النتيجة المُصنَّفة). هكذا تعرض واجهة المستخدم حالة
الـ vault الخاص بك، بما فيها حقول لم ترها من قبل، دون معرفة عقدك.
لكشف حقل حالة، أعلن دالة قراءة بمخرَج مُصنَّف:
m.name = "lockDuration";
m.selector = bytes4(keccak256("lockDuration()"));
m.isWrite = false;
m.outputs = new Field[](1);
m.outputs[0] = Field("lockDuration", FieldType.Time, false, 0, "Seconds until unlock");
تستدعي واجهة المستخدم الـ selector، وتفكّ ترميز العائد بالنوع المُعلَن، وتنسّقه. وهي لا تعرض إلا ما
تعلنه، لذا اذكر كل قراءة تريد إظهارها. قائمة الأنواع الكاملة (Field، Method، FieldType،
ApprovalHint) موجودة في events-and-data.md.
وضع الإيرادات الأصلية مقابل ERC20
تُحدَّد isNativeRevenue عند الإنشاء ويجب أن تساوي quoteToken == WBNB. يؤكّد الـ launchpad هذا عند
الإطلاق ويفشل التنفيذ عند عدم التطابق.
- الوضع الأصلي (التسعير هو WBNB): تصل الإيرادات كـ BNB عبر
receive()._revenueBalance()تقرأ رصيد BNB الخاص بالـ vault؛ و_payترسل BNB. - وضع ERC20: تصل الإيرادات كتوكن التسعير (ينمو الرصيد، دون استدعاء
receive()).VaultBase.receive()ترفض BNB العابر حتى لا تعلق الأموال._revenueBalance()تقرأ رصيد التسعير؛ و_payتجري تحويل ERC20.
إذا بنيت على VaultBase واستخدمت _revenueBalance() و_pay()، فكلا الوضعين مُعالَجان لك.
مثال أدنى كامل
vault يدفق 100% من الإيرادات إلى مستلم واحد يُحدَّد عند الإطلاق.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {VaultBase} from "./VaultBase.sol";
import {VaultFactoryBase} from "./VaultFactoryBase.sol";
import {LaunchDataV1} from "./IVaultFactory.sol";
import {VaultUISchema, VaultDataSchema, Method, Field, FieldType} from "./IVaultSchema.sol";
contract SingleRecipientVault is VaultBase {
address public recipient;
constructor(address _diamond, address _quote, address _creator, bool _native, address _recipient)
VaultBase(_diamond, _quote, _creator, _native)
{
require(_recipient != address(0), "bad recipient");
recipient = _recipient;
}
// Permissionless: push the whole balance to the recipient.
function distribute() external nonReentrant {
uint256 bal = _revenueBalance();
if (bal == 0) return;
_pay(recipient, bal); // production should track owed[] on failure; see the checklist
}
function description() external pure returns (string memory) {
return "Streams all revenue to a single recipient";
}
function vaultUISchema() external pure returns (VaultUISchema memory s) {
s.vaultType = "SingleRecipient";
s.description = "Streams all revenue to a single recipient";
s.methods = new Method[](2);
s.methods[0].name = "distribute";
s.methods[0].selector = bytes4(keccak256("distribute()"));
s.methods[0].isWrite = true;
s.methods[0].hint = "Send accrued revenue to the recipient";
s.methods[1].name = "recipient";
s.methods[1].selector = bytes4(keccak256("recipient()"));
s.methods[1].isWrite = false;
s.methods[1].outputs = new Field[](1);
s.methods[1].outputs[0] = Field("recipient", FieldType.Address, false, 0, "Who receives the revenue");
s.methods[1].hint = "Revenue recipient";
}
}
contract SingleRecipientVaultFactory is VaultFactoryBase {
address public immutable weth;
constructor(address _diamond, address _weth) VaultFactoryBase(_diamond) { weth = _weth; }
function supportsQuoteToken(address) external pure returns (bool) { return true; }
// Veto launches that would never fund the vault.
function _reviewV1(LaunchDataV1 memory d) internal pure override returns (bool, string memory) {
if (d.feeReceiverBps == 0) return (false, "feeReceiver bps must be > 0");
if (d.buyTaxBps == 0 && d.sellTaxBps == 0) return (false, "tax must be > 0");
return (true, "");
}
function createVault(address quoteToken, address creator, bytes calldata vaultData)
external onlyDiamond returns (address vault)
{
address recipient = abi.decode(vaultData, (address));
// isNativeRevenue MUST be (quoteToken == weth); the launchpad enforces this.
vault = address(new SingleRecipientVault(diamond, quoteToken, creator, quoteToken == weth, recipient));
}
function vaultDataSchema() external pure returns (VaultDataSchema memory s) {
s.description = "The single address that receives all revenue";
s.isArray = false;
s.fields = new Field[](1);
s.fields[0] = Field("recipient", FieldType.Address, false, 0, "Revenue recipient");
}
}
انشر المصنع مرة واحدة. يحتاج مُنشئه عنوان الـ launchpad بالإضافة إلى عنوان WBNB؛ اشتقّ WBNB من راوتر
الـ launchpad بالطريقة التي يفعلها scripts/launchpad/deploy-vault-factory.ts، وأكّد
factory.weth() == router.WETH(). ثم يمرّر المُنشئون عنوان مصنعك وabi.encode(recipient) كـ
vaultData عند الإطلاق.
قائمة تحقق قبل الشحن
- الـ vault يوسّع
VaultBase؛ وينفّذdistribute()وdescription()وvaultUISchema(). - المصنع يوسّع
VaultFactoryBase؛ وينفّذ_reviewV1وcreateVault(onlyDiamond) وsupportsQuoteTokenوvaultDataSchema(). createVaultتبني الـ vault بـisNativeRevenue == (quoteToken == weth).createVaultتفكّ ترميزvaultDataبالترتيب والأنواع بالضبط لـvaultDataSchema().receive()وonRevenue()رخيصتان، وللتراكم فقط، ولا تُعيدان الاستدعاء إلى داخل التوكن أبدًا.- كل دفع يحدث في
distribute()أوclaim()، وليس أبدًا عند استقبال الإيرادات. - استخدم
_pay()وتحقق من قيمة عائدها. تُبلّغ عن الفشل بدلًا من فشل التنفيذ، لذا احتفظ برصيد مستفيد فاشل كـowedلـclaim()لاحق، فلا يمكن لمستفيد واحد سيّئ أن يعطّل الجميع. vaultUISchema()تعلن كل حقل قراءة تريد أن تعرضه واجهة مستخدم، بمخرجات مُصنَّفة.- حقول المبالغ تستخدم
decimals = 18لـ WBNB، أو خانات توكن التسعير العشرية خلاف ذلك.
مطبّات
- غير قابل للكنس (Un-sweepable) بالتصميم. يُقصَد أن تكون vaults CheesePad ذات سيادة وغير قابلة
للتغيير، حتى يتمكن المشترون من التحقق من عدم وجود استنزاف من قِبل مسؤول. إذا أضفت مخرج هروب، فتوقّع
أن يُقرأ كسلبية ثقة. إذا أردت دعامة منصة احتياطية، اشترك فيها صراحةً عبر
platform()وonlyPlatform، بالطريقة التي يفعلها GiftVault لمسار إنقاذه. distribute()مفتوحة بلا إذن. يمكن لأي شخص استدعاؤها، لذا اجعل حسابات الدفع آمنة عند استدعائها من قِبل أي شخص في أي وقت، وبلا عملية (no-op) على رصيد صفري.- غبار القسمة الصحيحة (Integer-division dust) في منطق التقسيم يجب أن يُرحَّل إلى
distributeالتالي، لا أن يفشل التنفيذ. reviewLaunchلا ترىvaultDataأبدًا. لا يمكنك التحقق المتبادل بين معاملات الإطلاق ومعاملات كل vault؛ فهي تُتحقَّق بشكل منفصل بالتصميم.- إصدار مصنع واحد هو عنوان واحد. المصانع عديمة الحالة وغير قابلة للتغيير. الإصدار الجديد هو نشر
جديد. لدعم مراجعة بيانات إطلاق مستقبلية، اشحن مصنعًا محدّثًا بدالة مراجعة جديدة ووسّع
supportsLaunchRevision.
تطبيقات مرجعية
في contracts/launchpad/vaults/:
SplitterVault.solوSplitterVaultFactory.sol: مستفيدون ثابتون بنقاط الأساس.GiftVault.solوGiftVaultFactory.sol: تدفق مربوط بالهوية مع ربط مُرحِّل EIP-712 وحل احتياطي بأرباح الحائزين. يُظهر أيضًا دعامة المنصة الاحتياطية الاختيارية.VaultBase.sol،VaultFactoryBase.sol،IVault.sol،IVaultFactory.sol،IVaultSchema.sol: العقدان الأساسيان والواجهات التي تبني مقابلها.