0%

OpenZeppelin-ERC20-extensions源码学习(一)

Pre:

对OpenZeppelin_ERC20_extensions文件夹里的自定义拓展合约,源码学习一波。

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/README.adoc

目录:

Contract 使用场景
ERC20Burnable 对自己或有授权额度的地址进行代币销毁
ERC20Capped 在铸造代币时对总供应量设定一个不可变的上限值
ERC20Pausable 具有暂停代币传输的功能
ERC20Snapshot 有效存储过去的代币余额/供应量,以便以后随时查询

ERC20Burnable:

使用场景

对自己或有授权额度的地址进行代币销毁,使得代币总供应量↓,可能会让用户觉得自己持有的币会越来越有价值。。。

源码解析:

多了一个burnFrom()委托销毁的函数

重要函数变量表:

Contract Function/Variable 作用 操作
ERC20Burnable burn(uint256 amount) token持有者销毁自己的token 调用_burn函数,余额↓,总供应量↓
ERC20Burnable burnFrom(address account, uint256 amount) token授权者销毁指定地址的token caller授权额度↓,指定地址余额↓,总供应量↓

Inspire:

授权额度给别人或合约之后,第三者不只是可以拿来转账,也可以用来销毁,简言之就是具备了控制权

ERC20Capped:

使用场景

在代币的构造函数里对总供应量设定一个不可变的上限值,可防止在代码实现中不小心改动了这个值,从而防止代币无限增发。

注意:是上限值不可变,总供应量还是可变的,只是上限值被写死了。

源码解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 重要代码片段:

uint256 private immutable _cap; // 不可变常量

// 只在构造函数里,初次也只能有一次去定义该上限值
constructor(uint256 cap_) {
require(cap_ > 0, "ERC20Capped: cap is 0");
_cap = cap_;
}

function _mint(address account, uint256 amount) internal virtual override {
// 限制铸造增发不能超过 上限值
require(ERC20.totalSupply() + amount <= cap(), "ERC20Capped: cap exceeded");
super._mint(account, amount);
}

重要函数变量表:

Contract Function/Variable 作用 操作
ERC20Capped constructor(uint256 cap_) 只有在构造函数能够修改代币供应量上限的值 提前定义上限值为immutable的变量
ERC20Capped cap() 返回上限值 ——
ERC20Capped _mint(address account, uint256 amount) 铸币增发 判断:代币供应量+增发量<= 上限值

Inspire:

看来solidity里的各类修饰符 如immutable,还是大有学问的,在某些特定场景上使用起来能够起到合适的限制,后面要仔细总结一下

同理,对于某些通缩机制的代币,也可以设定一个下限值,在不断销毁代币时,也不能低于这个下限值

ERC20Pausable:

使用场景

出bug的时候,可以由管理员设置暂停开关,阻止代币的传输、铸造和燃烧等功能,相当于一个紧急停止机制。

源码解析:

1
2
3
4
5
6
7
8
9
10
11
12
// 重要代码片段:

// 重写_beforeTokenTransfer,增加对 暂停开关值的判断
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual override {
super._beforeTokenTransfer(from, to, amount);

require(!paused(), "ERC20Pausable: token transfer while paused");
}

ERC20Pausable.sol继承Pausable.solPausable.sol里围绕一个_paused暂停开关写了几个eventmodifier,核心还是这个暂停开关变量。

如果自己的erc20合约想要实现暂停开关功能的话,继承ERC20, Pausable合约,重写_beforeTokenTransfer函数,在里面加上对暂停开关的判断条件就可以了。

其中_beforeTokenTransfer函数在_transfer函数里被调用到。

ERC20.sol_transfer函数里可以分成3个阶段:

  1. token转移之前,_beforeTokenTransfer()

  2. token转移时的具体处理逻辑(一般是sender余额↓,recipient余额↑)

  3. token转移之后,_afterTokenTransfer

控制了_beforeTokenTransfer函数后,就不会进入到token转移时的具体处理逻辑,也就控制了代币的转账、铸造、销毁这三类行为,从而实现了暂停功能。

重要函数变量表:

Contract Function 作用 操作
Pausable constructor() —— 暂停开关初始化 为关
ERC20Pausable function _beforeTokenTransfer(address from,address to,uint256 amount) 阻止进入token转移的处理逻辑 加上对暂停开关变量的判断

ERC20Snapshot:

使用快照机制扩展了 ERC20 代币。创建快照时,记录用户余额和当时的总供应量,以供以后访问。

快照不是每个区块或每隔一段时间都会自动生成的,快照的周期依附于你设定的快照策略。

比如你可以设定每次Transfer执行一次快照等

使用场景:

  • 这可用于安全地创建基于代币余额的机制,例如无需信任的股息或加权投票。

  • 可以通过重用来自不同地方的相同余额来执行“双花”攻击帐户。通过使用快照来计算股息或投票权,这些攻击不再适用。

  • 它也可以用于创建高效的 ERC20 分叉机制

源码解析:

开始看之前有一些疑问:

  1. 用户余额该用什么数据结构来存储?

  2. 什么时候、多少周期创建一个快照?

存储的数据结构:

  • 存储用户的余额:

1
2
3
4
5
6
struct Snapshots {
uint256[] ids; // 存快照id
uint256[] values; // 存余额/总供应量
}

mapping(address => Snapshots) private _accountBalanceSnapshots;

存储用户余额的变量_accountBalanceSnapshots是一个二维的数据结构,类似于python里一个dict里存放两个list,两个list里的值索引是相对应的:

1
2
3
"addr":地址做键值
ids数组:存快照id
value:存余额
  • 存储总供应量:

1
Snapshots private _totalSupplySnapshots;

存储代币总供应量的变量_totalSupplySnapshots是一个一维的数据结构,类似于python里就两个list,两个list里的值索引是相对应的:

1
2
ids数组:存快照id
value:存余额

源码里的快照id策略:

还没跑过原本的代码,源码里的快照id策略应该是:定义了一个计数器变量,只能单增,具备单调性。

1
2
3
4
5
6
7
8
9
10
// 快照id单调递增,第一个值为1。id为0无效。
Counters.Counter private _currentSnapshotId;

function _snapshot() internal virtual returns (uint256) {
_currentSnapshotId.increment(); // !!!快照id单调递增

uint256 currentId = _getCurrentSnapshotId(); // 获取当前的快照Id
emit Snapshot(currentId);
return currentId;
}

自定义快照id策略:

自己实现了一下基于blockNumber的快照策略,详见文章

继承图:

Slither生成的,方便参照去阅读源码
A1D0D3CA-E749-44C4-A30E-9441BAD82C15

重要函数变量表:

Contract Function/Variable 使用场景 操作
ERC20Snapshot _accountBalanceSnapshots 存储用户快照余额 二维数据结构
ERC20Snapshot _totalSupplySnapshots 存储总供应量快照余额 一维数据结构
ERC20Snapshot _currentSnapshotId 当前快照id id为0无效,要具备单调性如单增
ERC20Snapshot _snapshot() 创建一个新快照并返回其快照 ID 可重写该函数,自定义快照策略
ERC20Snapshot _getCurrentSnapshotId() 获取当前的快照Id xx
ERC20Snapshot _updateSnapshot(Snapshots storage snapshots, uint256 currentValue) 更新快照 注意里面的触发快照的条件

Template

Contract Function/Variable 使用场景 操作
xx xx xx xx
xx xx xx xx

Refs:

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/README.adoc