۱۴۰۵ مهر ۶, دوشنبه
دلار آمریکا۲۴۳٬۴۱۵▲ ۳٫۵۸٪یورو۲۷۷٬۰۰۰▲ ۳٫۳۹٪درهم امارات۶۶٬۳۰۵▲ ۳٫۶۱٪سکه امامی۲۴۴٬۵۰۵٬۰۰۰▲ ۱٫۶۶٪طلای ۱۸ عیار (گرم)۲۴٬۳۱۹٬۰۰۰▲ ۱٫۷۵٪انس طلا۴٬۱۵۰ $▼ ۳٫۲۶٪تتر۲۴۴٬۴۵۶▲ ۳٫۲۴٪بیت‌کوین۸۲٬۶۵۳ $▼ ۲٫۴۱٪اتریوم۲٬۶۴۰ $▼ ۱٫۸۹٪سولانا۱۱۷٫۸۴ $▼ ۴٫۳۴٪اپل۳۴۱٫۰۷ $▲ ۱٫۵۳٪انویدیا۲۲۵٫۰۷ $▲ ۰٫۲۲٪مایکروسافت۵۱۶٫۱۷ $▲ ۳٫۶۶٪آلفابت (گوگل)۳۴۳٫۹۲ $▲ ۰٫۴۶٪تسلا۳۷۲٫۱۱ $▼ ۱٫۵۴٪شاخص نزدک۲۷٬۰۶۹ $▲ ۰٫۴۸٪
نرخ ارز

آموزش Solidity: نوشتن اولین قرارداد هوشمند اتریوم قدم‌به‌قدم

آموزش Solidity از صفر: یک قرارداد هوشمند واقعی و کامپایل‌شدنی بنویسید، خط‌به‌خط بفهمید و با Remix یا Foundry روی تست‌نت اتریوم دیپلوی کنید.

کریپتو و بلاکچین۵ دقیقه مطالعه
آموزش Solidity: نوشتن اولین قرارداد هوشمند اتریوم قدم‌به‌قدم
فهرست مطالب
  1. پیش‌نیازها
  2. قرارداد TipJar چه می‌کند؟
  3. کد کامل قرارداد
  4. توضیح خط‌به‌خط
  5. آموزش Solidity در عمل: دیپلوی با Remix
  6. همین پروژه با Foundry
  7. اشتباه‌های رایج و نکات امنیتی
  8. قدم بعدی

آموزش Solidity معمولاً با یک «Hello World» خشک شروع می‌شود که چیز زیادی درباره دنیای واقعی قراردادهای هوشمند یاد نمی‌دهد. در این آموزش یک قرارداد کوچک اما واقعی می‌نویسیم: قلکی برای دریافت انعام به اتر که هر کسی می‌تواند به آن پول و پیام بفرستد و فقط مالکش می‌تواند برداشت کند. در مسیر، مهم‌ترین مفاهیم Solidity را خط‌به‌خط می‌بینیم و در نهایت قرارداد را روی تست‌نت دیپلوی می‌کنیم.

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

پیش‌نیازها

  • آشنایی مقدماتی با یک زبان برنامه‌نویسی.
  • مرورگر برای کار با Remix، یا ترمینال برای Foundry.
  • یک کیف پول نرم‌افزاری آزمایشی و مقداری اتر تست‌نت Sepolia از یک فاست (Faucet). هرگز از کیف پول اصلی‌تان برای آزمایش استفاده نکنید.

قرارداد TipJar چه می‌کند؟

قرارداد ما چهار قابلیت دارد:

  1. دریافت انعام همراه با یک پیام کوتاه (حداکثر ۱۴۰ بایت).
  2. دریافت مستقیم اتر بدون پیام.
  3. ثبت تاریخچه انعام‌ها و مجموع آن‌ها.
  4. برداشت کل موجودی، فقط توسط مالک.

کد کامل قرارداد

فایلی به نام TipJar.sol بسازید. این کد با کامپایلر Solidity نسخه 0.8.37 (آخرین نسخه در زمان نگارش) بدون خطا و هشدار کامپایل شده است:

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

/// @title TipJar - a minimal tip jar for ETH
/// @notice Anyone can tip; only the owner can withdraw.
contract TipJar {
    struct Tip {
        address from;
        uint256 amount;
        string message;
        uint256 timestamp;
    }

    uint256 public constant MAX_MESSAGE_LENGTH = 140;

    address public immutable owner;
    uint256 public totalTips;
    Tip[] private tips;

    event TipReceived(address indexed from, uint256 amount, string message);
    event Withdrawn(address indexed to, uint256 amount);

    error NotOwner();
    error ZeroAmount();
    error MessageTooLong(uint256 length);
    error TransferFailed();

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function tip(string calldata message) external payable {
        _recordTip(message);
    }

    receive() external payable {
        _recordTip("");
    }

    function withdraw() external onlyOwner {
        uint256 amount = address(this).balance;
        if (amount == 0) revert ZeroAmount();

        (bool ok, ) = payable(owner).call{value: amount}("");
        if (!ok) revert TransferFailed();

        emit Withdrawn(owner, amount);
    }

    function tipCount() external view returns (uint256) {
        return tips.length;
    }

    function getTip(uint256 index) external view returns (Tip memory) {
        return tips[index];
    }

    function _recordTip(string memory message) private {
        if (msg.value == 0) revert ZeroAmount();
        uint256 length = bytes(message).length;
        if (length > MAX_MESSAGE_LENGTH) revert MessageTooLong(length);

        tips.push(Tip({
            from: msg.sender,
            amount: msg.value,
            message: message,
            timestamp: block.timestamp
        }));
        totalTips += msg.value;

        emit TipReceived(msg.sender, msg.value, message);
    }
}

