本文详细介绍了 ERC4626 标准,它为 token vaults 提供了一个标准化的接口,并探讨了 Vault 的概念及 Yearn Vaults 的实际应用。此外,文章还深入分析了 Openzeppelin 的 ERC4626 实现,包括构造函数、公共/外部函数、私有/内部函数和 getter 函数,最后强调了 ERC4626 Vault 的安全问题,如通货膨胀攻击,并提出了相应的防御措施。
ERC4626 是 ERC20 标准的扩展,为 token 金库提供了一个标准化的接口。该标准支持各种合约,例如借贷服务、聚合器和赚取利息的 token,从而增强去中心化金融 (DeFi) 领域内的功能和精确性。
金库是一个智能合约,用户可以在其中存入他们的 token(如 USDC、DAI 等),并获得 shares 作为回报。 这些 shares 代表用户在金库中的所有权,与其存入的 token 成正比。
根据金库的表现,用户可以提取他们的初始存款以及任何获得的利润或产生的损失,此时他们的 shares 将被取消,这标志着他们所有权的结束。
金库的运营由所有者管理,所有者将存入的 token 投资到各种 DeFi 协议中以产生收益。 这些投资的成功决定了金库的利润或损失,这反过来又影响了用户提款时收到的价值。
当用户将 token 存入金库时,合约会计算要铸造的 shares 数量,以响应金库总 token 余额的增加。 这确保了 shares 的分配是公平的,并且与存款规模直接相关。
这个原则很简单:如果一笔存款使金库的 token 余额增加了一定的百分比,那么总 shares 的数量也会增加相同的百分比,从而确保所有参与者之间的公平。
提款时,流程会反转:shares 被销毁,用户收到他们的 token,并根据金库所经历的任何收益或损失进行调整。 这种机制确保了参与金库的所有用户都能获得透明和公平的系统。
Yearn 金库就像特殊的储蓄账户,旨在通过使用智能策略来增加你的数字货币。与仅仅将你的 coins 保存在普通账户中不同,这些金库会积极工作以从你的投资中赚取更多钱。
每个人的选择:Yearn 金库提供不同类型的投资,例如稳定币(DAI、USDC、USDT)、特殊资金池 token(如 y、sbtc、busd),甚至投资于流行的加密货币(如 LINK、YFI、ETH)。 这种多样性意味着可能有一个金库适合你的需求。
Openzeppelin 提供了 ERC4626 的基本实现,其中包括一个简单的金库。 此合约的设计方式允许开发人员轻松地重新配置金库的行为。
让我们彻底了解一下合约。
constructor(IERC20 asset_)
这是在部署合约时调用的函数。 它使用特定的底层资产初始化金库,该资产是金库将管理的 ERC20 token。
构造函数尝试获取底层资产的小数位数,以确保 shares 计算准确地反映资产的面额。
如果无法获取小数位数,则默认为 18,这是 ERC20 token 的常用标准。 此设置对于确保金库内的所有数学运算都考虑资产的正确规模至关重要。
这些函数旨在由用户或其他合约调用。 它们构成了与金库交互的核心接口:
deposit(uint256 assets, address receiver)
目的:允许用户将底层资产存入金库。
参数:
assets
: 要存入的底层资产金额receiver
: 将收到与存入资产对应的 shares 的地址。返回值:由于存款而铸造给接收者的 shares 数量。
行为:此函数根据资产和 shares 之间的当前汇率,计算应铸造多少 shares 以换取存入的资产。 然后,它将这些 shares 铸造到 receiver
地址。
mint(uint256 shares, address receiver)
目的:允许用户直接铸造 shares,指定所需的 shares 数量而不是资产数量。
参数:
shares
: 用户想要铸造的 shares 数量receiver
: 将收到铸造 shares 的地址返回值:为了换取铸造的 shares 而存入金库的资产金额。
行为:此函数计算铸造指定数量的 shares 所需的基础资产数量。 然后,它将这些资产从调用者转移到金库,并将 shares 铸造到 receiver
。
withdraw(uint256 assets, address receiver, address owner)
目的:允许 shares 的所有者从金库中提取底层资产。
参数:
assets
: 要提取的底层资产金额receiver
: 将收到资产的地址owner
: 将从中销毁 shares 的地址返回值:为了换取提取的资产而被销毁的 shares 数量。
行为:此函数计算与请求的资产提取金额相对应的 shares 数量。 它从 owner
的余额中销毁计算出的 shares,并将请求的资产从金库转移到 receiver
。
redeem(uint256 shares, address receiver, address owner)
目的:允许用户赎回 shares 以获取底层资产。
参数:
shares
: 要赎回的 shares 数量。receiver
: 将收到与赎回的 shares 对应的底层资产的地址owner
: 将从中销毁 shares 的地址_convertToShares(uint256 assets, Math.Rounding rounding)
目的:根据金库内的当前汇率,将底层资产的金额转换为等量的 shares 。
参数:
assets
: 要转换为 shares 的底层资产金额rounding
: 计算中要使用的舍入方向。返回值:与指定的资产金额相对应的计算 shares 金额。
行为:此函数使用金库当前的资产总持有量和 shares 供应量来确定资产和 shares 之间的汇率。 然后,它将此汇率应用于指定的资产金额,并根据指定进行舍入调整,以计算等效的 shares 金额。
_convertToAssets(uint256 shares, Math.Rounding rounding)
目的:将指定数量的 shares 转换回底层资产,反映这些 shares 在金库中的当前价值。
参数:
shares
: 要转换为资产的 shares 数量rounding
: 在转换过程中应用的舍入方向返回值:相当于指定 shares 数量的底层资产金额。
行为:利用金库的总 shares 和资产余额,此函数确定单个 share 的价值。 然后,它计算指定 shares 数量的总资产价值,再次考虑所需的舍入方向。
_deposit(address caller, address receiver, uint256 assets, uint256 shares)
目的:管理将资产存入金库并铸造相应 shares 的内部机制。
参数:
caller
: 启动存款的地址receiver
: 指定接收新铸造 shares 的地址。assets
: 正在存入的资产金额shares
: 为了换取存入的资产而铸造的 shares 数量。行为:此函数将指定的资产金额从 caller
安全地转移到金库。 然后,它将计算出的 shares 金额铸造到 receiver
。 此过程确保资产托管安全转移并在 shares 的铸造中得到准确反映。
_withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares)
目的:处理销毁 shares 并将相应的资产金额从金库转移到接收者的过程。
参数:
caller
: 请求提款的地址receiver
: 接收与销毁的 shares 对应的资产的地址。owner
: 从中赎回 shares 的地址。assets
: 要转移到接收者的资产金额。shares
: 要从所有者余额中销毁的 shares 数量。行为:如果 caller
不是 owner
,此函数首先验证然后花费 share 金额。 它从 owner
的余额中销毁指定数量的 shares,确保 shares 供应准确地反映提款。
随后,它将计算出的资产金额转移到 receiver
,完成赎回过程。
asset()
目的:检索金库管理的底层资产 token 的地址。
返回值:ERC20 token 的地址被认为是金库的底层资产。
totalAssets()
目的:提供金库当前持有的底层资产总额。
返回值:代表金库内底层资产总余额的 uint256
。
convertToShares(uint256 assets)
目的:根据金库的当前状态,计算给定数量的底层资产将转换为多少 shares。
参数:
assets
: 要转换的底层资产金额。返回值:给定资产金额的等效 shares 金额。
convertToAssets(uint256 shares)
目的:确定与特定数量的 shares 相对应的底层资产金额。
参数:
shares
: 要转换为底层资产的 shares 数量。返回值:相当于指定 shares 的底层资产金额。
maxDeposit(address)
目的:指示在查询时,可以为特定地址存入的最大底层资产金额。
参数:
返回值:底层资产的最大存款金额。
maxMint(address)
目的:显示在调用时可以为特定地址铸造的最大 shares 数量。
参数:
返回值:可以铸造的最大 shares 数量。
maxWithdraw(address owner)
目的:指定可以从金库中提取的最大底层资产金额,对应于特定地址持有的 share。
参数:
owner
: share 所有者的地址。返回值:所有者可以提取的最大资产金额。
maxRedeem(address owner)
目的:定义特定地址可以赎回底层资产的最大 shares 数量。
参数:
owner
: share 所有者的地址。返回值:可以赎回的最大 shares 数量。
previewMint(uint256 shares)
目的:提供铸造指定数量 的shares 所需的底层资产数量的预览,而无需进行交易。
参数:
shares
: 要铸造的 shares 数量。返回值:铸造指定 shares 所需的底层资产金额。
previewWithdraw(uint256 assets)
目的:在任何交易之前,估计需要销毁多少 shares 才能提取指定数量的底层资产。
参数:
assets
: 要提取的底层资产金额返回值:为指定的资产金额销毁的 shares 数量。
previewRedeem(uint256 shares)
目的:在执行交易之前,估计通过赎回特定数量的 shares 将获得的底层资产金额。
参数:
shares
: 要赎回的 shares 数量返回值:为指定的 shares 获得的底层资产金额。
decimals()
目的:覆盖 ERC20 的 decimals
方法,以反映金库 shares 的小数精度,这可能与底层资产的小数位数不同,尤其是在应用 _decimalsOffset
时。
返回值:用于金库 shares 的小数位数。
当你将资产存入 ERC4626 金库时,你会收到 shares。 这些 shares 代表你在金库中的一部分,可以换成原始资产加上任何收益。
你为存入的资产获得的 shares 数量取决于金库的当前汇率,而汇率又取决于金库持有的流动性(或资产数量)。
当你存入资产时,系统会计算你应该收到的 shares 数量。 但是,此数字会向下舍入到最接近的整数。 这种舍入有时会导致损失,尤其是在小额存款的情况下。
例如,存入一个应该让你获得少于一个 share 的金额意味着你什么也得不到,实际上是将你的存款捐赠给金库。
攻击者可以通过首先进行少量存款以成为股东,然后向金库存入大量资产来利用这一点。 这种捐赠会极大地改变汇率。 当另一个用户随后进行存款时,倾斜的汇率会导致他们的存款转换为明显更少的 shares,甚至可能为零,从而有效地将他们存款的价值转移给攻击者。
拟议的防御措施包括使用虚拟偏移量(shares 的小数位数多于资产)并在汇率计算中包括虚拟资产和 shares。 通过确保攻击者在不造成重大损失的情况下无法受益,这使得攻击的盈利能力降低甚至无利可图。
虚拟资产和 shares 以这样一种方式调整汇率,即攻击者的捐赠部分地增加了金库的整体价值,而不仅仅是攻击者的 share 。 这减少了攻击的潜在收益,从而保护了用户的存款。
- 原文链接: medium.com/@tomarpari90/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!