create2 و آسیب پذیری قراردادها در اتریوم

بحث‌های زیادی در مورد آسیب‌‌پذیری احتمالی اتریوم به واسطه‌‌ کدهای جدید، که create2 نام دارند و مکانیزم جدیدی برای ساخت قراردادها ایجاد می‌‌کنند، مطرح شده است. Deviate Fish در این مقاله که در medium.com منتشر کرده است، به بررسی این موضوع و تفاوت بین create و create2 و اینکه چرا باید از این کدهای جدید استفاده کرد، می‌پردازد.

0 104

چند روز پیش، به ردیت اتریوم مراجعه کردم و آن جا مطالبی در مورد آسیب‌‌پذیری احتمالی اتریوم به واسطه‌‌ی کدهای جدید، خواندم. این کدها، create2 نام دارند و مکانیزم جدیدی برای ساخت قراردادها ایجاد می‌‌کنند. آن‌‌ها به جای ساخت یک قرارداد در آدرسی که سازنده‌‌ی قرارداد آن را ذکر کرده است، قرارداد را در یک address و salt (یک مقدار دلخواه) که در init_code موجودند، می‌‌سازد. این کار به شکل زیر انجام می‌‌شود:

[keccak256(0xff ++ address ++ salt ++ keccak256(init_code))[12:

نکته: معلوم نیست این address همان آدرسی باشد که فرستنده‌‌ی قرارداد گفته بود. احتمالا یک آدرس دلخواه است ولی نمی‌‌توان با قطعیت در این باره حرف زد.

این کد می‌‌خواهد آدرس قرارداد را قبل از انعقاد آن مشخص کند تا قرارداد تنها در صورت لزوم انجام شود. فکر می‌‌کنم به این ایده “حمایت احتمالی” می‌‌گویند.

این ایده خیلی خوب به نظر می‌‌رسد ولی create هم همین کار را می‌‌تواند انجام دهد. من این نکته را چند بار بیان کرده ام و فورا توسط یکی از ارائه‌‌دهندگان این روش، متهم شده ام.

جف کولمن، یکی از این افراد است که به شدت به کدهای خود اعتقاد دارد. او ادعا کرد که کد create نمی‌‌توانست قبل از عقد قرارداد، به یک کد مشخص در یک آدرس مشخص متعهد شود. من به این موضوع علاقمند شدم و از چند الگو استفاده کردم. نتیجه‌‌ای که گرفتم این بود که نه تنها با create این امکان وجود دارد، بلکه خیلی ساده‌‌تر هم این کار را می‌‌کند.

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

این‌‌ها اصول الگویی هستند و مانند کدهای کارخانه کار می‌‌کنند با این تفاوت که این الگو قبل از انقضا، تنها یک Machine تولید می‌‌کند.

با استفاده از آدرس‌‌هایی که create درست کرده، می‌‌توانیم commit را فراخوانی کرده و آدرس Machine خود پس از انعقاد قرارداد را به آن بدهیم. وقتی MachineCommitment ساخته شد، مطمئن می‌‌شویم که Machine تنها در آدرسی که MachineCommitment ساخته است قرار دارد.

این کار را در Solidity نیز می‌‌توانیم انجام دهیم:

(address(keccak256(0xd6, 0x94, address(commitment), 0x01);

این الگو به تنهایی کافی نیست. این کد نسبت به ساخت یک Machine جدید، کارمزد بیشتری را برای اجرا نیاز دارد. زیرا MachineCommitment باید کد Machine را بخواند تا بتواند آن را اجرا کند.

می‌‌توانیم این مشکل را با الگوهای کارخانه حل کنیم. ولی ابتدا باید این مساله را به مولفه‌‌های آن تقسیم کنیم: تعهد به اجرای یک سری کد و تعهد به اجرای یک سری آدرس.

نتیجه‌‌ای که گرفته ام، با الگویی که در بالا ارائه دادم، انطباق دارد.

از آن جایی که این الگوی به خصوص در ethfiddle به درستی کار نمی‌‌کند، هنوز مشکلاتی داریم. ولی در Remix این مشکلات وجود ندارد.

با اجرای کدهای کارخانه، به ساخت یک سری کد مشخص متعهد می‌‌شویم. با اجرای تعهد (Commitment) و ارائه‌‌ی آدرس کارخانه به آن، تعهد را نسبت به یک آدرس مشخص برقرار کرده ایم. این همان تعهد به یک کد مشخص در یک آدرس مشخص است؛ ولی به شکل قابل استفاده‌‌ی مجدد و قابل انتقال.

این الگو به طور دقیق‌‌تر می‌‌خواهد در مورد ادعای زیر تحقیق کند:

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

الگوی بالا دقیقا این کار را می‌‌کند: این الگو به انعقاد کدهای مشخصی که به آدرسی مشخص متعهد می‌‌شود قبل از اینکه به خود کد دسترسی داشته باشد. الگوی ما به “هک تراکنش‌‌های چندگانه” و “امضاهای رمزنگاری‌‌نشده” یا “پرداخت پیش از موعد یک کارمزد مشخص” نیاز ندارد. تنها با تغییراتی ساده می‌‌توان کاری کرد که الگو از اشخاص غیر قابل اعتماد و دلخواه، به عنوان اجراکننده‌‌ی کد، حمایت کند. قبل از تعهد اولیه، هیچ فرضیاتی در مورد تقابل بین آدرس‌‌ها وجود نداشته است.

ولی نتیجه‌‌ای که به دست آوردیم امکان‌‌پذیری این الگو را نشان نمی‌‌داد. بنابراین جنبه‌‌های مختلف آن را به چالش کشیدیم.

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

انتقاد دوم این است که ساختار الگو قابل بحث است. به دلیل اتصال تنگاتنگ کد به commitment، با اعمال تغییرات کوچکی این مشکل حل می‌‌شود.

انتقاد سوم این است که برای هر قرارداد به یک کارخانه مجزا نیاز است. درست است ولی این تنها یک الگوی نادر است. ما می‌‌توانیم آن را به روش‌‌های مختلفی اجرا کنیم ولی ساده‌‌ترین راه این است که الگوی پروکسی معروفی را قرض بگیریم تا یک تعهد جامع بسازیم:

همان طور که می‌‌بینید، تعهد از کد جدا شده است. کارخانه یک آدرس کلی است و تابع مربوط به آن، تمام داده‌‌ها را در کارخانه فراخوانی می‌‌کند و روش‌‌های دلخواه را در کارخانه فراخوانی می‌‌کند. در این مثال مشخص می‌‌توانید از مقدار بازگشتی از getSig در یک factory به عنوان calldata استفاده کنید تا تابع خود را به commitment فراخوانی کنید.

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

برای دستیابی به هدفمان، یعنی تعهد به کدهای دلخواه، بیایید همه‌‌ی این تکه‌‌ها را کنار هم بگذاریم:

MachineCommitment یک تعهد است که با کمک قرارداد جامع Deployer کدهای مشخصی را اجرا می‌‌کند. می‌‌توانیم با init_code از اجرای کدها مطمئن شویم و این همان تعهدی است که از ابتدا داشتیم. از دو چیز دیگر باید مطمئن شویم: تعهدی که به یک آدرس مشخص داریم و تعهدی که به یک کد مشخص داریم.

اوضاع می‌‌تواند از این بهتر هم شود. ما اصلا به یک Deployer نیاز نداریم. به این ترتیب کارمزد کاهش می‌‌یابد. روش delegatecall که در بالا گفتیم این کار را می‌‌کند ولی این ویژگی واقعا مورد نیاز نیست و کار را پیچیده می‌‌کند.

به جای آن، می‌‌توانیم قراردادی بسازیم که مثل contract2 باشد. این کار ساده است، ببینید:

اگر بخواهید این روش را در محیطی مناسب‌‌تر امتحان کنید، کدهایتان را در Remix IDE اجرا کنید. ماشین خودتان را اجرا کنید تا مقدار مورد نیاز کدهای init_code به دست بیاید. با اجرای تعهدهای مختلفِ create2 این محیط را بیازمایید. اگر می‌‌خواهید init_code متفاوتی را اجرا کنید، لازم است codeHash را خودتان محاسبه کنید.

مطالبی که در صورت آپدیت مورد نیاز باشد را نیز اینجا آورده ایم.

در حال حاضر، فاصله‌‌ی بین توانایی‌‌های create و create2 کاهش یافته است. در واقع تنها تفاوت بین این دو این است که باید تعهدهای create2 را به طور ONChain تهیه کنیم.

حالا همه چیز را در مورد ساخت یک تعهد به یک کد مشخص در یک آدرس مشخص را دریافته ایم. وقت آن رسیده که کاربردهای آن را بدانیم.

بخش آخری که می‌‌خواستم ارائه دهم در واقع هیچ فایده و کاربرد‌‌ی ندارد. در واقع فکر می‌‌کنم نهایتا باید از بعضی از نسخه‌‌های اولیه‌‌ی این الگو را در دنیای واقعی به کار ببریم. زیرا ما واقعا می‌‌خواهیم به یک کد مشخص و معتبر تعهد داشته باشیم پس نیازی به تغییرات کلی نداریم.

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

تفاوت آشکاری که بین این دو وجود دارد این است که create2 بدون تراکنش‌‌های درون زنجیره به یک کد مشخص در یک آدرس مشخص متعهد می‌‌شود (که خیلی عالی است) ولی این بیشتر یک جور بهینه‌‌سازی است نه بیشتر.

ارسال پاسخ

آدرس ایمیل شما منتشر نخواهد شد.