How to Build a Compound Liquidation Bot

Image result for boron flame color
Boron Flame

Why?

  • Learn about the Compound DeFi protocol.
  • Model expected return of liquidating underwater accounts.
  • Make some m-m-m-money.

What?

  • GitHub Project Boron.
  • How to discover underwater accounts?
  • How to analyze the business model of liquidating underwater accounts?
  • How to liquidate an underwater account?

Existing Work

How to discover underwater accounts?

The Compound API input and output formats are specified by Protocol Buffers, known colloquially as protobufs. Unlike typical protobufs endpoints, the Compound endpoints support JSON for input and output in addition to the protobufs binary format. To use JSON in both the input and the output, specify the headers “Content-Type: application/json”and “Accept: application/json”in the request.

https://compound.finance/developers/api#compound-api

API keys are currently optional, and are included in the HTTP header compound-api-key; you can request keys from Compound in the #development channel of our Discord server. In the future, API keys will be required to access the API.

https://compound.finance/developers/api#compound-api

The Account API retrieves information for various accounts which have interacted with Compound. You can use this API to pull data about a specific account by address, or alternatively, pull data for a list of unhealthy accounts (that is, accounts which are approaching under-collateralization).

https://compound.finance/developers/api#AccountService

Use big numbers to do computation. Convert to decimal when displaying in UX.

Exponential Math

The Compound smart contracts use a system of exponential math in order to represent fractional quantities with sufficient precision. Throughout the documentation and code we make reference to mantissas, which are unsigned integers scaled up by a factor of 1e18 from their nominal value. By using mantissas within our contracts, we may perform basic mathematical operations like multiplication and division at a higher resolution than working with the unscaled quantities directly as integers. To gain a better understanding of how this works, see Exponential.sol.

https://compound.finance/developers#exponential-math

AccountRequest

{
  "addresses": [] // returns all accounts if empty or not included
  "block_number": 0 // returns latest if given 0
  "max_health": { "value": "10.0" } // Filter for accounts where outstanding borrows divided by collateral value
                                    // is less than the provided amount. If returned value is less than 1.0, for instance,
                                    // the account is subject to liquidation.
  "min_borrow_value_in_eth": { "value": "0.002" }
  "page_number": 1
  "page_size": 10
}

AccountResponse

close_factor: The portion of an outstanding borrow that can be closed in a liquidation, which is a percentage of the total underlying borrow balance. For example if the close factor is 0.1, then an account in liqudation is liable to have 10% of its borrows liquidated.

https://compound.finance/developers/api#compound-api

Close Factor: The portion of a borrow in a single market that may be closed by a liquidator during a single liquidation. For example, if Joe borrows 100 ZRX and the close factor is 0.3, then a liquidator may close 30 ZRX. The close factor is globally defined across all markets.

https://compound.finance/developers#glossary

Close Factor

The percent, ranging from 0% to 100%, of a liquidatable account’s borrow that can be repaid in a single liquidate transaction. If a user has multiple borrowed assets, the closeFactor applies to any single borrowed asset, not the aggregated value of a user’s outstanding borrowing.

Comptroller
function closeFactorMantissa() view returns (uint)
  • RETURN : The closeFactor, scaled by 1e18, is multiplied by an outstanding borrow balance to determine how much could be closed.

Can multiple transactions, each liquidating 0.3, be sent sequentially? For example, the first transaction liquidates 0.3, and the health is still < 1.0. Can a second transaction liquidate another 0.3? Each liquidation also removes 1.05 collateral.

Counterproductive Incentives
The protocol includes a mechanism to incentivize arbitrageurs to repay loans that are under-collateralized. This should increase the borrower’s liquidity, reducing the risk of insolvency. After all, that is the point of the mechanism. However, there is a threshold where the incentives reverse and the liquidation mechanism actually pushes borrowers towards insolvency.

https://blog.openzeppelin.com/compound-audit/

liquidation_incentive: The amount of extra collateral that will be seized to incentivize liquidation. For example, an incentive of 1.05 implies that a liquidator will receive a 5% bonus on the exchange of collateral during a liquidation.

https://compound.finance/developers/api#compound-api

Liquidation Incentive: The additional collateral given to liquidators as an incentive to perform liquidation of underwater accounts. For example, if the liquidation incentive is 1.1, liquidators receive an extra 10% of the borrowers collateral for every unit they close. The liquidation incentive is globally defined across all markets.

