区块链安全

区块链安全第 6 课——合约调用合约

你不应该跳过的内容是:事件的定义、触发及捕获。但是因为我在本地写了很多段测试用例了,我实在不想再写一遍了,所以在本文中我会跳过。但是没关系,后面应该还会在具体的场景中碰到。

本文主要介绍的是:从合约中调用其他合约的多种方法

在进入正式内容之前我们本节课基于的代码示例是:

里面加入了事件的定义及触发,可以搜索一下 event 以及 emit 关键字。

1. 调用自己编写的合约

使用 new 来在以太坊区块链上创建一个合约实例,并返回一个可供引用的对象。
如:

contract Token is mortal{
    Faucet _faucet;
    constructor(){
        _faucet = new Faucet();
    }
}

new 也可以接收可选的参数,这些参数用来指定合约创建时转入的以太币数量,以及可能需要传递给新合约和构造函数的一些参数:

contract Token is mortal{
    Faucet _faucet;
    constructor(){
        _faucet = (new Faucet).value(0.5 ether)();
    }
}

实例化 Faucet 对象之后,我们也可以调用这个合约中的方法,如:

contract Token is mortal{
    Faucet _faucet;
    constructor(){
        _faucet = (new Faucet).value(0.5 ether)();
    }
    function destroy() OnlyOwner{
        _faucet.destroy();
    }
}
  • 注意:Token 合约的所有者是开发和部署 Token 合约的外部账号,而 Faucet 合约是有 Token 合约创建和部署的,因此它的所有者是 Token 合约。

2. 转换合约现有实例的地址

也就是说,将一个地址强转为某个合约类型。这样做,可以直接使用已存在的合约对象实例。

contract Token is mortal{
    Faucet _faucet;

    constructor(address _f){
        _faucet = Faucet(_f);
        _faucet.withdraw(0.1 ether);
    }
}

3. 使用底层函数调用其他合约

Solidity 提供了一些更底层的函数,用于调用其他合约。这些直接对应着 EVM 的字节码,允许开发者手动构建合约对合约的调用。如:

contract Token is mortal{
    contructor(address _faucet){
        _faucet.call("withdraw",0.1 ether);
    }
}
  • 可以看到,这样的调用基本就是盲调用,是比较危险的。
  • 如果遇到问题,call 函数会返回 false,于是可以如下做一些错误处理:
contract Token is mortal{
    contructor(address _faucet){
        if(!_faucet.call("withdraw",0.1 ether)){
            revert("Withdrawal from faucet failed");
        }
    }
}
  • 如上是使用 call() 函数调用其他的合约;
  • 另一种方式是使用 delegatecall()。
  • 这两个函数的区别是:call() 函数会导致 msg.sender 的值替换为发起调用的合约,但 delegatecall() 仍保持 msg.sender 不变。
  • 也就是说,delegatecall() 就是在当前合约的上下文中运行另一个合约。
  • msg.sender:发起合约调用的地址。可能是外部账户地址,也可能是调用这个合约的合约的地址
  • tx.origin:发起这个交易的外部账户的地址
  • this:当前的合约对象

分析一下上面的代码:
caller 是主合约,它分别调用了 callContract 合约和库合约 callLibrary 的 calledFunction 方法。每次调用时候都会触发一个 calledEvent 事件。这个事件会导致在交易日志中记录 msg.sender、tx.origin 和 this 三部分数据。

  • 直接调用 callContract 合约 _calledContract.calledFunction()
    • msg.sender:caller 地址
    • tx.origin:触发合约的以太坊账户地址
    • this:calledContract 地址,因为当前事件由此合约产生
  • 直接调用 calledLibrary 库合约 calledLibrary.calledFunction()
    • msg.sender:触发合约的以太坊账户地址
    • tx.origin:触发合约的以太坊账户地址
    • this:caller 地址
      这是因为,在调用库合约时候,调用总是采用 delegatecall 方式进行,库合约的代码始终运行在调用发起方(触发合约的外部账户地址)的上下文中。因此当 calledLibrary 的代码运行时,它继承了 caller 的执行上下文,就如同库合约代码直接运行在 caller 合约内部一样,所以 this 变量是 caller 的地址,虽然实际上代码的执行和变量访问都是在 calledLibrary 中发生的。
  • 使用 call 方法调用 callContract 合约 _calledContract.calledFunction()
    • msg.sender:caller 地址
    • tx.origin:触发合约的以太坊账户地址
    • this:calledContract 地址,因为当前事件由此合约产生
      • 同1
  • 使用 delegatecall 方法调用 callContract 合约 _calledContract.calledFunction()
    • msg.sender:触发合约的以太坊账户地址
    • tx.origin:触发合约的以太坊账户地址
    • this:caller 地址
      • 同2

本节课的内容就到此为止,需要好好体会 call 和 delegetacall 两种底层函数调用其他合约方式上下文的不同。虽然有点绕但也不是很难理解!

下节课会学习另一种面向合约的编程语言 Vyper,体会 Solidity 和 Vyper 语言的差异,加深对智能合约编程的理解。