缺陷出在市场计算 MAMO 的方式上。存款凭证 mMAMO 的价值等于市场的 MAMO 余额除以凭证总量,任何人都可以通过直接把 MAMO 发送到合约来抬高这个余额。该市场原本设有 2000 万 MAMO 的供应上限,但 Moonwell 的风险经理 Anthias Labs 在事后分析中总结道:“2000 万 MAMO 的供应上限检查针对的是正式的 mint 输入。”于是攻击者先存入 1500 万 MAMO,再直接向合约发送 5300 万 MAMO。结果每个 mMAMO 的价值骤增至原来的 3.7 倍,而供应上限从未被触发。随后 MAMO 价格被拉高,攻击的剩余部分得以完成。
三天之后,同样的模式出现在 Cronos 上的 Tectonic。Certora 对那次攻击的分析提出了修复方案:将现金保存在账本中,而不是直接读取 token 余额。我们在 Moonwell 市场上运行了 AutoProver——Certora 用于编写和证明规范的智能代理——所用代码是 2026 年 8 月 24 日的状态,也就是攻击前的最后一次提交。这次运行发生在攻击后的 10 月,并针对借贷市场使用了一个简短的威胁模型。AutoProver 编写了一条规则:不经过市场函数的转账不得改变市场欠供应者的金额,而这个数字正是供应上限检查的依据。Certora Prover 报告该规则在已部署合约上被违反,并在下面假设条件下验证了基于现金账本的修复方案。
16 万美元存款如何变成 1100 万美元贷款
攻击者通过两种方式抬高其抵押品价值,将 16 万美元存款放大为 1100 万美元贷款:直接将 MAMO 发送到市场,以及拉高 MAMO 价格。
从 UTC 07:04 起,攻击者向市场供应了 15,089,595 MAMO,按当日上午 0.0106 美元的价格约合 16 万美元,同时获得约 7.36 亿 mMAMO,约占当时全部流通 mMAMO 的四分之三。每笔存款都通过了供应上限检查,因为市场始终低于 2000 万 MAMO。
攻击的其余部分相互重叠推进。从约 09:11 起,攻击者开始在流动性稀薄的去中心化交易所买入 MAMO,其中包括 Aerodrome 的 MAMO/USDC 池,MAMO/USD 喂价随之攀升。Anthias Labs 估计总购买额约为 750 万美元,其中大部分是在操作期间循环使用的借入资产。第一笔借款发生在 08:43,更多借款从 09:16 开始,因此有些贷款是在直接转账之前就已借出。
在 09:19 和 09:21,攻击者通过普通 token 转账分别向 mMAMO 合约发送了 3500 万和 1800 万 MAMO。过程中没有创建任何 mMAMO,也没有触发存款函数,因此上限检查从未执行。一个 mMAMO 的价值从 0.0205 MAMO 上升到 0.0755 MAMO,增至 3.7 倍。攻击者持有的 7.36 亿 mMAMO 现在代表约 5500 万 MAMO,而不是 1500 万——仅凭这一点就几乎达到供应上限的三倍。
喂价在 09:28 达到 0.43 美元的峰值,Moonwell 在 09:29 接受了 0.40 美元的价格。Moonwell 计算抵押品价值的方式是:mMAMO 数量 × 每个 mMAMO 代表的 MAMO 数量 × MAMO 价格。按 0.40 美元计算,5500 万 MAMO 约合 2200 万美元抵押品,而 50% 的抵押因子允许约 1100 万美元贷款。攻击者在 08:43 至 09:30 之间完成了 18 笔借款,涉及 cbBTC、WETH、USDC 和 wstETH,随后在 09:41 和 09:42 通过 Circle 的 CCTP 分两次 burn,将 870 万 USDC 从 Base 跨链转移到 Ethereum。清算人扣押了攻击者几乎全部的 mMAMO,但彼时 MAMO 价格已回落至接近 0.01 美元,大部分债务已无人偿还。
要达到 1100 万美元,两个环节缺一不可。如果没有直接转账,攻击者的抵押品仍是 1500 万 MAMO,处于上限之内;如果没有价格拉高,5500 万 MAMO 按 0.0106 美元计算不过约 59 万美元。
为什么 Moonwell 的供应上限没有阻止攻击?
只有在有人存款时才会检查上限,而直接将 MAMO 发送到合约并不算存款,因此检查从未运行。
Moonwell 是 Compound v2 的分叉。一个 mMAMO 的价值,即其汇率,在 MToken.sol 中由四个数字计算:
exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply
Moonwell 自己的函数负责写入其中三个。第四个是 totalCash,每次都在 MErc20.sol 中从 token 读取:
function getCashPrior() internal view virtual override returns (uint) {
EIP20Interface token = EIP20Interface(underlying);
return token.balanceOf(address(this));
}
因此任何进入合约的 MAMO 都会被计入,无论它是否通过存款路径到达。供应上限在 Comptroller(批准每个操作的合约)的 mintAllowed 中检查,该函数在存款之前运行,且只在此时运行:
uint totalCash = MToken(mToken).getCash();
uint totalBorrows = MToken(mToken).totalBorrows();
uint totalReserves = MToken(mToken).totalReserves();
// totalSupplies = totalCash + totalBorrows - totalReserves
uint totalSupplies = sub_(
add_(totalCash, totalBorrows),
totalReserves
);
uint nextTotalSupplies = add_(totalSupplies, mintAmount);
require(nextTotalSupplies < supplyCap, "market supply cap reached");
这个上限衡量的确实是对的指标——市场欠其供应者的全部金额,但只在有人存款时才衡量。MAMO 转账调用的是 MAMO token 本身,而不是 Moonwell,因此无法触发任何 Moonwell 层的检查。两笔转账完成后,市场会拒绝任何新存款。问题是,当时已经没有什么可拒绝的了。
上限的存在并非无缘无故。MIP-B48 是 2025 年 10 月上架 MAMO 的提案,其中引用了 Anthias Labs 的说明:“约 20 万美元的 MAMO 可以立即兑换成稳定币,同时滑点保持在 7% 的清算奖励以下。”在攻击当天早晨,2000 万 MAMO 约值 21.2 万美元。这个上限确保 MAMO 抵押品接近清算人能够卖出的规模,而那两笔转账将其推高至接近该规模的三倍。
Moonwell 攻击事件时间线
整个攻击在 2026 年 8 月 27 日不到三小时内完成,从 UTC 07:04 的第一笔存款到 UTC 09:42 的第二笔跨链转账。
所有时间均来自 Anthias 事后分析。
- 2026 年 8 月 27 日,06:09 UTC — 一个关联账户向攻击者主账户 0x719e...919D 发送 gas 费用。
- 07:04 UTC — 第一笔 MAMO 存入 Moonwell。
- 08:43 UTC — 第一笔借款。
- 09:19 和 09:21 UTC — 两笔直接转账,总计 5300 万 MAMO。
- 09:28 UTC — MAMO/USD 喂价达到 0.43 美元峰值。
- 09:30 UTC — 最后一笔借款(99 万 USDC)以及第一次清算。
- 09:41 和 09:42 UTC — 两笔 CCTP burn,每笔 436 万 USDC。
- 10:53 UTC — Moonwell 将借款上限降至 1 wei。
- 11:09 UTC — Moonwell 将 MAMO 供应上限降至 1 wei。
借款过程耗时 47 分钟。跨链转账发出时,清算人仍在扣押 mMAMO。
Moonwell 攻击是针对 Compound 分叉的捐赠攻击吗?
是的。将抵押品 token 直接发送到 Compound v2 分叉以抬高其汇率,这就是典型的捐赠攻击,Hundred Finance、Venus 和 Tectonic 都曾以同样的方式中招。
这类攻击其实早已写在 Moonwell 自己的仓库里。2023 年添加的一个测试在 Moonwell 市场上复现了 2023 年的 Hundred Finance 攻击。测试将 100 个 token 直接放入市场余额,随后的注释写道:“由于汇率被大幅抬高,我们现在可以借走市场中的所有资产。”文件中的两个测试都执行了这笔借款,并且成功了。它们检查的是后续步骤:攻击者不能通过 redeemUnderlying 将捐赠取回。MAMO 攻击者从来不需要那一步——以被抬高的抵押品借款并留下债务就足够了。
另一个 Compound v2 分叉 Venus Protocol 在 2026 年 3 月 15 日因同一套操作损失。据 Rekt 报道,攻击者存入了 1200 万 THE,占上限的 84%,然后直接将 THE 发送到市场,使仓位达到 5300 万,峰值时达到上限的 367%,给 Venus 留下 220 万美元坏账。Moonwell 之后三天,Tectonic 也遭到直接转账与价格拉高这一相同组合的打击。
测试检查的是有人明确写下来的场景。是否覆盖了进入 totalCash 的所有途径则是另一个问题,这正是测试与形式化验证之间的差距。
AutoProver 在 Moonwell 的 MAMO 市场中发现了什么?
在攻击后的运行中,AutoProver 编写了一条规则,Prover 显示该规则被已部署合约违反:直接把 MAMO 发送到市场会改变市场欠供应者的金额。
AutoProver 在 Moonwell 仓库的 3b32d53 上运行,该提交于 2026 年 8 月 24 日合并,是攻击前的最后一次提交。其目标是 Base 上运行的 mMAMO 市场合约 MErc20Delegate。
AutoProver 为该市场对 150 个候选属性进行了排序。其中一个 supply_cap_bypass_and_dos_via_donation 询问“供应者的索取权是否会超过供应上限”。随后它为 supply 和 redeem 函数起草了 13 个属性,并将它们写成 CVL(Certora Prover 的规范语言)中的规则。该次运行使用 comprehensive 模式,花费约 400 美元。
与攻击匹配的规则是其市场会计文件中的第一条规则。用通俗的话说:在不调用任何市场函数的情况下将 MAMO 放入市场,市场欠供应者的金额不得改变。AutoProver 是这样编写的:
rule supplier_claim_unchanged_by_external_donation(uint256 donation) {
marketIsWellFormed();
address u = underlying();
require !isKnownMToken(u);
require donation > 0;
uint256 marketBalance = balanceByToken[u][currentContract];
require marketBalance + donation <= max_uint256;
mathint claimBefore = supplierClaim();
balanceByToken[u][currentContract] = require_uint256(marketBalance + donation);
mathint claimAfter = supplierClaim();
assert claimAfter == claimBefore,
"a direct underlying donation changed the quantity the supply cap is checked against, with no cap check performed";
}
supplierClaim() 即 getCash() + totalBorrows - totalReserves,也就是 Comptroller 与上限比较的数字。捐赠在不调用 Moonwell 的情况下向市场余额增加 MAMO,这正是攻击者两笔转账的所作所为。
形式化验证可以就供应上限证明什么
一旦市场将现金保存在自己的账本中,形式化验证就可以在关于 token 和预言机的明确假设下证明:没有任何直接转账会改变市场欠供应者的金额。
修复方案来自我们对 Tectonic 的分析:将现金保存在一个只有市场自身转账才会变动的账本中,这样发送到合约的 MAMO 不再改变 totalCash 或汇率。Moonwell 尚未改变其市场计算现金的方式,而是将 MAMO 供应上限和借款上限都设为 1 wei。因此下面的修复后合约由我们提供:
contract MErc20DelegateInternalCash is MErc20Delegate {
uint256 internal internalCash;
function getCashPrior()
internal view virtual override returns (uint256)
{
return internalCash;
}
function doTransferIn(address from, uint256 amount)
internal virtual override returns (uint256)
{
uint256 received = super.doTransferIn(from, amount);
internalCash += received;
return received;
}
function doTransferOut(address payable to, uint256 amount)
internal virtual override
{
internalCash -= amount;
super.doTransferOut(to, amount);
}
}
一个运行中的市场还需要一次性的管理员调用,将账本设置为当前余额。
在已部署代码上,Prover 找到了一个反例。在一个欠供应者 1 wei MAMO 的市场中,有人直接向合约发送 1 wei。现在欠供应者 2 wei,且没有运行任何上限检查。这就是最小规模的攻击。
使用账本后,Prover 验证了该规则:没有任何直接转账可以改变市场欠供应者的金额。这些运行假设 MAMO 是标准 ERC-20 token,恰好转移所请求的数量且从不回调市场,并允许价格预言机、利率模型和其他市场返回任意值。
截至 2026 年 10 月 28 日的状态
自 8 月 27 日 UTC 10:53 起,Moonwell 的 Base 市场借款上限一直为 1 wei;自 UTC 11:09 起,MAMO 存款上限也一直为 1 wei。9 月初执行的 MIP-X66 使用了约 24.6 万美元来自 Base 和 OP Mainnet 的协议储备,用于偿还 USDC 市场中的坏账,并修改了利率模型。据 Anthias 估计,此举使坏账每月增长约 5 万美元,而非之前的 33.9 万美元。MIP-X69 在一次未达法定人数的投票后,从 10 月 17 日起将 Base 上 MAMO 的抵押因子设为 0%。
供应者的恢复仍在进行中。8 月 31 日,一位 MAMO 供应者报告称,市场持有 729.5 MAMO 的流动性,而供应量为 126 万 MAMO。Moonwell 表示正在努力追回被盗资金,存款人也已在论坛上要求制定恢复计划和公平的提款规则。
Moonwell 攻击的关键要点
在一个入口点检查的上限只能限制该入口点。Moonwell 的上限衡量了正确的数字,但只在存款时衡量,而这个数字可以通过从不调用 Moonwell 的普通转账增长。市场只能对它所计算的内容设上限,因此修复方法是停止计入它不是通过自身函数收到的 token。
同一模式在 2026 年打击了三个 Compound 分叉:3 月 15 日的 Venus、8 月 27 日的 Moonwell 和 8 月 30 日的 Tectonic。每次攻击者都直接将抵押品 token 发送到市场,并推高一种交易稀薄的抵押品 token 的价格。能捕获直接转账的属性可以用一句话概括:从不调用市场的转账不得改变市场欠供应者的金额。
原文链接: certora.com/blog/moonwel...
如果你想深入了解 Web3 安全领域更多类似漏洞案例与防御思路,欢迎常来云栈社区逛逛,这里有大量关于智能合约安全、形式化验证和 DeFi 风险管理的技术讨论。