https://compound.finance/developers#glossary

accounts: The list of accounts (see Account below) matching the requested filter, with the associated account and cToken data.

https://compound.finance/developers/api#compound-api

Get Account Liquidity

Within the Compound Protocol, account liquidity is defined as the total estimated ether value of an account’s collateral (supply balances multiplied by the protocol collateral factor, minus the total value of that account’s borrow balances. These values are calculated using only the markets which the account has entered into.

Users who do not have positive account liquidity do not have the ability to withdraw or borrow any assets until they bring their account liquidity back positive by supplying more assets or paying back outstanding borrows.

Sometimes account liquidity refers to a single signed value, rather than two unsigned values – however the Compound Protocol only deals with unsigned integers. A negative value for account liquidity also means the user is subject to liquidation to bring their account liquidity back to zero.

Comptroller
function getAccountLiquidity(address account) view returns (uint, uint, uint)
  • account : The account whose liquidity shall be calculated.
  • RETURN : Tuple of values (error, liquidity, shortfall). The error shall be 0 on success, otherwise an error code. A non-zero liquidity value indicates the account has available account liquidity. A non-zero shortfall value indicates the account is currently below his/her collateral requirement and is subject to liquidation. At most one of liquidity or shortfall shall be non-zero.

How to retrieve AccountResponse.

Use browser native fetch API. Understand how to fetch data during React lifecycle. See example. Understand how to render lists. The AccountService does not support filtering by token. The tokens data structure can be used to filter the response.

View Code

  async loadUnhealthyAccounts() {
    let url = new URL('https://api.compound.finance/api/v2/account');
    let params = {
      "max_health[value]": "1.0",
    };
    url.search = new URLSearchParams(params).toString();
    try {
      // Call the fetch function passing the url of the API as a parameter
      let response = await fetch(url);
      // Your code for handling the data you get from the API
      // TODO: Filter by token.
      let data = await response.json();
      this.setState({ accountResponse: data });
    }
    catch (e) {
      // This is where you run code if the server returns any errors
      console.log(e);
    }
  }
  
  renderUnderwaterAccounts() {
    if (this.state.accountResponse) {
      return (
        <div className={styles.instructions}>
          <h1> Browse Underwater Accounts </h1>
          <Table width="2">
            <thead>
              <tr>
                <th>Address</th>
                <th>Health</th>
                <th>Borrow Value (Ξ)</th>
                <th>Collateral Value (Ξ)</th>
              </tr>
            </thead>
            <tbody>
              {this.state.accountResponse.accounts.map((value, index) => {
                return (
                  <tr>
                    <td>{value.address}</td>
                    <td>{value.health.value.slice(0, 10)}</td>
                    <td>{value.total_borrow_value_in_eth.value.slice(0, 10)}</td>
                    <td>{value.total_collateral_value_in_eth.value.slice(0, 10)}</td>
                  </tr>
                );
              })}
            </tbody>
          </Table>
        </div>
      );
    }
    return (
      <div className={styles.instructions}>
        <h1> Browse Underwater Accounts </h1>
      </div>
    );
  }
Sample AccountResponse
Output of AccountResponse

Account

total_collateral_value_in_eth: The value of all collateral supplied by the account. Calculated as cTokens held * exchange rate * collateral factor. Note: assets can be supplied and gain interest without being counted as collateral.

https://compound.finance/developers/api#compound-api

total_borrow_value_in_eth: The value of all outstanding borrows with accumulated interest.

https://compound.finance/developers/api#compound-api

health: total_collateral_value_in_eth / total_borrow_value_in_eth. If this value is less than 1.0, the account is subject to liquidation.

https://compound.finance/developers/api#compound-api

How to analyze the business model of liquidating underwater accounts?

Examine LiquidateBorrow events.

What is the TAM?

USDC market size is $32.36M. This would be the upper bound for the TAM if all collateral was loaned out. Compound defines a Collateral Factor per market. Using a Collateral Factor of 0.75 reduces the upper bound to $24.27M. Using a Utilization of 0.4744 reduces the upper bound to $15.34M. How much of $15.34M may be liquidated? This can be approximated by estimating the Liquidation Rate. When liquidations are maximized the Liquidation Rate should equal the Liquidation Incentive. The actual revenue so far is $0.13M.

This article makes a comparison of DeFi leading to the global peer to peer lending market. The global peer to peer lending market is expected to grow at CAGR of 51.5% from 2016 to 2022 with the total market expected to reach $460 billion dollars by 2022. This article suggests if DeFi were to become 5% of the peer to peer market it would be a $23 billion dollar market.

If you believe in crypto and you believe in lending its not hard to imagine a 10x growth rate to the total DeFi lending space in the next 2 to 5 years annual liquidation revenues will be $24mill. If DeFi becomes as large as peer to peer lending liquidations become a $110 mill dollar market annually. (This assumes liquidations remain 0.48% of total $ locked in DeFi).

https://medium.com/efficient-frontier/decentralized-finance-liquidations-a-business-opportunity-assessment-c0eea7bdacec

Collateral Factor: The amount of an asset that may be borrowed for each unit of collateral provided. The collateral factor is defined per market.

https://compound.finance/developers#glossary
Compound Market circa 2019-11-30
Compound USDC Market circa 2019-11-30

What is the expected Liquidation Rate?

What is the age of Compound USDC market?

This can be determined by looking at the age of the contract. The cUSDC contract is 207 days old. Is that a sufficient sample size? It will have to do. What is a sufficient sample size?

What is the contract addresss?

View on Etherscan.

The contract was deployed 207 days ago on Tuesday 7, May 2019.
Transactions over lifetime of cUSDC contract.
How many liquidations have occurred over the lifetime of cUSDC market?

Count the number of LiquidateBorrow events. There have been 373 liquidations.

Event
LiquidateBorrow(address liquidator, address borrower, uint repayAmount, address cTokenCollateral, uint seizeTokens)

Emitted upon a successful Liquidate Borrow.

How much value has been liquidated over the lifetime of cUSDC market?

The sum of repayAmount can be used to calculate total liquidation amount. The sum of seizeTokens can be used to calculate total revenue. The sum of repayAmount is 2.79M USDC.

The code below shows how to calculate the revenue from a liquidation. The total revenue is $0.13M. This is approximately 5% of the total liquidation amount, which is equal to the Liquidation Incentive.

          // Liquidation incentive is 1.05.
          // seizeTokens = x * 1.05
          // x = seizeTokens / 1.05
          // revenue = seizeTokens - x
          // revenue = seizeTokens - (seizeTokens / 1.05)
          const liquidationIncentive = 1.05;
          element.revenue = seizeTokens - (seizeTokens / liquidationIncentive);
          element.revenue = element.revenue * price;
What is the liquidation growth rate?

The count and sum of liquidations will grow in correlation with the size of the Compound USDC market. Increasing price volatility of the underlying collateral assets will also increase liquidations. See Defi Pulse.

How to calculate monthly revenue? Daily revenue? Quarterly?

Each LiquidateBorrow event contains a blockNumber. Use blockNumber to obtain the timestamp.

        let blocksRetrieved = 0;
        liquidations.forEach(liquidation => {
          web3.eth.getBlock(liquidation.blockNumber, (error, block) => {
            blocksRetrieved++;
            liquidation.timestamp = block.timestamp;
            liquidation.timestampISO = (new Date(block.timestamp * 1000)).toISOString();
            // wait for all blocks to be retrieved.
            if (blocksRetrieved === liquidations.length) {
              dailyRevenue = liquidations.reduce(function(acc, cur) {
                let dateString = cur.timestampISO.substring(0, 10);
                acc[dateString] = (acc[dateString] || 0) + cur.revenue;
                return acc;
              }, {});
              // trigger ui update once after all blocks have been retrieved
              // to avoid degrading performance.
              this.setState({
                liquidations: liquidations,
                dailyRevenue: dailyRevenue,
              });
            }
          });
        });

Use react-plotly to render time series chart. When running node without enough memory the following error may occur when compiling. See issue. See fix.

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
export NODE_OPTIONS=--max_old_space_size=4096
          <Plot
            data={[
              {
                x: Object.keys(this.state.dailyRevenue),
                y: Object.values(this.state.dailyRevenue),
                type: 'scatter',
              },
            ]}
            layout={{
              title: 'USDC Liquidation Revenue',
              yaxis: {
                title: 'Revenue ($)'
              },
            }}
          />
What is the distribution of liquidations over the lifetime of cUSDC market?
          <Plot
            data={[
              {
                x: this.state.liquidations.map(liquidation => liquidation.revenue),
                type: 'histogram',
              },
            ]}
            layout={{
              title: 'USDC Liquidation Revenue Distribution',
              yaxis: {
                title: 'Count'
              },
              xaxis: {
                title: 'Revenue ($500 bins)'
              },
            }}
          />

A more complicated estimation technique is to do a Monte Carlo simulation. Estimate the expected revenue over some period of days given the distribution of liquidations over the lifetime of the market. Run a simulation given the known distribution. For each day out of 365 days generate a random number using the known distribution to choose a liquidation value for that day. Sum this over the target period to calculate the expected revenue. Run this simulation a few thousand times to determine the range of expected revenue.

Who are the market agents?

There were 51 distinct liquidators in the USDC market. There were 132 distinct borrowers who were liquidated.

Who were the top 10 liquidators by revenue?

Given this distribution a new entrant could expect less than $3K revenue over the last 207 days.

Who were the top 10 liquidated borrowers by revenue?

How to query LiquidateBorrow events?

Query past events with getPastEvents(). See Solidity documentation. In theory options parameter is optional. In practice an empty array is returned if options is not provided with fromBlock and toBlock set.

The ABI can be obtained from Etherscan. Import the ABI JSON file using require. Filters cannot be used because the LiquidateBorrow event does not have indexed parameters. Learn how to format numbers using React. There is a bug where very small decimals have the exponent incorrectly formatted. Use toFixed to convert the number to a fixed decimal string as a workaround.

In this example the repayAmount values need to be divided by 10**6. The seizeTokens values need to be divided by 10**8. This does not seem to be documented anywhere. The values can be cross-validated using Etherscan. These values are the token decimals and vary depending on the token. The number of decimals for a token can be obtained from Etherscan.

USDC decimal value is 10**6.
Discord chat verifying the documentation.

Use reduce to aggregate data.

View code on GitHub.

  async loadLiquidations() {
    const web3 = await getWeb3();
    let compoundUsdAbi = require('../../../../contracts/cusdc.json');
    // Implies only liquidations of Compound USDC debt are retrieved.
    let compoundUsd = new web3.eth.Contract(JSON.parse(compoundUsdAbi.result), '0x39aa39c021dfbae8fac545936693ac917d5e7563');
    if (compoundUsd) {
      let options = {
        fromBlock: 0,
        toBlock: 'latest'
      };
      try {
        // In theory options parameter is optional. In practice an empty array is
        // returned if options is not provided with fromBlock and toBlock set.
        let liquidations = await compoundUsd.getPastEvents('LiquidateBorrow', options);
        this.setState({ liquidations: liquidations });
      }
      catch (error) {
        console.log(error);
      }
    }
  }
  renderLiquidations() {
    if (this.state.liquidations) {
      return (
        <div className={styles.instructions}>
          <h1> Browse Compound USDC Liquidations </h1>
          <Table width="2">
            <thead>
              <tr>
                <th>Liquidator</th>
                <th>Borrower</th>
                <th>Repay Amount (cUSDC)</th>
                <th>Revenue (cToken)</th>
                <th>Collateral cToken Address</th>
              </tr>
            </thead>
            <tbody>
              {this.state.liquidations.map((value, index) => {
                return (
                  <tr>
                    <td>{value.returnValues['liquidator']}</td>
                    <td>{value.returnValues['borrower']}</td>
                    <td><NumberFormat value={value.returnValues['repayAmount'] / 10**6} displayType={'text'} thousandSeparator={true} /></td>
                    <td><NumberFormat value={value.returnValues['seizeTokens'] / 10**8} displayType={'text'} thousandSeparator={true} /></td>
                    <td>{value.returnValues['cTokenCollateral']}</td>
                  </tr>
                );
              })}
            </tbody>
          </Table>
        </div>
      );
    }
    return (
      <div className={styles.instructions}>
        <h1> Browse Compound USDC Liquidations </h1>
      </div>
    );
  }
Sample liquidation response.
Output of query for LiquidateBorrow events.

What are factors driving the decision to liquidate?

  • There must be unhealthy accounts to liquidate. The close factor of 0.5 determines how much of the debt can be liquidated.
  • The unhealthy account must have collateral that can be easily liquidated on an exchange. A collateral with low liquidity will make it hard to monetize.
  • An unhealthy account may have collateral and debt distributed over numerous assets. Each liquidateBorrow transaction can specify 1 debt contract and 1 collateral contract. In the worst case multiple liquidateBorrow transactions may be needed to liquidate the maximum amount distributed over multiple assets.
  • The agent’s wallet must have sufficient funds of the underlying asset to liquidate.
  • The Ethereum Virtual Machine (EVM) is a global state machine that must process liquidateBorrow transactions sequentially. Only one agent will be able to capitalize on an opportunity to liquidate.
  • How fast an agent can spot an opportunity.
    • What are the factors affecting the latency of spotting opportunities?
      • Choosing a collateral that has insufficient funds will cause the transaction to fail. Checking for collateral that has sufficient funds adds latency. How can this be cached?
      • Relying on the AccountService API implies the latency is dependent upon the API latency.
        • Can the query for unhealthy accounts be done directly on the blockchain? getAccountLiquidity can be used to query the blockchain.
        • What is the distribution of block delay for AccountService responses?
Discord chat regarding AccountService block delay.
  • How fast an agent can get its liquidateBorrow transaction accepted into the blockchain.
    • Gas cost.
    • Is it better to broadcast many transactions with high gas or a single?
      • It is probabilistic which miner will create the next valid block containing the transaction. Therefore, broadcast to many miners to improve the probability the transaction will be accepted into the blockchain first.
    • Consider hosting a proxy contract to enable more advanced operations in a single transaction. For example, liquidating multiple contracts in one transaction rather than having to submit multiple transactions. This may also reduce latency by batching multiple operations into the proxy contract.
Discord chat regarding using proxy contract to batch operations.
  • Network latency and hardware speed.
  • Hosting the bot in the browser may be too high level abstraction if speed is a concern. Consider hosting the bot in a container running low level code to increase software performance. What low-level web3 libraries are available? Use Geth directly.
Discord chat regarding agent speed.

Fig. 9. Evolution of the PGA bots’ latency strategies over time. Each point is the mean latency of a bot in some observed auction on the plotted date. The coloring of dots shows the number of total raises each bot placed in that respective auction. Early in the observed market, the majority of auctions contained 0-10 raises per bot. Over time, these auctions increasingly contained more raises and a lower mean latency.

Flash Boys 2.0
  • How may unhealthy accounts be predicted ahead of time? Then spam transactions when high market volatility is expected. Machine learning magic.
  • Inspect and anticipate the transaction mempool. https://www.mycryptopedia.com/mempool-explained/
  • What happens when there are many unhealthy accounts with small value that liquidators do not want to liquidate? In aggregate the value may be significant, but the transaction cost on each liquidation outweigh the aggregate and make it not profitable to liquidate?
    • Reserves should mitigate this risk. Low risk of being used as attack vector.
Discord chat regarding the risk of attack vector.

Executing liquidations is tricky

  • Intensive capital requirements — In order to liquidate a $1m loan the liquidator needs $1m to do it.
  • Highly periodic — Liquidations come in bursts with major market moves. On Sept 24 $8.2M was liquidated in a single day and roughly $320k in liquidation revenues were earned. (not including gas and other costs). 30 days can elapse with no significant liquidations taking place.
  • Watching Loans — a monitoring system is required to watch loans that are at risk of becoming under collateralized
  • Watching MakerDAO Price Oracle — The ability to watch the MakerDAO price oracle (everyone using this for market prices) and predict when it will change price as well as how this flows through to the liquidation key moments is key.
  • Gas Prices — can erode profits can this be navigated?
  • Hedging — I’ve read that hedging is part of some players strategy to offset movements in PETH, ETH and other volatile assets
  • Borrowing — The system may need the capability to programmatically barrow capital from the DeFi ecosystem in order to pay off loans.

https://medium.com/efficient-frontier/decentralized-finance-liquidations-a-business-opportunity-assessment-c0eea7bdacec

How to liquidate an unhealthy account?

Liquidate Borrow

https://compound.finance/developers/ctokens#liquidate-borrow

CErc20

function liquidateBorrow(address borrower, uint amount, address collateral) returns (uint)
  • msg.sender : The account which shall liquidate the borrower by repaying their debt and seizing their collateral.
  • borrower : The account with negative account liquidity that shall be liquidated.
  • repayAmount : The amount of the borrowed asset to be repaid and converted into collateral, specified in units of the underlying borrowed asset.
  • cTokenCollateral : The address of the cToken currently held as collateral by a borrower, that the liquidator shall seize.
  • RETURN : 0 on success, otherwise an Error codes

Before supplying an asset, users must first approve the cToken to access their token balance.

What must the agent be able to do?

  • Query the Compound API for unhealthy accounts.
    • Requires connection to Compound API.
    • Use setInterval to poll for opportunities using fetch from client browser.
  • Send liquidateBorrow transactions.
    • Requires a private account to send transactions.
    • Requires sufficient ETH to pay for gas.
    • Requires a connection to the blockchain. Can use web3js for client to blockchain communication.
  • The create-react-app that comes with OpenZeppelin Tutorial Starter Kit comes with the webpack development server to serve the single page app. Multiple instances of the app can be opened using multiple browser tabs. Each browser tab can host a single or multiple bots. A server is not needed.

Reuse the code from UnhealthyAccounts component by refactoring into higher order component.

No tools existed for analysis of unconfirmed and rejected transactions, so we wrote our own. We forked the GoEthereum client to record unconfirmed transaction in the mempool. We deployed six geodistributed nodes across multiple data centers, with timestamps synchronized to the nanosecond level by NTP. We collected an observation every time one of the≈ 256 nodes peered with one of our deployed modified nodes relayed a transaction to us. We collected nine months of data, amounting to over 300 gigabytes, including 708,385,840 unique observations of PGA arbitrage bots.

Flash Boys 2.0

We supplemented this mempool data we collected on PGAs with on-chain data sourced from Google’s BigQuery Ethereum service (as well as other on-chain metadata, like block timestamps), allowing us to parse logs for successful transactions and determine their profit. We also used daily price data from coinmetrics.io for USD conversions.

Flash Boys 2.0

c) All pay: In PGAs, losing players pay gas costs for their failed transactions. Our model captures this cost by having a losing player P pay loss($blast), where $blast is the last bid made by P and loss() is a loss function. This type of all-pay auction in which players can submit multiple bids is known as a dollar auction [44], in which not only the winner, but losers pay. In our study, we typically observe auctions that are partial all-pay, i.e., loss($blast) < $blast.

