xChar
·7 months ago

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

背景

ERC20 Rebase机制是在ERC20协议基础之上衍生的,用来对代币持有者做激励分红,这里将以ethereum-credit-guild项目中设计的ERC20RebaseDistributor合约为基础,讲解Rebase机制的设计与实现思路。

在ecg中,ERC20RebaseDistributor合约是作为底层的creditToken,类比到其他借贷协议,creditToken等同于compound中的cToken,是lender的质押凭证,也是生息资产,针对不同的抵押物都有一个绑定的creditToken,质押者可以通过持有creditToken来累积收益。

功能分析

ERC20RebaseDistributor合约中并不是默认所有holder都持有生息资产,而是将参与rebase和非rebase的分开,显然分红只针对参与rebase的holder展开,如图所示,ERC20Rebase由rebasingSupply和nonRebasingSupply两部分构成

image

这里我们总结出下面的恒等式:

  • totalSupply() == nonRebasingSupply() + rebasingSupply()
  • sum of balanceOf(x) == totalSupply()

接下来,再分析分红机制,在ecg协议中,存在一个合约来汇总单个creditToken的收益,并按照比例把一部分token转为对参与rebase的holder分红,这里的分红逻辑也很直接,每个参与rebase的用户根据当前余额来瓜分分红额度即可。

至此,我们的ERC20Rebase基础设计已经明确,需要提供enter/exit rebase方法和分红distribute方法,具体的代码如下所示(注:这里只实现关键逻辑,仍有缺漏):

  • 定义 rebasingAccounts(array) rebasingAccount(mapping) 来跟踪参与rebase的地址
  • 定义 rebasingSupply 来记录所有参与rebase的供应量
  • enterRebase 函数:将该地址标记为参与rebase,累加rebase供应量
  • exitRebase 函数:取消参与rebase的标记,扣减rebase供应量
  • distribute 函数:先将分红数额销毁,再按照比例mint给所有参与rebase的地址
    function enterRebase() external {
        require(!rebasingAccount[msg.sender], "SimpleERC20Rebase: already rebasing");
        uint256 balance = balanceOf(msg.sender);
        rebasingAccount[msg.sender] = true;
        rebasingSupply += balance;
        rebasingAccounts.push(msg.sender);
    }

    function exitRebase()  external {
        require(rebasingAccount[msg.sender], "SimpleERC20Rebase: not rebasing");
        uint256 balance = balanceOf(msg.sender);
        rebasingAccount[msg.sender] = false;
        rebasingSupply -= balance;
        for (uint256 i = 0; i < rebasingAccounts.length; i++) {
            if (rebasingAccounts[i] == msg.sender) {
                rebasingAccounts[i] = rebasingAccounts[rebasingAccounts.length - 1];
                rebasingAccounts.pop();
                break;
            }
        }
    }

    function distribute(uint256 amount) external {
        require(balanceOf(msg.sender)>=amount, "SimpleERC20Rebase: not enough");
        _burn(msg.sender, amount);
        for (uint256 i = 0; i < rebasingAccounts.length; i++) {
            uint256 delta = amount * balanceOf(rebasingAccounts[i]) / rebasingSupply;
            _mint(rebasingAccounts[i], delta);
        }
        rebasingSupply += amount;
    }

    function mint(address user, uint256 amount) external {
        return _mint(user, amount);
    }

最基础的rebase分红机制已经实现了,回顾上面的代码,存在非常关键的问题:如果参与rebase的地址很多,每次分红都会有大量的mint操作,成本过高,系统无法拓展

完整代码

// 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 {

    mapping(address => bool) internal rebasingAccount;
    address[] internal rebasingAccounts;
    uint256 public rebasingSupply;
    
    constructor(
        string memory _name,
        string memory _symbol
    ) ERC20(_name, _symbol) {}

    function nonRebasingSupply() public view returns (uint256) {
        return totalSupply() - rebasingSupply;
    }

    function enterRebase() external {
        require(!rebasingAccount[msg.sender], "SimpleERC20Rebase: already rebasing");
        uint256 balance = balanceOf(msg.sender);
        rebasingAccount[msg.sender] = true;
        rebasingSupply += balance;
        rebasingAccounts.push(msg.sender);
    }

    function exitRebase()  external {
        require(rebasingAccount[msg.sender], "SimpleERC20Rebase: not rebasing");
        uint256 balance = balanceOf(msg.sender);
        rebasingAccount[msg.sender] = false;
        rebasingSupply -= balance;
        for (uint256 i = 0; i < rebasingAccounts.length; i++) {
            if (rebasingAccounts[i] == msg.sender) {
                rebasingAccounts[i] = rebasingAccounts[rebasingAccounts.length - 1];
                rebasingAccounts.pop();
                break;
            }
        }
    }

    function distribute(uint256 amount) external {
        require(balanceOf(msg.sender)>=amount, "SimpleERC20Rebase: not enough");
        _burn(msg.sender, amount);
        for (uint256 i = 0; i < rebasingAccounts.length; i++) {
            uint256 delta = amount * balanceOf(rebasingAccounts[i]) / rebasingSupply;
            _mint(rebasingAccounts[i], delta);
        }
        rebasingSupply += amount;
    }

    function mint(address user, uint256 amount) external {
        return _mint(user, amount);
    }
}
Loading comments...