天天看点

solidity[48]-call函数

调用外部合约的代码

在之前我们已经看到过,使用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..

调用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;
  }
}