引出 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;
}
}