“ Code is Law. 2021 年 10 月 15 日 加密货币指数平台 Indexed Finance 遭受闪电贷攻击,资产损失约 1600 万美元。”
这是去年写的旧文,2021 年 DeFi 攻击屡见不鲜,这次攻击并不是损失最大的,漏洞也不是算有多精妙。但这笔者入圈后第一个见证全程的攻击,因此对攻击做了一个复盘。
本文附有攻击 PoC 代码。
01
—
攻击基本信息
Indexed Finance 的 DEFI Top 5 Tokens Index, DEFI5 和 Cryptocurrency Top 10 Tokens Index, CC10 指数池遭到攻击。二者攻击手法相同,本文以 DEFI 5 分析为主。
攻击者地址(创建攻击合约、调用合约获利的地址):
0xba5ed1488be60ba2facc6b66c6d6f0befba22ebe。
该地址已经被 Etherscan 标记为 Indexed Finance Exploiter。
截至成文时 (2021/11/07) 该地址资产尚未被转移。
DEFI5 攻击信息:
DEFI5 合约地址
0xfa6de2697d59e88ed7fc4dfe5a33dac43565ea41
攻击交易
0xbde4521c5ac08d0033019993b0e7e1d29b1457e80e7743d318a3c27649ca4417
攻击合约
0xfbc2e6b188013fc5eacd9944e6b8ced2c467464a
时间线(北京时间):
2021/10/15 02:37
Indexed Finance 遭受闪电贷攻击,资产损失约 1600 万美元
2021/10/15 03:11
官方发推声明发现 DEFI5 和 CC10 池被攻击
2021/10/15 03:41
官方尝试与攻击者进行链上交涉
2021/10/15 23:36
Indexed Finance 创始人之一 Dr Laurence Ξ. Day 发布文章 Update #1: Indexed Finance Attack 要求攻击者返还被盗奖金,承诺给攻击者金额的 10% 作为白帽子奖金
2021/10/16 12:34
Dr Laurence Ξ. Day 发布文章 Update #2: Indexed Finance Attack 称已经确认攻击者线下身份。要求在 17:00 UTC on the 17th of October 2021 前归还资金。
2021/10/16 21:54
Indexed Finance 发推表示 10% 奖金已失效。
2021/10/19 23:58
Indexed Finance 另一创始人 Dillon Kellar 公布攻击者个人信息, 发布文章 Update #3: Indexed Finance Attack
02
—
Indexed Finance 介绍
Indexed Finance 是一个以太坊上的被动投资组合管理策略项目,项目治理权由 NDX token 持有者掌握。
其主要产品为指数池(Index Pools),与股票交易中的指数基金类似,这里的指数池则是以加密货币的市值为权重表示的多种资产的投资组合。本次被攻击的 DEFI5 指数池就是代表市值最高的 DeFi 项目 token 的组合,池中资产包括 UNI, AAVE 等知名项目代币。每个指数池对应一种 ERC20 token,投资者向池中注入资产可以获得 token;燃烧掉 token 可以取回资产(收取 0.5% 退出费,转给官方)。另外池中多种资产可以进行兑换(收取 2% 的费用,直接流入池中,不会转给官方)。指数池不额外收取管理费。
由于指数池中的投资组合的各资产价格会有浮动,指数池需要定期进行资产再平衡(Rebalance),以更好的追踪指数。投资组合的喂价是由 Uniswap v2 Oralce 提供的。指数池建立后不再有任何人工干预,由合约自动管理。
所谓再平衡,具体来说是指数池会根据预设的权重算法定期重新计算所有资产的权重。仍以 DEFI5 池为例,DEFI5 池维护市值最高的 5 种 DEFI 项目代币。其权重为市值的平方根(平方根可以降低高市值的权重,使资产组合配比更均衡)。该值每一周会进行一次 reweight,即根据市值重新计算池中每种代币新的权重。每 3 次 reweight 可以进行一次 reindex,即重新评估最高市值的 5 种代币,加入新晋的 top 5 代币,移除市值跌出 top 5 的代币。reweight 和 reindex 只能重新计算相关权重指标,但无法立刻调整池中各种代币的比例。更新的权重也不会立刻升效,避免指数剧烈波动,而是在每次资产交换或者进出时逐步调整为前述计算的值。而当池中各币种所占比例与其权重不匹配时,即意味着存在套利空间,此时会有套利机器人等进行交换套利,从而逐步将池中各资产的比例调整的与其权重相匹配(有点类似多币种的 Uniswap)。
03
—
漏洞原理
合约中主要存在的问题在于错误地使用投资组合中单一的某种资产及其权重来估计总资产价值。从前文的介绍中可以看出,只有在多方参与自动平衡资产比例与权重后,这种估计才会比较准确。当资产比例有大幅变化时,其权重是没法及时自动更新的。那么通过闪电贷获取大量资产,操纵调整资产池中各资产的比例,就可以让合约错误的估计池的总资产。而在注入新资产时,其权重与总资产值相关。最终导致可以用较少的资金占用较大的权重,从而产生巨大的套利空间。
从代码细节上看,以下为估计资产池总资产的代码。
// 使用第一权重的 token 估算池的总价值。
// 以 UNI 为例
// 池总价值(以 UNI 为单位)= 池中 UNI 数 * 总权重 / UNI 权重。
function extrapolatePoolValueFromToken()
external
view
override
_viewlock_
returns (address/* token */, uint256/* extrapolatedValue */)
{
address token;
uint256 extrapolatedValue;
uint256 len = _tokens.length;
for (uint256 i = 0; i < len; i++) {
token = _tokens[i];
Record storage record = _records[token];
if (record.ready && record.desiredDenorm > 0) {
extrapolatedValue = bmul(
record.balance,
bdiv(_totalWeight, record.denorm)
);
break;
}
}
require(extrapolatedValue > 0, "ERR_NONE_READY");
return (token, extrapolatedValue);
}
// 通过 oracle 获取价格,计算池的总价值,以 ETH 为单位
function _estimatePoolValue(IIndexPool pool) internal view returns (uint144) {
(address token, uint256 value) = pool.extrapolatePoolValueFromToken();
return oracle.computeAverageEthForTokens(
token,
value,
SHORT_TWAP_MIN_TIME_ELAPSED,
SHORT_TWAP_MAX_TIME_ELAPSED
);
注入新资产时计算 MinimumBalance 的代码如下,会调用 _estimatePoolValue()
函数估计池总资产。
// 将 token 的 MinimumBalance 设置为池总值的 1%
function updateMinimumBalance(IIndexPool pool, address tokenAddress) external _havePool(address(pool)) {
IIndexPool.Record memory record = pool.getTokenRecord(tokenAddress);
require(!record.ready, "ERR_TOKEN_READY");
uint256 poolValue = _estimatePoolValue(pool);
PriceLibrary.TwoWayAveragePrice memory price = oracle.computeTwoWayAveragePrice(
tokenAddress,
SHORT_TWAP_MIN_TIME_ELAPSED,
SHORT_TWAP_MAX_TIME_ELAPSED
);
uint256 minimumBalance = price.computeAverageTokensForEth(poolValue) / 100;
pool.setMinimumBalance(tokenAddress, minimumBalance);
}
注入新资产计算权重的代码如下,可以看到权重与 MinimumBalance 正相关。
function gulp(address token) external override _lock_ {
Record storage record = _records[token];
uint256 balance = IERC20(token).balanceOf(address(this));
if (record.bound) {
if (!record.ready) {
uint256 minimumBalance = _minimumBalances[token];
if (balance >= minimumBalance) {
_minimumBalances[token] = 0;
record.ready = true;
emit LOG_TOKEN_READY(token);
uint256 additionalBalance = bsub(balance, minimumBalance);
uint256 balRatio = bdiv(additionalBalance, minimumBalance);
uint96 newDenorm = uint96(badd(MIN_WEIGHT, bmul(MIN_WEIGHT, balRatio)));
record.denorm = newDenorm;
record.lastDenormUpdate = uint40(now);
_totalWeight = badd(_totalWeight, newDenorm);
emit LOG_DENORM_UPDATED(token, record.denorm);
}
}
_records[token].balance = balance;
} else {
_pushUnderlying(token, address(_unbindHandler), balance);
_unbindHandler.handleUnbindToken(token, balance);
}
}
从代码上来看,需要在池中加入新资产才能利用漏洞。而普通用户是无法随意增加池中资产种类的。但由于 DEFI5 需要定期进行 reindex。在 reindex 时,如果 Top 5 资产排名发生变动,那么就可以引入新的资产。目前合约设定至少要 3 周才能进行一次 reindex,因此其实攻击的时间点也是经过精心挑选的。(也正是因为这个原因,有些已经短期内无法进行 reindex 的池没有被攻击,只有 DEFI5 和 CC10 两个池被攻击。)攻击者正是在刚好可以触发 reindex 的时刻,利用上述漏洞进行套利。
04
—
攻击流程
针对 DEFI5 的攻击交易是一笔合约调用交易。即攻击者事先创建了一个攻击合约,通过调用该合约,完成闪电贷攻击,最终获利价格约 1000 万美元的 token,包括 UNI, AAVE, COMP, CRV 等。具体交易在
https://etherscan.io/tx/0x44aad3b853866468161735496a5d9cc961ce5aa872924c5d78673076b1cd95aa
使用 ethtx.info 可以查看合约调用过程的 call trace,包括显示完整的调用树,及每个 call 对应的参数和返回值。
攻击交易为合约调用交易,攻击合约在
https://etherscan.io/address/0x277e851587eb5da22b52a10f4788576e68150277
该合约当然是没有源码的。
通过对交易的 trace 进行分析和合约的反编译逆向,笔者还原了攻击合约。并使用 hardhat 对主网进行 fork 测试,实现了对攻击的复现。点击阅读原文可以查看完整的攻击代码(含注释)。
这里简述一下攻击流程:
通过 UniswapV2 闪电贷借入 UNI,AAVE,COMP,MKR,SNX,CRV,SUSHI 几种资产。
合约调用 MarketCapSqrtController.reindexPool(DEFI5) 触发 reindex。此时 Top 5 发生变化,reindex 之后, SUSHI 被加入池中,初始权重为 0。
合约调用 DEFI5.swapExactAmountIn 将 DEFI5 池中的 AAVE, COMP, MKR, SNX, CRV 换成 UNI 取出。此时 DEFI5 池里 UNI 资产大量减少,其他 5 个资产大量增多。
MarketCapSqrtController.updateMinimumBalance(DEFI5, SUSHI) 更新 SUSHI 的 MinimumBalance,这个 MinimumBalance 是池中总资产的 1%。而池中的总资产是根据 UNI 来估计的。前面攻击者已经将 UNI 大量兑出,此时以 UNI 估计总资产,得到的值只有真实值的 1/40 左右。
SUSHI.transfer(DEFI5, 22*10**22); DEFI5.gulp(address(SUSHI)); 向 DEFI5 中转入 SUSHI,调用 gulp 更新权重。这个权重根据前面的 MinimumBalance 和投入的 SUSHI 总值记算,计算后 SUSHI 的权重变得很大(大概为原本最大权重 UNI 的 20 倍)。
假设原本投入 100 UNI 可以获得 1 DEFI5 token,现在则投入 100 SUSHI 可以获得 20 DEFI5 。而实际 SUSHI 价格远没有 20 倍 UNI 这么高。那么以 SUSHI 兑入 DEFI5,再使用 DEFI5 取出其他 5 种代币,以此循环,就可以用便宜的 SUSHI 换出大量贵重的 UNI 等代币。
偿还闪电贷,得到最终获利。
具体代码细节参考。
https://github.com/lmy375/Practices/blob/master/web3/indexed_attack/contracts/Attack.sol
(写闪电贷攻击 PoC 可比以前根据浏览器补丁写 JS PoC 容易多了)
05
—
攻击溯源
由于区块链自身的匿名性,链上攻击很难直接定位到线下具体攻击者。但此次官方竟然依靠圈内关系和一些社工手段成功定位到了的攻击者,并且据说攻击者为一个 18 岁的大学学生,略显离奇。(成文的时候官方和攻击者还在交涉,最近看新闻已经开始打官司了)
以下为溯源过程整理。
受攻击后,主办方回忆起在 Discord 上曾有人咨询过相关的技术问题。在 09 月 15 日前后 Discord 用户 UmbralUpsilon(后改成 BogHolder) 以要开发套利机器人的名义向 Indexed 相关人员咨询了有关 TWAP oracle 的问题。后来 Indexed 官方支付 $4K USDC 希望他可以分享代码。BogHolder 提供了地址 0xb7e77cdaf7ebf76db72571f2d6e43aa5e84a5e64。10 月 10 日时,支付了 $2K。后相关私信内容被删除(此地无银了),但官方公布了截图。
来自 @pcaversaccio 的线索:twitter 用户 @ZetaZeroes 通过 Gitter 向 @pcaversaccio 索要了 Kovan 测试币。
来自 Discord 中某人的线索: @ZetaZeroes 参加过 Code 423n4 的比赛,并拿到了 4000 多 USDC 奖金,其收款地址 0x3c86....00ab3。该地址在攻击前几小时前向 Tornado.Cash 转入 4 Ether。而攻击地址也从 Tornado.Cash 转出了 3 个 Ether 用于实施攻击。
通过与 Code 423n4 交涉,确认其 10 月 1 日举办的比赛 中的第 4 名 ID 为 tensors 的用户在 Discord 中的 ID 为 BogHolder。相关其与 Code 423n4 主办方的聊天记录后面也被删除,与 UmbralUpsilon 做法一致。
对应的收款地址曾在 Binance 和 Coinbase 等地址提款。通过与交易所的沟通,拿到了该用户的 KYC 信息。
另一方面,通过人肉搜索,官方发现了更多的信息佐证了上述推断。
tensors 注册 Code 423n4 时使用 github 账号为 mtheorylord1 。搜索找到了名称类似的账号 mtheorylord 。该账号 2016 年创建了名为 Grade-12-Project repo,似乎是高中作业。由于使用 Grade-12 而没用 12th Grade 这种表达习惯,推测可能并非美国高中。commit 使用的邮箱为 amedjedo7874@hwdsb.on.ca ,这个域名的所有者是一个加拿大高中(地址在 Hamilton, Ontario, Canada)
mtheorylord 同名账户在 StackExchange 曾问过有关申请美国博士的问题,并提到目前他有数学硕士学位。该账号在 7 月 18 日问了关于智能合约编写的一些问题。
mtheorylord 同名账户 2016 在 Wikipedia 中编辑过某些页面(关于某加拿大知识竞赛)的某些章节(Alumni 即校友一节)。Google 搜索对这一节的编辑,找到 nontrivial.xyz 网站。
该网站为其个人主页,在攻击发生后的几天该网站被关掉。但在 Google cache 中可以看到,本站博主在 University of Waterloo 学数学,并且对 cryptocurrency and other decentralized open source software 感兴趣。后来该网站又恢复上线,并删除了加密货币相关的内容。网站中有其个人简历,其中有姓名和生日,个人邮箱。生日显示博主年龄 18 岁。继续搜索姓名找到与某加拿大高中的关联,与前面域名显示的高中一致。其在 13 岁时高中毕业。
在确认其身份后,Indexed 创始人之一 Pr0 向网站的个人邮箱中发送的言辞比较强硬的邮箱,要求其归还资金,若还款会给其 $50000 的奖金。该邮箱回复同意,并附上收款地址 0xb7e77cdaf7ebf76db72571f2d6e43aa5e84a5e64 (但事后攻击者并未归还),注意该地址与攻击事件发生前 BogHolder 提供的地址一致。这个实锤进一步确认了调查中推断的人确实是攻击者。
06
—
一些思考
攻击方角度
使用 Tornado.Cash 应该保证足够时间间隔,且转入转出金额不要过于接近。
攻击过程所有涉及地址应使用全新地址,避免被追溯。
不同网络平台不建议使用相同的关联邮箱或者同名账号。
不要在公开社交媒体上发布线下身份相关的内容,避免人肉。
防守方角度
长时间上线运行也不能保证没有安全问题(上线 10 个月,仍存在漏洞被攻击利用)。
众测仍是目前较有效的发现漏洞的方式(攻击者就是来自某众测审计平台)。
不迷信混币平台,通过时间、金额关联性分析有定位混币平台交易往来的可能,不能过早放弃。
足够的情报网络(溯源线索、人肉)、圈内资源(交易所关系)可有效帮助解决紧急问题。。
项目方可考虑监控 mempool 中有关自身合约的大宗交易,避免风险,早做准备。
研究者角度
目前针对 DeFi 合约的攻击在合约语言层面的安全问题已经不多,逻辑套利类问题成为主要漏洞类型。
经过代码审计的合约未必就没有问题,漏洞往往存在代码细节中。
通过对金融模型的建模,或许可以通过形式化验证的方法检查这类安全问题。
07
—
尾声
对区块链安全、网络安全、软件安全有兴趣的同学欢迎关注公众号私信讨论。
另长期招智能合约、DeFi、区块链大佬。
待遇优厚,技术氛围好,不打卡。年底了,欢迎来简历
如果你不肯来,那么欢迎帮我推一下
或者,点下在看
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。