[elip-002]: Slashing 版本中舍入和溢出的安全性影响

  • Layr-Labs
  • 发布于 2025-04-03 22:40
  • 阅读 30

本文档深入探讨了在 slashing release 中执行的各种数学运算,重点关注舍入和溢出情况的安全性问题。

Shares Accounting Edge Cases(份额计算的极端情况)

本文档旨在探讨和分析我们在罚没版本中执行的不同数学运算。 主要我们想确保四舍五入和溢出情况下的安全性。 需要先阅读 Shares Accounting 才能理解本文档。

Prior Reading(前置阅读)

Fully Slashed for a Strategy(策略的完全罚没)

在单个策略的上下文中,回想一下,存款比例因子 $k_n$ 的更新定义如下:

$$ k_{n+1} = \frac{s_n k_n m_n + dn}{s{n+1} l{n+1} m{n+1}}=\frac{s_n k_n l_n m_n + d_n}{(s_n+d_n)l_nm_n} $$

我们可以看到,如果 $(s_n + d_n)$、$l_n$ 或 $mn$ 中的任何一个等于 0,则计算 $k{n+1}$ 可能会导致除以 0 的错误。$(s_n + d_n) = 0$ 的情况不应出现,因为 EigenPodManagerStrategyManager 在这种情况下不会报告份额增加。 但是,其他两个术语可能达到 0:

  • 当一个 operator 对于给定的策略被 100% 罚没,并且他们的最大幅度 $m_n = 0$
  • 当一个 staker 的 EigenPod 原生 ETH 余额为 0 并且 他们的验证者都已被罚没,使得 $l_n = 0$

在这些情况下,对 staker 的存款比例因子的更新将遇到除以 0 的错误。 在任何一种情况下,我们都知道,由于 operator 被完全罚没,或者 staker 对于 beaconChainETHStrategy 被完全罚没,那么他们的可提取份额 $a_n = 0$。

在实践中,如果对于给定的 operator,$m_n = 0$,那么:

  1. 已经委托给该 operator 的任何 staker 将无法将额外的资产存入相应的策略
  2. 当前在该策略中持有存款份额并且未委托给该 operator 的任何 staker 将无法委托给该 operator

请注意,在第一种情况下,staker 可以 取消委托、排队并完成提款 - 但由于 $a_n = 0$,他们不会因此收到任何可提取的份额。

此外,如果在信标链 ETH 策略中,给定 staker 的 $l_n = 0$,那么 任何进一步的 ETH 存款或验证者的重新 stake 都不会产生 EigenLayer 中的份额。 这只应在特殊情况下发生,因为 0 的信标链罚没因子意味着 staker 在他们的 EigenPod 中只有 ~0 的资产,并且他们所有的验证者都已经在信标链上被罚没 ~100% - 这种情况只有在协调的验证者组被罚没时才会发生。 如果发生这种情况,EigenPod 在本质上就被破坏了 - pod 所有者不应向 pod 发送 ETH,也不应将额外的验证者指向该 pod。

如果一个 operator 在 EigenLayer 中有他们自己的原生 ETH 份额,并且被 AVS 完全罚没($m_n = 0$),则 operator 新的 ETH 存款将无法恢复。 Staker 可以从被完全罚没的 operator 那里取消委托以恢复新的存款,但 operator 不能,因为它无法从自己那里取消委托。

这些都是预期的极端情况,它们的发生和副作用都在可接受的容错范围内。

Upper Bound on Deposit Scaling Factor $k_n$(存款比例因子 $k_n$ 的上限)

让我们检查一下在计算 staker 的可提取份额时可能出现的溢出情况。 以下是 SlashingLib.sol 中计算 $a_n = s_nk_nl_nm_n$ 的函数。\ 注意:slashingFactor = $l_nm_n$

function calcWithdrawable(
    DepositScalingFactor memory dsf,
    uint256 depositShares,
    uint256 slashingFactor
) internal pure returns (uint256) {
    /// forgefmt: disable-next-item
    return depositShares
        .mulWad(dsf.scalingFactor())
        .mulWad(slashingFactor);
}

depositShares 是存储中 staker 的份额 $s_n$。 我们知道这最多可以是 1e38 - 1,因为这是我们在一个策略中允许的最大总份额。$l_n ≤ 1e18$ 并且 $m_n ≤ 1e18$,因为它们是单调递减的值。 因此,slashingFactor 操作的 mulWad 永远不会导致溢出,它总是会导致一个更小或相等的数字。

现在的问题是 depositShares.mulWad(dsf.scalingFactor()) 以及这个术语是否会溢出 uint256。 让我们检查一下这背后的数学原理。 函数 SlashingLib.update 执行以下计算:

