قرارداد هوشمند: فراخوان تابع از قرارداد دیگر

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

مثلا ممکن است بخواهید یک توکن از نوع ERC20 (مانند تتر) را که اکنون متعلق به قرارداد هوشمندتان است، به آدرس دیگری منتقل کنید. در این مثال لازم است که تابع transfer از قرارداد هوشمند توکن مربوطه فراخوانی شود. در این گونه موارد راه‌های مختلفی پیش روی کاربر است که در این مقاله به بررسی آنها می‌پردازیم:

استفاده از اینترفیس قرارداد دیگر

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

اگر با استاندارد ERC20 آشنا باشید، اینترفیس توکن‌های مبتنی بر این استاندارد به ترتیب زیر خواهد بود:

interface IERC20 {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function decimals() external view returns (uint8);
    function totalSupply() external view returns (uint256) ;
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns(bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

در مثال فوق حرف I در ابتدای نام IERC20 مخفف Interface است.

تذکر: اگر ما در قرارداد هوشمند خودمان، فقط با تابع خاصی مانند transfer سروکار داشته باشیم، می‌توانیم بقیه‌ی کد را حذف کنیم:

interface IERC20 {
    function transfer(address recipient, uint256 amount) external returns (bool);
}

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

interface IERC20 {
    function transfer(address recipient, uint256 amount) external returns (bool);
}
contract Distribute {
    ...
    address tokenAddress = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4; // only example
    function transferToken(uint value) public {
        IERC20 token = IERC20(tokenAddress);
        token.transfer(msg.sender, value);
    }
    ...
}

در مثال فوق آدرس قرارداد هوشمند توکن فوق در متغیر tokenAddress قرار می‌گیرد. در تابع انتقال توکن متغیری به نام token از نوع اینترفیس IERC20 تعریف می‌شود. مقدار این متغیر، تبدیل نوع یافته‌ی آدرس توکن به اینترفیس IERC20 است. بعد ار این تعریف هر یک از تابع‌های ذکر شده در اینترفیس IERC20 را می‌توان توسط token فراخوانی کرد. همان کاری که در سطر 9 انجام شده است.

فراخوان مستقیم تابع‌های قرارداد دیگر

برای فراخوان مستقیم دو راه حل مختلف وجود دارد که به بیان آنها و تفاوت آنها می‌پردازیم:

استفاده از متد call

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

Contract2.call('function_name()');

در مثال بالا Contract2 آدرس قرارداد هوشمند دوم (از نوع address payable) و function_name نام یکی از توابع آن است.

مقدار خروجی متد call یک جفت داده است. خروجی اول آن از نوع bool و معرف اجرا یا عدم اجرای متد call است. خروجی دوم آن نیز از نوع bytes است و با استفاده از آن می‌توان خروجی تابع function_name را استخراج کرد. پس می‌توانیم بنویسیم:

(bool success, bytes memory output) = Contract2.call('function_name()');

حال ببینیم چگونه می‌توان خروجی تابع مورد نظر را از متغیر output از نوع bytes استخراج کرد.

فرض کنید تابع function_name فقط یک خروجی از نوع uint داشته باشد. در این صورت می‌توانیم این خروجی را به کمک کد زیر از متغیر خروجی output استخراج کنیم:

uint a = abi.decode(output, (uint));

اگر تابع function_name دو خروجی به ترتیب از نوع uint و string داشته باشد، می‌توان این دو خروجی را با کد زیر از متغیر output استخراج کرد:

(uint a, string memory s) = abi.decode(output, (uint, string));

به این ترتیب هر تعداد خروجی را می‌توان با استفاده از روش فوق استخراج کرد.

گفتیم که روش صدا زدن متد call به ترتیب زیر است:

(bool success, bytes memory output) = Contract2.call('function_name()');

این روش تنها زمانی کارآیی دارد که که تابع function_name هیچ آرگومان ورودی نداشته باشد.

برای آوردن آرگومان ورودی برای این متد چه باید کرد؟ فرض کنید که تابع function_name دو آرگومان ورودی به ترتیب از نوع uint8 و string قبول کند. در این صورت سطر فوق به ترتیب زیر نوشته می‌شود:

(bool success, bytes memory output) = Contract2.call(abi.encodeWithSignature('function_name(uint8, string)', 1, '2nd arguement'));

توجه کنید که در مثال فوق در داخل پرانتز جلوی نام تابع فقط نوع داده‌های ورودی به ترتیب مشخص می‌شود. در ادامه به عنوان پارامترهای بعدی مقدار آرگومان‌های ورودی تابع نیز داده می‌شود که در این جا به ترتیب 1 و "2nd arguement" است.

فقط یک نکته در این مورد ناگفته مانده و آن این که چطور می‌توانیم هنگام فراخوان یک تابع از قرارداد دوم، مقداری توکن پایه‌ی شبکه (مثلا اتر در شبکه اتریوم یا BNB در شبکه BSC) را نیز به قرارداد دوم پرداخت کنیم.

در مثال زیر تابع function_name (که فرض می‌کنیم آرگومان ورودی ندارد) و payable هم هست، فراخوانده می‌شود و مقدار 1 اتر به آن پرداخت می‌شود:

Contract2.call{value: 1000000000000000000}('function_name()');

در مثال فوق از ۱۸ صفر استفاده کردیم، زیرا مقدار ارزش‌ها بر حسب واحد wei است. در زبان سالیدیتی می‌توان از کلمه‌ی ether برای تبدیل واحد به اتر نیز استفاده کرد:

Contract2.call{value: 1 ether}('function_name()');

در این جا یک نمونه‌ی کامل از استفاده از متد call را مشاهده می‌کنید:

pragma solidity ^0.6.10;
contract A {
    uint p = 10;
    address payable BAddress = payable(0x1aE0EA34a72D944a8C7603FfB3eC30a6669E454C); // Address of contract B
    function getP() public returns(uint) {
        (bool success, bytes memory output) = BAddress.call('getPB()');
        require(success);
        return(abi.decode(output, (uint)));
    }
    function incP() public returns(uint) {
        (bool success, bytes memory output) = BAddress.call('incPB()');
        require(success);
        return(abi.decode(output, (uint)));
    }
}
contract B {
    uint p = 501;
    function getPB() public view returns(uint) {
        return(p);
    }
    function incPB() public {
        p++;
    }
}

واضح است که در این جا لازم است ابتدا قرارداد هوشمند B دپلوی شود و آدرس آن در متغیر BAddress در قرارداد هوشمند A ذکر شود و سپس قرارداد A دپلوی شود.

تابع getP از قرارداد هوشمند A مقدار متغیر p از قرارداد هوشمند B را به شما برمی‌گرداند و تابع incP مقدار متغیر p از قرارداد هوشمند B را یک واحد زیاد می‌کند.

تابع getP از قرارداد هوشمند A با این که ظاهرا تغییری در متغیرهای بلاکچین نمی‌دهد، با این حال نمی‌تواند از نوع view تعریف شود. دلیل این موضوع آن است که متد call بالقوه امکان تغییر متغیرهای بلاکچین را دارد و از این رو نمی‌تواند در توابع view به کار برده شود.

استفاده از متد delegatecall

متد delegatecall دقیقا مانند متد call استفاده می‌شود. و تقریبا همان کاربرد را دارد. با این تفاوت که در این متد بعضی رفتارهای درونی متفاوت است.

  • در متد call وقتی قرارداد هوشمند A تابعی از قرارداد هوشمند B را صدا می‌زند، آن تابع می‌تواند با داده‌های ذخیره شده در قرارداد هوشمند B کار کند و در صورت لزوم آنها را تغییر دهد. اما در متد delegatecall آن تابع می‌تواند با داده‌های ذخیره شده در قرارداد هوشمند A کار کند یا آنها را تغییر دهد. به این ترتیب از متد delegatecall برای آپگرید کردن قراردادهای هوشمند به کار می‌رود.
  • در متد delegatecall امکان ارسال توکن پایه‌ی شبکه برای قرارداد دوم امکان‌پذیر نیست، چرا که قرارداد دوم با داده‌های قرارداد اول کار می‌کند و مقدار توکن ارسالی نیز یکی از همین نوع داده‌ها است.
  • در متد call متغیر msg.sender حاوی آدرس قرارداد اول است، در حالی که در متد delegatecall متغیر msg.sender حاوی آدرس حساب فراخوان کننده‌ی تابع قرارداد اول است. اما در هر دو حالت متغیر tx.origin حاوی آدرس حساب اولیه‌ی فراخوان کننده است.

برای بررسی دقیق‌تر در مورد متد delegatecall کدهای زیر را اجرا کنید و نتایج آن را بررسی کنید:

pragma solidity ^0.6.10;
contract A {
    uint p = 10;
    address payable BAddress = payable(0x1aE0EA34a72D944a8C7603FfB3eC30a6669E454C); // Address of contract B
    function getP() public returns(uint) {
        (bool success, bytes memory output) = BAddress.delegatecall('getPB()');
        require(success);
        return(abi.decode(output, (uint)));
    }
    function incP() public returns(uint) {
        (bool success, bytes memory output) = BAddress.delegatecall('incPB()');
        require(success);
        return(abi.decode(output, (uint)));
    }
}
contract B {
    uint p = 501;
    function getPB() public view returns(uint) {
        return(p);
    }
    function incPB() public {
        p++;
    }
}