banner
zach

zach

github
twitter
medium

ERC20 Rebaseの設計と実装-2 shareとbalance

個人ブログから転載 https://www.hackdefi.xyz/posts/erc20-rebase-2/

引出 Share#

私たちは上記の問題に対して最適化を行う必要があります。分配メカニズムを観察することで、すべてのアカウントに対応する残高が比例して増加することがわかります。では、ここでのループ更新は必要なのでしょうか?グローバルに係数を記録し、distribute を呼び出すたびにこの係数だけを更新し、残高を照会する際にはこの係数を掛ければよいのではないでしょうか。

ここで、balance と share の 2 つの概念が分かれ、sharePrice が派生します。rebase に参加するすべてのユーザーは share のみを記録し、グローバルな sharePrice を持ち、ユーザーの残高を照会する際には share と sharePrice を基に残高を計算し、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 時に、まずトークンを焼却し、その後 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) {
            _exitRebasing(to);
        }
        bool result = ERC20.transfer(to, amount);
        if (isFromRebasing) {
            _enterRebase(from);
        }
        if (isToRebasing && to != from) {
            _enterRebase(to);
        }
        return result;
    }

残された問題#

大まかなロジックはすでに実装されており、ユーザーの残高も share と sharePrice を通じて正しく計算できます。では、前述の恒等式が満たされているかどうかを再確認します:

  • totalSupply() == nonRebasingSupply() + rebasingSupply()
    • rebasingSupply は現在の share と sharePrice を用いて計算できますが、totalSupply は再実装していません。totalSupply=nonRebasingSupply の例として、nonRebasingSupply は減少しましたが、share は変わらず、sharePrice は増加しました。したがって、明らかに totalSupply は rebasingSupply よりも小さくなります。
  • sum of balanceOf(x) == totalSupply()
    • 同様に、ユーザーの share は変わらず、sharePrice が増加したため、明らかに totalSupply も小さくなります。

では、この不足している部分は何でしょうか?

コード#

// 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;
    }
}
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。