$$ k_{n+1} =\frac{s_n k_n l_n m_n + d_n}{(s_n+d_n)l_nm_n} $$

假设:

  • $k_0 = 1$
  • 0 < $l_0$ ≤ 1 并且是单调递减的,但不会达到 0
  • 0 < $m_0$ ≤ 1 并且是单调递减的,但不会达到 0
  • 0 ≤ $sn, {s{n+1}}$ ≤ 1e38 - 1 (MAX_TOTAL_SHARES = 1e38 - 1 in StrategyBase.sol)
  • 0 < $d_n$ ≤ 1e38 - 1
  • ${s_{n+1}}={s_n} + {d_n}$

重写上述内容,我们可以通过分解 k 并取消一些项来得到以下结果。

$$ k_{n+1} = k_n\frac{s_n}{s_n + d_n} + \frac{d_n}{(s_n+d_n)l_nm_n} $$

第一项 $\frac{s_n}{{{s_n} + {d_n}}}$ < 1,因此如果仅考虑这一项,则与 $kn$ 相乘不会导致 ${k{n+1}}$ 的增长。

然而,第二项 $\frac{d_n}{({{s_n} + {d_n}}){l_n}{m_n}}$ 可以使 $k_n$ 随着时间的推移而增长,具体取决于 ${l_n}{m_n}$ 变得多么小,以及 $d_n$ 相对于 $s_n$ 的比例有多大。 我们只关心这里最坏的情况,因此让我们通过将该值四舍五入到 1 来假设现有份额和新存款金额的上限。

现在,实际上,${l_n}$ 和 ${m_n}$ 的最小值可能等于 1/1e18。 将其代入上述第二项得出以下结果:

$$ \frac{d_n}{(s_n+d_n)l_nm_n} = \frac{d_n}{s_n+d_n}*1e18^2 $$

因此,让我们将第一项 $\frac{s_n}{{{s_n} + {d_n}}}$ 四舍五入为 1,也将第二项中的 $\frac{d_n}{{{s_n} + {d_n}}}$ 四舍五入为 1。 在这种最坏的情况下,我们可以将 k 的递归定义简化为以下形式。

$$ k_{n+1} = k_n\frac{s_n}{s_n + d_n} + \frac{d_n}{(s_n+d_n)l_nm_n} $$

$$ => k_{n+1} = k_n+ \frac{d_n}{(s_n+d_n)l_nm_n} $$

$$ => k_{n+1} = k_n + 1e36 $$

由于策略中份额的最大存储量为 1e38 - 1,并且存款必须为非零,因此我们实际上可以通过进行 1e38-1 次金额为 1 的存款,每次更新 ${k_n}$ 来得出 ${k_n}$ 的上限。

$$ k_{1e38-1} \approx (1e38-1)\cdot 1e36 < 1e74 $$

经过 1e38-1 次迭代/存款后,我们计算出的 k 的上限在最坏情况下为 1e74。 如果作为 staker,你被委托给 beaconChainStrategy 的 operator,其中你的 operator 因原生 ETH 而被罚没 99.9999999…%,并且作为 staker,你的 EigenPod 余额也已按比例减少到 99.9999999…..%。 这在技术上是可能的。

1e38-1 的最大份额也容纳了 ETH 的全部供应量(只需要 27 位)。 对于正常的 StrategyManager 策略,${l_n} = 1$,并且 ${k_n}$ 不会增长到相同的程度。

显然,${k_n}$ 的 1e74 值适合 uint256 存储槽。

将所有这些带回到用于计算 staker 实际可提取份额的 calcWithdrawable 方法以及实际的下一个 ${k_{n+1}}$ 值。 我们可以看到,考虑到我们所有变量的约束以及 depositScalingFactor 的使用是安全的,因此预计份额不会溢出。

Staker 的 depositScalingFactor 在如何随着时间的推移而增加方面没有限制,但由于我们对 ${l_n}$ 和 ${m_n}$ 有下限,以及对策略拥有的份额数量(或关于 beaconChainStrategy 的 ETH 存在量)有上限,我们可以看到我们的合约中 depositScalingFactor $k_n$ 溢出是不可行的。

Rounding Behavior Considerations(舍入行为注意事项)

SlashingLib.sol 由于在合约中使用 mulWad/divWad 操作而引入了一些小的舍入精度误差,我们在合约中执行 x * y / denominator 操作。 在 Solidity 中,我们向下舍入到最接近的整数,从而引入了高达 1 wei 的绝对误差。 考虑到这一点,在代码的某些部分,我们将明确使用 x * y / denominator 的下限值或上限值。

