DeFi Interest Rate Indexes: Principal value and Present Value in Compound V3

The intuitive way to track lender deposits is to record the amount of USDC they deposited and the time they deposited. Compound V3 does not do this.

Instead, similar to SushiSwap Masterchef Staking Algorithm, Compound V3 tracks the hypothetical gain of one dollar lent “since the beginning of time.” (Readers not already familiar with this algorithm should read the linked resource).

The hypothetical gain of one dollar lent since the beginning of time is tracked in the baseSupplyIndex (in CometStorage.sol). It behaves very similar to to the “rewardPerTokenAccumulator” from Sushiswap. It starts at 1.0 and each time a state-changing operation occurs (deposit, withdraw, borrow, etc), it is increased proportional to the time passed and the interest rate over that period. For example, if 100 seconds passed, and the interest rate was 0.001 per second (unrealistically high, but easy to reason about), then baseSupplyIndex will be updated to 1.1. Specifically, the following formula is used:

baseSupplyIndex += supplyInterestRatePerSecond(utilization) × secondsElapsed

The only place baseSupplyIndex is ever changed is on line 403 in Comet.sol, inside accrueIndexIndices.

accrueInterestIndices function

Because interest rates are a direct function of utilization, baseSupplyIndex increases faster during times of high utilization and slower during times of low utilization.

The following hypothetical plot shows the baseSupplyIndex and baseBorrowIndex increasing at different rates depending on the utilization. Generally, borrowers pay a higher interest than lenders are paid, so the baseBorrowIndex increases faster.

interest indices as a function of uitilization

The following example illustrates how this variable is used.

Example and Terminology

Alice deposits \$1,000 at a time when the baseSupplyIndex is 2.5. She is not credited with depositing \$1000, instead she is credited with depositing \$400, which is her deposit divided by the current baseSupplyIndex ($1,000 ÷ 2.5). Alice has a “principal value” of \$400 in her account (yellow box). This is the value Compound stores for users (CometStorage.sol).

userBasic struct

If she were to withdraw right away, Compound would calculate her balance as \$400 times the current baseSupplyIndex, which is 2.5, so she would withdraw \$1,000. Compound calls the principal value times the baseSupplyIndex the “present value”.

Compound V3 does not “remember” that her original real deposit was \$1,000. This is implied because the baseSupplyIndex is currently at 2.5 and her deposit is recorded as being \$400.

The “downscaled” or “scaled backward” dollar value that Compound V3 stores is called the “principal value.” When we multiply the principal value by the current baseSupplyIndex, or “scale forward” we get the “present value.”

Readers coming from a traditional finance background may find Compound’s use of the terms “principal value” and “present value” confusing — we suggest not trying to relate the terms to their traditional meanings and just accept Compound’s usage.

If she were to wait until the baseSupplyIndex increases to 3.0, the principal value would still be \$400, but the present value would increase to \$1,200 \$1,200 ($400 x 3.0 = 1,200).

example growth of present value

In CometCore.sol, we see that:

  1. the “principal value” is computed by dividing the “present value” by the baseSupplyIndex

  2. the “present value” is computed by multiplying by the “principal value” by the baseSupplyIndex.

principal and present value functions

So to summarize the important terms:

Principal value

Principal value is the USDC deposited divided by the baseSupplyIndex value at the time of deposit. This is kept in a storage variable associated with the user’s account and does not change unless the user deposits or withdraws. The principal value is generally less than the actual deposit because baseSupplyIndex is always greater than or equal to 1.

Present value

The present value is the principal value times the current value of the baseSupplyIndex. This value is not stored anywhere but is computed on the fly.

Principal value and present value are two of the most critical concepts for understanding Compound V3.

Principal is the only variable tracked for lenders

Let’s revisit the struct we screenshotted above:

user basic struct again

The variable baseTrackingAccrued is used by CometRewards to determine how much COMP to award the account for participation in the protocol. The variable baseTrackingIndex is related to that but currently unused. The variable assetsIn is only used for borrowers as an indicator for whether certain collateral assets are used. The variable _reserved is unused.

Therefore, principal is the only essential variable for lender accounting. Note that this is a signed variable — for borrowers it is negative. For borrowers, we also need to track how much collateral they have deposited, but that is for another article.

balanceOf() — checking the lender’s positive balance

To illustrate that Compound V3 stores the principal value, but values the account at the present value, consider the balanceOf() function in Comet.sol shown below.

First it will read the updated baseSupplyIndex without updating it, as this is a view function. Then it read’s out the principal balance of the lender and multiplies it by the baseSupplyIndex.

Interest accrued is a function of time and utilization, so as long as the utilization is not zero, each time balanceOf is queried, it will return a higher value.

