1 of 79

Computing and Accumulating Interest On-chain

Austin Williams

2 of 79

Part I: Computing interest on-chain

How to compute interest in a land of integers

3 of 79

Simple interest

A = P + rtP

4 of 79

Simple interest

A = P + rtP

total Amount owed

5 of 79

Simple interest

A = P + rtP

total Amount owed

the Principal (original amount borrowed)

6 of 79

Simple interest

A = P + rtP

total Amount owed

the Principal (original amount borrowed)

the amount of Time that has passed. (units matter)

7 of 79

Simple interest

A = P + rtP

total Amount owed

the Principal (original amount borrowed)

the amount of Time that has passed (units matter)

the Rate at which interest accumulates per unit of time (not including any compounding). AKA “nominal interest rate” or “simple annual interest rate”.

8 of 79

Simple interest

A = P + rtP

the total amount of interest that has accrued

9 of 79

Simple interest

A = P (1+ rt)

factoring out P to get the more common expression

10 of 79

Compounding

Suppose you want to add the interest to the principal. And suppose you want to do this with a frequency of n times per unit of time.

Then the amount that you would owe after the first 1/n unit of time would be:

11 of 79

Compounding

Suppose you want to add the interest to the principal. And suppose you want to do this with a frequency of n times per unit of time.

Then the amount that you would owe after the first 1/n unit of time would be:

simplify

12 of 79

Compounding

Suppose you want to add the interest to the principal. And suppose you want to do this with a frequency of n times per unit of time.

Then the amount that you would owe after the first 1/n unit of time would be:

13 of 79

Compounding

Suppose you want to add the interest to the principal. And suppose you want to do this with a frequency of n times per unit of time.

Then the amount that you would owe after the first 1/n unit of time would be:

The amount that you would owe after the second 1/n unit of time would be:

14 of 79

Compounding

Suppose you want to add the interest to the principal. And suppose you want to do this with a frequency of n times per unit of time.

Then the amount that you would owe after the first 1/n unit of time would be:

The amount that you would owe after the second 1/n unit of time would be:

15 of 79

Compounding

Suppose you want to add the interest to the principal. And suppose you want to do this with a frequency of n times per unit of time.

Then the amount that you would owe after the first 1/n unit of time would be:

The amount that you would owe after the second 1/n unit of time would be:

The amount that you would owe after the third 1/n unit of time would be:

16 of 79

Compounding

Suppose you want to add the interest to the principal. And suppose you want to do this with a frequency of n times per unit of time.

Then the amount that you would owe after the first 1/n unit of time would be:

The amount that you would owe after the second 1/n unit of time would be:

The amount that you would owe after the third 1/n unit of time would be:

17 of 79

Compounding

Suppose you want to add the interest to the principal. And suppose you want to do this with a frequency of n times per unit of time.

Then the amount that you would owe after the first 1/n unit of time would be:

The amount that you would owe after the second 1/n unit of time would be:

The amount that you would owe after the third 1/n unit of time would be:

The amount that you would owe after the nth 1/n unit of time (that is, after 1 unit of time) would be:

18 of 79

Compounding

The amount that you would owe after the nth 1/n unit of time (that is, after 1 unit of time) would be:

19 of 79

Compounding

So the amount that you would owe after t units of time would be:

The amount that you would owe after the nth 1/n unit of time (that is, after 1 unit of time) would be:

20 of 79

Compounding

So the amount that you would owe after t units of time would be:

Two quick notes:

  1. This is the standard compound interest formula.�
  2. It is possible to compute compound interest by using the simple interest formula (A = P(1 + rt)) iteratively -- which is what we just did to arrive at the compound interest formula. Indeed, this is how Compound computes compound interest.

21 of 79

Compounding

Total amount owed after after t units of time on a loan with an initial principal amount P, earning interest at a nominal rate r, and compounding n times per unit of time:

22 of 79

Continuous Compounding

Total amount owed after after t units of time on a loan with an initial principal amount P, earning interest at a nominal rate r, and compounding n times per unit of time:

23 of 79

Continuous Compounding

Total amount owed after after t units of time on a loan with an initial principal amount P, earning interest at a nominal rate r, and compounding n times per unit of time:

“Euler’s number” (base of natural log) 2.71828

24 of 79

Continuous Compounding

Total amount owed after after t units of time on a loan with an initial principal amount P, earning interest at a nominal rate r, and compounding n times per unit of time:

Total amount owed after after t units of time on a loan with an initial principal amount P, earning interest at a nominal rate r, and compounding continuously:

