作者:深圳职业技术学院高旭亮
我们在使用solidity语言编写智能合约时有一个痛点,在智能合约出现bug或者因为业务扩展需要增加新的特性时,对开发人员是一种折磨,由于新部署的合约并不会继承旧合约的数据,我们就需要将旧合约的数据迁移到新合约上,也就是常说的数据迁移。
本系列文章会循序渐进帮助开发人员打造可升级合约,从只能升级逻辑的合约,到能增删状态变量的合约,并且会有番外篇扩展许多合约相关的知识,总之,让开发人员能根据业务需求自在的修改合约,本篇主要讲解通过代理实现基本的可升级逻辑的合约,solidity使用0.5.x版本,在使用其他版本时会特别注明,代码会尽量简介清晰容易阅读。
如果我们能将合约分成两个部分,一部分合约用于业务本身逻辑,称为逻辑合约,一部分合约则存储链上数据,称为代理合约,也就是逻辑和数据分离,那么我们在升级业务本身逻辑的时候,自然就不需要进行链上数据迁移,并且由于我们会通过代理合约去间接调用逻辑合约,那么在逻辑合约升级时,其他需要调用当前合约的外部合约也不需要跟着修改新合约的地址了,因为他始终调用的是代理合约。
逻辑合约较为简单,不作解析
pragma solidity >=0.5.2;
contract LogicContract{
uint256 public number;
function getNumber() public view returns (uint256){
return number;
}
function incrNumber() public{
number++;
}
}
此处为了让代理调用更清晰,并且考虑到手动传递calldata较为麻烦,所以暂时不通过fallback进行调用,通过在代理合约编写方法直接进行逻辑合约的调用,实际应用会通过fallback进行调用的。
合约解析
- 状态变量中number与implement的顺序不能改变,这里有一个知识点为solidity slot,读者可自行查阅资料,后续会单独编写一篇文档介绍solidity slot
- 函数的选择器,指定了要调用的函数
- abi.encodeWithSelector根据函数选择器编译成calldata,最后通过发送calldata到逻辑合约完成方法调用
- abi.decode将调用逻辑合约的结果解析为uint256
pragma solidity >=0.5.2;
import "./LogicContract.sol";
contract Proxy{
uint256 internal number;
address internal implement;
function setImplement(address addr) public{
implement = addr;
}
function getImplement() public view returns (address){
return implement;
}
function callGetNumber() public returns (uint256){
// 获取逻辑合约中对应方法的选择器
bytes4 getNumber = LogicContract(implement).getNumber.selector;
// calldata
bytes memory data = abi.encodeWithSelector(getNumber);
// 进行delegatecall调用
(bool success, bytes memory result) = implement.delegatecall(data);
return abi.decode(result, (uint256));
}
function callIncrNumber() public returns (bool){
// 获取逻辑合约中对应方法的选择器
bytes4 getNumber = LogicContract(implement).incrNumber.selector;
// calldata
bytes memory data = abi.encodeWithSelector(getNumber);
// 进行delegatecall调用
(bool success, ) = implement.delegatecall(data);
return success;
}
}
注意:当前代理合约为了演示方便,并没有使用modifier来限制所有者,实际应用中应该有所限制。
将逻辑合约部署获取合约地址后,部署代理合约并调用setImplement传入逻辑合约地址完成地址更新后进行测试。我们通过测试发现不通过代理合约,直接调用逻辑合约修改状态变量时,调用代理合约的callIncrNumber获取到的值没有改变,而通过代理合约调用逻辑合约修改状态变量时,直接调用逻辑合约获取到的值也没有改变。说明我们已经实现了前面所说的逻辑与数据分离的合约,不信我们可以升级逻辑合约。
当前修改了incrNumber方法
function incrNumber() public{
number++;
number++;
}
我们可以重新部署逻辑合约,并更新代理合约中的逻辑合约地址,进行调用,会发现原来的合约数据依然存在,至此,可升级逻辑的合约演示完毕。
在代理合约中添加fallback回调方法,并且像调用逻辑合约一样,向代理合约发送calldata即可,在调用代理合约不存在的方法时,就会间接调用fallback方法。
function() external {
(bool success, ) = getImplement().delegatecall(msg.data);
assembly {
let free_mem_ptr := mload(0x40)
returndatacopy(free_mem_ptr, 0, returndatasize())
switch success
case 0 { revert(free_mem_ptr, returndatasize()) }
default { return(free_mem_ptr, returndatasize()) }
}
}
本文通过代码演示了可升级逻辑的智能合约,在业务逻辑需要升级时我们只需要重新部署逻辑合约,并将代理合约上的逻辑合约地址进行更新即可,免去了数据迁移的麻烦,但现在合约还有个问题是无法升级数据结构,后续篇章会进行可升级逻辑与数据结构的合约,在这之前会先写一篇文档写出当前可能存在的坑。