banner
zach

zach

github
twitter
medium

Design and Implementation of ERC20 Rebase - 3 Compensation Minting

Reprinted from personal blog https://www.hackdefi.xyz/posts/erc20-rebase-3/

Practical Example#

Let's expand with a practical example. Suppose both A and B have rebase enabled, with initial balances and shares of 100, while C has rebase disabled with a balance of 200.

The current state is as follows, which satisfies the equation condition:

  • totalSupply = 100 + 100 + 200 = 400
  • sharePrice = 1
  • rebasingSupply = (100 + 100) * 1 = 200
  • nonRebasingSupply = 200

Now C calls distribute to contribute their 200 tokens, resulting in the following state change:

  • totalSupply = 100 + 100 - 200 = 0
  • sharePrice = 1 + (200/200) = 2
  • rebasingSupply = (100 + 100) * 2 = 400
  • nonRebasingSupply = 0

At this point, the equation is clearly no longer valid. To ensure the equation holds, totalSupply should be 400 instead of 0. Returning to the first version, each time we call distribute, we modify the balance of each user participating in rebase through _mint. In fact, the system has issued new tokens because a burn operation was performed earlier, thus maintaining balance.

In the second version, while a burn was also performed earlier, no new tokens were issued; only the sharePrice was updated. Therefore, even though the user's balance on paper increased, the actual total available for withdrawal is insufficient. Thus, the missing 400 is the amount that needs to be minted. Similarly, we do not need to proportionally mint for each user, but rather record it in a global variable, and mint the corresponding amount when the user exits rebase.

Compensation Minting#

First, define a global unminted variable:

uint256 public unminted;

Unminted needs to be increased during distribute and decreased during 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;
    }

The modified code is as follows. We have added the unminted variable and accumulate it during distribute, minting the corresponding number of tokens to the user during exit.

Thus, the core framework of ERC20Rebase has been implemented. However, the code is for reference only and focuses solely on the core logic. Compared to the ERC20RebaseDistributor contract in ECG, we still lack a very critical part: linear release. The dividend tokens are not returned to holders in a lump sum but are linearly increased over a certain period.

With the addition of a time dimension, many issues will also arise: if the total shares change during the dividend period, how can we ensure fair distribution?

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 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;
    }
}
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.