背景#
ERC20 Rebase メカニズムは、ERC20 プロトコルを基にして派生したもので、トークン保有者にインセンティブ配当を行うために使用されます。ここでは、ethereum-credit-guildプロジェクトで設計されたERC20RebaseDistributor
コントラクトをベースに、Rebase メカニズムの設計と実装方法について説明します。
ecg では、ERC20RebaseDistributor
コントラクトは基礎となる creditToken として機能します。他の貸借プロトコルに対する比較として、creditToken は cToken と同等であり、貸し手の担保証明書であり、利息を生む資産です。異なる担保物にはそれぞれバインドされた creditToken があり、担保者は creditToken を保有することで利益を蓄積することができます。
機能分析#
ERC20RebaseDistributor
コントラクトでは、すべてのホルダーが自動的に利息を持つわけではありません。代わりに、rebase に参加するホルダーと非 rebase に参加するホルダーを分けています。明らかに、配当は rebase に参加するホルダーにのみ適用されます。以下の図に示すように、ERC20Rebase は rebasingSupply と nonRebasingSupply の 2 つの部分から構成されています。
ここで、以下の等式をまとめます:
totalSupply() == nonRebasingSupply() + rebasingSupply()
balanceOf(x)の合計 == totalSupply()
次に、配当メカニズムを分析します。ecg プロトコルでは、個々の creditToken の利益を集計し、一部のトークンを 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);
}
}