DeFi 项目的基石 - ERC4626 代币金库协议的实现

ERC4626代币金库(Vault)是ERC20的拓展。本质上可以理解为一个智能合约,支持把资产托管进去这个合约中,合约代表你去赚钱,赚到的钱按份额分配。ERC4626继承于ERC20,基本接口和ERC20一致,但添加了增强的存款、取款、赎回、会计等接口:https://learnblockchai

什么是代币金库

ERC4626 代币金库(Vault)是 ERC20 的拓展。本质上可以理解为一个智能合约,支持把资产托管进去这个合约中,合约代表你去赚钱,赚到的钱按份额分配。 ERC4626 继承于 ERC20,基本接口和 ERC20 一致,但添加了增强的存款、取款、赎回、会计等接口。

著名代币金库项目

  • Yearn 智能调度资金,帮助用户去不同 DeFi 协议去挖矿,获取收益。 image.png
  • Aave v3 存款人存入资产获得 aToken,aave 给资产借贷出去,收益给到存款人。

image.png

代币金库和 ERC20 的对比

ERC4626 是建立在 ERC20 之上的扩展标准,用于构建「资产托管 + 份额映射 + 收益分配」的智能金库系统。简单来说,ERC20 是货币,是金钱。而 ERC4626 是基于金钱而建立的基金,相当于把钱托管到基金里面让它帮我们操作,赚到的钱按份额平分。

代币金库和质押、流动性池的区别

  • 代币金库:单资产。用于借贷转利润。通常无锁仓机制。 可以将代币金库类比于基金,把资产交给他就能给你赚取收益,通过份额来赎回。

  • 质押:单资产。用于协议奖励、出块奖励。有锁仓机制。 质押一般用于协议的运行(PoS),需要锁仓、解锁。收益一般来自于治理分红、协议通胀奖励。

  • 流动性池:多资产。用于做市赚取手续费。无锁仓机制。 流动性提供者可以类比与交易所的做市商角色。通过将资金给到流动性池中,赚取交易的手续费(如 0.3%),但有可能需要承担价格变动的亏损。

    核心变量

    下面我们来分析 ERC4626 中的核心状态变量。

    abstract contract ERC4626 is ERC20, IERC4626 {
    using Math for uint256;
    
    IERC20 private immutable _asset;
    uint8 private immutable _underlyingDecimals;
    ...
    }
  • 在上面代码 ERC4626 的代码片段中,可以看出,ERC4626 是继承自 ERC20 的,故 ERC4626 拥有其所有状态变量和函数。有关 ERC20 请看:https://learnblockchain.cn/article/14429

  • IERC20 private immutable _asset: 这是一个指向 IERC20 的接口,存储着外部资产的合约地址。例如我们有一个外部资产 USDT ,和我们金库内嵌的 vUSDT 进行交互。此处存储的就是 USDT 的合约地址。

  • uint8 private immutable _underlyingDecimals: 这是一个存储外部资产的精度变量。例如我们有外部资产 USDT,则会在此处存储 6 。(因为我们金库内嵌的资产的精度可能与之不同,例如我们设置 vUSDT 的精度为 10,此处存储可以为我们提供一个 USDT 的精度缓存,方便我们合约进行精度换算)

  • 注意:为避免资产换算问题,内嵌 ERC20 和外部资产的精度在真实场景下一般是设置为相同的(绝大多数项目都是这样设计的)。如果精度设置不一致,需要在 deposit()、mint()、withdraw()、redeem()等函数中手动进行份额与资产的换算。

    接口

    ERC4626 的接口相对而言较多,我们分为核心、会计、辅助接口三个板块进行讲解。

    1. 核心接口
    /**
    返回金库内嵌的 ERC20 合约地址
    */
    function asset() external view returns (address assetTokenAddress);
     /**
    存款,用于存基础资产,获取相应份额给 receiver
     */
    function deposit(uint256 assets, address receiver) external returns (uint256 shares);
        /**
    铸造,和存款类似。拥有铸造相应的份额给 receiver (会换算基础资产)
     */
    function mint(uint256 shares, address receiver) external returns (uint256 assets);
        /**
    提现,提现基础资产给 receiver,相应的 owner 需要销毁相应份额
     */
    function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares);
        /**
    赎回,和提现类似。赎回相应份额给 receiver,owner 销毁相应资产(会换算成基础资产)
     */
    function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets);

其中存款、提现可以看做成用外部资产(如 USDT 当单位进行操作) 而铸造、赎回可以看做用内嵌资产(如 vUSDT 当单位进行操作)

  1. 会计接口 会计接口此处我们来简要理解
    • totalAssets(): 返回金库中管理的资产总额。
    • covertToShares(): 返回基础资产可换取的金库额度。
    • previewDeposit(): 模拟存款一定基础资产可以换取的金库份额。
    • previewMint(): 模拟铸造一定金库份额需要存基础资产的数量。
    • previewWithdraw(): 模拟提现一定数量的基础资产需要消耗的份额。
    • previewRedeem(): 模拟销毁一定金库份额能赎回的基础资产数量。
  2. 辅助接口
    • maxDeposit(): 返回最大单次可存的资产。
    • maxMint(): 返回单次可铸造最大金库份额。
    • maxWithdraw(): 返回单次最大可提现资产。
    • maxRedeem(): 返回单次最大赎回份额。