这对系统的几个部分产生影响。 例如,完成作为份额的提款,并且由于舍入导致你更新后的可提取份额少于原来的份额。 对于具有非 WAD 信标链罚没因子 (BCSF) 的 staker 来说,这本质上是由于在 BC 上受到惩罚/罚没而自我造成的。 对于特定策略具有非 WAD maxMagnitue 的 operator,这也是他们被分配到的 OperatorSet 罚没的结果。 Staker 在委托给策略中具有低 maxMagnitude 的 operator 时应谨慎,因为舍入误差的影响可能导致他们_应该_提取的金额与他们实际可以提取的金额之间存在更大的差异。

Rounding up on Slashing(罚没时向上舍入)

当 operator 在 AllocationManager 中被 operatorSet 罚没时,我们实际上希望在罚没时向上舍入。 我们想要 ceiling(x * y / denominator),而不是从 mulDiv 计算 floor(x * y / denominator)。 这是因为我们不希望出现任何 DOS 情况,其中 operatorSet 尝试罚没 operator 会四舍五入为 0; 如果 operator 为他们自己的虚假 AVS 注册并反复罚没自己以将其 maxMagnitude 降低到足够小的值,则可能会出现这种情况。 这将确保始终从 operator 的 maxMagnitude 中罚没一定数量,如果他们被罚没得足够多,最终可以达到 0。

AllocationManager.slashOperator

// 3. 计算要罚没的幅度量,并从
// operator 当前分配的幅度中减去,以及策略的
// 最大幅度和受限幅度
uint64 slashedMagnitude = uint64(uint256(allocation.currentMagnitude).mulWadRoundUp(params.wadsToSlash[i]));

Deposits actually reducing withdrawableShares(存款实际上会减少可提取的份额)

在某些非常特殊的极端情况下,由于舍入错误,存款实际上可能会减少 staker 的可提取份额,这在概念上是错误的。 单元测试 DelegationUnit.t.sol:test_increaseDelegatedShares_depositRepeatedly 举例说明了这一点,其中在多次存款过程中,staker 的可提取份额与 staker 委托的 operator 份额之间存在越来越大的差异。 本质上,此测试用例中发生的情况是,在首次存入大量份额后,后续存入 1000 的金额会导致 staker 的 getWithdrawable 份额实际减少。

由于 operatorShares 只是按精确的 depositShares 递增,因此 operatorShares 映射按预期增加。 这最终在执行 1000 次存款后,在两个值之间造成了非常大的差异/漂移。 operatorShares 和 staker 的可提取份额之间的差异最终为 4.418e13

诚然,初始存款金额为 4.418e28,比这里的差异大几个数量级,但重要的是要注意重新设计的会计模型的副作用。 我们引入了幅度和比例因子变量,而不是纯粹递增/递减的金额,这现在导致了几个地方的除法运算中出现少量的舍入误差。 考虑到模拟此交易所需的交易数量以及比例误差非常小,我们认为这种舍入行为是可以容忍的。

Slashing and Rounding Up Operator Shares and Rounding down on Staker Withdrawable Shares(在罚没时向上舍入 operator 份额,并向下舍入 Staker 的可提取份额)

正如在 SlashingLib.sol 库中可以观察到的那样,我们在罚没时向上舍入 operatorShares,并在 staker 的可提取份额上向下舍入。 如果我们查看份额核算模型的核心不变性,我们理想情况下希望保留以下内容:

$$ opn = \sum{i=1}^{k} a_{n,i} $$

其中 $opn$ 是时间 $n$ 的 operatorShares,$a{n,i}$ 是 $i^{th}$ staker 在时间 $n$ 的可提取份额。

但是,由于舍入的限制,在计算要罚没的 operator 份额量以及计算 staker 的可提取份额时,会引入一些误差。 为了防止所有 staker 尝试提款并且 operatorShares 下溢的情况,我们在罚没时向上舍入 operatorShares,并在 staker 的可提取份额上向下舍入。

因此,在实践中,上述不变性变为。

$$ opn \geq \sum{i=1}^{k} a_{n,i} $$

在罚没后计算要给 operator 的 operatorShares 数量时,有意地在 SlashingLib.calcSlashedAmount 中执行向上舍入。 对于计算 staker 的可提取份额,有许多不同的因素需要考虑,例如计算他们的 depositScalingFactor、他们的 slashingFactor 以及完全使用他们的 depositShares 计算可提取份额的数量。 这些变量在计算中默认全部向下舍入,这是 staker 的预期行为。

Upper bound on Residual Operator Shares(剩余 Operator 份额的上限)

