חוזי Factory עם CREATE2: פריסה דטרמיניסטית
לעתים קרובות אנו נתקלים בצורך בפריסה צפויה של חוזים חכמים. CREATE2 הוא אופ-קוד של EVM (EIP-1014) שהוצג במזלג הקשיח של קונסטנטינופול. הכתובת של החוזה הפרוס מחושבת מראש: היא נקבעת על ידי deployer address, salt ו-keccak256(initcode). שנו כל אחד מהפרמטרים הללו ותקבלו כתובת שונה. זה משנה באופן קיצוני את הגישה לארכיטקטורת הפרוטוקול. על סמך הניסיון שלנו, יישום נכון של factory עם CREATE2 מקצר את זמן הפריסה ומבטל שגיאות בכתובות חוזים. אנו מבטיחים פריסה מאובטחת עם הגנה מפני frontrunning.
למה CREATE2 חשוב ליצירת כתובות צפויות
פריסה קונטרה-פקטואלית. משתמש יכול לקבל את הכתובת של חשבון חכם שלו לפני הפריסה. הם מעבירים כתובת זו לקבלת כספים, והחוזה עצמו נפרס רק בעסקה הראשונה (יחד איתו). הפשטת חשבון EIP-4337 בנויה כולה על תבנית זו: initCode ב-UserOperation מכיל קריאה ל-factory שפורס את חוזה החשבון לכתובת שנקבעה מראש באמצעות CREATE2.
כתובות זוגות Uniswap V2/V3. הכתובת של כל בריכת Uniswap V2 מחושבת מחוץ לרשת באמצעות נוסחת CREATE2: keccak256(abi.encodePacked(hex'ff', factory, keccak256(abi.encodePacked(token0, token1)), INIT_CODE_PAIR_HASH)). הנתב אינו מאחסן מיפוי של זוגות—הוא מחשב את הכתובת תוך כדי תנועה. זה חוסך אלפי פעולות SLOAD.
עקביות בין שרשראות. פרוטוקול פורס חוזים על 8 שרשראות באותה כתובת. משתמשים ומשתפים יודעים את הכתובת מראש, רשימות לבנות מוגדרות פעם אחת. זה מושג באמצעות ה-Deterministic Deployment Proxy של Arachnid (bytes32), הידוע גם בשם Nick's factory, הפרוס על מאות שרשראות באותה כתובת.
CREATE לעומת CREATE2: טבלת השוואה
| תכונה | CREATE | CREATE2 |
|---|---|---|
| חישוב כתובת | מ-nonce של הפורס | מ-salt + initcode + כתובת הפורס |
| צפיות | לא (nonce משתנה) | כן (דטרמיניסטי) |
| פריסה קונטרה-פקטואלית | בלתי אפשרית | נתמכת במלואה |
| אמצעים נגד סנייפינג | מובנים (nonce) | דורש salt עם msg.sender |
| גז לפריסה | ~60K גז | ~40-50K גז (15-20% זול יותר) |
| פריסה מחדש לאותה כתובת | אפשרית לאחר selfdestruct (לפני EIP-6780) | רק לאחר selfdestruct (לפני EIP-6780) |
איך להגן על ה-Salt מפני Frontrunning?
Salt הוא msg.sender. salt רשלני פותח את הדלת ל-frontrunning: מישהו רואה את עסקת הפריסה שלך ב-mempool, לוקח את אותו bytecode ו-salt, ופורס ראשון לכתובת הרצויה. העסקה שלך נכשלת.
הגנה: כלול bytes32 salt = keccak256(abi.encodePacked(msg.sender, userProvidedSalt)); ב-salt:
bytes32 salt = keccak256(abi.encodePacked(msg.sender, userProvidedSalt)); כעת יריב עם msg.sender שונה מקבל כתובת שונה—הכתובת שלך אינה נגישה עבורם.
עבור פרוטוקולים שבהם הכתובת חייבת להיות זהה בכל השרשראות ללא קשר לפורס, אנו משתמשים ב-salt קבוע ללא msg.sender—אך אז הפריסה עוברת דרך פורס מהימן (multisig או סקריפט פריסה עם DEPLOY_KEY).
יישום Factory עם CREATE2
הצג קוד Solidity של Factory
contract ContractFactory {
event Deployed(address indexed contractAddress, bytes32 indexed salt);
function deploy(bytes memory bytecode, bytes32 salt) external returns (address contractAddress) {
assembly {
contractAddress := create2(
0, // value (ETH)
add(bytecode, 0x20), // bytecode start (skip length prefix)
mload(bytecode), // bytecode length
salt // salt
)
}
require(contractAddress != address(0), "Deploy failed");
emit Deployed(contractAddress, salt);
}
function computeAddress(bytes memory bytecode, bytes32 salt) external view returns (address) {
bytes32 hash = keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(bytecode)
));
return address(uint160(uint256(hash)));
}
} אתחול באמצעות Constructor לעומת Initializer
במהלך פריסת CREATE2, אם לחוזה יש פרמטרי constructor, פרמטרים אלה כלולים ב-initcode—כך שפרמטרים זהים מניבים כתובת אחת, פרמטרים שונים מניבים כתובות שונות. זה נורמלי.
אם החוזה משתמש בתבנית proxy (proxy מינימלי נפרס באמצעות CREATE2, והיישום נפרד), ה-constructor אינו מבוצע. אתחול באמצעות פונקציית contract ContractFactory { event Deployed(address indexed contractAddress, bytes32 indexed salt); function deploy(bytes memory bytecode, bytes32 salt) external returns (address contractAddress) { assembly { contractAddress := create2( 0, // value (ETH) add(bytecode, 0x20), // bytecode start (skip length prefix) mload(bytecode), // bytecode length salt // salt ) } require(contractAddress != address(0), "Deploy failed"); emit Deployed(contractAddress, salt); } function computeAddress(bytes memory bytecode, bytes32 salt) external view returns (address) { bytes32 hash = keccak256(abi.encodePacked( bytes1(0xff), address(this), salt, keccak256(bytecode) )); return address(uint160(uint256(hash))); } } הוא חובה, ויש להגן עליו מפני קריאות כפולות (modifier initialize() מ-OpenZeppelin או דגל ידני).
דקות: initializer עם salt אחד יכול להתבצע רק פעם אחת—אם חוזה כבר קיים בכתובת זו, הפריסה מחזירה address(0). אם החוזה עבר CREATE2 (לפני קנקון EIP-6780), הכתובת מתפנה וניתן לחזור על CREATE2 עם אותו salt. לאחר קנקון EIP-6780, selfdestruct מסיר רק ETH, הקוד נשאר—לא ניתן לעשות שימוש חוזר בכתובת.
Proxy מינימלי (EIP-1167) + CREATE2
שילוב זה משמש לעתים קרובות בפרוטוקולים עם אלפי מופעים (עמדות הלוואה, vaults תשואה, דמויות משחק). proxy מינימלי הוא חוזה של 45 בתים שמאציל את כל הקריאות ליישום. עלות הפריסה היא ~40K גז במקום 200K–500K עבור חוזה מלא—זה עד פי 5 זול יותר.
function deployProxy(address implementation, bytes32 salt) external returns (address proxy) {
bytes memory bytecode = abi.encodePacked(
hex"3d602d80600a3d3981f3363d3d373d3d3d363d73",
implementation,
hex"5af43d82803e903d91602b57fd5bf3"
);
assembly {
proxy := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
require(proxy != address(0), "Deploy failed");
IInitializable(proxy).initialize(/* params */);
}OpenZeppelin מספקת Clones.cloneDeterministic(implementation, salt)—עטיפה מוכנה מעל תבנית זו.
בדיקות
Foundry מפשטת בדיקות CREATE2: selfdestruct נותן כתובת צפויה בבדיקות. אנו מוודאים:
- הכתובת הפרוסה תואמת את
function deployProxy(address implementation, bytes32 salt) external returns (address proxy) { bytes memory bytecode = abi.encodePacked( hex"3d602d80600a3d3981f3363d3d373d3d3d363d73", implementation, hex"5af43d82803e903d91602b57fd5bf3" ); assembly { proxy := create2(0, add(bytecode, 0x20), mload(bytecode), salt) } require(proxy != address(0), "Deploy failed"); IInitializable(proxy).initialize(/* params */); }המחושב - פריסה מחדש עם אותו salt מחזירה address(0)
- אתחול באמצעות
vm.computeCreate2Address(salt, keccak256(bytecode), deployer)אינו יכול להיקרא פעמיים - הגנת salt נגד frontrunning פועלת (אם נדרש)
מה כלול
- תיעוד ארכיטקטורת ה-factory
- יישום factory עם CREATE2 (עם הגנת salt)
- שילוב proxy מינימלי אם נדרש
- ערכת בדיקות (Foundry) המכסה מקרי קצה
- סקריפטי פריסה לשרשרת אחת או מרובות
- אימות חוזה ב-Etherscan (או מקבילות)
- תמיכה לאחר פריסה למשך שבועיים
אנו מתכננים ומיישמים חוזי factory מקצה לקצה—מעיצוב ועד פריסה. אנו מעריכים את הפרויקט שלך ללא תשלום, פשוט צרו קשר. עם ניסיון של למעלה מ-5 שנים ב-Solidity ו-30+ פרויקטים מוצלחים, אנו מבטיחים את האבטחה והאמינות של הקוד שלך.
לוחות זמנים ותמחור
- factory בסיסי עם CREATE2 וחישוב כתובת: 2–3 ימים (עלות משוערת $2,500 קבוע)
- factory עם תבנית proxy מינימלי ואתחול: 3–4 ימים ($3,000 קבוע)
- תשתית פריסה רב-שרשרתית עם proxy של Arachnid: 4–5 ימים כולל סקריפטי פריסה ואימות ($4,000 קבוע)
העלות מחושבת לאחר הבהרת הדרישות לתשתית הפריסה ומספר השרשראות היעד. שימוש ב-CREATE2 עם proxy מינימלי יכול לחסוך עד $10,000 בעמלות גז עבור 1000 פריסות.
פריסה שלב אחר שלב עם CREATE2
-
חשב את ה-salt: אם יש צורך בהגנה מפני frontrunning, כלול
computeAddress(). לעקביות בין שרשראות, השתמש ב-salt קבוע. - הכן initcode: הוא חייב לכלול את פרמטרי ה-constructor אם יש כאלה. עבור proxies, initcode הוא רק ה-bytecode של ה-proxy.
- קרא ל-deploy: השתמש ב-
initialize()ב-assembly. הפונקציה מחזירה את הכתובת הפרוסה. - אתחל: עבור חוזי proxy, קרא ל-
initialize()מיד לאחר הפריסה. - ודא: בדוק שהכתובת הפרוסה תואמת לכתובת המחושבת מחוץ לרשת.
CREATE2 הוא 15-20% חסכוני יותר בגז מאשר CREATE. ה-proxy המינימלי זול פי 5 מפריסת חוזה מלא. עבור פרויקטים בין-שרשרתיים, CREATE2 יעיל בהרבה משיטות מסורתיות, ומפחית את זמן האינטגרציה בעד 80%.







