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

فهرست مطالب
آموزش Solidity معمولاً با یک «Hello World» خشک شروع میشود که چیز زیادی درباره دنیای واقعی قراردادهای هوشمند یاد نمیدهد. در این آموزش یک قرارداد کوچک اما واقعی مینویسیم: قلکی برای دریافت انعام به اتر که هر کسی میتواند به آن پول و پیام بفرستد و فقط مالکش میتواند برداشت کند. در مسیر، مهمترین مفاهیم Solidity را خطبهخط میبینیم و در نهایت قرارداد را روی تستنت دیپلوی میکنیم.
اگر هنوز با مفاهیمی مثل بلاک، تراکنش و گس آشنا نیستید، پیش از شروع توضیح فنی بلاکچین برای برنامهنویسها را بخوانید.
پیشنیازها
- آشنایی مقدماتی با یک زبان برنامهنویسی.
- مرورگر برای کار با Remix، یا ترمینال برای Foundry.
- یک کیف پول نرمافزاری آزمایشی و مقداری اتر تستنت Sepolia از یک فاست (Faucet). هرگز از کیف پول اصلیتان برای آزمایش استفاده نکنید.
قرارداد TipJar چه میکند؟
قرارداد ما چهار قابلیت دارد:
- دریافت انعام همراه با یک پیام کوتاه (حداکثر ۱۴۰ بایت).
- دریافت مستقیم اتر بدون پیام.
- ثبت تاریخچه انعامها و مجموع آنها.
- برداشت کل موجودی، فقط توسط مالک.
کد کامل قرارداد
فایلی به نام 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 مرورگری است و سادهترین راه برای شروع:
- به remix.ethereum.org بروید، فایل
TipJar.solرا بسازید و کد را در آن بچسبانید. - در تب Solidity Compiler نسخه کامپایلر را روی 0.8.24 یا بالاتر بگذارید و Compile را بزنید.
- در تب Deploy & Run Transactions، یکی از محیطهای Remix VM را انتخاب کنید. این یک بلاکچین شبیهسازیشده با حسابهای پراتر آزمایشی است.
- Deploy را بزنید. حالا در فیلد Value مثلاً
1000000wei وارد کنید، در ورودیtipپیامی بنویسید و فراخوانی کنید. tipCountوgetTip(0)را صدا بزنید و نتیجه را ببینید. بعد با حساب دیگریwithdrawرا امتحان کنید تا خطایNotOwnerرا ببینید.- برای تستنت واقعی، محیط را روی گزینه اتصال به کیف پول مرورگر (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 بهترین مرجع شماست.
مطالب مرتبط

ارتقای گلمستردام اتریوم ۶ اکتبر روی تستنت Sepolia فعال میشود

بلاکچین چیست؟ توضیح فنی برای برنامهنویسها، از هش تا اجماع

ورود ۲٫۴ میلیارد دلار به ETF بیتکوین؛ بزرگترین هفته از اکتبر ۲۰۲۵
