如何在智能合约中使用ERC-4626

  • QuickNode
  • 发布于 2024-05-10 11:58
  • 阅读 20

本文介绍了ERC-4626标准及其在构建收益型保险库中的应用。作者详细阐述了收益型保险库的原理、相关标准的功能、如何构建、部署智能合约以及与各种DeFi协议的兼容性,适合希望深入了解DeFi和Solidity的开发者。

概述

你是否尝试过构建依赖于多个 DeFi 协议代币的协议或工具?如果有,你可能遇到过每个协议的代码逻辑不同的挑战。但如果我告诉你有解决方案呢?来看看 EIP-4626,这是一个解决这个问题的标准。事实上,包括 Yearn V3(在他们的 vaults 中可以看到)的许多知名协议已经接受了这个标准。

本指南将教你关于 The Vault Standard (ERC-4626),它简化了与不同 DeFi 协议交互和创建收益生成保管箱的过程。你将了解 ERC-4626 标准、收益生成保管箱的运作方式以及如何使用 ERC-4626 Tokenized Vault Standard 创建收益生成保管箱。

你将做什么

  • 理解收益生成保管箱的功能。
  • 探索 ERC-4626 Tokenized Vault Standard。
  • 使用 ERC-4626 Tokenized Vault Standard 开发一个收益生成保管箱。

你将需要什么

什么是收益生成保管箱?

收益生成保管箱是去中心化金融(DeFi)平台的关键组成部分。它们是设计用来优化加密资产回报的智能合约,通过在各个 DeFi 协议之间进行汇集和战略配置。用户将他们的代币,通常是 ERC-20 代币,存入保管箱,作为回报,他们会获得保管箱特定的代币(vTokens),这些代币代表他们在汇集的资产和获得的利息中的份额。例如,当参与 Compound 上的 USDC 稳定币收益农场时,你将获得 cUSDC。同样,当在 Curve 上存入 ETH 时,你将得到 stETH。

收益生成保管箱使用自动化策略持续寻找 DeFi 生态系统内的最佳收益机会。这些策略可以包括借贷、借款、流动性提供、质押和套利机会。通过这样做,保管箱为用户生成被动收入,同时最大限度地减少单个收益农场活动的复杂性。

由于智能合约的设计通常经过严格的审计和测试,以确保用户资金的安全,这使得保管箱通常被认为比钱包更安全。这导致许多 DeFi 平台更喜欢使用保管箱来存入和管理资金。利用收益生成保管箱的知名 DeFi 协议包括 Aave、Compound、Sushiswap、Balancer 等等。

代币化保管箱的问题是什么?

收益生成代币对开发者构成了重大挑战,特别是在整合多个协议的代币时。例如,如果你想构建一个国库、捐赠基金或任何需要整合多个协议代币的 DeFi dApp,你需要研究每个协议,了解它的收益累积模型,并将其整合到你的代码库中。

如果你想在 Compound 上整合 cUSDC、在 Curve 上整合 stETH,或任何其他收益生成代币,你将需要对它们的智能合约有深入的了解,并开发自定义解决方案,以成功将其整合到你的 DeFi 应用中。

这个整合不同代币的过程可能压力重重且耗时,也增加了智能合约错误的风险。开发者需要花更多时间检查适配器可能存在的漏洞,在某些情况下,他们可能需要外包审计给第三方智能合约审计员,这可能会很昂贵。现在这一点尤为重要,因为攻击者几乎每周都在突破许多协议和 DeFi 应用的完整性。

什么是 ERC-4626?

ERC-4626,亦称为 Tokenized Vault Standard,是一个旨在创建代表收益生成代币份额的代币化保管箱的协议。它为代币化收益生成保管箱提供了标准的 API,这些保管箱代表了单个基础 ERC-20 代币的份额。它扩展了 ERC-20 代币标准的功能,使用户能够从他们的股权中获利。

在 ERC-20 之上构建,ERC-4626 引入了以下功能:

  • 存款和赎回
  • 转换率
  • 保管箱余额
  • 接口
  • 事件

在下一节,我们将向你展示如何在 QuickNode 创建一个端点,你将使用它在 Remix.IDE 上部署并与合约交互。

在 QuickNode 上创建 Sepolia 端点

要将智能合约部署到以太坊的测试区块链 Sepolia,你需要一个 API 端点来与网络进行通信。你可以使用公共节点,也可以部署并管理自己的基础设施;不过,如果你想要更快的响应时间,可以让我们来处理繁重的工作。请在 这里 注册一个免费账户。

登录后,点击 创建一个端点 按钮,然后选择 以太坊 链和 Sepolia 网络。

