本文解释了Solodit Checklist中的关于“Rug Pull”的风险,即管理员权限过大导致恶意提取用户资金的安全问题。文章通过Zunami Protocol事件,强调了限制管理员权限的重要性,并提供了诸如费用分离、时间锁等安全设计模式,以降低合约风险,保障用户资产安全。
Hans
了解未经检查的管理员权限如何在 DeFi 中启用 rug pull。探索安全的设计模式,如费用分离、时间锁和受限的提款函数。
本文探讨了智能合约安全的一个关键方面:拥有允许耗尽用户资金的管理员权限的风险,通常称为“rug pull”。
这是“Solodit 清单解释”系列的一部分。你可以在这里找到之前的文章:
为了获得最佳体验,请打开一个包含 Solodit 清单 的标签页,以便在阅读时参考。
我的 GitHub 上提供了示例 这里。
从字面上看,“rug pull”是指突然从某人身下拉出地毯,导致他们意外摔倒的行为。
在去中心化金融(DeFi)中,rug pull 是指 诈骗,其中开发者推广一种新的代币或平台,只是为了突然耗尽流动性池(LP)或大量出售其持有的代币,导致代币价值暴跌。
这是一种“中心化风险”。 中心化风险包含各种各样的漏洞,例如单个实体控制升级、暂停 函数 或关键参数。本文将专门关注资金耗尽的 rug pull,即管理员利用其权限 窃取用户存入的资金或协议中持有的资金。
其他类型的中心化风险在 Solodit 清单的单独类别中得到了全面解决,涵盖了诸如暂停开关控制和参数修改功能等问题。在这里,我们只关注通过管理员控制的提款函数直接盗取价值。
对管理访问的有效管理超越了编写安全代码。它涉及 将信任设计到协议中。
描述:某些协议授予管理员直接提取资产的权限。通常,任何可以直接影响用户资金的行为者都必须受到审查。
补救措施:限制仅访问协议资金的相关部分,或许可以通过内部跟踪费用来实现。对管理员操作强制执行时间锁也可以减轻风险。
此清单项目解决了管理员提取资产的能力这一关键风险,这可能会直接危及用户资金。
管理角色对于维护和升级协议至关重要,但 授予对资产的无限制访问权限会产生重大漏洞。如果攻击者获得对管理员密钥的控制权,他们可以耗尽资金,从而通过耗尽协议的储备有效地执行“rug pull”。即使没有恶意,由于安全漏洞而泄露的密钥也可能导致严重的后果,从而损害协议的声誉和用户信任。
为了减轻这种风险,智能合约的设计应 最大限度地减少受损或恶意管理员帐户的影响。这包括限制管理权限,对所有管理操作强制执行严格的检查,以及实施安全措施以防止单方面或立即的资产转移。管理职能应该像精确的外科手术一样对待,这是必要的,但要仔细限制在安全的、预定义的范围内。
最近的一个关于此漏洞的例子是 2025 年 5 月的 Zunami 协议事件,该事件导致 500,000 美元的 zunUSD 和 zunETH 抵押品损失。根据 rekt.news,这不是一个涉及闪电贷或价格操纵的复杂漏洞。相反,这只是对权力过大的管理函数的简单滥用。一个拥有“上帝模式访问权限”的人调用了 withdrawStuckToken()
函数,清空了保险库的内容。
此事件凸显了 一些最具破坏性的“黑客”是由于授权管理员(无论是恶意的还是受损的)滥用合法的合约函数造成的。 Zunami 案例提醒我们,如果不加以适当保护,不安全的紧急函数可能会成为攻击者的工具。
这是一个示例,显示了恶意或受损的管理员如何耗尽保险库、执行 rug pull:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VulnerableVault {
address public admin;
// 将用户地址映射到其存入的余额
mapping(address => uint256) public userBalances;
constructor() {
// 合约的部署者成为管理员
admin = msg.sender;
}
/// @notice 允许用户将以太币存入保险库。
/// 将对存款收取 1% 的费用,并发送到费用池。
function deposit() public payable {
require(msg.value > 0, "Deposit must be greater than zero");
// 计算 1% 的费用。理论上,这笔费用属于协议。
uint256 fee = msg.value / 100; // 存款金额的 1%
uint256 amountToDeposit = msg.value - fee;
// 用户的余额会以其净存款进行更新。
userBalances[msg.sender] += amountToDeposit;
// msg.value 的 'fee' 部分保留在合约的总余额中。
// 如果只有费用应该可以被管理员提取,那么在单独的变量中显式跟踪它是一种设计缺陷。
// if only fees should be withdrawable by admin.
}
/// @notice 允许用户提取其存入的资金。
/// @param amount 要提取的金额。
function withdraw(uint256 amount) public {
require(userBalances[msg.sender] >= amount, "Insufficient balance");
// 减少用户的余额并转移资金。
userBalances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
/// @notice 管理员可以从合约的余额中提取 *任何* 金额。
/// 这是 rug pull 漏洞! 这模拟了一个类似 `withdrawStuckToken()`-like 的函数。
/// @param amount 要从合约中提取的金额。
function adminRugPullWithdraw(uint256 amount) public {
require(msg.sender == admin, "Admin control required"); // 只有管理员可以调用。
// 没有检查以确保 'amount' 仅来自应计费用。
// 管理员可以提取任何金额,最高可达合约的总以太币余额(address(this).balance),
// 有效地窃取用户存款,因为用户存款也有助于 address(this).balance。
// 此函数将合约中的所有资金视为可供管理员使用的资金。
require(address(this).balance >= amount, "Insufficient contract balance for withdrawal");
payable(admin).transfer(amount); // 将选择的金额转移给管理员。
}
}
在此 VulnerableVault
合约中,adminRugPullWithdraw
函数 允许管理员从合约的总余额中提取 任何 金额的以太币。这是一个关键漏洞。如果管理员密钥被盗,或者控制密钥的个人决定采取恶意行为(正如在 Zunami 案例中质疑的关于“内部工作”),他们可以使用此函数耗尽保险库中存入的 所有 以太币,包括用户资金。
require
语句仅验证调用者是否为 admin
,完全无法根据应合法提取的内容(例如,仅协议费用,而不是用户本金)验证 amount
。
为了减轻此漏洞并防止 rug pull,请考虑以下可靠的策略:
限制管理员对特定资金的访问:限制任何管理员提款函数 仅 对协议拥有的资金(例如,收取的费用、国库资金)进行操作,切勿 对用户存款进行操作。用户存款应仅由用户自己提取。
实施时间锁:对于任何敏感的管理操作,特别是涉及资金转移或关键参数变更的操作,都需要时间锁。这种延迟为用户和监控系统提供了一个关键的窗口来检测潜在的恶意交易并做出反应(例如,通过提取他们的资金或发出警报),然后在操作完成之前。这可能是 Zunami 案例中的一个关键缓解因素,为防御即时耗尽提供了保障。
这是一个修改后的合约版本,通过引入内部费用跟踪并将管理员提款限制为仅限这些应计费用来减轻该漏洞:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SecuredVault {
address public admin;
// 将用户地址映射到其存入的余额
mapping(address => uint256) public userBalances;
// 此变量 *仅* 考虑管理员可以提取的费用。
// 它与用户余额显式分离。
uint256 public totalAccruedFees;
constructor() {
// 合约的部署者成为管理员
admin = msg.sender;
}
/// @notice 允许用户将以太币存入保险库。
/// 将对存款收取 1% 的费用,并发送到费用池。
function deposit() public payable {
require(msg.value > 0, "Deposit must be greater than zero");
// 计算 1% 的费用
uint256 fee = msg.value / 100; // 存款金额的 1%
uint256 amountToDeposit = msg.value - fee;
userBalances[msg.sender] += amountToDeposit; // 用户的本金
totalAccruedFees += fee; // 为协议累积费用
}
/// @notice 允许用户提取其存入的资金。
/// @param amount 要提取的金额。
function withdraw(uint256 amount) public {
require(userBalances[msg.sender] >= amount, "Insufficient balance");
userBalances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
/// @notice 管理员只能提取总应计费用。
/// 这可以防止 rug pull,因为管理员无法触及用户本金。
/// @param amount 管理员希望提取的费用金额。
function adminWithdrawFees(uint256 amount) public {
require(msg.sender == admin, "Admin control required");
// 关键检查:确保管理员仅提取费用中可用的金额,
// 因为它与用户本金分开跟踪。
require(totalAccruedFees >= amount, "Insufficient accrued fees for withdrawal");
totalAccruedFees -= amount; // 从可用费用中扣除提取的金额
payable(admin).transfer(amount); // 仅将请求的费用金额转移给管理员
}
}
在此改进的 SecuredVault
版本中,adminWithdrawFees
函数不再直接与用户余额交互。用户资金受到保护,因为它们与 totalAccruedFees
变量 分开持有。这减轻了管理员对用户本金的特权风险,解决了 Zunami 攻击中看到的核心问题。
管理管理权限对于保障安全和维持用户信任至关重要。不受限制地访问资产可能会导致毁灭性的损失,正如 Zunami 协议等事件所证明的那样,在这些事件中,未经检查的管理权限导致用户资金完全耗尽。遵守此清单项目对于防止此类漏洞至关重要。
强大的安全性需要积极的措施,包括严格控制的管理职能和精心设计的安全措施,以减轻风险。
在下一篇文章中,我们将研究智能合约安全性的另一方面:夹层攻击。
请继续关注有关开发安全且有弹性的去中心化应用的更多见解!
- 原文链接: cyfrin.io/blog/solodit-c...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!