Building a MelonBot
The Melon Protocol has been designed to automate and simplify the boring parts of asset management. For about $50 in Ethereum gas, you can spin up a fund that will perform all of its own accounting functions, manage investments and redemptions, and enforce compliance mandates programatically. And since all of these actions are performed on chain, it’s 100% transparent and able to be audited. As a manger you can build an immutable track record to show your prowess, and as an investor you can have full confidence in the fund where you’re parking your assets.
The fun part of an asset manager’s job, managing risk, has remained largely manual on Melon to date. To the best of our knowledge, trades have exclusively been made via the Melon Terminal. However, with the recent release of MelonJS, it’s possible to automate trading strategies that run in the background, without having to manually enter and execute trades.
This series of blog posts will be an evolving document, starting with outlining the most basic functions that a trading bot requires before moving on to deployment and potentially strategy.
To start, I’ll make the assumption that we’re building a very basic
if this then that bot. In other words, it will monitor the markets for a certain condition (
this), and when that condition is met, adjust the user's portfolio according to whatever rules the trader has put in place (
this portion of this bot will be up to you. My example will be simple, bordering on stupid. It will only trade two tokens - WETH and MLN - and will be 100% allocated to one or the other at all times. It will make decisions about when and what to trade based on a pseudo-random condition. It will likely do better than my current Melon Fund.
I will, however, delve into the
that portion by touching on the following topics in depth:
- How to check your Melon fund’s current balances
- How to check asset prices on Uniswap
- How to trade on Uniswap
First we’ll start a project and install the necessary dependencies. For the three methods above, we’ll need a few
web3 packages and
MelonJS. Use yarn or npm to install
web3-utils. Additionally, I'm using
bignumber.js to handle all numbers and token math, and
dotenv-extended to access environment variables. I'm also writing this in TypeScript, so I added that as a dev dependency. When you're done, your
package.json should look something like this:
We’ll also need the latest protocol deployment configurations to track various contract addresses. These configuration files are available in
JSON form in various Melon repositories. I took them from here since the Melon Terminal is very actively maintained and is highly likely to be the most current version. I added a couple of files to the tree to hold our various classes, methods, and utilities, so my directory now looks like this:
I’ve taken the liberty of appropriating some utility methods that show up throughout the Melon and Avantgarde codebases. Two work with token math and the other queries ethgasstation.io to give us an appropriate gas price. I also stuck the function to create an environment (discussed below) in that folder.
Wallet, Account, and Environment Setup
Our final chore before we get to the fun part is to discuss private key management. In most cases, a user signs transactions using a browser extension or hardware wallet. In the case of this bot, we will automate the process of signing a transaction, so we need access to the wallet directly. This can be achieved through using the private key, or an encrypted
JSON wallet file and password.
For simplicity’s sake, I’ll be storing mine as an environment variable and using
dotenv-extended to access it. I have already added
.env files to my
.gitignore so there's no chance of accidentally pushing this to a hosted repo with keys intact for the world to see. There are a bunch of different ways of managing this interaction, and it's worth researching how you want to handle it. If you take one thing away from this paragraph, it should be:
Do not hardcode your private keys and do not store your private keys on Github.
I know that’s two things. But they’re both important. Cover your ass here.
Environment variable is passed to their constructor method in order to provide the class with context about the network within which it is operating and the specific Ethereum account that's accessing it. We'll write a couple of methods to correctly configure that
Environment variable, along with your Ethereum wallet information, in order to successfully use the Melon contracts exported by MelonJS. I’ve stuck those methods in the
util directory, and they look like this:
The first step is to create a function that returns a
provider, which is a
web3 object that represents a connection to an Ethereum node of your choosing. We call that function and pass the returned provider into the constructor for a new
Eth class which we'll call
Eth class is exported from web3. We then create an account linked to our private key (which remember, I've stored as an environment variable), and add that to our client. Note that as I mentioned above, this key must be to the account with which you've created the Melon fund for which this bot will trade.
With that, you’ve now proven that you are the owner of the account in question. Create a new
environment using the newly instantiated client and the deployment
JSON I described above.
The Bot Class
Next, we’ll create a class that will contain all the methods our bot will need to run, as well as hold the variables necessary to execute them in state. More explicitly, using that state in combination with those methods, our bot should be able to:
- Query a fund’s token balances
- Query a token’s price denominated in WETH or the price of WETH denominated in that token on Uniswap
- Trade on Uniswap to adjust the fund’s balances of two distinct tokens tokens (in this case WETH and MLN)
It’s worth noting that I’m taking the simplest route possible here. Though you can trade any one token for another on the current Uniswap release, in order to generate a token to token price, you need to go through WETH. In other words, you need to make two calls (Token 1 to WETH, WETH to Token 2) and subsequently interpolate how many of Token 1 get you a Token 2. I’m just going to trade WETH for MLN and back again. However, if you’d like to see this complexity handled in a fairly seamless manner, check out the Melon Terminal code here.
Throughout the code blocks below, I’ll be referencing class properties. That being the case, I’ll start out my discussion with a section on the parameters that we need to pass to the class’ constructor. The repo contains a fully-typed example class.
hubAddress and the the symbols of the two tokens you want the bot to trade.
Note also, we’ll need to import and call the
createEnvironment method when the class is created.
We’ll start by using the
Accounting contract exported by MelonJS to see what your fund is currently holding.
FundHolding is a type that's also exported from MelonJS. It is an object with the size of the holding and the token's address.
We’ll use various contract classes related to Uniswap to query the current prices of the fund holdings that we just returned and then eventually to trade into and out of positions.
To check a price, the order of operations is as follows:
- figure out which token you’re trading against WETH (the base currency is that which is leaving your inventory, the quote currency is what you’re buying)
- find the address of the Uniswap exchange contract for that token using the
UniswapFactorywe instantiated above
- create a contract object for that exchange using
- call a function on that contract to return the rate (in the base currency) to trade an amount (of that base currency).
In our case, we’re going to interpolate the size and rate for the quote currency side of the equation as well, since trading on Uniswap requires a parameters object with some of that information. The method ends up looking something like this:
MelonJS imports in that code block include
Creating a trade transaction is executed in a similar fashion. We’ll take the
uniswapTradingAdapter that we instantiated with the class and call the
takeOrder method on it. We’ll pass that method the manager’s address and an object of order arguments that we’ll construct from the result of the price query.
Worth noting here — I built a slippage variable into this method because if you pass an amount greater than is currently available on Uniswap, the transaction will be rejected. If you pass an amount that’s less than what’s currently available, Uniswap will execute at the best available price. I ballparked 3%; in reality my test trades have been so small that I’ve gotten almost the entire size quoted on every trade I’ve done. Slippage in the liquidity pools is similar to slippage in traditional exchanges — the bigger the size you’re trying to trade, the more there’s likely to be. This will be something you’ll have to optimize on your own.
Our trade function here returns a Promise of a Transaction. I’ve left it up to my execution script to handle that promise’s resolution.
Pulling it All Together
So far, we have a class that contains methods to check your funds token balances, check the prices of those tokens on Uniswap, and trade those tokens on Uniswap. What’s missing is the secret sauce that you’ll provide to inject some logic into the bot. How should it trade and when? It’s easy to envision a method that checks balances, then checks prices, then runs those prices through some sort of indicator algorithm, then trades if the indicator comes back positive. For me, that magic function looks like this:
I don’t have any of that secret sauce for you. This is where you can get creative — use your best TA or on-chain metrics or other voodoo. As I mentioned above, my strategy will be to trade at random. That being the case, I’ve written a script that calls itself recursively every 15 minutes, generates a random number between 0 and 1 and trades if that number is greater than .50. If it does trade, the script handles the transaction execution. There are a multitude of edge and corner cases that will make this implementation fail, many related to ethereum transaction logic.
It’s worth digging into web3’s promievents to think about how you’ll handle any errors that occur, and that seems like it could be a good topic for me to cover in a future blog post. Other topics on my list include:
- managing more complex state and persisting it in a database
- implementing an actual strategy using that complex state and various market metrics
- checking all available trading venues and executing at the best possible rate
- doing the devops work to run this bot indefinitely
As it stands, my bot is operational and burning through gas, trading on my behalf. When I run it in my terminal, it looks like so:
➜ yarn devyarn run v1.21.1$ cross-env NODE_ENV=development ts-node --require dotenv-extended/config --transpile-only srcINSTANTIATING ENVIRONMENT ==>FIRING UP THE BOT ==>CONSULTING THE ORACLE ==>NO TRADING BY ORDER OF THE ORACLEGoing to sleep.INSTANTIATING ENVIRONMENT ==>FIRING UP THE BOT ==>CONSULTING THE ORACLE ==>THE ORACLE SAYS TO TRADEBuying 26270323039658927422.53 MLN by selling 430703553862299497 WETHTransaction successful.blockHash: 0xe0a94b99ec8a3ca8d5ba2d7e71a2f4ca0301cd96764d3d5f0afe9db76f71204ftransactionHash: 0xaa0edecb4c741c58b44daf5742305abb5c055949028b913d18f84cdd260bec33gasUsed: 684251Going to sleep.