Matching fat fingers for crypto gains
31 October 2017Note: This blog post has been written(rather poorly) with comedic effect and doesn't represent my ambitions. For a summary of the real reason I got excited about this see the Finally section.
I got the crypto bug a few months ago when I saw a few digits replicated on peoples computers was trading billions worth of volume a day. I had ignored it for way too long, and it was time to see what it was about. I was astonished, there was so much ... everything. Things to learn, philosophy, value. It was all there, like an alternative dimension.
It didn't take long until I had gotten myself a little bit of Ethereum. Everyone was saying how solid the project was (every project in crypto is). Fast forward a month and I was substantially invested.
About this time, I started to look at alternative coins to see if I could be making a quick buck buying and selling them. ZRX (0xProject) had just had an ICO and was about to list on exchanges. Again, this project was an A++ solid project with a strong team so I just had to get in. The big exchanges bittrex, bitfinex etc. had strict listing requirements which took time so no ZRX there. I was desperately looking for a way to catch some of these coins but how?
Enter EtherDelta
EtherDelta was apparently a decentralised exchange. I was told that everything that I did on the exchange was being done on the blockchain and for this reason it was extremely slow and frustrating. "No worries" I thought, I'm an engineer, I can do this!
While my friend trying to get in was panicking over his 45 minute long transaction that was still pending, I loaded up with some Ether and stuck an order in there. A few hours later, I had my ZRX. Moon time baby!
While I was there, I noticed something odd:
Someone had bought 0.0007 ZRX for 10,000 ETH per ZRX. Why would someone do that I thought? What made me even more suspicious is that my friend (by this time panicking on some other pending transaction) had put in a sell order at 10x the market price and it had filled. Later I found out that the UI on EtherDelta was misleading and this was causing fat finger errors, in the example before due to the amount and price field being switched around.
How Lucky!?
I put as many high price orders as I could on the order book, I woke up every morning expecting a full bag of sweet Ξ (ETH) but nothing. What a dumb plan I thought to myself, I've never been lucky. I cancelled the Lamborghini test drive, maybe next time.
Instead of the money dreams, I started to focus on how PonziCoins are made using the Ethereum blockchain. A few weeks later, I was an expert. I was chaining those blocks together like it was nothing and you wouldn't have seen a smarter contract than the ones I was writing. Point being, I was now looking at blockchain systems and apps more critically than before.
The return of the EtherDelta
For one reason or another, I ended up on EtherDelta. There they are again! An incredible amount of money up for grabs on the top of the order book. Just as I thought about putting an order in to fulfill it, it was taken. Damnit! If only I was a few minutes faster.
The Opportunity
Experience had taught me that whenever speed was an issue, any automated solution would perform well. Therein lied the opportunity, if I could automate the detection and filling of these mistake orders, i was GAURANTEEED free ether.
I couldn't have been more naive. What followed was a week long episode of fear and stress with moments of joy.
How EtherDelta works
I previously mentioned that EtherDelta was a decentralised exchange. This was to my advantage as all the trading functionality was publicly visible on the Ethereum blockchain. Here I will described what I begun to understand of how it functions.
Smart Contracts
A consequence of the crypto world we are living in today and especially true for Ethereum is that unless your smart contracts are publicly auditable, they are no safer than a centralised solution. This is because the blockchain does not store the smart contract code that a developer has written, but rather the bytecode produced from the compilation. It is therefore very difficult to validate a smart contract's behavior without having access to 2 by-products of the smart contract compilation: source code and ABI.
ABI (Application Binary Interface) in Ethereum is a json structure that specifies the functions and properties that are available for a given smart contract. This allows applications intending to use these smart contracts to be able to construct the right data for interacting with a piece of functionality.
Luckily, since EtherDelta is an extremely commonly used and trusted smart contract on Ethereum, the developer has uploaded and verified their source code on Etherscan. You can read it here.
The trade function
What i had been looking for was the trade function:
function trade(address tokenGet,
uint amountGet,
address tokenGive,
uint amountGive,
uint expires,
uint nonce,
address user,
uint8 v,
bytes32 r,
bytes32 s,
uint amount) {
I'm sure you can guess what tokenGet, amountGet, tokenGive, amountGive, user and amount values would be but what about the rest?
- expires: The block number that the order expires at.
- nonce: A unique number so that the order can be differentiated with others (when all other parameters would be the same)
- v, r, s: These values are 3 parts of a SHA3 signed message.
But what does the code actually do?
bytes32 hash = sha256(this, tokenGet, amountGet, tokenGive, amountGive, expires, nonce);
if (!(
(orders[user][hash] || ecrecover(sha3("\x19Ethereum Signed Message:\n32", hash),v,r,s) == user) &&
block.number <= expires &&
safeAdd(orderFills[user][hash], amount) <= amountGet
)) throw;
Looking at the method in detail, we can see that it is first producing the hash of the order parameters and then verifying that the 3 part signed message was indeed for this order. This means that v, r, s parameters is the product of signing an order by the user that intended to book that order. By providing these parameters to the trade function, we are effectively filling that order - with the balances changing owners within the EtherDelta contract.
But where do we get the user's signed order details from?
The Order Book
It turns out that EtherDelta is not entirely decentralised. The order book is implemented as a HTTP web service. Creating and retrieving order details happens through this service. The obvious reason for this is that orders on chain would be slow to place, slow to retrieve and incur a cost.
Here's an example of the order book for a token. An order from the API looks like this:
{
"id":"ff9ea54b148c2de0fb720c2e1cdc6cf21ff8be1869943cf0d823304a4927d576_buy",
"amount":"5000000000000000000",
"price":"0.0035",
"tokenGet":"0xdd974d5c2e2928dea5f71b9825b8b646686bd200",
"amountGet":"5000000000000000000",
"tokenGive":"0x0000000000000000000000000000000000000000",
"amountGive":"17500000000000000",
"expires":"4419285",
"nonce":"1179821333",
"v":27,
"r":"0x76a077663706c4351ed699515d2046515b90683657ef89843f59fe311023d3e9",
"s":"0x376dce4a26b4f56e8af83086479b9db1c70440c7c10391068dfa536f72728c2c",
"user":"0x4ee8a997592D70Eb0FBe6436A1Bee38Bea908931",
"updated":"2017-10-22T16:38:03.428Z",
"availableVolume":"5000000000000000000",
"ethAvailableVolume":"5",
"availableVolumeBase":"17500000000000000",
"ethAvailableVolumeBase":"0.0175",
"amountFilled":null
}
Any of the parameters there look familiar? That's right, there's the v, r, s parameters which together compose a signature by the user confirming their intended order. We now have the order details which can be used to execute a trade through the EtherDelta smart contract.
The Heist
I had it all planned.
- Query the off-chain order book periodically.
- Check the top order on the buy side for any ridiculous amounts.
- Compose and sign a call to the trade function of the EtherDelta contract that would fulfill this order.
- Collect the ETH.
- Re book the lambo test drive.
I was very nervous. Was it going work? Surely not... at least not the first time?
Why wouldn't anyone else be doing this already?
Did i forget something...
Am I going to send all my ether to the void?
I hope this works.
The app was running and all I could do was wait. It was 1AM and I was starting to get very impatient.
And then it happened. An order was found and my account had sent a trade to the smart contract to fulfill it. I opened up the trade and it had already succeeded but did it actually work? I anxiously browsed to EtherDelta.
There it was
I had just made a trade that gave me 1 ETH virtually FREE
I couldn't stop there, I desperately wanted to know if the process was repeatable. I needed to scale this FAST, the next opportunity could be right around the corner. The next few hours were a blur, I had just a slither of energy left to bang out some more code and get some much needed sleep.
I had scaled the application in a way that would allow me specify configs of tokens that I wanted to watch for. A config looked like this:
{
"name": "KNC",
"tokenAddress": "0xdd974d5c2e2928dea5f71b9825b8b646686bd200",
"tradingAddress": "0x000000ZerosBecauseAnonymityAndSuch000000",
"maxTokens": 20,
"minPrice": 0.1,
"decimals": 18
}
I had configured the app to run on 5 different tokens using 5 different wallets to give me a higher chance of picking up an order. At this point I really HAD to sleep. What was I going to wake up to?
What. The. Flying. Mother. Of. Satoshi. Every trade while I was sleeping had failed.
Why? What is going on? Did I really know what I was doing? Over the following week, I went from challenge to challenge trying to turn this ETH making machine into a success, in the process learning most of what I know about Ethereum and blockchain technology today.
Who wouldn't want free ether?
I had guessed at the beginning that there were others really keen on filling these orders. What I was not aware of was how this impacted my strategy and to what extent.
Transactions, Blocks and Mining in Ethereum
To understand how our trade transaction is executed, we need to cover some base around how blocks are created and mined in Ethereum.
When transactions(balance transfer or smart contract execution) are sent, they are included within a pool of pending transactions by the miners. Every N seconds, the mining clients pick up transactions from the pool and create a block - a collection of transactions to start mining. In addition to the proof of work calculations that they do, they also execute the transactions within the block.
What is interesting about this process is that the mining clients may use whichever algorithm they wish to achieve this. Of particular interest for us is how the miners decide to order the execution. If we can be on the top for every block, then we can beat the competition in filling the order!
In Ethereum "gas price" is the amount of Ether per unit of execution that the miner collects from the sender for executing that transaction so it is only natural that miners will prioritise transactions with a higher gas price.
You might have now guessed what the issue was, gas price. Other users were competing to claim those orders by setting high gas prices, knowing that this will increase the chances of hitting the jackpot. Helpfully, Etherscan keeps a log of all blocks and their transactions, so matching this up I could see the enemy had paid a staggering 0.5 ETH to claim the order for 1.2 ETH of profit. A worthy opponent.
It's important to understand that this has the opportunity to go right as well as wrong! Just have a look at this ICO investor who paid $70,000 dollars in fees for a chance at picking up tokens and failed. In fact in the case of these mistake orders, I recorded in one instance 6 ETH being lost on a failed transaction.
Now having understood this, I had to alter the application to adjust it's gas price based on the potential profit. I took the baseline for this from my last failed attempt, expecting that the competition is using some sort of percentage based calculation (in this case about 40% of profit). I then upped this to 45%. I was truly playing with fire now.
Not being quite whale territory, I was extremely cautious in letting this application attempt more than an ETH worth of profit (or potentially 0.45 ETH of loss!). Finally the config had been changed to add feePercent and maxAmount fields:
{
"name": "KNC",
"tokenAddress": "0xdd974d5c2e2928dea5f71b9825b8b646686bd200",
"tradingAddress": "0x000000ZerosBecauseAnonymityAndSuch000000",
"maxTokens": 20,
"minPrice": 0.1,
"decimals": 18,
"feePercent": 0.45, <---
"maxAmount": 1.0 <---
}
Did it work? It worked, and then it didn't. Pause the celebrations, the next challenge coming up.
A block too late
What had happened this time around? To my devastation, the order had already been fulfilled in the previous block. I was a block too late! How on earth could that happen? Was there some secret that I didn't know about? My attention shifted towards the app. Whatever it was, it had to be related to the latency between an order being available on the order book and the trade transaction being received by miners.
- Order book: I had mentioned previously that the order book was implemented as a web service. Could it be that the app was slower in successfully retrieving these requests than competitors?
- Sending trade: Since this is a P2P network, could I be in a position that had a long distance to reach the miners?
Both of these issues had one solution - locating the application in multiple regions - but the way in which they improve things are quite interesting to look at from a technical perspective.
Order book latency
I had noticed that the order book API was being served using a Content Delivery Network (CDN) called CloudFlare. One of the important things that Cloudflare does is to keep secret the actual host server of the application to prevent against attacks. Given this, I couldn't figure out the actual location of the server to try to co-locate, so the closest I could achieve was to have the application running in multiple locations dotted across the globe. I also imagined that the cache would be invalidated at different intervals between the CDN nodes. This way, I would be increase my chances of hitting the freshest order book. Below are the CDN locations cloudflare claims to operate:
Based on the density of the nodes, the 4 nodes of the application were placed in the following regions:
- New York
- San Francisco
- Germany
- Singapore
Miners receiving transactions
Now is a good opportunity to explain how the app was actually sending a transaction. You might know that to send a transaction, you need to be running a node. To run a full node on an average computer can get quite tricky. To sidestep the whole issue, I had turned to a service called Infura. Infura has deployed and maintains infrastructure (including nodes) for Ethereum so that interacting with the blockchain can be as easy as making HTTP requests (or using a JS lib like Web3). If you had ever wondered how those extensions and sites like MetaMask and MyEtherWallet can work without syncing, this is it.
The question for me was, is this the most efficient way of sending a transaction? Is the latency of interacting with this web service an issue? Or does Infura do an even better job at distributing transactions. Unfortunately, I couldn't find any documentation around this so I sided with running my own nodes. Sorry Infura :(.
The way of interacting with Infura and a local Ethereum node is virtually identical since the interface to both is JSON RPC. All I had to do was setup a Partiy/Geth on each of the nodes the app was running on.
Further from what you can see on the map below (from ethernodes.org), the Ethereum node density plays very well with the location of our app nodes.
So you got your lambo?
That was the plan. However I realised that I'm not a big enough player to win this game. What does that mean? Let me explain.
As I slowly but surely figured out the quirks to guaranteeing my opportunity to bid for that order, the deciding factor came to how much are you willing to risk to win X amount. There were orders with upto 30 ETH of profit to be taken, but are you willing to put in risk half or more of that? This is effectively blind bidding and a constant out thinking game which I didn't have the time or nerves for. Just limiting myself to 1 ETH had given me dreams and nightmares. The day I woke up to see that I had lost a trade to the competition paying 75% of the profit amount as fee to secure it, was the day I decided I was done.
Who was the competition?
I remember one particular account that piqued my interest: 0x7aa6e2506316f6d613277d4f2f4e4e1dc55e528e. It flew right under the radar for a while when I was investigating who was winning these orders because it is not a wallet but a smart contract. Smart contract transactions do not show their final destinations (etherdelta) within the Etherscan UI, so I had missed this for a while. Until one day I had doubts why for any other reason I was not winning the trade so decided to dig deeper. Had a look at the events that the smart contract was emitting (this is logged in etherscan) and lo and behold! That looks like the set of parameters for etherdelta!
This person was sending a transaction to their custom smart contract that would in turn invoke the trade function of etherdelta. There must be a special reason for this, which is probably leading to a great success rate as can be seen from the history of transactions. I have only guesses:
- The smart contract checks before invoking etherdelta that the trade has not been taken, effectively getting around the problem of sending a transaction to etherdelta when it may already be filled. Returning a success from the contract means that it only consumes a small amount of gas required to do the initial check and the sender keeps the rest. This is contrast to letting etherdelta error out on a failed transaction - consuming all the gas (and its cost).
- The smart contract can somehow alter the gas price to win at a given block. From my research, I couldn't find anything that would allow such a thing.
I'm sure the net profit here is minimum at the scale of 100s of ETH. One account which was a little less successful though is 0x2af5c32c76ddd5be71c1e3f7d06d0b1a2d204cf2. You can have a look at the record of transaction fees and eye out the number that have failed vs succeeded. Having said that, must still be profitable as it's showing no signs of stopping.
I want a lambo!
Sure, if you have some time and money on your hands, this is a great great great opportunity! Here's some suggestions:
- Create a simple dashboard to track the transaction fees relative to profit being paid for these orders. This visibility will let you tune your strategy for greater success.
- Strike a deal with the creator of EtherDelta to push you updates to orders a short while before updating it publicly.
- Collude with miners so that they front run your transactions. Remember it's all up to them in what order they process it!
- Bonus Tip: With the Byzantium update, errors thrown in smart contracts now return the fee for any unused gas! Less fee, more profit!
You are a HORRIBLE PERSON! Give me my ETH back!
Sorry :( I lost most of the time actually. The main culprit has over 8500 transactions, If you're looking for your ETH back, you'll find it with them.
Finally
I just wanted to highlight what this experience means to me and give you the whole 'Blockchain is the future' spiel. Due to the open nature of value systems in the new blockchain world, people are more than ever going to be capable of taking control of things around them. I have worked at an investment bank and never had the opportunity to actually invest. Yet in a month, I had full control of assets that didn't exist the previous month, I used a trading platform and I created my own trading bot. I am hoping that the same evolution is going to happen at some point in the future to every other value system.
Interested in the code? Looking for an answer to something? Let me know.