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

درک مفهوم اسکرو و طراحی قرارداد

اسکرو در معاملات آنلاین یعنی نگهداری مبلغ نزد یک واسطه بی طرف تا زمانی که شرایط توافق شده انجام شود. در نسخه غیرمتمرکز، قرارداد هوشمند این نقش را بازی می کند و قوانین را به صورت خودکار اجرا می کند. هدف ما کاهش ریسک کلاهبرداری و اختلاف است.

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

نقش ها و مسئولیت ها

  • خریدار: واریز کننده وجه و تایید کننده آزادسازی.
  • فروشنده: ارائه دهنده کالا یا خدمت و دریافت کننده وجه.
  • داور: مرجعی برای حل اختلاف در صورت بروز مشکل بین طرفین.

جریان کلی تراکنش

ابتدا قرارداد با مشخصات طرفین و مهلت انجام کار ایجاد می شود. سپس خریدار مبلغ را واریز می کند. اگر همه چیز درست پیش برود، خریدار وجه را برای فروشنده آزاد می کند. در صورت عدم انجام کار یا گذشت مهلت، امکان بازگشت وجه به خریدار وجود دارد. اگر اختلاف ایجاد شود، داور تصمیم می گیرد وجه به کدام طرف برسد.

حالات اصلی و رویدادها

  • حالات پیشنهادی: در انتظار پرداخت، تامین مالی شد، آزاد شد، بازپرداخت شد، اختلاف، حل اختلاف.
  • رویدادها: برای هر تغییر مهم مانند تامین مالی، آزادسازی، بازپرداخت و حل اختلاف لاگ ثبت کنید تا پیگیری روی زنجیره ساده باشد.

آموزش سالیدیتی + 5 پروژه عملی

پیاده سازی در سالیدیتی گام به گام

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

  1. تعریف نقش ها و حالت ها: سه آدرس برای خریدار، فروشنده، داور و یک enum برای حالات تعریف کنید.
  2. قیود دسترسی: مادیفایرهایی مانند onlyBuyer و onlyArbiter بنویسید تا هر تابع فقط توسط نقش مجاز اجرا شود.
  3. پیشگیری از تکرارپذیری: یک قفل ساده nonReentrant برای توابع انتقال وجه اضافه کنید.
  4. تامین مالی: تابع fund فقط توسط خریدار و فقط در حالت در انتظار پرداخت قابل اجرا باشد.
  5. آزادسازی وجه: تابع releaseToSeller با تایید خریدار و در حالت تامین مالی شد اجرا شود و کل موجودی را برای فروشنده ارسال کند.
  6. بازپرداخت: تابع refundToBuyer با درخواست فروشنده یا بعد از پایان مهلت انجام کار، وجه را به خریدار بازگرداند.
  7. اختلاف و داوری: تابع openDispute برای اعلام اختلاف و توابع resolve برای تخصیص وجه توسط داور.
  8. رویدادها و نماها: برای هر عمل مهم رویداد منتشر کنید و توابع دیداری مانند getBalance اضافه کنید.
  9. اعتبارسنجی ورودی: از require برای جلوگیری از آدرس صفر، مبالغ نامعتبر و حالات نادرست استفاده کنید.

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

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

contract Escrow {
    address public buyer;
    address payable public seller;
    address public arbiter;
    uint256 public deadline;

    enum State { AwaitingPayment, Funded, Released, Refunded, Disputed, Resolved }
    State public state;

    bool private locked; // برای جلوگیری از حمله reentrancy

    event Funded(address indexed from, uint256 value);
    event Released(address indexed to, uint256 value);
    event Refunded(address indexed to, uint256 value);
    event Disputed(address indexed by);
    event Resolved(address indexed to, uint256 value);

    modifier onlyBuyer() {
        require(msg.sender == buyer, "only buyer");
        _;
    }

    modifier onlyArbiter() {
        require(msg.sender == arbiter, "only arbiter");
        _;
    }

    modifier inState(State s) {
        require(state == s, "invalid state");
        _;
    }

    modifier nonReentrant() {
        require(!locked, "reentrancy");
        locked = true;
        _;
        locked = false;
    }

    constructor(address payable _seller, address _arbiter, uint256 _deadline) {
        require(_seller != address(0) && _arbiter != address(0), "zero address");
        require(_deadline > block.timestamp, "bad deadline");
        buyer = msg.sender;
        seller = _seller;
        arbiter = _arbiter;
        deadline = _deadline;
        state = State.AwaitingPayment;
    }

    function fund() external payable onlyBuyer inState(State.AwaitingPayment) {
        require(msg.value > 0, "no value");
        state = State.Funded;
        emit Funded(msg.sender, msg.value);
    }

    function releaseToSeller() external onlyBuyer inState(State.Funded) nonReentrant {
        state = State.Released;
        uint256 value = address(this).balance;
        (bool ok, ) = seller.call{value: value}("");
        require(ok, "transfer failed");
        emit Released(seller, value);
    }

    function refundToBuyer() external inState(State.Funded) nonReentrant {
        require(msg.sender == seller || block.timestamp > deadline, "not allowed");
        state = State.Refunded;
        uint256 value = address(this).balance;
        (bool ok, ) = payable(buyer).call{value: value}("");
        require(ok, "refund failed");
        emit Refunded(buyer, value);
    }

    function openDispute() external inState(State.Funded) {
        require(msg.sender == buyer || msg.sender == seller, "not a party");
        state = State.Disputed;
        emit Disputed(msg.sender);
    }

    function resolveToSeller() external onlyArbiter inState(State.Disputed) nonReentrant {
        state = State.Resolved;
        uint256 value = address(this).balance;
        (bool ok, ) = seller.call{value: value}("");
        require(ok, "transfer failed");
        emit Resolved(seller, value);
    }

    function resolveToBuyer() external onlyArbiter inState(State.Disputed) nonReentrant {
        state = State.Resolved;
        uint256 value = address(this).balance;
        (bool ok, ) = payable(buyer).call{value: value}("");
        require(ok, "refund failed");
        emit Resolved(buyer, value);
    }

    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }
}

