Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

Add proposal for interest bearing tokens #15927

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ module.exports = {
"proposals/optimistic_confirmation",
"proposals/embedding-move",
"proposals/rip-curl",
"proposals/interest-bearing-tokens",
]
},
],
Expand Down
331 changes: 331 additions & 0 deletions docs/src/proposals/interest-bearing-tokens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
---
title: Interest-Bearing Tokens
---

This document describes a solution for interest-bearing tokens as an on-chain
program. Interest-bearing tokens continuously accrue interest as time passes.
joncinque marked this conversation as resolved.
Show resolved Hide resolved

## Problem

Yield on blockchain assets is a crucial feature of many decentralized finance
applications, as token holders want to be compensated for providing their tokens
as liquidity to applications. The concept of "yield farming" on tokens has even
spawned very successful tongue-in-cheek projects, such as
[$MEME farming](https://dontbuymeme.com/).

Tokens that accrue interest are an attractive and popular version of yield farming,
as seen most prominently with Aave's [aTokens](https://aave.com/atokens). For
simplicity, we'll refer to interest-bearing tokens as i-tokens.

### How do these work?

Let's explain through an example: assume there's an i-token mint with a total
supply of 1,000 tokens
and a 10% annual interest rate. This means that one year later, the total supply
will be 1,100 tokens, and all token holders will have 10% more tokens in their
holding account.

Since interest is accrued continuously, however, just 1 second later, the total
supply has again increased. A 10% annualized rate gives us a continuous interest
rate of ~9.5% (`ln(1.1)`), which allows us to calculate the new instantaneous supply.

```
new_supply = previous_supply * exp(continuous_rate / seconds_per_year)
new_supply = 1100 * exp(0.095 / 31536000)
new_supply = 1100.0000033...
```

Since i-tokens are constantly accruing interest, they do not have the same
properties as normal tokens. Most importantly, i-tokens held in two different
accounts are only fungible if they have been updated to the same time. For example,
10 i-tokens in the year 1999 are worth much more than 10 i-tokens in the year 3000, since
those tokens from 1999 have over 1,000 years of interest (see Futurama). Because
of this, we have to compound interest before any minting, burning, and
transferring.
joncinque marked this conversation as resolved.
Show resolved Hide resolved

### Initial Attempt

The concept seems simple to implement. We can create an SPL token, allow
tokens to be minted by a new "interest accruing" program, and then make sure to
call it regularly.

This solution has a few problems. First, how do we compound interest regularly?
We can create a separate service which periodically sends transactions to update
all of the accounts. Unfortunately, this will cost the service a lot, especially if
there are thousands of accounts to update.

We can enforce compounding before any transfer / mint / burn, but then all programs
and web consumers need to change to make these update calls. How do we change programs
and wallets to properly update accounts without creating a big `if is_i_token` case
all over the code?

This solution creates friction for almost all SPL token users: on-chain programs,
wallets, applications, and end-users.

## Proposed Solution
joncinque marked this conversation as resolved.
Show resolved Hide resolved

Following the approach taken by Aave, we can lean on a crucial feature of i-tokens:
although the token holding amounts are constantly changing, each token holder's
proportion of total supply stays the same. Therefore, we can satisfy the
fungibility requirement for tokens on proportional ownership. The actual token
amounts used for token transfers / mints / burns are just an interface, and
everything is converted to proportional shares inside the program.

Let's go through an example. Imagine we have an i-token mint with a total of
10,000 proportional shares, a 20% annual interest rate, and 1 share is currently
worth 100 tokens. If I transfer 1,000 tokens today, the program converts that and moves
10 shares. One year later, 1 share is worth 120 tokens, so if I transfer
1,000 tokens, the program converts and only moves 8.33 shares. The same concept
applies to mints and burns.

Following this model, we never have to worry about compounding interest. We
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would the interest be compounded periodically by the runtime and stored as a number in the i-token? Or would it need to be computed every time by AmountToUiAmount and not stored?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runtime would not be involved at all for this, so it would need to be computed every time and not stored anywhere.

simply need to call the right program to perform instructions.

## Required Changes

A new interest-bearing token program is only one component of the overall solution,
since i-tokens need to be easily integrated in on-chain programs, web apps,
and wallets.

### Token Program Conformance
joncinque marked this conversation as resolved.
Show resolved Hide resolved

Since our solution entails the creation of a new token program, we're opening the
floodgates for other app developers to do the same. To ensure safe and easy integration
of new token programs, we need a set of conformance tests based on the current
SPL token test suite.
joncinque marked this conversation as resolved.
Show resolved Hide resolved

A new token program must include all of the same instructions as SPL token,
but the "unchecked" variants should return errors. Concretely, this means the
program must implement:

* `InitializeMint`
* `InitializeHolding` (previously `InitializeAccount`)
* `Revoke`
* `SetAuthority`
* `CloseHolding` (previously `CloseAccount`)
* `FreezeHolding` (previously `FreezeAccount`)
* `ThawHolding` (previously `ThawAccount`)
* `TransferChecked`
joncinque marked this conversation as resolved.
Show resolved Hide resolved
* `ApproveChecked`
* `MintToChecked`
* `BurnChecked`
* `InitializeHolding2` (previously `InitializeAccount2`)
joncinque marked this conversation as resolved.
Show resolved Hide resolved

And the program should throw errors for:

joncinque marked this conversation as resolved.
Show resolved Hide resolved
* `Transfer`
* `Approve`
* `MintTo`
* `Burn`
* `InitializeMultisig` (use a more general multisig)

New instructions required:

* `CreateHolding` (TODO name): only performs `Allocate` and `Assign` to self,
useful for creating a holding when you don't know the program that you're
interacting with. See the Associated Token Account section for how to use this.

There are also new read-only instructions, which write data to a provided account
buffer:

* `GetHoldingBalance`: balance of the holding
* `GetHoldingOwner`: owner's public key
* `GetMintSupply`: mint supply
* `GetMintAuthority`: mint authority's public key
* `GetMintDecimals`: mint decimals
joncinque marked this conversation as resolved.
Show resolved Hide resolved

See the Runtime section for more information on how these are used.

Programs and wallets that wish to support i-tokens must update the instructions
to use all `Checked` variants and use the new read-only instructions to fetch
information.

The SPL token library provides wrappers compatible with any conforming program.
For the base SPL token program, the wrapper simply deserializes the account data
and returns the relevant data. For other programs, it calls the appropriate
read-only instruction using an ephemeral account input.

The structure for new holding and mint accounts must follow the layout of the
existing SPL token accounts to ensure compatibility in RPC when fetching pre and
post token balances.

For a holding, the first 165 bytes must contain the same information
Copy link
Contributor

@CriesofCarrots CriesofCarrots Mar 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually need to require that i-token holdings conform to any particular layout? As long as the program implements a SerializeToSPL method that is called anytime a caller wants to access the account as an SPL token, it seems like the underlying i-token holding could be whatever state is most efficient for the particular program.

For instance, in the specific case of interest-bearing tokens laid out below, there wouldn't be any need to store both share amount and a cached token amount (that may always be wrong). It could be instead:

pub struct Holding {
    pub mint: Pubkey,
    pub owner: Pubkey,
    pub share_amount: u64, // SerializeToSPL => token_amount
    pub delegate: COption<Pubkey>,
    pub state: AccountState,
    pub is_native: COption<u64>,
    pub delegated_amount: u64,
    pub close_authority: COption<Pubkey>,
}

where SerializeToSPL converts share_amount to current token_amount

Copy link
Contributor

@mvines mvines Mar 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to expose SerializeToSPL over RPC as well for spl-token cli and other front-ends to use if we do this, not bad though.

I guess the counter is that we generally expect iTokens to fully implement SPL Token (and then add some), so why wouldn't they need all (or at least most of) these fields. Implementation wise I think we want to encourage a common SPL Token library crate that is built on rather than everybody forking from day 1, so we can easily publish updates for security fixes. Not having a common struct to rally around makes integration of new SPL Token releases much harder for the downstream

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to expose SerializeToSPL over RPC as well for spl-token cli and other front-ends to use if we do this, not bad though.

I think that we're going to need to do this regardless in order to return correct token amounts at any given time.

I see your point about new token programs being additive.
I suppose it is only 8 bytes. Just seems a waste, if amount is essentially going to become unused.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the docs to enforce that new token programs conform their structs to look like SPL token, along with the important difference between UI amount and amount.

as a normal SPL token holding. The following byte must be the type of
joncinque marked this conversation as resolved.
Show resolved Hide resolved
account (ie. `Holding`), and after that, any data is allowed as required by the
joncinque marked this conversation as resolved.
Show resolved Hide resolved
new token program.
joncinque marked this conversation as resolved.
Show resolved Hide resolved

The same applies for mints, but only for the first 82 bytes.
joncinque marked this conversation as resolved.
Show resolved Hide resolved

The `amount` field in an SPL token holding should contain a balance in order
to properly integrate with RPC's `preTokenBalances` and `postTokenBalances`.
joncinque marked this conversation as resolved.
Show resolved Hide resolved

For i-tokens, `amount` will be in terms of tokens, and not shares, so that UIs
can properly display amounts moved.

### Token Program Registry

We need the [token-list](https://github.com/solana-labs/token-list) to include
vetted SPL token-conforming programs and group known mints by their program id.

The token list will publish a file of known program ids at
joncinque marked this conversation as resolved.
Show resolved Hide resolved
[token-list releases](https://github.com/solana-labs/token-list/releases/latest),
to be used by RPC and programs.

TODO consider adding Rust and C versions of the registry for on-chain programs and RPC

TODO consider detailing the process of including a new program
joncinque marked this conversation as resolved.
Show resolved Hide resolved

### Runtime

#### Ephemeral Accounts
joncinque marked this conversation as resolved.
Show resolved Hide resolved

In order to reduce friction when using read-only instructions on other programs,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that having "set return value" and "get return value" syscalls would be a better approach and a better UX for devs designing programs which return computed information. Tag @jackcmay

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, yeah. I had same idea for awhile. :) cc: @joncinque

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this idea and it would be very useful outside of interest bearing tokens. For example, it would allow CPI in Anchor to look like a normal function call.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seanyoung, @jackcmay, dmakarov and I had a chat about this earlier today. @seanyoung is going to write up a brief design proposal PR soon. Basically we need the same for solang as well to return arbitrary data to the CPI caller

we need the ability for on-chain programs to dynamically create "ephemeral
accounts", which only contain data, can be passed through CPI to other programs,
and disappear after the instruction.

For a program using i-tokens, to get the balance of a holding, the program creates
an ephemeral account with 8 bytes of data, passes it to the i-token program's
`GetHoldingBalance` instruction, then reads the information back as a `u64`. At the
end of the instruction, the account disappears.

TODO What's the best approach to implement this? For example, does an ephemeral
account even need a public key?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably just a syscall, create_ephemeral_account(program_id, size), that returns an AccountInfo. No public key required, the lamport balance is 0 (and probably the instruction fails if the lamport balance is !0 when the current instruction exits).

Flesh out more details with @jackcmay though

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's what I had in mind, could add information about the granted ephemeral accounts to InvokeContext so we can track it between calls. Need to flesh out granting memory access to the program, etc...


This solution avoids the need to pass additional scratch accounts to almost all
programs that use tokens.

TODO what limits do we consider for these? A certain number of accounts?
A total amount of bytes? Regarding security, this breaks the previous
check that a CPI must use a subset of accounts provided to the calling program.
joncinque marked this conversation as resolved.
Show resolved Hide resolved

#### Dynamic sysvars

The i-token program also requires the use of dynamic sysvars. For example,
during the `GetHoldingBalance` instruction, the program needs to know the current
time in order to properly convert from token amount to share amount.
joncinque marked this conversation as resolved.
Show resolved Hide resolved

Without dynamic sysvars, we need to create a system to tell what sysvars are
needed for different instructions on each program, which creates complications
for all users.
joncinque marked this conversation as resolved.
Show resolved Hide resolved

### RPC

The validator's RPC server needs to properly handle new token program accounts
for read-only instructions and pre / post balances.
joncinque marked this conversation as resolved.
Show resolved Hide resolved

#### `getBalance` and `getSupply`

Instead of deserializing SPL token accounts and returning the data, the
RPC server runs read-only transactions on the i-token program.

RPC simply uses the same flow as on-chain programs: create an ephemeral account
then runs the read-only transaction onto the bank requested.

TODO how do we set the cost of read-only transactions? Do we lower it specially
joncinque marked this conversation as resolved.
Show resolved Hide resolved
for read-only RPC calls? The i-token program will probably be one of the more
cost-intensive computations possible since it is performing present value
discounting.

#### `preTokenBalances` and `postTokenBalances`

As mentioned earlier in the Token Program Conformance section, the i-token
program caches balances in the `amount` field of the SPL token holding. This
should only be used by RPC, and not by programs to get the balance of a non-SPL
holding.

#### New Secondary Indexes

Token-specific RPC calls need smarter secondary indexes to pick up accounts from
new token programs. These include:

* `getTokenAccountBalance`
* `getTokenAccountsByDelegate`
* `getTokenAccountsByOwner`
* `getTokenLargestAccounts`
* `getTokenSupply`

TODO any others?

#### Vetted Token Programs

RPC will learn about the known program ids through a file published in the
[token-list releases](https://github.com/solana-labs/token-list/releases/latest),
downloaded automatically at startup.

### Associated Token Program

The Associated Token Program needs to support all token programs seamlessly.
Currently, it performs the following sequence:

* `SystemInstruction::Transfer` the required lamports to make the SPL holding account rent-exempt
* `SystemInstruction::Allocate` space for the SPL holding account
* `SystemInstruction::Assign` the holding account to the SPL token program
* `InitializeHolding` on SPL token program

This does not work for an opaque token program, because we do not know the size required
joncinque marked this conversation as resolved.
Show resolved Hide resolved
from the outside. Conversely, if we allow a token program to take the lamports it wants,
a malicious token program could take too much from users.

To get around this, the Token Program interface has a new `CreateHolding`
joncinque marked this conversation as resolved.
Show resolved Hide resolved
instruction, which only allocates space and assigns the account to the program.
Once the space is allocated, the Associated Token Program transfers the
required lamports, and returns an error if that number is too large. The process
becomes:

* call `CreateHolding` on the token program (allocate and assign)
* calculate rent requirement based on the data size of holding account
* `SystemInstruction::Transfer` the required lamports to make the holding account rent-exempt
* call `InitializeHolding` on the token program

### Web3 / Wallets

To properly support i-tokens, wallets and applications will need to make the
following changes:

* get holdings: query for holdings owned by the public key, for all official
token programs listed in the registry (not just `Tokenkeg...`)
joncinque marked this conversation as resolved.
Show resolved Hide resolved
* get balances: avoid deserializing holding data, and instead use
`getBalance` from RPC
* transfer tokens: only use `ApproveChecked` and `TransferChecked` on the token
program
* create holding: avoid directly creating an account and initializing,
and use the associated token account program instead

joncinque marked this conversation as resolved.
Show resolved Hide resolved
### On-chain Programs

To properly support i-tokens on-chain, programs will need to make these changes:

* multiple token programs: all programs that support SPL Tokens must always
include the program id of the token(s) holdings they're passing in.
For example, token-swap needs to accept separate token programs for token A and B
* hard-coded token program: avoid using `Tokenkeg...` directly, and delete `id()`
the SPL token code
* get balance: instead of deserializing the account data into an SPL holding, use
the new wrapper to deserialize or call `GetHoldingBalance` instruction with the
appropriate token program
* get owner: same as above, but using the `GetHoldingOwner`
* get supply: same as above, but using the `GetMintSupply`
* get mint authority: same as above, but using the `GetMintAuthority`
* get mint decimals: same as above, but using the `GetMintDecimals`
* transfer: only use `TransferChecked`, which requires passing mints
joncinque marked this conversation as resolved.
Show resolved Hide resolved

## Other New Token Programs

For awhile now, people have been coming up with ideas that likely require a new
token program to implement. These include:

### Voting Tokens
ryoqun marked this conversation as resolved.
Show resolved Hide resolved

Outlined in a [GitHub issue](https://github.com/solana-labs/solana-program-library/issues/131),
voting tokens use additional data about how long a token has been held.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The opposite logic would be to make it possible to lock tokens for certain amount of time to know for how long they would be held. This would allow to calculate voting power based on the long term commitment (skin in the game).
Users don't really like to deposit governance tokens to vote. It makes them very uncomfortable especially when the governance tokens they posses have substantial economic value.
A better solution would be to allow the tokens to be locked to gain voting power while still being in user's possession.


### Non-Fungible Tokens

It will be possible to add a lot of metadata regarding owners and traders, either
on a new mint or NFT account.

### Allow-list Tokens

Only a certain list keys are allowed (or not) to transfer to tokens, controlled
by the mint.