我们监控到 BNB Smart Chain 上针对 WET Token 的攻击事件,共造成 40k USD 的损失。
<!--StartFragment-->
2025年9⽉17⽇,我们监控到 BNB Smart Chain 上针对 WET Token 的攻击事件:
https\://bscscan.com/tx/0xf92539acf7eadfd4a98925927a52af5349cb13c2a250908373a5baf8ea4b49ad
攻击共造成 40k USD 的损失。
<!--EndFragment-->
<!--StartFragment-->
攻击者⾸先从 Moolah 平台利⽤ flashloan 贷了 5,000,000 BUSD,
<!--StartFragment-->
在 flashloan 的回调函数 onMoolahFlashLoan 中实现真正的攻击流程。攻击者⾸先使⽤ 5,000,000 BUSD 兑换了 19,432,567WETToken,
<!--EndFragment-->
<!--StartFragment-->
接着,攻击者调⽤受害者合约 0x0A4085F8a587af76936Ac6368DFc161e5C875882 的函数 0x6a154c56 。
完成了 332 WETToken 兑换 2,234 WUSD。
<!--EndFragment-->
<!--StartFragment-->
随后⼜利⽤剩下的 19,432,234 WETToken 兑换了 4,999,040 BUSD,
<!--EndFragment-->
<!--StartFragment-->
然后,⼜调⽤受害者合约 0x0A4085F8a587af76936Ac6368DFc161e5C875882 的函数 0xa6ff54ad 。将之前兑换的 2,234 WUSD 兑换了 223, 800 WETToken 。
<!--EndFragment-->
<!--StartFragment-->
整理的流程如下:
1. 花费 5,000,000 BUSD ,得到 19,432,567 WETToken
2. 花费 332 WETToken ,得到 2,234 WUSD
3. 花费 2,234 WUSD ,得到 223, 800 WETToken
所以,受害者合约 0x0A4085F8a587af76936Ac6368DFc161e5C875882 的函数 0x6a154c56 本质上是完成了 WETToken 兑换 WUSD 的操作,函数 0xa6ff54ad 完成的是 WUSD 兑换 WETToken 的操作。具体的兑换逻辑我们可以看⼀下函数的实现,由于函数没有开源,只能看反编译后的内容。
函数 0x6a154c56 的实现如下:
<!--EndFragment-->
function 0x6a154c56(uint256 varg0) public nonPayable { find similar
require(msg.data.length - 4 >= 32);
v0, /* bool */ v1 = _wet.transferFrom(msg.sender, address(this), varg0).gas(msg.gas);
require(bool(v0), 0, RETURNDATASIZE()); // checks call status, propagates error data on error
require(MEM[64] + RETURNDATASIZE() - MEM[64] >= 32);
require(v1 == bool(v1));
require(v1, Error('failed'));
v2 = 0x235f(varg0); // WET amount to WUSD amount
v3, /* bool */ v4 = _wusd.transfer(msg.sender, v2).gas(msg.gas);
require(bool(v3), 0, RETURNDATASIZE()); // checks call status, propagates error data on error
require(MEM[64] + RETURNDATASIZE() - MEM[64] >= 32);
require(v4 == bool(v4));
emit 0x23266bed0cd8b71bf4eea333265915d69df9cc6cd109a7f5978ff864bdac5c48(msg.sender, varg0, v2);
}
<!--StartFragment-->
逻辑很简单,输入的 varg0 为输入 WET Token 数量,通过函数 0x235f 转化为 WUSD 数量,再转给⽤户。
0x235f 的主要逻辑如下:
<!--EndFragment-->
function 0x235f(uint256 varg0) private {
if (varg0 > 0) {
v0 = new address[](2);
CALLDATACOPY(v0.data, msg.data.length, 64);
require(v0.length, Panic(50)); // access an out-of-bounds or negative index of bytesN array or
slice
v1 = v0.data;
v0[0] = _wet;
require(1 < v0.length, Panic(50)); // access an out-of-bounds or negative index of bytesN array
or slice
v0[1] = _usdt;
v2 = new address[](v0.length);
v3 = v4 = v2.data;
v5 = v6 = v0.data;
v7 = v8 = 0;
while (v7 < v0.length) {
MEM[v3] = address(MEM[v5]);
v3 += 32;
v5 += 32;
v7 += 1;
}
v9, /* uint256 */ v10 = _uniswapV2Router.getAmountsOut(varg0, v2, v11, _wet).gas(msg.gas);
//pancakeswap
require(bool(v9), 0, RETURNDATASIZE()); // checks call status, propagates error data on error
RETURNDATACOPY(MEM[64], 0, RETURNDATASIZE());
MEM[64] = MEM[64] + (0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0 &
RETURNDATASIZE() + 31);
require(MEM[64] + RETURNDATASIZE() - MEM[64] >= 32);
require(MEM[MEM[64]] <= uint64.max);
require(MEM[64] + MEM[MEM[64]] + 31 < MEM[64] + RETURNDATASIZE());
require(MEM[MEM[64] + MEM[MEM[64]]] <= uint64.max, Panic(65)); // failed memory allocation (too
much memory)
v12 = new uint256[](MEM[MEM[64] + MEM[MEM[64]]]);
require(!((v12 + ((MEM[MEM[64] + MEM[MEM[64]]] << 5) + 63 &
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) > uint64.max) | (v12 + ((MEM[MEM[64]
+ MEM[MEM[64]]] << 5) + 63 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) < v12)),
Panic(65)); // failed memory allocation (too much memory)
v13 = v14 = v12.data;
require(32 + ((MEM[MEM[64] + MEM[MEM[64]]] << 5) + (MEM[64] + MEM[MEM[64]])) <= MEM[64] +
RETURNDATASIZE());
v15 = v16 = 32 + (MEM[64] + MEM[MEM[64]]);
while (v15 < 32 + ((MEM[MEM[64] + MEM[MEM[64]]] << 5) + (MEM[64] + MEM[MEM[64]]))) {
MEM[v13] = MEM[v15];
v15 += 32;
v13 += 32;
}
require(1 < v12.length, Panic(50)); // access an out-of-bounds or negative index of bytesN array
or slice
return v12[1];
} else {
return 0;
}
}
<!--StartFragment-->
这段代码看起来复杂,实际上⼤多数都是验证内存是否越界和函数返回值是否正常。本质上就是通过 pancakeSwapV2 计算输入 varg0 数量的 WET Token 可以兑换的 BUSD 的数量,按照该数量给⽤户转 WUSD ,其中 WUSD 的价值在代码中认为和 BUSD 为 1:1 。合约 0x0A4085F8a587af76936Ac6368DFc161e5C875882 的函数 0xa6ff54ad 也是类似的逻辑,完成了由 WUSD 兑换 WET Token 的⼯作。
在步骤2中, 322 WETToken 价值 2,234 USD ,⽽在步骤3中, 2,234 USD 却兑换了 223,800 WETToken 。造成该原因的本质是,代码在计算 WET Token 的价格使⽤的是 PancakeSwapV2 pair ,⽽ PanckeSwap V2 使⽤的是恒定乘积算法,价格及容易被⼤额买卖操纵。攻击者在使⽤ WET Token 兑换了 WUS 后,使⽤⼤额卖单,卖出 WET Token 获取 BUSD ,⼤幅拉低了 WET Token 的价格,由原先的 1 WET Token = 6.9 BUSD ,拉低到了 1 WET Token = 0.00998 BUSD 。所以,在步骤3中,攻击者可以⽤ 2,234WUSD 购买了 223, 800 WETToken 。
随后,攻击者通过反复的操作,最终获利 40,000 USD ,完成攻击。
<!--EndFragment-->
<!--StartFragment-->
本次漏洞的成因是 WET Token 项⽬合约的在计算 WET Token 价格时,使⽤了 PancakeSwapV2 Pair ,⽽ PanckeSawpV2 使⽤恒定乘积算法,价格容易被操纵。导致攻击者通过反复买入,操纵价格,卖出获利,最终完成对 WET Token 项⽬的攻击。
建议项⽬⽅在设计经济模型和代码运⾏逻辑时要多⽅验证,合约上线前审计时尽量选择多个审计公司交叉审计。
<!--EndFragment-->
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!