実際の例#
実際の例を挙げて説明します。A と B が両方ともリベースを開始し、初期のバランスとシェアがそれぞれ 100 で、C はリベースを開始しておらず、バランスが 200 であると仮定します。
この時の状態は以下の通りで、等式条件が満たされていることがわかります:
- totalSupply = 100 + 100 + 200 = 400
- sharePrice = 1
- rebasingSupply = (100 + 100) * 1 = 200
- nonRebasingSupply = 200
この時、C が自分の 200 トークンを貢献するために distribute を呼び出すと、状態は以下のように変化します:
- totalSupply = 100 + 100 - 200 = 0
- sharePrice = 1 + (200/200) = 2
- rebasingSupply = (100 + 100) * 2 = 400
- nonRebasingSupply = 0
この時、明らかに等式は成り立たなくなります。等式を成立させるためには、totalSupply は 400 であるべきで、0 ではありません。最初のバージョンに戻ると、distribute を呼び出すたびに、_mint を通じて各リベース参加者のバランスを変更します。実際にはシステムが増発されているため、前の操作で消失した分を補うことでバランスを保っています。
第二版では、前の操作で消失が行われましたが、増発は行われず、sharePrice だけが更新されます。そのため、ユーザーのアカウント上のバランスは増加しますが、実際に受け取る際にはシステムの総量が不足しています。したがって、ここで不足している 400 が増発が必要な量です。同様に、各ユーザーに比例して増発する必要はなく、グローバル変数に記録し、ユーザーがリベースを終了する際に相応の量を mint します。
補償ミント#
まず、グローバルな unminted 変数を定義します:
uint256 public unminted;
unminted は distribute 時に増加し、exit 時に減少します:
function _exitRebase(address user) internal {
uint256 shares = rebasingAccount[user].nShares;
rebasingAccount[user].isRebasing = false;
rebasingAccount[user].nShares = 0;
totalShares -= shares;
uint256 balance = share2Balance(shares);
uint256 rawBalance = ERC20.balanceOf(user);
if (balance > rawBalance) {
uint256 delta = balance - rawBalance;
ERC20._mint(user, delta);
unminted -= delta;
}
emit RebaseExit(user, shares, block.timestamp);
}
function distribute(uint256 amount) external {
require(balanceOf(msg.sender)>=amount, "SimpleERC20Rebase: not enough");
_burn(msg.sender, amount);
sharePrice += amount*1e30 / totalShares;
unminted += amount;
}
修正されたコードは以下の通りで、unminted 変数を追加し、distribute 時に累積し、exit 時に相応のトークンをユーザーに増発します。
これで、ERC20Rebase のコアフレームワークは実装されましたが、コードは参考用であり、コアロジックの実装にのみ焦点を当てています。ecg の ERC20RebaseDistributor コントラクトと比較すると、依然として非常に重要な部分が欠けています:線形リリースです。配当のトークンは一度にホルダーに返されるのではなく、一定の周期内で線形に増加します。
時間の次元を追加したため、相応に多くの問題も派生します:配当周期内にシェアの総量が変化した場合、公平な分配をどのように保証するか?
コード#
// 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 {
event RebaseEnter(address indexed account, uint256 indexed shares, uint256 indexed timestamp);
event RebaseExit(address indexed account, uint256 indexed shares, uint256 indexed timestamp);
struct RebasingState {
bool isRebasing;
uint256 nShares;
}
mapping(address => RebasingState) internal rebasingAccount;
uint256 public totalShares;
uint256 public unminted;
uint256 public sharePrice = 1e30;
constructor(
string memory _name,
string memory _symbol
) ERC20(_name, _symbol) {}
function rebasingSupply() public view returns (uint256) {
return share2Balance(totalShares);
}
function nonRebasingSupply() public view returns (uint256) {
return totalSupply() - rebasingSupply();
}
function share2Balance(uint256 shares) view public returns (uint256) {
return shares * sharePrice / 1e30;
}
function balance2Share(uint256 balance) view public returns (uint256) {
return balance * 1e30 / sharePrice ;
}
function enterRebase() external {
require(!rebasingAccount[msg.sender].isRebasing, "SimpleERC20Rebase: already rebasing");
_enterRebase(msg.sender);
}
function _enterRebase(address user) internal {
uint256 balance = balanceOf(user);
uint256 shares = balance2Share(balance);
rebasingAccount[user].isRebasing = true;
rebasingAccount[user].nShares = shares;
totalShares += shares;
emit RebaseEnter(user, shares, block.timestamp);
}
function exitRebase() external {
require(rebasingAccount[msg.sender].isRebasing, "SimpleERC20Rebase: not rebasing");
_exitRebase(msg.sender);
}
function _exitRebase(address user) internal {
uint256 shares = rebasingAccount[user].nShares;
rebasingAccount[user].isRebasing = false;
rebasingAccount[user].nShares = 0;
totalShares -= shares;
uint256 balance = share2Balance(shares);
uint256 rawBalance = ERC20.balanceOf(user);
if (balance > rawBalance) {
uint256 delta = balance - rawBalance;
ERC20._mint(user, delta);
unminted -= delta;
}
emit RebaseExit(user, shares, block.timestamp);
}
function distribute(uint256 amount) external {
require(balanceOf(msg.sender)>=amount, "SimpleERC20Rebase: not enough");
_burn(msg.sender, amount);
sharePrice += amount*1e30 / totalShares;
unminted += amount;
}
function totalSupply() public view override returns (uint256) {
return super.totalSupply() + unminted;
}
function balanceOf(address account) public view override returns (uint256) {
uint256 rawBalance = ERC20.balanceOf(account);
if (rebasingAccount[account].isRebasing) {
return share2Balance(rebasingAccount[account].nShares);
} else {
return rawBalance;
}
}
function mint(address user, uint256 amount) external {
bool isRebasing = rebasingAccount[user].isRebasing;
if (isRebasing) {
_exitRebase(user);
}
ERC20._mint(user, amount);
if (isRebasing) {
_enterRebase(user);
}
}
function transfer(address to, uint256 amount) public virtual override returns (bool) {
bool isFromRebasing = rebasingAccount[msg.sender].isRebasing;
bool isToRebasing = rebasingAccount[to].isRebasing;
if (isFromRebasing) {
_exitRebase(msg.sender);
}
if (isToRebasing && to != msg.sender) {
_exitRebase(to);
}
bool result = ERC20.transfer(to, amount);
if (isFromRebasing) {
_enterRebase(msg.sender);
}
if (isToRebasing && to != msg.sender) {
_enterRebase(to);
}
return result;
}
function transferFrom(
address from,
address to,
uint256 amount
) public virtual override returns (bool) {
bool isFromRebasing = rebasingAccount[from].isRebasing;
bool isToRebasing = rebasingAccount[to].isRebasing;
if (isFromRebasing) {
_exitRebase(from);
}
if (isToRebasing && to != from) {
_exitRebase(to);
}
bool result = ERC20.transfer(to, amount);
if (isFromRebasing) {
_enterRebase(from);
}
if (isToRebasing && to != from) {
_enterRebase(to);
}
return result;
}
}