跨链协议遭受了极为严重的打击,有高达1.2亿美元在精度方面存在的漏洞当中消失不见,而此次事故已然将DeFi领域长久以来一直被忽视的技术安全隐患明确地暴露了出来。
精度损失触发机制
当用户进行交易的金额小至特定的区间之时,协议内部有关数值的处理方式会引发相应的问题,比如说在处于8 – 9 wei这个微小的范围里边,系统采纳向下取整的计算方法,如此下去便会导致实际给出的金额与系统当中所记录的数值出现偏差,这种偏差在一般的、平常的交易里是微不足道的,可是在特定的条件之下就会被放大 。
攻击者借由Vault合约去调用专门的函数,那函数用以获取缩放因子用以对数值予以处理。在稳定币池的计算模型之内,这般的缩放操作原本应当维持精度,然而当余额处在临界数值之时,向下取整这种操作就会致使相对精度出现误差。这个误差看起来好像微小,却给后续的连锁反应埋下了伏笔。
function onSwap(
SwapRequest memory swapRequest,
uint256[] memory balances,
uint256 indexIn,
uint256 indexOut
) external override onlyVault(swapRequest.poolId) returns (uint256) {
_beforeSwapJoinExit();
_validateIndexes(indexIn, indexOut, _getTotalTokens());
uint256[] memory scalingFactors = _scalingFactors();
return
swapRequest.kind == IVault.SwapKind.GIVEN_IN
? _swapGivenIn(swapRequest, balances, indexIn, indexOut, scalingFactors)
: _swapGivenOut(swapRequest, balances, indexIn, indexOut, scalingFactors);
}
不变值操控过程
精度误差一旦产生,便会对协议核心参数不变值D的计算造成进一步影响哦。这个参数可是维持资产价格稳定的关键指标呢,它的计算经过依赖于经过缩放处理的余额数组啦。当精度损失传递到这个环节的时候呀,D值就会出现异常缩小的情况哟。
D值出现异常变动,这直接致使BPT价格被压低。BPT属于流动性凭证,它的价格原本应当反映池内资产的实际价值。然而在漏洞被触发之际,攻击者能够以远远低于实际价值的价格获取BPT。此过程如同有人蓄意操纵秤砣,使得贵重物品被错误标价。
BPT 价格 = D / totalSupply 其中 D = 不变值(Invariant),来自 Curve 的 StableSwap 模型
攻击路径剖析
// StableMath._calcOutGivenIn
function _calcOutGivenIn(
uint256 amplificationParameter,
uint256[] memory balances,
uint256 tokenIndexIn,
uint256 tokenIndexOut,
uint256 tokenAmountIn,
uint256 invariant
) internal pure returns (uint256) {
/**************************************************************************************************************
// outGivenIn token x for y - polynomial equation to solve //
// ay = amount out to calculate //
// by = balance token out //
// y = by - ay (finalBalanceOut) //
// D = invariant D D^(n+1) //
// A = amplification coefficient y^2 + ( S + ---------- - D) * y - ------------- = 0 //
// n = number of tokens (A * n^n) A * n^2n * P //
// S = sum of final balances but y //
// P = product of final balances but y //
**************************************************************************************************************/
// Amount out, so we round down overall.
balances[tokenIndexIn] = balances[tokenIndexIn].add(tokenAmountIn);
uint256 finalBalanceOut = _getTokenBalanceGivenInvariantAndAllOtherBalances(
amplificationParameter,
balances,
invariant, // 使用旧的 D
tokenIndexOut
);
// No need to use checked arithmetic since `tokenAmountIn` was actually added to the same balance right before
// calling `_getTokenBalanceGivenInvariantAndAllOtherBalances` which doesn't alter the balances array.
balances[tokenIndexIn] = balances[tokenIndexIn] - tokenAmountIn;
return balances[tokenIndexOut].sub(finalBalanceOut).sub(1);
}
经由精心设计交易路径的黑客展开了攻击行动,并在一个批处理交易里接连进行三次资产交换进而实施攻击,也就是先对特定代币余额予以调整,随后触发精度损失漏洞,接下来利用被压低了的BPT价格达成套利,这三个步骤紧密相连。
// StableMath._calculateInvariant
function _calculateInvariant(uint256 amplificationParameter, uint256[] memory balances)
internal
pure
returns (uint256)
{
/**********************************************************************************************
// invariant //
// D = invariant D^(n+1) //
// A = amplification coefficient A n^n S + D = A D n^n + ----------- //
// S = sum of balances n^n P //
// P = product of balances //
// n = number of tokens //
**********************************************************************************************/
// Always round down, to match Vyper's arithmetic (which always truncates).
uint256 sum = 0; // S in the Curve version
uint256 numTokens = balances.length;
for (uint256 i = 0; i < numTokens; i++) {
sum = sum.add(balances[i]); // balances 是缩放后的值
}
if (sum == 0) {
return 0;
}
uint256 prevInvariant; // Dprev in the Curve version
uint256 invariant = sum; // D in the Curve version
uint256 ampTimesTotal = amplificationParameter * numTokens; // Ann in the Curve version
// 迭代计算 D...
// D 的计算影响 balances 的精度
for (uint256 i = 0; i < 255; i++) {
uint256 D_P = invariant;
for (uint256 j = 0; j prevInvariant) {
if (invariant - prevInvariant <= 1) {
return invariant;
}
} else if (prevInvariant - invariant <= 1) {
return invariant;
}
}
_revert(Errors.STABLE_INVARIANT_DIDNT_CONVERGE);
}
那种批处理交易具备的特性致使精度缺失能够积累扩大,第一次交易引发的误差会对第二次交易的计算结果产生影响,且第二次的误差又会叠加渗进第三次交易里,如同多米诺骨牌效应那般,起初的细微偏差最终造成了巨大的财务漏洞。
系统设计缺陷
// BaseGeneralPool._swapGivenIn
function _swapGivenIn(
SwapRequest memory swapRequest,
uint256[] memory balances,
uint256 indexIn,
uint256 indexOut,
uint256[] memory scalingFactors
) internal virtual returns (uint256) {
// Fees are subtracted before scaling, to reduce the complexity of the rounding direction analysis.
swapRequest.amount = _subtractSwapFeeAmount(swapRequest.amount);
_upscaleArray(balances, scalingFactors); // 关键:放大余额
swapRequest.amount = _upscale(swapRequest.amount, scalingFactors[indexIn]);
uint256 amountOut = _onSwapGivenIn(swapRequest, balances, indexIn, indexOut);
// amountOut tokens are exiting the Pool, so we round down.
return _downscaleDown(amountOut, scalingFactors[indexOut]);
}
在多个关键环节当中,协议有着设计疏漏存在。缩放函数是以向下取整的方式去处理微小余额的,这便是引发精度损失的起始点。与此同时,系统对于不变值D的变动范围缺少有效的监控,没能及时发现异常波动。
更严重的情形表现为,批处理交易里的连续操作,会致使精度误差持续不断地累积。而系统却并未设置具备必须性的校验机制,用以阻断这种累积效应。这就如同在银行柜台持续办理多笔业务的时候,每一笔业务都会出现计算方面的错误,并且没有人去复核最终所产生的结果一样 。
// ScalingHelpers.sol
function _upscaleArray(uint256[] memory amounts, uint256[] memory scalingFactors) pure {
uint256 length = amounts.length;
InputHelpers.ensureInputLengthMatch(length, scalingFactors.length);
for (uint256 i = 0; i < length; ++i) {
amounts[i] = FixedPoint.mulDown(amounts[i], scalingFactors[i]); // 向下舍入
}
}
// FixedPoint.mulDown
function mulDown(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 product = a * b;
_require(a == 0 || product / a == b, Errors.MUL_OVERFLOW);
return product / ONE; // 向下舍入:直接截断
}
安全防护建议
项目方理应再度评估数值处理办法,思索采用更高精准度的运算方式。针对诸如不变值D这般的关键参数,要设定实时监控以及波动阈值,一旦察觉到异常便马上暂停交易。与此同时,要对批处理交易增添中间校验环节。
攻击者: BPT → cbETH 目标: 使 cbETH 余额调整到舍入边界(如末位是 9) 假设初始状态: cbETH 余额(原始): ...000000000009 wei (末位是 9)
建议引进第三方审计组织,针对数学模型的边界条件展开专门测试。在2023年Curve攻击事件发生之后,多家安全公司均提出了针对精度问题的检测方案。要定期开展极端情况压力测试,模拟在极小余额条件之下的系统表现。
行业影响展望
攻击者: wstETH (8 wei) → cbETH 缩放前: cbETH 余额: ...000000000009 wei wstETH 输入: 8 wei 执行 _upscaleArray: // cbETH 缩放: 9 * 1e18 / 1e18 = 9 // 但如果实际值是 9.5,由于向下舍入变成 9 scaled_cbETH = floor(9.5) = 9 精度损失: 0.5 / 9.5 = 5.3% 的相对误差 计算交换: 输入 (wstETH): 8 wei (缩放后) 余额 (cbETH): 9 (错误,应该是 9.5) 由于 cbETH 被低估,计算出的新余额也会被低估 导致 D 计算错误: D_original = f(9.5, ...) D_new = f(9, ...) < D_original
此次事件再度为DeFi安全拉响了警钟,它不但将技术层面之中所存的隐患给暴露了出来,而且更是把整个行业于代码审计以及风险控制这两方面之处表露出的欠缺反映表明了,伴随监管机构对加密货币范畴关注度出现提升,这般的事件极有望会促使相关立法进程得以加速推进其发展进程呢。
攻击者: 底层资产 → BPT 此时: D_new = D_original - ΔD BPT 价格 = D_new / totalSupply < D_original / totalSupply 攻击者用较少的底层资产换得相同数量的 BPT 或用相同的底层资产换得更多的 BPT
顺着积极的视角去看,那件事故推动了更多的开发团队着手重新审视他们自己的代码库。比如说 CertiK 和 Trail of Bits 这类安全审计公司的业务量在事故发生之后显著地增长了。行业正在塑造出更为严格的安全标准以及更为完备的保险机制,这对 DeFi 的长久健康发展是有帮助的。
DeFi交易里,您可曾碰到过类同的安全问题呀?欢迎针对此点,于评论区去分享一回您的过往经历呢。要是觉着本文是有一定助益的,那就请点赞予以支持,并且分享给更多的朋友们哟。