本文主要分析了 ERC4626 标准下的 On-chain Vault 可能遭受的通胀攻击,攻击者通过操纵 Vault 中的资产总量来稀释其他用户的份额,并探讨了多种防御方法,包括初始化 Vault 资产、内部控制资产总量、使用 decimals offset 以及 OpenZeppelin 提出的虚拟份额和资产方案。
什么是 ERC4626?
ERC4626 是 ERC20 的扩展,它为 token 金库提出了一种标准接口,提供存款、取款 token 和读取余额的功能。
contract ERC4626 is ERC20, IERC4626 {}
实现的主要功能有
function deposit(uint256 assets, address receiver) external returns (uint256 shares);
function mint(uint256 shares, address receiver) external returns (uint256 assets);nt256 shares, address receiver, address owner) external returns (uint256 assets);
function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares);
function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets);
function convertToShares(uint256 assets) external view returns (uint256 shares);
function convertToAssets(uint256 shares) external view returns (uint256 assets);
份额和金额之间存在一个公式:
这里的 share 是用户应该获得的份额数量,asset 是用户存入或提取到金库的资产数量。确定资产和份额之间的比率。
Uniswap V2 流动性池本质上就是一个金库。
让我们考虑一个场景
这是如何发生的?攻击者可以通过增加分母来操纵分母,导致用户由于向下舍入而获得比预期更少的份额。
让我们根据 通货膨胀攻击 对其进行分析,假设
在攻击者捐赠后,金库的比率为
为了将存款稀释为 0 份额,只需要确保
使用 a0 为 1 且 a1 为 u 就足够了。因此,攻击者只需要 u+1 即可执行攻击
如果金库中的初始资产不为零,则攻击者的成本很高。即使没有其他防御方法,假设所有者将 1000 个资产存入金库。
userShare = totalyShare(1000) * assetAmount(1000) / totalAsset(1000 + x)
攻击者需要使 (x + 1000) 大于 10⁶,因此 x = 10^-1000
攻击成功的另一个原因是,攻击者可以将资产(捐赠)转移到合约中,从而稀释份额。
跟踪金库持有的资产的内部状态,而不是依赖于 balanceOf 函数。这样,捐赠的资产就无法影响金库的内部会计。
YieldBox 在 totalShare 中添加了一个十进制偏移量,这意味着 1 个资产不对应 1 个份额。
function _toShares(
uint256 amount,
uint256 totalShares_,
uint256 totalAmount,
bool roundUp
) internal pure returns (uint256 share) {
// To prevent reseting the ratio due to withdrawal of all shares, we start with
// 1 amount/1e8 shares already burned. 这也从 1 : 1e8 的比率开始
// functions like 8 decimal fixed point math. This prevents ratio attacks or inaccuracy
// due to 'gifting' or rebasing tokens. (Up to a certain degree)
totalAmount++;
totalShares_ += 1e8;
// Calculte the shares using te current amount to share ratio
// 使用当前金额与份额的比率计算份额
share = (amount * totalShares_) / totalAmount;
// Default is to round down (Solidity), round up if required
// 默认是向下舍入 (Solidity),如果需要则向上舍入
if (roundUp && (share * totalAmount) / totalShares_ < amount) {
share++;
}
}
Oz 提出了基于 YieldBox 的防御措施,它由两部分组成:虚拟份额和十进制偏移量
上面的公式变为:
这是 openzeppelin 在 v4.9 之后根据上述公式的实现
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) {
return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding);
}
让我们看看如果没有小数位,上述攻击是否会成功。现在我们有:
因此,最小的 x 应该是 1999。金库的总资产为 2000
用户存入 1000 个资产,获得 shareReceived = 1000 * (1 + 1) / 2001 = 0,金库状态:totalAssets = 3000,totalShares = 1
攻击者赎回 1 个份额,收到 token 数量 = 1 * (3001) / (1 + 1 ) = 1500
我们可以看到,即使没有小数位,攻击者仍然损失了 500 个资产。如果用户没有存款,则损失至少等于用户的存款 (1000)。因此,虚拟份额和资产使此攻击对攻击者来说无利可图。
实际上,攻击的捐赠必须遭受更多的损失,以确保如果没有小数位,用户无法获得份额。
这是我编写的一个用于测试的程序:
https://x.com/bloom_cry/status/1852965843988365736
ERC4626,熟悉 ERC20 的扩展,为 token 金库提供了一个标准化接口,使开发人员能够构建…
- 原文链接: blog.blockmagnates.com/o...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!