ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

WTF-Solidity 教程精讲:ERC4626 代币化金库标准——从标准接口到金库合约实战

WTF-Solidity 教程精讲:ERC4626 代币化金库标准——从标准接口到金库合约实战 WTF-Solidity 教程精讲ERC4626 代币化金库标准——从标准接口到金库合约实战【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity本文基于 WTF-Solidity 极简入门教程第 51 讲Languages/en/51_ERC4626_en/readme.md深入讲解 DeFi 新一代标准 ERC4626代币化金库标准。你将理解金库Vault合约在 DeFi 乐高中的作用、ERC4626 如何扩展 ERC20 实现收益金库标准化并完整掌握 IERC4626 接口的 16 个函数与 2 个事件最终能用 Solidity 手写一个可运行的极简代币化金库合约并在 Remix 中完成部署与存取款全流程验证。为什么需要金库标准DeFi 常被比喻为货币乐高通过组合多个协议可以创造新的协议。但金库合约Vault Contract缺乏统一标准各家实现五花八门一个收益聚合器往往需要针对不同的 DeFi 项目编写大量对接接口严重制约了 DeFi 的可组合性。金库合约是 DeFi 乐高的基础组件它允许你把基础资产代币质押到合约中换取一定收益典型应用场景包括收益农场Yield Farming在 Yearn Finance 中质押USDT获取利息。借贷Borrow/Lend在 AAVE 中出借ETH获取存款利息并可在抵押后借出其他资产。质押Stake在 Lido 中质押ETH参与 ETH 2.0 质押获得可以生息的stETH。ERC4626代币化金库标准概述注上图位于 Languages/en/51_ERC4626_en/img/51-1.png展示了 ERC4626 金库收资产、发份额、投策略的核心架构。ERC4626 扩展了 ERC20 代币标准旨在推动收益金库的标准化使 DeFi 能够轻松扩展。它带来了三大优点代币化TokenizationERC4626 继承了 ERC20向金库存款时你将得到同样符合 ERC20 标准的金库份额。例如在 Lido 质押 ETH会自动获得stETH作为你的份额。更好的流通性Better Liquidity由于份额被代币化你可以在不取回基础资产的情况下利用金库份额做其他事情。以 Lido 的stETH为例你可以直接在 Uniswap 上为它提供流动性或进行交易而无需取出其中的 ETH。更好的可组合性Better Composability有了统一标准后用一套接口即可与所有 ERC4626 金库交互让基于金库的应用、插件和工具开发变得更容易。可以说ERC4626 对 DeFi 的重要性不亚于 ERC721 对 NFT 的重要性。ERC4626 的四大核心逻辑ERC20 继承ERC4626 继承 ERC20金库份额就是用 ERC20 代币代表的。用户将特定的 ERC20 基础资产如 WETH存入金库合约为其铸造特定数量的金库份额代币当用户提取基础资产时合约销毁相应数量的份额代币。asset()函数返回金库基础资产的代币地址。存款逻辑允许用户存入基础资产并铸造相应数量的金库份额。相关函数为deposit()和mint()。deposit(uint assets, address receiver)让用户存入assets单位资产并铸造相应份额给receivermint(uint shares, address receiver)类似区别是以要铸造的份额数量为参数。提款逻辑允许用户销毁金库份额并提取金库中相应数量的基础资产。相关函数为withdraw()和redeem()前者以取出基础资产数量为参数后者以销毁的金库份额为参数。会计与限额逻辑其余函数用于统计金库资产、设定存款/提款限额以及换算存款/提款对应的基础资产与金库份额数量。IERC4626 接口合约全解IERC4626 接口合约源码见 Languages/en/51_ERC4626_en/IERC4626.sol共包含2 个事件Deposit事件存款时触发。Withdraw事件取款时触发。接口还包含16 个函数按功能分为4 大类元数据Metadataasset()返回金库基础资产代币地址用于存款与取款。存款/提款逻辑Deposit/Withdrawal Logicdeposit()用户存入assets单位基础资产合约铸造shares单位金库份额给receiver释放Deposit事件。mint()用户指定想获得的shares份额函数计算出需存入的assets基础资产并转出再给receiver铸造指定份额释放Deposit事件。withdraw()owner销毁shares份额合约将相应基础资产发送给receiver释放Withdraw事件。redeem()owner销毁shares份额合约将相应基础资产发给receiver释放Withdraw事件。会计逻辑Accounting LogictotalAssets()返回金库中管理的基础资产总额须包含利息与费用。convertToShares()返回用一定数额基础资产可换取的金库份额不含费用与滑点。convertToAssets()返回用一定数额金库份额可换取的基础资产不含费用与滑点。previewDeposit()模拟当前链上环境存款assets可获得的金库份额考虑费用须不大于同交易实际获得值可与convertToAssets差值计算滑点。previewMint()模拟当前链上环境铸造shares份额需要存款的基础资产数量。previewWithdraw()模拟当前链上环境提取assets基础资产需要赎回的份额。previewRedeem()模拟当前链上环境销毁shares份额能赎回的基础资产数量。存款/提款限额逻辑Deposit/Withdrawal Limit LogicmaxDeposit()返回某地址单次存款的最大基础资产数额。maxMint()返回某地址单次铸造的最大金库份额。maxWithdraw()返回某地址单次取款可提取的最大基础资产。maxRedeem()返回某地址单次赎回可销毁的最大金库份额无其他限制时应等于balanceOf(owner)。接口合约完整代码如下对应仓库 IERC4626.sol// SPDX-License-Identifier: MIT // Author: 0xAA from WTF Academy pragma solidity ^0.8.34; import openzeppelin/contracts/token/ERC20/IERC20.sol; import openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol; /** * dev ERC4626 Tokenized Vaults Standard interface contract * https://eips.ethereum.org/EIPS/eip-4626. */ interface IERC4626 is IERC20, IERC20Metadata { /*////////////////////////////////////////////////////////////// event //////////////////////////////////////////////////////////////*/ // triggered when depositing event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); // triggered when withdrawing event Withdraw( address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares ); /*////////////////////////////////////////////////////////////// metadata //////////////////////////////////////////////////////////////*/ /** * dev returns the address of the underlying asset token of the vault (used for deposit and withdrawal) * - has to be ERC20 token contract address * - cannot revert */ function asset() external view returns (address assetTokenAddress); /*////////////////////////////////////////////////////////////// deposit/withdraw logic //////////////////////////////////////////////////////////////*/ /** * dev deposit function: user deposit ${assets} units of underlying asset to vault, * and the contract mints ${shares} unit vault share to receivers address * * - has to emit Deposit event * - if asset cannot be deposited succuessfully, must revert. e.g. when deposit amount exceeds limit */ function deposit(uint256 assets, address receiver) external returns (uint256 shares); /** * dev mint function: users deposit ${assets} units of the underlying asset * and the contract mints the corresponding amount of the vaults shares to the receivers address * - has to emit Deposit event * - if it cannot mint, must revert. e.g. minting amount exceeds limit */ function mint(uint256 shares, address receiver) external returns (uint256 assets); /** * dev withdraw function: owner address burns ${share} units of the vaults shares, * and the contract transfers the corresponding amount of the underlying asset to the receiver address * * - emit Withdraw event * - if all assets cannot be withdrew, it will revert */ function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); /** * dev redeem function: owner address burns ${share} units of the vaults shares, * and the contract transfers the corresponding amount of the underlying asset to the receiver address * * - emit Withdraw event * - if vaults share cannot be redeemed, then revert */ function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); /*////////////////////////////////////////////////////////////// Accounting Logic //////////////////////////////////////////////////////////////*/ /** * dev returns the total amount of underlying asset tokens managed in the vault * * - include interest * - include fee * - cannot revert */ function totalAssets() external view returns (uint256 totalManagedAssets); /** * dev returns the amount of vault shares that can be obtained by using a certain amount of the underlying asset * - do not include fee * - do not include slippage * - cannot revert */ function convertToShares(uint256 assets) external view returns (uint256 shares); /** * dev returns the amount of underlying asset that can be obtained by using a certain amount of vault shares * * - do not include fee * - do not include slippage * - cannot revert */ function convertToAssets(uint256 shares) external view returns (uint256 assets); /** * dev used by both on-chain and off-chain users to simulate the amount of vault shares they can obtain by depositing a certain amount of the underlying asset in the current on-chain environment * * - the return value should be close to and not greater than the vault amount obtained by depositing in the same transaction * - do not consider about restrictions like maxDeposit, assume that user deposit will succeed * - consider fee * - cannot revert * NOTE: use the difference of the return values of convertToAssets and previewDeposit to calculate slippage */ function previewDeposit(uint256 assets) external view returns (uint256 shares); /** * dev used by both on-chain and off-chain users to simulate the amount of underlying asset needed to mint a certain amount of vault shares in the current on-chain environment * - the return value should be close to and not less than the deposit amount required to mint a certain amount of vault amount in the same transaction. * - do not consider about restrictions like maxMint, assume that user mint transaction will succeed * - consider fee * - cannot revert */ function previewMint(uint256 shares) external view returns (uint256 assets); /** * dev used by both on-chain and off-chain users to simulate the amount of vault shares they need to redeem to withdraw a certain amount of the underlying asset in the current on-chain environment * - the return value should be close to and not greater than the vault share needed to redeem a certain amount of underlying asset withdrawn in the same transaction. * - do not consider about restrictions like maxWithdraw, assume that user withdraw transaction will succeed * - consider fee * - cannot revert */ function previewWithdraw(uint256 assets) external view returns (uint256 shares); /** * dev used by on-chain and off-chain users to simulate the amount of underlying asset they can redeem by burning a certain amount of vault shares in the current on-chain environment * - the return value should be close to and not less than the amount of underlying asset that can be redeemed by the vault amount burnt in the same transaction. * - do not consider about restrictions like maxRedeem, assume that user redeem transaction will succeed * - consider fee * - cannot revert */ function previewRedeem(uint256 shares) external view returns (uint256 assets); /*////////////////////////////////////////////////////////////// deposit/widthdrawal limit logic //////////////////////////////////////////////////////////////*/ /** * dev returns the maximum amount of underlying asset that can be deposited in a single transaction for a given user address. * - if there is max deposit limit, return value should be a finite value * - return value should not be greater than 2 ** 256 - 1 * - cannot revert */ function maxDeposit(address receiver) external view returns (uint256 maxAssets); /** * dev returns the maximum vault amount that can be minted in a single transaction for a given user address. * - if there is max mint limit, return value should be a finite value * - return value should not be greater than 2 ** 256 - 1 * - cannot revert */ function maxMint(address receiver) external view returns (uint256 maxShares); /** * dev returns the maximum amount of underlying asset that can be withdrawn in a single transaction for a given user address. * - return value should be a finite value * - cannot revert */ function maxWithdraw(address owner) external view returns (uint256 maxAssets); /** * dev returns the maximum vault amount that can be redeemed in a single transaction for a given user address. * - return value should be a finite value * - if there are no other restrictions, the return value should be balanceOf(owner) * - cannot revert */ function maxRedeem(address owner) external view returns (uint256 maxShares); }手写极简 ERC4626 金库合约设计约定仓库中的教学合约 Languages/en/51_ERC4626_en/ERC4626.sol与 51_ERC4626/ERC4626.sol 同源实现了一个极简代币化金库遵循以下约定构造函数初始化基础资产的合约地址、金库份额代币的名称与符号。注意份额代币的名称/符号应与基础资产关联例如基础资产叫WTF金库份额最好叫vWTF。存款用户存入x单位基础资产铸造x单位等量金库份额。提款用户销毁x单位金库份额提取x单位等量基础资产。完整实现与逐段剖析// SPDX-License-Identifier: MIT pragma solidity 0.8.0; import {IERC4626} from ./IERC4626.sol; import {ERC20, IERC20Metadata} from openzeppelin/contracts/token/ERC20/ERC20.sol; /** * dev ERC4626 Tokenized Vaults Standard contract. * FOR TEACHING PURPOSE ONLY, DO NOT USE IN PRODUCTION */ contract ERC4626 is ERC20, IERC4626 { /*////////////////////////////////////////////////////////////// state variables //////////////////////////////////////////////////////////////*/ ERC20 private immutable _asset; // uint8 private immutable _decimals; constructor( ERC20 asset_, string memory name_, string memory symbol_ ) ERC20(name_, symbol_) { _asset asset_; _decimals asset_.decimals(); } /** dev See {IERC4626-asset}. */ function asset() public view virtual override returns (address) { return address(_asset); } /** * See {IERC20Metadata-decimals}. */ function decimals() public view virtual override(IERC20Metadata, ERC20) returns (uint8) { return _decimals; } /*////////////////////////////////////////////////////////////// deposit/withdrawal logic //////////////////////////////////////////////////////////////*/ /** dev See {IERC4626-deposit}. */ function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) { // use previewDeposit() to calculate vault share to be retained shares previewDeposit(assets); // transfer first then mint, prevent reentrancy attack _asset.transferFrom(msg.sender, address(this), assets); _mint(receiver, shares); // emit Deposit event emit Deposit(msg.sender, receiver, assets, shares); } /** dev See {IERC4626-mint}. */ function mint(uint256 shares, address receiver) public virtual returns (uint256 assets) { // use previewDeposit() to calculate amount of underlyting asset that needs to be deposited assets previewMint(shares); // transfer first then mint, prevent reentrancy attack _asset.transferFrom(msg.sender, address(this), assets); _mint(receiver, shares); // emit Deposit event emit Deposit(msg.sender, receiver, assets, shares); } /** dev See {IERC4626-withdraw}. */ function withdraw( uint256 assets, address receiver, address owner ) public virtual returns (uint256 shares) { // use previewWithdraw() to calculate vault share that will be burnt shares previewWithdraw(assets); // if caller is not owner, check and update allownance if (msg.sender ! owner) { _spendAllowance(owner, msg.sender, shares); } // burn first then transfer, prevent reentrancy attack _burn(owner, shares); _asset.transfer(receiver, assets); // emit Withdraw event emit Withdraw(msg.sender, receiver, owner, assets, shares); } /** dev See {IERC4626-redeem}. */ function redeem( uint256 shares, address receiver, address owner ) public virtual returns (uint256 assets) { // use previewRedeem() to calculate the amount of underlying asset that can be redeemed assets previewRedeem(shares); // if caller is not owner, check and update allownance if (msg.sender ! owner) { _spendAllowance(owner, msg.sender, shares); } // burn first then transfer, prevent reentrancy attack _burn(owner, shares); _asset.transfer(receiver, assets); // emit Withdraw event emit Withdraw(msg.sender, receiver, owner, assets, shares); } /*////////////////////////////////////////////////////////////// accounting logic //////////////////////////////////////////////////////////////*/ /** dev See {IERC4626-totalAssets}. */ function totalAssets() public view virtual returns (uint256){ // returns balance of underlying asset for this contract return _asset.balanceOf(address(this)); } /** dev See {IERC4626-convertToShares}. */ function convertToShares(uint256 assets) public view virtual returns (uint256) { uint256 supply totalSupply(); // if supply is 0, then mint vault share at 1:1 ratio // if supply is not 0, then mint vault share at actual ratio return supply 0 ? assets : assets * supply / totalAssets(); } /** dev See {IERC4626-convertToAssets}. */ function convertToAssets(uint256 shares) public view virtual returns (uint256) { uint256 supply totalSupply(); // if supply is 0, then redeem underlying asset at 1:1 ratio // if supply is not 0, then redeem underlying asset at actual ratio return supply 0 ? shares : shares * totalAssets() / supply; } /** dev See {IERC4626-previewDeposit}. */ function previewDeposit(uint256 assets) public view virtual returns (uint256) { return convertToShares(assets); } /** dev See {IERC4626-previewMint}. */ function previewMint(uint256 shares) public view virtual returns (uint256) { return convertToAssets(shares); } /** dev See {IERC4626-previewWithdraw}. */ function previewWithdraw(uint256 assets) public view virtual returns (uint256) { return convertToShares(assets); } /** dev See {IERC4626-previewRedeem}. */ function previewRedeem(uint256 shares) public view virtual returns (uint256) { return convertToAssets(shares); } /*////////////////////////////////////////////////////////////// DEPOSIT/WITHDRAWAL LIMIT LOGIC //////////////////////////////////////////////////////////////*/ /** dev See {IERC4626-maxDeposit}. */ function maxDeposit(address) public view virtual returns (uint256) { return type(uint256).max; } /** dev See {IERC4626-maxMint}. */ function maxMint(address) public view virtual returns (uint256) { return type(uint256).max; } /** dev See {IERC4626-maxWithdraw}. */ function maxWithdraw(address owner) public view virtual returns (uint256) { return convertToAssets(balanceOf(owner)); } /** dev See {IERC4626-maxRedeem}. */ function maxRedeem(address owner) public view virtual returns (uint256) { return balanceOf(owner); } }关键实现点解析份额换算公式ERC4626.solconvertToShares在totalSupply() 0时按1:1铸造份额否则按assets * supply / totalAssets()的当前汇率换算convertToAssets与之互为逆运算。这正是早期 1:1、收益累积后按净值兑换的代币化金库核心。防重入顺序deposit/mint采用先transferFrom转资产、后_mint铸份额的顺序ERC4626.solwithdraw/redeem采用先_burn销毁、后transfer转出的顺序ERC4626.sol避免重入攻击。授权校验withdraw/redeem在调用者不是owner时通过_spendAllowance(owner, msg.sender, shares)校验并扣减 ERC20 授权额度复用继承自 ERC20 的授权机制教学版 ERC20 实现见 Languages/en/51_ERC4626_en/ERC20.sol。无限制默认值maxDeposit/maxMint直接返回type(uint256).max表示无上限maxWithdraw按份额折算maxRedeem直接返回balanceOf(owner)。小数位对齐构造函数从基础资产读取decimals()并存入 immutable 变量decimals()函数重写为返回基础资产的小数位保证份额与资产数量级一致。教学版与 OpenZeppelin 实现的差距仓库内嵌了 OpenZeppelin Contractslib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC4626.sol对照其源码可清晰看到教学版省略的两类关键工程细节四舍五入方向Rounding DirectionERC4626 标准要求会计函数明确向上/向下取整OpenZeppelin 用Math.mulDiv等库在存款向用户有利、提款向协议有利的方向取整防止精度损耗被套利教学版直接整除未做方向控制。通胀攻击Inflation Attack防护空金库或接近空的金库中攻击者可通过捐赠抬高份额价格、front-run 首笔存款来偷取价值。OpenZeppelin 从 v4.9 起引入可配置的虚拟资产/虚拟份额_decimalsOffset()来抑制该攻击——源码注释明确指出默认偏移量0即可让攻击无利可图更大的偏移量会使攻击成本呈数量级上升同时其实现还定义了ERC4626ExceededMaxDeposit、ERC4626ExceededMaxMint、ERC4626ExceededMaxWithdraw、ERC4626ExceededMaxRedeem等自定义错误用于限额 revert。因此教学合约仅用于理解标准逻辑严禁用于生产生产环境建议直接使用 OpenZeppelin 的成熟实现。Remix 实战演示以下演示使用 Remix 中第二个账户0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2来部署合约和调用函数完整走通部署 → 授权 → 存款 → 提款闭环部署基础资产 ERC20部署仓库附带的 ERC20.sol将代币名称和符号均设为WTF并给自己铸造10000枚代币。部署 ERC4626 金库部署 ERC4626.sol构造参数中基础资产地址填WTF合约地址名称和符号均设为vWTF。授权调用ERC20合约的approve()函数将代币授权给ERC4626合约金额需大于后续存款额。deposit 存款调用ERC4626合约的deposit()存款1000枚代币再调用balanceOf()确认金库份额变为1000。mint 存款调用mint()存入另一笔1000枚代币此处教学版 1:1 汇率下mint(1000, receiver)对应存入 1000调用balanceOf()确认份额变为2000。withdraw 提款调用withdraw()提取1000枚代币调用balanceOf()确认份额降至1000。redeem 赎回调用redeem()赎回剩余1000枚代币调用balanceOf()确认份额归零。以上截图均为教程仓库中的真实 Remix 运行截图Languages/en/51_ERC4626_en/img/依次对应部署 ERC20、部署 ERC4626、approve 授权、deposit、mint、withdraw、redeem 的完整操作链。小结本讲系统介绍了代币化金库标准ERC4626并手写了一个可将基础资产按 1:1 汇率转换为金库份额代币的极简金库合约。核心要点回顾ERC4626 继承 ERC20通过2 个事件 16 个函数元数据、存款/提款、会计、限额四大类统一了收益金库的标准接口教学合约通过convertToShares/convertToAssets的汇率换算、先转账后铸造/先销毁后转账的防重入顺序、ERC20 授权复用等技巧浓缩了金库逻辑的精华生产环境必须处理四舍五入方向与通胀攻击等问题应优先采用 OpenZeppelin 的正式实现可对比仓库 lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC4626.sol 学习。ERC4626 为 DeFi 提升了流动性与可组合性未来将逐渐普及。接下来不妨思考基于这个金库标准你会构建什么应用【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表