链眼社区:专注于区块链安全,区块链数据分析, 区块链信息整合,区块链技术服务和区块链技术咨询。

solidity智能合约call函数
扫地僧
2021-08-06 23:53:05

调用外部合约的代码

在之前我们已经看到过,使用interface、library的方式调用外部合约的代码。
接下来,我们将为大家补充第三种形式:
在下面的代码中,部署cat合约之后,例如地址为 0x345678.. 在部署animal合约时,传递此cat合约地址。从而能够存储合约的引用。调用test方法即可调用到外部合约的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pragma solidity ^0.4.23;

contract cat{

   uint public a=5;
   function eat() public returns(uint){
       a = 256;
       return a;
   }
}

contract animal{
   cat c;

   constructor(address _addr){
       c = cat(_addr);
   }

   function test() public returns(uint){

       return c.eat();
   }
}
call函数

不管是interface、library还是上面看到的形式,要调用外部代码,都是底层调用了call或者是delecall函数。

call函数基本使用方法

call函数的使用方法,首先需要外部合约的地址。如下例中的animalCall合约,在部署合约时,传递了外部合约cat的地址 0x345678.. ,存储在address c当中。
通过合约地址.call(函数标志符)的方式来调用合约。函数标志符是对于函数声明哈希之后的前4个字节的数据。
如下例中,c.call(bytes4(keccak256(“eat()”)))将调用cat合约中的eat方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
contract cat{

   uint public a=5;
   function eat() public returns(uint){
       a = 256;
       return a;
   }
}
contract animalCall{
   address c;
   constructor(address _addr){
       c = _addr;
   }

   function test1() public returns(bool){
       return c.call(bytes4(keccak256("eat()")));
   }
   function test2() public returns(bool){
       return c.call(bytes4(keccak256("eat")));
   }
}

call函数返回值

call函数的返回值为true或者false。只有当能够找到此方法并执行成功后,会返回true,而如果不能够找到此函数或执行失则会返回false。因此调用test1方法会返回true,调用test2方法会返回false,因为找不到函数。

call 函数与回调函数

call函数如果找不到函数,默认会调用回调函数。
回调函数是特殊的函数,其没有函数名。
其形式为:

1
2
3
function(){

}

对于如下的cat合约。书写了回调函数。假设合约地址为c.那么在外部调用c.call(“abc”);会找不到此函数,默认会执行回调函数.因此在外部调用的c.call(“abc”) 会使得cat合约的状态变量变为999。而且call函数会返回true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract cat{

   uint public a=5;

   function eat() public returns(uint){
       a = 256;
       return a;
   }

   function (){
       a=999;
   }

}
call函数与msg.data
回调函数是非常有用的,例如我们可以在外部调用失败的时候,执行某一些操作。
对于如下的cat合约。书写了回调函数。假设合约地址为c.那么在外部调用c.call(“abc”);会找不到此函数,默认会执行回调函数.回调函数中,将msg.data的值赋值给了fail变量。通过getfail函数可查看call函数传递过来的完整数据。fail变量的值为32个字节0x6162630000000000000000000000000000000000000000000000000000000000,前3个字节是参数字母a、b、c的ASCII码。61、62、63.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.23;

contract cat{

   bytes fail;
   function (){
       fail = msg.data;
   }

   function getfail() returns(bytes){
       return fail;
   }

}
call函数修改外部合约的状态变量

在下例中,cat合约与animalcall合约中都有状态变量我们首先部署cat合约,得到地址0x3456.., 接下来,将合约地址作为参数部署anumalCall合约。
调用test2方法,其调用了cat合约的eat方法,修改了cat合约中a的值为256. call函数调用外部合约,修改外部合约中的状态变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity ^0.4.23;
contract cat{
   uint public a=5;
   function eat() public returns(uint){
       a = 256;
       return a;
   }
}

contract animalCall{
    uint public a=4;
   address c;

   constructor(address _addr){
       c = _addr;
   }

     function test2() public returns(bool){
       return c.call(bytes4(keccak256("eat()")));
     }
}

