תארו לעצמכם שאתם משיקים אפליקציית הגרלה מבוזרת (dApp) עם קופה של 100 אלף דולר. המשתתפים דורשים הוגנות מובטחת—בחירת זוכה באמצעות block.timestamp נותנת למאמתים כוח מניפולציה. צעד אחד שגוי והכספים עלולים ללכת לאיבוד עקב התקפת כניסה חוזרת (reentrancy) או ריצה מקדימה (front-running). אנחנו, מהנדסי בלוקצ'יין עם ניסיון בעשרות פרויקטים, פותרים בעיות אלו על ידי הטמעת Chainlink VRF וביקורות חוזים קפדניות. ארכיטקטורה דומה שימשה בהגרלות עם קופות מצטברות של למעלה מ-2 מיליון דולר—אפס פרצות. במאמר זה, נעבור על בניית הגרלה הוגנת וניתנת לאימות על חוזים חכמים.
מדוע אקראיות על השרשרת אינה בטוחה
block.prevrandao באתריום נותן למאמתים סיבית אחת של השפעה. RANDAO מצרף אנטרופיה, אך לחשיפה האחרונה יש השפעה. עבור הגרלה עם קופה של יותר ממיליון דולר, הדבר ניתן לניצול כלכלי: מאמת יכול לעכב את החשיפה. Chainlink VRF פותר זאת קריפטוגרפית: אקראיות נוצרת מחוץ לשרשרת עם הוכחה הניתנת לאימות על השרשרת. זיוף האקראיות בלתי אפשרי. יתרה מכך, VRF משתמש בצמד מפתחות: המפתח הסודי של האורקל יוצר את המספר, והמפתח הציבורי מאפשר לחוזה לאמת את ההוכחה. זה מבטיח הוגנות גם אם מפעיל ה-VRF אינו מהימן.
כיצד Chainlink VRF מבטיח הוגנות
החוזה מבקש מספר אקראי באמצעות requestRandomWords, והאורקל מחזיר אותו ב-fulfillRandomWords יחד עם הוכחה. החוזה מאמת את ההוכחה—אם היא לא תקינה, התוצאה נדחית. אנו משתמשים במודל המנויים (VRF 2.5), שזול יותר לבקשות תכופות: משלמים פעם אחת על מנוי, ואז רק על גז. עלות הגז לבקשה היא כ-200 אלף גז, שבאתריום ב-20 Gwei הוא בערך 5-10 דולר. עבור הגרלות עם הגרלות תכופות, זה מקובל.
ארכיטקטורת חוזה הגרלה עם VRF
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
contract Lottery is VRFConsumerBaseV2Plus {
uint256 public subscriptionId;
bytes32 public keyHash;
uint32 public callbackGasLimit = 100000;
uint16 public requestConfirmations = 3;
address[] public participants;
uint256 public pendingRequestId;
LotteryState public state;
enum LotteryState {
OPEN,
DRAWING,
CLOSED
}
function drawWinner() external onlyOwner {
require(state == LotteryState.OPEN, "Not open");
require(participants.length > 0, "No participants");
state = LotteryState.DRAWING;
pendingRequestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: keyHash,
subId: subscriptionId,
requestConfirmations: requestConfirmations,
callbackGasLimit: callbackGasLimit,
numWords: 1,
extraArgs: VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
)
})
);
}
function fulfillRandomWords(
uint256 requestId,
uint256[] calldata randomWords
) internal override {
require(requestId == pendingRequestId, "Wrong requestId");
uint256 winnerIndex = randomWords[0] % participants.length;
address winner = participants[winnerIndex];
state = LotteryState.CLOSED;
// выплата победителю
payable(winner).transfer(address(this).balance);
}
} פרטי יישום קריטיים
// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol"; import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol"; contract Lottery is VRFConsumerBaseV2Plus { uint256 public subscriptionId; bytes32 public keyHash; uint32 public callbackGasLimit = 100000; uint16 public requestConfirmations = 3; address[] public participants; uint256 public pendingRequestId; LotteryState public state; enum LotteryState { OPEN, DRAWING, CLOSED } function drawWinner() external onlyOwner { require(state == LotteryState.OPEN, "Not open"); require(participants.length > 0, "No participants"); state = LotteryState.DRAWING; pendingRequestId = s_vrfCoordinator.requestRandomWords( VRFV2PlusClient.RandomWordsRequest({ keyHash: keyHash, subId: subscriptionId, requestConfirmations: requestConfirmations, callbackGasLimit: callbackGasLimit, numWords: 1, extraArgs: VRFV2PlusClient._argsToBytes( VRFV2PlusClient.ExtraArgsV1({nativePayment: false}) ) }) ); } function fulfillRandomWords( uint256 requestId, uint256[] calldata randomWords ) internal override { require(requestId == pendingRequestId, "Wrong requestId"); uint256 winnerIndex = randomWords[0] % participants.length; address winner = participants[winnerIndex]; state = LotteryState.CLOSED; // выплата победителю payable(winner).transfer(address(this).balance); } } : כמה בלוקים להמתין לפני יצירת האקראיות. מינימום 3, מומלץ 5 או יותר. requestConfirmations: מגבלת גז עבור callbackGasLimit. אם הלוגיקה משתמשת ביותר גז, העסקה נכשלת והחוזה נתקע. עדיף לאחסן את winnerIndex ולתת לזוכה לתבוע את הפרס. fulfillRandomWords: מודל המנויים מומלץ להגרלות קבועות—הוא מפחית את עלות הבקשה בעד 40%.
כיצד להגן מפני ריצה מקדימה?
אם זמן ההגרלה ידוע, בוטים של MEV יכולים לקנות את הכרטיס האחרון באותו בלוק כמו Subscription vs Direct Funding. פתרונות: commit-reveal לרכישת כרטיסים או סגירת מכירות N בלוקים לפני ההגרלה. Chainlink Automation מבטל קריאות ידניות—החוזה עצמו קורא ל-drawWinner בלוח זמנים או כאשר תנאים מתקיימים. זה הופך ריצה מקדימה לבלתי אפשרית כמעט.
פרצות נפוצות בחוזי הגרלה
כניסה חוזרת במהלך תשלום
אנו משתמשים בתבנית משיכה (pull-pattern): הזוכה קורא ל-drawWinner, שמעדכן את המצב לפני ההעברה. זה מונע כניסה חוזרת. בקוד לעיל אנו משתמשים בדחיפה (push)—עבור חוזי ייצור אנו תמיד מחליפים אותה במשיכה.
ריכוזיות שליטה
claimPrize() על onlyOwner הוא ריכוזי. אנו הופכים את ההגרלה לאוטומטית באמצעות Chainlink Automation, ומסירים את הסיכון לקנוניה בין הבעלים למשתתפים.
אינטגרציה עם Chainlink Automation
הגרלה בלוח זמנים או לפי תנאי ללא קריאה ידנית:
function checkUpkeep(bytes calldata) external view override returns (bool upkeepNeeded, bytes memory) {
upkeepNeeded = (state == LotteryState.OPEN && participants.length >= minParticipants && block.timestamp >= nextDrawTime);
}
function performUpkeep(bytes calldata) external override {
drawWinner();
} בדיקות וביקורת
בדיקות עם Foundry באמצעות רכז VRF מדומה. כיסוי קוד: 100% ענפים, 99% שורות. Fuzzing על פרמטרים (מספר משתתפים, סכומים, מגבלת גז). עבור רשת בדיקות: Sepolia עם VRF אמיתי. אנו מבטיחים איכות: למעלה מ-50 פרויקטים מיושמים, 15+ מערכות הגרלה.
מה כלול
- פיתוח חוזה חכם עם VRF ו-Automation
- כיסוי בדיקות מלא (Foundry, fuzzing)
- פריסה לרשת ראשית/רשת בדיקות
- תיעוד ומדריך תפעול
- תמיכה לאחר השקה (שבועיים)
| שיטת תשלום | אבטחה | עלות גז | סיכונים נוספים |
|---|---|---|---|
| דחיפה (העברה ישירה) | נמוכה (כניסה חוזרת) | נמוכה | כניסה חוזרת, עלות גבוהה בשגיאה |
| משיכה (תביעה) | גבוהה | בינונית (המשתמש משלם) | תלות במשתמש |
| מקור אקראיות | אבטחה | עלות | דוגמה |
|---|---|---|---|
| block.timestamp | נמוכה (התקפת כורה) | חינם | חוזה תחביב |
| block.prevrandao | בינונית (השפעת סיבית אחת) | חינם | פרויקטים ישנים |
| Chainlink VRF | קריפטוגרפית | LINK לבקשה | הגרלה אמינה |
לוחות זמנים
חוזה בסיסי: 3-5 ימי פיתוח + 1-2 ימי בדיקות. מורחב (קופות מרובות, כרטיסי NFT): 2-3 שבועות. ביקורת מומלצת לכל חוזה עם קופה של יותר מ-50 אלף דולר. עלות מחושבת באופן אישי.
צרו קשר לייעוץ על הפרויקט שלכם—ננתח את הדרישות ונציע פתרון אופטימלי. בקשו פיתוח חוזה הגרלה סוהר עם ערבות להוגנות.