与上述存款舍入误差相关,我们想计算 staker 将份额存入 EigenLayer 的最坏情况舍入误差是多少。 也就是说,存入的 depositShares 和导致的可提取份额之间的最大差异是多少? 对于最初存款而没有被罚没的 staker,这两个值在概念上应该是相等的。 让我们在下面检查一下。

以下是 SlashingLib.sol 的代码片段

function update(
    DepositScalingFactor storage dsf,
    uint256 prevDepositShares,
    uint256 addedShares,
    uint256 slashingFactor
) internal {
    // 如果这是 staker 的第一次存款,请将比例因子设置为
    //slashingFactor 的倒数
    if (prevDepositShares == 0) {
        dsf._scalingFactor = uint256(WAD).divWad(slashingFactor);
        return;
    }

...

function calcWithdrawable(
    DepositScalingFactor memory dsf,
    uint256 depositShares,
    uint256 slashingFactor
) internal pure returns (uint256) {
    /// forgefmt: disable-next-item
    return depositShares
        .mulWad(dsf.scalingFactor())
        .mulWad(slashingFactor);
}

从数学上讲,可提取的份额可以表示为如下

$$ withdrawableShares = d\space\cdot\space \frac{k}{WAD} \space\cdot\space \frac{slashingFactor}{WAD} $$

如果 staker 只进行了一次 $d$ 金额的存款,则用 WAD.divWad(slashingFactor) 替换 $k$(请参阅上面的更新函数)。 同样展开 slashingFactor,即 maxMagnitude.mulWad(beaconChainScalingFactor)

$$ = d\space\cdot\space \frac{\frac{WAD\space\cdot \space WAD}{m{deposit} \cdot l{deposit}}}{WAD} \space\cdot\space \frac{\frac{m \space\cdot\space l}{WAD}}{WAD} $$

上面是 staker 拥有的可提取份额的真实值,但在实践中,每次除法运算都会产生舍入影响。 它变为以下

$$ withdrawableShares (rounded) = \lfloor \lfloor d \space\cdot\space \frac{\lfloor\frac{WAD\space\cdot \space WAD }{m{deposit} \space\cdot\space l{deposit}} \rfloor }{WAD} \rfloor \space\cdot\space \frac{\lfloor \frac{m \space\cdot\space l}{WAD}\rfloor}{WAD} \rfloor $$

每个 floor 操作最多会引入 1 wei 的舍入误差。 然而,由于存在嵌套除法,因此此误差可能导致大于仅相差 1 wei 的总误差。 我们可以用 epsilon $e$ 重写上述部分,它在 [0,1] 范围内。

  1. 第一个内部舍入项

$$ \frac{WAD \cdot WAD}{m{deposit} \cdot l{deposit}} = \lfloor \frac{WAD \cdot WAD}{m{deposit} \cdot l{deposit}} \rfloor + \epsilon_1 $$

$$ \frac{\lfloor \frac{WAD \cdot WAD}{m{deposit} \cdot l{deposit}} \rfloor}{WAD} = \frac{\frac{WAD \cdot WAD}{m{deposit} \cdot l{deposit}} - \epsilon_1}{WAD} $$

  1. 第二个舍入项

$$ \lfloor d \cdot \frac{\lfloor \frac{WAD \cdot WAD}{m{deposit} \cdot l{deposit}} \rfloor}{WAD} \rfloor $$

$$ = \lfloor d \cdot \frac{WAD \cdot WAD}{m{deposit} \cdot l{deposit} \cdot WAD} - d \cdot \frac{\epsilon_1}{WAD} \rfloor $$

$$ = d \cdot \frac{WAD \cdot WAD}{m{deposit} \cdot l{deposit} \cdot WAD} - d \cdot \frac{\epsilon_1}{WAD} - \epsilon_2 $$

  1. 第 3 个舍入项

$$ \lfloor \frac{m \cdot l}{WAD} \rfloor = \frac{m \cdot l}{WAD} - \epsilon_3 $$

$$ => \frac{\lfloor \frac{m \cdot l}{WAD} \rfloor}{WAD} = \frac{\frac{m \cdot l}{WAD} - \epsilon_3}{WAD} $$

$$ => \frac{\lfloor \frac{m \cdot l}{WAD} \rfloor}{WAD} = \frac{m \cdot l}{WAD^2} - \frac{\epsilon_3}{WAD} $$

  1. 现在将所有内容带回到原始方程

$$ withdrawableShares (rounded) = \lfloor \lfloor d \space\cdot\space \frac{\lfloor\frac{WAD\space\cdot \space WAD }{m{deposit} \space\cdot\space l{deposit}} \rfloor }{WAD} \rfloor \space\cdot\space \frac{\lfloor \frac{m \space\cdot\space l}{WAD}\rfloor}{WAD} \rfloor $$

$$ = \lfloor\left(d \cdot \frac{WAD \cdot WAD}{m{deposit} \cdot l{deposit} \cdot WAD} - d \cdot \frac{\epsilon_1}{WAD} - \epsilon_2\right)\cdot\left(\frac{m \cdot l}{WAD^2} - \frac{\epsilon_3}{WAD}\right)\rfloor $$

$$ = \left( d \cdot \frac{WAD \cdot WAD}{m{deposit} \cdot l{deposit} \cdot WAD} - d \cdot \frac{\epsilon_1}{WAD} - \epsilon_2 \right) \cdot \left( \frac{m \cdot l}{WAD^2} - \frac{\epsilon_3}{WAD} \right) - \epsilon_4 $$

展开并进行一些简化后

$$ withdrawableShares (rounded) = d \cdot \frac{m\cdot l}{m{deposit} \cdot l{deposit}\cdot WAD} - d \cdot \frac{\epsilon_1 \cdot m \cdot l}{WAD^3} - \frac{\epsilon_2 \cdot m \cdot l}{WAD^2} - d \cdot \frac{\epsilon3}{m{deposit} \cdot l_{deposit} } + \text{(higher-order terms)} $$

请注意,(高阶项)是具有多个 epsilon 项的项,其中金额变得可以忽略不计,因为每个项 $e$ < 1。

真值项如下:

$$ withdrawableShares = d\space\cdot\space \frac{\frac{WAD \space\cdot\space WAD}{m{deposit} \cdot l{deposit}}}{WAD} \space\cdot\space \frac{\frac{m \space\cdot\space l}{WAD}}{WAD} $$

$$ = d\space\cdot\space \frac{WAD }{m{deposit} \cdot l{deposit}}\space\cdot\space \frac{m \space\cdot\space l}{WAD^2} $$

$$ d \cdot \frac{m\cdot l}{m{deposit } \cdot l{deposit}\cdot WAD} $$

但是我们可以看到这个项显示在上面的 withdrawableShares(rounded) 中的第一项中! 然后我们可以看到我们可以将方程表示为如下。

$$ withdrawableShares (rounded) = withdrawableShares - d \cdot \frac{\epsilon_1 \cdot m \cdot l}{WAD^3} - \frac{\epsilon_2 \cdot m \cdot l}{WAD^2} - d \cdot \frac{\epsilon3 }{m{deposit} \cdot l_{deposit} } + \text{(higher-order terms)} $$

这在直觉上是有意义的,因为所有的舍入误差都来自 epsilon 项以及它们如何从被嵌套中传播出来。 因此,来自舍入而产生的误差是所有舍入项相加,忽略了高阶项。

$$ roundedError =d \cdot \frac{\epsilon_1 \cdot m \cdot l}{WAD^3} + \frac{\epsilon_2 \cdot m \cdot l}{WAD^2} + d \cdot \frac{\epsilon3 }{m{\text{deposit}} \cdot l_{deposit} } $$

现在让我们假设最大化上述总和的最坏情况,如果每个 epsilon $e$ 都被替换为 1 的值,因为四舍五入了一个完整的 wei,我们可以得到以下结果。

$$ d \cdot \frac{m \cdot l}{WAD^3} + \frac{ m \cdot l}{WAD^2} + \frac{ d}{m{\text{deposit}} \cdot l{deposit}} $$

假设接近导致舍入行为的最大值,我们可以通过使 $d = 1e38$,$m, m{deposit}, l, l{deposit}$ 等于 WAD(1e18) 来最大化这个总和,然后我们得到以下结果:

$$ \frac{1e38\cdot WAD^2}{WAD^3} + \frac{ WAD^2}{WAD^2} + \frac{1e38}{1e36} $$

$$ => \frac{1e38}{1e18} + 1 + 100 $$

$$ \approx 1e20 $$

换一种方式来说,staker 可能损失的金额是存款金额的 $\frac{1}{1e18}$。 这是由于具有嵌套的 floor 操作,然后将其与外部项相乘而产生的。 随着时间的推移,随着 staker 存款和提款,他们可能无法收到与其“真实”可提取金额一样多的份额,因为这是向下舍入的,并且委托的 operatorShares 映射和原始 Strategy 合同中可能存在剩余/灰尘份额。 这是已知的,我们专门向下舍入以避免如果所有委托的 staker 都提款,则 operatorShares 会下溢。

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

0 条评论

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