25 of 79

Continuous Compounding

Total amount owed after after t units of time on a loan with an initial principal amount P, earning interest at a nominal rate r, and compounding n times per unit of time:

Total amount owed after after t units of time on a loan with an initial principal amount P, earning interest at a nominal rate r, and compounding continuously:

Goal: compute these values on-chain...

26 of 79

Challenges

27 of 79

Challenges

Example: �

  • Borrow 1.5 ETH
  • 10%/year nominal interest rate
  • 30 day loan
  • Time measured in years

28 of 79

Challenges

Example: �

  • Borrow 1.5 ETH
  • 10%/year nominal interest rate
  • 30 day loan
  • Time measured in years

uint256

float

float

float

29 of 79

Challenges

Example: �

  • Borrow 1.5 ETH
  • 10%/year nominal interest rate
  • 30 day loan
  • Time measured in years

uint256

float

float

float

Non-integer exponents 😬.

Very expensive to compute.

Can we do better? 🤔

30 of 79

Challenges

Example: �

  • Borrow 1.5 ETH
  • 0.0000003170979%/second nominal interest rate
  • 2,592,000 second loan
  • Time measured in seconds

uint256

float

float

uint256

Measure time in seconds.

Write nominal interest in seconds.

31 of 79

Challenges

Example: �

  • Borrow 1.5 ETH
  • 0.0000003170979%/second nominal interest rate
  • 2,592,000 second loan
  • Time measured in seconds

uint256

float

float

uint256

One of the factors in the exponent is now a uint256, but the other is still a float. 😫

😔

😊

32 of 79

Challenges

Example: �

  • Borrow 1.5 ETH
  • 0.0000003170979%/second nominal interest rate
  • 2,592,000 second loan
  • Time measured in seconds

uint256

float

float

uint256

One of the factors in the exponent is now a uint256, but the other is still a float. 😫

😔

😊

33 of 79

Challenges

Example: �

  • Borrow 1.5 ETH
  • 0.0000003170979%/second nominal interest rate
  • 2,592,000 second loan
  • Time measured in seconds

uint256

float

float

uint256

(Side note: The fact that e is irrational is not part of the problem. The only problem is that the exponent is not a non-negative integer)

34 of 79

Challenges

Total amount owed after after t units of time on a loan with an initial principal amount P, earning interest at a nominal rate r, and compounding n times per unit of time:

Total amount owed after after t units of time on a loan with an initial principal amount P, earning interest at a nominal rate r, and compounding continuously:

?

35 of 79

Challenges

Example: �

  • Borrow 1.5 ETH
  • 10%/year nominal interest rate
  • 30 day loan
  • Time measured in years
  • Compounded once per second (n=31,104,000)

uint256

float

float

uint256

36 of 79

Challenges

Example: �

  • Borrow 1.5 ETH
  • 10%/year nominal interest rate
  • 30 day loan
  • Time measured in years
  • Compounded once per second (n=31,104,000)

uint256

float

float

uint256

Again, we have a non-integer in the exponent. Let’s try measuring time in seconds again...

37 of 79

Challenges

Example: �

  • Borrow 1.5 ETH
  • 0.0000003170979%/second nominal interest rate
  • 2,592,000 second loan
  • Time measured in seconds
  • Compounded once per second (n=1)

uint256

float

uint256

38 of 79

Challenges

Example: �

  • Borrow 1.5 ETH
  • 0.0000003170979%/second nominal interest rate
  • 2,592,000 second loan
  • Time measured in seconds
  • Compounded once per second (n=1)

uint256

float

uint256

Integer exponent! 🎉

39 of 79

Challenges

Example: �

  • Borrow 1.5 ETH
  • 0.0000003170979%/second nominal interest rate
  • 2,592,000 second loan
  • Time measured in seconds
  • Compounded once per second (n=1)

uint256

float

uint256

Integer exponent! 🎉

Bonus points for eliminating a division

40 of 79

Challenges

Total amount owed after after t units of time on a loan with an initial principal amount P, earning interest at a nominal rate r, and compounding n times per unit of time:

41 of 79

Challenges

Total amount owed after after t units of time on a loan with an initial principal amount P, earning interest at a nominal rate r, and compounding n times per unit of time:

Measure time in seconds, compound once per second, and we get something very close to continuous compounding that can be computed efficiently on-chain:

42 of 79

Challenges

Total amount owed after after t units of time on a loan with an initial principal amount P, earning interest at a nominal rate r, and compounding n times per unit of time:

