4 月 28 号上午 11 点的时候,大佬突然在群里丢了个链接[1],监控到了 FTM 上的一笔异常交易
DexScreener 上查看 K 线,这个长长的上影线,稳定币 DEI 价格最高被抬到了 $15 左右。
直接翻到引发异常报价的交易 0x3982[2]
投入 0.3m ,赚回 17 m,赚麻了。
扫一眼交易的 Token Transfer
显然是闪电贷攻击模式。
ethtx.info 不支持 FTM 网络,原来一直想自己搭一个,但懒癌晚期,这回要用的时候露怯了。只能先用万能的 Tenderly[3] 了。
Tenderly 功能是很强的,就是用户体验不太舒服。展开闪电贷的层层 callback,在最里层[4]大概找到了问题的关键。
攻击者通过 DeiLenderSolidex.borrow()
借出了 17m 的 DEI。交易细节如下:
{
"[FUNCTION]": "borrow",
"[OPCODE]": "CALL",
"from": {
"address": "0x1f56ccfe85dc55558603230d013e9f9bfe8e086c",
"balance": "0"
},
"to": {
"address": "0x8d643d954798392403eea19db8108f595bb8b730",
"balance": "0"
},
"value": "0",
"input": {
"to": "0x701428525cbac59dae7af833f19d9c3aaa2a37cb",
"amount": "17246885701212305622476302",
"price": "20923953265992870251804289",
"timestamp": "1651113560",
"reqId": "0x01701220183a8e97b39ebe3c38b6166cd7c9ddfe3c38fd76352e5652b9c25467aa47b040",
"sigs": [
{
"signature": "1835036472718200664753898924933875196349373787186253604571797551094739683650",
"owner": "0xf096ec73cb49b024f1d93efe893e38337e7a099a",
"nonce": "0xd58d8931b98942ee19c431b72f4bc8b3ed28d8df"
}
]
},
"output": {
"debt": "17380403324861055919790384"
},
"gas": {
"gas_left": 779081,
"gas_used": 147051,
"total_gas_used": 2821527
}
}
DeiLenderSolidex
使用的抵押物是 sex-sAMM-USDC/DEI[5]。
攻击者这里用了 0.9292 个 sex-sAMM-USDC/DEI,这个实际是由 0.9m DEI 和 0.9m USDC 添加到 Solidly 中[6] , 再投到 Solidex 中[7]。原始抵押物价值大概是 1.8m 左右。加上本次交易中投入的 0.3 m,也是远小于借出金额 17m 的。借贷通常要求超额抵押,因此这里抵押物计价很可能存在问题。
正常 sex-sAMM-USDC/DEI 报价约为 2m,但实际可以看到 borrow 中抵押物报价为 21m,高了近 10 倍,因此可以借出 17m 的 DEI。
那么 borrow 使用的报价是从哪里来的?在 trace 中可以看到 DeiLenderSolidex
使用 oracle.getPrice()
获取抵押物价格。翻一下 Oracle 合约代码[8] 可以看到如下显眼的漏洞 pattern。
/// @title Oracle of DeiLenderLP
/// @author DEUS Finance
/// @notice to provide LP price for DeiLenderLP
contract Oracle is AccessControl {
/// @notice returns on chain LP price
function getOnChainPrice() public view returns (uint256) {
return
((dei.balanceOf(address(pair)) * IBaseV1Pair(address(pair)).getAmountOut(1e18, address(dei)) * 1e12 / 1e18) + (usdc.balanceOf(address(pair)) * 1e12)) * 1e18 / pair.totalSupply();
}
}
getOnChainPrice()
直接使用 LP Pair 中代币余额计算 LP 的币价。结合上前面攻击交易中大量的闪电贷操作,那么一切就都非常顺理成章了。
攻击者通过闪电贷使用大量 USDC 换成 DEI,从而使 DEI 对 USDC 价格大幅升高(正常二者价格应该是近 1:1 的)。使 Oracle 对 Solidly LP Pair 计价也大幅升高。然后攻击者利用这个错误报价以较少的抵押物借出了大量的稳定币资产 DEI。
然而真的就这么简单吗?
Deus Finance 实际在 3 月 15 号[9] 就已经遭受过一次闪电贷操纵预言机导致恶意清算的攻击,时隔一月,被一块石头拌倒两次?项目方真的就这么蠢?同样类型的漏洞,为什么隔了这么久才又会被利用?
仔细一点可以注意到前面的分析中有一个疑点。DeiLenderSolidex
实际使用的是 oracle.getPrice()
而不是 oracle.getOnChainPrice()
。具体看一下 oracle.getPrice()
代码如下:
function getPrice(
uint256 price, // 价格是外部传入的
uint256 timestamp,
bytes calldata reqId,
SchnorrSign[] calldata sigs // 带一个签名
) public returns (uint256) {
require(
timestamp + expireTime >= block.timestamp,
"ORACLE: SIGNATURE_EXPIRED"
);
uint256 onChainPrice = getOnChainPrice();
uint256 diff = onChainPrice < price ? onChainPrice * 1e18 / price : price * 1e18 / onChainPrice;
require(
1e18 - diff < threshold // threshold = 0.005 * 1e18
,"ORACLE: PRICE_GAP"
);
address[] memory pairs1 = new address[](1);
pairs1[0] = address(pair);
bytes32 hash = keccak256(
abi.encodePacked(
appId,
address(pair),
new address[](0 ""),
pairs1,
price,
timestamp
)
);
require(
muon.verify(reqId, uint256(hash), sigs), // 验证签名
"ORACLE: UNVERIFIED_SIGNATURES"
);
return price; // 返回传入的价格
}
这个预言机的 oracle.getPrice()
与其说是报价,不如说是验价。因为其返回的价格实际上是由调用方传入的。onChainPrice
只是进行一个验证,保证传入的报价与链上价格差距不超过 0.5%
。
再向上追一下,可以发现这个价格以及相应的签名实际在 DeiLenderSolidex.borrow()
时就已经传入了。所以关键不在于后面的预言机控制,而是传入的报价和签名本身就是错的!
那么问题来了,这个报价是有官方签名的,攻击者是怎么做出这个有效签名的?私钥泄露?签名服务器被控制?签名接口有漏洞导致可以签任意数据?
DeiLenderSolidex.borrow()
这个接口在普通用户操作 dApp 进行借贷操作时也会调用,那么对于普通用户来说怎样才能获得这个签名呢?为了搞清楚这个签名是怎么来的,直接登录 DEI 借贷的 dApp[10] 实际进行一个 borrow 操作,在浏览器 console 中抓到如下请求
POST https://node-balancer.muon.net/v1
# 请求
{"app":"solidly_permissionless_oracles_vwap","method":"lp_price","nSign":4,"params":{"token":"0x5821573d8F04947952e76d94f3ABC6d7b43bF8d0","hashTimestamp":true,"pairs0":"","pairs1":"0x5821573d8F04947952e76d94f3ABC6d7b43bF8d0"}}
# 应答
{"success":true,"result":{"confirmed":true,"_id":"626a357f3883486e75c9a904","app":"solidly_permissionless_oracles_vwap","method":"lp_price","nSign":4,"owner":"0x031e6efe16bCFB88e6bfB068cfd39Ca02669Ae7C","peerId":"QmPMbqPL4ry8ef73VfjyKcVJHuUNFqF4k2XT61y6KJG7jH","data":{"params":{"token":"0x5821573d8F04947952e76d94f3ABC6d7b43bF8d0","hashTimestamp":true,"pairs0":"","pairs1":"0x5821573d8F04947952e76d94f3ABC6d7b43bF8d0"},"timestamp":1651127679,"result":{"token":"0x5821573d8F04947952e76d94f3ABC6d7b43bF8d0","tokenPrice":"1977521616249579357592959","pairs0":[],"pairs1":["0x5821573d8F04947952e76d94f3ABC6d7b43bF8d0"],"volume":"403246276530832094694013","timestamp":1651127679},"init":{"party":"P16415418721872448934","nonce":"K16511276797573334511","nonceAddress":"0x8c00EBeF337A72adFea08910b830857ee5320C25"}},"startedAt":1651127679,"confirmedAt":1651127682,"signatures":[{"owner":"0xF096EC73cB49B024f1D93eFe893E38337E7a099a","ownerPubKey":{"x":"0xeae3877457595b4884e6fffa853ad34ca19cb142e06e90796c3cdf983893b8d","yParity":"1"},"timestamp":1651127682,"result":{"token":"0x5821573d8F04947952e76d94f3ABC6d7b43bF8d0","tokenPrice":"1977521616249579357592959","pairs0":[],"pairs1":["0x5821573d8F04947952e76d94f3ABC6d7b43bF8d0"],"volume":"403246276530832094694013","timestamp":1651127679},"signature":"0xd6533ff6c2de92ba7c7e30c2ac7745d43286ee8da9550ce331e8397e7365648b"}],"cid":"f0170122016b1aedf50a8805f3aa57694bc8a867d842310a07f383603e14c01b62267aded"}}
原来是在前端会请求 Muon API 获取 solidly LP 价格,并把返回的结果及签名放到 borrow 合约请求中。所以本质来说是 Muon 的价格报错了,链上大费周章的闪电贷操纵,只是为了配合这个错误价格,通过验证而已。
这里先不提这个链下报价的操作有多么的 web2
,重点来研究一下攻击者怎么做到让 Muon 报错价格的呢?
首先想到的一点是 Muon API 本身有没有简单的逻辑错误,比如可以传入不同的 token 和 pairs,使用某个不相关的 pairs 为 token 报价,比如拿 BTC 价格给 DEI 报价。但遗憾的是简单测试一遍没发现类似的问题。更大可能是攻击者先黑掉了 Muon 的服务器或者拿到了 Muon 的私钥之一(虽然 Muon 报价签名设计上可以用多签,但在上面可以看到实际只需要一个签名就通过了)从而构造出了有效的签名。当然还有一种可能,就是这个报价机制也可能被链上某些操作操纵。
这种签名报价的形式,其实可以看作是链下报价,链下不会读到某个交易中间的报价结果,因此不受闪电贷攻击的影响。官方发布的 3 月 15 号攻击复盘文章中, Muon VWAP Oracle[11] 链下报价恰恰就是作为解决闪电贷攻击的手段而引入的。因此即使被操纵也不会是通过闪电贷的形式,那么攻击者具体是怎么做的呢?
VWAP 指 Volume-Weighted Average Price
,相比于 Time-Weighted Average Price (TWAP)
,VWAP 指会根据交易量进行加权平均,求得最终报价。
Muon VWAP Oracle[12] 官方文档中具体的技术细节比较少,只在github 上[13] 找到一份可能的相关代码。
在这份代码中可以看到,Muon 也支持对股票、加密货币中心化交易所的交易进行报价。而所谓的 VWAP 简单理解可以当作是把每笔交易的成交价以成交量作为权重计算平均值。
DEX 交易如何进行报价呢?推测其实现方式就是监听 DEX Swap Event,根据 amount1Out/amount0In
或 amount1Out/amount0In
作为价格计算。
这个计算规则在股票和中心化交易所中的订单簿撮合成交的体系下问题不大,一笔交易就是由 A 换成 B,计算下数量比值就是成交价了。但其实这种实现在 DEX 中是有一个大问题的,那就是 DEX 的一笔交易,除了 A 换成 B
和 B 换成 A
这种形式外,还可能是 A + B 换成 B + A
!
翻一下攻击者的交易记录[14],在 0x3982
巨额套利之前的上一个交易 0x8589[15],正是这种形式!
这一次连 Tenderly 都不需要打开,直接看一下 event log 即可
在大部分的代码实现中, DEX 交易中实际并不限制只能由单币种进出,只要完成兑换后池子总流动性是在增加的(收取了手续费)即可。因此攻击者可以发起这样的交易,将 2m USDC 和 0.1m DEI
换成 0.1m DEI 和 2m USDC
,交易成本只需要支付 10 DEI 和 200 USDC
的手续费即可
在 VWAP Oracle 计算模式下,会将这笔交易解析成 2m USDC 换成了 0.1m DEI
,且交易量又非常大,因此整体 VWAP 价格被操纵成 1 DEI = 20 USDC
。
对应的 LP 价格也会受影响,变成 1 sAMM-USDC/DEI = 1m USDC + 1m DEI = 21m USDC
。也就是前面分析中 borrow 使用的错误报价。实际攻击中配合闪电贷,将链上价格也操纵成同样的价格,至此才完成完整的报价操纵。
Deus Finance 项目方在攻击后执行了一些多签交易[16],将DeiLenderSolidex
的 oracle 设置成了 0x0000000000000000000000000000000000000000
,即暂时停止了借贷功能。同时从 DAO 提取出了 15m 的 UDSC,似乎是打算再次进行赔偿。
peth > tx 0xa0F395aD5df1Fceb319e162CCf1Ef6645dE8508f 0xe3d84d5e00000000000000000000000004068da6c83afcfa0e13ba15a6696662335d5b7500000000000000000000000000000000000000000000000000000da475abf000000000000000000000000000e5227f141575dce74721f4a9be2d7d636f923044
Method:
0xe3d84d5e function emergencyWithdrawERC20(address token, uint256 amount, address to)
Arguments:
address token = Usdc(0x04068da6c83afcfa0e13ba15a6696662335d5b75)
uint256 amount = 15000000000000
address to = 0xe5227f141575dce74721f4a9be2d7d636f923044
peth > tx 0x8D643d954798392403eeA19dB8108f595bB8B730 0x7adbf9730000000000000000000000000000000000000000000000000000000000000000
Method:
0x7adbf973 function setOracle(address oracle_)
Arguments:
address oracle_ = 0x0000000000000000000000000000000000000000
Deus Finance 的@lafachief[17] twitter 中也表示漏洞本质原因是 Muon Oracle 被操纵,后续会和 Muon 一起讨论新的预言机实现方案。
发文前搜索了下,rekt.news[18] 也更新了比较完整的复盘。
DexScreener DEI 报价: https://dexscreener.com/fantom/0x5821573d8f04947952e76d94f3abc6d7b43bf8d0
[2]攻击交易 0x3982: https://ftmscan.com/tx/0x39825ff84b44d9c9983b4cff464d4746d1ae5432977b9a65a92ab47edac9c9b5
[3]Tenderly 分析交易: https://dashboard.tenderly.co/tx/fantom/0x39825ff84b44d9c9983b4cff464d4746d1ae5432977b9a65a92ab47edac9c9b5
[4]Tenderly DeiLenderSolidex.borrow(): https://dashboard.tenderly.co/tx/fantom/0x39825ff84b44d9c9983b4cff464d4746d1ae5432977b9a65a92ab47edac9c9b5/debugger?trace=0.3.2.4.2.4.2.4.2.4.2.4.2.4.2.4.2.4.2.4.2.4.2.4.2.4.2.4.2.4.2.4.2.4.2.4.2.4.2.4.2.4.2.1.8
[5]sex-sAMM-USDC/DEI 合约: https://ftmscan.com/tx/0x97ca22c23d8791adf25ef926a576fcd2461fb831413b1a5ba04c9c0d87dadcd5
[6]添加流动性交易: https://ftmscan.com/tx/0x4bff7224310f6576d9312c710057ed27e0b9113d5852407dd8d9584db15baeed
[7]Solidex deposit 交易: https://ftmscan.com/tx/0x2cad1d5890c457aa27c406d93a79354ea171dcb600f9a79bbc8bcca4a736fc76
[8]DeiLenderSolidex Oracle 合约: https://ftmscan.com/address/0x8129026c585bcfa530445a6267f9389057761a00#code
[9]Deus Finance 3 月 15 号攻击复盘: https://lafayettetabor.medium.com/deus-post-mortem-3c65df12927f
[10]DEI 借贷 dApp: https://app.dei.finance/borrow
[11]Muon VWAP Oracle 介绍: https://medium.com/muon/muon-a-superior-decentralized-oracle-for-next-generation-price-feeds-d6da9299f27f
[12]Muon VWAP Oracle 文档: https://muon.gitbook.io/basics/use-cases-1/oracles-data-feeds
[13]Muon stock-api github: https://github.com/muon-protocol/stock-api/tree/main/src/main/java/com/vitamin/stocx/impl/dex
[14]攻击者的交易记录: https://debank.com/profile/0x701428525cbac59dae7af833f19d9c3aaa2a37cb/history?chain=ftm
[15]攻击 vwap 交易 0x8589: https://ftmscan.com/tx/0x8589e136e6ad927096d07baa16852d16f11456c0446efb8f1ecd467ce0d4cb10
[16]项目方地址: https://ftmscan.com/address/0xef6b0872cfdf881cf9fe0918d3fa979c616af983
[17]@lafachief twitter: https://twitter.com/lafachief/status/1519624600719663106
[18]rekt.news: https://rekt.news/deus-dao-rekt-2/
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。