我们为什么要升级智能合约呢?
如今智能合约的生态已经比较完善。无论我们在编码时多么的仔细,测试多么的严密,如果我们构建一个复杂的项目,那么很有可能我们会需要更新业务逻辑,给系统打补丁,修复bug或添加新的特性。有时,我们可能因为EVM的改变或因为新的漏洞的发现而需要更新我们的合约代码。
通常情况下,对于传统的开发项目来说,开发者可以随时更新代码。但是智能合约与此不同,它一旦部署完成之后代码就无法修改。
还记得当面Parity多签名钱包被黑,导致150000以太币被偷的恶劣事件吗?在整个攻击中,就因为钱包中的一个bug导致很多巨额钱包的资金被清空。而唯一的解决方案就是尝试以比黑客更快的速度,利用相同的漏洞攻击剩余的钱包,来把以太币重新分配给它们合法的所有者。
如果我们可以使用合适的技术,我们可以部署一个新的合约,并且抛弃旧的合约,那该多好…
尽管想升级已经部署的智能合约中的代码是不可能的,但是可以通过设计一个代理合约结构,这个结构可以让你可以通过新部署一个合约的方式,来实现升级主要的处理逻辑的目的。
代理结构模式就像下面这张图一样:所有消息通过一个代理合约来间接调用最新部署的逻辑合约。如果想要升级的话,只需要部署一个新的合约,然后在代理合约中更新引用新的合约地址就可以了。
继承存储模式 Inherited Storage
永久存储模式 Eternal Storage
非结构化存储模式 Unstructured Storage
所有三种模式都依赖低阶的delegatecall。尽管solidity提供了一个delegatecall方法,但它只能返回true或者false来显示调用是否成功,而不是允许你操作返回的数据。
当调用一个合约中并不支持的的方法时,就会调用合约中的fallback方法。你可以自己写一个fallback函数来处理这种场景。代理合约就是用自定义的fallback方法将调用重定向到其他合约实现。
每当合约A授权对另一个合约B的调用时,它就会在合约A的上下文中执行合约B的代码。这就意味着msg.value和msg.sender的值会被保留。并且对存储的修改将会作用在合约A的存储上。
假设personA调用了contractA中的functionA,这个方法内部同时使用了delegatecall调用了contractB中的functionB,那么对于functionB来说,msg.sender依然是personA,而不是contractA。
delegatecall并不通过变量名称来修改变量值,而是修改变量所在的存储槽。
在代理合约中,为了可以返回调用逻辑合约后的结果,实现了自己的delegatecall方法,所有模式都是这样。如果你想要使用下列的代理合约代码,你就要理解代码的每一个细节。让我们先来看看它是如何发挥作用的,以及理解为了达到目的它所使用的assembly操作码。
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize)
let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
let size := returndatasize
returndatacopy(ptr, 0, size)
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
为了授权对另一个合约中方法的调用,我们需要把它赋值给proxy合约接收的msg.data。因为msg.data是bytes类型的,是一个动态的数据结构,所以它在msg.data的第一个字(word,也就是32个字节)中的存储长度会不一样。如果我们想要只取出真正的数据,我们需要跳过第一个字(word),从msg.data的0x20(32个字节)开始。然而,我们会用到两个操作码来实现此目的。我们会使用calldatasize来获取msg.data的大小,以及calldatacopy来把它复制到ptr所指向的位置。
注意到我们是如何初始化ptr变量的。在solidity中,内存槽中的0x40位置是很特殊的,因为它存储了指向下一个可用自由内存的指针。每次当你想往内存里存储一个变量时,你都要检查存储在0x40的值。这就是你变量即将存放的位置。现在我们知道了我们要在哪儿存变量,我们就可以使用calldatacopy,把大小为calldatasize的calldata从0开始啊复制到ptr指向的那个位置了。
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize)
我们再看看下面的assembly代码(使用delegatecall操作码)
let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
gas:函数执行所需的gas
_impl:我们调用的逻辑合约的地址
ptr:内存指针(指向数据开始存储的地方)
calldatasize:传入的数据大小
0:调用逻辑合约后的返回值。我们没有使用这个参数因为我们还不知道返回值的大小,所以不能把它赋值给一个变量。我们可以后面可以进一步使用returndata操作码来获取这些信息。
0:返回值的大小。这个参数也没有被使用因为我们没有机会创造一个临时变量用来存储返回值。鉴于我们在调用其他合约之前无法知道它的大小(所以就无法创造临时变量呀)。我们稍后可以用returndatasize操作码来得到这个值。
下面一行代码就是用returndatasize操作码得到了返回数据的大小:
let size := returndatasize
我们使用这个返回值的大小,来把返回值复制到ptr指向的内存,使用returndatacopy来达到这个目的:
returndatacopy(ptr, 0, size)
最后,switch语句要么返回 【返回值】,要么抛出错误,如果发生错误的话。
这三种方法用不同的方法来解决同一个难点:怎样确保逻辑合约不会重写/覆盖代理中的状态变量。
任何代理结构模式的主要问题就是如何分配存储。记住,既然我们使用一个合约来存储,另一个合约来实现逻辑,它们中的任何一个都有可能重写一个已经使用的存储槽。这意味着如果代理合约有一个状态变量在某个存储槽中存储着最新的逻辑合约地址,但是逻辑合约却不知道的话,那么逻辑合约可能就会在那个槽中存一些其他数据,这样就把代理合约中的重要信息覆盖了。
非结构化存储模式,不要求逻辑合约继承任何和升级相关的状态变量。这个模式使用代理合约中定义的非结构化的存储槽来保存升级所需的数据。
在代理合约中,我们定义了一个常量,每当哈希的时候,就给出一个足够随机的存储位置来存储代理合约需要调用的逻辑合约的地址。
bytes32 private constant implementationPosition =
keccak256("org.mytoken.proxy.implementation");
因为常量(恒定)状态变量并不占用存储槽,所以并不用担心implementationPosition会不小心被逻辑合约占用。鉴于solidity在存储中放置状态变量的方法,依然有非常非常非常小的概率可能发生要存储新变量的存储槽已经被占用了。
这个模式也使用了代理合约所有权的概念。只有代理合约的所有者可以更新逻辑合约的地址,也是唯一可以转移所有权的地址。
pragma solidity ^0.4.21;
/**
* @title Proxy
* @dev Gives the possibility to delegate any call to a foreign implementation.
*/
contract Proxy {
/**
* @dev Tells the address of the implementation where every call will be delegated.
* @return address of the implementation to which it will be delegated
*/
function implementation() public view returns (address);
/**
* @dev Fallback function allowing to perform a delegatecall to the given implementation.
* This function will return whatever the implementation call returns
*/
function () payable public {
address _impl = implementation();
require(_impl != address(0));
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize)
let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
let size := returndatasize
returndatacopy(ptr, 0, size)
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
}
/**
* @title UpgradeabilityProxy
* @dev This contract represents a proxy where the implementation address to which it will delegate can be upgraded
*/
contract UpgradeabilityProxy is Proxy {
/**
* @dev This event will be emitted every time the implementation gets upgraded
* @param implementation representing the address of the upgraded implementation
*/
event Upgraded(address indexed implementation);
// Storage position of the address of the current implementation
bytes32 private constant implementationPosition = keccak256("org.mytoken.proxy.implementation");
/**
* @dev Constructor function
*/
function UpgradeabilityProxy() public {}
/**
* @dev Tells the address of the current implementation
* @return address of the current implementation
*/
function implementation() public view returns (address impl) {
bytes32 position = implementationPosition;
assembly {
impl := sload(position)
}
}
/**
* @dev Sets the address of the current implementation
* @param newImplementation address representing the new implementation to be set
*/
function setImplementation(address newImplementation) internal {
bytes32 position = implementationPosition;
assembly {
sstore(position, newImplementation)
}
}
/**
* @dev Upgrades the implementation address
* @param newImplementation representing the address of the new implementation to be set
*/
function _upgradeTo(address newImplementation) internal {
address currentImplementation = implementation();
require(currentImplementation != newImplementation);
setImplementation(newImplementation);
emit Upgraded(newImplementation);
}
}
/**
* @title OwnedUpgradeabilityProxy
* @dev This contract combines an upgradeability proxy with basic authorization control functionalities
*/
contract OwnedUpgradeabilityProxy is UpgradeabilityProxy {
/**
* @dev Event to show ownership has been transferred
* @param previousOwner representing the address of the previous owner
* @param newOwner representing the address of the new owner
*/
event ProxyOwnershipTransferred(address previousOwner, address newOwner);
// Storage position of the owner of the contract
bytes32 private constant proxyOwnerPosition = keccak256("org.mytoken.proxy.owner");
/**
* @dev the constructor sets the original owner of the contract to the sender account.
*/
function OwnedUpgradeabilityProxy() public {
setUpgradeabilityOwner(msg.sender);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyProxyOwner() {
require(msg.sender == proxyOwner());
_;
}
/**
* @dev Tells the address of the owner
* @return the address of the owner
*/
function proxyOwner() public view returns (address owner) {
bytes32 position = proxyOwnerPosition;
assembly {
owner := sload(position)
}
}
/**
* @dev Sets the address of the owner
*/
function setUpgradeabilityOwner(address newProxyOwner) internal {
bytes32 position = proxyOwnerPosition;
assembly {
sstore(position, newProxyOwner)
}
}
/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferProxyOwnership(address newOwner) public onlyProxyOwner {
require(newOwner != address(0));
emit ProxyOwnershipTransferred(proxyOwner(), newOwner);
setUpgradeabilityOwner(newOwner);
}
/**
* @dev Allows the proxy owner to upgrade the current version of the proxy.
* @param implementation representing the address of the new implementation to be set.
*/
function upgradeTo(address implementation) public onlyProxyOwner {
_upgradeTo(implementation);
}
/**
* @dev Allows the proxy owner to upgrade the current version of the proxy and call the new implementation
* to initialize whatever is needed through a low level call.
* @param implementation representing the address of the new implementation to be set.
* @param data represents the msg.data to bet sent in the low level call. This parameter may include the function
* signature of the implementation to be called with the needed payload
*/
function upgradeToAndCall(address implementation, bytes data) payable public onlyProxyOwner {
upgradeTo(implementation);
require(this.call.value(msg.value)(data));
}
}
pragma solidity ^0.4.21;
/**
* @title SafeMath
* @dev Math operations with safety checks that throw on error
*/
library SafeMath {
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;
}
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 c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
/**
* @title Ownable
* @dev This contract has the owner address providing basic authorization control
*/
/**
* @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 public owner;
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
function Ownable() public {
owner = msg.sender;
}
/**
* @dev Sets a new owner address
*/
function setOwner(address newOwner) internal {
owner = newOwner;
}
/**
* @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));
setOwner(newOwner);
}
}
/**
* @title ERC20Basic
* @dev Simpler version of ERC20 interface
* @dev see https://github.com/ethereum/EIPs/issues/20
*/
contract ERC20Basic {
uint public _totalSupply;
function totalSupply() public constant returns (uint);
function balanceOf(address who) public constant returns (uint);
function transfer(address to, uint value) public returns (bool);
event Transfer(address indexed from, address indexed to, uint value);
}
/**
* @title ERC20 interface
* @dev see https://github.com/ethereum/EIPs/issues/20
*/
contract ERC20 is ERC20Basic {
function allowance(address owner, address spender) public constant returns (uint);
function transferFrom(address from, address to, uint value) public returns (bool);
function approve(address spender, uint value) public returns (bool);
event Approval(address indexed owner, address indexed spender, uint value);
}
/**
* @title Basic token
* @dev Basic version of StandardToken, with no allowances.
*/
contract BasicToken is Ownable, ERC20Basic {
using SafeMath for uint;
mapping(address => uint) public balances;
// additional variables for use if transaction fees ever became necessary
uint public basisPointsRate = 0;
uint public maximumFee = 0;
/**
* @dev Fix for the ERC20 short address attack.
*/
modifier onlyPayloadSize(uint size) {
require(!(msg.data.length < size + 4));
_;
}
/**
* @dev transfer token for a specified address
* @param _to The address to transfer to.
* @param _value The amount to be transferred.
*/
function transfer(address _to, uint _value) public onlyPayloadSize(2 * 32) returns (bool) {
uint fee = (_value.mul(basisPointsRate)).div(10000);
if (fee > maximumFee) {
fee = maximumFee;
}
uint sendAmount = _value.sub(fee);
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(sendAmount);
if (fee > 0) {
balances[owner] = balances[owner].add(fee);
emit Transfer(msg.sender, owner, fee);
}
emit Transfer(msg.sender, _to, sendAmount);
return true;
}
/**
* @dev Gets the balance of the specified address.
* @param _owner The address to query the the balance of.
* @return An uint representing the amount owned by the passed address.
*/
function balanceOf(address _owner) public constant returns (uint balance) {
return balances[_owner];
}
}
/**
* @title Standard ERC20 token
*
* @dev Implementation of the basic standard token.
* @dev https://github.com/ethereum/EIPs/issues/20
* @dev Based oncode by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
*/
contract StandardToken is BasicToken, ERC20 {
mapping (address => mapping (address => uint)) public allowed;
uint public constant MAX_UINT = 2**256 - 1;
/**
* @dev Transfer tokens from one address to another
* @param _from address The address which you want to send tokens from
* @param _to address The address which you want to transfer to
* @param _value uint the amount of tokens to be transferred
*/
function transferFrom(address _from, address _to, uint _value) public onlyPayloadSize(3 * 32) returns (bool) {
var _allowance = allowed[_from][msg.sender];
// Check is not needed because sub(_allowance, _value) will already throw if this condition is not met
// if (_value > _allowance) throw;
uint fee = (_value.mul(basisPointsRate)).div(10000);
if (fee > maximumFee) {
fee = maximumFee;
}
if (_allowance < MAX_UINT) {
allowed[_from][msg.sender] = _allowance.sub(_value);
}
uint sendAmount = _value.sub(fee);
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(sendAmount);
if (fee > 0) {
balances[owner] = balances[owner].add(fee);
emit Transfer(_from, owner, fee);
}
emit Transfer(_from, _to, sendAmount);
return true;
}
/**
* @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
* @param _spender The address which will spend the funds.
* @param _value The amount of tokens to be spent.
*/
function approve(address _spender, uint _value) public onlyPayloadSize(2 * 32) returns (bool) {
// To change the approve amount you first have to reduce the addresses`
// allowance to zero by calling `approve(_spender, 0)` if it is not
// already 0 to mitigate the race condition described here:
// https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
require(!((_value != 0) && (allowed[msg.sender][_spender] != 0)));
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
/**
* @dev Function to check the amount of tokens than an owner allowed to a spender.
* @param _owner address The address which owns the funds.
* @param _spender address The address which will spend the funds.
* @return A uint specifying the amount of tokens still available for the spender.
*/
function allowance(address _owner, address _spender) public constant returns (uint remaining) {
return allowed[_owner][_spender];
}
}
/**
* @title Pausable
* @dev Base contract which allows children to implement an emergency stop mechanism.
*/
contract Pausable is Ownable {
event Pause();
event Unpause();
bool public paused = false;
/**
* @dev Modifier to make a function callable only when the contract is not paused.
*/
modifier whenNotPaused() {
require(!paused);
_;
}
/**
* @dev Modifier to make a function callable only when the contract is paused.
*/
modifier whenPaused() {
require(paused);
_;
}
/**
* @dev called by the owner to pause, triggers stopped state
*/
function pause() onlyOwner whenNotPaused public {
paused = true;
emit Pause();
}
/**
* @dev called by the owner to unpause, returns to normal state
*/
function unpause() onlyOwner whenPaused public {
paused = false;
emit Unpause();
}
}
contract BlackList is Ownable, BasicToken {
/// Getters to allow the same blacklist to be used also by other contracts (including upgraded Tether) ///
function getBlackListStatus(address _maker) external constant returns (bool) {
return isBlackListed[_maker];
}
function getOwner() external constant returns (address) {
return owner;
}
mapping (address => bool) public isBlackListed;
function addBlackList (address _evilUser) public onlyOwner {
isBlackListed[_evilUser] = true;
emit AddedBlackList(_evilUser);
}
function removeBlackList (address _clearedUser) public onlyOwner {
isBlackListed[_clearedUser] = false;
emit RemovedBlackList(_clearedUser);
}
function destroyBlackFunds (address _blackListedUser) public onlyOwner {
require(isBlackListed[_blackListedUser]);
uint dirtyFunds = balanceOf(_blackListedUser);
balances[_blackListedUser] = 0;
_totalSupply -= dirtyFunds;
emit DestroyedBlackFunds(_blackListedUser, dirtyFunds);
}
event DestroyedBlackFunds(address _blackListedUser, uint _balance);
event AddedBlackList(address _user);
event RemovedBlackList(address _user);
}
contract MyToken is Pausable, StandardToken, BlackList {
string public name;
string public symbol;
uint public decimals;
bool internal _initialized;
function BaseWineToken() public {}
function transfer(address _to, uint _value) public whenNotPaused returns (bool success){
require(!isBlackListed[msg.sender]);
return super.transfer(_to, _value);
}
function transferFrom(address _from, address _to, uint _value) public whenNotPaused returns (bool) {
require(!isBlackListed[_from]);
return super.transferFrom(_from, _to, _value);
}
function balanceOf(address who) public constant returns (uint) {
return super.balanceOf(who);
}
function approve(address _spender, uint _value) public onlyPayloadSize(2 * 32) returns (bool) {
return super.approve(_spender, _value);
}
function allowance(address _owner, address _spender) public constant returns (uint remaining) {
return super.allowance(_owner, _spender);
}
// deprecate current contract if favour of a new one
function totalSupply() public constant returns (uint) {
return _totalSupply;
}
// The contract can be initialized with a number of tokens
// All the tokens are deposited to the owner address
//
// @param _balance Initial supply of the contract
// @param _name Token Name
// @param _symbol Token symbol
// @param _decimals Token decimals
function initialize(address owner) public {
require(!_initialized);
_totalSupply = 0;
name = "My Token";
symbol = "MT";
decimals = 6;
balances[owner] = 0;
_initialized = true;
setOwner(owner);
}
// Issue a new amount of tokens
// these tokens are deposited into the owner address
//
// @param _amount Number of tokens to be issued
function issue(uint amount) public onlyOwner {
require(_totalSupply + amount > _totalSupply);
require(balances[owner] + amount > balances[owner]);
balances[owner] += amount;
_totalSupply += amount;
emit Issue(amount);
}
// Redeem tokens.
// These tokens are withdrawn from the owner address
// if the balance must be enough to cover the redeem
// or the call will fail.
// @param _amount Number of tokens to be issued
function redeem(uint amount) public onlyOwner {
require(_totalSupply >= amount);
require(balances[owner] >= amount);
_totalSupply -= amount;
balances[owner] -= amount;
emit Redeem(amount);
}
function setParams(uint newBasisPoints, uint newMaxFee) public onlyOwner {
// Ensure transparency by hardcoding limit beyond which fees can never be added
require(newBasisPoints < 20);
require(newMaxFee < 50);
basisPointsRate = newBasisPoints;
maximumFee = newMaxFee.mul(10**decimals);
emit Params(basisPointsRate, maximumFee);
}
function changeName(string _name, string _symbol) public onlyOwner {
name = _name;
symbol = _symbol;
}
// Called when new token are issued
event Issue(uint amount);
// Called when tokens are redeemed
event Redeem(uint amount);
// Called if contract ever adds fees
event Params(uint feeBasisPoints, uint maxFee);
}
部署OwnedUpgradeabilityProxy实例
部署逻辑合约的初始版本(V1)
调用OwnedUpgradeabilityProxy实例来更新到初始版本的逻辑合约
如果你的逻辑合约依赖自己的构造函数(constructor)来设置某个初始状态,那么在它和代理合约产生联系之后,之前的这些状态就要重新修改,因为代理合约的存储并不知道(逻辑合约里的)这些值。OwnedUpgradeabilityProxy有一个upgradeToAndCall方法专门来调用一些逻辑合约中的方法,一旦代理合约升级到最新版本时,就把连接到的那个逻辑合约里的初始设置重新设置一遍。
部署一个新版本的逻辑合约(V2),确保它继承了上一个版本里的状态变量结构。
调用ownedUpgradeabilityProxy实例来升级到新版本合约的地址。
这个方法很棒,因为它不需要逻辑合约知道它是整个代理系统的一部分。
重要:如果你的逻辑合约依赖自己的构造器来设置一些初始状态的话,这个过程在新版本的逻辑合约注册到代理中时需要重新做一遍。举个例子,逻辑合约继承Zeppelin中的Ownable合约,这很常见。当你的逻辑合约继承Ownable,它也就继承了Ownable的构造器,构造器会在合约创建的时候就设置合约的所有者是谁。当你让代理合约来使用你的逻辑合约的时候,代理合约是不知道逻辑合约的所有者是谁的。
升级代理合约的一种常见的模式就代理立即对逻辑合约调用一个初始化方法。这个初始化方法应该去模仿在构造器中做的一些事情。同时你也想要一个标识,用来确保你不可以再次对同一个逻辑合约调用初始化方法。(只能调用一次)
你的逻辑合约看上去可能像下面这样:
contract MyToken is Pausable, StandardToken, BlackList{
...
bool internal _initialized;
function initialize(address owner) public {
require(!_initialized);
setOwner(owner);
_initialized = true;
...
}
...
}
当然这取决于你的部署策略,你可以写一个帮助部署的合约,或者你可以可以单独部署代理合约和逻辑合约。如果你单独部署的话,你需要使用upgradeToAndCall把代理合约链接到逻辑合约上,这看上去就会像下面这样:
const initializeData = encodeCall('initialize', ['address'], [tokenOwner])
await proxy.upgradeToAndCall(logicContract.address, initializeData, { from: proxyOwner })
代理模式的概念已经出来有一段时间了,但是由于太复杂了、害怕引入安全漏洞以及绕过了区块链不可变的特性,它还没有被广泛接受。过去的解决方法在关于未来版本的逻辑合约可以添加和修改的东西上有严格的限制,这很不灵活。但是很显然,开发者对于可升级合约的需求很迫切。
原文:How to make smart contracts upgradable!
Proxy Patterns
OpenZeppelin 源码: OpenZeppelin
智能合约升级模式介绍
深度理解delegatecall