תארו לעצמכם: הפרויקט שלכם מגייס 10 מיליון דולר ו-100,000 אנשים רוצים לקנות טוקנים, אבל יש מספיק הקצאות רק ל-5,000 משתתפים. אם תשתמשו ב-FCFS פשוט (כל הקודם זוכה), בוטים יחטפו הכל בשניות, וישאירו משתמשים אמיתיים בידיים ריקות. או שתפעילו הגרלה, אבל ללא אקראיות אמינה על-השרשרת—התוצאה צפויה למניפולטור. בחירת משתתפים נאמנים והגנה מפני התקפות Sybil הם האתגר המרכזי בעיצוב מערכת הקצאה הוגנת וחזקה. אנו פותרים זאת על ידי פיתוח חוזים חכמים באמצעות תקנים מודרניים (ERC-20, ERC-721, ERC-1155) וכלים (Chainlink VRF, Merkle proof, Gitcoin Passport).
המשימה של מערכת ההקצאה היא לקבוע מי זכאי לרכוש טוקנים, כמה, וכיצד לאכוף את הביצוע ללא ניצול לרעה. להלן נפרט את המודלים המרכזיים, היישום שלהם ב-Solidity, וההגנה מפני התקפות.
אילו מודלי הקצאה קיימים?
השוואת מודלי הקצאה
| מודל | מורכבות יישום | הגנה מפני בוטים | הוגנות | גמישות |
|---|---|---|---|---|
| הגרלה (VRF) | נמוכה | גבוהה | אקראית | נמוכה |
| FCFS עם קבוצות | בינונית | בינונית | שווה | בינונית |
| מבוסס ניקוד | גבוהה | תלוי במשקלים | יחסי | גבוהה |
| מערכת מדורגת | גבוהה | גבוהה | מובחנת | גבוהה |
הגרלה עם Chainlink VRF
האפשרות הפשוטה ביותר: מתוך N משתתפים רשומים, אנו בוחרים באקראיות K זוכים. הוגן, אבל המזל אינו מתואם עם מעורבות או עניין בפרויקט.
אקראיות על-השרשרת היא בעיה קשה. Chainlink VRF v2 הוא הפתרון הנכון לסביבת ייצור:
import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
contract AllocationLottery is VRFConsumerBaseV2 {
uint256 public subscriptionId;
bytes32 public keyHash;
address[] public applicants;
address[] public winners;
uint256 public winnerCount;
mapping(uint256 => uint256) private requestToWinnerCount;
function drawWinners(uint256 count) external onlyOwner {
winnerCount = count;
uint256 requestId = COORDINATOR.requestRandomWords(
keyHash,
subscriptionId,
3,
100000,
1
);
requestToWinnerCount[requestId] = count;
}
function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
uint256 seed = randomWords[0];
uint256 count = requestToWinnerCount[requestId];
uint256 total = applicants.length;
// Fisher-Yates partial shuffle
address[] memory pool = applicants; // копия для мутации
for (uint256 i = 0; i < count; i++) {
uint256 j = i + (seed % (total - i));
seed = uint256(keccak256(abi.encode(seed, i)));
(pool[i], pool[j]) = (pool[j], pool[i]);
winners.push(pool[i]);
}
}
} FCFS עם קבוצות
במקום FCFS טהור, אנו מחלקים את המכירה לקבוצות זמן—כל אחת נמשכת שעה. בכל קבוצה, כל משתתף מאומת יכול לקנות לכל היותר X טוקנים. בוטים מאבדים את היתרון שלהם: המגבלה לכתובת זהה, והמהירות לא עוזרת.
הקצאה מבוססת ניקוד
משתתפים צוברים נקודות לפני המכירה: החזקת טוקנים של הפרויקט, השתתפות ב-testnet, פעילות קהילתית. ההקצאה יחסית לנקודות.
contract ScoreBasedAllocation {
mapping(address => uint256) public scores;
uint256 public totalScore;
uint256 public totalAllocation; // общий пул для распределения
function getAllocation(address user) public view returns (uint256) {
if (totalScore == 0) return 0;
return (scores[user] * totalAllocation) / totalScore;
}
function finalizeScores(address[] calldata users, uint256[] calldata userScores) external onlyAdmin {
for (uint256 i = 0; i < users.length; i++) {
scores[users[i]] = userScores[i];
totalScore += userScores[i];
}
}
}בעיה: הניקוד מחושב מחוץ לשרשרת, מה שדורש אמון במפעיל. פתרון: פרסום שורש Merkle של תמונת הניקוד ואימות על-השרשרת במהלך הרכישה.
מערכת מדורגת
מספר רמות עם מגבלות ועדיפויות שונות:
| דרגה | תנאי כניסה | הקצאה מובטחת | FCFS מעבר להבטחה |
|---|---|---|---|
| זהב | Stake >= 10,000 טוקנים למשך 90 יום | $5,000 | כן, עד $15,000 |
| כסף | Stake >= 1,000 טוקנים למשך 30 יום | $1,000 | כן, עד $5,000 |
| ארד | KYC עבר | $200 | לא |
| ציבורי | — | — | FCFS, שארית |
enum Tier { NONE, BRONZE, SILVER, GOLD }
struct TierConfig {
uint256 minStake;
uint256 minStakeDays;
uint256 guaranteedAllocationUSD;
uint256 maxAllocationUSD;
}
mapping(Tier => TierConfig) public tierConfigs;
mapping(address => Tier) public userTier;
function computeTier(address user) public view returns (Tier) {
uint256 staked = stakingContract.stakedAmountFor(user);
uint256 stakeDuration = stakingContract.stakeDurationFor(user);
if (staked >= tierConfigs[Tier.GOLD].minStake && stakeDuration >= tierConfigs[Tier.GOLD].minStakeDays * 1 days) return Tier.GOLD;
if (staked >= tierConfigs[Tier.SILVER].minStake && stakeDuration >= tierConfigs[Tier.SILVER].minStakeDays * 1 days) return Tier.SILVER;
if (kycRegistry.isVerified(user)) return Tier.BRONZE;
return Tier.NONE;
} כיצד להגן על ההקצאה מפני התקפות Sybil?
מערכות מבוססות דרגות וניקוד פגיעות להתקפות Sybil: משתתף אחד יוצר 100 כתובות ומחלק את ה-stake. ההגנה היא רב-שכבתית:
- Gitcoin Passport או Proof of Humanity — זהות על-השרשרת עם עמידות ל-Sybil. משולב כתנאי מוקדם לרישום:
import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol"; contract AllocationLottery is VRFConsumerBaseV2 { uint256 public subscriptionId; bytes32 public keyHash; address[] public applicants; address[] public winners; uint256 public winnerCount; mapping(uint256 => uint256) private requestToWinnerCount; function drawWinners(uint256 count) external onlyOwner { winnerCount = count; uint256 requestId = COORDINATOR.requestRandomWords( keyHash, subscriptionId, 3, 100000, 1 ); requestToWinnerCount[requestId] = count; } function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override { uint256 seed = randomWords[0]; uint256 count = requestToWinnerCount[requestId]; uint256 total = applicants.length; // Fisher-Yates partial shuffle address[] memory pool = applicants; // копия для мутации for (uint256 i = 0; i < count; i++) { uint256 j = i + (seed % (total - i)); seed = uint256(keccak256(abi.encode(seed, i))); (pool[i], pool[j]) = (pool[j], pool[i]); winners.push(pool[i]); } } }. - ניקוד ריבועי — הקצאה יחסית ל-√(stake) במקום stake. זה מפחית את היתרון של מחזיקים גדולים.
- Staking עם נעילה — טוקנים חייבים להיות נעולים לפחות 30-90 יום לפני תמונת המצב. זה הופך התקפת Sybil ליקרה.
- ניתוח גרף חברתי — מחוץ לשרשרת: אשכולות כתובות עם דפוסים דומים מוחרגים מהרשימה הלבנה.
פרטי יישום של הגנת Sybil
לאינטגרציה עם Gitcoin Passport, אנו משתמשים ב-oracle שמנפיק ניקוד לפעולות מאומתות. דוגמה לתנאי: contract ScoreBasedAllocation { mapping(address => uint256) public scores; uint256 public totalScore; uint256 public totalAllocation; // общий пул для распределения function getAllocation(address user) public view returns (uint256) { if (totalScore == 0) return 0; return (scores[user] * totalAllocation) / totalScore; } function finalizeScores(address[] calldata users, uint256[] calldata userScores) external onlyAdmin { for (uint256 i = 0; i < users.length; i++) { scores[users[i]] = userScores[i]; totalScore += userScores[i]; } } } . Staking עם נעילה מיושם באמצעות חוזה מותאם שבו טוקנים נעולים לתקופה מוגדרת. אשכולות כתובות מזוהים באמצעות מודל ML מחוץ לשרשרת שמנתח דפוסי עסקאות.
למה אופטימיזציית גז חשובה?
כל עסקה ברשת Ethereum עולה כסף. עם השתתפות המונית (עשרות אלפי כתובות), עלויות הגז יכולות לחרוג מתקציב הפרויקט. אנו משתמשים בעיבוד קבוצתי, אופטימיזציית calldata, ודפוסי אחסון (למשל, enum Tier { NONE, BRONZE, SILVER, GOLD } struct TierConfig { uint256 minStake; uint256 minStakeDays; uint256 guaranteedAllocationUSD; uint256 maxAllocationUSD; } mapping(Tier => TierConfig) public tierConfigs; mapping(address => Tier) public userTier; function computeTier(address user) public view returns (Tier) { uint256 staked = stakingContract.stakedAmountFor(user); uint256 stakeDuration = stakingContract.stakeDurationFor(user); if (staked >= tierConfigs[Tier.GOLD].minStake && stakeDuration >= tierConfigs[Tier.GOLD].minStakeDays * 1 days) return Tier.GOLD; if (staked >= tierConfigs[Tier.SILVER].minStake && stakeDuration >= tierConfigs[Tier.SILVER].minStakeDays * 1 days) return Tier.SILVER; if (kycRegistry.isVerified(user)) return Tier.BRONZE; return Tier.NONE; } במקום require(passport.getScore(msg.sender) >= MIN_SCORE) לאיטרציה). לפי Etherscan, העלות הממוצעת של פריסת חוזה חכם מורכב היא כ-$3,000 בגז. זה מפחית את עלויות הפריסה ב-40% ואת הפעולות ב-30%.
מכניקת ביצוע: רשימה לבנה + רכישה
לאחר קביעת ההקצאות, אנו מפרסמים שורש Merkle ומתחילים את תקופת הרכישה:
contract TokenSale {
bytes32 public whitelistRoot;
mapping(address => uint256) public purchased;
struct AllocationProof {
uint256 maxAllocationUSD;
bytes32[] merkleProof;
}
function purchase(uint256 usdcAmount, AllocationProof calldata proof) external {
bytes32 leaf = keccak256(bytes.concat(
keccak256(abi.encode(msg.sender, proof.maxAllocationUSD))
));
require(MerkleProof.verify(proof.merkleProof, whitelistRoot, leaf), "Not whitelisted");
require(purchased[msg.sender] + usdcAmount <= proof.maxAllocationUSD, "Exceeds allocation");
uint256 tokenAmount = (usdcAmount * TOKEN_PRICE_DENOMINATOR) / tokenPriceUSD;
purchased[msg.sender] += usdcAmount;
usdc.transferFrom(msg.sender, treasury, usdcAmount);
token.transfer(msg.sender, tokenAmount);
emit Purchase(msg.sender, usdcAmount, tokenAmount);
}
} מה כלול בפיתוח מערכת ההקצאה?
אנו מתכננים ומיישמים מערכות הקצאה turnkey. במשך עשרות מכירות טוקנים, צברנו מומחיות באופטימיזציית גז והגנה מפני בוטים. מה תקבלו:
- עיצוב מודל הקצאה למכירת הטוקנים שלכם (הגרלה, ניקוד, דרגות, היברידיים).
- פיתוח ובדיקת חוזים חכמים (Hardhat + Foundry), אינטגרציה של Chainlink VRF, Gitcoin Passport.
- פריסה ברשת היעד (Ethereum, Polygon, Arbitrum, BNB Chain).
- תיעוד: תיאורי פונקציות, תרחישי שימוש, מדריך ניהול.
- תמיכה קצרת טווח במהלך השקת מכירת הטוקנים (עד שבועיים).
מערכת הקצאה מעוצבת היטב היא גם הנדסה וגם תורת המשחקים. המטרה היא להפוך השתתפות כנה לזולה יותר ממניפולציה. רשימה לבנה מבוססת Merkle היא קו הבסיס המינימלי; staking מדורג והגנת Sybil הם מה שמבדיל launchpad מחושב היטב מ-FCFS פרימיטיבי.
קבלו ייעוץ למכירת הטוקנים שלכם—נעריך את המודל ונציע פתרונות אופטימליים. צרו קשר להערכה חינם.







