Solidity学习之代理合约
什么是代理合约
代理合约针对的是链上合约一经部署无法修改的问题,通过增加一层代理合约,就可以在不修改代理合约代码和地址的前提下,对实际执行的逻辑进行调整,满足了合约升级的需要。
实现逻辑
代理模式将状态变量存储在代理合约中,而逻辑执行在逻辑合约中。
通过delegateCall调用,执行逻辑合约的同时,改变的是代理合约中的状态变量,并且将执行结果返回给caller。
具体实现
代理合约
contract Proxy {
address public implementation; // 逻辑合约地址
/**
* @dev 初始化逻辑合约地址
*/
constructor(address implementation_){
implementation = implementation_;
}
/**
* @dev 回调函数,将本合约的调用委托给 `implementation` 合约
* 通过assembly,让回调函数也能有返回值
*/
fallback() external payable {
address _implementation = implementation;
assembly {
// 将msg.data拷贝到内存里
// calldatacopy操作码的参数: 内存起始位置,calldata起始位置,calldata长度
calldatacopy(0, 0, calldatasize())
// 利用delegatecall调用implementation合约
// delegatecall操作码的参数:gas, 目标合约地址,input mem起始位置,input mem长度,output area mem起始位置,output area mem长度
// output area起始位置和长度位置,所以设为0
// delegatecall成功返回1,失败返回0
let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)
// 将return data拷贝到内存
// returndata操作码的参数:内存起始位置,returndata起始位置,returndata长度
returndatacopy(0, 0, returndatasize())
switch result
// 如果delegate call失败,revert
case 0 {
revert(0, returndatasize())
}
// 如果delegate call成功,返回mem起始位置为0,长度为returndatasize()的数据(格式为bytes)
default {
return(0, returndatasize())
}
}
}
在代理合约中指定了一个implementation,即逻辑合约的地址,并且只实现了一个fallback()方法,所有发送给代理合约的调用都会通过fallback()发到逻辑合约上,代理合约只起到一个中转的作用。
代理合约的重点和难点就是fallback()的实现,代码中用assembly标明使用的是内联汇编的操作码,可以直接执行一些内存操作,而不是经过solidity的高级语法。
此处用到了5个内联汇编的方法:
calldatacopy(destOffset,dataOffset, size)calldatasize()returndatacopy(destOffset,dataOffset,size)returndatasize()delegatecall(gas,address,destOffset,size, destOffset,size)
其中两个size方法比较好理解,也就是拿到数据的长度。
而两个call方法做了类似的事情,即从某个数据来源处将数据复制到合约的memory内存中,比如calldatacopy()就会从calldata获取数据,而returndatacopy()则从returndata buffer里面获取,拿到的是上一次 call / delegatecall / staticcall 的返回值。
至于参数,第一个destOffset指向了内存的某个位置,是用来存放新数据的起始位置点,而dataOffset则是在获取数据时候的偏移值,比如calldatacopy的dataOffset为1,那么就是从calldata的第二个字节开始取值,一共向后取size个字节的数据。
最后就是delegatecall(),此处和高级语法中不同,参数用的都是内存值。首先指定了gas和调用的对象address,然后用四个参数标明calldata和returndata的存放位置,与copy的时候是一致的。
此时可以总结一下fallback()做的事情:
- 将
calldata复制到内存中起始为0的位置 - 用
delegatecall调用address的方法,calldata来自于内存,而对于returndata,最后一个参数为0,表示返回值的处理范围为0,也就是不做处理,这是因为把处理留到了后面。 - 将返回值从
returndata buffer中复制到内存中 - 根据调用成功与否,选择用
revert或return返回returndata
重点
- 不在
delegatecall中读取返回值是因为此时的返回值长度是不确定的,所以放到后面用returndatacopy去处理。 delegatecall处理返回值与否不会影响到returndata buffer中的数据,在下一次call调用之前,buffer中的返回值会一直存在。- 写法中
returndatacopy()的时候实际上覆盖了原来的calldata,因为写入的起点都是0,但因为calldata在delegatecall之后就没用了,所以这是安全的
逻辑合约
此处实现一个简单的逻辑合约即可,和正常的合约没什么区别。
contract Logic {
address public implementation; // 与Proxy保持一致,防止插槽冲突
uint public x = 99;
event CallSuccess(); // 调用成功事件
// 这个函数会释放CallSuccess事件并返回一个uint。
// 函数selector: 0xd09de08a
function increment() external returns(uint) {
emit CallSuccess();
return x + 1;
}
}
逻辑合约唯一要注意的点就是必须定义代理合约中的成员变量,这是因为delegatecall的时候是根据逻辑合约的成员变量位置而去修改代理合约中相同位置的状态量。
比如此处如果在函数中修改了implementation的值,因为implementtion位于slot[0]的位置,那么代理合约收到的修改指令同样是修改slot[0]的值,如果逻辑合约中implementation不是第一个定义的,而是第二个,位于slot[1]的位置,一旦发生修改,即使代理合约中并不存在slot[1]的变量,也依然会在内存位置里覆盖写入,造成不可预知的问题。
调用合约
contract Caller{
address public proxy; // 代理合约地址
constructor(address proxy_){
proxy = proxy_;
}
// 通过代理合约调用increment()函数
function increment() external returns(uint) {
( , bytes memory data) = proxy.call(abi.encodeWithSignature("increment()"));
return abi.decode(data,(uint));
}
}
在调用合约中,方法签名和参数都是根据逻辑合约来的,代理合约只起到中转的作用。
执行结果
最后返回的结果是1,这是因为increment去读取代理合约中的x值,位于slot[1],而代理合约在这个位置没有定义变量,为0,所以此时返回0+1 =1。
可升级合约
以代理合约为基础,在其中增加一个upgrade()函数,就可以实现逻辑合约的升级。
// 升级函数,改变逻辑合约地址,只能由admin调用
function upgrade(address newImplementation) external {
require(msg.sender == admin);
implementation = newImplementation;
}
通过require控制升级函数只能由admin调用,调用upgrade()后逻辑合约就会变成传入的地址。
选择器冲突
在solidity中,函数选择器是函数签名的哈希的前4个字节,因为字节数少,所以会出现不同函数的函数选择器相同的情况,被称为选择器冲突。
正常情况下,如果在合约中出现选择器冲突,会在编译的时候被编译器发现并报错,无法通过编译。
但是在可升级合约的场景中,因为代理合约实现了upgrade()方法,存在一种可能性,就是逻辑合约中有某个函数的函数选择器与upgrage()方法相同。调用这个方法时,data先传到了代理合约中,被认为是一次调用upgrade()的请求,从而出现了错误的调用。
严重的情况下,因为传入的参数不确定,可能会将逻辑合约指向黑洞地址。
为了解决这一问题,有透明代理和UUPS两种方法。
透明代理
透明代理的实现非常简单,通过权限隔离的方式,在upgrade()和fallback()方法中限制调用方的地址:upgrade()只能由管理员调用,而fallback()则必须由非管理员的地址调用。
这样一来就可以避免因为调用相同函数选择器而错误调用升级函数的情况,保证了升级的安全。
fallback() external payable {
require(msg.sender != admin);
(bool success, bytes memory data) = implementation.delegatecall(msg.data);
}
// 升级函数,改变逻辑合约地址,只能由admin调用
function upgrade(address newImplementation) external {
if (msg.sender != admin) revert();
implementation = newImplementation;
}
但逻辑函数中依然有可能存在与upgrade()函数选择器相同的方法,并且这个方法永远无法被调用。但是这种情况下并不会影响合约的安全所以可以忽视,只是在写合约时应当手动检查避免无效函数出现。
UUPS
UUPS是universal upgradeable proxy standard的缩写,指的是通用可升级代理标准。这一标准规定了升级合约要写在逻辑合约中,利用编译检查来避免选择器冲突的问题。
因为delegatecall的特性,所以在逻辑合约中依然可以判断msg的来源是否为管理员,并且修改代理合约中implementation的值,如下:
function upgrade(address newImplementation) external {
require(msg.sender == admin);
implementation = newImplementation;
}
二者的优劣势
UUPS:
- 代理合约更小(只负责 delegatecall)
- 升级逻辑灵活:每个实现合约可以定义自己的升级策略(如版本检测、权限校验)
- 省 gas:因为 upgrade 逻辑不在 Proxy 中
- ️ 如果你实现的
upgradeTo()没写权限控制,会被任意升级(重大安全风险) - ️ 升级逻辑自己写的不好,容易让合约变砖(如升级自己指向了一个非实现合约)
- ️ OpenZeppelin 要求必须继承
UUPSUpgradeable并带onlyProxy修饰函数,防止错误调用
透明代理:
- 最稳定、最传统的升级方式
- upgrade 权限由 Proxy 管理,不容易犯错
- 社区使用广泛,工具链支持成熟
- Proxy 更复杂、部署成本高
- 所有调用都要通过 proxy,admin 地址不能调用逻辑函数(会被拒绝)
- 无法动态调整 upgrade 权限策略(都是在 Proxy 里写死的)
Solidity学习之代理合约的更多相关文章
- solidity学习-cryptoPunks为实例
在这里使用cryptoPunks为实例来进行solidity的介绍,一般这些内容理解了就能够进行相对简单的智能合约的编写了,同时会添加一些我认为也十分重要的内容学习文档为http://solidity ...
- solidity 学习笔记(7)内联汇编
为什么要有内联汇编? //普通循环和内敛汇编循环比较 pragma solidity ^0.4.25; contract Assembly{ function nativeLoop() public ...
- solidity 学习笔记(6)call 函数
call() 方法 call()是一个底层的接口,用来向一个合约发送消息,也就是说如果你想实现自己的消息传递,可以使用这个函数.函数支持传入任意类型的任意参数,并将参数打包成32字节,相互拼接后向合约 ...
- solidity 学习笔记(5)接口
接口:不用实现方法,仅仅定义方法. pragma solidity ^; contract cat{ //cat实际上实现了接口animalEat,因为他们有相同的方法. string name; f ...
- solidity 学习笔记(3) 函数修饰符/继承
修饰符: 函数修饰符有 constant view pure 其中 constant和view的功能是一样的 不会消耗gas 也就是说不会做任何存储 constant在5.0以后的版本中被废弃 ...
- solidity 学习笔记 2 (二维数组)
solidity 二维数组: pragma solidity ^0.4.23; contract twoArray{ uint[2][3] grade =[[20,30],[40,50],[45,60 ...
- solidity学习笔记
一 pragam solidity ^0.4.23; contract helloword{ string public name ="hello"; function getN ...
- 以太坊智能合约开发 Solidity学习
1. pragma solidity >=0.4.22 <0.6.0;//版本号,头文件 contract BooleanTest { bool _a;//默认返回false int nu ...
- solidity 学习笔记(4)library库
library库的申明: library SafeMath{ functrion mul(uint a,uint b) public returns (uint){ uint c= a*b; asse ...
- cryptopunks的代码解释
1.imageHash就是将punk所有图像合在一起的那张图punks.png进行hash得到一个值,并将该值存储到链上,用处就是你可以通过将图像hash然后跟该值对比看图像对不对.这就是它的用处,在 ...
随机推荐
- AtCoder Beginner Contest 357-D
Problem For a positive integer \(N\), let \(V_N\) be the integer formed by concatenating \(N\) exact ...
- MongoDB入门实战教程(2)
上一篇我们了解了MongoDB的基本概念与单节点环境搭建,本篇我们来学习如何搭建一个高可用的复制集集群. 1 关于MongoDB复制集 MongoDB复制集的主要意义在于实现服务的高可用,它是Mong ...
- Layui 更新Table 表格内容的值
$.ajax({ //请求方式 type: "POST", //请求地址 url: "/", //数据,json字符串 data: { }, //请求成功 su ...
- 批量生成测试数据,再次迎来升级,支持API调用,开发者的好帮手
前端时间发表一篇文章介绍了FabricateData的在线批量生成测试数据的能力,这几天在看,平台不仅添加了本地数据源的概念,还增设了本地API的能力. FabricateData 网站地址:http ...
- notebook 开启 有限元学习
简介 jupyter-notebook --ip 0.0.0.0 开启 sudo docker run -ti -p 127.0.0.1:8888:8888 -v $(pwd):/home/fenic ...
- qt 显示中文
参考链接 CSDN Tips 直接使用第三种方法 也可以使用 QString::fromLocal8Bit("打开文档文件") 这种方式
- iga 入门之 强解表达式和 弱解表达式
简介 摘自 流体力学数值方法 弱解几分表达式 对Galerkin几分表达式(1-76)式进行分布几分,然后将自然边界条件带入表达式中,由此所获得的几分表达式,将作为Galerkin法求解的出发点.此时 ...
- 解非线性约束 Matlab
简介 matlab 解 非线性约束 函数 fmincon QU \[\min f(x)=x_{1}^{2}+x_{2}^{2}+x_{3}^{2}+8 \] \[\left\{\begin{array ...
- API开发平台,专注API高效开发平台
为什么要选择RestCloud API开发平台? API开发平台是RestCloud团队研发的基于微服务架构的专注API高效开发的专业化平台,与传统的API开发模式相比,具有更轻量级,开发速度更快,功 ...
- POLIR-Society-Organization-Lawsuits: (2020)粤0303民初16184号判决书
(2020)粤0303民初16184号判决书 深圳市罗湖区人民法院 送达公告页:https://guanwang.szlhfy.gov.cn/news/14209.cshtml 送达公告列表页(第16 ...