合约升级的诉求:业务逻辑升级,数据结构升级,原始数据保留.
传统升级方案
对旧合约做快照,然后把数据导入到新合约中。工作量大,需要脚本扫链,很容易出错。一旦出错,后果可能是无法承担的.保留了区块链特性,合约的更新是用户有感的
proxy-delegate合约升级框架
预备知识
call()
调用远程合约
delegateCall()
拉取远程合约逻辑在本合约环境执行.
特性:在本合约的环境执行远程代码,对数据的更新无视变量名,而是位置(插槽),因此可以扩展数据,也因此不能改变已有数据的位置顺序
fallback函数
function()external{
}
特征:没有入参,没有返回值,没有方法名,没有标记为payable时如果附带币直接报错
功能:当调用一个合约中不存在的函数时,将调用这个函数。在这里我们重载了这个函数,实现了重定向的功能
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) }
}
Solidity汇编 — Solidity develop 文档 (solidity-cn.readthedocs.io)
使用操作码直接与EVM进行交互。 可以对程序(=智能合约)要执行的操作进行更精细的控制,也可以节省gas
在合约中直接使用delegatecall()无法获取返回值,需要汇编辅助
思路和分析
将合约分为逻辑层和代理层,数据存储在代理层,用户调用的合约地址也是代理层,用detegateCall()拉取逻辑层代码在代理层执行
优势:
代理层合约地址不变,用户无感知
升级代价小
可保留历史记录
劣势:
违背了智能合约,区块链去中心,不可篡改等特性
逻辑层新合约必须继承老合约
合约数据可扩展,但不能更改已有的数据结构
示例
pragma solidity ^0.4.24;
contract logic {
address public l;
mapping(string => string) private store;
string public plus;
int public plus1;
function Store(string _key,string _value) public {
store[_key] = _value;
}
function get(string _key) public view returns (string) {
return store[_key];
}
function PlusMessage(string _val) public {
plus = _val;
}
function Plus1Message(int _val) public {
plus1 = _val;
}
}
pragma solidity ^0.4.24;
contract proxy {
address public logic;
mapping(string => string) private store;
constructor(address _logic) public {
logic = _logic;
}
function setLogic(address _logic) public {
logic = _logic;
}
function get(string memory _key)public view returns (string memory){
return store[_key];
}
function () payable public {
address _impl = logic;
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) }
}
}
function getPositionAt(uint n) public view returns (address) {
assembly {
let d := sload(n)
mstore(0x80, d)
return(0x80,32)
}
}
}
部署流程
初始化
逻辑合约必须包含代理合约中已有的全局变量,且顺序必须一致
- 部署逻辑合约 logic
- 部署代理合约 proxy,传入逻辑合约地址
- 使用代理合约地址调用逻辑合约的方法
升级
新的逻辑合约必须包含上一代合约中已有的全局变量,且顺序必须一致
- 部署新逻辑合约
- 在代理合约中设置新的逻辑合约地址
- 使用代理合约调用新逻辑合约方法