背景#
ERC20 Rebase 机制是在 ERC20 协议基础之上衍生的,用来对代币持有者做激励分红,这里将以ethereum-credit-guild项目中设计的ERC20RebaseDistributor
合约为基础,讲解 Rebase 机制的设计与实现思路。
在 ecg 中,ERC20RebaseDistributor
合约是作为底层的 creditToken,类比到其他借贷协议,creditToken 等同于 compound 中的 cToken,是 lender 的质押凭证,也是生息资产,针对不同的抵押物都有一个绑定的 creditToken,质押者可以通过持有 creditToken 来累积收益。
功能分析#
在ERC20RebaseDistributor
合约中并不是默认所有 holder 都持有生息资产,而是将参与 rebase 和非 rebase 的分开,显然分红只针对参与 rebase 的 holder 展开,如图所示,ERC20Rebase 由 rebasingSupply 和 nonRebasingSupply 两部分构成
这里我们总结出下面的恒等式:
totalSupply() == nonRebasingSupply() + rebasingSupply()
sum of balanceOf(x) == totalSupply()
接下来,再分析分红机制,在 ecg 协议中,存在一个合约来汇总单个 creditToken 的收益,并按照比例把一部分 token 转为对参与 rebase 的 holder 分红,这里的分红逻辑也很直接,每个参与 rebase 的用户根据当前余额来瓜分分红额度即可。
至此,我们的 ERC20Rebase 基础设计已经明确,需要提供 enter/exit rebase 方法和分红 distribute 方法,具体的代码如下所示(注:这里只实现关键逻辑,仍有缺漏):
- 定义
rebasingAccounts(array)
rebasingAccount(mapping)
来跟踪参与 rebase 的地址 - 定义
rebasingSupply
来记录所有参与 rebase 的供应量 enterRebase
函数:将该地址标记为参与 rebase,累加 rebase 供应量exitRebase
函数:取消参与 rebase 的标记,扣减 rebase 供应量distribute
函数:先将分红数额销毁,再按照比例 mint 给所有参与 rebase 的地址
function enterRebase() external {
require(!rebasingAccount[msg.sender], "SimpleERC20Rebase: already rebasing");
uint256 balance = balanceOf(msg.sender);
rebasingAccount[msg.sender] = true;
rebasingSupply += balance;
rebasingAccounts.push(msg.sender);
}
function exitRebase() external {
require(rebasingAccount[msg.sender], "SimpleERC20Rebase: not rebasing");
uint256 balance = balanceOf(msg.sender);
rebasingAccount[msg.sender] = false;
rebasingSupply -= balance;
for (uint256 i = 0; i < rebasingAccounts.length; i++) {
if (rebasingAccounts[i] == msg.sender) {
rebasingAccounts[i] = rebasingAccounts[rebasingAccounts.length - 1];
rebasingAccounts.pop();
break;
}
}
}
function distribute(uint256 amount) external {
require(balanceOf(msg.sender)>=amount, "SimpleERC20Rebase: not enough");
_burn(msg.sender, amount);
for (uint256 i = 0; i < rebasingAccounts.length; i++) {
uint256 delta = amount * balanceOf(rebasingAccounts[i]) / rebasingSupply;
_mint(rebasingAccounts[i], delta);
}
rebasingSupply += amount;
}
function mint(address user, uint256 amount) external {
return _mint(user, amount);
}
最基础的 rebase 分红机制已经实现了,回顾上面的代码,存在非常关键的问题:如果参与 rebase 的地址很多,每次分红都会有大量的 mint 操作,成本过高,系统无法拓展
完整代码#
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.13;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract SimpleERC20Rebase is ERC20 {
mapping(address => bool) internal rebasingAccount;
address[] internal rebasingAccounts;
uint256 public rebasingSupply;
constructor(
string memory _name,
string memory _symbol
) ERC20(_name, _symbol) {}
function nonRebasingSupply() public view returns (uint256) {
return totalSupply() - rebasingSupply;
}
function enterRebase() external {
require(!rebasingAccount[msg.sender], "SimpleERC20Rebase: already rebasing");
uint256 balance = balanceOf(msg.sender);
rebasingAccount[msg.sender] = true;
rebasingSupply += balance;
rebasingAccounts.push(msg.sender);
}
function exitRebase() external {
require(rebasingAccount[msg.sender], "SimpleERC20Rebase: not rebasing");
uint256 balance = balanceOf(msg.sender);
rebasingAccount[msg.sender] = false;
rebasingSupply -= balance;
for (uint256 i = 0; i < rebasingAccounts.length; i++) {
if (rebasingAccounts[i] == msg.sender) {
rebasingAccounts[i] = rebasingAccounts[rebasingAccounts.length - 1];
rebasingAccounts.pop();
break;
}
}
}
function distribute(uint256 amount) external {
require(balanceOf(msg.sender)>=amount, "SimpleERC20Rebase: not enough");
_burn(msg.sender, amount);
for (uint256 i = 0; i < rebasingAccounts.length; i++) {
uint256 delta = amount * balanceOf(rebasingAccounts[i]) / rebasingSupply;
_mint(rebasingAccounts[i], delta);
}
rebasingSupply += amount;
}
function mint(address user, uint256 amount) external {
return _mint(user, amount);
}
}