用法说明
ERC20Votes是基于ERC20的扩展,支持投票与委托投票,首先来看下一个具体实现,MyToken继承了ERC20Votes合约,并且为合约创建地址mint了10000代币:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";
contract MyToken is ERC20, ERC20Permit, ERC20Votes {
constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {
_mint(msg.sender,10000*(10**decimals()));
}
// The functions below are overrides required by Solidity.
function _update(address from, address to, uint256 amount) internal override(ERC20, ERC20Votes) {
super._update(from, to, amount);
}
function nonces(address owner) public view virtual override(ERC20Permit, Nonces) returns (uint256) {
return super.nonces(owner);
}
}
部署之后可用接口如下图所示:
解释下其中比较重要的几个函数:
1、approve:从msg.sender向指定地址授权可转移额度;
2、delegate:将msg.sender的投票能力委托给指定账户,有ERC20余额的账户必须先委托给自己才能激活自身账户的投票能力;
3、delegateBySig;将签名的地址的投票能力委托给指定账户;
4、permit:通过签名验证的方式(验证签名是否与owner匹配)进行从owner到spender的可转移额度授权,通过该种方式不需要用户地址进行approve(approve必须要从用户地址发起,需要用户支付gas),而只需要用户体地址提供签名即可(用户不需要支付gas,因为permit不是用户地址发起,gas支付方为调起permit函数的地址的第三方);
5、transferFrom:从指定地址A向地址B进行token转移,消耗地址A对msg.sender授权的可转移额度,如果地址A就是美msg.sender,那么也要先进行对自身地址的approve,才有权限进行transferFrom;
6、transfer:从msg.sender向指定地址转移token,不消耗相关授权可转移额度;
7、allowance:查询owner对spender授权的可转移额度;
8、checkpoints:查询指定地址指定数组位置的投票能力;
9、delegates:查询指定地址的投票委托账户;
10、getPastTotalSupply:查询截至指定区块高度的系统总的投票能力,查询的区块高都需要小于当前区块高度;
11、getPastVotes:查询指定账户截至指定区块高度的投票能力,查询的区块高都需要小于当前区块高度;
12、getVotes:查询指定账户当前投票能力;
13、numCheckpoints:查询指定账户的快照数组的长度。
构成详解
ERC20Votes.sol中引入合约情况如下:
import {ERC20} from "../ERC20.sol";
import {Votes} from "../../../governance/utils/Votes.sol";
import {Checkpoints} from "../../../utils/structs/Checkpoints.sol";
ERC20不必多说,Votes是投票功能的主要实现部分,Checkpoints为库合约,引入结构体和结构体相关操作。
首先,分析下Votes的实现部分。
1、Votes状态变量:
abstract contract Votes is Context, EIP712, Nonces, IERC5805 {
//引入library处理Checkpoints.Trace208结构体数据
using Checkpoints for Checkpoints.Trace208;
bytes32 private constant DELEGATION_TYPEHASH =
keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");
//存储着账户的投票权委托关系
mapping(address account => address) private _delegatee;
//存储着账户不同区块高度的投票能力(可用票数)快照
mapping(address delegatee => Checkpoints.Trace208) private _delegateCheckpoints;
//存储着不同区块高度的总的投票能力(是总的可投票票数)快照
Checkpoints.Trace208 private _totalCheckpoints;
其记录着账户的可用投票数、账户的委托投票关系以及系统总的可投票数量,其中账户可投票数量以及总的可投票数量都通过Checkpoints.Trace208结构体记录着其多个区块高度的快照。
为了更好的理解,需要先了解下引入的Checkpoints库合约
2、Checkpoints库合约
其定义了如下结构体:
//采用数组的原因是会记录Checkpoint208不同_key的记录,实现按照区块高度进行快照记录,其中_key随着数组下表的增加一定是升序的
struct Trace208 {
Checkpoint208[] _checkpoints;
}
//在Votes中,_key记录着区块高度,_value记录着可用投票数
struct Checkpoint208 {
uint48 _key;
uint208 _value;
}
定义了如下对外暴露的函数:
//一:实现了向Trace208 (Checkpoint208结构体数据)新增数据,会保证key不能小于Trace208最大的key,如果相等,则进行更新,如果大于,则新增数组数据,后续Votes合约传入的key都是时间戳
function push(Trace208 storage self, uint48 key, uint208 value) internal returns
(uint208, uint208)
{
return _insert(self._checkpoints, key, value);
}
//二:在Trace208数组中寻找_key大于等于指定key的最小下标数组元素的_value,二分法查找
function lowerLookup(Trace208 storage self, uint48 key) internal view returns
(uint208)
{
uint256 len = self._checkpoints.length;
uint256 pos = _lowerBinaryLookup(self._checkpoints, key, 0, len);
return pos == len ? 0 : _unsafeAccess(self._checkpoints, pos)._value;
}
//二:在Trace208数组中寻找_key小于等于指定key的最大下标数组元素的_value,二分法查找
function upperLookup(Trace208 storage self, uint48 key) internal view returns
(uint208) {
uint256 len = self._checkpoints.length;
uint256 pos = _upperBinaryLookup(self._checkpoints, key, 0, len);
return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
}
//三:在Trace208数组中寻找_key小于等于指定key的最大下标数组元素的_value,二分法查找,是上一步函数的优化算法
function upperLookupRecent(Trace208 storage self, uint48 key) internal view returns
(uint208) {
uint256 len = self._checkpoints.length;
uint256 low = 0;
uint256 high = len;
if (len > 5) {
uint256 mid = len - Math.sqrt(len);
if (key < _unsafeAccess(self._checkpoints, mid)._key) {
high = mid;
} else {
low = mid + 1;
}
}
uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high);
return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
}
//四:返回Trace208数组中最后一个元素的_value
function latest(Trace208 storage self) internal view returns (uint208) {
uint256 pos = self._checkpoints.length;
return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
}
//五:返回Trace208数组中最后一个元素
function latestCheckpoint(Trace208 storage self) internal view returns (bool exists,
uint48 _key, uint208 _value) {
uint256 pos = self._checkpoints.length;
if (pos == 0) {
return (false, 0, 0);
} else {
Checkpoint208 memory ckpt = _unsafeAccess(self._checkpoints, pos - 1);
return (true, ckpt._key, ckpt._value);
}
}
function length(Trace208 storage self) internal view returns (uint256) {
return self._checkpoints.length;
}
function at(Trace208 storage self, uint32 pos) internal view returns (Checkpoint208
memory) {
return self._checkpoints[pos];
}
3、Votes合约函数。
clock函数用于获取当前区块时间:
//获取当前区块高度,在后续生成快照时,会调用该方法获取区块高度作为_key
function clock() public view virtual returns (uint48) {
return Time.blockNumber();
}
getVotes函数,用于获取指定账户的投票能力:
//获取指定地址当前的可用的投票能力
function getVotes(address account) public view virtual returns (uint256) {
return _delegateCheckpoints[account].latest();
}
getPastVotes函数,获取截至指定区块高度,指定账户的投票能力;
//获取在某个时间点(也即区块高度之,包括该区块高度),指定账户最新的投票能力
function getPastVotes(address account, uint256 timepoint) public view virtual returns
(uint256)
{
uint48 currentTimepoint = clock();
if (timepoint >= currentTimepoint) {
revert ERC5805FutureLookup(timepoint, currentTimepoint);
}
return
_delegateCheckpoints[account].upperLookupRecent(SafeCast.toUint48(timepoint));
}
getPastTotalSupply函数,获取截至指定区块高度,系统总的投票能力;
//获取截至指定时间(也即特定区块高度)前的系统总的可用投票票数
function getPastTotalSupply(uint256 timepoint) public view virtual returns (uint256) {
uint48 currentTimepoint = clock();
if (timepoint >= currentTimepoint) {
revert ERC5805FutureLookup(timepoint, currentTimepoint);
}
return _totalCheckpoints.upperLookupRecent(SafeCast.toUint48(timepoint));
}
delegates函数,获取指定账户的投票委托地址:
//获取账户的委托地址
function delegates(address account) public view virtual returns (address) {
return _delegatee[account];
}
delegate函数,将投票能力委托给指定账户,具体为:1)首先获取旧的委托账户地址,2)然后更新委托账户为新的地址,3)其次发布委托人变更事件,4)最后将当前账户的投票能力从旧的委托账户转移到新的委托账户。在上面这个过程中,会触发账户的投票能力的快照记录。
//将自身账户的投票能力委托给另一个账户
function delegate(address delegatee) public virtual {
address account = _msgSender();
_delegate(account, delegatee);
}
函数delegateBySig实现功能同delegate,只不过发起委托的源地址是通过签名方式来恢复的:
//通过签名进行投票能力委托
function delegateBySig(
address delegatee,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
if (block.timestamp > expiry) {
revert VotesExpiredSignature(expiry);
}
address signer = ECDSA.recover(
_hashTypedDataV4(keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry))),
v,
r,
s
);
_useCheckedNonce(signer, nonce);
_delegate(signer, delegatee);
}
这里再分析下_transferVotingUnits函数,后续在token转移的过程中会用于投票能力的转移:
//如果是mint,则进行系统总的投票能力增加,同时进行快照;如果是burn,则进行系统总的投票能力减少,同时进行快照;最后按照数量将投票能力从源地址的委托账户转移到目的地址的委托账户
function _transferVotingUnits(address from, address to, uint256 amount) internal virtual {
if (from == address(0)) {
_push(_totalCheckpoints, _add, SafeCast.toUint208(amount));
}
if (to == address(0)) {
_push(_totalCheckpoints, _subtract, SafeCast.toUint208(amount));
}
_moveDelegateVotes(delegates(from), delegates(to), amount);
}
最后分析下ERC20Votes构成
4、ERC20Votes构成。
重写了_update函数,保证在token转移的过程中,投票能力也一并转移:
//通过 _transferVotingUnits(from, to, value)进行投票能力转移
function _update(address from, address to, uint256 value) internal virtual override {
super._update(from, to, value);
if (from == address(0)) {
uint256 supply = totalSupply();
uint256 cap = _maxSupply();
if (supply > cap) {
revert ERC20ExceededSafeSupply(supply, cap);
}
}
_transferVotingUnits(from, to, value);
}