过去一周(2026年10月19日–2026年10月25日),本报告追踪了两起安全事件,估计损失约为 557万 USD。详细分析聚焦于以太坊上 X100 Freezer 的治理权被夺取事件。
| 日期 | 事件 | 类型 | 估计损失* |
|---|---|---|---|
| 2026年10月21日 | NEM Token | 业务逻辑缺陷 | ~82.0万 USD |
| 2026年10月24日 | X100 Freezer | 治理设计缺陷 | ~475万 USD |
*损失金额均为约数。X100 Freezer 的金额对应下文分析的代表性交易,与 Phalcon 的初步估计一致;总额不包括另行报告的后续资金转出。NEM 的日期是链上交易日期;公开告警发布于 10 月 22 日 [1]。
- X100 Freezer:这起事件表明,治理资格即时生效与缺少执行时间锁会共同带来风险:攻击者利用闪电贷资金购买并冻结代币,立即取得参议院过半席位,并在同一笔交易中通过并执行了一项恶意财库提案。
Web3 领先的安全审计机构
上线前验证设计、代码和业务逻辑
本周焦点:X100 Freezer
2026年10月24日,以太坊上的链上质押与流动性管理系统 X100 Freezer 遭到攻击,在本文分析的交易中损失约 475万 USD。治理设计缺陷使新获得并被冻结的代币余额能够立即赋予参议院席位,而提案获批与执行之间没有时间锁。攻击者取得了三个席位中的两个,引导财库在攻击者控制的资金池中进行兑换,并出售收回的部分代币,以偿还闪电贷并获利 [2]。
背景
X100 用户将 X 代币冻结在分层质押仓位中,以获取奖励。位于 0x79f3...2a46 的 Freezer 合约还管理该项目在 Uniswap V3/V4 和 PancakeSwap V3 上的流动性。它通过 DexTreasury 继承 Senate 治理合约;参议院通过提案和投票决定财库操作。
公开的 freeze() 入口函数将用户的代币转入合约,然后调用私有函数 _freeze() 创建质押仓位并更新相关账目。_tierTotal[owner][tier] 记录每个账户在每个层级中的冻结数量。_freeze() 更新这些余额,并通过 _onStakeChanged(owner) 通知治理系统:
function _freeze(address owner, uint8 tier, uint256 amount) private returns (uint256 id) {
_settle(owner);
Account storage account = _accounts[owner];
if (account.firstFreezeAt == 0) {
account.firstFreezeAt = block.timestamp;
}
id = _create(owner, tier, amount, block.timestamp, block.timestamp);
_tierTotal[owner][tier] += amount;
totalFrozen += amount;
totalFrozenIn[tier] += amount;
_onStakeChanged(owner);
emit Frozen(id, owner, tier, amount);
}
账户要加入参议院候选人名单,需调用 applyForSenate()。该调用会触发 _register(),后者记录注册信息、收取候选资格费用,并调用同一个质押更新钩子:
function _register(address account) internal {
require(!_registered[account], AlreadyRegistered(account));
_registered[account] = true;
_collectCandidacyFee(account);
emit Registered(account);
_onStakeChanged(account);
}
该钩子会跳过未注册账户。对于已注册账户,它维护一份按冻结余额排序的名单,并通过 _link() 插入或移动账户。isSenator() 将该名单中排在前 senateSize 位的账户认定为参议员。系统支持 3 至 21 个席位,每个席位拥有一票。冻结代币有提取期限,由 _unfreezeAt() 根据仓位的 frozenAt 时间戳和质押层级计算。
参议员通过 propose() 提交获准的财库操作;该函数记录提案,并自动为提案人投下赞成票。其他参议员通过 vote() 投票,_tally() 则统计当前席位的赞成票。提案会记录创建时间,以及由 votingPeriod 决定的截止时间;该参数默认为三天。
v3Swap() 等财库函数在 DexTreasury._isProposable() 中属于允许的提案目标。其 onlySenate 修饰符要求调用来自合约自身。执行路径会使用已获批准的提案数据进行这项自调用。随后,Freezer._afterExecute() 检查剩余的 X 余额是否足以覆盖 totalFrozen,以保留冻结本金总额。
漏洞分析
Freezer 的治理设计存在缺陷:新获得的质押在赋予参议院权限前无需等待,已获批准的提案在执行前也没有时间锁。
首先,用于排名的质押数量直接取自当前冻结余额。_stakeOf() 调用 frozenOf(),后者对 _tierTotal 求和,既不检查质押时长,也不读取历史余额:
function _stakeOf(address account) internal view override returns (uint256) {
return frozenOf(account);
}
function frozenOf(address owner) public view returns (uint256 total) {
total = 0;
for (uint8 tier = 0; tier < TIERS; ++tier) {
total += _tierTotal[owner][tier];
}
}
上文所示的存入和注册路径在同一调用序列中更新资格。虽然 _freeze() 会记录仓位的时间戳,但排名和参议员资格检查并不使用这些时间戳来强制执行最低持有期。因此,资金充足的新候选人可以在同一笔交易中取得席位,并行使提案权和投票权。代币的提取期限不会延迟这些治理权利的生效。
其次,三天的投票期只决定提案何时到期。提案一经创建,_state() 就会返回 Open;在执行、取消或到期之前,提案始终保持开放状态。过半数检查与经过的时间无关:
function _state(Proposal storage proposal) private view returns (ProposalState) {
if (proposal.executed) {
return ProposalState.Executed;
}
if (proposal.cancelled) {
return ProposalState.Cancelled;
}
if (block.timestamp > proposal.deadline) {
return ProposalState.Expired;
}
return ProposalState.Open;
}
function _isPassed(uint256 support) private view returns (bool) {
return support * 2 > senateSize;
}
只要开放状态提案的赞成票超过 senateSize 的一半,execute() 就会接受该提案。它在调用财库操作前,既不检查自 createdAt 以来是否经过最低时长,也不要求批准后等待一段时间:
function execute(uint256 id) external {
Proposal storage proposal = _requireOpen(id);
uint256 size = senateSize;
(uint256 support, address[] memory seats) = _tally(id);
require(_isPassed(support), NotPassed(id, support, size));
proposal.executed = true;
(bool ok, bytes memory result) = address(this).call(proposal.data);
if (!ok) {
assembly ("memory-safe") {
revert(add(result, 32), mload(result))
}
}
_afterExecute(id);
emit ProposalExecuted(id, _msgSender(), support, size, seats);
}
自调用满足 onlySenate 的要求,因此财库访问检查不会增加执行延迟。最后的冻结本金检查既不限制提案的执行时间,也不强制要求财库兑换达到最低回报。这些条件使刚刚形成的多数席位能够在一笔交易中批准并执行财库操作。
攻击分析
以下分析基于交易 0x38c608...4d81d0。
-
步骤 1:攻击者使用闪电贷资金购买
X代币,并将其分配给两个受控合约。借入的资金用于购买代币,以建立两个质押仓位。 -
步骤 2:每个受控合约都在 Freezer 中冻结其持有的
X,并调用applyForSenate()。交易调用轨迹显示,两个合约在注册后均收到了Ranked事件。它们的冻结余额使其在这笔交易中取得两个参议院席位。攻击发生时,参议院共有三个席位,因此控制其中两个就取得了通过财库提案所需的多数席位。

