在以太坊区块链上,智能合约是头等公民,由于其重要性,Solidity(目前是编写以太坊智能合约的标准语言)提供了几种使合约与其他合约互动的方法。
智能合约可以调用其他合约的功能,甚至能够创建和部署其他合约(例如,发行硬币)。这种行为有几个用例。
最有趣的用例之一是可升级合约。由于区块链的不可改变性,一旦部署了智能合约,就不可能改变它的代码。但通过使用委托调用的机制,一个代理可以部署指向(委托函数调用)另一个合约,该合约持有实际的业务逻辑。 然后,这个机制使得升级合约的功能成为可能,通过向代理合约提供不同的目标地址,例如一个新部署的目标合约的版本,并进行一些错误修复。
同样的原则可以被利用来作为库使用其他合同,从而减少部署成本,因为使用库的合同不需要包括所有的代码本身。
另一个用例是简单地将合约作为数据存储的一种。例如,人们可以努力将逻辑和数据分离到不同的智能合约中。现在,逻辑合约可以通过代理更新或交换,同时保留数据合约中的所有相关状态。
能够从智能合约中调用和创建合约是一个强大的概念,这篇文章提供了一个简单的例子,说明如何使用Solidity实现和测试这种行为。
契约
首先,我们需要两个智能合约来测试我们的互动。
在这个小例子中,我们将创建一个Callee 合同,它持有一些状态并被Caller 合同调用。
在Solidity中,有几种方法可以委托合约之间的调用。一个部署的合约总是驻扎在一个address ,Solidity中的这个address-对象提供了三种方法来调用其他合约。
call- 执行另一个合同的代码delegatecall- 执行另一个合同的代码,但要有调用合同的状态(存储)。callcode- (已废止)
也可以为这样的call 调用提供气体和醚。
someAddress.call.gas(1000000).value(1 ether)("register", "MyName");
delegatecall 方法是对callcode 的错误修复,它没有保留msg.sender 和msg.value ,所以callcode 已经被废弃,将来会被删除。
值得注意的是,delegatecall 涉及到调用合约的安全风险,因为被调用的合约可以访问/操纵调用合约的存储。在call 和delegatecall ,由于EVM的限制,不可能从委托的调用中接收返回值。
当然,从一个给定的地址调用合约上的函数存在固有的安全风险,这种调用合约的方式破坏了 Solidity 的类型安全。因此,call,callcode 和delegatecall 应该只作为最后手段使用。
另一种从智能合约中调用合约的方式是使用像依赖性注入这样的机制。通过这种方法,调用者可以实例化它想调用的合约,并知道函数的类型签名,这也有一个很好的副作用,即可以接收返回值。
在下面的例子中,我们将同时使用call 和dependency-injection 方法。
首先,我们定义Callee 合同。
pragma solidity ^0.4.6;
contract Callee {
uint[] public values;
function getValue(uint initial) returns(uint) {
return initial + 150;
}
function storeValue(uint value) {
values.push(value);
}
function getValues() returns(uint) {
return values.length;
}
}
这个简单的契约持有一个整数数组,提供了一个添加值和检索存储值数量的方法。它还有一个getValue 方法,它接受一个输入并返回一个改变后的输出,以展示返回值和参数是如何工作的。
Caller 合约除了使用上面描述的接口方法调用Callee 合约外,还使用call 方法。
pragma solidity ^0.4.6;
contract Caller {
function someAction(address addr) returns(uint) {
Callee c = Callee(addr);
return c.getValue(100);
}
function storeAction(address addr) returns(uint) {
Callee c = Callee(addr);
c.storeValue(100);
return c.getValues();
}
function someUnsafeAction(address addr) {
addr.call(bytes4(keccak256("storeValue(uint256)")), 100);
}
}
contract Callee {
function getValue(uint initialValue) returns(uint);
function storeValue(uint value);
function getValues() returns(uint);
}
在底部,你可以看到Callee 接口,反映了它背后合约的函数签名。这个接口也可以在另一个.sol 文件中定义并导入,以使事情更简洁地分开。
除此之外,合同只是提供了3个函数,这些函数都是用address 。 这是部署的Callee 合同的地址。也可以用某个地址来初始化合同,并在一段时间后改变这个地址,例如使用目标合同的较新版本。
测试交互
像这样的互动可以通过多种方式进行测试。例如,我们可以使用web3和testrpc、Go,或者像我在这个案例中所做的那样,使用Remix浏览器IDE。
Remix是试验智能合约的一个很好的工具--它有一个很好的编辑器和选项来模拟部署合约和调用合约上的功能。
将合同复制/粘贴到Remix的两个文件中后,我们可以先创建(部署)Callee 合同。
然后,我们可以使用右下方的Copy address 按钮来获取合同的部署地址。在这个例子中,这个地址是0xdcb77b866fe07451e8f89871edb27b27af9f2afc 。
然后,我们创建Caller 合同,并使用右边的面板,以Callee 合同的地址为参数(用引号表示,是一个字符串)调用someAction 方法。
这将返回值250 ,正如预期的那样。
现在我们可以再做一些实验,以与someAction 相同的方式使用someUnsafeAction 和storeAction 的调用。对storeAction 的调用会返回存储在Callee 合同中的当前值的数量(本例中为13)。
就这样了。你可以在Remix中对这个例子进行更多的实验,例如通过代理交易或使用
delegatecall 方法而不是call 。
总结
本篇文章中概述的互动是绕过智能合约开发的一些限制的一种巧妙的方法。不过,应该注意的是,这些机制都很容易受到攻击,所以应该谨慎使用。
我相信,关于智能合约的开发,在安全和稳健性方面的质量水平需要提高不少,才能将以太坊等技术安全地用于关键应用。
但是,现在有很多研究和实验正在进行,所以我相信在未来几年,我们会有一些非常有趣的发现和突破。)