برخی از توسعه دهندگان از “delegatecall” می ترسند زیرا به آنها گفته شده است که “خطرناک” است. ترس و خطر ناشی از عدم درک چگونگی عملکرد یک چیز و نحوه استفاده ایمن از آن است. به عنوان مثال، اکثر ما از رانندگی با یک ماشین نمی ترسیم، زیرا به اندازه کافی در مورد نحوه عملکرد آن آگاهی داریم و می دانیم که چگونه آن را با خیال راحت انجام دهیم.
پیشنهاد ویژه: آموزش رایگان سالیدیتی
هنگامی که یک قرارداد با استفاده از delegatecall یک تابع را فراخوانی می کند، کد تابع را از قرارداد دیگری بارگیری می کند و آن را به گونه ای اجرا می کند که گویی کد خودش است.
هنگامی که یک تابع با delegatecall اجرا می شود، این مقادیر تغییر نمی کنند:
آدرس (این)
msg.sender
msg.value
خواندن و نوشتن متغیرهای حالت در قراردادی اتفاق می افتد که توابع را با delegatecall بارگیری و اجرا می کند. خواندن و نوشتن هرگز برای قراردادی که دارای توابع بازیابی شده است اتفاق نمی افتد.
بنابراین اگر ContractA از delegatecall برای اجرای تابعی از ContractB استفاده کند، دو نکته زیر درست است:
متغیرهای حالت در ContractA قابل خواندن و نوشتن هستند.
متغیرهای حالت در ContractB هرگز خوانده یا نوشته نمی شوند.
هر دو ContractA و ContractB می توانند متغیرهای حالت یکسانی را اعلام کنند، و توابع ContractB می توانند مقادیر را در این متغیرهای حالت بخوانند و بنویسند. اما فقط متغیرهای حالت ContractA خوانده یا نوشته می شوند.
delegatecall بر متغیرهای حالت قرارداد که تابعی را با delegatecall فراخوانی می کند، تأثیر می گذارد. متغیرهای حالت قرارداد که دارای توابع وام گرفته شده هستند خوانده یا نوشته نمی شوند.
مثال
بیایید به یک مثال ساده نگاه کنیم.
ContractA دارای موارد زیر است:
address(this) == 0x2791bca1f2de4661ed88a30c99a7a9449aa84174
متغیر حالت «string tokenName» دارای مقدار «FunToken» است.
یک تابع خارجی به نام “initialize()” که تابع “setTokenName(رشته calldata _newName)” را در ContractB با delegatecall فراخوانی می کند.
ContractB دارای موارد زیر است:
address(this) == 0x6b175474e89094c44da98b954eedeac495271d0f
متغیر حالت «string tokenName» دارای مقدار «BoringToken» است.
یک تابع خارجی به نام «setTokenName(رشته calldata _newName)» که متغیر حالت «tokenName» را روی مقدار «_newName» تنظیم می کند.
وقتی تابع “initialize()” در ContractA با 2 ETH فراخوانی می شود، این اتفاق می افتد:
این مقادیر تنظیم می شوند:
address(this) == 0x2791bca1f2de4661ed88a30c99a7a9449aa84174
msg.sender == 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B
msg.value == 2 ETH
مقدار متغیر حالت «string tokenName» در ContractA تغییر کرده است. در ContractB تغییر نمی کند، حتی اگر کد از ContractB آمده است.
آدرسها در Solidity دارای یک متد «delegatecall» هستند که به شما امکان میدهد تماس delegate را اجرا کنید. این متد delegatecall یک متغیر وضعیت بولی را برمیگرداند که به شما میگوید آیا فراخوانی تابع برگردانده شده است یا خیر. تابع delegatecall مقدار دوم را برمی گرداند که هر مقدار بازگشتی از فراخوانی تابع است. نمونه کد بالا را ببینید.
Table of contents [Show]
اکنون که فهمیدید delegatecall چگونه کار می کند، بیایید نحوه استفاده ایمن از آن را بررسی کنیم.
کد نامعتبر را با delegatecall اجرا نکنید زیرا می تواند به طور مخرب متغیرهای حالت را تغییر دهد یا “selfdestruct” را فراخوانی کند تا قرارداد فراخوان را از بین ببرد. از مجوزها یا احراز هویت یا نوع دیگری از کنترل برای تعیین یا تغییر عملکردها و قراردادهایی که با delegatecall اجرا می شوند، استفاده کنید.
Delegatecall اگر در آدرسی فراخوانی شود که قراردادی نیست و بنابراین کدی ندارد، مقدار “True” را برای مقدار وضعیت برمی گرداند. اگر کد انتظار داشته باشد که توابع فراخوانی delegate در زمانی که نمی توانند اجرا شوند، “False” را برگردانند، می تواند باعث ایجاد اشکال شود.
اگر مطمئن نیستید که متغیر آدرس همیشه آدرسی را نگه میدارد که دارای کد است و از delegatecall روی آن استفاده شده است، قبل از استفاده از delegatecall روی آن، بررسی کنید که هر آدرسی از متغیر دارای کد باشد و اگر کد نداشت، آن را برگردانید. در اینجا نمونه ای از کد است که بررسی می کند آیا یک آدرس دارای کد است:
Solidity داده ها را در قراردادها با استفاده از فضای آدرس عددی ذخیره می کند. متغیر حالت اول در موقعیت 0، متغیر حالت بعدی در موقعیت 1، متغیر حالت بعدی در موقعیت 2 و غیره ذخیره می شود.
قرارداد و تابعی که با delegatecall اجرا میشود، فضای آدرس متغیر حالت یکسانی را با قرارداد فراخوانی به اشتراک میگذارد، زیرا توابع فراخوانی شده با delegatecall، متغیرهای وضعیت قرارداد فراخوان را میخوانند و مینویسند.
بنابراین یک قرارداد و تابعی که با delegatecall فراخوانی میشود باید طرح متغیر حالت یکسانی را برای مکانهای متغیر حالتی که خوانده و نوشته میشوند، داشته باشد. داشتن چیدمان متغیر حالت یکسان به این معنی است که متغیرهای حالت یکسان در هر دو قرارداد به یک ترتیب اعلام می شوند.
اگر قراردادی که delegatecall را فراخوانی میکند و قرارداد با توابع قرضگرفتهشده، طرحبندی متغیر حالت یکسانی نداشته باشد و آنها در مکانهای مشابهی در ذخیرهسازی قرارداد بخوانند یا بنویسند، متغیرهای حالت یکدیگر را بازنویسی میکنند یا به اشتباه تفسیر میکنند.
به عنوان مثال، فرض کنید که یک ContractA متغیرهای حالت را “uint first;” و “bytes32 second;” و ContractB متغیرهای حالت را “uint first;” و “string name;” اعلام می کند. آنها متغیرهای حالت متفاوتی در موقعیت 1 دارند (“bytes32 second” و “string name”) در ذخیره سازی قرارداد و بنابراین در صورت استفاده از delegatecall بین آنها، داده های اشتباه را در موقعیت 1 بین آنها می نویسند و می خوانند.
مدیریت طرح متغیر حالت قراردادهایی که توابع را با delegatecall فراخوانی میکنند و قراردادهایی که با delegatecall اجرا میشوند در عمل زمانی که از یک استراتژی برای انجام آن استفاده میشود، سخت نیست. در اینجا چند استراتژی شناخته شده است که با موفقیت در تولید استفاده شده است:
یک استراتژی ایجاد قراردادی است که همه متغیرهای حالت استفاده شده توسط همه قراردادهایی را که فضای اضافی ذخیرهسازی قرارداد را به اشتراک میگذارند، اعلام میکند، زیرا از callcall بین آنها استفاده میکنند. می توان آن را “ذخیره سازی” یا چیزی دیگر نامید. سپس میتواند توسط هر قراردادی که فضای آدرس ذخیرهسازی یکسانی دارد به ارث برسد. این استراتژی کار می کند اما محدودیت هایی دارد و به نظر من استراتژی مشابه اما بهتری پیدا کرده ام.
محدودیتی که Inherited Storage دارد این است که از قابل استفاده مجدد قراردادها جلوگیری می کند. اگر قراردادی را مستقر میکنید که از فضای ذخیرهسازی ارثی استفاده میکند، احتمالاً نمیتوانید از آن قرارداد مستقر شده با قراردادهای مختلف که متغیرهای حالت متفاوتی دارند هنگام استفاده از callcall استفاده مجدد کنید.
یکی دیگر از محدودیتها، به نظر من، این است که خیلی آسان است که به طور تصادفی چیزی مانند یک تابع داخلی یا متغیر محلی را به همان نام متغیر حالت نامگذاری کنید و یک نام تداخل داشته باشد. اما میتوان با استفاده از قراردادهای نامگذاری رمزی که از چنین تداخل نامهایی جلوگیری میکند، بر این مشکل غلبه کرد.
قراردادهایی که از callcall بین آنها استفاده میکنند، در واقع اگر دادهها را در مکانهای مختلف ذخیره میکنند، مجبور نیستند متغیرهای حالت یکسان را به همان ترتیب اعلام کنند.
همانطور که قبلا ذکر شد، Solidity به طور خودکار متغیرهای حالت را در مکان های ذخیره سازی ذخیره می کند که از 0 شروع می شود و یک افزایش می یابد. اما ما مجبور نیستیم از مکانیسم چیدمان ذخیره سازی پیش فرض Solidity استفاده کنیم. ما مجبور نیستیم دادههایی را که از مکان 0 شروع میشود ذخیره کنیم. میتوانیم تعیین کنیم که ذخیره دادهها از کجا شروع شود در فضای آدرس. برای قراردادهای مختلف میتوانیم مکانهای مختلفی را برای شروع ذخیرهسازی دادهها مشخص کنیم، بنابراین از برخورد قراردادهای مختلف با متغیرهای حالت مختلف با مکانهای ذخیرهسازی جلوگیری میکنیم. این کاری است که Diamond Storage انجام می دهد.
ما می توانیم یک رشته منحصر به فرد را هش کنیم تا موقعیت ذخیره سازی تصادفی به دست آوریم و یک ساختار را در آنجا ذخیره کنیم. ساختار می تواند شامل تمام متغیرهای حالت مورد نظر ما باشد. رشته منحصر به فرد می تواند مانند یک فضای نام برای عملکرد خاص عمل کند.
به عنوان مثال می توانیم قرارداد ERC721 را پیاده سازی کنیم. این قرارداد می تواند ساختاری به نام “ERC721Storage” را در موقعیت “keccak256(“com.myproject.erc721”) ذخیره کند. ساختار می تواند شامل تمام متغیرهای حالت مربوط به عملکرد ERC721 باشد که قرارداد ERC721 می خواند و می نویسد. چند مزیت خوب برای این وجود دارد. یکی اینکه قرارداد ERC721 قابل استفاده مجدد است. قرارداد ERC721 را می توان تنها یک بار اجرا کرد، و قرارداد ERC721 مستقر را می توان با چندین قرارداد مختلف که از delegatecall با آن استفاده می کنند و از متغیرهای حالت مختلف استفاده می کنند استفاده کرد. چیز خوب دیگر این است که قرارداد ERC721 با اعلانهای متغیر حالت متغیرهایی که استفاده نمیکند مملو نیست.
یکی دیگر از مزیتهای خوب Diamond Storage این است که امکان دسترسی عملکردهای داخلی کتابخانههای Solidity به Diamond Storage مانند هر تابع قرارداد معمولی وجود دارد. من یک پست وبلاگی در مورد استفاده از کتابخانه های Solidity با Diamond Storage در اینجا نوشتم: کتابخانه های Solidity نمی توانند متغیرهای حالت داشته باشند — اوه بله آنها می توانند!
برای کسب اطلاعات بیشتر در مورد ذخیره سازی الماس و یک مثال کد، به این پست وبلاگ مراجعه کنید: نحوه ذخیره الماس چگونه کار می کند. من همچنین خواندن درک الماس در اتریوم را توصیه می کنم.
AppStorage شبیه به Storage ارثی است اما مشکل تداخل نام را حل میکند، جایی که نامگذاری تصادفی چیزی مانند یک تابع داخلی یا متغیر محلی به همان نام متغیر حالت بسیار آسان است. این ممکن است یک موضوع بی اهمیت به نظر برسد، اما من در عمل متوجه شدم که بسیار خوب است زیرا AppStorage همچنین کد را به گونه ای متمایز می کند که اسکن و خواندن آن را آسان تر می کند. اگر به خوانایی کد اهمیت می دهید، AppStorage را دوست خواهید داشت.
AppStorage یک قرارداد نامگذاری یا دسترسی را اعمال می کند که تداخل نام متغیرهای حالت را با چیز دیگری غیرممکن می کند.
ساختاری به نام AppStorage در فایل Solidity نوشته شده است.
ساختار AppStorage شامل متغیرهای حالت است که بین قراردادها به اشتراک گذاشته می شود. برای استفاده از آن، یک قرارداد ساختار AppStorage را وارد میکند و “AppStorage داخلی s;” را به عنوان اولین و تنها متغیر حالت در قرارداد اعلام میکند. سپس قرارداد به تمام متغیرهای حالت در توابع از طریق ساختاری مانند این دسترسی پیدا می کند: `s.myFirstVariable`, `s.mySecondVariable` و غیره. در اینجا یک مثال آورده شده است:
مهم است که “AppStorage داخلی ” به عنوان اولین و تنها متغیر حالت در تمام قراردادهایی که از آن استفاده می کنند، اعلام شود. که آن را در موقعیت 0 در فضای آدرس ذخیره سازی قرار می دهد. بنابراین اگر همه قراردادها آن را به عنوان اولین و تنها متغیر حالت اعلام کنند، داده های ذخیره سازی بین قراردادهایی که از delegatecall استفاده می کنند به درستی ردیف می شوند. متغیرهای حالت را مستقیماً به قرارداد اضافه نکنید زیرا با متغیرهای وضعیت اعلام شده در ساختار AppStorage در تضاد است. برای اضافه کردن متغیرهای حالت بیشتر، آنها را به انتهای ساختار AppStorage اضافه کنید یا از Diamond Storage استفاده کنید.
استفاده از AppStorage نسبت به Diamond Storage راحتتر است، زیرا در هر عملکرد، Diamond Storage نیاز به گرفتن اشارهگر به یک ساختار دارد، در حالی که با AppStorage، نشانگر ساختار «s» بهطور خودکار در طول قرارداد در دسترس است.
مزیت دیگری که AppStorage نسبت به Inherited Storage دارد این است که AppStorage می تواند توسط کتابخانه های Solidity به همان روشی که Diamond Storage می تواند دسترسی داشته باشد. یک ساختار AppStorage همیشه در مکان 0 ذخیره میشود، بنابراین توابع داخلی در کتابخانههای Solidity میتوانند از آن برای مقداردهی اولیه نشانگر ذخیرهسازی «s» برای اشاره به ساختار AppStorage استفاده کنند. در اینجا یک نمونه از آن است:
همانطور که در تابع myLibraryFunction2 در بالا مشاهده می شود، اشاره گر ذخیره سازی به ساختار AppStorage نیز می تواند به عنوان آرگومان به توابع کتابخانه منتقل شود.
AppStorage را می توان با وراثت قراردادی استفاده کرد. این کار با اعلام “AppStorage داخلی s” در یک قرارداد انجام می شود. سپس تمام قراردادهایی که از AppStorage استفاده می کنند، آن قرارداد را به ارث می برند.
AppStorage مخصوصاً برای قراردادهای خاص برنامه یا پروژه که با سایر پروژهها یا قراردادهایی که از AppStorage یا Inherited Storage نیز استفاده میکنند، استفاده مجدد نمیشود. AppStorage را می توان با Diamond Storage در همان قرارداد استفاده کرد.
AppStorage همچنین در قراردادهای هوشمندی که از تماس نمایندگی استفاده نمیکنند مفید است، زیرا کد را خواناتر میکند و از برخورد نامها جلوگیری میکند.
برای اطلاعات بیشتر، این پست وبلاگ را درباره AppStorage بررسی کنید: الگوی AppStorage برای متغیرهای حالت در Solidity
در اینجا معماری قراردادهای هوشمندی وجود دارد که از delegatecall استفاده می کنند:
یک قرارداد پراکسی از delegatecall برای واگذاری فراخوانی های تابع خارجی به یک قرارداد پیاده سازی استفاده می کند. قراردادهای پروکسی برای اجرای قراردادهای قابل ارتقا استفاده می شود. برای ارتقای قرارداد پروکسی به یک قرارداد اجرایی متفاوت اشاره شده است.
نسخههای مختلف قراردادهای پیادهسازی که توسط یک قرارداد پراکسی استفاده میشوند، باید از یک استراتژی برای مدیریت طرحبندی متغیر حالت استفاده کنند. در غیر این صورت پیاده سازی های مختلف می توانند داده ها را در مکان های ذخیره سازی اشتباه بخوانند و بنویسند. قراردادهای پیادهسازی میتوانند از ذخیرهسازی ارثی یا ذخیرهسازی الماس یا AppStorage استفاده کنند.
OpenZeppelin از قراردادهای پروکسی پشتیبانی می کند.
کتابخانه قرارداد هوشمند SolidState از قراردادهای پروکسی و قراردادهای پیاده سازی که از Diamond Storage استفاده می کنند، پشتیبانی می کند.
الماس EIP-2535 استانداردی است که از ساخت سیستمهای قرارداد هوشمند مدولار پشتیبانی میکند که میتوانند در تولید گسترش پیدا کنند.
الماس یک قرارداد پروکسی است که چندین قرارداد اجرایی دارد. درباره الماس از استاندارد و این مقدمه بیشتر بیاموزید: مقدمه ای بر استاندارد الماس، الماس EIP-2535
قراردادهای پیاده سازی الماس ها که به آنها وجه گفته می شود، می توانند از ذخیره سازی ارثی، ذخیره سازی الماس و AppStorage استفاده کنند.
چندین پیاده سازی مرجع، که ممیزی شده اند، برای شروع کار با الماس وجود دارد. اینجا را ببینید: پیاده سازی مرجع الماس.
کتابخانه قراردادهای هوشمند جامد جامد از الماس پشتیبانی می کند.
پلاگین hardhat-deploy از استقرار و ارتقاء الماس پشتیبانی می کند.
کتابخانه های Solidity یک معماری قرارداد هوشمند نیستند، مانند پروکسی ها و الماس ها. آنها ابزار و بخشی از زبان Solidity هستند.
از مستندات Solidity:
کتابخانه ها شبیه قراردادها هستند، اما هدف آنها این است که فقط یک بار در یک آدرس خاص مستقر می شوند و کد آنها با استفاده از ویژگی DELEGATECALL EVM دوباره استفاده می شود.
توابع خارجی کتابخانه های Solidity با استفاده از delegatecall اجرا می شوند.
کتابخانه های Solidity می توانند با استفاده از نشانگرهای ذخیره سازی به عنوان پارامترهای توابع به متغیرهای حالت دسترسی داشته باشند. کتابخانه های Solidity همچنین می توانند به Diamond Storage و AppStorage دسترسی داشته باشند و از آن استفاده کنند. در اینجا مقالهای وجود دارد که نشان میدهد چگونه کتابخانههای Solidity میتوانند از Diamond Storage استفاده کنند: کتابخانههای Solidity نمیتوانند متغیرهای حالت داشته باشند – اوه بله، آنها میتوانند!
منبع:
https://eip2535diamonds.substack.com/p/understanding-delegatecall-and-how