در این مقاله قصد نداریم راهنمای رسمی Solidity Style Guide را تکرار کنیم، بلکه میخواهیم به نکات و خطاهای رایجی بپردازیم که توسعه دهندگان اغلب در بازبینی کد (Code Review) یا حسابرسی امنیتی (Audit) قراردادهای هوشمند مرتکب میشوند. برخی از نکاتی که در اینجا مطرح میشود در راهنمای رسمی وجود ندارد، اما خطاهای رایج سبک نگارشی در میان توسعه دهندگان Solidity محسوب میشوند.
دو خط اول هر فایل در سالیدیتی
۱. اضافه کردن SPDX-License-Identifier
درست است که نبود این خط باعث خطای کامپایل نمیشود، اما هشدار (warning) تولید میکند. پس بهتر است این هشدار را از بین ببرید.
۲. مشخص کردن دقیق نسخه solidity
مگر اینکه کتابخانه بنویسید
احتمالاً تا به حال دو نوع نحوه نگارش نسخه Solidity را دیدهاید:
1 |
pragma solidity ^0.8.0; |
1 |
pragma solidity 0.8.21; |
اما در حالتی که در حال توسعه یک کتابخانه (Library) هستید — مانند آنچه پروژه های OpenZeppelin یا Solady انجام میدهند — نباید نسخه را قفل (fix) کنید. دلیل آن ساده است: شما نمیدانید توسعه دهنده ای که از کتابخانه تان استفاده میکند از چه نسخه ای از کامپایلر استفاده خواهد کرد.
وارد کردن (Import) فایل ها در Solidity
۳. نسخه کتابخانه را بهصورت دقیق در دستور import مشخص کنید
به جای این که بنویسید:
1 |
import "@openzepplin/contracts/token/ERC20/ERC20.sol"; |
1 |
import "@openzeppelin/contracts@4.9.3/token/ERC20/ERC20.sol"; |
اگر نسخه کتابخانه را در دستور import مشخص نکنید، ممکن است در آینده که آن کتابخانه به روزرسانی میشود، کد شما دیگر قابل کامپایل نباشد یا رفتارهای پیش بینی نشده ای از خود نشان دهد. مشخص کردن نسخه، باعث میشود کد شما پایدارتر و قابل اطمینانتر باشد.
۴. از Named Import استفاده کنید، نه کل فضای نام (Namespace)
به جای اینکه به شکل زیر تمام محتوای فایل را وارد کنید:
1 |
import "@openzeppelin/contracts@4.9.3/token/ERC20/ERC20.sol"; |
1 |
import {ERC20} from "@openzeppelin/contracts@4.9.3/token/ERC20/ERC20.sol"; |
۵. import های بدون استفاده را حذف کنید
اگر از ابزارهای امنیتی تحلیل کد مانند Slither استفاده میکنید، این موارد معمولاً شناسایی میشوند. با این حال، خودتان هم باید مراقب باشید. هرگاه کدی دیگر استفاده نمیشود، بهراحتی آن را حذف کنید. از پاک کردن کد نترسید — نگه داشتن کدهای اضافی فقط باعث پیچیدگی و افزایش احتمال خطا میشود.
سطح قرارداد (Contract Level) در سالیدیتی
۶. استفاده از مستندسازی سطح قرارداد با NatSpec
هدف از استفاده از NatSpec (Natural Specification) ایجاد مستنداتی خوانا برای انسان، بهصورت درون کدی (inline) است. این نوع مستندسازی، اطلاعات مفیدی را برای کاربران نهایی، توسعه دهندگان و ابزارهای تحلیل کد فراهم میکند.
در زیر یک نمونه مستند سازی NatSpec برای سطح قرارداد آورده شده است:
1 2 3 4 5 6 7 |
/// @title Liquidity token for Foo protocol /// @author Foo Incorporated /// @notice Notes for non-technical readers/ /// @dev Notes for people who understand Solidity contract LiquidityToken { } |
در اینجا:
-
@title
: عنوان قرارداد را مشخص میکند. -
@author
: نام یا سازمان نویسنده را درج میکند. -
@notice
: توضیحاتی برای مخاطب غیر فنی فراهم میکند. -
@dev
: نکاتی تخصصی برای توسعه دهندگان ارائه میدهد.
استفاده از این توضیحات باعث افزایش خوانایی، مستند سازی دقیق و تعامل بهتر با ابزارهایی مانند Etherscan و IDEها میشود.
۷. ساختار دهی منظم به بدنه قرارداد طبق راهنمای سبک نگارش
ساختار کلی بدنه یک قرارداد باید از نظر ترتیب توابع و اجزای مختلف به شکلی منظم و قابل پیشبینی تنظیم شود. این ساختار بر اساس دو اصل زیر طبقهبندی میشود:
-
سطح دسترسی (Externality): توابع ابتدا بر اساس سطح دسترسی شان دسته بندی میشوند:
-
توابع
receive
وfallback
(در صورت وجود) -
توابع
external
-
توابع
public
-
توابع
internal
-
توابع
private
-
-
میزان تغییر وضعیت (State-Changingness): در هر دسته، توابع باید به ترتیب زیر مرتب شوند:
-
توابع
payable
-
توابع بدون
payable
-
توابع
view
-
توابع
pure
-
نمونه ای از چینش صحیح ساختار قرارداد:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
contract ProperLayout { // type declarations, e.g. using Address for address // state vars address internal owner; uint256 internal _stateVar; uint256 internal _starteVar2; // events event Foo(); event Bar(address indexed sender); // errors error NotOwner(); error FooError(); error BarError(); // modifiers modifier onlyOwner() { if (msg.sender != owner) { revert NotOwner(); } _; } // functions constructor() { } receive() external payable { } falback() external payable { } // functions are first grouped by // - external // - public // - internal // - private // note how the external functions "descend" in order of how much they can modify or interact with the state function foo() external payable { } function bar() external { } function baz() external view { } function qux() external pure { } // public functions function fred() public { } function bob() public view { } // internal functions // internal view functions // internal pure functions // private functions // private view functions // private pure functions } |
ثابت ها (Constants) در سالیدیتی
۸. جایگزینی اعداد جادویی (Magic Numbers) با مقادیر ثابت
اگر عددی مانند 100
را بهتنهایی در کد ببینید، فوراً مشخص نیست منظور چیست؛ آیا منظور ۱۰۰ درصد است؟ ۱۰۰ واحد پایه (basis points)؟ یا چیز دیگر؟
برای خوانایی و نگهداری بهتر کد، اعداد باید بهصورت ثابت (constant) در بالای قرارداد تعریف شوند. برای مثال:
1 |
uint256 private constant MAX_PERCENT = 100; |
۹. استفاده از کلمات کلیدی زمان و اتر در سالیدیتی
زمانی که مقادیر عددی برای اندازه گیری زمان یا واحد اتر استفاده میشوند، بهتر است بهجای ضرب و تقسیم دستی، از کلمات کلیدی مشخص زبان Solidity استفاده کنید.
مثال نادرست:
1 2 |
uint256 secondsPerDay = 60 * 60 * 24; require(msg.value == 10**18 / 10, "must send 0.1 ether"); |
1 2 |
uint256 secondsPerDay = 1 days; require(msg.value == 0.1 ether, "must send 0.1 ether"); |
1 days
، 1 weeks
، 0.5 ether
و مانند آن باعث افزایش خوانایی و کاهش احتمال خطاهای محاسباتی میشود.
۱۰. استفاده از زیرخط (_) برای خواناتر کردن اعداد بزرگ
اعداد بزرگ را میتوان با استفاده از علامت زیرخط (underscore) قابلفهمتر کرد:
مثلا بجای اینکه بنویسید:
1 |
uint256 private constant BASIS_POINTS_DENOMINATOR = 10000; |
1 |
uint256 private constant BASIS_POINTS_DENOMINATOR = 10_000; |
این روش فقط ظاهر عدد را خواناتر میکند و تأثیری بر عملکرد برنامه ندارد.
توابع (Functions) در سالیدیتی
۱۱. حذف کلید واژه virtual
از توابعی که قرار نیست بازنویسی شوند
اگر تابعی در قرارداد پایه با کلید واژه virtual
تعریف شده باشد، به این معنی است که میتوان آن را در قراردادهای فرزند بازنویسی کرد. اما اگر میدانید که این تابع در هیچجا بازنویسی نخواهد شد (مثلاً چون خودتان مستقیماً قرارداد را مستقر میکنید)، این کلید واژه اضافی است و بهتر است حذف شود.
1 2 3 4 5 6 7 8 9 |
// اشتباه: function example() public virtual { // ... } // صحیح: function example() public { // ... } |
این کار باعث کاهش ابهام و بهبود امنیت قرارداد میشود.
۱۲. رعایت ترتیب صحیح در تعریف مشخصه های تابع
ترتیب مشخصه ها در امضای توابع باید طبق الگوی زیر باشد:
دسترسی (visibility) → قابلیت تغییر وضعیت (mutability) → virtual → override → محدودکننده های سفارشی (modifiers)
مثال صحیح:
1 2 3 4 5 6 7 8 9 |
// visibility (payability), [virtual], [override], [custom] function foo() public payable onlyAdmin { } function bar() internal view virtual override onlyAdmin { } |
۱۳. استفاده صحیح از Natspec در سالیدیتی
Natspec که گاهی بهاشتباه به آن «سبک کامنتگذاری در Solidity» گفته میشود، در واقع یک استاندارد مستندسازی رسمی برای قراردادهای هوشمند در Solidity است که مخفف Ethereum Natural Language Specification Format میباشد.
استفاده صحیح از Natspec به مستندسازی خوانا، دقیق و قابل پردازش توسط ابزارهایی مانند Etherscan کمک میکند. این مستندسازی نهتنها برای مخاطبان فنی، بلکه برای کاربران عادی نیز مفید است.
نکات مهم در استفاده از Natspec برای توابع
ساختار Natspec در توابع شبیه به کامنت گذاری در سطح قرارداد است، با این تفاوت که در اینجا پارامترها (@param
) و مقدار بازگشتی (@returns
) نیز مشخص میشوند. همچنین میتوان توضیحاتی درباره رفتار فنی تابع (با @dev
) ارائه کرد.
مثال استاندارد:
1 2 3 4 5 6 7 8 9 10 |
/// @notice واریز توکنهای ERC20 /// @dev در صورت موفقیت رویداد Deposit صادر میشود /// @dev اگر توکن در لیست مجاز نباشد، تراکنش revert میشود /// @dev اگر قرارداد مجاز به برداشت از ERC20 نباشد، تراکنش revert میشود /// @param token آدرس توکن ERC20 که قرار است واریز شود /// @param amount مقدار توکنی که باید واریز شود /// @return مقدار توکن نقدشوندگی (liquidity token) که کاربر دریافت میکند function deposit(address token, uint256 amount) public returns (uint256) { // ... } |
استفاده از @param
برای شرح هر پارامتر، و @return
برای مشخص کردن خروجی تابع، به خوانایی بیشتر کد کمک میکند. حتی اگر نام متغیر ها خیلی خلاصه باشد، توضیحات Natspec میتوانند آنها را معنا دار کنند.
ارث بری از مستند سازی (natspec) در توابع به ارث رسیده
اگر تابعی از قرارداد پایه به ارث بردهاید و مستندسازی در آن وجود دارد، میتوانید با @inheritdoc
به سادگی مستندات را از قرارداد پایه به ارث ببرید:
1 2 3 4 5 6 7 8 9 |
/// @inheritdoc Lendable function calculateAccumulatedInterest(address token, uint256 since) public override view returns (uint256 interest) { // ... } |
چه چیزهایی را در @dev
توضیح دهیم؟
در قسمت @dev
معمولاً موارد فنیتر مثل تغییرات در وضعیت قرارداد، ارسال اتر، صدور رویداد، فراخوانی های حساس یا عملیات مخرب (مثل selfdestruct
) ذکر میشود.
مستند سازی
@notice
و@param
توسط Etherscan خوانده میشود و به کاربران نهایی نمایش داده میشود.
میتوانید در تصویر زیر از کد، ببینید که Etherscan این اطلاعات را از کجا آورده است:
پاکیزگی کلی کد (General Cleanliness)
14. حذف کد های کامنت شده
این مورد نیازی به توضیح زیادی ندارد. اگر قطعهای از کد را کامنت کردهاید و قصد ندارید دوباره از آن استفاده کنید، آن را حذف کنید. وجود چنین کدهایی باعث شلوغی فایل ها و کاهش خوانایی کلی پروژه میشود. پاک سازی این کدها یکی از ساده ترین و مؤثرترین راه های افزایش شفافیت است.
15. در انتخاب نام متغیرها با دقت فکر کنید
انتخاب نام مناسب برای متغیرها و توابع یکی از چالشبرانگیزترین بخشهای برنامه نویسی است، اما تأثیر چشمگیری بر خوانایی و نگهداری کد دارد. در ادامه، چند توصیه مهم برای نامگذاری آورده شده است:
-
از اسامی کلی و مبهم مانند
user
خودداری کنید. به جای آن از اسامی دقیقتری مانندadmin
،buyer
یاseller
استفاده کنید که نقش واقعی موجودیت را بهتر بیان میکنند. -
واژه هایی مانند
data
معمولاً نشاندهنده نامگذاری مبهم هستند. به جایuserData
بهتر است ازuserAccount
استفاده شود که مشخصتر است. -
از بهکارگیری اسامی متفاوت برای اشاره به یک مفهوم واحد بپرهیزید. مثلاً اگر
depositor
وliquidityProvider
در واقع به یک موجودیت اشاره دارند، یکی را انتخاب کرده و در سراسر کد از همان استفاده کنید. -
اگر متغیر عددی دارای واحد خاصی است، آن را در نامگذاری منعکس کنید. مثلاً به جای
interestRate
بنویسیدinterestRateBasisPoints
یاfeeInWei
. -
توابعی که وضعیت قرارداد را تغییر میدهند باید شامل فعل باشند، تا ماهیت اجرایی آنها بهوضوح مشخص باشد.
-
از نشانهگذاری (مثل زیرخط یا _underscore) بهشکل یکسان و منسجم برای تمایز متغیرهای داخلی و آرگومان های ورودی استفاده کنید. مثلاً اگر قراردادی بین اعضای تیم وجود دارد که متغیرهای داخلی با زیرخط شروع میشوند (
_internalVar
)، در استفاده از آن استاندارد بمانید و آن را برای کاربردهای دیگر (مثلاً آرگومان هایی که با متغیرهای همنام تداخل دارند) به کار نبرید. -
استفاده از پیشوندهای
get
وset
برای توابعی که بهترتیب فقط داده ها را نمایش میدهند یا وضعیت را تغییر میدهند، یک عرف رایج و پذیرفتهشده در برنامه نویسی است. در صورت امکان، این رویه را رعایت کنید.
در پایان، پس از نوشتن کد، مدتی از آن فاصله بگیرید و بعد از حدود ۱۵ دقیقه، به آن بازگردید. سپس برای هر نام متغیر و تابع از خودتان بپرسید: «آیا این نام دقیقترین و شفافترین انتخاب ممکن بوده است؟» این بازنگری آگاهانه، تأثیر بیشتری نسبت به هر چکلیستی خواهد داشت؛ چرا که شما بهتر از هر کسی از هدف و منطق پشت کدتان آگاه هستید.
ترفندهای اضافی برای سازمان دهی کدهای بزرگ در سالیدیتی
وقتی پروژه شما بزرگتر میشود و کدها گستردهتر میشوند، داشتن ساختار منظم و مقیاسپذیر اهمیت زیادی پیدا میکند. در ادامه چند ترفند کاربردی برای مدیریت بهتر کدهای حجیم آورده شده است:
1. جدا کردن متغیرهای ذخیره سازی در یک قرارداد مستقل
اگر تعداد متغیرهای ذخیره سازی (Storage Variables) زیاد است، میتوانید آنها را در یک قرارداد جداگانه تعریف کرده و سپس در سایر قراردادها از آن ارث بری (inheritance) انجام دهید.
مثال:
1 2 3 4 5 6 7 8 9 |
contract StorageLayout { address internal _owner; uint256 internal _feeInWei; mapping(address => uint256) internal _balances; } contract MyToken is StorageLayout { // از متغیرهای _owner، _feeInWei، و _balances میتوان بهصورت مستقیم استفاده کرد } |
2. استفاده از ساختار (struct) برای توابع با پارامترهای زیاد
اگر تابعی پارامترهای زیادی دارد، آنها را در یک struct
قرار دهید تا خوانایی و توسعه پذیری تابع افزایش یابد.
1 2 3 4 5 6 7 8 9 10 |
struct OrderParams { address buyer; address seller; uint256 amount; uint256 price; } function placeOrder(OrderParams calldata params) external { // استفاده از params.buyer و ... } |
3. تجمیع ایمپورت ها در یک فایل مشترک
اگر پروژه شما شامل ده ها فایل است و نیاز به ایمپورت های متعدد دارید، میتوانید یک فایل مثلاً Imports.sol
ایجاد کرده و تمام ایمپورتها را در آن بنویسید. سپس در سایر فایل ها فقط آن فایل را import کنید. (البته این کار عمداً برخلاف قانون استفاده از ایمپورت های نامگذاریشده است.)
1 2 3 4 |
// Imports.sol import "@openzeppelin/contracts@4.9.3/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts@4.9.3/access/Ownable.sol"; // و غیره... |
4. استفاده از کتابخانه ها (libraries) برای گروهبندی توابع مشابه
اگر چند تابع کاربردی مربوط به یک موضوع خاص دارید (مثل محاسبات مالی)، آنها را در یک library
تعریف کرده و در چندین قرارداد استفاده کنید. این کار باعث کاهش تکرار و افزایش خوانایی میشود.
1 2 3 4 5 |
library MathUtils { function percentOf(uint256 value, uint256 percentBasisPoints) internal pure returns (uint256) { return (value * percentBasisPoints) / 10_000; } } |
جمعبندی
سازماندهی کد در پروژه های بزرگ یک هنر است، نه صرفاً یک مهارت فنی. بهترین راه برای یادگیری آن، مطالعه پروژه های بزرگ و معتبر مانند Aave، Compound، Uniswap و OpenZeppelin است. این کار به شما دید عمیقتری درباره معماری کد، جداسازی مسئولیت ها و مدیریت وابستگی ها میدهد.
راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.
- انتشار: ۲۳ اردیبهشت ۱۴۰۴
دسته بندی موضوعات
- آموزش ارز دیجیتال
- آموزش برنامه نویسی
- آموزش متنی برنامه نویسی
- اطلاعیه و سایر مطالب
- پروژه برنامه نویسی
- دوره های تخصصی برنامه نویسی
- رپورتاژ
- فیلم های آموزشی
- ++C
- ADO.NET
- Adobe Flash
- Ajax
- AngularJS
- apache
- ARM
- Asp.Net
- ASP.NET MVC
- AVR
- Bootstrap
- CCNA
- CCNP
- CMD
- CSS
- Dreameaver
- EntityFramework
- HTML
- IOS
- jquery
- Linq
- Mysql
- Oracle
- PHP
- PHPMyAdmin
- Rational Rose
- silver light
- SQL Server
- Stimulsoft Reports
- Telerik
- UML
- VB.NET&VB6
- WPF
- Xml
- آموزش های پروژه محور
- اتوکد
- الگوریتم تقریبی
- امنیت
- اندروید
- اندروید استودیو
- بک ترک
- بیسیک فور اندروید
- پایتون
- جاوا
- جاوا اسکریپت
- جوملا
- دلفی
- دوره آموزش Go
- دوره های رایگان پیشنهادی
- زامارین
- سئو
- ساخت CMS
- سی شارپ
- شبکه و مجازی سازی
- طراحی الگوریتم
- طراحی بازی
- طراحی وب
- فتوشاپ
- فریم ورک codeigniter
- فلاتر
- کانستراکت
- کریستال ریپورت
- لاراول
- معماری کامپیوتر
- مهندسی اینترنت
- هوش مصنوعی
- یونیتی
- کتاب های آموزشی
- Android
- ASP.NET
- AVR
- LINQ
- php
- Workflow
- اچ تی ام ال
- بانک اطلاعاتی
- برنامه نویسی سوکت
- برنامه نویسی موبایل
- پاسکال
- پایان نامه
- پایتون
- جاوا
- جاوا اسکریپت
- جی کوئری
- داده کاوی
- دلفی
- رباتیک
- سئو
- سایر کتاب ها
- سخت افزار
- سی اس اس
- سی پلاس پلاس
- سی شارپ
- طراحی الگوریتم
- فتوشاپ
- مقاله
- مهندسی نرم افزار
- هک و امنیت
- هوش مصنوعی
- ویژوال بیسیک
- نرم افزار و ابزار برنامه نویسی
- وردپرس