引出 Share#
我們需要針對上面提出的問題進行優化,通過觀察分紅機制可以看出,所有 account 對應的 balance 都是按比例增加,那麼這裡的循環更新是否有存在的必要呢?是否可以全局記錄一個係數,每次調用 distribute 時只更新這個係數,要查詢 balance 時,乘上這個係數即可
那麼這裡就拆分出兩個概念,balance 和 share,並且衍生出 sharePrice,對於所有進入 rebase 的用戶都只記錄 share,並且有一個全局的 sharePrice,查詢用戶 balance 時需要根據 share 和 sharePrice 計算出 balance,sharePrice 在分紅時增加
針對第一個版本,我們梳理一下需要修改的部分和積攢的問題:
- 需要記錄每個參與 rebase 的地址的 share
- share 與 balance 如何映射
- sharePrice 在每次分紅時如何累加
具體的改動如下:
- 將 mapping 的 value 拓展為結構體,記錄 share
- 將 rebasingSupply 改為 totalShares
- 定義初始 sharePrice=1e30
struct RebasingState {
bool isRebasing;
uint256 nShares;
}
mapping(address => RebasingState) internal rebasingAccount;
uint256 public totalShares;
uint256 public sharePrice = 1e30;
用下面的函數處理 share 和 balance 的映射關係:
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 ;
}
繼續改寫 enter/exit 和 distribute 部分:
- enter 時先把 balance 轉換為 share,更新 rebase 記錄
- exit 時把 share 轉換為 balance,移除 rebase 記錄
- distribute 時,先銷毀 token,再更新 sharePrice
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(address user) internal {
uint256 shares = rebasingAccount[user].nShares;
rebasingAccount[user].isRebasing = false;
rebasingAccount[user].nShares = 0;
totalShares -= shares;
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;
}
ERC20 方法重寫#
同時,我們需要對 ERC20 的幾個方法進行重寫:
- balanceOf:對於參與 rebase 的用戶需要用 share 轉換到 balance
- mint burn transfer transferFrom:對於相關用戶,如果之前參與 rebase 的需要先 exit,操作後再 enter
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 burn(address user, uint256 amount) external {
bool isRebasing = rebasingAccount[user].isRebasing;
if (isRebasing) {
_exitRebase(user);
}
ERC20._burn(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;
}
遺留的問題#
大致邏輯已經實現了,用戶的 balance 也能通過 share 和 sharePrice 來正確計算,我們再來看是否滿足了前面的恆等式:
totalSupply() == nonRebasingSupply() + rebasingSupply()
- rebasingSupply 可以用當前的 share 和 sharePrice 計算出,但是 totalSupply 我們沒有重寫,totalSupply=nonRebasingSupply,以 distribute 為例,nonRebasingSupply 減少了,但是 share 不變,sharePrice 還增加了,那麼顯然 totalSupply 小於 rebasingSupply
sum of balanceOf(x) == totalSupply()
- 同理,用戶的 share 不變但是 sharePrice 增加了,顯然 totalSupply 也偏小
那麼缺少的這部分是什麼?
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 {
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 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;
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;
}
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 burn(address user, uint256 amount) external {
bool isRebasing = rebasingAccount[user].isRebasing;
if (isRebasing) {
_exitRebase(user);
}
ERC20._burn(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;
}
}