balanceOf function

The following screenshot shows the relationship between the lender’s balance on the Compound V3 dapp and the balanceOf return value from Etherscan imposed on it for that same address. Both values highlighted in an orange box.

balanceOf for lender

Visual Example of baseSupplyIndex

When a lender deposits, the USDC balance of their deposit is divided by the baseSupplyIndex to produce the principal value, which is what gets stored. The later they deposit, the larger baseSupplyIndex will be, and they will be credited with a lower principal value.

The principal value is static (unless they deposit or withdraw), but the present value is the principal value * baseSupplyIndex, and that is constantly increasing.

In the graphic below, Alice deposited \$1 when the baseSupplyIndex equaled 1.01, so her principal value is 0.99 (1 ÷ 1.01). Bob also deposited \$1, but at a later time when the baseSupplyIndex had a value of 1.03 (1 ÷ 0.97). Therefore, his principal value was 0.97.

Bob and Alice both deposited the same amount: \$1. But because Bob deposited later, his principal value is lower.

present and principal value plot

supplyBase() and withdrawBase()

When a user supplies (or withdraws) the base asset, the principal value is reset. For example, if Alice deposited \$10 when the baseSupplyIndex was 10, her principal value would be \$1. Now suppose at a time when the baseSupplyIndex index increases to 20, her present value for her account would be \$20. She adds \$10 more. Her new present value should be \$30.

What should her principal value be? (Think about it for a second!)

The baseSupplyIndex is currently 20, the present value is currently \$30, so her principal value should be \$1.5 ($30 ÷ 20).

With this example in mind, we show the supplyBase() function from Comet.sol line 829 which is called when a lender deposits.

supplyBase function

When Alice deposits, we read her principal value (orange box), convert it to present value (green box), and add the amount just deposited to it (blue box). This is converted from a present value to a principal value stored in dstPrincipalNew (yellow box) which we store to be the new principal value for that user (red box)

The function updateBasePrincipal() (red box) above simply updates the UserBasic struct associated with the user, overwriting the old principal with dstNewPrincipal.

The opposite function, withdrawBase() (Comet.sol line 1051), applies the same logic in reverse.

What about baseBorrowIndex?

Similar to how baseSupplyIndex tracks the gain in one dollar lent since the beginning of time, baseBorrowIndex tracks the accumulation of debt for one dollar borrowed since the beginning of time. Lenders and borrowers have different interest rate curves, so two separate variables are needed to track them.

When will the indexes overflow?

Both baseSupplyIndex and baseBorrowIndex are treated as fixed point numbers with 15 decimals, so 1e15 is treated as 1.0. The largest number a signed 104 bit number can hold is 1.014e31. Therefore, the largest number the accumulator can hold is 1.014e16 (with 15 decimals).

Let’s assume that the lending protocol will never be over 100% APR. It would take 53 years for the index to overflow. Using a more realistic 10% interest rate, it would take 386 years for one dollar to compound to 10 trillion dollars.

It’s pretty reasonable to assume that Compound will upgrade to version 4 before that day comes.

Learn More with RareSkills

Please see our Solidity Bootcamp to learn more advanced Solidity concepts.

Originally Published January 5, 2024

get_D() and get_y() in Curve StableSwap

get_D() and get_y() in Curve StableSwap This article shows algebraically step-by-step how the code for get_D() and get_y() are derived from the StableSwap invariant. Given the StableSwap Invariant: $$ An^n\sum x_i +D=An^nD+\frac{D^{n+1}}{n^n\prod x_i} $$ There are two frequent math operations we wish to conduct with it: Compute $D$ given fixed values for $A$, and the […]

Fixed Point Arithmetic in Solidity (Using Solady, Solmate, and ABDK as Examples)

Fixed Point Arithmetic in Solidity (Using Solady, Solmate, and ABDK as Examples) A fixed-point number is an integer that stores only the numerator of a fraction — while the denominator is implied. This type of arithmetic is not necessary in most programming languages because they have floating point numbers. It is necessary in Solidity because […]

Uniswap V2: Calculating the Settlement Price of an AMM Swap

Uniswap V2: Calculating the Settlement Price of an AMM Swap This article explains how to determine the price settlement of a trading pair in an Automated Market Maker (AMM). It answers the question of “How many token X can be swapped for token Y from the AMM?”. The swap() function on Uniswap V2 requires you […]

How Chainlink Price Feeds Work

How Chainlink Price Feeds Work Chainlink price oracles are smart contracts with public view functions that return the price of a particular asset denominated in USD. Off-chain nodes collect the prices from various sources like exchanges and write the price data to the smart contract. Here is the smart contract for getting the price of […]