背景#
ERC20 Rebase メカニズムは、ERC20 プロトコルを基にして派生したものであり、トークン保有者にインセンティブ配当を行うために使用されます。ここでは、ethereum-credit-guildプロジェクトで設計されたERC20RebaseDistributor
コントラクトをベースに、Rebase メカニズムの設計と実装方法について説明します。
ecg では、ERC20RebaseDistributor
コントラクトは基礎となる creditToken として機能し、他の貸借プロトコルと同様に、creditToken は compound の cToken に相当し、貸し手の担保証明書であり、利息を生む資産です。異なる担保物にはそれぞれバインドされた creditToken があり、担保者は creditToken を保有することで利益を蓄積することができます。
機能分析#
ERC20RebaseDistributor
コントラクトでは、すべてのホルダーが自動的に利息を保有しているわけではありません。代わりに、rebase に参加するホルダーと非 rebase に参加するホルダーを分けています。明らかに、配当は rebase に参加するホルダーにのみ適用されます。以下の図に示すように、ERC20Rebase は rebasingSupply と nonRebasingSupply の 2 つの部分から構成されています。
ここで、次の恒等式をまとめます:
totalSupply() == nonRebasingSupply() + rebasingSupply()
balanceOf(x)の合計 == totalSupply()
次に、配当メカニズムを分析します。ecg プロトコルでは、個々の creditToken の利益を集計し、一部のトークンを rebase に参加するホルダーに配当として転送します。配当のロジックも非常に直接的であり、各 rebase 参加ユーザーは現在の残高に基づいて配当の割り当てを受けることができます。
これで、ERC20Rebase の基本的な設計が明確になりました。enter/exit rebase メソッドと配当の distribute メソッドを提供する必要があります。具体的なコードは以下の通りです(注:ここでは主要なロジックのみを実装しており、抜け漏れがあります):
rebasingAccounts(array)
とrebasingAccount(mapping)
を定義して、rebase に参加するアドレスを追跡します。rebasingSupply
を定義して、すべての rebase 参加者の供給量を記録します。enterRebase
関数:このアドレスを rebase に参加したとマークし、rebase 供給量を累積します。exitRebase
関数:rebase からの参加をキャンセルし、rebase 供給量を減らします。distribute
関数:配当額をまず破棄し、その後、比率に従ってすべての rebase 参加アドレスに mint します。
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);
}
}