۳ روش مؤثر برای تشخیص قرارداد هوشمند در سالیدیتی

در این مقاله، با سه روش مختلف برای تشخیص قرارداد هوشمند در سالیدیتی آشنا می‌شوید که به شما کمک می‌کنند بفهمید آیا یک آدرس مشخص مربوط به یک قرارداد هوشمند هست یا خیر:

  1. بررسی شرط msg.sender == tx.origin
    این روش پیشنهاد نمی‌شود، اما به دلیل استفاده گسترده آن در بسیاری از قراردادها، برای تکمیل بحث به آن خواهیم پرداخت.

  2. بررسی اندازه کد بایت آدرس با استفاده از code.length
    این روش، راهکار پیشنهادی ماست، هرچند همچنان محدودیت‌هایی دارد که توسعه‌دهنده باید آن‌ها را در نظر بگیرد.

  3. استفاده از 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 را هنگامی که یک کیف پول مستقیماً یک قرارداد را فراخوانی می‌کند و هنگامی که یک کیف پول از طریق یک قرارداد هوشمند دیگر، یک قرارداد هوشمند را فراخوانی می‌کند، نشان می‌دهد.

بر همین اساس، می‌توان با شرط msg.sender == tx.origin تشخیص داد که آیا فراخوانی از سمت یک قرارداد دیگر بوده یا مستقیماً از یک کیف پول.

اما استفاده از require(msg.sender == tx.origin) یک الگوی نادرست است

با رواج استفاده از قراردادهای هوشمند به‌عنوان کیف پول—چه در قالب کیف پول‌های چند امضایی (مانند Gnosis Safe) و چه در قالب حساب‌های انتزاعی (Account Abstraction) مانند استاندارد ERC-4337—کاربرد چنین شرطی به مشکلی جدی تبدیل می‌شود.

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

همچنین، این روش تنها می‌تواند بررسی کند که آیا msg.sender یک قرارداد است یا نه؛ ولی قادر به بررسی یک آدرس دلخواه (غیر از msg.sender) نخواهد بود.

روش دوم: استفاده از code.length برای تشخیص قرارداد هوشمند در سالیدیتی

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

کد زیر را در نظر بگیرید:

با اینکه همه قراردادهای هوشمند دارای بایت کد هستند و آدرس‌های کیف پول فاقد بایت کد می‌باشند، اما نکاتی ظریف و مهمی وجود دارد که باید به آن‌ها توجه کرد:

  • آدرسی که اکنون بایت کدی ندارد، ممکن است در آینده دارای بایت کد شود، اگر یک قرارداد جدید در آن مستقر (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 انجام می‌شود.

قطعه کدی از تابع checkOnERC721Received که عبارت if مربوط به code.length را هایلایت می‌کند.

(کد اصلی)

اگر مشخص شود که آدرس مقصد یک قرارداد است، قرارداد ERC-721 تلاش می‌کند یک تابع خاص در آن قرارداد را فراخوانی کند تا مطمئن شود که قابلیت دریافت توکن را دارد. اگر چنین تابعی در قرارداد مقصد وجود نداشته باشد، انتقال متوقف می‌شود و از گیر افتادن توکن جلوگیری می‌گردد.

این اقدام، یک لایه محافظتی برای حفظ امنیت و دسترسی‌پذیری توکن ها در شبکه است.

روش سوم: استفاده از codehash روش مناسبی برای تشخیص قرارداد بودن آدرس نیست

تابع codehash مقدار هش keccak256 از بایت‌کد یک آدرس را بازمی‌گرداند. اگرچه ممکن است در ظاهر روشی دقیق به نظر برسد، اما پیچیدگی‌ها و رفتارهای خاصی دارد که آن را برای استفاده در عمل نامناسب می‌کند.

رفتار تابع codehash به این شکل است:

  • اگر آدرس فاقد موجودی اتر و فاقد بایت کد باشد، چون چیزی برای هش کردن وجود ندارد، مقدار بازگشتی برابر خواهد بود با bytes32(0) (یعنی ۳۲ بایت صفر).

  • اگر آدرس دارای موجودی اتر ولی فاقد بایت کد باشد، مقدار بازگشتی برابر خواهد بود با هش داده خالی keccak256("")، که مقدار آن برابر است با:
  • اگر آدرس دارای بایت کد باشد (بدون توجه به موجودی)، مقدار بازگشتی برابر با keccak256 محتوای بایت‌کد قرارداد خواهد بود.

رفتار دقیق این تابع در کامنت های کلاینت اتریوم توضیح داده شده است.

برخی قراردادها به اشتباه از codehash برای بررسی اینکه آیا یک آدرس دارای بایت کد است یا نه استفاده کرده‌اند. اما این روش مشکل‌ساز است، زیرا:

  • اگر آدرس هیچ بایت کدی نداشته باشد، مقدار بازگشتی ممکن است bytes32(0) یا keccak256("") باشد، که نیاز به بررسی هر دو حالت دارد.

  • فرض کنید آدرس a هیچ بایت کدی نداشته و موجودی اتر هم ندارد؛ در این صورت، address(a).codehash برابر bytes32(0) است. اما اگر بعداً فقط مقدار کمی اتر به آن آدرس ارسال شود، مقدار codehash به keccak256("") تغییر می‌کند—در حالی که آن آدرس همچنان نه کیف پول است و نه قرارداد.

می‌توانید کد زیر را در Remix تست کنید تا رفتار codehash را ببینید:

در نتیجه، اگرچه هم code.length و هم codehash می‌توانند وجود بایت کد را بررسی کنند، اما استفاده از codehash باعث افزایش پیچیدگی غیرضروری می‌شود و سه نتیجه متفاوت را باید مدیریت کرد. این در حالی است که با استفاده از code.length فقط کافی‌ست بررسی کنیم صفر هست یا نه.

به همین دلیل، استفاده از code.length روش بسیار ساده‌تر، شفاف‌تر و مطمئن‌تری برای تشخیص قرارداد بودن یک آدرس است.

معما برای سنجش درک شما از مفاهیم

معما ۱

آیا می‌توانید کاری کنید که قرارداد زیر، هنگام فراخوانی تابع puzzle، مقدار true بازگرداند و خطا (revert) ندهد؟

معما ۲

تابع tx.origin.code.length چه مقداری را بازمی‌گرداند؟ آیا این مقدار همیشه ثابت است؟

5/5 - (1 امتیاز)

راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.

پکیج آموزش پروژه محور لاراول و طراحی وب سایت کانون قلم چی
  • انتشار: ۲۳ اردیبهشت ۱۴۰۴

دسته بندی موضوعات

آخرین محصولات فروشگاه

مشاهده همه

نظرات

بازخوردهای خود را برای ما ارسال کنید