Building a Trading Bot on Enzyme
Enzyme allows asset managers to develop and deploy proprietary strategies within the burgeoning DeFi ecosystem. The protocol connects natively to other leading projects to do a bunch of different things with a huge number of ERC-20 tokens including swapping them, lending them, staking them, and using them to provide liquidity.
The money-lego nature of DeFi means that opportunities for profit can come and go in the time it takes to mine a block. For those Enzyme managers looking to programmatically capture such opportunities, it might be worth building a bot that executes their strategy on behalf of their Enzyme product.
Luckily, the team at Avantgarde Finance has built a couple of tools that make this sort of programmatic execution achievable.
The first is an expansive subgraph. There has been a lot written about The Graph so I won’t rehash it here. But suffice it to say that it’s an incredible tool that captures and indexes contract event-level detail about what’s happening on Enzyme. We’ll use it to dynamically access boring stuff — token and contract addresses — but the amount of data you can generate from the Enzyme subgraph deserves its own post.
The second tool, and likely the reason you’re here, is the EnzymeSDK. It’s a TypeScript library that exports the Enzyme smart contracts as classes and exposes their various methods. When put together in the right order, you can use these methods to run your strategies automatically. In the example that follows, I’ll build a naive bot that regularly selects a random token and allocates all of an Enzyme vault’s funds to it. I shall name it *THE MINER’S DELIGHT*. In building such a stupid bot, I’m trying to showcase how you can use the smart contracts we’ve deployed, rather than providing any strategic or investment advice. Do not copy this code wholesale.
The current release of EnzymeSDK is extremely low-level and requires the user to be very explicit. Future versions of this package will be more abstract and require less developer input.
The third tool is a Kovan (testnet) deployment of Enzyme. We’ll set up the development environment to test the bot on Kovan to save you gas fees and error trades. It’s important to note that not all mainnet functionality is available on the Kovan deployment, but you can trade on a mock Uniswap contract, which is what the bot we’re building does. You can set up your own Kovan vault using the instructions here.
Using those three tools, the bot I lay out below will perform a couple of functions that you may find useful:
- Check your Enzyme vault’s portfolio balances
- Check token prices on Uniswap
- Execute a trade on Uniswap
First we’ll start a project and install the necessary dependencies. Generally we’ll need tools to interact with the Ethereum blockchain, access the subgraph, and manage our environment variables.
ethersto make contract calls, handle token math, etc
@uniswap/sdkto query current token pair pricing
@enzymefinance/protocolto access the Enzyme contracts
axiosto get gas prices, if and when you need them
graphql-requestto write queries and spin up a minimal graphql client through which to serve them
dotenv-extendedto keep your secrets safe
I’m writing this in
TypeScript, and tossed in
prettier out of habit. You can see I’ve also added some TypeScript-specific
graphql dev dependencies as well.
In meatspace, signing and sending an Ethereum transaction requires a user to interact with some combination of a hardware wallet, browser extension, mobile app, et cetera. Since we’ll be building a bot that signs and sends transactions on our behalf, we need to give it access to the private key for the appropriate Ethereum account.
There are many schools of thought on how best to store and secure private keys for programatic access and it’s out of the scope of this article to discuss them. For simplicity’s sake, I’m storing my keys (for both Kovan and Mainnet) as environment variables and using
dotenv-extended to access them. I’ve added
.env files to my
.gitignore, so there’s no chance of accidentally pushing these to a hosted repo for the world to see.
As an additional precaution, you should permission a wallet to trade on behalf of your Enzyme vault that you have created for that express purpose. The wallet you permission will need ETH to pay for gas but you can otherwise leave it empty, decreasing your potential attack surface. More about the process for delegating trading responsibilities can be found here.
Testnet and Mainnet
Enzyme maintains deployed versions of its smart contracts on Kovan for testing purposes. Functionally, the bot will be identical on both networks. However, some wallet, node, and subgraph configuration variables differ between the two. We’ll use a pass a
NETWORK argument to our Bot on instantiation to dynamically adjust those configuration variables.
Beyond the network-specific variables, we also need to store a couple of the Enzyme Vault contract addresses. Our Bot needs the vault’s Comptroller and Vault addresses. These are available in the Factsheet section at the bottom of the vault’s overview page at app.enzyme.finance.
When it’s all said and done, our environment variables look like this:
Fetching Subgraph Data
The first step to do so is to write a couple of graphql queries against the Enzyme subgraph’s schema. My preferred method for drafting these queries is in the explorer. Once you’ve the info you need there, you can copy it over to your project.
I’ve written two queries. One returns the addresses of the Enzyme contracts on the particular network that we’ve declared in our environment variables. The other returns information on the various tokens that are available in the Enzyme asset universe. You can have a look at the format of those queries here.
Now we’ll need to do a bit of configuration to generate the correct graphql types based on the schema provided by the appropriate Enzyme subgraph. There’s a Kovan version as well as Mainnet and they’re accessible via different endpoints. You’ll need to specify which you are using in a
codegen.yaml file. You’ll also declare where you’ve written the queries, and where you want graphql to put the generated code.
This is the only part in this tutorial where you’ll have to manually choose an environment variable. My
codegen.yaml file looks like this:
When you switch from testing to mainnet, change your schema property to
codegen script in my
package.json takes this file as a configuration parameter, and uses
dotenv to load the appropriate environment variables before running the codegen function.
At this point, you can run
yarn codegen (or
npm codegen whatever your little heart desires) and graphql works its magic to output a file loaded with types and other goodies (including a ready-to-use SDK that we’ll instantiate next).
For this implementation, I’ve written a function that, when passed the appropriate subgraph endpoint, returns the
graphql-request SDK, upon which we can call the queries we’ve written above. You can check that code out here.
Putting this all together, we can use the results returned from this object’s methods to inform our bot about the contracts it needs to call and the state of the vault on whose behalf it’s acting.
Interacting with Your Enzyme Vault
Network Provider and Wallet Setup
Signer object to its constructor method. This
Signer object, per Ethers, is “an abstraction of an Ethereum Account which can be used to sign messages and transactions and send signed transactions to the Ethereum Network to execute state changing operations”.
I’ve included two helper functions in this repo to generate a
Signer object. The first is
getProvider(), which returns a provider object that describes the app’s connection to the Ethereum network. One note here: I’d recommend signing up for a node provider api key. Ethers does provide a generic node connection, but it is throttled pretty tightly. They talk about that here. If you just want to use the
defaultProvider you can change the
getProvider() helper function to ignore the
…NODE_ENDPOINT environment variables and return a
new providers.DefaultProvider(network.toLowerCase()) .
The second helper function is
getWallet() which takes that provider and your Ethereum account’s private key as arguments and returns a
Wallet instance (which is a concrete subclass of
Enyzme Bot Logic
My bot’s strategy, as I mentioned, is fairly simple. However, the methods it uses to accomplish it can be instructive. On a regular cadence, the bot will:
- Call a function that returns a random asset from the Enzyme asset universe, or
- check your Enzyme vault’s current portfolio allocations
- if the random asset you’ve generated is not
undefined, swap your current largest position (by number of tokens held) for as much of the random asset as you can via Uniswap
The bot will ensure that your Enzyme vault’s portfolio positions are essentially randomly constructed, and it will change those position arbitrarily and at a regular cadence. Again, you’re not here for trading advice.
Upon its creation, our bot will be endowed with the knowledge of which Ethereum network it’s operating on, as well as the addresses of all the deployed Enzyme contracts and tokens in the Enzyme asset universe on that network. Additionally, we’ll configure our Ethereum provider and wallet instance properties. And finally, we’ll need to reference the particular Enzyme vault’s comptroller and vault addresses in order to make calls on its behalf. You’ll recall I’ve stored those addresses in my environment variables. Everything else I mentioned above uses helper methods I wrote to make the source of this data more explicit. The end result looks like this:
The Bot Class’s Methods
chooseRandomAsset() doesn’t deserve all that much description. It generates a random number between 0 and the length of our bot class’s
tokens array and returns the asset at that index.
getHoldings() is our first opportunity to use an Enzyme contract in the wild. The
VaultLib contract, when instantiated with a specific vault address and a valid Ethereum
Signer, exposes a method that returns an array of the vault’s tracked asset addresses.
getPrice() takes the tokens being traded and the amount of the token being sold as arguments and passes them through to a
getTradeDetails() helper function. It returns the path a trade must take to get from the token being sold to the token being bought, a minimum incoming amount of the token being bought, and it passes through the amount of the token being sold.
On mainnet, the helper function offloads this work to the Uniswap SDK. There’s lots you can do in terms of path optimization, but I’ve made the assumption that the best price on Uniswap for any given token is that token vs WETH. So when
getPrice() passes Token A and Token B to the helper function that incorporates the SDK, it returns a path that looks like
Token A => WETH => Token B . How you manage that optimization is up to you; Uniswap’s got great docs. Note that I’ve also built a little bit (2%) of potential slippage into the
minIncomingAssetAmount that I return from the
On Kovan, the helper function just assembles the simplest path
(Token A => Token B) and returns a very small value for
swapTokens() takes the Uniswap trade data returned from the
getPrice() function as a parameter and executes the trade via the
ComptrollerLib contract. It also uses two helper functions exported from
swapTokens() highlights a few features of the new Enzyme architecture, an in-depth discussion of which can be found here. The
ComptrollerLib contains the canonical logic for interacting with vaults on the protocol. It can talk to
Extensions, which extend that logic. One such extension is the
IntegrationManager, which allows the exchange of the fund’s assets for other assets via “adapters” to external DeFi protocols (Uniswap, in this case). In the
swapTokens() function, I prepare the arguments necessary for the Uniswap Adapter to execute the trade (via the
uniswapV2TakeOrderArgs() helper), then pass those arguments to the
callOnIntegrationArgs() helper, which ultimately gets passed to the ComptrollerLib's
callOnExtension() function. Note that both of these
Note that much of the logic above will be abstracted away in subsequent releases of
swapTokens() returns a transaction object, which you can prepare with gas estimations and the like and then eventually execute.
tradeAlgorithmically() ties everything together by calling the functions we’ve laid out above. It:
- Chooses an asset from the Enzyme asset universe at random
- Checks the Enzyme vault’s portfolio to determine which current holding it will sell to buy the random asset
- Gets the necessary info to trade the current holding for the random asset
- Calls the
swapTokensfunction to start the transaction
Transaction Execution Logic
index.ts will serve as our Bot’s entry point. The
run function there holds the logic for when and how often the bot calls the
tradeAlgorithmically() function and what to do with the transaction that function returns. You’ll notice I also added a couple of helper functions to check for gas prices on Mainnet and interpret revert errors from the blockchain.
This example of the EnzymeSDK is just a tiny portion of what you can do by programmatically calling the protocol’s contracts. It’s possible to build many types of tools for Vault managers and depositors. Future versions of the package will be abstracted and even easier to use. This post will be updated and expanded upon when those future versions become available.