创建完端点后,复制 HTTP Provider 链接并做好准备,因为接下来你需要用到它。

QuickNode Sepolia 端点

用 QuickNode 配置你的 Web3 钱包

如果你使用 MetaMask 来部署这个合约,首先需要配置你的 RPC 设置。不过,请注意,你也可以为本指南使用 WalletConnect 兼容的钱包。一些兼容的钱包包括 Coinbase WalletRainbow WalletTrustWallet

打开 MetaMask 并点击顶部的网络下拉菜单。之后,点击 添加网络 按钮。

在 MetaMask 中添加自定义网络

在页面底部,点击 手动添加网络,并填写以下详细信息:

  • 网络名称:Sepolia QuickNode
  • 新 RPC URL:输入你刚才获取的 QuickNode HTTP URL
  • 链 ID:11155111
  • 货币符号:ETH
  • 区块浏览器 URL:https://sepolia.etherscan.io/

它应该看起来类似于此:

sepolia-network.png

构建 ERC-4626 保管箱合约

现在我们已经对 ERC-4626 标准和收益生成保管箱有了基本的理解,让我们应用我们的知识,构建一个实现这两者的智能合约。打开 Remix.IDE 并创建一个名为 TokenVault.sol 的新文件,在其中编写我们的保管箱智能合约。完整的代码将在本节的底部,但首先让我们逐步回顾代码。

构建保管箱的第一步是确定我们的许可证标识符,Solidity 编译版本,然后导入 ERC-4626 库。

//SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

import "https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC4626.sol";

导入 ERC-4626 库后,下一步是给你的合约命名,并使用 is 关键字继承库。

//SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

import "https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC4626.sol";

contract TokenVault is ERC4626 {

}

下一步是创建一个映射,跟踪用户在存入后的保管箱份额。这个映射将维护每个用户的份额余额记录。

// a mapping that checks if a user has deposited the token
mapping(address => uint256) public shareHolder;

接下来,创建一个构造函数,为 ERC-4626 构造函数分配值:_asset 代表 ERC20 代币地址,_name 代表保管箱代币的名称,以及 _symbol 代表保管箱代币的符号。例如,如果你存入 USDC,可以将 ‘vaultUSDC’ 用作 _name,将 ‘vUSDC’ 用作 _symbol

constructor(ERC20 _asset, string memory _name, string memory _symbol) ERC4626 (_asset, _name, _symbol){}

你可能会想知道为什么我们只分配这些特定的参数。原因是这些参数是 ERC-4626 保管箱标准的构造函数所必需的。以下是接受这些参数的 ERC-4626 库构造函数。

// ERC-4626 LIBRARY
constructor(
        ERC20 _asset,
        string memory _name,
        string memory _symbol
    ) ERC20(_name, _symbol, _asset.decimals()) {
        asset = _asset;
}

现在,我们将创建一个 _deposit 函数,允许用户将资产代币存入保管箱。保管箱的功能应该允许用户存入代币并以“股份”的形式获得存款证明。这些股份代表用户对存入代币的所有权,允许他们在需要时从保管箱中提取代币。

/**
 * @notice function to deposit assets and receive vault token in exchange
 * @param _assets amount of the asset token
 */
function _deposit(uint _assets) public {
    // checks that the deposited amount is greater than zero.
    require(_assets > 0, "Deposit less than Zero");
    // calling the deposit function ERC-4626 library to perform all the functionality
    deposit(_assets, msg.sender);
    // Increase the share of the user
    shareHolder[msg.sender] += _assets;
}

首先,我们检查变量 _assets 的值以确保其大于零。然后,我们利用 ERC-4626 库中的 deposit 函数。该函数处理接收资产代币、为用户铸造保管箱代币和发出存款事件的所有逻辑。例如,如果用户存入 100 美元,他们会收到 100 vUSD(保管箱 USD)作为存款证明。最后,我们使用 shareHolder 映射增加用户的股份值。以下是执行所有这些功能的 ERC-4626 库中的 deposit 函数。

// ERC-4626 LIBRARY
function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) {
        // Check for rounding errors, as we round down in previewDeposit.
        require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES");

        // Need to transfer before minting or ERC777s could reenter.
        asset.safeTransferFrom(msg.sender, address(this), assets);

        _mint(receiver, shares);

        emit Deposit(msg.sender, receiver, assets, shares);

        afterDeposit(assets, shares);
}

创建完 \_deposit 函数后,我们现在创建一个 getter 函数以查看存入此保管箱的资产总量。我们将在 ERC-4626 库中覆盖现有的 totalAssets 函数并添加自定义逻辑,以检索保管箱在资产代币方面的余额。

