背景#
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);
}
}