By Mark Carney
The world of Miner Extractable Value, or as they are now known as Maximal Extractable Value bots (MEV) is a fascinating one. It’s a blend of tech, sniping, person in the middle and criminality and whilst there have been many creating MEV bots, the most effective was one called 0xbaDc0dEfAfCF6d4239BDF0b66da4D7Bd36fCF05A
MEV is a dynamic that allows miners to maximize their profit by determining the order of transactions on a blockchain network to their advantage. This includes arbitrarily reordering, including, or excluding transactions within a block at the expense of users.
Mempool democratisation has allowed traders more opportunities to extract value by influencing what goes into a block. MEV Searchers can easily affect which transactions are included and excluded, or even change the order of transactions in a block, by seeing what’s already in the mempool and then timing and pricing their transactions accordingly.
In essence, it acts in a similar way to a Person-in-the-middle attack, or more precisely, a Bot-in-the-middle attack.
Imagine you are at the movies and in line to buy tickets to the latest Tom Cruise film. You’ve got a crisp £20 note and you are waving it around, telling everyone how excited you are to see this movie.
Uglyb0t hears this and joins the line, but he doesn’t got to the back, he pays someone in front of you to take their place.
As Uglyb0t gets to the teller, it seems there are only 2 tickets left at a cost of £10 each and he buys both.
He then turns around and offers to sell you one of those tickets for your £20, knowing full well that’s what you have and wanted to spend.
You, super keen to see handsome Tom, happily pay the price and smile like mad, because you’ve just got a good deal.
The reason this could happen is due to the way that Ethereum contracts handle money held in those contracts.
As Dan Robinson and Georgios Konstantopoulos write in Ethereum is a Dark Forest, many contracts’ burn()
functions allow money lying in the contract’s balance (in their example, sent to a contract by accident) to be drained and sent to a wallet of the caller’s choosing (presumably, their own).
They describe the behaviour of these “arbitrage bots” as predatory on hanging value left on contracts - once the bots see anything left, they then frontrun the access to it and then maximise its value in a sequence of exchanges. They do this by using a predefined algorithm, and then copy the transaction replacing the addresses with their own.
0xbaDc0dE worked very well in this regard and made a considerable sum of WETH as a result (1100) and did so by front running traders in order to exploit slippage, which is a huge headache for traders where there is a price difference between a quote price and paid cost.
Gordon Gekko in the classic 80s “Wall Street” movie gave an insightful speech where he said, “Greed, for lack of a better word, is good.”
Well, not always and 0xbaDc0dE got too greedy. They attempted to exploit every opportunity when detected.
Karma can be a cruel mistress and indeed with 0xbaDc0dE, she hit them pretty hard, to the tune of 1101 ETH. The hack itself is fascinating and Mark and I delved deep into this as with all things Smart Contracts, you need to be 100% sure your code is perfect before you deploy it, because if there’s a single flaw, you can bet it will be exploited.
Up until now, 0xbaDc0dE was earning around 200k USD but it detected a user trying to sell 1.8 million worth of Compound USD Coin Price (CUSDC) on Uniswap V2. However, there was only $500 liquidity available!
Sadly, they ended up with just the $500 in return and this in itself created a massive arb (arbitrage) opportunity as so much was left in the contract. Arbitrage is the process of simultaneously buying and selling assets, usually on different markets, to profit from price differences.
0xbaDc0dE moved fast and performed an arb in the mempool by touching many protocols, this made them a lovely 800ETH.
Flashloans are when you have an uncollateralized loan without borrowing limits in which a user can borrow funds and returns them in the same transaction. If the user cannot repay the loan before the transaction is completed, a smart contract cancels the transaction and returns the money to the lender.
In the case of 0xbaDc0dE, dYdX ended up calling a function called ‘callFunction’, an internal call that looks like:
ICallee(args.callee).callFunction(
msg.sender,
args.account,
args.data
);
If everything is fine, it will execute this and invole the callFunction from 0xbaDc0dE’s smart contract.
Thing is, 0xbaDc0dE didn’t check their code and that allowed for arbitrary execution.
Clearly some hackers have an issue with MEVs. So much so that one looks to have found a bug in this particular bot’s code, and managed to leverage the entire balance very quickly! Let’s have a look at what happened by decompiling the various contracts and analysing the data sent in some transactions.
On Sept 27th 2022 at 02:59:11pm UTC, a little over 1,101 WETH (wrapped ETH) was transferred from an MEV contract 0xbadc0defafcf6d4239bdf0b66da4d7bd36fcf05a
to a wallet 0xb9f78307ded12112c1f09c16009e03ef4ef16612
. This transfer at the time represented just over $1.5M of value being transferred in a transaction that cost around $2.29 in fees. Let’s explore how this hack took place.
The attacker (0xb9f78307ded12112c1f09c16009e03ef4ef16612
) has a contract at 0x6554ff0f2b6613bb2baa9a45788ad8574a805f6d
. Aside from the contract creation transaction, there are only two other transactions to execute the attack.
Let’s look at the second of the two to get a sense of what the first one did. The transaction hash is 0x631d206d49b930029197e5e57bbbb9a4da2eb00993560c77104cd9f4ae2d1a98
. Looking at Etherscan, there’s not much to see about what really happened…
Looking at the data submitted to the contract also fails to really show what is going on:
0xcfdb5486
000000000000000000000000badc0defafcf6d4239bdf0b66da4d7bd36fcf05a
000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
So we see the method call to 0xcfdb5486
, but if we look at a 4byte directory we don’t find anything either. So whilst we don’t know what this function does, we do know that it took two arguments:
0xbadc0defafcf6d4239bdf0b66da4d7bd36fcf05a
- this is the address of the MEV contract.0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
- this is the address of the master WETH contract.So we are stuck again. But if we look at the execution flow we will see a lot more about what happened:
If we decompile the relevant part of the contract we see the following:
def unknowncfdb5486(uint256 _param1, uint256 _param2) payable:
require calldata.size - 4 >= 64
require _param1 == addr(_param1)
require _param2 == addr(_param2)
require 0xb9f78307ded12112c1f09c16009e03ef4ef16612 == caller
require ext_code.size(addr(_param2))
static call addr(_param2).balanceOf(address tokenOwner) with:
gas gas_remaining wei
args addr(_param1)
if not ext_call.success:
revert with ext_call.return_data[0 len return_data.size]
require return_data.size >= 32
require ext_code.size(addr(_param2))
call addr(_param2).transferFrom(address from, address to, uint256 tokens) with:
gas gas_remaining wei
args addr(_param1), 0xb9f78307ded12112c1f09c16009e03ef4ef16612, ext_call.return_data[0]
if not ext_call.success:
revert with ext_call.return_data[0 len return_data.size]
require return_data.size >= 32
require ext_call.return_data == bool(ext_call.return_data[0])
We won’t go through the whole thing, but just note that after some checks there are two calls - one to balanceOf
to get the balance of a tokenOwner, and one to transferFrom
, which does pretty much what you would expect. So the attacker was able to look at their own balance on the actual contract being used by the MEV bot and then just… transfer all the WETH out. All 1,101.65 of it. O.o
Clearly this was done by more than just sending two addresses to a function. Two things have happened; first, the hacker has managed to leverage some control over the MEV contract, and then second, they utilised this control to move all of the WETH to be on their own balance. Now we have a better idea what we’re looking for, let’s turn our attention to the first transaction.
So the real question turns out to be - how can you, not the contract owner, manage to empty a contract holding 1,101.65 WETH? Well, let’s hav a look at the previous transaction with this contract. It’s quite the ride!
First up, let’s do the same analysis - if we look at the decompilation of the attacker’s contract, there are two functions - 0xcfdb5486
which we analysed above, and execute(address,bytes)
.
The decompilation shows around 100 lines of ‘memory juggling’ code - clearly some nefarious payload is being crafted. But what does it do?
Well, we can follow the call at the end of the contract:
call 0x1e0447b19bb6ecfdae1e4ae1694b0c3659614e4e.mem[mem[64] len 4] with:
gas gas_remaining wei
args mem[mem[64] + 4 len t + -mem[64] - 4]
So we see that a call is made to 0x1e0447b19bb6ecfdae1e4ae1694b0c3659614e4e
- the SoloMargin contract from dXdY trading. Indeed, they publish the code in the Etherscan link, so we can see what functions are being accessed.
We first need to see what function is being called - and that’s the last function to be referenced at the end of the memory block that execute
takes some time assembling. For those playing along at home, line 73 of the decompilation of 0x655...
shows the 4byte signature for this function as being 0xa67a6a45
, which decodes to
So what does operate
do? It’s a complicated function, but the description in the source code is:
* The main entry-point to Solo that allows users and contracts to manage accounts.
* Take one or more actions on one or more accounts. The msg.sender must be the owner or
* operator of all accounts except for those being liquidated, vaporized, or traded with.
* One call to operate() is considered a singular "operation". Account collateralization is
* ensured only after the completion of the entire operation.
So operate()
is the function used to administer the managed contracts, of which our MEV contract is one - we can see this in its profile. If we follow the code, we see that after some checks, operate()
makes a call to _runActions
, which after a few more checks (none of which are user authentication checks, btw - this is all just parameter checking) makes a call to _call()
which then returns a callback to callFunction
in the managed contract (in this case, the MEV) which we can see in the flow of the transaction (look at the fourth line of the trace):
So clearly the MEV contract’s callFunction
wasn’t very well protected. But what did the hacker have the function do? Let’s look at the data blob sent in the transaction:
Function: execute(address _target, bytes _data) ***
MethodID: 0x1cff79cd
[0]: 000000000000000000000000badc0defafcf6d4239bdf0b66da4d7bd36fcf05a
[1]: 0000000000000000000000000000000000000000000000000000000000000040
[2]: 0000000000000000000000000000000000000000000000000000000000000320
[3]: 0000000000000000000000000000000000000000000000000000000000000003
[4]: 0000000000000000000000001e0447b19bb6ecfdae1e4ae1694b0c3659614e4e
[5]: 0000000000000000000000000000000000000000000000000000000000000000
[6]: 0000000000000000000000000000000000000000000000000000000000000000
[7]: 0000000000000000000000000000000000000000000000000000000000000000
[8]: 00000000000000000000000000000000000000000000000000000000000000e0
[9]: 000000000000000000000000000000000000000000000beff1ceef246ef7bd1f
[10]: 0000000000000000000000000000000000000000000000000000000000000001
[11]: 0000000000000000000000000000000000000000000000000000000000000020
[12]: 0000000000000000000000000000000000000000000000000000000000000000
[13]: 0000000000000000000000000000000000000000000000000000000000000000
[14]: 0000000000000000000000006554ff0f2b6613bb2baa9a45788ad8574a805f6d
[15]: 000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
[16]: 00000000000000000000000000000000000000000000000000000000000000a0
[17]: 0000000000000000000000006554ff0f2b6613bb2baa9a45788ad8574a805f6d
[18]: 0000000000000000000000000000000000000000000000000000000000000040
[19]: 00000000000000000000000000000000000000000000000000000000000000a0
[20]: 0000000000000000000000000000000000000000000000000000000000000004
[21]: 4798ce5b00000000000000000000000000000000000000000000000000000000
[22]: 0000000000000000000000000000000000000000000000000000000000000002
[23]: 0000000000000000000000000000000000000000000000000000000000000004
[24]: 0000000000000000000000000000000000000000000000000000000000000001
[25]: 0000000000000000000000000000000000000000000000000000000000000001
[26]: 0000000000000000000000000000000000000000000000000000000000000002
[27]: 0000000000000000000000000000000000000000000000000000000000000002
The first address is our target (the contract that execute
targets), the next is the manager of that contract, 0x1e0447...
, then there is some more data. What stands out is the [21]
parameter, 4798ce5b
. As a function the 4byte decoding is given as
exchange(address,address,address,uint256,uint256)
This function is called a lot in other requests made to this contract, and this is understandable given what it does is enable arbitrage. So when this call is made to the MEV contract, a fallback loop is hit where the function is called to the parent contract, but the checks are not in full alignment. In short, anyone could trigger this execution loop and claim an exchange for themselves!
And so with this crafted payload, the attacker was able to get the SoloMargin dXdY contract to execute an exchange on the MEV bot contract and place the attacker’s address as the one with all the ethereum.
As much many hate upon all things Web3, we find it truly fascinating as the technical level of attacks is something else, as the above hopefully has shown. As with all things Smart Contracts, you really need to be 100% sure they are secure before deploying to the blockchain as otherwise they will be exploited and often you end up loosing a lot of cash.