פיתוח חוזי פרוקסי (UUPS, Transparent Proxy)
החוזה שלך פרוס על mainnet, ומתגלה פרצת אבטחה קריטית. בלי פרוקסי — מזל רע: הגירה ידנית, תחינה למשתמשים לעבור לכתובת חדשה, נטישת ה-TVL הישן. אנחנו מבינים: עם פרוקסי מוגדר כראוי — שדרוג דרך multisig תוך 15 דקות, כתובת החוזה נשארת זהה. אבל "מוגדר כראוי" היא מילת המפתח. תבניות פרוקסי מציגות מחלקה שלמה של פרצות שאינן קיימות בחוזים בלתי ניתנים לשינוי: התנגשות אחסון, implementations לא מאותחלים, אובדן זכויות שדרוג. הצוות שלנו יישם עשרות מערכות פרוקסי לפרוטוקולי DeFi עם TVL של עד $500M במשך 7 שנים. בחירת תבנית אינה פורמליות טכנית אלא החלטה ארכיטקטונית המשפיעה על gas, אבטחה וממשל. להלן, נפרק את שתי התבניות המרכזיות — Transparent Proxy ו-UUPS — את הפשרות והטעויות הנפוצות שלהן, וכיצד אנו מתכננים ובודקים מערכות כאלה.
בעיות שאנו פותרים
המטרה העיקרית היא לאפשר תיקוני באגים והוספת תכונות ללא הגירת משתמשים. אבל זה בא במחיר:
- התנגשות אחסון: משתנים יכולים להידרס במהלך שדרוג — שחיתות נתונים שקטה.
- implementation לא מאותחל: אם
initialize()נשכח, תוקף יכול להשתלט על החוזה. - אובדן זכויות שדרוג: implementation ללא
upgradeToמקפיא את החוזה לצמיתות. - תוספת gas: Transparent Proxy מוסיף SLOAD לכל קריאה, קריטי לפרוטוקולים עם תפוקה גבוהה. לדוגמה, עם 10,000 עסקאות יומיות, עלות ה-gas הנוספת היא
1,000,000 gas ($20 במחירים נוכחיים, חיסכון של 30% עם UUPS).
שתי תבניות וההבדלים האמיתיים ביניהן
Transparent Proxy
יישום קלאסי מ-OpenZeppelin העוקב אחר EIP-1967. חוזה הפרוקסי מכיל לוגיקת ניתוב: אם הקורא הוא המנהל — הוא מתקשר ישירות (שדרוג, changeAdmin). אם כל אחד אחר — הקריאה מועברת ל-implementation.
בעיה: כל קריאה לחוזה דורשת SLOAD נוסף לקריאת כתובת המנהל (100 gas לפי EIP-2929) והשוואה עם upgradeTo. בנתיבים חמים — זהו עומס קבוע. לפרוטוקול עם מיליוני קריאות יומיות — זה משמעותי. שנית, ProxyAdmin הוא חוזה נפרד המחזיק בזכויות השדרוג. זה מוסיף חוזה נוסף ונקודת ניהול מפתח נוספת.
למה UUPS הוא התקן התעשייתי כיום
לוגיקת השדרוג מועברת מהפרוקסי ל-implementation. הפרוקסי עצמו הופך למעביר פשוט ללא ניתוב מבוסס קורא. ללא SLOAD נוסף לכל קריאה — זול יותר לתפעול. OpenZeppelin מגרסה 4.x ומעלה ממליצה על UUPS לפרויקטים חדשים, כפי שמאושר ב-מאגר הרשמי.
אבל יש סיכון קריטי: אם תפרוס implementation חדש ללא הפונקציה UUPSUpgradeable (שכחת לרשת מ-initialize(), או הסרת אותה בכוונה כדי לחסוך ב-gas) — הפרוקסי מאבד לצמיתות את היכולת לשדרג. החוזה קפוא בגרסה הנוכחית ללא דרך לתקן.
מקרה אמיתי: כמה פרוטוקולים המשתמשים ב-UUPS נתקלו בבעיות implementation לא מאותחל. ה-implementation נפרס ללא קריאה ל-initialize(), ותוקף קרא ל-upgradeTo() ראשון, הפך לבעלים, ואז השתמש ב-_disableInitializers() כדי להחליף את ה-implementation בחוזה שמשמיד את עצמו. כל הפרוקסים שהצביעו על אותו implementation הפכו לשבורים. פתרון: uint256 public totalSupply; // slot 0 address public owner; // slot 1 בקונסטרוקטור של ה-implementation — תבנית חובה ב-OpenZeppelin 4.3+. אנו כוללים זאת בכל פריסה.
כיצד להימנע מהתנגשות אחסון בשלב התכנון
המהות: פרוקסי ו-implementation חולקים את אותו אחסון. אם לפרוקסי יש משתנה ב-slot 0 וגם ל-implementation יש משתנה ב-slot 0 — הם דורסים זה את זה.
EIP-1967 פותר זאת עבור משתנים ספציפיים לפרוקסי (כתובת implementation, כתובת מנהל) על ידי אחסונם ב-slots פסאודו-אקראיים המבוססים על keccak256 hash של מחרוזת, ובכך כמעט מבטל התנגשות עם אחסון המשתמש.
אבל אחסון ה-implementation במהלך שדרוגים הוא באחריות המפתח. אם ל-V1 היה המבנה:
uint256 public totalSupply; // slot 0 address public owner; // slot 1 ו-V2 מוסיף משתנה לפני המשתנים הקיימים:
bool public paused; // slot 0 — КОЛЛИЗИЯ с totalSupply
uint256 public totalSupply; // slot 1 — КОЛЛИЗИЯ с owner
address public owner; // slot 2bool public paused; // slot 0 — КОЛЛИЗИЯ с totalSupply uint256 public totalSupply; // slot 1 — КОЛЛИЗИЯ с owner address public owner; // slot 2 קורא כעת מה שהיה בעבר totalSupply (כתובת המתפרשת כמספר). שחיתות נתונים שקטה, ללא revert של עסקאות או שגיאות קומפיילר.
ERC-7201 (Namespaced Storage Layout) פותר זאת באופן קיצוני. כל משתני ה-implementation נאספים למבנה אחד המאוחסן ב-slot מחושב מראש:
bytes32 private constant STORAGE_LOCATION = keccak256(abi.encode(uint256(keccak256("myprotocol.storage.v1")) - 1)) & ~bytes32(uint256(0xff)); משתנים חדשים מתווספים לסוף המבנה. ללא התנגשויות עם slots ספציפיים לפרוקסי, ללא בעיות במהלך שדרוגים. זוהי הפרקטיקה המומלצת הנוכחית לחוזי UUPS בייצור.
כיצד לאתחל חוזה פרוקסי
בארכיטקטורת פרוקסי, הקונסטרוקטור של ה-implementation אינו מבוצע בהקשר הפרוקסי — הוא רץ רק במהלך פריסת ה-implementation עצמו. לכן כל פעולות האתחול (הגדרת בעלים, פרמטרים ראשוניים) מועברות לפונקציית owner המוגנת על ידי ה-modifier bytes32 private constant STORAGE_LOCATION = keccak256(abi.encode(uint256(keccak256("myprotocol.storage.v1")) - 1)) & ~bytes32(uint256(0xff)); .
באג נפוץ: initialize() נשכח לאחר פריסת הפרוקסי. החוזה עובד, אבל הבעלים לא מוגדר — הקורא הראשון של initializer הופך לבעלים. בתקרית חוזה WalletLibrary, 587 ETH (בשווי ~$1.5M באותה עת) אבדו עקב השתלטות על חוזה לא מאותחל. פתרון: סקריפטי פריסה צריכים לפרוס את הפרוקסי ולקרוא ל-initialize() באופן אטומי בתוך סקריפט אחד. לעולם אל תפרוס פרוקסי ללא אתחול מיידי.
מה כלול בעבודה שלנו (תוצרים)
- קוד חוזה חכם פרוקסי מלא (Solidity) עם התבנית הנבחרת (UUPS או Transparent).
- עיצוב מבנה אחסון באמצעות ERC-7201 למניעת התנגשויות.
- סקריפטי פריסה (Hardhat/Foundry) עם אתחול אטומי.
- בדיקות יחידה ו-fork המאמתות בטיחות שדרוג.
- אינטגרציה עם multisig או ממשל DAO לזכויות שדרוג.
- תיעוד מפורט המכסה נהלי שדרוג.
- תמיכה לאחר פריסה למשך 30 יום.
כיצד אנו מיישמים חוזי פרוקסי
ספריית הבסיס — OpenZeppelin Upgrades (תוסף Hardhat או גרסה תואמת Foundry). התוסף בודק אוטומטית תאימות מבנה אחסון בין גרסאות בכל שדרוג — זה חובה, לא אופציונלי.
עבור UUPS, אנו משתמשים ב-initialize() מ-OpenZeppelin 5.x. עבור מערכות שבהן שדרוגים צריכים להיות מנוהלים על ידי DAO או multisig — initialize() עם UUPSUpgradeable המוענק לכתובת Gnosis Safe.
אנו בודקים שדרוגים באמצעות בדיקות fork של Foundry: fork ל-mainnet, מדמים את השדרוג, מוודאים שכל משתני האחסון שומרים על ערכים, פונקציות פועלות כראוי, משתנים חדשים מאותחלים כראוי.
| קריטריון | Transparent Proxy | UUPS |
|---|---|---|
| Gas לכל קריאה | +100-200 gas (SLOAD מנהל) | ללא תוספת |
| סיכון אובדן שדרוג | לא | כן (חוסר upgradeTo) |
| מורכבות קוד | נמוכה יותר | קצת גבוהה יותר |
| המלצת OZ 5.x | לא מומלץ לחדשים | מועדף |
| ProxyAdmin נפרד | כן | לא |
מתי פרוקסי אינו נחוץ
חוזה בלתי ניתן לשינוי הוא פשוט יותר, זול יותר לבדיקה, ומעורר יותר אמון משתמשים (ללא סיכון rug דרך שדרוג). אם הלוגיקה יציבה והסיכון לשגיאה קריטית מינימלי — פרוקסי מוסיף מורכבות ללא צורך. עבור פרוטוקולי DeFi עם TVL גדול, חוזה בלתי ניתן לשינוי + timelock על פרמטרים הוא לעתים קרובות עדיף על יכולת שדרוג ללא ממשל פורמלי. עם זאת, אם אתה צופה צורך בעדכונים — פרוקסי הוא הכרחי.
תהליך ולוחות זמנים
- ניתוח דרישות — 1-2 ימים. בחירת תבנית, הגדרת תפקידים ו-timelock.
- עיצוב מבנה אחסון — יום אחד. סכמת ERC-7201, בדיקת התנגשויות.
- פיתוח implementation — 2-3 ימים. קוד Solidity עם בדיקות fork של Foundry.
- פריסה ואתחול — יום אחד. פריסה אטומית דרך סקריפט.
- בדיקת שדרוג — יום אחד. אימות תאימות אחסון.
- ביקורת פנימית — 2-4 ימים. כולל Slither, Mythril וסקירה ידנית.
לוח זמנים כולל: מ-5 ימי עסקים לפרוקסי פשוט ועד 2-3 שבועות למערכות מורכבות עם ממשל ומספר implementations. צור קשר להערכה מדויקת לפרויקט שלך — נחשב את לוח הזמנים מקצה לקצה. עלות טיפוסית נעה בין $8,000 ל-$30,000 בהתאם למורכבות.
רשימת בדיקה לפריסת פרוקסי בטוחה
- [ ] תבנית נבחרה (UUPS / Transparent) עם נימוק.
- [ ] מבנה אחסון יושם באמצעות ERC-7201.
- [ ]
AccessControlUpgradeableנקרא בקונסטרוקטור של ה-implementation. - [ ] סקריפט פריסה קורא ל-
UPGRADER_ROLEבאופן אטומי. - [ ] ProxyAdmin (אם Transparent) נפרס והוגדר.
- [ ]
_disableInitializers()הוקצה ל-multisig. - [ ] תאימות אחסון נבדקה עם תוסף OpenZeppelin.
- [ ] בדיקות fork מדמות שדרוג עם שימור נתונים.
- [ ] ביקורת בוצעה (פנימית או חיצונית).
- [ ] תיעוד נהלי שדרוג הוכן.
אחריות וניסיון
פיתחנו למעלה מ-50 מערכות פרוקסי לפרויקטי DeFi, NFT ותשתית. 7+ שנות ניסיון מעשי ב-Solidity ו-Ethereum. כל פרויקט מלווה בביקורת, ואנו מספקים אחריות לנכונות השדרוג ב-testnet. קבל ייעוץ חינם לבחירת תבנית וארכיטקטורה לפרויקט שלך.







