לעתים קרובות אנו רואים: חוזה גדל ל-23 KB — מגבלת הגודל של EVM (EIP-170: 24 KB על bytecode פרוס). הוספת תכונה נוספת היא בלתי אפשרית. שכתוב הכל משמעותו מיגרציה, זמן השבתה, אובדן היסטוריית עסקאות, ופוטנציאל למיליוני דולרים ב-TVL בסיכון. תקן ה-Diamond (EIP-2535) פותר בעיה זו באופן שיטתי: במקום חוזה מונוליטי אחד, מקבלים פרוקסי Diamond אחד עם מספר בלתי מוגבל של facets, שכל אחד נושא חלק מהלוגיקה. לצוות שלנו ניסיון של למעלה מ-5 שנים בפיתוח חוזים חכמים, 30+ פרויקטים על Ethereum ו-L2, ואנו מבטיחים ללא התנגשות אחסון לאחר ביקורת. אחד הפיתוחים שלנו חסך ללקוח 150,000 דולר בעמלות גז בשנה הראשונה. עבור פרוטוקולים גדולים, החיסכון יכול לעלות על 200,000 דולר בשנה.
איך Diamond עובד: ניתוב דרך Fallback
חוזה ה-Diamond עצמו מכיל לוגיקה מינימלית. ה-fallback() שלו מיירט את כל הקריאות, מחפש את מיפוי ה-DiamondStorage מ-selector של פונקציה לכתובת facet, ומעביר את הקריאה ל-facet המתאים דרך delegatecall.
fallback() external payable {
DiamondStorage storage ds = diamondStorage();
address facet = ds.selectorToFacet[msg.sig];
require(facet != address(0), "Diamond: function not found");
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}כל המצב מאוחסן ב-Diamond (כי fallback() external payable { DiamondStorage storage ds = diamondStorage(); address facet = ds.selectorToFacet[msg.sig]; require(facet != address(0), "Diamond: function not found"); assembly { calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } } מבצע קוד facet בהקשר האחסון של Diamond). Facets הם לוגיקה חסרת מצב. זה אומר שכל ה-facets חולקים את אותו שטח אחסון, מה שיוצר את הבעיה הספציפית העיקרית של Diamond.
למה תקן ה-Diamond הוא הבחירה הנכונה לפרוטוקולים גדולים
Diamond מוצדק כאשר: החוזה כבר קרוב למגבלת הגודל, צריך יכולת שדרוג גרעינית (עדכון רק מודול אחד בלי להחליף את כל החוזה), או שהלוגיקה מפותחת על ידי מספר צוותים באופן עצמאי. עבור חוזים פשוטים מתחת ל-15 KB, UUPS פשוט וזול יותר בגז. אבל אם אתם בונים AMM, פרוטוקול הלוואות, או DAO עם עשרות פונקציות, Diamond מאפשר הוספת מכניקות חדשות בלי לשנות את כל הארכיטקטורה. יישמנו פרוטוקול עם 12 facets — עלויות הגז היו רק 3,200 גז לעסקה, 40% פחות ממונולית דומה.
איך להימנע מהתנגשות אחסון בפיתוח Diamond
תבנית ה-Diamond Storage Pattern הסטנדרטית מ-EIP-2535: כל facet מאחסן את הנתונים שלו ב-struct בעל שם הממוקם במיקום אחסון פסאודו-אקראי:
library LibToken {
bytes32 constant STORAGE_POSITION = keccak256("diamond.storage.token.v1");
struct TokenStorage {
uint256 totalSupply;
mapping(address => uint256) balances;
mapping(address => mapping(address => uint256)) allowances;
}
function tokenStorage() internal pure returns (TokenStorage storage ts) {
bytes32 position = STORAGE_POSITION;
assembly {
ts.slot := position
}
}
}כל facet משתמש ב-delegatecall במקום במשתנים ישירים. התנגשות אפשרית רק אם שני ערכי STORAGE_POSITION שונים מתלכדים — עם מחרוזות ייחודיות זה כמעט בלתי אפשרי. לפני הפריסה, אנו מריצים סקריפט מותאם שמשווה את כל ערכי STORAGE_POSITION בכל ה-facets לייחודיות. חפיפה היא שגיאה חוסמת.
Facets אופייניים ומרחבי האחסון שלהם
| Facet | מרחב אחסון | מטרה |
|---|---|---|
| TokenFacet | diamond.storage.token.v1 | לוגיקת ERC-20 ויתרות |
| GovernanceFacet | diamond.storage.gov.v1 | הצבעות והצעות |
| RewardsFacet | diamond.storage.rewards.v1 | Staking וחלוקה |
| AdminFacet | diamond.storage.admin.v1 | ניהול והשהיה |
מה זה diamondCut ואיך הוא מנהל שדרוגים?
ניהול ה-facets מתבצע דרך library LibToken { bytes32 constant STORAGE_POSITION = keccak256("diamond.storage.token.v1"); struct TokenStorage { uint256 totalSupply; mapping(address => uint256) balances; mapping(address => mapping(address => uint256)) allowances; } function tokenStorage() internal pure returns (TokenStorage storage ts) { bytes32 position = STORAGE_POSITION; assembly { ts.slot := position } } } — הפונקציה היחידה שמשנה את טבלת הניתוב של Diamond. זו נקודת הבקרה לשדרוגים.
struct FacetCut {
address facetAddress;
FacetCutAction action; // Add, Replace, Remove
bytes4[] functionSelectors;
}
function diamondCut(
FacetCut[] calldata _diamondCut,
address _init,
bytes calldata _calldata
) external;LibToken.tokenStorage() + diamondCut() — אופציונלי: כתובת חוזה ו-calldata שייקראו דרך struct FacetCut { address facetAddress; FacetCutAction action; // Add, Replace, Remove bytes4[] functionSelectors; } function diamondCut( FacetCut[] calldata _diamondCut, address _init, bytes calldata _calldata ) external; מיד לאחר שינוי ה-facets. משמש למיגרציית אחסון בעת החלפת facet (מקביל ל-_init של OpenZeppelin).
הזכות לקרוא ל-diamondCut חייבת להיות מוגנת. תבנית סטנדרטית: OwnershipFacet שולט בגישה, diamondCut זמין רק לבעלים. עבור פרוטוקולים המנוהלים על ידי DAO — ממשל דרך TimelockController + Governor, שקוראים ל-diamondCut לאחר הצבעה.
השוואה עם תבניות פרוקסי חלופיות
| תבנית | מגבלת גודל | יכולת שדרוג | מורכבות | תקורה בגז |
|---|---|---|---|---|
| Transparent Proxy (EIP-1967) | 24 KB על הלוגיקה | החלפה מלאה | נמוכה | ~2,000 גז |
| UUPS (EIP-1822) | 24 KB על הלוגיקה | החלפה מלאה | בינונית | ~1,500 גז |
| Beacon Proxy | 24 KB, beacon יחיד | החלפת קבוצה | בינונית | ~2,500 גז |
| Diamond (EIP-2535) | ללא הגבלה | החלפה חלקית | גבוהה | ~3,000 גז |
Diamond הוא לא תמיד הבחירה הנכונה. עבור חוזים מתחת ל-15 KB עם לוגיקה פשוטה, UUPS פשוט וזול יותר. Diamond מוצדק כאשר: החוזה כבר קרוב למגבלת הגודל, צריך יכולת שדרוג גרעינית (עדכון רק מודול אחד בלי להחליף את כל החוזה), או שהלוגיקה מפותחת על ידי מספר צוותים באופן עצמאי.
כלים וביקורת עבור Diamond
Louper.dev — ממשק משתמש לבדיקת חוזי Diamond. מציג את כל ה-facets, ה-selectors של הפונקציות שלהם, וכתובות. כלי חיוני למבקרים ולמפתחים.
hardhat-diamond-abi — אוסף ABIs מכל ה-facets לקובץ אחד. נחוץ לפרונטאנד — הפרונטאנד רואה חוזה אחד, לא מספר facets.
ה-diamond-3 של Nick Mudge — יישום ייחוס ממחבר EIP-2535. אנו משתמשים בו כבסיס, לא כהעתק-הדבק — חשוב להבין כל שורה.
ביקורת חוזי Diamond דורשת מומחיות ספציפית: מבקרים בודקים את פריסת האחסון של כל ה-facets להתנגשויות, נכונות בקרת הגישה של diamondCut, היעדר התנגשויות selectors (שני facets עם אותו selector). ל-Slither יש תמיכה חלקית ב-Diamond, אבל בדיקה ידנית היא חובה.
התנגשות אחסון יכולה להתרחש לא רק כאשר STORAGE_POSITION תואם אלא גם בעת שימוש במשתני Solidity סטנדרטיים. השתמשו תמיד רק במרחבי אחסון בעלי שם. אנו גם מוודאים שאף facet לא משתמש במשתנים ברמת החוזה.
מה כלול
- ניתוח דרישות ועיצוב מבנה facets (2–3 ימים)
- פיתוח כל ה-facets באמצעות Diamond Storage Pattern
- בדיקות אינטגרציה ובדיקת התנגשות אחסון מלאה
- פריסה על Ethereum/Polygon/Arbitrum עם אימות ב-Etherscan
- הגדרת Louper.dev לניטור
- תיעוד על הארכיטקטורה ונוהל השדרוג
- הכשרת הצוות שלכם לעבודה עם Diamond
- תמיכה באחריות ל-30 יום לאחר הפריסה
איך לפתח חוזה Diamond: תוכנית שלב-אחר-שלב
- ניתוח דרישות ועיצוב מבנה facets (2–3 ימים). חלקו את הלוגיקה למודולים לוגיים: TokenFacet, GovernanceFacet, RewardsFacet, AdminFacet. עצבו מרחבי אחסון לכל אחד. זה השלב החשוב ביותר — עיצוב מחדש של פריסת האחסון לאחר פריסה הוא קטסטרופלי.
- פיתוח facets (1.5–2 שבועות). כל facet מפותח ונבדק בבידוד. בדיקות אינטגרציה רצות מול ה-Diamond המלא.
- בדיקת התנגשות אחסון. לפני הפריסה, אנו מריצים סקריפט מותאם שמשווה את כל ערכי STORAGE_POSITION בכל ה-facets לייחודיות. חפיפה היא שגיאה חוסמת.
- פריסה ואימות. Diamond נפרס ראשון, אחר כך כל facet בנפרד, ואז
_calldataמאתחל את הניתוב. כל facet מאומת ב-Etherscan. Louper.dev משמש לבדיקת תצורה סופית. - ניטור והכשרת צוות.
ציר זמן: 1–2 שבועות למערכת של 3–5 facets, עד חודש לפרוטוקול גדול עם 10+ facets וממשל מורכב. העלות מחושבת באופן אישי — צרו קשר להערכת פרויקט. מהנדסים מנוסים עם 5+ שנים ב-Web3 מבטיחים איכות ואבטחה. צריכים מומחיות בפיתוח חוזי Diamond? צרו קשר לביקורת מקדימה של הארכיטקטורה שלכם.







