ERC20 Votes
本文假设读者已了解 ERC20 Snapshot 的相关知识,关于该主题的介绍,请参阅我们关于 ERC20 Snapshot 的文章。
ERC20 Votes 实际上并不负责执行投票过程,它仍然是一个具有快照和委托投票功能的常规 ERC20 代币。投票通常由治理合约来处理。
委托意味着一个地址可以将其投票权借给另一个地址,而无需将代币转移给该地址。
与 ERC20 Snapshot 不同,它不保存代币余额的快照,而是保存地址投票权的快照。
这样做的动机有几个:
- 这允许被动的代币持有者通过委托其投票权来参与治理
- 小额余额的账户可以在不消耗 gas 的情况下进行投票,因为受托人代表他们进行投票。这在生态系统层面节省了 gas。如果所有参与者都委托给了 5 个受托人,那么对于一个投票项目,只需要进行 5 次投票,而不是可能的数千次。
- 当然,正如 ERC20 Snapshot 所做的那样,快照(检查点)对于防止双重投票是必要的。
ERC20 Votes 继承了 ERC20、ERC6372 和 ERC5805。这意味着它具有 ERC20 代币的所有功能,并带有以下描述的附加属性。
ERC5805 功能
getVotes(address delegate)
此函数接收一个账户地址并返回其拥有的总投票权。如果其他地址将其投票权委托给了它,该值可以大于 balanceOf(delegate)。
delegate(address delegatee)
此函数允许 msg.sender 将其投票权委托给 delegatee,并且 delegatee 将代表 msg.sender 进行投票。请注意,代币并未转移给 delegatee,转移的仅仅是投票权。可以通过使用不同的 delegatee 或 address(0) 再次调用 delegate,随时更改或撤销委托。
委托是全额的。没有选项可以只委托一部分投票权。
需要特别注意的是,一个地址必须先将其投票权委托给自己,其选票才会被计入。引入这种特殊设定是出于 gas 效率的考虑。
delegates(address account)
此函数返回参数中的 account 已将其投票权委托给了哪个账户。
delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s)
delegateBySig 函数允许用户通过无 gas 交易来委托投票权,并由另一个账户支付 gas 费并执行该交易。
expiry 设置了委托的有效时长,而 v、r 和 s 是椭圆曲线数字签名的组成部分。
此签名预期符合 EIP 712 格式。在内部,合约会针对每个地址递增 nonce,这将在下文描述。
nonces(address account)
签名需要一个 nonce 来防止重放攻击,因此 nonce 的内部追踪器通过此函数暴露出来。这使得签名者能够知道他们接下来应该签名的值。
getPastVotes(account, timepoint)
正如我们在 ERC20 Snapshot 文章中所讨论的,除非对账户余额进行快照,否则可能会发生双重投票攻击。这也是投票合约需要查看过去快照的原因。
与 ERC20 Snapshot 不同,这里的快照不是由某个地址调用 snapshot 函数来触发的。当发生以下任一事件时,将针对每个账户触发一次快照:
- 铸造
- 销毁
- 转账
- 有人委托他们的选票
每当上述事件之一发生时,一个包含投票权和时间戳的结构体就会被追加到一个存储该用户投票权历史的数组中。
与 ERC20 Snapshot 一样,ERC20 Votes 将对时间戳执行二分查找,以找到在 timepoint 之后最早的检查点。然后返回该时间点的投票权。
这意味着,与 ERC20 Snapshot 不同,这里没有“全局”快照 ID。如果你想查询过去的一个时间点,你需要选择一个时间戳或区块号,并将其作为 “timepoint” 提供给该函数。
事件
ERC5805 有两个事件,其含义正如其名称所示:DelegateChange 和 DelegateVotesChanged。
ERC5805 是一个接口,而不是代币
在本文中,我们在解释 ERC20 Votes,但这并不意味着 ERC5805 必须是同质化代币。它可以是 NFT,甚至是通过其他方式管理的选票记账,例如中心化实体向地址分配选票,但希望保留一份不可篡改的投票权分配历史记录。
ERC6372
我们在某种程度上假设了合约正在使用 block.timestamp 进行记账以记录检查点。然而,有些合约可能会使用 block.number 或这些全局变量的某种单调递增函数。
ERC6372 是一个标准,允许合约查询该合约使用的是哪种“时钟”。它具有两个函数:
clock()
这将返回一个 uint48,它可以是区块号或区块时间戳,或者是它们的函数。选择 uint48 是因为它有足够的位数来表示所有合理的时间形式,并且可以表示比人类有记载的历史更遥远的未来区块号。
CLOCK_MODE()
是的,在 Solidity 中使用全大写蛇形命名法的函数非常罕见,但这正是该 EIP 的规定。此函数返回一个字符串,告诉阅读者时钟使用的是什么单位。
- 如果是时间戳,它将是
“mode=timestamp”。 - 如果是区块号,它将是
mode=blocknumber&from=default。
这意味着它仅仅使用了 block.number 变量。如果它是从另一个区块开始的,则必须指定链 ID 及其起始的区块号。例如,Avalanche 的链 ID 是 43114,因此如果它是从第 100 个区块开始,那么 CLOCK_MODE() 的响应将是 mode=blocknumber&from=43114:100。
EIP 6372 没有事件。
了解更多详情,请参阅 EIP,该文档实际上非常易懂。请注意,此 EIP 尚未最终定稿。
与 ERC20 Snapshot 的差异总结
时间概念
- ERC20 Votes 具有明确的时间概念
- ERC20 Snapshot 使用递增的 ID,这些 ID 随时间增加仅仅是作为计数器的副产品
记录的内容
- ERC20 Votes 对投票权进行快照
- ERC20 Snapshot 跟踪余额
检查点何时更新
- ERC20 Votes 在发生委托或转账时更新
- ERC20 Snapshot 需要显式调用 “_snapshot()”
决定使用 ERC20 Snapshot 还是 ERC20 Votes
选择使用 ERC20 Snapshot 还是 ERC20 Votes,主要取决于是否需要委托选票,或者更抽象地说,是否需要委托 ERC20 代币所赋予的某种权利。
了解更多
查看我们的高级 Solidity 编程训练营 以及我们提供的其他 区块链训练营 课程。
最初发布于 2023 年 2 月 23 日