以太坊智能合约升级
问题
智能合约一旦发布就不能修改了,那如果出现bug怎么办?可以开发一个新的合约,但之前合约中的数据怎么办?重新导入一遍吗?可能需要几天的时间,而且使用新的合约又涉及到调用者更改合约地址,比较麻烦。
所以我们需要一个能够升级的合约,来解决这2个麻烦的问题。
方案
普通合约的方式:

合约肯定是不能修改的,那么升级就肯定是使用新的合约,为了使旧合约的数据复用,就需要把数据层提取出来,作为一个单独的存储合约,供业务合约调用。

为了使合约的变动对 client 透明,我们可以添加一个 proxy 代理合约,client 和 proxy 合约对接,不再直接调用业务合约,当业务合约升级时,只需要通过 proxy 指定新版合约。

示例
我们需要开发3个类型的合约:
- 存储合约
处理底层通用的存储操作。
- 委托合约
也就是约我合约,处理业务逻辑,每次业务升级就创建一个新版本的委托合约。
- 代理合约
用于和 client 对接,内部使用 delegatecall
机制可以调用不同的合约。
(1)创建项目
新建目录 upgradetest
,进入目录执行初始化命令:
$ truffle init
(2)存储合约
contracts/KeyValueStorage.sol
pragma solidity ^0.4.18;
contract KeyValueStorage {
mapping(address => mapping(bytes32 => uint256)) _uintStorage;
mapping(address => mapping(bytes32 => address)) _addressStorage;
mapping(address => mapping(bytes32 => bool)) _boolStorage;
mapping(address => mapping (address => mapping(address => uint256))) _allowanceStorage;
/**** Get Methods ***********/
function getAddress(bytes32 key) public view returns (address) {
return _addressStorage[msg.sender][key];
}
function getUint(bytes32 key) public view returns (uint) {
return _uintStorage[msg.sender][key];
}
function getBool(bytes32 key) public view returns (bool) {
return _boolStorage[msg.sender][key];
}
function getAllowance(address owner,address spender) public view returns (uint) {
return _allowanceStorage[msg.sender][owner][spender];
}
/**** Set Methods ***********/
function setAddress(bytes32 key, address value) public {
_addressStorage[msg.sender][key] = value;
}
function setUint(bytes32 key, uint value) public {
_uintStorage[msg.sender][key] = value;
}
function setBool(bytes32 key, bool value) public {
_boolStorage[msg.sender][key] = value;
}
function setAllowance(address owner,address spender,uint value) public {
_allowanceStorage[msg.sender][owner][spender] = value;
}
/**** Delete Methods ***********/
function deleteAddress(bytes32 key) public {
delete _addressStorage[msg.sender][key];
}
function deleteUint(bytes32 key) public {
delete _uintStorage[msg.sender][key];
}
function deleteBool(bytes32 key) public {
delete _boolStorage[msg.sender][key];
}
}
contracts/StorageState.sol
pragma solidity ^0.4.18;
import "./KeyValueStorage.sol";
contract StorageState {
KeyValueStorage _storage;
}
(3)委托合约V1版本
contracts/SafeMath.sol
pragma solidity ^0.4.18;
/**
* @title SafeMath
* @dev Math operations with safety checks that throw on error
*/
library SafeMath {
/**
* @dev Multiplies two numbers, throws on overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
/**
* @dev Integer division of two numbers, truncating the quotient.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
// uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return a / b;
}
/**
* @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
/**
* @dev Adds two numbers, throws on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
contracts/DelegateV1.sol
pragma solidity ^0.4.18;
import "./SafeMath.sol";
import "./StorageState.sol";
contract DelegateV1 is StorageState {
using SafeMath for uint256;
function setNumberOfOwners(uint256 num) public {
_storage.setUint("total", num);
}
function getNumberOfOwners() view public returns (uint256) {
return _storage.getUint("total");
}
}
(4)代理合约
contracts/Ownable.sol
pragma solidity ^0.4.18;
/**
* @title Ownable
* @dev The Ownable contract has an owner address, and provides basic authorization control
* functions, this simplifies the implementation of "user permissions".
*/
contract Ownable {
address owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
constructor() public {
owner = msg.sender;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0));
emit OwnershipTransferred(owner, newOwner); // solhint-disable-line
owner = newOwner;
}
}
contracts/Proxy.sol
pragma solidity ^0.4.18;
import "./Ownable.sol";
import "./StorageState.sol";
contract Proxy is StorageState , Ownable {
constructor(KeyValueStorage storage_, address _owner) public {
_storage = storage_;
_storage.setAddress("owner", _owner);
}
event Upgraded(address indexed implementation);
address public _implementation;
function implementation() public view returns (address) {
return _implementation;
}
function upgradeTo(address impl) public onlyOwner {
require(
_implementation != impl,
"Cannot upgrade to the same implementation."
);
_implementation = impl;
emit Upgraded(impl);
}
function () public payable {
address _impl = implementation();
require(
_impl != address(0),
"Cannot set implementation to address(0)"
);
bytes memory data = msg.data;
assembly {
let result := delegatecall(gas, _impl, add(data, 0x20), mload(data), 0, 0)
let size := returndatasize
let ptr := mload(0x40)
returndatacopy(ptr, 0, size)
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
}
(5)部署测试
migrations/2_Storage_migration.js
const KeyValueStorage = artifacts.require("KeyValueStorage")
const Proxy = artifacts.require("Proxy")
module.exports = async function(deployer, network, accounts) {
deployer.deploy(KeyValueStorage).then(function() {
return deployer.deploy(Proxy, KeyValueStorage.address, accounts[0]);
});
};
migrations/3_v1_migration.js
const DelegateV1 = artifacts.require('DelegateV1')
module.exports = async function(deployer, network, accounts) {
deployer.deploy(DelegateV1);
};
在项目根目录位置执行进入控制台的命令:
$ truffle develop
编译部署:
> compile
> migrate
测试代码 test/test.js:
const KeyValueStorage = artifacts.require('KeyValueStorage')
const DelegateV1 = artifacts.require('DelegateV1')
const ProxyContract = artifacts.require('Proxy')
contract('Storage and upgradability example', async (accounts) => {
let proxy, delegateV1
it('initalize test', async() => {
proxy = await ProxyContract.deployed()
delegateV1 = await DelegateV1.deployed()
});
it('should ...', async () => {
await proxy.upgradeTo(delegateV1.address)
proxy = _.extend(proxy,DelegateV1.at(proxy.address));
await proxy.setNumberOfOwners(10);
let numOwnerV1 = await proxy.getNumberOfOwners();
console.log(numOwnerV1.toNumber())
});
});
运行测试代码:
> test
执行成功后,我们可升级合约模式已经跑通了。
(6)委托合约V2版本
开发新版本的业务合约 contracts/DelegateV2.sol
pragma solidity ^0.4.18;
import "./DelegateV1.sol";
import "./StorageState.sol";
contract DelegateV2 is StorageState {
modifier onlyOwner() {
require(
msg.sender == _storage.getAddress("owner"),
"msg.sender is not owner"
);
_;
}
function setNumberOfOwners(uint num) public onlyOwner {
_storage.setUint("total", num);
}
function getNumberOfOwners() public view returns (uint) {
return _storage.getUint("total");
}
}
(7)部署测试
V2合约的迁移脚本 migrations/2_v2_migration.js
const DelegateV2 = artifacts.require('DelegateV2')
module.exports = async function(deployer, network, accounts) {
deployer.deploy(DelegateV2);
};
编译部署:
> compile
> migrate
测试代码 test/test.js:
const KeyValueStorage = artifacts.require('KeyValueStorage')
const DelegateV1 = artifacts.require('DelegateV1')
const DelegateV2 = artifacts.require('DelegateV2')
const ProxyContract = artifacts.require('Proxy')
contract('Storage and upgradability example', async (accounts) => {
let proxy, delegateV2
it('initalize test', async() => {
proxy = await ProxyContract.deployed()
delegateV1 = await DelegateV1.deployed()
delegateV2 = await DelegateV2.deployed()
});
it('should create and upgrade idap token', async () => {
// 调用 v1 的 set 方法
await proxy.upgradeTo(delegateV1.address)
proxy = _.extend(proxy,DelegateV1.at(proxy.address));
await proxy.setNumberOfOwners(101);
// 调用 v2 的 get
await proxy.upgradeTo(delegateV2.address)
proxy = DelegateV2.at(proxy.address);
let numOwnerV2 = await proxy.getNumberOfOwners();
console.log(numOwnerV2.toNumber()) // 输出了 v1 设置的 101
});
});
运行:
> test