本文深入探讨了流动性再质押协议中的常见漏洞,包括重入攻击、拒绝服务、不正确的奖励分配以及经济攻击。文章通过Sigma Prime的审计案例,详细分析了EigenLayer集成及其他质押协议中存在的具体问题,旨在为DeFi开发者和安全研究人员提供有价值的安全指导,以提升系统的安全性,保护用户资产。
流动性再质押协议在 DeFi 领域迅速获得了关注,改变了用户管理和最大化其质押策略的方式。通过使资产能够在多个平台上重复使用,这些协议提供了更高的灵活性和资本效率。然而,伴随这种创新而来的是一些可被利用的风险。
开发者和安全研究人员必须对这些漏洞保持警惕,以加强其系统的安全性。在本文中,我们将探讨流动性再质押协议中可能出现的普遍问题。我们将研究 EigenLayer 集成以及其他形式的质押协议的最新挑战,以强调为什么谨慎的实现对于避免诸如重入攻击、拒绝服务(DoS)漏洞和不正确的奖励分配等问题如此重要。
我们将研究典型的安全缺陷和技术挑战,并以 Sigma Prime 审查中的例子作为支撑。我们的目标是提供一个简洁而全面的流动性(再)质押协议中常见漏洞的概述,为 DeFi 领域的开发人员和安全研究人员提供有价值的资源。
流动性质押、再质押和 EigenLayer 协议是在权益证明(PoS)区块链网络中使用的先进机制,旨在增强质押资产的流动性、安全性和可用性。这三种协议在目标上是相似的,但在一些关键方面有所不同。
流动性质押允许用户在保持流动性的同时质押其代币。用户无需将代币锁定在质押合约中,而是会收到代表其质押资产的衍生代币。这些衍生代币可以进行交易、用作抵押品或部署在其他 DeFi 协议中。这增加了你质押价值的灵活性。
再质押是指跨多个协议或服务质押资产的做法。此过程涉及利用同一组质押代币来保护区块链生态系统中的其他层或服务。再质押增强了质押代币的经济效用,而无需质押者投入额外的资本。
EigenLayer 协议旨在提高各种区块链服务的安全性,例如Layer2解决方案、桥和预言机,这些服务统称为积极验证服务(AVS)。借助 EigenLayer,用户可以存入原生 ETH 或流动性质押代币(LST),然后将其质押委托给受信任的运算符。这些由 AVS 批准的运营商使用委托的质押来验证不同的服务。获得的奖励与委托其质押的用户分享,但如果运营商行为不诚实,则质押金额可能会被削减。
了解这些协议如何工作以及它们如何相互交互是发现和修复潜在问题的关键。在下一节中,我们将研究这些协议中的常见漏洞,使用 Sigma Prime 最近的审查中的真实示例来展示其影响。在本文的其余部分中,我们将统称所有这些原语为流动性质押协议。
EigenLayer 再质押协议是相当新的,涉及几个重要的组件和工作流程,以使系统正常工作。以下是一个分解,以帮助理解它们如何运作以及可能出现问题的潜在位置。
StrategyManager
管理的策略中,然后将其添加到 DelegationManager
中的当前代理人。EigenPod
并在 Beacon Chain 上激活验证器。验证器激活通过 verifyWithdrawalCredentials()
验证,并且份额被分配给 DelegationManager
中的当前代理人。份额默认不被委托,可以通过两种方式委托:
提款可以从 EigenLayer 或 Beacon Chain 发起:
podOwner
在 EigenLayer 上对其进行验证。提款金额包括全部余额。EigenLayer 的惩罚机制尚未启用。集成商应做好准备,确保他们的代码库可以处理未来的惩罚事件。但是,Beacon Chain 确实启用了启动时就启用的惩罚机制。
我们将简要介绍流动性质押协议中发现的一些常见问题。这些例子来自我们的审查,以突出我们通常寻找和自问的内容。
nonReentrant
修饰符来阻止对同一函数的多次调用,并遵循 Checks-Effects-Interactions (CEI) 模式,以确保状态更改发生在交互之前。msg.value
不在循环中重放,因为它可能导致多次不期望的付款。create2()
,它的一些复杂性以及合约元数据所包含的内容。create2()
创建的不同合约地址。如果合约地址略有不同,这可能导致用户无法提取资金。inflationPercentage
是通过执行乘法和除法的两个单独案例来计算的。这种疏忽会导致双重舍入损失(每个除法运算一次),从而减少铸币金额。// 计算存款后价值的百分比
uint256 inflationPercentaage = (SCALE_FACTOR * _newValueAdded) / (_currentValueInProtocol + _newValueAdded);
// 计算新的供应量
uint256 newEzETHSupply = (_existingEzETHSupply * SCALE_FACTOR) / (SCALE_FACTOR - inflationPercentaage);
// 从新的供应量中减去旧的供应量以获得要铸造的金额
uint256 mintAmount = newEzETHSupply - _existingEzETHSupply;
为了提供对上述漏洞更实际的理解,我们提供了自己对流动性质押协议的审查中的一些示例。这些示例重点介绍了现实世界的错误及其对这些系统的安全性和功能的影响。
stakedButUnverifiedNativeETH
的错误会计处理在我们最近对 Kelp 的 LRT 智能合约更新 (KLP2-01) 的安全审查中,我们发现了一个问题,即协议如何核算尚未验证的已质押 ETH。像这样的会计问题太常见了,但在像质押这样高风险的环境中尤其重要。
当质押 32 ETH 时,名为 stakedButUnverifiedNativeETH
的变量会增加 32 ETH。验证股权后,应减少此金额,以反映 ETH 现在已验证并在 EigenLayer 中核算。但是,在这种情况下,问题出现在验证过程中。协议不是减去 32 ETH 总额,而是减去验证器的“有效余额”。
验证器的有效余额可能低于 32 ETH。如果发生这种情况,变量 stakedButUnverifiedNativeETH
最终会留下一些实际上不属于协议的 ETH。这种差异意味着协议的总资金被高估,并且其代币价格可能计算不正确。
在以下代码段中,我们可以看到 32 ETH 在调用 stake()
后递增。但是,我们可以看到在第二个代码段中,它减去的是有效余额,而不是质押的 32 ETH。
IEigenPodManager eigenPodManager = IEigenPodManager(lrtConfig.getContract(LRTConstants.EIGEN_POD_MANAGER));
eigenPodManager.stake{ value: 32 ether }(pubkey, signature, depositDataRoot);
// 跟踪已质押但未验证的原生 ETH
stakedButUnverifiedNativeETH += 32 ether;
eigenPod.verifyWithdrawalCredentials(
oracleTimestamp, stateRootProof, validatorIndices, withdrawalCredentialProofs, validatorFields
);
uint256 totalVerifiedEthGwei = 0;
for (uint256 i = 0; i < validatorFields.length;) {
uint64 validatorCurrentBalanceGwei = BeaconChainProofs.getEffectiveBalanceGwei(validatorFields[i]);
totalVerifiedEthGwei += validatorCurrentBalanceGwei;
unchecked {
++i;
}
}
// 减少已验证的 eth 金额
stakedButUnverifiedNativeETH -= (totalVerifiedEthGwei * LRTConstants.ONE_E_9);
我们在 Omni 代币和 AVS 审查 (OMNI-01) 中发现了一个问题,涉及其 _getSelfDelegations()
函数,该函数容易受到拒绝服务 (DoS) 状况的影响。当运营商在未包含在 _strategyParams
数组中的策略中进行质押时,就会出现此漏洞。因此,如果任何运营商在不兼容的策略中进行了质押,则 syncWithOmni()
函数将失败。
该错误在以下代码段中进行了说明:
for (uint256 i = 0; i < strategies.length;) {
IStrategy strat = strategies[i];
// 查找策略的策略参数
StrategyParam memory params;
for (uint256 j = 0; j < _strategyParams.length;) {
if (address(_strategyParams[j].strategy) == address(strat)) {
params = _strategyParams[j];
break;
}
unchecked {
j++;
}
}
// 如果未找到策略,请勿在质押中考虑它
if (address(params.strategy) == address(0)) continue;
staked += _weight(shares[i], params.multiplier);
unchecked {
i++;
}
}
如果当前策略在 _strategyParams
中未找到,则循环将继续,而不会递增 i
迭代器。因此,将重复检查相同的不兼容策略,从而导致无限循环,该循环仅在交易耗尽 gas 时才结束,最终导致拒绝服务。
在我们对 EigenLayer (EGN3-01) 的审查中,我们发现 BeaconChainProof.verifyWithdrawal()
函数中存在缺陷。我们发现它不支持在 Ethereum Deneb 升级 之后发生的提款。这导致提款卡在 EigenLayer 的 EigenPod
合约中或被恶意伪造。
BeaconChainProofs
库假定树的高度为 4。但是,Deneb 升级向 ExecutionPayload
容器添加了两个新字段,以容纳 blob,作为 EIP-4844 的一部分。这使字段数从 15 个增加到 17 个,因此也使树的高度从 4 个增加到 5 个。
这导致了两个结果:
EigenPod
合约提取资金。此示例将介绍本文 RocketPool 和 Lido 抢先交易 Bugfix 审查 中的发现。
根据 Beacon Chain 的设计,存款合约仅需要 1 ETH 作为存款实例化的最小值,并且这第一个存款是设置提款凭据的存款。其目的是允许存款人进行部分付款,而不是 32 ETH 的一次性付款。但是,这为池化质押打开了一个抢先交易攻击向量。
为了利用此问题,节点运营商使用相同的公钥但不同的提款凭据生成两个存款数据实例。一个实例是真正的 32 ETH 存款,将由质押池处理并发送到存款合约。另一个实例是恶意实现,仅存入 1 ETH,并且提款凭据已替换为攻击者控制中的地址。
然后,攻击者等待质押池准备使用真正的存款数据将其提交到存款合约的 32 ETH。但是,在存入真正的 32 ETH 之前,攻击者直接使用他们自己的存款数据抢先交易至存款合约。
以下是 Beacon Chain 逻辑的样子:
def process_deposit(state: BeaconState, deposit: Deposit) -> None:
# Verify the Merkle branch
assert is_valid_merkle_branch(
leaf=hash_tree_root(deposit.data),
branch=deposit.proof,
depth=DEPOSIT_CONTRACT_TREE_DEPTH + 1, # Add 1 for the List length mix-in
index=state.eth1_deposit_index,
root=state.eth1_data.deposit_root,
)
# Deposits must be processed in order
state.eth1_deposit_index += 1
pubkey = deposit.data.pubkey
amount = deposit.data.amount
validator_pubkeys = [v.pubkey for v in state.validators]
if pubkey not in validator_pubkeys:
# Verify the deposit signature (proof of possession) which is not checked by the deposit contract_
deposit_message = DepositMessage(
pubkey=deposit.data.pubkey,
withdrawal_credentials=deposit.data.withdrawal_credentials,
amount=deposit.data.amount,
)
domain = compute_domain(DOMAIN_DEPOSIT) # Fork-agnostic domain since deposits are valid across forks
signing_root = compute_signing_root(deposit_message, domain)
if not bls.Verify(pubkey, signing_root, deposit.data.signature):
return
# Add validator and balance entries
state.validators.append(get_validator_from_deposit(state, deposit))
state.balances.append(amount)
else:
# Increase balance by deposit amount
index = ValidatorIndex(validator_pubkeys.index(pubkey))
increase_balance(state, index, amount)
现在,由于 Beacon Chain 仅在第一次有效提交时注册提款凭据,因此它是攻击者为此公钥指定的提款凭据。来自存款池的第二个 32 ETH 存款然后触发最终的 else
语句(在上面的代码段中),以简单地将验证器的余额增加 32 ETH,而忽略了真正的提款凭据。
然后,攻击者只需等待他们的验证器被激活,然后退出,他们将收到 33 ETH 到他们的提款地址。
本文介绍了流动性质押协议的一些细节,包括流动性质押、再质押和 EigenLayer 集成。我们回顾了组件和工作流程、常见漏洞(如汇率操纵、不正确的访问控制、拒绝服务攻击等)。Sigma Prime 审查中的真实示例展示了其中一些漏洞的实际作用。
理解和缓解这些漏洞对于开发人员和安全审查人员来说非常重要。它可以提高协议的安全性并保护用户的资产,并保持整个生态系统的安全和可信。我们鼓励所有开发人员和安全审查人员保持知情,并积极主动地发现和缓解这些类型的威胁。
在 Sigma Prime,我们致力于保护和加强区块链网络和协议。如果你正在构建流动性再质押解决方案,并希望利用我们在该领域的前沿安全专业知识,请 联系我们。
以下是由 Sigma Prime 执行的一些质押审查的列表。此列表包含已公开的报告,也可以在我们的 公共报告存储库 中找到。
以下是来自其他安全研究人员和开发人员的一些其他文章:
- 原文链接: blog.sigmaprime.io/liqui...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!