Measure time in seconds, compound once per second, and we get something very close to continuous compounding that can be computed efficiently on-chain:

The difference between this approach and actual/proper continuous compounding is less than 9 cents for a 1 year loan of $1M at a nominal interest rate of 10%/year. That’s very good!

43 of 79

Efficiency

An arbitrary fixed-precision base can be raised to a non-negative integer exponent, t, using O(log(t)) fixed-point multiplications by using the “exponentiation by squaring” algorithm.

44 of 79

Efficiency

An arbitrary fixed-precision base can be raised to a non-negative integer exponent, t, using O(log(t)) fixed-point multiplications by using the “exponentiation by squaring” algorithm.

This is how Maker computes interest. They implement the “exponentiation by squaring” algorithm in the rpow function of their Jug and Pot contracts.

45 of 79

“Manual Method”

You can also compute compound interest “manually” as we alluded to earlier.

A = P (1+ rt)

The idea is to update the amount owed using the simple interest formula once per block (or as close to that as possible).

46 of 79

“Manual Method”

You can also compute compound interest “manually” as we alluded to earlier.

A = P (1+ rt)

The idea is to update the amount owed using the simple interest formula once per block (or as close to that as possible).

This has the effect of compounding once every 15 seconds, assuming you update every block.

Plenty accurate if updated frequently. If updated every block, the difference between “manual compounding” and continuous compounding of a 1 year, $1M loan with a 10% nominal interest rate is, again, less than 9 cents.

47 of 79

“Manual Method”

You can also compute compound interest “manually” as we alluded to earlier.

A = P (1+ rt)

The idea is to update the amount owed using the simple interest formula once per block (or as close to that as possible).

This has the effect of compounding once every 15 seconds, assuming you update every block.

Plenty accurate if updated frequently. If updated every block, the difference between “manual compounding” and continuous compounding of a 1 year, $1M loan with a 10% nominal interest rate is, again, less than 9 cents.

This is how Compound computes interest.

48 of 79

Comparison

“Manual” Compounding

49 of 79

Comparison

“Manual” Compounding

  • Each call can be computed in O(log(t))
  • Each call can be computed in O(1)

X

50 of 79

Comparison

“Manual” Compounding

  • Each call can be computed in O(log(t))
  • Will update accurately even if not called for a long time�

  • Each call can be computed in O(1)
  • Must be updated frequently in order to remain accurate.�

X

X

51 of 79

Comparison

“Manual” Compounding

  • Each call can be computed in O(log(t))
  • Will update accurately even if not called for a long time�
  • Gas required to compute/maintain accurate answer over t seconds: O(log(t)).
  • Each call can be computed in O(1)
  • Must be updated frequently in order to remain accurate.�
  • Gas required to compute/maintain accurate answer over t seconds: O(t).�

X

X

X

52 of 79

Comparison

“Manual” Compounding

  • Each call can be computed in O(log(t))
  • Will update accurately even if not called for a long time�
  • Gas required to compute/maintain accurate answer over t seconds: O(log(t)).�
  • Code complexity: medium (must implement exponentiation by squaring).
  • Each call can be computed in O(1)
  • Must be updated frequently in order to remain accurate.�
  • Gas required to compute/maintain accurate answer over t seconds: O(t).�
  • Code complexity: easy (no non-native math operations needed)

X

X

X

X

53 of 79

Comparison

“Manual” Compounding

  • Each call can be computed in O(log(t))
  • Will update accurately even if not called for a long time�
  • Gas required to compute/maintain accurate answer over t seconds: O(log(t)).�
  • Code complexity: medium (must implement exponentiation by squaring).
  • Each call can be computed in O(1)
  • Must be updated frequently in order to remain accurate.�
  • Gas required to compute/maintain accurate answer over t seconds: O(t).�
  • Code complexity: easy (no non-native math operations needed)

X

X

X

X

No clear winner. Both are good.

54 of 79

Part II: Accumulating interest

How to accumulate interest for several users at once

55 of 79

The problem

  • You may have many users lending/borrowing simultaneously.�
  • They may all open/close their loans at different times.�
  • The loans may have different durations.�
  • The interest rate (r) is often a global variable set dynamically. It may update several times during a loan’s existence.�

Given all of this, how do we go about tracking all of the interest from all of our loans accurately and efficiently?

56 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

57 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

58 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

59 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

