Reprinted from personal blog https://www.hackdefi.xyz/posts/erc20-rebase-1/
Background#
The ERC20 Rebase mechanism is derived from the ERC20 protocol and is used to incentivize and distribute dividends to token holders. Here, we will use the ERC20RebaseDistributor
contract designed in the ethereum-credit-guild project as a basis to explain the design and implementation of the Rebase mechanism.
In ECG, the ERC20RebaseDistributor
contract serves as the underlying creditToken, analogous to the cToken in Compound, which is the lender's collateral and interest-earning asset. Each collateral has a corresponding creditToken, and pledgers can accumulate profits by holding creditTokens.
Functional Analysis#
In the ERC20RebaseDistributor
contract, not all holders hold interest-earning assets by default. Instead, they are divided into rebasing and non-rebasing holders. It is obvious that dividends are only distributed to rebasing holders. As shown in the figure, ERC20Rebase consists of rebasingSupply and nonRebasingSupply.
Here, we summarize the following identities:
totalSupply() == nonRebasingSupply() + rebasingSupply()
sum of balanceOf(x) == totalSupply()
Next, let's analyze the dividend distribution mechanism. In the ECG protocol, there is a contract that aggregates the profits of individual creditTokens and converts a portion of the tokens into dividends for rebasing holders based on the proportion. The dividend distribution logic is also straightforward, with each rebasing user sharing the dividend based on their current balance.
So far, the basic design of our ERC20Rebase has been clarified. We need to provide the enter/exit rebase methods and the dividend distribution method. The specific code is as follows (Note: only the key logic is implemented here, there are still omissions):
- Define
rebasingAccounts(array)
andrebasingAccount(mapping)
to track the addresses participating in rebase. - Define
rebasingSupply
to record the total supply of all addresses participating in rebase. enterRebase
function: Mark the address as participating in rebase and accumulate the rebase supply.exitRebase
function: Cancel the participation in rebase and deduct the rebase supply.distribute
function: Destroy the dividend amount first, and then mint it to all addresses participating in rebase according to the proportion.
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);
}
The most basic rebase dividend mechanism has been implemented. Looking back at the above code, there is a critical issue: if there are many addresses participating in rebase, there will be a large number of mint operations for each dividend distribution, resulting in high costs and an unscalable system.
Complete Code#
// 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);
}
}