تست، استقرار و نکات امنیتی

پس از نوشتن کد نوبت آزمون سناریوهای موفق و شکست است. از ابزارهایی مانند Remix برای آزمایش سریع و از فریم ورک های تست مانند Hardhat یا Foundry برای پوشش دهی بهتر استفاده کنید.

سناریوهای پیشنهادی برای تست

  • تامین مالی موفق: خریدار fund را فراخوانی کند و حالت به Funded تغییر کند.
  • آزادسازی: خریدار releaseToSeller را فراخوانی کند و موجودی به فروشنده برسد.
  • بازپرداخت: فروشنده refundToBuyer را قبل از ددلاین یا هر طرف پس از ددلاین فراخوانی کند.
  • اختلاف: هر یک از طرفین openDispute بزنند و فقط داور قادر به resolve باشد.
  • ممانعت از حمله: در توابع انتقال وجه reentrancy رخ ندهد و دو بار پرداخت اتفاق نیفتد.

مراحل سریع استقرار با Remix

  1. وارد Remix شوید و فایل قرارداد را ایجاد و کامپایل کنید.
  2. در بخش Deploy، آدرس فروشنده، آدرس داور و ددلاین بر حسب ثانیه آینده را وارد و قرارداد را مستقر کنید.
  3. با حساب خریدار، تابع fund را با مقدار دلخواه اجرا کنید.
  4. پس از انجام کار، releaseToSeller را صدا بزنید یا در صورت نیاز، سناریوهای اختلاف و بازپرداخت را بررسی کنید.

نکات امنیتی کلیدی

  • پیشگیری از reentrancy: از call با الگوی nonReentrant استفاده کنید و قبل از انتقال وجه حالت را تغییر دهید.
  • بررسی حالت ها: با مادیفایر inState جلوی اجرای توابع در زمان نامناسب را بگیرید.
  • مهلت زمانی شفاف: deadline را منطقی تعیین کنید و واحد زمان را بر اساس block.timestamp مدیریت کنید.
  • عدم اعتماد به آدرس ها: از آدرس صفر جلوگیری کنید و نقش ها را در سازنده ثبت کنید.
  • رویدادها: برای هر تغییر مالی رویداد منتشر کنید تا ابزارهای مانیتورینگ بتوانند ردیابی کنند.
  • سقف موجودی و خطاها: فرض نکنید مقدار ثابت است. همیشه از address(this).balance استفاده کنید و خطاها را مدیریت کنید.

گزینه های تصمیم گیری برای آزادسازی وجه

با توجه به نوع کسب و کار می توانید یکی از الگوهای زیر را برای تصمیم گیری انتخاب کنید.

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

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

توسعه های پیشنهادی

  • پشتیبانی از توکن ERC20: به جای Ether از توکن استفاده کنید و توابع انتقال را مطابق استاندارد پیاده سازی کنید.
  • آزادسازی مرحله ای: امکان پرداخت چند مرحله ای برای پروژه های بلندمدت.
  • کارمزد پلتفرم: درصدی از تراکنش به آدرس مالک برای تامین هزینه های سرویس.
  • توقف اضطراری: افزودن امکان pause برای مواقع بحرانی.
  • ارتقاپذیری: اگر نیاز به تغییرات آینده دارید، از الگوهای پروکسی استفاده کنید اما پیچیدگی و ریسک آن را بسنجید.

پیش از استقرار قرارداد escrow با سالیدیتی در شبکه اصلی، حتما تست های واحد و ادغام، بازبینی دستی و در صورت امکان ممیزی مستقل انجام دهید.

جمع بندی

اسکرو غیرمتمرکز ابزاری عملی برای کاهش ریسک در معاملات آنلاین است. با تعریف روشن نقش ها، حالات و قیود، می توانید یک پیاده سازی شفاف و امن داشته باشید. کد نمونه ارائه شده نقطه شروع خوبی است و با افزودن ویژگی هایی مانند آزادسازی مرحله ای، پشتیبانی از توکن و سیاست های حل اختلاف، آن را با نیازهای واقعی تطبیق می دهید. مهم ترین اصل، پایبندی به سادگی، تست کافی و رعایت نکات امنیتی است تا قرارداد escrow با سالیدیتی در عمل قابل اتکا باشد.