My ERC-777 smart contract (Schnoodle, symbol SNOOD) was attacked yesterday resulting in the entire liquidity in the UniswapV2Pair token being drained (104 ETH). The attack was by way of an attacker contract that performed a series of interactions with the liquidity token and my smart contract during its creation (txn here).
By clicking the 👁 icon on the list of internal transactions, I can see the following input/output data:
Input:
0x022c0d9f000000000000000000000000000000000000000000000005a3f13b802bf25f800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180ea08644b123d8a3f0eccf2a3b45a5820755380000000000000000000000000000000000000000000000000000000000000080
Input:
0x0902f1ac
Output:
0x000000000000000000000000000000000000000000000005a3f13b802bf25f8100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000062ad7934
Input:
0xa9059cbb0000000000000000000000000f6b0960d2569f505126341085ed7f0342b67dae0000000000000000000000000000000000000000686558904b462764f122c4e1
Input:
0xfff6cae9
Input:
0x23b872dd0000000000000000000000000f6b0960d2569f505126341085ed7f0342b67dae000000000000000000000000273521f6582076a5fc54da9af9bfca5435ffe9ec0000000000000000000000000000000000000000686558904b462764f122c4e1
Input:
0x70a082310000000000000000000000000f6b0960d2569f505126341085ed7f0342b67dae
Output:
0x0000000000000000000000000000000000000000686558904b462764f122c4e2
I've deciphered this, and it translates to these function calls:
- 1. UniswapV2Pair.swap(amount0Out, amount1Out, to)
- amount0Out: 104047009087796436864 or 104.xxx...4 ETH
- amount1Out: 0
- to: 0x180ea08644b123d8a3f0eccf2a3b45a582075538 (message sender)
- 2. UniswapV2Pair.getReserves()
- OUTPUT
- reserve0: 104047009087796436865 or 104.xxx...5 ETH
- reserve1: 1 or 1E-18 SNOOD
- blockTimestampLast: 1655535924
- OUTPUT
- 3. Schnoodle.transfer(recipient, amount)
- recipient: 0xf6b0960d2569f505126341085ed7f0342b67dae (liquidity token)
- amount: 32308960759206669952686933217 or 32.3b SNOOD
- 4. UniswapV2Pair.sync
- 5. Schnoodle.transferFrom(holder, recipient, amount)
- holder: 0xf6b0960d2569f505126341085ed7f0342b67dae (liquidity token)
- recipient: 0x273521f6582076a5fc54da9af9bfca5435ffe9ec (attacker contract)
- amount: 32308960759206669952686933217 or 32.3b SNOOD
- 6. Schnoodle.balanceOf(account)
- account: 0xf6b0960d2569f505126341085ed7f0342b67dae (liquidity token)
- OUTPUT
- 32308960759206669952686933217 or 32.3b SNOOD
They all happened at the same second, so they're not listed in the correct order on Etherscan. But it's clear that the output of #2 (getReserves
) is used as parameter amount0Out
of #1 (swap
) albeit with 1 subtracted which is interesting. And the output of #6 (balanceOf
) is used as parameter amount
of #3 and #5 (transfer
and transferFrom
).
So, it seems that the attacker contract gets the SNOOD balance of the liquidity token, and the amount of ETH in the liquidity token reserves, then does the following at the same time:
transferFrom
to transfer all SNOOD from the liquidity token to the attacker contracttransfer
to transfer all SNOOD from the message sender to the liquidity tokenswap
to swap SNOOD for 104 ETH to the message sender
At no point does the message sender receive any SNOOD. Yet it is able to transfer SNOOD to the liquidity token. I am unable to figure this out further. I decompiled the byte code using this decompiler and the attacker contract address (0x273521F6582076a5FC54da9Af9bFca5435ffE9eC), but it's outside of my comfort zone. Could this be a reentrancy attack on the SNOOD or the UniswapV2Pair contract?
I've done some research, and I found that there was previously a reentrancy vulnerability for ERC-777 tokens using Uniswap v1. See here which links to the exploit example on GitHub, and also the Uniswap whitepaper which indicates that the vulnerability was patched in Uniswap v2.
What I would like to know is, how did this attack take place, where is the vulnerability? And how do I protect against it? If this is an vulnerability in the SNOOD code, then how do I fix it? If it's a vulnerability in the UniswapV2Pair code related to ERC-777 tokens which is what SNOOD is, then I guess I would have to change my contract to ERC-20, or use UniswapV3Pair, or another DEX altogether.
There is also a $1500 bounty (negotiable) available for solving this vulnerability which can be viewed here via our Dework platform.