در این مقاله، با سه روش مختلف برای تشخیص قرارداد هوشمند در سالیدیتی آشنا میشوید که به شما کمک میکنند بفهمید آیا یک آدرس مشخص مربوط به یک قرارداد هوشمند هست یا خیر:
-
بررسی شرط
msg.sender == tx.origin
این روش پیشنهاد نمیشود، اما به دلیل استفاده گسترده آن در بسیاری از قراردادها، برای تکمیل بحث به آن خواهیم پرداخت. -
بررسی اندازه کد بایت آدرس با استفاده از
code.length
این روش، راهکار پیشنهادی ماست، هرچند همچنان محدودیتهایی دارد که توسعهدهنده باید آنها را در نظر بگیرد. -
استفاده از
codehash
این روش نیز پیشنهاد نمیشود، زیرا علاوه بر داشتن محدودیتهای مشابه باcode.length
، پیچیدگی بیشتری هم دارد.
در ادامه هر روش را به تفصیل بررسی میکنیم. در انتهای مقاله نیز چند معمای سالیدیتی برای محک زدن درک شما ارائه شده است.
روش اول: بررسی msg.sender == tx.origin
در سالیدیتی، متغیر جهانی tx.origin
نمایانگر کیف پولی است که تراکنش را آغاز کرده، در حالی که msg.sender
آدرسی است که مستقیماً قرارداد را فراخوانی کرده است.
اگر یک کیف پول بهصورت مستقیم با قرارداد تعامل داشته باشد، tx.origin
و msg.sender
برابر خواهند بود. اما اگر آن کیف پول ابتدا با قرارداد A تماس بگیرد و سپس A قرارداد B را فراخوانی کند، در قرارداد B مقدار msg.sender
برابر با A خواهد بود، و tx.origin
همچنان همان کیف پول اولیه باقی میماند. این یعنی در چنین حالتی msg.sender
با tx.origin
برابر نیست. دیاگرام زیر رابطه میان msg.sender
و tx.origin
را نشان میدهد:
بر همین اساس، میتوان با شرط msg.sender == tx.origin
تشخیص داد که آیا فراخوانی از سمت یک قرارداد دیگر بوده یا مستقیماً از یک کیف پول.
اما استفاده از require(msg.sender == tx.origin)
یک الگوی نادرست است
با رواج استفاده از قراردادهای هوشمند بهعنوان کیف پول—چه در قالب کیف پولهای چند امضایی (مانند Gnosis Safe) و چه در قالب حسابهای انتزاعی (Account Abstraction) مانند استاندارد ERC-4337—کاربرد چنین شرطی به مشکلی جدی تبدیل میشود.
وقتی در یک قرارداد هوشمند از این شرط استفاده میکنید، به این معناست که هیچ کیف پول مبتنی بر قرارداد هوشمند نمیتواند با آن قرارداد تعامل داشته باشد. در نتیجه، بسیاری از کاربران یا اپلیکیشنهای مدرن از استفاده از قرارداد شما محروم خواهند شد.
همچنین، این روش تنها میتواند بررسی کند که آیا msg.sender
یک قرارداد است یا نه؛ ولی قادر به بررسی یک آدرس دلخواه (غیر از msg.sender) نخواهد بود.
روش دوم: استفاده از code.length
برای تشخیص قرارداد هوشمند در سالیدیتی
روش پیشنهادی و رایجتر برای تشخیص قرارداد هوشمند در سالیدیتی، بررسی طول کد بایت یک آدرس است. اگر آدرس دارای بایتکد باشد، بهاحتمال بسیار زیاد یک قرارداد هوشمند محسوب میشود.
کد زیر را در نظر بگیرید:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
contract TestAddress { function test( address target ) public view returns (bool isContract) { if (target.code.length == 0) { isContract = false; } else { isContract = true; } } } |
با اینکه همه قراردادهای هوشمند دارای بایت کد هستند و آدرسهای کیف پول فاقد بایت کد میباشند، اما نکاتی ظریف و مهمی وجود دارد که باید به آنها توجه کرد:
-
آدرسی که اکنون بایت کدی ندارد، ممکن است در آینده دارای بایت کد شود، اگر یک قرارداد جدید در آن مستقر (Deploy) گردد. بنابراین، بررسی لحظهای
code.length
تضمین نمیکند که آن آدرس همیشه بدون بایت کد باقی خواهد ماند. -
استفاده از شرط
msg.sender.code.length == 0
روش قابل اعتمادی برای تشخیص اینکه آیا فراخوانی از سمت یک قرارداد بوده یا نه نیست. دلیل آن این است که اگر یک قرارداد هوشمند در حال فراخوانی از درون تابع سازنده (constructor) باشد، در آن لحظه هنوز بایت کد آن روی شبکه مستقر نشده است. بنابراین مقدارcode.length
صفر خواهد بود، حتی اگر آدرسmsg.sender
در نهایت به یک قرارداد مربوط شود. -
در شبکههایی که از دستور
selfdestruct
پشتیبانی میکنند، ممکن است یک آدرس در گذشته دارای قرارداد بوده باشد، اما آن قرارداد با استفاده ازselfdestruct
حذف شده باشد. در این صورت، بررسی فعلیcode.length
صفر را نشان میدهد، اما این مقدار نشاندهنده گذشته آن آدرس نیست.
در نتیجه، گرچه بررسی بایت کد روشی مفید و کاربردی است، اما درک محدودیتهای آن برای پیادهسازی امن و دقیق، ضروری است.
بررسی msg.sender
با استفاده از code.length
اگر یک کیف پول مستقیماً یک قرارداد را فراخوانی کند، مقدار msg.sender.code.length
قطعاً برابر با صفر خواهد بود. چرا که کیف پولها فاقد بایت کد هستند.
اما اگر یک قرارداد هوشمند، قرارداد دیگری را فراخوانی کند:
- اگر این فراخوانی از درون تابع سازنده (constructor) انجام شود، در آن لحظه هنوز بایت کد قرارداد مستقر نشده و مقدار
msg.sender.code.length
صفر خواهد بود. - اگر فراخوانی از یک تابع معمولی در قرارداد صورت گیرد (یعنی پس از استقرار کامل قرارداد)، مقدار
msg.sender.code.length
صفر نخواهد بود.
بررسی یک آدرس دلخواه (و نه msg.sender) با استفاده از code.length
اگر یک قرارداد هوشمند با استفاده از address(target).code.length
بررسی کند که آیا آدرس مشخصی یک قرارداد است یا خیر، نتایج به صورت زیر خواهد بود:
اگر آدرس هدف (target
) مربوط به یک قرارداد هوشمند باشد، مقدار address(target).code.length
قطعاً غیر صفر خواهد بود.
البته باید در نظر داشت که اگر قرارداد مربوطه از تابع
selfdestruct
استفاده کند و شبکه نیز از آن پشتیبانی کند، بایت کد قرارداد حذف میشود و در نتیجه مقدارcode.length
در آینده میتواند صفر شود.
اگر آدرس هدف مربوط به یک کیف پول (Wallet) باشد، مقدار address(target).code.length
قطعاً صفر خواهد بود.
با این حال، اینکه الان مقدار code.length
صفر است، به این معنا نیست که همیشه صفر باقی خواهد ماند. ممکن است در آینده در همان آدرس یک قرارداد مستقر شود.
برای مثال، فرض کنید من یک آدرس به شما میدهم و شما همین حالا مقدار address(target).code.length
را اندازه میگیرید و مقدار صفر دریافت میکنید. این مقدار در لحظه درست است، اما ممکن است بعداً یک قرارداد جدید روی آن آدرس مستقر شود و اگر دوباره بررسی کنید، مقدار code.length
دیگر صفر نباشد.
بنابراین، code.length
تنها در لحظه بررسی، معتبر است و نباید بهعنوان یک ویژگی دائمی یک آدرس در نظر گرفته شود.
کاربرد رایج تشخیص قرارداد هوشمند در سالیدیتی
یکی از کاربردهای مهم تشخیص قرارداد بودن یک آدرس، در انتقال توکن ها است. اگر یک توکن به آدرسی ارسال شود که مربوط به یک قرارداد هوشمند بدون قابلیت دریافت یا انتقال توکن باشد، آن توکن ها در آن آدرس گیر میافتند و برای همیشه غیرقابل دسترسی خواهند شد.
برای جلوگیری از این مشکل، برخی از استانداردهای توکن سازی در اتریوم، مکانیزمهایی برای بررسی نوع آدرس مقصد در نظر گرفتهاند.
بهعنوان مثال، استاندارد ERC-721 در تابع safeTransferFrom
پیش از انجام انتقال، بررسی میکند که آیا آدرس گیرنده یک قرارداد هوشمند است یا نه. این بررسی معمولاً با استفاده از تکنیک code.length
انجام میشود.
(کد اصلی)
اگر مشخص شود که آدرس مقصد یک قرارداد است، قرارداد ERC-721 تلاش میکند یک تابع خاص در آن قرارداد را فراخوانی کند تا مطمئن شود که قابلیت دریافت توکن را دارد. اگر چنین تابعی در قرارداد مقصد وجود نداشته باشد، انتقال متوقف میشود و از گیر افتادن توکن جلوگیری میگردد.
این اقدام، یک لایه محافظتی برای حفظ امنیت و دسترسیپذیری توکن ها در شبکه است.
روش سوم: استفاده از codehash
روش مناسبی برای تشخیص قرارداد بودن آدرس نیست
تابع codehash
مقدار هش keccak256
از بایتکد یک آدرس را بازمیگرداند. اگرچه ممکن است در ظاهر روشی دقیق به نظر برسد، اما پیچیدگیها و رفتارهای خاصی دارد که آن را برای استفاده در عمل نامناسب میکند.
رفتار تابع codehash
به این شکل است:
-
اگر آدرس فاقد موجودی اتر و فاقد بایت کد باشد، چون چیزی برای هش کردن وجود ندارد، مقدار بازگشتی برابر خواهد بود با
bytes32(0)
(یعنی ۳۲ بایت صفر). - اگر آدرس دارای موجودی اتر ولی فاقد بایت کد باشد، مقدار بازگشتی برابر خواهد بود با هش داده خالی
keccak256("")
، که مقدار آن برابر است با:
1 |
0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 |
-
اگر آدرس دارای بایت کد باشد (بدون توجه به موجودی)، مقدار بازگشتی برابر با
keccak256
محتوای بایتکد قرارداد خواهد بود.
رفتار دقیق این تابع در کامنت های کلاینت اتریوم توضیح داده شده است.
برخی قراردادها به اشتباه از codehash
برای بررسی اینکه آیا یک آدرس دارای بایت کد است یا نه استفاده کردهاند. اما این روش مشکلساز است، زیرا:
-
اگر آدرس هیچ بایت کدی نداشته باشد، مقدار بازگشتی ممکن است
bytes32(0)
یاkeccak256("")
باشد، که نیاز به بررسی هر دو حالت دارد. -
فرض کنید آدرس
a
هیچ بایت کدی نداشته و موجودی اتر هم ندارد؛ در این صورت،address(a).codehash
برابرbytes32(0)
است. اما اگر بعداً فقط مقدار کمی اتر به آن آدرس ارسال شود، مقدارcodehash
بهkeccak256("")
تغییر میکند—در حالی که آن آدرس همچنان نه کیف پول است و نه قرارداد.
میتوانید کد زیر را در Remix تست کنید تا رفتار codehash را ببینید:
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 |
contract TestHash { function getHash() external view returns (bytes32) { // random address with no balance or code return address(101).codehash;// returns 0x000...000 } function hashOfNonEmptyWallet() external view returns (bytes32) { // tx.origin has a non-zero ether balance return tx.origin.codehash; // returns a non-zero hash } // observe that `keccakNil` and `hashOfNonEmptyWallet` // return the same value function keccakNil() external pure returns (bytes32) { return keccak256(""); } // Deploy SomeTestContract and put its address in // codeHashOtherContract to test it function codeHashOtherContract( address _a ) external view returns (bool) { // returns true because the codehash // of another contract // is equal to the `keccak256` of its bytecode return a.codehash == keccak256(a.code); } } contract SomeTestContract { function someFunction() external pure returns (uint256) { return 5; } } |
در نتیجه، اگرچه هم code.length
و هم codehash
میتوانند وجود بایت کد را بررسی کنند، اما استفاده از codehash
باعث افزایش پیچیدگی غیرضروری میشود و سه نتیجه متفاوت را باید مدیریت کرد. این در حالی است که با استفاده از code.length
فقط کافیست بررسی کنیم صفر هست یا نه.
به همین دلیل، استفاده از code.length
روش بسیار سادهتر، شفافتر و مطمئنتری برای تشخیص قرارداد بودن یک آدرس است.
معما برای سنجش درک شما از مفاهیم
معما ۱
آیا میتوانید کاری کنید که قرارداد زیر، هنگام فراخوانی تابع puzzle
، مقدار true
بازگرداند و خطا (revert) ندهد؟
1 2 3 4 5 6 7 8 9 10 |
contract Puzzle { function puzzle() external view returns (bool success) { require(msg.sender != tx.origin); require(msg.sender.code.length == 0); success = true; } } |
معما ۲
تابع tx.origin.code.length
چه مقداری را بازمیگرداند؟ آیا این مقدار همیشه ثابت است؟
راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.
- انتشار: ۲۳ اردیبهشت ۱۴۰۴
دسته بندی موضوعات
- آموزش ارز دیجیتال
- آموزش برنامه نویسی
- آموزش متنی برنامه نویسی
- اطلاعیه و سایر مطالب
- پروژه برنامه نویسی
- دوره های تخصصی برنامه نویسی
- رپورتاژ
- فیلم های آموزشی
- ++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
- اچ تی ام ال
- بانک اطلاعاتی
- برنامه نویسی سوکت
- برنامه نویسی موبایل
- پاسکال
- پایان نامه
- پایتون
- جاوا
- جاوا اسکریپت
- جی کوئری
- داده کاوی
- دلفی
- رباتیک
- سئو
- سایر کتاب ها
- سخت افزار
- سی اس اس
- سی پلاس پلاس
- سی شارپ
- طراحی الگوریتم
- فتوشاپ
- مقاله
- مهندسی نرم افزار
- هک و امنیت
- هوش مصنوعی
- ویژوال بیسیک
- نرم افزار و ابزار برنامه نویسی
- وردپرس