技术详解 | 链上打新局中局,大规模Rug Pull手法解密
近日,CertiK安全专家团队频繁检测到多起手法相同的“退出骗局”,也就是我们俗称的Rug Pull。
在我们进行深入挖掘后发现,多起相同手法的事件都指向同一个团伙,最终关联到超过200个Token退出骗局。这预示着我们可能发现了一个大规模自动化的,通过“退出骗局”方式进行资产收割的黑客团队。
在这些退出骗局中,攻击者会创建一个新的ERC20代币,并用创建时预挖的代币加上一定数量的WETH创建一个Uniswap V2的流动性池。
当链上的打新机器人或用户在该流动性池购买一定次数的新代币后,攻击者则会通过凭空产生的代币,将流动性池中的WETH全部耗尽。
由于攻击者在凭空获取的代币没有体现在总供应量中(totalSupply),也不触发Transfer事件,在etherscan是看不到的,因此外界难以感知。
攻击者不仅考虑了隐蔽性,还设计了一个局中局,用来麻痹拥有初级技术能力,会看etherscan的用户,用一个小的问题来掩盖他们真正的目的……
深入骗局
我们以其中一个案例为例,详解一下该退出骗局的细节。
被我们检测到的实际上是攻击者用巨量代币(偷偷mint的)耗干流动性池并获利的交易,在该交易中,项目方共计用416,483,104,164,831(约416万亿)个MUMI兑换出了约9.736个WETH,耗干了池子的流动性。
然而该交易只是整个骗局的最后一环,我们要了解整个骗局,就需要继续往前追溯。
部署代币
3月6日上午7点52分(UTC时间,下文同),攻击者地址(0x8AF8)Rug Pull部署了名为MUMI(全名为MultiMixer AI)的ERC20代币(地址为0x4894),并预挖了420,690,000(约4.2亿)个代币且全部分配给合约部署者。
预挖代币数量与合约源码相对应。
添加流动性
8点整(代币创建8分钟后),攻击者地址(0x8AF8)开始添加流动性。
攻击者地址(0x8AF8)调用代币合约中的openTrading函数,通过uniswap v2 factory创建MUMI-WETH流动性池,将预挖的所有代币和3个ETH添加到流动性池中,最后获得约1.036个LP代币。
从交易细节可以看出,原本用于添加流动性的420,690,000(约4.2亿)个代币中,有63,103,500(约6300万)约个代币又被发送回代币合约(地址0x4894),通过查看合约源码发现,代币合约会为每笔转账收取一定的手续费,而收取手续费的地址正是代币合约本身(具体实现在“_transfer函数中”)。
奇怪的是,合约中已经设置了税收地址0x7ffb(收取转账手续费的地址),最后手续费却被发到代币合约自身。
因此最后被添加到流动性池的MUMI代币数量为扣完税的357,586,500(约3.5亿),而不是420,690,000(约4.3亿)。
锁定流动性
8点1分(流动性池创建1分钟后),攻击者地址(0x8AF8)锁定了通过添加流动性获取的全部1.036个LP代币。
LP被锁定后,理论上攻击者地址(0x8AF8)拥有的所有的MUMI代币便被锁定在流动性池内(除开作为手续费的那部分),因此攻击者地址(0x8AF8)也不具备通过移除流动性进行Rug Pull的能力。为了让用户放心购买新推出的代币,许多项目方都是将LP进行锁定,意思就是项目方在说:“我不会跑路的,大家放心买吧!”,然而事实真的是这样吗?显然不是,这个案例便是如此,让我们继续分析。
Rug Pull
8点10分,出现了新的攻击者地址②(0x9DF4),Ta部署了代币合约中声明的税收地址0x7ffb。
这里有三个值得一提的点:
1.部署税收地址的地址和部署代币的地址并不是同一个,这可能说明项目方在有意减少各个操作之间与地址的关联性,提高行为溯源的难度
2.税收地址的合约不开源,也就是说税收地址中可能隐藏有不想暴露的操作
3.税收合约比代币合约晚部署,而代币合约中税收地址已被写死,这意味着项目方可以预知税收合约的地址,由于CREATE指令在确定创建者地址和nonce的情况下,部署合约地址是确定的,因此项目方提前就使用创建者地址模拟计算出了合约地址
实际上有不少退出骗局都是通过税收地址进行,且税收地址的部署模式特征符合上述的1、2点。
上午11点(代币创建3小时后),攻击者地址②(0x9DF4)进行了Rug Pull。他通过调用税收合约(0x77fb)的“swapExactETHForTokens”方法,用税收地址中的416,483,104,164,831(约416万亿)个MUMI代币兑换出了约9.736个ETH,并耗尽了池子中流动性。
由于税收合约(0x77fb)不开源,我们对其字节码进行反编译,反编结果如下:https://app.dedaub.com/decompile?md5=01e2888c7691219bb7ea8c6b6befe11c查看完税收合约(0x77fb)的“swapExactETHForTokens”方法反编译代码后,我们发现实际上该函数实现的主要功能就是通过uniswapV2 router将数量为“xt”(调用者指定)的税收合约(0x77fb)拥有的MUMI代币兑换成ETH,并发送给税收地址中声明的“_manualSwap”地址。
_manualSwap地址所处的storage地址为0x0,用json-rpc的getStorageAt命令进行查询后发现_manualSwap对应的地址正是税收合约(0x77fb)的部署者:攻击者②(0x9DF4)。
该笔RugPull交易的输入参数xt为420,690,000,000,000,000,000,000,对应420,690,000,000,000(约420万亿)个MUMI代币(MUMI代币的decimal为9)。
也就是说,最终项目方用420,690,000,000,000(约420万亿)个MUMI将流动性池中的WETH耗干,完成整个退出骗局。
然而这里有一个至关重要的问题,就是税收合约(0x77fb)哪来的这么多MUMI代币?
从前面的内容我们得知,MUMI代币在部署时的代币合约时的总供应量为420,690,000(约4.2亿),而在退出骗局结束后,我们在MUMI代币合约中查询到的总供应量依旧是420,690,000(下图中显示为420,690,000,000,000,000,需要减去decimal对应位数的0,decimal为9),税收合约(0x77fb)中的远超总供应量的代币(420,690,000,000,000,约420万亿)就仿佛凭空出现的一样,要知道,如上文所提,0x77fb作为税收地址甚至没有被用于接收MUMI代币转账过程中产生的手续费,税收被代币合约接收了。
手法揭秘
-
税收合约哪来的代币
为了探究税收合约(0x7ffb)的代币来源,我们查看了它的ERC20转账事件历史。
结果发现在全部6笔关于0x77fb的转账事件中,只有从税收合约(0x7ffb)转出的事件,而没有任何MUMI代币转入的事件,乍一看,税收合约(0x7ffb)的代币还真是凭空出现的。
所以税收合约(0x7ffb)地址中凭空出现的巨额MUMI代币有两个特点:
1.没有对MUMI合约的totalSupply产生影响
2.代币的增加没有触发Transfer事件
那么思路就很明确了,即MUMI代币合约中一定存在后门,这个后门直接对balance变量进行修改,且在修改balabce的同时不对应修改totalSupply,也不触发Transfer事件。
也就是说,这是一个不标准的、或者说是恶意的ERC20代币实现,用户无法从总供应量的变化和事件中感知到项目方在偷偷mint代币。
接着就是验证上面的想法,我们直接在MUMI代币合约源码中搜索关键字“balance”。
结果我们发现合约中有一个private类型的“swapTokensForEth”函数,传入参数为uint256类型的tokenAmount,在该函数的第5行,项目方直接将_taxWallet,也就是税收合约(0x7ffb)的MUMI余额修改为tokenAmount * 10**_decimals,也就是tokenAmount的1,000,000,000(约10亿)倍,然后再从流动性池中将tokenAmount数量的MUMI兑换为ETH并存在代币合约(0x4894)中。
再接着搜索关键字“swapTokenForEth“。
“swapTokenForEth”函数在“_transfer”函数中被调用,再细看调用条件,会发现:
1.当转账的接收地址to地址为MUMI-WETH流动性池。
2.当有其他地址在流动性池中购买MUMI代币的数量超过_preventSwapBefore(5次)时,“swapTokenForEth”函数才会被调用
3.传入的tokenAmount为代币地址所拥有的MUMI代币余额和_maxTaxSwap之间的较小值
也就是说当合约检测到用户在池子中用WETH兑换成MUMI代币超过5次后,便会为税收地址偷偷mint巨量代币,并将一部分代币兑换成ETH存储在代币合约中。
一方面,项目方表面上进行收税并定期自动换成少量ETH放到代币合约,这是给用户看的,让大家以为这就是项目方的利润来源。
另一方面,项目方真正在做的,则是在用户交易次数达到5次后,直接修改账户余额,把流动性池全部抽干。
-
如何获利
执行完“swapTokenForEth”函数后,“_transfer”函数还会执行sendETHToFee将代币地址中收税获得的ETH发送到税收合约(0x77fb)。
税收合约(0x77fb)中的ETH可以被其合约内实现的“rescue”函数取出。
现在再回看整个退出骗局中最后一笔获利交易的兑换记录。
获利交易中共进行了两次兑换,第一次是4,164,831(约416万)个MUMI代币换0.349个ETH,第二次是416,483,100,000,000(约416万亿)个MUMI代币换9.368个ETH。其中第二次兑换即为税收合约(0x7ffb)中“swapExactETHForTokens”函数内发起的兑换,之所以数量与输入参数代表的420,690,000,000,000(约420万亿)个代币不符,是因为有部分代币作为税收发送给了代币合约(0x4894),如下图所示:
而第一次兑换对应的,则是在第二次兑换过程中,当代币从税收合约(0x7ffb)发送至router合约时,由因为满足代币合约内的后门函数触发条件,导致触发“swapTokensForEth”函数所发起的兑换,并非关键操作。
-
背后的大镰刀
从上文中可以看出,MUMI代币从部署,到创建流动性池,再到Rug Pull,整个退出骗局周期才约3个小时,但是却以不到约6.5个ETH的成本(3 ETH用于添加流动性,3 ETH用于从流动性池中兑换MUMI以作诱导,不到0.5 ETH用于部署合约和发起交易)获得了9.7个ETH,利润超过50%。
攻击者用ETH换MUMI的交易有5笔,前文中并没有提到,交易信息如下:
-
https://etherscan.io/tx/0x62a59ba219e9b2b6ac14a1c35cb99a5683538379235a68b3a607182d7c814817
-
https://etherscan.io/tx/0x0c9af78f983aba6fef85bf2ecccd6cd68a5a5d4e5ef3a4b1e94fb10898fa597e
-
https://etherscan.io/tx/0xc0a048e993409d0d68450db6ff3fdc1f13474314c49b734bac3f1b3e0ef39525
-
https://etherscan.io/tx/0x9874c19cedafec351939a570ef392140c46a7f7da89b8d125cabc14dc54e7306
-
https://etherscan.io/tx/0x9ee3928dc782e54eb99f907fcdddc9fe6232b969a080bc79caa53ca143736f75
通过分析在流动性中进行操作的eoa地址后发现,相当一部分的地址为链上的“打新机器人”,结合整个骗局快进快出的特点,我们有理由认为,这整个骗局针对的对象正是链上十分活跃的各种打新机器人、打新脚本。
因此无论是代币看似没必要但是复杂的合约设计、合约部署、流动性锁定流程,还是中途攻击者相关地址主动用ETH换取MUMI代币的疑惑行为,都可以理解成是攻击者为了试图骗过链上各类打新机器人的反欺诈程序而做的伪装。
我们通过追踪资金流后发现,攻击所获得的收益最后全被攻击地址②(0x9dF4)发送到了地址资金沉淀地址(0xDF1a)。
而实际上我们最近检测到的多起退出骗局最初的资金来源以及最后的资金去向都指向这个地址,因此我们对这个地址的交易进行了大致的分析和统计。
最终发现,该地址在约2个月前开始活跃,到今天为止已经发起了超过7,000笔交易,并且该地址已经和超过200个代币进行过交互。
我们对其中的约40个代币交易记录进行分析,然后发现我们查看的几乎所有代币对应的流动性池中,最后都会有一笔输入数量远大于代币总供应量的兑换交易将流动性池中的ETH耗尽,且整个退出骗局的周期都较短。
其中部分代币(名烟中华)的部署交易如下:https://etherscan.io/tx/0x324d7c133f079a2318c892ee49a2bcf1cbe9b20a2f5a1f36948641a902a83e17
https://etherscan.io/tx/0x0ca861513dc68eaef3017e7118e7538d999f9b4a53e1b477f1f1ce07d982dc3f
因此我们可以认定,该地址实际上就是一个大规模的自动化“退出骗局”收割机,收割的对象就是链上的打新机器人。
该地址现在仍在活跃。
写在最后
如果一个代币在mint时不对应修改totalSupply,也不触发Transfer事件,那么我们是很难感知项目方是否有在偷偷mint代币的,这也将加剧“代币是否安全,完全依赖于项目方是否自觉”的现状。
因此我们可能需要考虑改进现有的代币机制或者引入一种有效的代币总量检测方案,来保障代币数量变更的公开透明,现在凭借event来捕获代币状态变更是不够的。
并且我们需要警醒的是,尽管现在大家的防骗意识在提高,但是攻击者的反防骗手段也在提高,这是一场永不停息的博弈,我们需要保持不断学习和思考,才能在这样的博弈中保全自身。
-
本文使用工具
查看交易基本信息:https://etherscan.io/
合约反编译:app.dedaub.com/decompilejson-rpc:
https://www.quicknode.com/docs/ethereum/eth_getStorageAt