大型智能合约(Solidity)项目最佳实践
从 19 年的 5 月底开始,到现在(看文章发布时间),小型合约到大型、超大型合约也都实际做过项目了,比如 ERC20、ERC777 的代币 算是小型项目,然后到后面一个合约里面 2000+ 行代码的单体大型合约。再到后面逻辑越来越复杂,功能越加越多后的多合约协作框架。总体加起来超 5000 行代码(这不算 OpenZeppelin 提供的一些 util 库)。
架构
奶爸主业是一名后端开发工程师,后端有架构,从
- DNS · Anycast、分地区分运行商解析
- CDN、负载均衡
- 分库分表缓存、Queue
把整个项目从各个层面解耦,使系统变得可伸缩。那么智能合约(Solidity)呢?首先接入层(既 transaction 处理)这里属于以太坊基础设施负责的,如果做智能合约开发的话,基本上考虑不到,除非是几个亿的项目 😂 ,那么 TPS 优化这快基本上就是 Layer 2 优化,这里有个 GitHub 仓库,可以看一下:https://github.com/Awesome-Layer-2/awesome-layer-2
👌 我们废话不多说,说一下大型智能合约的架构,主要就是下面这一种,多合约协作。
奶爸呢,是从 NexusMutual 中抽取出来的一个合约协作框架,然后公司几个项目中一直在用,他(之前奶爸抽取的时候,现在也往后发展了不少)是这样的结构:
- Master.sol 协作合约管理,存放各协作合约的合约地址,未各个协作合约提供注册管理、地址验证、合约升级的功能。
- Iupgradable.sol 各个协作合约的父类,提供一些协作鉴权使用的工具方法,和更新协作合约依赖、更新 Master 合约地址等方法。
他这个架构中主要就是这么实现的。
优点
可以适合超大型合约架构,像我司之前是单合约 2000+ 行代码,一不小心就 bytecode
超限了,势必要砍掉一些东西才被允许部署到 mainnet
。使用了这种多合约协作架构后可以将代码分散到数个合约中协同工作,我司目前使用的是 数据逻辑分离(部分逻辑混合在数据合约中)的架构。
优点主要就是 可构建超复杂合约项目,还有一个另外的优点也顺带提一提 隐藏合约背后逻辑,这种合约协作架构中,只需要暴露出协作合约的 interface
中定义的接口来,然后在 etherscan 验证代码即可,隐藏在合约背后的逻辑 可能很多。
比如一个合约暴露给协作合约的接口是这样的:
interface IBullshit {
function aGoodFunc(address payable goodMan) external;
}
而它内部实现的逻辑又是这样的:
class Bullshit is IBullshit {
function aGoodFunc(address payable devil) public onlyOwner {
decil.transfer(address(this).balance);
};
}
弊端
多合约协作面临着跨合约调用,每次跨合约调用就是 6000 gas,还不包含被调用方法内部消耗的 gas,对于数据合约分离的这种架构来说,需要的不同的数据越多,读写次数越多,消耗的 gas 是非常多的,最近(8 月 20 日) gasPrice 居高不下换算下来的成本可谓不菲。
优化
- 对于跨合约调用可以把一些需要的数据和写操作尽量归集到一次调用中,既保证扩展性又最大程度减少跨合约调用。
- 不要使用
users[id] = User();
这种形式来储存或修改一个struct
会十分消耗 gas,一定要一个字段一个字段的去修改。 - 对于大量的相同类型的字段,比如一个
struct
定义type Uints struct { uint u1, uint u2, uint u3 ... uint u20};
可以把字段全部归集到数组里面uint[20] uints;
来简化数据结构方便传递和修改。 - 对于只有外部调用的方法可以使用
external
修饰符代替public
会减少一丢丢 gas 消耗。 - 不要在写操作内放置不确定循环次数的
for
循环,循环次数上来之后 gas 消耗非常高
好的智能合约项目要求工程师要十分了解业务逻辑,对数据、逻辑作出合理的规划,尽量减小用户使用合约的 Gas 成本和公司后期运营成本。
坑
fallback 方法
- 有些逻辑比较复杂的方法 不建议使用 fallback 来触发,某些钱包(imToken)的 Gas 会预估不足,导致执行失败。
- 在合约互相调用时,fallback 方法内不可占用太多 gas,超过一定数量 gasUsed 就会失败(安全的做法),查看这里的展开说明 https://ethereum.stackexchange.com/questions/5992/how-much-computation-can-be-done-in-a-fallback-function
- 合约内部调用其他合约方法时的 option https://solidity.readthedocs.io/en/latest/control-structures.html
安全
有了一个好的架构之后还远远不够,安全和发布前的完全测试是十分重要的,就在前两天 Yam 这个币在 Uniswap 上面暴涨 100 倍又接近归零的事情,给我们敲响了警钟。
Production Ready 的库
- 🥇 https://github.com/OpenZeppelin 主要关注
- contracts/access/Ownable.sol 基本的管理员权限
- contracts/math/SafeMath.sol 数据类型安全
- token/ERC20/SafeERC20.sol 安全的 ERC20 操作库
- contracts/utils/Pausable.sol 可在紧急情况下暂停合约
- contracts/utils/ReentrancyGuard.sol 防止重入攻击(这里有一篇讲重入攻击的 Paper https://paper.seebug.org/801/ )
- ⚠️ 这是奶爸近期看到的基于
delegatecall
的一种合约协作和升级的方案,看起来比较不错,但未投入使用过 https://github.com/mudgen/Diamond
测试工具
越大的系统越要在小处做好,系统出现问题不好去定位,都是成本。奶爸在这里推荐的方式有两种:
- 基于浏览器的 Remix IDE,功能很完善,集开发编译部署测试调试一条龙的开发环境,不过多合约协作的架构部署起来就很费劲了,也没法定义部署流程,只能人力 one by one 的部署。
- truffle 虽然奶爸自己有给公司弄了一套基于 Golang 的 测试、部署 的工具,但是不是时分完善,不如 truffle 的生态鉴权,且 truffle 还有自己的配套测试链 ganache (吐槽一下,这个 ganache 跑我们的逻辑的时候,debug 的过程中内存耗光「占用 12G」也没能对一个 transaction 进行步进调试。)虽然 ganache 调试复杂逻辑很鸡肋,但是配合 truffle 跑测试是很 nice 的。
- truffle 的一个 assert 库:https://github.com/rkalis/truffle-assertions 对于验证 event 的触发和验证预期结果很有用
- OpenZeppelin 的一个测试用工具类库 https://github.com/OpenZeppelin/openzeppelin-test-helpers,也包含一些 event 的触发检测、主要是 可以自主 挖掘新区块(增长区块高度) 和 增长链上时间 方便测试一些与链上时间和区块高度绑定的逻辑
- truffle 和 remix IDE 的单元测试都没问题,集成测试还是推荐 truffle,除非你像奶爸一样牛自己去做一套开发工具出来。
单元测试、继承测试都是必须要做的,对于保障整个项目的质量有根本性的意义。
保命
「项目中去中心化治理应循序渐进,在项目开始阶段,需要设置适当的权限以防发生黑天鹅事件。」—— 慢雾科技评论 Yam 代码问题导致崩盘的文章 如是说。
奶爸在这里也以一个一年项目,一次升级迁移的过来人的的身份告诉大家,一定要预留一些管理接口或者暂停合约这种接口来防止发生黑天鹅事件。
同样自己公司的项目中逻辑数据分离的合约留足增删改查接口,根据墨菲定律,一定会有修改数据的需求。