区块链安全第 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 语言的差异,加深对智能合约编程的理解。