- 步骤 3:攻击者创建了一个 PancakeSwap V3
X/USDT资金池,并向其中提供流动性。一个受控合约提出财库兑换提案,并自动投下赞成票;另一个合约投下第二张赞成票。达到过半数门槛后,Freezer 执行了编号为 7 的提案。其中的v3Swap()调用指定以 400,000X作为输入,并设置了amountOutMinimum = 0。财库仅收到约 35.68USDT。交易调用轨迹记录了votesFor = 2和senateSize = 3,表明攻击者取得的多数席位批准了该操作。

- 步骤 4:攻击者从其新创建的 PancakeSwap V3 资金池中撤出流动性,收回财库的
X。他们通过现有的 Uniswap V3X/USDT资金池出售部分代币,并偿还闪电贷。还款资金来自随后进行的出售,而不是立即提取被冻结的质押仓位。
结论
这起事件源于治理设计缺陷:当前冻结余额能够立即赋予治理权限,而已获批准的提案无需经过时间锁即可执行。攻击者利用这两个条件,在必须偿还闪电贷之前取得并行使了多数席位的权力。随后,财库通过攻击者控制的 PancakeSwap V3 资金池,将 400,000 X 兑换为约 35.68 USDT。攻击者从该资金池撤出流动性,收回财库的代币,再通过另一个现有的 Uniswap V3 X/USDT 资金池出售其中一部分,并用所得资金偿还闪电贷、保留利润。
治理机制应要求达到最低质押时长,或根据当前交易之前某个区块的历史余额快照确定席位资格和投票权。这可以防止在当前交易中注资的仓位立即批准财库资金转移。批准与执行之间设置时间锁,应能防止完整的夺权和转出资金过程在一笔交易中完成,并留出审查或取消恶意提案的时间。这些控制措施针对治理流程中的不同阶段,应在智能合约审计中一并评估。
参考资料
[1] BlockSec Phalcon:NEM Token 事件分析
[2] BlockSec Phalcon:X100 Freezer 治理事件分析
关于 BlockSec
BlockSec 是一家提供全栈区块链安全与加密资产合规服务的公司。我们打造的产品和服务帮助客户在协议与平台的整个生命周期中开展代码审计(包括智能合约、区块链和钱包)、实时拦截攻击、分析事件、追踪非法资金,并履行 AML/CFT 义务。
BlockSec 曾在知名会议上发表多篇区块链安全论文,报告多起 DeFi 应用的零日攻击,阻止多起黑客攻击、挽救了超过 2,000万 USD 的资产,并保护了价值数十亿美元的加密资产。
-
官方 Twitter 账号:https://twitter.com/BlockSecTeam
-
🔗 BlockSec 审计服务 : 提交申请



