برای دیپلوی قرارداد در آدرس قابل پیش بینی، از CREATE2 در سالیدیتی استفاده کنید. آدرس نهایی فقط به چهار عامل وابسته است: 0xff، آدرس دیپلوی کننده (فکتوری)، salt دلخواه شما و keccak256(init code). با ساخت init code درست (creationCode به اضافه آرگومان‌های سازنده)، محاسبه آدرس با فرمول EIP-1014، اطمینان از خالی بودن آدرس هدف و سپس فراخوانی new Contract{salt: …} می‌توانید قرارداد را دقیقا در همان آدرس از پیش محاسبه‌شده مستقر کنید.

CREATE2 دقیقا چه می کند و چه زمانی به کار می آید؟

CREATE2 یک دستور EVM است که آدرس قرارداد تازه را مستقل از nonce و فقط بر اساس ورودی‌های مشخص محاسبه می‌کند. فرمول آن چنین است:

address = keccak256( 0xff ++ deployer ++ salt ++ keccak256(init_code) )[12:]

در اینجا init_code همان creationCode به اضافه کدگذاری آرگومان‌های سازنده است. مزیت اصلی: می‌توانید «قبل از دیپلوی» آدرس دقیق قرارداد را بدانید، آن آدرس را به اشتراک بگذارید، حتی وجوه را به آن واریز کنید و سپس هر زمان خواستید همان قرارداد را دقیقا در همان آدرس بسازید (counterfactual deployment).

پیش نیازها و نکات امنیتی کلیدی

  • init code را درست بسازید: creationCode به علاوه abi.encode(پارامترهای سازنده). استفاده از runtimeCode اشتباه است.
  • salt باید bytes32 باشد. برای مشتق کردن از رشته یا عدد از keccak256 روی abi.encode/abi.encodePacked استفاده کنید و نوع کدگذاری را ثابت نگه دارید.
  • آدرس خروجی به آدرس فکتوری وابسته است. تغییر فکتوری یا تغییر نسخه/تنظیمات کامپایلر که creationCode را عوض کند، آدرس را تغییر می‌دهد.
  • اگر در آدرس پیش بینی شده «قبلا کد قرارداد» وجود داشته باشد، دیپلوی با خطا مواجه می‌شود. اما می‌توانید قبل از دیپلوی به این آدرس وجه واریز کنید.
  • تابع deploy فکتوری را محدود کنید یا salt را به هویتcaller گره بزنید تا شخص دیگری نتواند زودتر با همان پارامترها قرارداد را برای شما مستقر کند.
  • روی بازیافت آدرس با selfdestruct برای دیپلوی دوباره حساب باز نکنید. رفتار آن محدود شده و الگوی قابل اتکایی برای تولید مجدد آدرس نیست.

یک مثال عملی: فکتوری CREATE2 با آدرس قابل پیش بینی

در این مثال یک قرارداد ساده با مالک می سازیم و با فکتوری، آدرس دقیق آن را قبل از دیپلوی حساب می کنیم و سپس مستقرش می کنیم. کد صرفا آموزشی است.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// قرارداد مورد نظر برای استقرار
contract SimpleWallet {
    address public owner;

    event Received(address sender, uint256 amount);
    event Withdrawn(address to, uint256 amount);

    constructor(address _owner) payable {
        owner = _owner;
    }

    receive() external payable {
        emit Received(msg.sender, msg.value);
    }

    function withdraw(address payable to, uint256 amount) external {
        require(msg.sender == owner, "not owner");
        to.transfer(amount);
        emit Withdrawn(to, amount);
    }
}

// فکتوری CREATE2
contract Create2Factory {
    event Deployed(address addr, bytes32 salt);

    // creationCode + constructor args
    function _getInitCode(address owner) internal pure returns (bytes memory) {
        return abi.encodePacked(type(SimpleWallet).creationCode, abi.encode(owner));
    }

    // محاسبه آدرس قابل پیش بينی بر اساس EIP-1014
    function computeAddress(bytes32 salt, address owner) public view returns (address) {
        bytes32 initCodeHash = keccak256(_getInitCode(owner));
        bytes32 hash = keccak256(
            abi.encodePacked(bytes1(0xff), address(this), salt, initCodeHash)
        );
        return address(uint160(uint256(hash)));
    }

    // بررسی اینکه آیا کدی در این آدرس مستقر شده است یا نه
    function isDeployed(address predicted) public view returns (bool) {
        return predicted.code.length > 0;
    }

    // دیپلوی در آدرس از پیش محاسبه شده؛ می توانید msg.value هم بفرستید
    function deploy(bytes32 salt, address owner) external payable returns (address deployed) {
        address predicted = computeAddress(salt, owner);
        require(!isDeployed(predicted), "code already exists");

        // روش سطح بالا با CREATE2
        SimpleWallet wallet = new SimpleWallet{salt: salt, value: msg.value}(owner);
        deployed = address(wallet);

        // اطمینان از همسانی با آدرس پیش بينی شده
        require(deployed == predicted, "unexpected address");

        emit Deployed(deployed, salt);
    }
}

چطور از کد بالا استفاده کنیم؟

  1. فکتوری را دیپلوی کنید.
  2. یک salt بسازید. مثلا: bytes32 salt = keccak256(abi.encodePacked(“codity”, msg.sender, uint256(1)));
  3. با computeAddress(salt, owner) آدرس را به دست بیاورید.
  4. در صورت نیاز، قبل از دیپلوی به همان آدرس وجه ارسال کنید.
  5. تابع deploy را صدا بزنید تا قرارداد دقیقا در همان آدرس ساخته شود.

