MEV Memoirs: Into the Arena - Chapter 1, Part 2 🤖
Unearthing Alpha through Decompiled Contracts
So you’ve heard about the dark forest, now you want to unearth the secrets inside.
MEV bots are scampering around the forest floor and we want to decipher what those bots are doing under the hood.
To find these secrets we’re going to have the learn the tools & techniques related to decompiling contract bytecode. This article is going to run you through them and in the process decompile an unverified MEV bot contract.
This is Part 2 of a series detailing MevAlphaLeak’s first foray into the MEV arena. This article follows directly from Part 1 so if you haven’t read that yet I recomment you do so before continuing.
We left off in Part 1 having discovered the encoding of the callInfo variable within the ApeBot Contract and determined that 3 calls are made via the assembly code. These 3 calls were made to 2 unverified contracts.
Our only way to progress is to decompile these contracts to see whats going on.
ApeBot Contract Calldata
The image below shows the calldata for the original wfjizxua( ) call and how it is split up to be used as inputs for subsequent calls. This represents the majority of our findings from Part 1.
The 3 calls target the addresses 0xd380f7e1dc7408aa007744ed3af390f8a47f9b75 & 0xf4863028b093fdac9cf7fd67c0df6866ac3c7a60, both of which are unverified contracts.
Before we begin let’s quickly clarify what an unverified contract is. On Etherscan when you create a contract you have the ability to verify it by submitting the code that created the contract.
This enables users to see the solidity code that created the contract. An official project will always do this to be as transparent as possible. When a contract isn’t verified you will instead see this.
You will only have access to the bytecode of the contract which is stored on-chain. You will have no solidity code to reference. As such it can be very hard to understand what is going on within the contract.
Our task is to decompile the bytecode for the unverified contracts to understand what happened in the ApeBot transaction.
Tools & Techniques
Our investigation will use the following tools.
Online decompiler
There are a variety of other online decompilers
Cast cli using --debug mode on the transaction hash
Useful for identitfying decompiled variable values, calldata input etc.
cast run --debug --rpc-url $ETH_RPC_URL 0x1126aa5e5b648eebad1c88141e5142cf0a4082e6ccf9fed77d69a190c21724a3
Transaction visualisation
https://versatile.blocksecteam.com/tx/eth/0x0efe3832b85e610fc4ba815f2be3943036a1a1be33cbd8f378a20d20667c1b39
The best transaction visualisation tool I’ve come across
Etherscan
Easy access to parity traces, contract byte code, sub calls etc.
The first step in decompiling the bytecode is to use an online decompiler to get a rough outline of the code. Note whatever the decompiler gives us will likely not be 100% accurate. There are occasions where the decompiler will give you incorrect information. This is why you must cross reference with the opcode dubugger.
The first task is to run through the decompiled code line by line and attempt to associate values and meaning to any variables declared. Here’s a list of the things to watch out / do when running through the code,
If we see a variable that represents bytes 32 - 64 of the calldata, find those exact bytes and note it down in a comment next to that line so you know the actual value
If we see an address pop up in one of the variables, look it up on Etherscan to see if it’s a known address, does it have a verified contract etc.
If variables are being passed into verified contract functions we should be able to work out what those variables represent. Lookup it’s inputs in the docs / code.
Can we identify what each piece of calldata represents.
If a contract is called multiple times we should check a few transactions - sometimes the calldata won’t make sense till you look at multiple transcations.
What other contracts does the contract interact with, are they verified If they’re not we have repeat all these step on those contracts also.
Now let’s run through these tools & techniques one by one with the ApeBot example.
Decompiler - Dedaub
We’ll start with the decompiler, I’m using the Dedaub decompiler for this example. Below is a snippet of the decompiled contract 0xd380f7e1dc7408aa007744ed3af390f8a47f9b75 that is involved in Call [1] & Call [2].
The Dedaub decompiler uses a function signature lookup table to find any known functions that are called. We can see the address 0x8d12a197cb00d4747a1fe03395095ce2a5cc6819 has the “orderFills” & “balanceOf” function. Let’s have a closer look at it.
Turns out 0x8d12a197cb00d4747a1fe03395095ce2a5cc6819 is the EtherDelta contract. It’s a verified contract which means we can look at the code for each of these functions.
A huge part of understanding unverified contracts is understanding the entities that they interact with. As such we need to take a quick detour to get an overview of what EtherDelta is and how it works.
EtherDelta 2
Etherdelta 2 is an on-chain orderbook exchange, this differs from an “Automated Market Maker” exchange (AMM).
An on-chain orderbook exchange acts much like a off-chain orderbook exchange ie Binance. There are Makers & Takers rather than liquidity pools. A Maker order defines a asset to sell/buy for another asset at a certain price.
In EtherDelta’s case these “Maker” orders can be made on-chain via a transaction or by signing an order transaction on the EtherDelta site.
Note EtherDelta is no longer functional but I assume these signatures would have been stored in an off-chain DB somewhere and “Taker” orders could request the signature of the “Maker” order they want to take.
If a “Maker” order is made on-chain an event is emitted. I did a look-up of the event for the associated address with this ApeBot transaction on-chain and found nothing. As a result, we can infer that the “Maker” order involve in the ApeBot transaction was made via a signature.
Now we have a basic understanding of how EtherDelta works lets jump back to the code. Here’s the full decompiled contract for 0xd380f7e1dc7408aa007744ed3af390f8a47f9b75.
Let’s start with the first call (Call [1]) to to function signature 0x78789120..
Function 0x78789120 - Contract 0xd3…75 - Call [1]
Below is the decompiled code for function 0x78789120. Snippets of the the EtherDelta 2 contract have also been included along with the parity traces for the internal calls (orderFills & balanceOf). Let’s run through it piece by piece.
We start with a call to the orderFills function for address 0x8d12a197cb00d4747a1fe03395095ce2a5cc6819 which we know is the EtherDelta 2. OrderFills takes in varg4, varg5 and returns v0 & v1. v0 represents whether the call was successful and v1 the actual returned result. The require of v0 after this call verifies the call was successful. We’ll use the parity traces to determinie varg4 & varg5 but we can see they come from the calldata.
The Parity trace for this internal call, here we can see the input data (calldata) for this call
0x19774d43
Function Signature
000000000000000000000000aac971235706aa7b49dd3cc2e42a9695d2060da0
This is varg4 from above, it looks like an Ethereum Address, If we look it up on Etherscan we can see it is an EOA address
6ce7751bd5b9723aa3fa7e4fc4d97865cf912fce880a7797dc0a0da9508f31d9
This is varg5 from above, at the moment we don’t know what the value represents but since this is an input to a function we can look it up in the verified EtherDelta contract.
If we check the orderFills code in the EtherDelta contract we can see orderFills is a mapping of a mapping. A “User Account” is mapped to an “Order Hash” which maps to a uint. This uint represents how much of an order has been filled. Using this new information we can determine that 0xaac971235706aa7b49dd3cc2e42a9695d2060da0 is in fact the “Maker User Account Address” and that 0x6ce7751bd5b9723aa3fa7e4fc4d97865cf912fce880a7797dc0a0da9508f31d9 is actually the hash of an order on EtherDelta.
varg1 - v1, we know from the previous function that v1 represents the amount of an order that has been filled (Note this is in ETH terms). We can’t be sure yet but it’s likely that varg1 is the total order amount in ETH. The total order amount minus the filled amount would yield the remaining order amount. varg1 is equal to 0x00000000000000000000000000000000000000000000000000f0142dd1c52700 from our calldata and v1 can be found from the output of parity trace [ 2, 1, 0, 3, 0, 2, 1 ] (orderFills) which equals 0x0000000000000000000000000000000000000000000000000000000000000000
Call to EtherDelta 2 address on the balanceOf function, takes in varg2 & varg4. Again we’ll use the parity traces.
Parity trace for this internal call, calldata
0xf7888aec
Function signature
000000000000000000000000bb49a51ee5a66ca3a8cbe529379ba44ba67e6771
This is varg2, again it appears to be an Ethereum address, looking it up on Etherscan shows it’s the CST token address.
000000000000000000000000aac971235706aa7b49dd3cc2e42a9695d2060da0
This is varg4 which we have already determined is the Maker User Account Address.
The balanceOf function in the EtherDelta contract interacts with the tokens mapping. The function returns to us the balance of a specific token for a specific address. So we are checking the CST balance of the “Maker User Account”. Looking at the output from the Parity trace we can see that v5 is equal to 0x00000000000000000000000000000000000000000000005b95329a965952e83d
We can see some calculation, v5 * varg1 / varg3 = CST Balance * (Trade Amount in ETH / varg3 ). At this stage we are unsure of what varg3 represents. As a result it’s hard for us to determine what this is trying to calculate.
This is a good example of where the next call (Call [2]) will aid us in detremining this calculation. For now I’m going fill you in on what varg3 is but watch out for when we discover this piece of info in the next call. In reality we would of had to loop back to this item once we had the new info.
varg3 tells us the CST amount on the Maker order. The Maker is willing to swap X CST for Y ETH. varg3 is the X. The calculation above uses X & Y to get the trade ratio 1/2500 ETH/CST. This value is multipled by the Makers CST balance. Therefore the result of this calculation, v5, is the Makers CST balance in ETH terms using the trade ratio as the converstion rate.
v3 < v6 = 0xf0142dd1c52700 < 0xf0142dd1dcb4be checks whether the trade amount in ETH is less than the Makers balance in ETH. This ensures the Maker is good to fulfill the trade. In our case this returns True. We can see this If statement has no code inside of it, when looking through the opcodes you can see there is a JUMPI based on this result. It is likely if this condition is not meet the execution will revert. You can confirm this using foundry with --debug & --fork-block-number to create transactions that would explore this section of the code and look at the opcodes that are called in different scenarios.
v2 < varg6 = 0xf0142dd1c52700 < 0x05 checks whether the Maker trade amount in ETH is less than the amount we want to trade in ETH. Note that v2 refers to its definition on line 15 rather than line 21 another thing to watch out for with decompiled code. This check makes sense, it’s verifying the open trade amount it big enough to fill our order. In our case, this returns false so we jump to the else statement.
Line 27 is not called in the flow of the ApeBot transaction however it is interesting to note that byte 2 of calldata item [0] in our case 0x4b appears to be some custom encoding. It is used when the balance of the maker trade amount is less than the amount we would like to trade.
v2 is set to 4200, unsure of what this value represents. The returned value isn’t used in any subsequent calls of the ApeBot transaction.
Returns 2 values. The 2 v2’s again represent different values, we can always verify returned values via the parity traces.
v2, v2 = 0x05, 0x1068
This shows us this first call (Call [1]) is ultimately about checking the validity of the trade ie is it possible.
A number of the calldata items weren’t used in this function call or were used without context ie varg3. As such we weren’t able to determine their meaning.
Let’s now look at Call [2] which calls the same contract 0xd380f7e1dc7408aa007744ed3af390f8a47f9b75 and see if it can shed any light on these unknown pieces of calldata.
Function 0x401687f4 - Contract 0xd3…75 - Call [2]
Let’s quickly refer back to the calldata from Call [1] and Call [2] at the top of the article. You’ll notice they are the same. Below is the decompiled code for function 0x401687f4, let’s dive in.
The 0x401687f4 Function call another internal function 0x74 and passes in the value 4
v0 - v11 are declared using the calldata in 32 byte chunks all with a 4 byte offset. The offset comes from, varg0 = 4, which represents the 4 byte function signature in the calldata.
A check is done to see if there is a non-zero address in the v0 = calldata item [0]. Note the zero address 0x000…00 represents ETH in EtherDelta so this is effectively asking is the trade in ETH or an ERC20. In our case we have a zero address so we wouldn’t enter this chunk of code. Let’s have a look at what it does anyway. After we enter theres a check on the first byte of v0. If it is zero it indicates the token hasn’t been approved for this contract.
This section calls the ERC20 contract for the given token and checks if EtherDelta (0x8d1…19) has any allowance using the allowance( ) function. If it doesn’t it calls the approve( ) function on the ERC20 contract with the EtherDelta address. This allows EtherDelta to move your ERC20 token without any further approval.
Next it checks the balance of the ApeBot contract (Since its a delegate call address(this) is the parent caller) for the given ERC20 token by calling balanceOf on the token contract address. It then deposits the full amount (which came from balanceOf) into EtherDelta minus 1 uint of the token. This minus 1 is a gas saving optimisation for future calls. It’s cheaper to change a slot from a non-zero value to another non-zero value than it is to change from a zero value to a non-zero value.
Now we’re back to the flow the ApeBot transaction took, this also does an EtherDelta deposit however uses v11 (calldata [11]) = 0x05 as the amount to deposit (5 Wei).
The EtherDelta trade function, I’ve included a snippet from the verified contract to show you the function inputs. A lot of the inputs we use are from our calldata, this gives us a chance to work out what each piece of calldata represents. Below is the list of inputs.
TokenGet (calldata [0]) = This is from the makers perspective so the token they would like to get is ETH.
AmountGet (calldata [1]) = The amount the maker would like to recieve in ETH for the full trade.
TokenGive (calldata [2]) = Again this is from the makers perspective so the token they would like to give is CST.
AmountGive (calldata [3]) = The amount the maker would like to give in CST for the full trade.
At this stage we can work out what the trade value was by looking at the ratio of CST/ETH = 0x5b95329a8d5d289800/0xf0142dd1c52700 = 1689404535900000000000/67576181436000000 = 2500 CST → 1 ETH
This is when we could refer back to varg3 in Call [1] to determine what the calculation was doing
Expires (calldata [4]) = The block number the order expires in. After this block number, the order can no longer trade.
Nonce (calldata [5]) = Used by EtherDelta, the nonce is a number you can include with your order to make it relatively unique. This way, if you want to place two otherwise identical orders, they won't have the same hash.
User (calldata [6]) = Maker’s Ethereum address
V (calldata [7]) = ECDSA signature component - used to verify the proposed trade is legitimate ie has been signed by the Maker
R (calldata [8]) = ECDSA signature component - used to verify the proposed trade is legitimate ie has been signed by the Maker
S (calldata [9]) = ECDSA signature component - used to verify the proposed trade is legitimate ie has been signed by the Maker
Amount = Amount the taker would like to trade, note the amount is in amountGet terms which in our case is ETH. The value here is 0x03 which is different from our 0x05 calldata. You’ll notice 0xde0b6b3a7640000/0xdeb5f2f95b78000 = 1000000000000000000/1003000000000000000 = ~0.997 the taker fee for a trade is 0.3%. The calculation above takes your trade input and multiples it by 0.997 so you have enough left over to pay the 0.3% taker fee. 5 Wei * 0.997 = 4.985 but Ethereum doesn’t have floating point numbers so we round down to 4. We then minus one to give us 3 Wei (The minus one is again is related to gas savings).
Finally after the trade is made we withdraw our funds. WithdrawToken( ) is used for ERC20 withdraws while withdraw( ) is used for ETH. Since v2 is the CST address we’ll use WithdrawToken( ) again there is a minus one for gas savings.
Now we have gone through both Call [1] and Call [2] we are able to determine what the calldata to the original call represents. Below is that calldata for Call [1] & [2] along with what each value represents.
This concludes the EtherDelta leg of the trade let’s recap what happened.
EtherDelta Leg
0xd380f7e1dc7408aa007744ed3af390f8a47f9b75 - Call [1]
Check to make sure the trade hasn’t been filled
0 ETH of the 0.067576181436 ETH trade had been filled
Therefore 0.067576181436 ETH of the trade still available
Check to make sure that the Maker address has sufficent tokens in their EtherDelta balance to make the trade (in ETH terms)
Maker (0xaac971235706aa7b49dd3cc2e42a9695d2060da0) had a balance of 1689.404535938590369853 CST
Convert to ETH terms base on trade ratio (1:2500) yields 0.067576181437543614 ETH
Open trade in ETH < Maker Balance in ETH
0.067576181436 ETH < 0.067576181437543614 ETH = True
Therefore Maker is has enough funds to cover the trade
Check to make sure the proposed trade amount by the taker is less than the open Maker trade amount in ETH
Open trade in ETH < Proposed Taker trade in ETH
0.067576181436 ETH < 0.000000000000000005 ETH = False
Therefore we will be able to execute the trade for our Taker amount
0xd380f7e1dc7408aa007744ed3af390f8a47f9b75 - Call [2]
Check if the Taker trade is in an ERC20 token or ETH, for us it’s ETH
Desposit ETH into EtherDelta from the MevAlphaLeak contract for the trade
5 Wei deposited
Excute trade of ETH for CST
Proposed trade amount 5 Wei is used in a calculation to determine the amount put into the trade function
((5 * 1000000000000000000) / 1003000000000000000) - 1
Yields 3.9850448654, no floating point numbers so rounds down to 3
3 Wei is swapped for 0.000000000000075 CST
Taker fee is (0.000000000000000003 * 3000000000000000) / 1000000000000000000 this yields 9e-21
1 Wei = 1e-18 ETH, you cannot go lower than 1 Wei & there are no floating point numbers in the EVM so this rounds down to 0
Therefore no taker fee is paid
Check CST balance of ApeBot contract on EtherDelta post trade
Balance returns 0.000000000000075 CST
Withdraw that CST balance from EtherDelta to MevAlphaLeak contract for next leg of the arb
Withdraws 0.00000000000007499 CST
There we have it, I’ve taken you through the first 2 calls and shown you the tools & techniques. For those interested in a challenge see if you can decompile the final piece of the puzzle, Call [2] to 0xf4863028b093fdac9cf7fd67c0df6866ac3c7a60 which interacts with Uniswap V2. Here’s the decompiled contract.
Hope you enjoyed, now go unearth some alpha anon!
noxx
Twitter @noxx3xxon
good
glad if you do live on YT or twitch ...