ERC20 Rebase的设计与实现-2 share与balance

转载于个人博客 https://www.hackdefi.xyz/posts/erc20-rebase-2/

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

Copy
struct RebasingState {
bool isRebasing;
uint256 nShares;
}
mapping(address => RebasingState) internal rebasingAccount;
uint256 public totalShares;
uint256 public sharePrice = 1e30;

用下面的函数处理 share 和 balance 的映射关系：

Copy
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

Copy
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

Copy
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

Copy
// 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;
}
}