ruvaag

Debt Accounting in Compound v2

Overview

Compound is one of the largest onchain money markets with over $5 billion in tvl as of today. This post dives into the mechanics of how compound keeps track of onchain deposit and debt balances as they dynamically accrue interest over time. The first section looks at how the protocol tracks lenders' deposits using cTokens and how those cTokens accrue interest in real-time. The second section explores how compound uses a borrowIndex to atomically update debt balances for different positions with varying size, tenure, and interest rates across the system.

Lenders

When a lender supplies assets to a compound market, they receive an equivalent value of cTokens in return. The number of cTokens received depends on the cToken exchange rate which can be queried using the exchangeRateCurrent() function and is cached in the exchangeRate state variable. The cTokens themselves represent the deposit of the lender and in this sense, the accounting is very straightforward -- from the protocol's perspective, a lender's deposit in any compound market is equal to the value of cTokens they possess at a given point in time.

Another aspect to look into is how these lenders' deposits (stored as cTokens) earn interest over time. This is achieved via a dynamic cToken exchange rate implementation. The cToken exchange rate represents the price of one cToken in terms of its underlying asset and is initialized with a value of $0.02$ when a new market is created. Theoretically, the exchange rate varies as a non-decreasing function and keeps increasing as the borrowers' debt accrues interest over time. This increasing rate implies that the lender can redeem the same number of cTokens received earlier for an increased number of underlying assets at a later point in time. This exchange rate differential across time represents the interest earned by lenders on their deposited assets.

cToken Exchange Rate

The ctoken exchange rate at any moment can be computed as ---

rate=liquidity+borrows−reservessupply

where,

To intuitively understand why this function is nondecreasing let's look at some common operations and how they affect the exchange rate ---

Borrowers

The cTokens play a dual role as they not only represent the lenders' deposits but also their collateral for any debt issued. After depositing assets into compound (and receiving cTokens) borrowers can now use these cTokens as collateral to open over-collateralized debt positions. The protocol accounting for debt balances needs to --- (i) atomically accrue interest over all debt positions (ii) atomically update position risk across all positions to facilitate timely liquidations and (iii) minimize gas costs incurred by protocol users. This is a non-trivial task considering the onchain storage premium, increasing gas prices and the permissionless nature of defi protocols vulnerable to certain exploits. Accordingly, Compound implements a neat solution to this problem that we look into below.

Compound accrues interest on a per-block basis and the instantaneous per-block borrow rate can be queried using the borrowRatePerBlock() function. This algorithmically-determined rate currently depends on pool utilization which reflects the demand for the underlying asset in the market.

The accrueinterest() function in the contracts can be called to accrue interest and atomically update the state for all debt positions in the system. Since ethereum doesn't have something similar to cron jobs, running a bot that periodically calls the accrueInterest() function would be an anti-pattern. Instead, the protocol is designed to call this function before executing any user operation - deposit, redeem, borrow, repay, or liquidation. This essentially amortizes the cost of keeping the state updated across all users proportional to how often they interact with the protocol, which arguably is a very reasonable approach.

Interest Accrual

The accrueInterest() function mentioned above is responsible for updating debt balances across the cToken contract. It is worth looking into what happens when this function is called. the protocol updates the totalBorrows, totalReserves and borrowIndex state variables based on interest accrued since the last time this function was called.

The borrowIndex is a index value initialized at 1 when a new compound market is created and tracks the cumulative interest accrued over time. Updating the borrowIndex is used as a proxy to atomically update all debt positions in the protocol and we explore how this actually works in the next subsection.

For now, let's consider a situation where accrueInterest() is called at time t and δt intervals have passed since the last time it was called. The changes in the cToken contract state after this call can be summarized as:

it=bt−δt(1+rt·δt)bt=bt−δt(1+rt·δt)rt=rt−δt+r―·bt−δt·(rt·δt)

where,

Borrow Storage Internals

Compound stores the debt balance for every account as a borrowSnapshot object composed of two values -- principal and interestIndex. The principal stores the raw amount of debt owed by the account denominated in terms of the underlying asset. The interestindex stores the borrowIndex of the cToken when the principal was last updated.

Consider the borrowSnapshot for an account a with principal pa, interestIndex ia while the current ctoken borrowIndex is i. we can use this data to deduce the following ---