محاسبه آدرس در خارج از زنجیره (ethers.js)

اگر لازم است قبل از ارسال تراکنش، آدرس را در برنامه کلاینت محاسبه کنید، به init code و salt نیاز دارید. توجه: باید دقیقا همان creationCode و همان کدگذاری آرگومان‌ها را بسازید.

import { getCreate2Address, keccak256, toUtf8Bytes, AbiCoder } from "ethers";

// ورودی ها
const factory = "0xYourFactoryAddress";
const owner   = "0xOwnerAddress";

// init code = creationCode + abi.encode(owner)
const creationCode = "0x..."; // خروجی type(SimpleWallet).creationCode از بیلد
const abiCoder = new AbiCoder();
const encodedArgs = abiCoder.encode(["address"], [owner]); // معادل abi.encode
const initCode = creationCode + encodedArgs.slice(2);
const initCodeHash = keccak256(initCode);

// salt باید 32 بایت باشد
const salt = keccak256(toUtf8Bytes("codity-unique-salt"));

const predicted = getCreate2Address(factory, salt, initCodeHash);
console.log("Predicted address:", predicted);

تطبیق آدرس محاسبه شده در کلاینت با خروجی computeAddress روی زنجیره، بهترین راه اطمینان از صحت init code است.

آیا می توان قبل از دیپلوی، آدرس را شارژ کرد؟

بله. چون آدرس از قبل قابل محاسبه است، می توانید وجوه را به همان آدرس واریز کنید و پس از دیپلوی، قرارداد به موجودی دسترسی خواهد داشت. تنها شرط شکست خوردن CREATE2 وجود کد در آن آدرس است. برای جلوگیری از سواستفاده، یا دیپلوی را محدود کنید یا salt و پارامترها را طوری انتخاب کنید که فقط شما بتوانید قرارداد مطلوب را با همان فکتوری بسازید.

خطاهای رایج و روش پیشگیری

  • اشتباه در init code: استفاده از runtimeCode یا فراموش کردن آرگومان‌های سازنده باعث محاسبه آدرس غلط می شود. همیشه creationCode را با abi.encode(args) بچسبانید.
  • تفاوت abi.encode و abi.encodePacked: اگر خارج از زنجیره از packed استفاده کنید اما در زنجیره encode، آدرس‌ها یکی نمی شوند. یک روش را ثابت نگه دارید.
  • تغییرات بیلد: تغییر نسخه سالیدیتی، تنظیمات optimizer یا metadata، creationCode را عوض می کند. برای آدرس ثابت، فرآیند بیلد را قفل کنید.
  • salt تکراری در همان فکتوری: برای همان init code و همان salt، آدرس ثابت است و فقط یک بار قابل دیپلوی است. برای دیپلوی مجدد، salt یا init code را عوض کنید.
  • اتکا به selfdestruct برای آزادسازی آدرس: بازاستقرار در همان آدرس قابل اتکا نیست. برنامه ریزی آدرس‌های بعدی را روی salt های متفاوت انجام دهید.
  • نبستن مجوزها در فکتوری: اگر هرکس بتواند deploy را صدا بزند، ممکن است پیش دستی کند. از require(msg.sender == owner) یا محاسبه salt مبتنی بر msg.sender استفاده کنید.

ارسال مقدار هنگام دیپلوی و تایید نتیجه

می توانید در فراخوانی new Contract{salt: s, value: v} مقدار اتر ارسال کنید. برای تایید نتیجه:

  • پیش از دیپلوی، computeAddress را در فکتوری یا کلاینت محاسبه کنید.
  • پس از دیپلوی، برابر بودن آدرس بازگشتی با predicted را بررسی کنید.
  • روی زنجیره، با predicted.code.length > 0 استقرار موفق را بسنجید.

بهینه سازی الگو و مدیریت نسخه ها

برای نسخه بندی و جلوگیری از تصادم، توصیه می شود salt را از ترکیب نام نسخه، chainid، و شناسه منطقی کسب و کار تولید کنید. مثلا:

bytes32 salt = keccak256(abi.encode(
    "simple-wallet",
    block.chainid,
    msg.sender,
    uint256(1) // نسخه
));

اگر به الگوی ارتقاپذیری نیاز دارید، معمولا CREATE2 را برای دیپلوی پروکسی یا رجیستری آدرس‌ها به کار می برند تا نشانی سرویس پایدار بماند. برای تولید init code پروکسی‌های استاندارد می توانید از کتابخانه های شناخته شده مانند Create2 در OpenZeppelin استفاده کنید.

چک لیست کوتاه اجرا

  • تعریف قرارداد هدف و مشخص کردن آرگومان‌های سازنده.
  • ساخت init code = creationCode + abi.encode(args).
  • تولید salt پایدار و بدون تصادم.
  • محاسبه آدرس با فرمول EIP-1014 در زنجیره و خارج از زنجیره.
  • در صورت نیاز، تامین موجودی آدرس پیش بینی شده.
  • دیپلوی با new Contract{salt: …, value: …}(args) و بررسی code.length.
  • قفل کردن فرایند بیلد تا آدرس‌ها پایدار بمانند.

گام بعدی

اگر با مفاهیم پایه سالیدیتی آشنا هستید، یک فکتوری مینیمال بسازید، آدرس را خارج از زنجیره محاسبه کنید و یک دیپلوی آزمایشی انجام دهید. برای تسلط بیشتر بر طراحی فکتوری‌ها، مدیریت salt و الگوهای پروکسی، شرکت در دوره آموزش سالیدیتی مسیر یادگیری شما را یکپارچه می کند.