This is how it is done by:�

  • Compound (“IOUs” = “cTokens”)
  • Maker/DAI DSS (“IOUs” = “savings DAI”)
  • Synthetix (“IOUs” = “Synths”)
  • Etc, etc

60 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

Contract

Example: Accumulating interest while lending...

61 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

Contract

Deposits X tokens

Example: Accumulating interest while lending...

62 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

Contract

Deposits X tokens

Receives

X / exchangeRate

IOUs

X / exchangeRate

IOUs

1

1

Example: Accumulating interest while lending...

63 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

Contract

X / exchangeRate IOUs

Time passes….

exchangeRate var gets updated (increases) over time...

1

Example: Accumulating interest while lending...

64 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

Contract

Sends the

X / exchangeRate

IOUs back to the contract

1

Example: Accumulating interest while lending...

65 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

Contract

Sends the

X / exchangeRate

IOUs back to the contract

1

tokens

tokens

Receives

Example: Accumulating interest while lending...

66 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

Contract

tokens

Example: Accumulating interest while lending...

Done!

Notice that we never had to record the times when the loan started or stopped.��The exchange rate was not account-specific.

This could have been any user at any time over any duration.

67 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

Contract

Example: Accumulating interest while borrowing...

Start off with X IOUs

(acquired by first putting depositing some token, just like the lender did in the previous example)

68 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

Contract

Example: Accumulating interest while borrowing...

Sends the

X IOUs to the contract to be “locked”

69 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

Contract

Example: Accumulating interest while borrowing...

Sends the

X IOUs to the contract to be “locked”

Allowed to borrow up to

tokens, where c is determined by the max LTV ratio

tokens

70 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

Contract

Example: Accumulating interest while borrowing...

tokens

Time passes….

exchangeRate var gets updated (increases) over time...

71 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

Contract

Example: Accumulating interest while borrowing...

tokens

Sends

tokens back to the contract

72 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

Contract

Example: Accumulating interest while borrowing...

tokens

Sends

tokens back to the contract

73 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

Contract

Example: Accumulating interest while borrowing...

Sends

tokens back to the contract

Contract unlocks the original X IOUs

X IOUs

74 of 79

IOUs

Very common solution is to use “IOUs” and update the exchange rate between IOUs and their underlying token.

Global var: r (interest rate)

Global var: exchangeRate (between IOUs and the underlying token).

init exchangeRate = 1 when contract is deployed.

Before any action that looks at the exchange rate, update the exchangeRate variable via:

exchangeRate = exchangeRate(1+r)^t.

Contract

Example: Accumulating interest while borrowing...

X IOUs

Done!

Again, no times needed to be recorded.��The exchange rate was not account-specific.

Could have had hundreds of these happening at once, and still only need to update one var: exchangeRate

75 of 79

IOUs

Benefits:

  • Only need to update two global vars (exchangeRate and r).�
  • No need to do anything special on an account-by-account or loan-by-loan basis. Very gas efficient.�
  • IOUs can be transferable (can make them ERC20 tokens).�
  • No need for price oracle.

76 of 79

IOUs

Benefits:

  • Only need to update two global vars (exchangeRate and r).�
  • No need to do anything special on an account-by-account or loan-by-loan basis. Very gas efficient.�
  • IOUs can be transferable (can make them ERC20 tokens).�
  • No need for price oracle.

Limitations:

  • Same interest rate, r, must apply to all accounts/loans.

77 of 79

Quick Security Notes ⚠️

  • The exchange rate should always be updated BEFORE:

  1. Any other adjustment to the user’s balance (deposit/withdraw/borrow/payback/etc)�
  2. Any computation of the LTV ratio (if there is an “overcollateralized” loan involved)�
  3. Any change to the global interest rate r
  4. r < -1 should be avoided (results in negative base or underflow when computing interest)�
  5. -1 < r < 0 are negative interest rates (obviously). Account values decrease over time. Math still works (base of the exponentiation is positive but less than 1). May result in unwanted incentives.

78 of 79

Possible Opportunities

  • Extend OZ Contract’s ERC20 token to support this “interest accumulating IOU” pattern.
    • E.g., make an “asset-backed, ERC20 token” that follows this pattern
    • I would recommended formalizing it via an EIP first
    • I would not recommend cooking the loan pattern into the token.
      • Separation of concerns:
        • Asset-backed “IOU” token that can accumulate interest
        • Overcollateralized loan contract�
  • Add fixed-precision library to OZ Contracts and be sure to include an implementation of the exponentiation by squaring algorithm (see Maker’s `rpow`)

79 of 79

The End