توضیح خط‌به‌خط

مجوز و نسخه کامپایلر

خط اول، شناسه مجوز SPDX است که کامپایلر انتظار دارد در هر فایل باشد. دستور pragma solidity ^0.8.24; می‌گوید این کد با هر نسخه از 0.8.24 تا پیش از 0.9.0 کامپایل می‌شود. از نسخه 0.8 به بعد، سرریز و زیرریز عددی (Overflow/Underflow) به‌صورت پیش‌فرض بررسی می‌شود و تراکنش را برمی‌گرداند؛ پس دیگر به کتابخانه‌هایی مثل SafeMath نیاز ندارید.

متغیرهای وضعیت

  • constant مقداری است که در زمان کامپایل ثابت است و در storage ذخیره نمی‌شود.
  • immutable فقط یک بار، در constructor، مقدار می‌گیرد و بعد از آن خواندنش ارزان است. آدرس مالک را این‌طور نگه می‌داریم.
  • public باعث می‌شود کامپایلر به‌طور خودکار یک تابع getter بسازد؛ مثلاً owner() و totalTips().
  • آرایه tips از نوع private است. توجه کنید: private فقط دسترسی قراردادهای دیگر را محدود می‌کند. همه داده‌های روی بلاکچین برای هر کسی قابل خواندن‌اند.

رویدادها (Events)

رویدادها در لاگ تراکنش ثبت می‌شوند و فرانت‌اند یا ایندکسرها می‌توانند به آن‌ها گوش دهند. کلمه indexed امکان فیلتر کردن لاگ‌ها بر اساس آدرس فرستنده را فراهم می‌کند. ثبت رویداد از ذخیره در storage بسیار ارزان‌تر است.

خطاهای سفارشی (Custom Errors)

به‌جای require(condition, "long message") از error و revert استفاده کرده‌ایم. خطای سفارشی حجم بایت‌کد و هزینه گس را کم می‌کند و می‌تواند داده هم حمل کند؛ مثل MessageTooLong(length) که طول پیام را برمی‌گرداند.

modifier و کنترل دسترسی

onlyOwner پیش از بدنه تابع اجرا می‌شود و علامت _ جای بدنه تابع را مشخص می‌کند. این ساده‌ترین شکل کنترل دسترسی است. در پروژه‌های واقعی معمولاً از قرارداد Ownable در کتابخانه OpenZeppelin استفاده می‌شود.

توابع payable و receive

فقط توابعی که payable دارند می‌توانند اتر دریافت کنند. مقدار ارسالی در msg.value و بر حسب wei است (هر اتر برابر ۱۰ به توان ۱۸ wei). تابع ویژه receive زمانی اجرا می‌شود که کسی بدون داده (calldata خالی) مستقیماً به قرارداد اتر بفرستد.

پارامتر tip از نوع calldata است که فقط‌خواندنی و برای ورودی توابع external ارزان‌تر است، اما _recordTip ورودی memory می‌گیرد تا بتوان رشته خالی را هم از receive به آن پاس داد.

برداشت امن: الگوی CEI

در withdraw ابتدا شرایط را بررسی می‌کنیم (Checks)، اگر وضعیتی باید تغییر کند پیش از فراخوانی خارجی تغییرش می‌دهیم (Effects) و در آخر اتر را می‌فرستیم (Interactions). این ترتیب، که به CEI معروف است، اصلی‌ترین دفاع در برابر حمله بازورود (Reentrancy) است.

برای ارسال اتر از call استفاده کرده‌ایم و نتیجه‌اش را بررسی می‌کنیم. توابع قدیمی transfer و send فقط ۲۳۰۰ واحد گس به گیرنده می‌دهند و ممکن است با کیف پول‌های قراردادی (مثل Safe) شکست بخورند.

آموزش Solidity در عمل: دیپلوی با Remix

