EIP-1167 در سالیدیتی که آن را بهعنوان “قرارداد پراکسی مینیمال” نیز میشناسند، یکی از الگوهای پرکاربرد در زبان سالیدیتی است. توسعه دهندگان از این الگو برای ایجاد نسخه های مشابه از یک قرارداد با هزینه بسیار پایین استفاده میکنند.
اگر در پروژهای لازم باشد که یک قرارداد یکسان یا بسیار مشابه چند بار روی شبکه مستقر شود، استفاده از این الگو روشی بهینه و کمهزینه خواهد بود.
برای مثال، پلتفرم Gnosis Safe هنگام ایجاد کیف پول جدید از الگوی clone استفاده میکند. زمانی که کاربران با Gnosis Safe کار میکنند، در واقع با یک نسخهی clone شده تعامل دارند.
قرارداد clone نقش یک پراکسی را ایفا میکند، با این تفاوت که نمیتوان آن را ارتقاء داد. چون اندازه این پراکسی ها بسیار کوچکتر از قرارداد اصلی است، هزینه استقرار آنها نیز بسیار کمتر خواهد بود.
در این الگو، clone ها مانند پراکسی های معمولی تمام فراخوانی ها را به قرارداد اصلی منتقل میکنند. با این حال، هر clone داده های وضعیت خود را در فضای ذخیره سازی اختصاصی نگه میدارد.
برخلاف الگوی پراکسی معمول، در الگوی clone میتوان چندین نسخه را به یک قرارداد پیاده سازی واحد متصل کرد. البته این نسخه ها قابلیت ارتقاء ندارند.
آدرس قرارداد اصلی در داخل bytecode ذخیره میشود. این کار مصرف گس را کاهش میدهد و همچنین مانع از تغییر مسیر clone به قراردادهای دیگر میشود.
طراحی این الگو هزینه استقرار را بهطور چشمگیری کاهش میدهد، زیرا bytecode مربوط به clone بسیار کوچکتر از قرارداد اصلی است. در واقع، استاندارد EIP-1167 تنها 55 بایت حجم دارد که 45 بایت آن به زمان اجرا مربوط میشود. این مقدار شامل کد راهاندازی اولیه نیز هست.
البته در زمان اجرا، هر بار فراخوانی هزینه بیشتری دارد، چون همیشه یک delegatecall
اضافی اجرا میشود.
در ادامه این مقاله، هم خود استاندارد EIP-1167 و هم نحوه استفاده از تابع مقداردهی اولیه برای جایگزینی پارامترهای سازنده (constructor) را توضیح میدهیم.
EIP-1167 در سالیدیتی چگونه کار میکند؟
در EIP-1167، قرارداد clone دقیقاً مانند یک پراکسی معمولی عمل میکند. ابتدا دادههای تراکنش را از طریق فراخوانی (call
) دریافت میکند. سپس این دادهها را به قرارداد پیادهسازی (implementation contract) منتقل میکند. اگر فراخوانی خارجی با موفقیت انجام شود، نتیجه را دریافت کرده و به کاربر بازمیگرداند. اما اگر در این فرآیند خطایی رخ دهد، تراکنش را با خطا (revert) متوقف میکند.
بایتکد قرارداد پراکسی مینیمال
قرارداد پراکسی مینیمال تنها ۵۵ بایت بایتکد دارد که بسیار فشرده و بهینه است. این بایتکد از بخشهای زیر تشکیل شده است:
-
کد اولیه استقرار (init code)
-
کد زمان اجرا (runtime code) که شامل دستوراتی برای دریافت داده های تراکنش (calldata) است
-
آدرس ۲۰ بایتی قرارداد پیاده سازی
-
دستور اجرای delegatecall
-
و در نهایت دستور بازگرداندن نتیجه یا توقف اجرا در صورت بروز خطا (revert)
در زیر بایتکد کامل این پراکسی مینیمال را مشاهده می کنید:
1 |
3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3 |
در این بایتکد، آدرس فرضی 0xbebebebebebebebebebebebebebebebebebebebe
به عنوان نمونه درج شده و هنگام ساخت واقعی قرارداد، با آدرس قرارداد پیاده سازی جایگزین می شود.
ما در ادامه، این بایتکد را بخش به بخش تحلیل می کنیم تا بهتر با ساختار آن آشنا شوید.
بخش init code
۱۰ بایت ابتدایی بایت کد به بخش init code اختصاص دارد. این بخش تنها یک بار اجرا می شود و وظیفه آن استقرار قرارداد پراکسی مینیمال بر بستر شبکه بلاکچین است.
در ادامه، مجموعه دستوراتی را بررسی خواهیم کرد که ماشین مجازی اتریوم (EVM) در این بخش اجرا می کند.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// copy the runtime bytecode of the minimal proxy // starting from offset 10, and save it to the blockchain [00] RETURNDATASIZE [01] PUSH1 2d [03] DUP1 //push 10 - offset to copy runtime code from [04] PUSH1 0a [06] RETURNDATASIZE // copy the runtime code and save it to the blockchain [07] CODECOPY [08] DUP2 [09] RETURN |
کپی کردن calldata
کد init در زمان استقرار قرارداد، وظیفه دارد بایت کد زمان اجرا (runtime bytecode) را از محل مشخصی در حافظه (از آفست 10 به بعد) کپی کرده و روی بلاکچین ذخیره کند. این بخش از بایت کد، همان قسمت مربوط به کپی calldata است.
پس از استقرار کامل یک پراکسی مینیمال، زمانی که تراکنشی به آن ارسال شود، ابتدا داده های تراکنش (calldata) را در حافظه کپی می کند. سپس آدرس 20 بایتی قرارداد پیاده سازی را در استک قرار می دهد و با استفاده از دستور delegatecall، کنترل را به قرارداد پیاده سازی منتقل می کند.
عملیات کپی کردن calldata با استفاده از مجموعه دستورات مشخصی در EVM انجام می شود که در ادامه به آن ها اشاره خواهیم کرد.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//copy the transaction calldata to memory [0a] CALLDATASIZE [0b] RETURNDATASIZE // this is a hack to push 0 onto the stack with less gas than doing PUSH 0 [0c] RETURNDATASIZE [0d] CALLDATACOPY [0e] RETURNDATASIZE [0f] RETURNDATASIZE [10] RETURNDATASIZE [11] CALLDATASIZE [12] RETURNDATASIZE //pushes the 20 bytes address of the implementation contract [13] PUSH20 |
آدرس قرارداد پیاده سازی
پس از آنکه داده های تراکنش (calldata) در حافظه کپی شد، ماشین مجازی اتریوم (EVM) استک را برای اجرای دستور delegatecall
آماده می کند. در این مرحله، آدرس ۲۰ بایتی قرارداد پیاده سازی در بالای استک قرار می گیرد.
همان طور که در بخش قبل اشاره شد، بایت کد با دستور PUSH20
پایان می یابد. بلافاصله پس از این دستور، آدرس قرارداد پیاده سازی قرار دارد که پراکسی تمام فراخوانی ها را به آن منتقل می کند.
1 2 |
//push the address of the implementation contract to the stack. The address here is just a dummy address [13] PUSH20 bebebebebebebebebebebebebebebebebebebebe |
بخش delegatecall
پس از آنکه داده های تراکنش (calldata) در حافظه کپی شد و آدرس قرارداد پیاده سازی در بالای استک قرار گرفت، پراکسی مینیمال آماده اجرای دستور delegatecall
به قرارداد پیاده سازی می شود.
اگر نیاز دارید تا عملکرد دقیق delegatecall را مرور کنید، می توانید به آموزش اختصاصی ما درباره این دستور مراجعه کنید.
زمانی که دستور delegatecall
اجرا شود، در صورتی که فراخوانی با موفقیت انجام شود، پراکسی مینیمال نتیجه آن را بازمی گرداند. اما اگر در جریان اجرا خطایی رخ دهد، تراکنش را متوقف کرده و عملیات را revert می کند.
دستورات اجرایی (opcodes) مربوط به بخش delegatecall در ادامه معرفی خواهند شد.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
//perform a delegate call on the implementation contract, and forward all available gas [28] GAS [29] DELEGATECALL //copy the return data of the call to memory [2a] RETURNDATASIZE [2b] DUP3 [2c] DUP1 [2d] RETURNDATACOPY // set up the stack for the conditional jump [2e] SWAP1 [2f] RETURNDATASIZE [30] SWAP2 [31] PUSH1 2b //jump to line 35 and return the result of the call if it was successful, else revert on line 34 [33] JUMPI [34] REVERT [35] JUMPDEST [36] RETURN |
این بخش، نمای کلی از استاندارد EIP-1167 در سالیدیتی و نحوه عملکرد آن را ارائه می دهد.
برای درک بهتر، تصور کنید که قرارداد پیاده سازی یک توکن ERC20 باشد. در این صورت، نسخه clone شده دقیقاً همانند یک توکن ERC20 رفتار خواهد کرد و تمامی توابع و ویژگی های آن را به همان شکل ارائه می دهد.
پیاده سازی قرارداد هوشمند EIP-1167 همراه با مقداردهی اولیه (Initialization)
در برخی موارد، هنگام ایجاد یک clone نیاز داریم که برخی پارامترها را در لحظه استقرار تنظیم کنیم. برای نمونه، اگر در حال clone گرفتن از یک توکن ERC20 باشیم، تمام نسخه های clone شده دارای totalSupply یکسان خواهند بود که در بسیاری از سناریوها مطلوب نیست.
برای حل این مشکل، می توان از الگوی «clone همراه با مقداردهی اولیه (initialization)» استفاده کرد.
در ادامه می بینیم که چگونه می توان با استفاده از استاندارد EIP-1167، نسخه هایی از یک قرارداد را clone کرده و در لحظه استقرار، پارامترهای اولیه مورد نیاز را تنظیم کرد. این فرآیند مراحل ساده ای دارد:
-
ابتدا قرارداد پیاده سازی (implementation contract) را ایجاد می کنیم.
-
سپس با استفاده از استاندارد EIP-1167 از آن clone می گیریم.
-
در مرحله بعد، clone را مستقر کرده و تابع مقداردهی اولیه (initialization function) را فراخوانی می کنیم. این تابع فقط یک بار قابل اجرا خواهد بود.
محدود بودن این تابع به یک بار اجرا ضروری است. در غیر این صورت، ممکن است پس از استقرار، شخصی مقدار یک پارامتر حیاتی مثل totalSupply را تغییر دهد.
در ادامه این مراحل را با یک مثال بررسی می کنیم.
قرارداد پیاده سازی که قرار است clone شود:
1 2 3 4 5 6 7 8 9 |
contractImplementationContract{ boolprivate isInitialized; //initializer function that will be called once, during deployment. functioninitializer() external { require(!isInitialized); isInitialized =true; } // rest of the implementation functions go here } |
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 |
contract MinimalProxyFactory { address[] public proxies; function deployClone(address _implementationContract) external returns (address) { // convert the address to 20 bytes bytes20 implementationContractInBytes = bytes20(_implementationContract); //address to assign a cloned proxy address proxy; // as stated earlier, the minimal proxy has this bytecode // <3d602d80600a3d3981f3363d3d373d3d3d363d73><address of implementation contract><5af43d82803e903d91602b57fd5bf3> // <3d602d80600a3d3981f3> == creation code which copies runtime code into memory and deploys it // <363d3d373d3d3d363d73> <address of implementation contract> <5af43d82803e903d91602b57fd5bf3> == runtime code that makes a delegatecall to the implentation contract assembly { /* reads the 32 bytes of memory starting at the pointer stored in 0x40 In solidity, the 0x40 slot in memory is special: it contains the "free memory pointer" which points to the end of the currently allocated memory. */ let clone := mload(0x40) // store 32 bytes to memory starting at "clone" mstore( clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000 ) /* | 20 bytes | 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000 ^ pointer */ // store 32 bytes to memory starting at "clone" + 20 bytes // 0x14 = 20 mstore(add(clone, 0x14), implementationContractInBytes) /* | 20 bytes | 20 bytes | 0x3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe ^ pointer */ // store 32 bytes to memory starting at "clone" + 40 bytes // 0x28 = 40 mstore( add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000 ) /* | 20 bytes | 20 bytes | 15 bytes | 0x3d602d80600a3d3981f3363d3d373d3d3d363d73b<implementationContractInBytes>5af43d82803e903d91602b57fd5bf3 == 45 bytes in total */ // create a new contract // send 0 Ether // code starts at the pointer stored in "clone" // code size == 0x37 (55 bytes) proxy := create(0, clone, 0x37) } // Call initialization ImplementationContract(proxy).initializer(); proxies.push(proxy); return proxy; } } |
با استفاده از قرارداد MinimalProxyFactory، می توان تعداد نامحدودی clone بر اساس استاندارد EIP-1167 در سالیدیتی ایجاد کرد. با این حال، در این مثال صرفاً قصد داریم همان قرارداد پیاده سازی که پیش از این تعریف شده را مستقر کنیم و یک clone از آن بسازیم.
در ادامه یک اسکریپت ساده با استفاده از فریمورک Hardhat ارائه شده است که مراحل استقرار قراردادها و تعامل با یک clone مستقر شده را انجام می دهد:
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 |
const hre = require("hardhat"); async function main() { const ImplementationContract = await hre.ethers.getContractFactory( "ImplementationContract" ); // deploy the implementation contract const implementationContract = await ImplementationContract.deploy(); await implementationContract.deployed(); console.log("Implementation contract ", implementationContract.address); const MinimalProxyFactory = await hre.ethers.getContractFactory( "MinimalProxyFactory" ); // deploy the minimal factory contract const minimalProxyFactory = await MinimalProxyFactory.deploy(); await minimalProxyFactory.deployed(); console.log("Minimal proxy factory contract ", minimalProxyFactory.address); // call the deploy clone function on the minimal factory contract and pass parameters const deployCloneContract = await minimalProxyFactory.deployClone( implementationContract.address ); deployCloneContract.wait(); // get deployed proxy address const ProxyAddress = await minimalProxyFactory.proxies(0); console.log("Proxy contract ", ProxyAddress); // load the clone const proxy = await hre.ethers.getContractAt( "ImplementationContract", ProxyAddress ); console.log("Proxy is initialized == ", await proxy.isInitialized()); // get initialized boolean == true } main().catch((error) => { console.error(error); process.exitCode = 1; }); |
در حال حاضر، قراردادهای مورد نظر با موفقیت روی شبکه Goerli مستقر شده اند و جزئیات تراکنش مربوط به سه قرارداد اصلی به شرح زیر است:
نکته مهم این است که Etherscan به درستی تشخیص می دهد که قرارداد پراکسی، یک قرارداد معمولی نیست؛ بلکه فراخوانی های دریافتی را با استفاده از دستور delegatecall
به قرارداد پیاده سازی منتقل می کند. این رفتار، یکی از ویژگی های اصلی استاندارد EIP-1167 در سالیدیتی است.
همچنین باید توجه داشت که نگهداری لیستی از clone های مستقرشده، صرفاً برای سهولت توسعه و بررسی در این پروژه انجام شده است. این قابلیت ضروری نیست و می توان آن را بسته به نیاز، حذف یا ساده تر کرد.
جمع بندی
استاندارد پراکسی مینیمال EIP-1167 در سالیدیتی روشی کارآمد و کمهزینه برای استقرار قراردادهایی است که دقیقاً رفتار یک قرارداد دیگر را تکرار می کنند. با استفاده از الگوی مقداردهی اولیه (initializer pattern)، می توان نسخه clone را طوری مستقر کرد که گویی دارای سازنده (constructor) با ورودی های دلخواه است. اگر به دنبال پیاده سازی عملی این الگو هستید، پیشنهاد می کنیم ابتدا مبانی آموزش برنامه نویسی قراردادهای هوشمند در سالیدیتی را مرور کنید.
هزینه اصلی این روش در این است که هر بار فراخوانی، با یک دستور delegatecall
همراه می شود و این موضوع باعث افزایش اندک مصرف گس در زمان اجرا خواهد شد. با این حال، مزایای آن در صرفه جویی در گس هنگام استقرار و ساختار ساده، این هزینه را در بسیاری از موارد توجیه پذیر می کند.
راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.
- انتشار: ۱۷ تیر ۱۴۰۴
دسته بندی موضوعات
- آموزش ارز دیجیتال
- آموزش برنامه نویسی
- آموزش متنی برنامه نویسی
- اطلاعیه و سایر مطالب
- پروژه برنامه نویسی
- دوره های تخصصی برنامه نویسی
- رپورتاژ
- فیلم های آموزشی
- ++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
- اچ تی ام ال
- بانک اطلاعاتی
- برنامه نویسی سوکت
- برنامه نویسی موبایل
- پاسکال
- پایان نامه
- پایتون
- جاوا
- جاوا اسکریپت
- جی کوئری
- داده کاوی
- دلفی
- رباتیک
- سئو
- سایر کتاب ها
- سخت افزار
- سی اس اس
- سی پلاس پلاس
- سی شارپ
- طراحی الگوریتم
- فتوشاپ
- مقاله
- مهندسی نرم افزار
- هک و امنیت
- هوش مصنوعی
- ویژوال بیسیک
- نرم افزار و ابزار برنامه نویسی
- وردپرس