事件

ERC4626 的事件较为简单,只有两个。分别是存款、取款。

//    存款、铸造时触发
    event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares);
//  提现、赎回时触发
    event Withdraw(
        address indexed sender,
        address indexed receiver,
        address indexed owner,
        uint256 assets,
        uint256 shares
    );

实现一个代币金库

  • 继承 ERC4626 的金库 Shawn4626.sol 通过继承 ERC4626 来实现一个简单的代币金库合约
contract Shawn4626 is ERC4626 {
    uint256 constant MAX_SUPPLY = 100 * 10 ** 18; // 最小单位,供应 100 个 vUSDT
    // 为避免精度换算问题,统一用 ERC 内置的精度 18
//    uint8 constant VUSDT_DECIMAL = 10;

    // 初始化出来内嵌的 ERC20 代币和外部资产的地址
    constructor(IERC20 asset_) ERC20("Voult USDT","vUSDT") ERC4626(asset_) {
    }

    /// 限制最大 mint 出的 vUSDT 数量为 100(10^18 最小单位)
    function deposit(uint256 assets, address receiver) public override returns (uint256) {
        uint256 shares = previewDeposit(assets);
        require(totalSupply() + shares <= MAX_SUPPLY, "Exceeds max supply");

        _deposit(msg.sender, receiver, assets, shares);
        return shares;
    }

    /// 如果允许直接 mint,可以在这里限制 max supply
    function mint(uint256 shares, address receiver) public override  returns (uint256 assets)  {
        require(totalSupply() + shares <= MAX_SUPPLY, "Exceeds max supply");
        super.mint(shares, receiver);
        return shares;
    }
}
  • 测试合约 TestErc20.sol 因为 ERC4626 代币金库合约创建时需要传入外部资产的地址,需要依赖到 ERC20 的合约,所以我们这里简单实现一个 ERC20 的合约作为代币金库的外部资产依赖。
contract TestERC20 is ERC20 {
    constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {
    }

    //    铸币(当前任何人都能铸币,实际应加上权限控制)
    function mint(address user,uint amount) external {
        _mint(user, amount);
    }
}
  • 测试脚本 Shaw4626.t.sol 在这个测试中,分别测试 deposit()、mint() 的正常存款流程以及超过最大额度存款的流程、混合存款的流程。
contract TestShawn4626 is Test {
    Shawn4626 vault;
    TestERC20 asset;

    uint256 constant MAX_SUPPLY = 100 * 10 ** 18; // 最大供应量 100 个 vUSDT(考虑精度)
    uint256 constant DEPOSIT_AMOUNT = 1 * 10 ** 18; // 每次存入的资产量(考虑精度)

    function setUp() public {
        // 创建一个 ERC20 资产合约,作为外部资产
        asset = new TestERC20("Test USDT", "USDT");
        // 部署 Shawn4626 Vault 合约
        vault = new Shawn4626(IERC20(address(asset)));

        // 给用户提供一些资产
        asset.mint(address(this),DEPOSIT_AMOUNT * 5); // 给用户 5 倍的存款量
    }

    // 测试最大供应量限制:存入资产时超过最大供应量应该失败
    function testDepositExceedsMaxSupply() public {
        // 存入资产,超过最大供应量
        asset.approve(address(vault), DEPOSIT_AMOUNT);
        vm.expectRevert("Exceeds max supply");
        vault.deposit(DEPOSIT_AMOUNT * 101, address(this));
    }

    // 测试最大供应量限制:mint 超过最大供应量时应该失败
    function testMintExceedsMaxSupply() public {
        asset.approve(address(vault), DEPOSIT_AMOUNT * 101);

        vm.expectRevert("Exceeds max supply");
        // 直接 mint 超过最大供应量
        vault.mint(DEPOSIT_AMOUNT * 101, address(this));

    }

    // 测试正常的存款操作
    function testNormalDeposit() public {
        asset.approve(address(vault), DEPOSIT_AMOUNT);
        vault.deposit(DEPOSIT_AMOUNT, address(this)); // 正常存款
        assertEq(vault.totalSupply(), DEPOSIT_AMOUNT); // 确保总供应量增加
    }

    // 测试正常的 mint 操作
    function testNormalMint() public {
        asset.approve(address(vault), type(uint256).max);
        vault.mint(DEPOSIT_AMOUNT, address(this)); // 正常 mint
        assertEq(vault.totalSupply(), DEPOSIT_AMOUNT); // 确保总供应量增加
    }

    // 测试正常的存款和 mint 组合
    function testDepositAndMint() public {
        asset.approve(address(vault), type(uint256).max);
//        默认 1:1 兑换
        console2.log("share of DEPOSIT_AMOUNT is " , vault.previewDeposit(DEPOSIT_AMOUNT));
        vault.deposit(DEPOSIT_AMOUNT, address(this)); // 正常存款

        vault.mint(DEPOSIT_AMOUNT, address(this)); // 再正常 mint

        assertEq(vault.totalSupply(), DEPOSIT_AMOUNT * 2); // 确保总供应量正确
    }
}
  • 测试结果

image.png

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
shawn_shaw
shawn_shaw
web3潜水员、技术爱好者、web3钱包开发工程师、欢迎交流工作机会。欢迎骚扰:vx:cola_ocean