// returns total number of assets
function totalAssets() public view override returns (uint256) {
    return asset.balanceOf(address(this));
}

现在,让我们实现 _withdraw 函数,以便用户可以在交换股份或保管箱代币时赎回其原始资产代币金额以及由这些资产代币产生的收益。

/**
 * @notice Function to allow msg.sender to withdraw their deposit plus accrued interest
 * @param _shares amount of shares the user wants to convert
 * @param _receiver address of the user who will receive the assets
 */
function _withdraw(uint _shares, address _receiver) public {
    // checks that the deposited amount is greater than zero.
    require(_shares > 0, "withdraw must be greater than Zero");
    // Checks that the _receiver address is not zero.
    require(_receiver != address(0), "Zero Address");
    // checks that the caller is a shareholder
    require(shareHolder[msg.sender] > 0, "Not a shareHolder");
    // checks that the caller has more shares than they are trying to withdraw.
    require(shareHolder[msg.sender] >= _shares, "Not enough shares");
    // Calculate 10% yield on the withdraw amount
    uint256 percent = (10 * _shares) / 100;
    // Calculate the total asset amount as the sum of the share amount plus 10% of the share amount.
    uint256 assets = _shares + percent;
    // calling the redeem function from the ERC-4626 library to perform all the necessary functionality
    redeem(assets, _receiver, msg.sender);
    // Decrease the share of the user
    shareHolder[msg.sender] -= _shares;
}

在上面的代码块中,我们首先接受两个参数:_shares,即用户希望赎回的股份数量,以及 _receiver,即将接收资产代币的用户的地址。我们有几个检查:_shares 必须大于零,_receiver 地址不能为空,调用者必须是股东,并且他们拥有的股份数必须大于或等于他们要赎回的股份。

一旦这些条件都满足,我们就计算生成的收益并将其加入原始 _shares,以确定 _receiver 将收到的资产代币的总金额。接下来,我们调用 ERC-4626 的 redeem 函数以销毁股份代币并将资产代币转移到 _receiver 的账户。最后,我们更新调用者的 shareHolder 值。

以下是从 ERC-4626 库调用的执行所有这些赎回功能的 redeem 函数:

function redeem(
        uint256 shares,
        address receiver,
        address owner
    ) public virtual returns (uint256 assets) {
        if (msg.sender != owner) {
            uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals.

            if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares;
        }

        // Check for rounding error since we round down in previewRedeem.
        require((assets = previewRedeem(shares)) != 0, "ZERO_ASSETS");

        beforeWithdraw(assets, shares);

        _burn(owner, shares);

        emit Withdraw(msg.sender, receiver, owner, assets, shares);

        asset.safeTransfer(receiver, assets);
}

我们还将创建 totalAssetsOfUser 函数,以通过传入用户的地址作为参数来检查用户的资产代币余额。

// returns total balance of user
function totalAssetsOfUser(address _user) public view returns (uint256) {
    return asset.balanceOf(_user);
}

这是完整的 VaultToken 智能合约:

//SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

import "https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC4626.sol";

contract TokenVault is ERC4626 {
    // a mapping that checks if a user has deposited the token
    mapping(address => uint256) public shareHolder;

    constructor(
        ERC20 _asset,
        string memory _name,
        string memory _symbol
    ) ERC4626(_asset, _name, _symbol) {}

    /**
     * @notice function to deposit assets and receive vault tokens in exchange
     * @param _assets amount of the asset token
     */
    function _deposit(uint _assets) public {
        // checks that the deposited amount is greater than zero.
        require(_assets > 0, "Deposit less than Zero");
        // calling the deposit function from the ERC-4626 library to perform all the necessary functionality
        deposit(_assets, msg.sender);
        // Increase the share of the user
        shareHolder[msg.sender] += _assets;
    }

    /**
     * @notice Function to allow msg.sender to withdraw their deposit plus accrued interest
     * @param _shares amount of shares the user wants to convert
     * @param _receiver address of the user who will receive the assets
     */
    function _withdraw(uint _shares, address _receiver) public {
        // checks that the deposited amount is greater than zero.
        require(_shares > 0, "withdraw must be greater than Zero");
        // Checks that the _receiver address is not zero.
        require(_receiver != address(0), "Zero Address");
        // checks that the caller is a shareholder
        require(shareHolder[msg.sender] > 0, "Not a share holder");
        // checks that the caller has more shares than they are trying to withdraw.
        require(shareHolder[msg.sender] >= _shares, "Not enough shares");
        // Calculate 10% yield on the withdrawal amount
        uint256 percent = (10 * _shares) / 100;
        // Calculate the total asset amount as the sum of the share amount plus 10% of the share amount.
        uint256 assets = _shares + percent;
        // calling the redeem function from the ERC-4626 library to perform all the necessary functionality
        redeem(assets, _receiver, msg.sender);
        // Decrease the share of the user
        shareHolder[msg.sender] -= _shares;
    }

    // returns total number of assets
    function totalAssets() public view override returns (uint256) {
        return asset.balanceOf(address(this));
    }

    // returns total balance of user
    function totalAssetsOfUser(address _user) public view returns (uint256) {
        return asset.balanceOf(_user);
    }
}