Flash Boys 2.0

Save the information of all borrowers and assets to enable fast lookup.

Use IndexedDB to save account data to local storage.

Identify the token addresses containing debt and collateral that can be liquidated and collected.

In this example the account has debt denominated in token cZRX and collateral denominated in token cSAI that can be liquidated.

Identify the token addresses containing debt and collateral that can be liquidated.
Unhealthy accounts with debt and collateral broken down by token.

Suppose the global close factor is 0.5 and an unhealthy account has N debt assets and M collateral assets. An agent wishes to maximize liquidation amount and collateral collected. This is a variation of the Knapsack Problem. Model each item as a <debt.weight, collateral.value> tuple. There would be N*M total items. The total debt.weight must be less than or equal to the close factor while maximizing the total collateral.value. Specifically, it is the simpler Fractional Knapsack Problem.

The knapsack problem or rucksack problem is a problem in combinatorial optimization: Given a set of items, each with a weight and a value, determine the number of each item to include in a collection so that the total weight is less than or equal to a given limit and the total value is as large as possible. It derives its name from the problem faced by someone who is constrained by a fixed-size knapsack and must fill it with the most valuable items.

https://en.wikipedia.org/wiki/Knapsack_problem

What is the maximum amount that can be liquidated and collected?

max_liquidation_amount_in_eth = total_borrow_value_in_eth * close_factor
token_borrow_balance_underlying_in_eth = token_borrow_balance_underlying * underlying_asset_to_eth_exchange_rate
total_borrow_value_in_eth = sum(token_borrow_balance_underlying_in_eth)
token_supply_balance_underlying_in_eth = token_supply_balance_underlying * underlying_asset_to_eth_exchange_rate
total_supply_value_in_eth = sum(token_supply_balance_underlying_in_eth * collateral_factor)
max_collectible_amount_in_eth = sum(token_supply_balance_underlying_in_eth)