Remix یک IDE مرورگری است و ساده‌ترین راه برای شروع:

  1. به remix.ethereum.org بروید، فایل TipJar.sol را بسازید و کد را در آن بچسبانید.
  2. در تب Solidity Compiler نسخه کامپایلر را روی 0.8.24 یا بالاتر بگذارید و Compile را بزنید.
  3. در تب Deploy & Run Transactions، یکی از محیط‌های Remix VM را انتخاب کنید. این یک بلاکچین شبیه‌سازی‌شده با حساب‌های پراتر آزمایشی است.
  4. Deploy را بزنید. حالا در فیلد Value مثلاً 1000000 wei وارد کنید، در ورودی tip پیامی بنویسید و فراخوانی کنید.
  5. tipCount و getTip(0) را صدا بزنید و نتیجه را ببینید. بعد با حساب دیگری withdraw را امتحان کنید تا خطای NotOwner را ببینید.
  6. برای تست‌نت واقعی، محیط را روی گزینه اتصال به کیف پول مرورگر (Injected Provider) بگذارید و کیف پول را روی شبکه Sepolia قرار دهید.

همین پروژه با Foundry

Foundry مجموعه‌ای از ابزارهای خط فرمان برای توسعه حرفه‌ای Solidity است. پس از نصب (طبق راهنمای getfoundry.sh):

forge init tipjar
cd tipjar
# put TipJar.sol in src/, then:
forge build

برای دیپلوی روی Sepolia، به‌جای گذاشتن کلید خصوصی در دستور یا فایل، آن را در keystore رمزگذاری‌شده Foundry وارد کنید:

cast wallet import deployer --interactive

forge create src/TipJar.sol:TipJar \
  --rpc-url "$SEPOLIA_RPC_URL" \
  --account deployer \
  --broadcast

سپس با cast با قرارداد تعامل کنید (به‌جای <CONTRACT_ADDRESS> آدرس قرارداد دیپلوی‌شده را بگذارید):

cast send <CONTRACT_ADDRESS> "tip(string)" "Hello from DevNA" \
  --value 0.001ether --rpc-url "$SEPOLIA_RPC_URL" --account deployer

cast call <CONTRACT_ADDRESS> "tipCount()(uint256)" --rpc-url "$SEPOLIA_RPC_URL"

اشتباه‌های رایج و نکات امنیتی

  • استفاده از tx.origin برای احراز هویت: همیشه از msg.sender استفاده کنید؛ tx.origin در برابر حملات فیشینگ قراردادی آسیب‌پذیر است.
  • حلقه روی آرایه‌های نامحدود: اگر تابعی روی کل tips حلقه بزند، با بزرگ‌شدن آرایه ممکن است از سقف گس بلوک عبور کند. ما عمداً فقط دسترسی تکی با getTip گذاشته‌ایم.
  • ذخیره داده حجیم در storage: نوشتن در storage گران‌ترین عملیات رایج است. اگر فقط نمایش پیام‌ها لازم است، می‌توانید آن را فقط در رویداد ثبت کنید.
  • فرض‌های ثابت درباره گس: مقادیر گس در ارتقاهای شبکه تغییر می‌کنند؛ نمونه تازه‌اش ارتقای گلمستردام اتریوم است. هیچ عددی را هاردکد نکنید.
  • دیپلوی بدون تست و بازبینی: قبل از اینکه قراردادی با پول واقعی کار کند، تست خودکار بنویسید و کد را بازبینی امنیتی کنید.

قدم بعدی

حالا که اولین قرارداد را نوشته‌اید، این مسیر را پیشنهاد می‌کنیم: برای همین قرارداد با Foundry تست بنویسید، onlyOwner را با Ownable از OpenZeppelin جایگزین کنید و یک فرانت‌اند ساده با کتابخانه‌ای مثل viem بسازید که رویدادهای TipReceived را نمایش دهد. در هر مرحله، مستندات رسمی Solidity بهترین مرجع شماست.

پرسش‌های پرتکرار

برای یادگیری Solidity باید اول چه زبانی بلد باشم؟

آشنایی با یک زبان برنامه‌نویسی مثل JavaScript، Python یا ++C کافی است. سینتکس Solidity به JavaScript و ++C نزدیک است، اما مفاهیمی مثل گس، storage و تغییرناپذیری کد دیپلوی‌شده را باید جداگانه یاد بگیرید.

آیا برای تست قرارداد باید اتر واقعی خرج کنم؟

خیر. در Remix VM یا شبکه محلی Anvil همه چیز رایگان است. برای تست روی شبکه عمومی هم از تست‌نت Sepolia و اتر آزمایشی فاست (Faucet) استفاده کنید.

آیا قرارداد هوشمند بعد از دیپلوی قابل ویرایش است؟

کد قرارداد پس از دیپلوی تغییر نمی‌کند. برای ارتقا باید از الگوهای پراکسی استفاده کنید یا قرارداد جدیدی دیپلوی کنید؛ به همین دلیل تست و بازبینی پیش از دیپلوی حیاتی است.

Remix بهتر است یا Foundry؟

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

منابع

  1. Solidity Documentation
  2. Introduction to Smart Contracts — Solidity Docs
  3. Solidity 0.8.37 Release Announcement
  4. Remix IDE
  5. Foundry

خطایی در این مطلب دیدید؟ به تحریریه گزارش دهید؛ اصلاحیه‌ها طبق اصول تحریریه ثبت می‌شوند.

مطالب مرتبط