Welcome to the Matrix of blockchain
How to get alerted *before* getting hacked and prevent it
Defi hacks alone have totaled $285M just since 2019. Let's take the Balancer hack for example. The hack was exploiting the fact that a pool with a deflationary token STA (Statera) was created. The pool accumulated a significant liquidity when it was eventually drained by the hack. Read my post on Balancer for more details here on Balancer itself.
Now the special thing about the STA token is that some percentage of the supply is burnt when you transfer it. Unfortunately Balancer didn't double-check the received balances and was assuming when you transfer amountA, you also receive amountA. When taking this to the extreme and repeating, one can create the situation where when trading with the pool, no STA has to be transferred for a swap. Now use a flash loan, repeat and profit.
This resulted in the following transaction:
Now you can see this was a single transaction which resulted in roughly $500K being lost.
Many tokens were transferred in a single transaction. But was there any way to prevent this?
Well this is what we'll talk about. Yes indeed this hack could have been stopped!
It's still very dark inside the mempool
You may remember the great blogpost about mempool manipulation on Black Thursday of Blocknative. If not, bookmark this now and read it. This issue is still very relevant. In fact one of the exploits used is still an open issue on Github and largely ignored. Since then we've heard many more stories about the mempool, sometimes nicknamed dark forest and how to escape it.
Illuminating this place is not easy. You can run a node like Geth and observe your local mempool. You can also see the mempool source code of Geth here. It gives you some ideas on the behavior. Or you could even create your own node and transaction discovery service with something like ethereumjs-devp2p. But you can already see that this is not only very difficult to do, requiring a lot of computing resources and time, but it's also incredibly difficult to do well.
A mempool is only existing of transactions that the nodes you connected to have seen and told you about. To get a full picture of the whole network's mempool therefore requires being connected to as many nodes as possible. The more nodes we are connected to, the earlier we will be notified of new mempool transactions. Now this is where Mempool Explorer from Blocknative comes to help.
The service of Blocknative goes beyond this:
- You could actually use the backend API behind this directly.
- Use the notify frontend lib to greatly improve your DAPP's user experience.
- Or simulate pending transactions. This is what we need for our hack preventation.
Entering the Matrix
Now how does the simulation feature work?
The simulations build upon the mempool service. In general dealing with internal transactions is difficult. Recall our Balancer hack transaction. In the advanced internal transactions tab, you can see the full monstrosity of transactions here. Now imagine you want to keep track of any calls to your smart contract off-chain. This is extremely difficult as you'd also have to find all internal transactions which you can only get by simulating the whole transaction.
Simulations by Blocknative basically take any pending transaction and run it in its their local EVM. They are run using the last known state of the blockchain (state trie). Since the state can change before the transaction is actually added to a block, the simulation is not always 100% accurate, so keep that in mind.
Also note that simulations currently only work for Ethereum mainnet.
Uniswap Example
A good way to visualize this is by looking at the Uniswap simulation examples. Create a Blocknative account and then go to this example query.
You can see an example result on the right. You can click the hash and it will direct you to Etherscan. Since this is a pending transaction, you might have to wait a few minutes for the transaction to be confirmed.
Once finished, you can see the actual token transferred by this Uniswap trade:
It likely won't match the Blocknative balanceChanges perfectly due to the constantly changing states in a Uniswap pool. You can see here instead of 10174 TRU tokens paid to the trader, it actually sent 10346 TRU. Close enough!
Simulations are challenging
The simulations are particularly useful for traders and protocols. They reveal all internal calls and its effects on the state. However, with the current node client implementations, this process can take substantially longer than the actual EVM execution. This time can often be the difference between doing a trade in time or preventing a hack.
To take it from Blocknative directly, the challenges for simulations include:
- Ensuring your node remains properly synced at the time of simulation.
- Capturing all pending transactions propagating through the mempool. Individual nodes frequently miss pending transactions, particularly during periods of network congestion.
- Detecting new pending transactions as rapidly as possible.
- Knowing which transactions are likely to be included in the next block – and thus are candidates to be simulated against the current block state.
- Performing the simulation quickly to maximize the time the simulation results are actionable.
- Interpreting the simulation to see how address balances are shifting.
Getting notified about the hack
Now how can we use this to prevent hacks?
Let's create a subscription for the Balancer STA pool.
- Now we can create a query. Also what about if the transaction is split over multiples? -
When you're finished, you have the option to add this to a webhook. Now you'll be alerted whenever such a transaction was found!
Of course creating these subscriptions can all be automated using the API. The 'Export Configuration' function can help you with the setup for this.
Let's prevent the hack...
Okay so now we are getting alerted about any hacks. What do we do now?
Well you have to respond quickly. And your contracts need to be designed to have some kind of emergency pause. One common pattern is to use the Openzeppelin Pausable contract. Make any function pausable except for some emergency withdrawal function. The emergency pause can only be called by a single private key. If you were to use Governance for this, you wouldn't be able to respond quickly enough. Now when the contracts are paused, users may only withdraw their funds in a safe way.
Awesome, we just prevented the hack. Get alerted about a big balance change, automatically call emergency pause and then let users safely withdraw funds.
Any downsides?
Well. There ain't no such thing as a free lunch. Of course we are giving up decentralization to some degree. Having the option to pause a contract by a single address goes against the spirit of decentralization. But there are ways to improve on this:
- Use this pausing ability only for the most critical parts of the protocol.
- Add some kind of auto-resume function when it makes sense. This way the owner can't pause indefinitely. For example the pause could be only for 14 days, giving users time to withdraw and then it automatically resumes.
And last but not least, add the option to remove the pausing feature later on when you feel the contracts are sufficiently battle-tested.
Solidity Developer