delegatecall

delegatecall函数的使用方法和call函数一样,通过合约地址.delegatecall(函数标志符)的方式来调用合约。函数标志符是对于函数声明哈希之后的前4个字节的数据。
library库的远程调用正是使用了delegatecall函数。delegatecall与call不同之处在于,delegatecall不会修改外部合约中的状态变量,其好像是将外部函数的代码加载到了本地合约中执行。会修改本地合约状态变量的值。
例如下面的代码,首先部署cat合约,得到地址0x3456.., 接下来,将合约地址作为参数部署anumalCall合约。
调用test2方法,其调用了cat合约的eat方法,但是却是修改了animalcall合约中的状态变量a。因此当查询后发现,cat合约中的a并没有变化,animalCall合约变量a变为了了256。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity ^0.4.23;
contract cat{
   uint public a=5;
   function eat() public returns(uint){
       a = 256;
       return a;
   }
}

contract animalCall{
    uint public a=4;
   address c;

   constructor(address _addr){
       c = _addr;
   }

     function test2() public returns(bool){
       return c.delegatecall(bytes4(keccak256("eat()")));
     }
}

call函数转账与回调函数细节

call函数可以进行转账,并且是transfer与send的底层函数。call函数转账的使用方法是
地址.call.value(转账金额)()

要注意的是,执行转账的时候,如果转账的地址为合约,并且转账合约中有回调函数。那么将默认会执行回调函数。
但是以太坊为了避免重入攻击,对于transfer与send函数进行了限制。当使用transfer与send函数,回调函数中执行的操作最多不能够超过2300gas。这也就意味着不能够执行转账、赋值等操作,而只能够执行事件触发等操作。
例如下面的代码: 首先部署Receiver合约,得到地址0x3456..,再传递Receiver的地址部署Sender合约。当调用sendMoney方法的时候,为合约地址0x3456..转账的操作会触发回调函数,将状态变量balance的数量增加。但是由于修改状态变量的操作超过了最大2300gas的限制,所以下面的操作不会成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.4.23;

contract Sender {
 function sendMoney(address _receiver) payable {
   _receiver.send(msg.value);
 }
}



contract Receiver {
 uint public balance = 0;

 function () payable {
   balance += msg.value;
 }
}

call函数能够让上面的操作成功。call函数能够指定gas的限制,超过2300gas限制的约束。
如下例所示:
首先部署Receiver合约,得到地址0x3456..,再传递Receiver的地址部署Sender合约。当调用sendMoney方法转移100wei的时候,为合约地址0x3456..转账的操作会触发回调函数,将状态变量balance的数量增加。由于call函数指定的最大gas限制为20317,所以触发回调函数可以将balance的金额修改为100.但是要注意,正因为此,call函数是危险的底层函数,不能够避免重入攻击的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.4.23;

contract Sender {
 function send(address _receiver) payable {
   _receiver.call.value(msg.value).gas(20317)();
 }
}

contract Receiver {
 uint public balance = 0;

 function () payable {
   balance += msg.value;
 }
}

回调函数细节

由于回调函数在转账或者call找不到函数的时候都会触发。因此,需要通过require(msg.data.length == 0)来明确当前的
必须为转账而不是call在调用函数。

1
2
3
4
5
// 坏的方式
function() payable { LogDepositReceived(msg.sender); }

// 好的方式
function() payable { require(msg.data.length == 0); LogDepositReceived(msg.sender); }

delegatecall陷阱与建议

如下例中,如果Worker合约远程调用Destructor合约的dowork方法,其实会销毁Worker合约。导致合约中的所有以太币被销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
contract Destructor
{
   function doWork() external
   {
       selfdestruct(0);
   }
}

contract Worker
{
   function doWork(address _internalWorker) public
   {
       // unsafe
       _internalWorker.delegatecall(bytes4(keccak256("doWork()")));
   }
}


合作伙伴