就这样!你已经创建了使用 ERC-4626 Tokenized Vault Standard 生成收益的保管箱智能合约。

部署保管箱智能合约

现在,让我们部署我们的保管箱智能合约。不过,在此之前,我们需要创建一个 ERC-20 智能合约,作为资产代币。

以下是用于资产代币的智能合约:

// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract USDC is ERC20 {
    constructor() ERC20("USDC", "USDC") {}

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

    function decimals() public view virtual override returns (uint8) {
        return 18;
    }
}

首先创建一个名为 USDC.sol 的文件,并粘贴上述资产代币智能合约代码。然后,在环境中选择 Injected Provider - Metamask 选项。最后,选择资产(USDC)智能合约并单击 部署 按钮。然后,复制代币的合约地址。

Remix.IDE USDC 部署

选择 TokenVault 智能合约并输入以下参数:

  • 资产代币地址
  • 保管箱代币名称
  • 保管箱代币符号

单击 事务 按钮。MetaMask 或其他以太坊钱包将弹出。在钱包中单击确认按钮。

在 MetaMask 中确认交易

现在,返回到资产智能合约,或 USDC.sol 智能合约,并铸造 10,000 wei 单位的代币。输入你的钱包地址和所需的数量。

在 Remix.IDE 中铸造交易

随后,通过调用 approve 函数授权保管箱合约地址支出 USDC。输入保管箱合约地址和要批准的金额。单击 事务 按钮,然后在 MetaMask 中单击 批准 按钮。

在 Remix.IDE 中批准交易

返回到 TokenVault.sol 并与 _deposit 函数交互,以便在资产中接收股份/保管箱代币。输入你想存入的资产数量。单击 事务 按钮。之后,在 MetaMask 中单击 确认 按钮。

在 Remix.IDE 中存入交易

现在,检查 totalAssetstotalAssetsOfUser 函数。如前所述,totalAssets 显示 TokenVault 合约中的 USDC 余额,而 totalAssetsOfUser 显示用户的 USDC 余额。

在 Remix.IDE 中读取状态

TotalAssets 是 10,000,因为我们刚存入了 10,000 wei 的 USDC,而 TotalAssetsOfUser 是 0,因为我们仅铸造了 10,000 wei,这现在已存入 TokenVault 合约中。

让我们通过与 _withdraw 函数交互来提取我们的资产代币以及收益以换取股份/vUSDC。输入你想要提取的数量和接收资产代币/USDC 的收件人地址。点击 事务 按钮以继续,然后在 MetaMask 中点击 确认 按钮。

现在,有一个问题。如果你尝试提取你存入的所有 vUSDC,你会遇到一个错误。这是因为合约没有足够的资产余额。如果你进行计算,10,000 vUSDC 等于 11,000 USDC(10,000 来自原始存款,加上 1,000 的收益),但合约只有 10,000 wei 的 USDC。

在 Remix.IDE 中提取交易

让我们再次检查 totalAssetstotalAssetsOfUser 函数。

在 Remix.IDE 中读取状态

我们可以看到 totalAssetsOfUser 现在是 1,100,这意味着用户收回了 1,000 的初始资本和 100 的利息。此外,保管箱合约中的总资产现在是 8,900,确认保管箱合约已归还 1,000 的初始资本和 100 的利息。

结论

如果你能读到这里,恭喜你!你正在成为 Solidity 专家的路上。本指南中,我们探讨了 ERC-4626 Tokenized Vault Standard,了解了收益生成保管箱的运作方式,并发现了如何使用 ERC-4626 Tokenized Vault Standard 构建我们自己的收益生成保管箱。

我们希望听到更多关于你正在构建的项目的信息。欢迎在 Discord 中发信息给我们,或者在 Twitter 上关注我们,了解最新信息!

我们 ❤️ 反馈!

如果你对此指南有任何反馈,请 告知我们。我们会很高兴听到你的意见。

  • 原文链接: quicknode.com/guides/eth...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。