Update, 7/26/21: I've open sourced Nantucket's successor here.
Nantucket is a (massively) upgraded version of my Compound Liquidation Bot. For capital-free liquidations, it was more or less state-of-the-art in November 2020. That said, this space moves quickly, and there are obviously improvements to be made -- I'm not gonna leak my most recent alpha.
- 🦄 Liquidate via Uniswap (v2) flash swaps
- 👻 Liquidate via AAVE (v1) flash loans - you'll have to go back in git history for this
- 🔢 Liquidate multiple accounts at once
- 🧮 Compute repay amounts atomically on-chain
- ⛽️ Burn CHI to reduce gas costs
- 🏷 Atomically post prices to Compound's Open Price Feed
- (optional) Contract to split profits between 2 people
- Fetch accounts from Compound's API every
N
minutes- Compute revenue (in ETH) that can be expected from liquidating each user
- Store expected revenue, account health, and [repay, seize] token pair in Postgres database
- Filter database accounts by
minRevenue
ormaxHealth
. Track these accounts- Fetch on-chain balances every block (don't try to do this for more than ~500 accounts)
- Check liquidatability based on most recent Coinbase prices for each asset
- If account has supply, use min price seen since last on-chain posting
- Otherwise, use max price seen since last on-chain posting
- Pass liquidatable accounts to a transaction manager
- Manage multiple nonces and/or automatic blind PGA bidding
- Separate processes for these broad tasks
- Can have multiple account-liquidatability-checkers and multiple transaction managers
- Extensive logging with Winston; optional Slack bot integration
- Some (mostly broken) tests
Looking back on it, the rest of this README is pretty poorly scoped and/or outdated. You're probably better off learning elsewhere, and coming back here for the code.
If you're planning to use this code, you should know this stuff already. But if you're a casual observer of my Github profile, feel free to read on.
Compound is both a company and a collection of code (a decentralized app or "Dapp") that's stored on the Ethereum blockchain. The Dapp allows users to supply and borrow crypto tokens (e.g. WBTC, USDC, DAI, BAT). Suppliers earn interest, while borrowers pay interest.
But this system doesn't work like a regular bank -- there's no way to identify
individuals on the blockchain, so there's no way of knowing their creditworthiness.
As such, in order to borrow anything, users must first put up collateral that
exceeds the value of their desired loan (if we get more technical, each crypto
token has a "collateral factor" that indicates the % of collateral that a user can
borrow). For example, suppose Bob believes that Bitcoin's price will fall soon.
Bob can supply USDC to Compound, borrow an amount of Bitcoin worth less than
collateralFactor * valueOfSuppliedUSDC
, and trade that borrowed Bitcoin for more
USDC. If Bob's belief comes true, he'll be able to re-trade the USDC for Bitcoin and
pay off his loan with some USDC left over.
If, on the other hand, Bob is wrong -- the price of Bitcoin rises -- then Bob is in
trouble. In this situation, the value of his borrowed Bitcoin may grow to exceed
the collateralFactor * valueOfSuppliedUSDC
. If Bob fails to pay off his loan
before this happens, then Bob is subject to liquidation.
For more introductory information, see Compound's website and for a deep dive into transaction dynamics read this paper.
let collatValue = 0.0;
let borrowValue = 0.0;
for (let cryptoToken of user.cryptoTokens) {
// Note that each crypto token can have a unique collateral factor
collatValue += user.walletSize[cryptoToken] * cryptoToken.priceInUSDollars * cryptoToken.collateralFactor;
borrowValue += user.loanSize[cryptoToken] * cryptoToken.priceInUSDollars;
}
const userIsLiquidatable = borrowValue > collatValue;
The pseudocode above shows how Compound determines if a user is liquidatable or
not. If they are liquidatable, the next question is "By how much?" The number
that governs this is called the "close factor," and so far has been constant at
50%. This means that liquidatableAmount <= borrowValue * 0.50
, but it's not
the only constraint...
If successful, liquidators receive a portion of the user's collateral: revenue = liquidatableAmount * liquidationIncentive
,
where the liquidation incentive is usually around 110%. In order for this to work,
the user must actually have that much collateral available for the taking. This
means that liquidatableAmount <= collatValue * liquidationIncentive
.
Both constraints must be satisfied for the liquidation to be successful. There are
other things to consider as well, such as "Which loan should I pay off?" (if the
user has borrowed multiple types of crypto tokens) and "Which collateral should I
seize?" (if the user has supplied multiple types of crypto tokens). To complicate
things further, v2 cTokens can be both repaid and seized in a single liquidation,
but normally repayTokenType != seizeTokenType
.
You can find most of this liquidation logic here and here.
This section describes AAVE flash loans. Nantucket now uses Uniswap flash swaps, which work somewhat differently (and are more gas efficient in most cases)
A flash loan is an atomic interaction (a single transaction on the blockchain) that (1) takes out a loan and (2) pays it off. Only certain Dapps allow this (e.g. AAVE, UniswapV2, and DyDx). What's great is that you can take out a loan of any size without first putting up collateral. If you fail to pay off your debt by the end of the transaction, the provider's software (AAVE, etc.) simply throws an error and the whole transaction is undone. The only penalty is the transaction fee (gas * gasPrice).
Nantucket uses AAVE flash loans to liquidate users on Compound:
- Borrow X tokens of type A from AAVE
- Liquidate user on Compound by paying off their debt with X tokens of type A
- As a reward, receive Y tokens of type B from Compound, seized from the user's collateral (where
Y = X * liquidationIncentive
). Note that type A and B can be the same for DAI and USDT, but must be different otherwise - Trade Y tokens of type B for Z tokens of type A on Uniswap. Assuming Uniswap's exchange rates aren't whack, Z should be greater than X.
- Repay AAVE loan using X tokens of type A. Technically AAVE also expects a small fee (0.0009%).
- Keep
Z - X
tokens of type A as profit.
This logic can be found in the contracts folder. I've already deployed the Solidity code to the blockchain, so it's now accessible via wrappers.
This section is also somewhat outdated. In lieu of describing the current state of affairs, I think it's reasonable to expect potential users to read the code. If you don't understand it, don't use it!!!
Compound (the company) provides an HTTP endpoint that returns information about all users (address, supply amounts, and borrow amounts). They provide another HTTP endpoint that returns information about all tokens (address, real-world price, collateral factor). Nantucket periodically polls this information, does some computation, and stores it in a Postgres database. The "computation" is really just to answer the questions "Which type of token should I repay and seize if this user becomes liquidatable?" and "How profitable would this be?"
A separate process periodically polls the database to get a subset of Compound
users. This subset is configurable via the arguments passed to the Main
constructor. Whenever a new block gets added to the Ethereum blockchain (~ every 15
seconds), Nantucket loops through the users to decide (1) if they are
liquidatable according to the Compound Dapp and (2) if they are liquidatable
according to token prices on Coinbase.
In case 1, the code immediately sends a transaction to liquidate them. The gas price
of that transaction is
gasPriceRecommendedByGethBlockchainClient * someMultiplier
where someMultiplier
is configurable in Main
's constructor. For example, if a Main
instance is
configured to look only at users where the potential profit is >$10000, the gas
price multiplier may be set higher so that the transaction goes through faster --
after all, there's lots of competition for these high value liquidations.
In case 2, the user is added to the "price wave" list. A transaction is sent with the intent that it will remain "pending" for a while. This is done by sending it with a relatively low gas price (high enough that miners keep it in the transaction pool, but low enough that it takes a while to be included in a block).
Nantucket also watches for the signature of Compound's price update transactions. This is when Compound (the company) updates the Dapp's knowledge of token prices to match those of the real world (on Coinbase, for example). As soon as one of these price update transactions is pending, Nantucket raises the gas price of pending transactions to match the gas price of the price update transaction. In theory, this should make the transactions happen consecutively, guaranteeing that we win the first-come first-serve liquidation battle. In practice, other liquidators manage to put themselves in that position more reliably, and Nantucket loses.
Key Files:
Don't. You will almost certainly loose money. Feel free to admire the code or use it as a reference point, but please don't try to run it as-is.