Notice sum(token_borrow_balance_underlying_in_eth) != total_borrow_value_in_eth. More accurate price oracle is needed to match the values returned by the Account API, rather than hard coding exchange rates. Perhaps to optimize for speed, accuracy can be sacrificed. Approximate prices may be used if enough margin of error is assumed.

Unhealthy accounts with debt, collateral, and expected transaction.

Notice some accounts may have less collateral than the max liquidation amount. If the max liquidation or max collectible amount is less than the gas used multiplied by the gas price, then the transaction will never be profitable.

How to calculate collateral seizeAmount?

PriceOracle can be used to getUnderlyingPrice for a cToken. It also implements liquidateCalculateSeizeTokens. cUSDC contract can be used to get exchangeRateStored. The corresponding cToken contract should be used to retrieve the desired exchange rate. Alternatively, max_liquidation_value_in_eth = total_borrow_value_in_eth * close_factor. max_liquidation_value_in_eth * liquidation_incentive * collateral_to_eth_exchange_rate will also give the expected collateral seizeAmount. If optimizing for speed, then this shortcut may be sufficient.

        /* We calculate the number of collateral tokens that will be seized */
        (uint amountSeizeError, uint seizeTokens) = comptroller.liquidateCalculateSeizeTokens(address(this), address(cTokenCollateral), repayAmount);
    /**
     * @notice Calculate number of tokens of collateral asset to seize given an underlying amount
     * @dev Used in liquidation (called in cToken.liquidateBorrowFresh)
     * @param cTokenBorrowed The address of the borrowed cToken
     * @param cTokenCollateral The address of the collateral cToken
     * @param actualRepayAmount The amount of cTokenBorrowed underlying to convert into cTokenCollateral tokens
     * @return (errorCode, number of cTokenCollateral tokens to be seized in a liquidation)
     */
    function liquidateCalculateSeizeTokens(address cTokenBorrowed, address cTokenCollateral, uint actualRepayAmount) external view returns (uint, uint) {
        /* Read oracle prices for borrowed and collateral markets */
        uint priceBorrowedMantissa = oracle.getUnderlyingPrice(CToken(cTokenBorrowed));
        uint priceCollateralMantissa = oracle.getUnderlyingPrice(CToken(cTokenCollateral));
        if (priceBorrowedMantissa == 0 || priceCollateralMantissa == 0) {
            return (uint(Error.PRICE_ERROR), 0);
        }
        /*
         * Get the exchange rate and calculate the number of collateral tokens to seize:
         *  seizeAmount = actualRepayAmount * liquidationIncentive * priceBorrowed / priceCollateral
         *  seizeTokens = seizeAmount / exchangeRate
         *   = actualRepayAmount * (liquidationIncentive * priceBorrowed) / (priceCollateral * exchangeRate)
         */
        uint exchangeRateMantissa = CToken(cTokenCollateral).exchangeRateStored(); // Note: reverts on error
        uint seizeTokens;
        Exp memory numerator;
        Exp memory denominator;
        Exp memory ratio;
        MathError mathErr;
        (mathErr, numerator) = mulExp(liquidationIncentiveMantissa, priceBorrowedMantissa);
        if (mathErr != MathError.NO_ERROR) {
            return (uint(Error.MATH_ERROR), 0);
        }
        (mathErr, denominator) = mulExp(priceCollateralMantissa, exchangeRateMantissa);
        if (mathErr != MathError.NO_ERROR) {
            return (uint(Error.MATH_ERROR), 0);
        }
        (mathErr, ratio) = divExp(numerator, denominator);
        if (mathErr != MathError.NO_ERROR) {
            return (uint(Error.MATH_ERROR), 0);
        }
        (mathErr, seizeTokens) = mulScalarTruncate(ratio, actualRepayAmount);
        if (mathErr != MathError.NO_ERROR) {
            return (uint(Error.MATH_ERROR), 0);
        }
        return (uint(Error.NO_ERROR), seizeTokens);
    }

How to calculate gas fee?

It is important to note that all transactions cost 21000 gas as a base. So if you are just transferring funds and not interacting with a contract, your transaction takes 21000 gas. If you are interacting with a contract, your transaction takes 21000 gas plus any gas associated with running the contract.

https://hackernoon.com/ether-purchase-power-df40a38c5a2f

If you are a developer and know the ABI of the Token contract, the easiest way is to call estimateGas on the contract function.

https://medium.com/@blockchain101/estimating-gas-in-ethereum-b89597748c3f

See Solidity documentation here and here. ETH Gas Station can also be used. A gas price of 13.2 Gwei should be used for the fastest transaction confirmation.

Gas prices from ETH Gas Station.

Also see: How to calculate characteristic and mantissa. Handy for computing the position of the decimal.

How to calculate expected profit?

All the accounts returned by the AccountService API have negative expected profit.

Expected profit after subtracting expected gas fee.
%d bloggers like this: