solana-labs / solana-program-library

A collection of Solana programs maintained by Solana Labs
https://solanalabs.com
Apache License 2.0
3.46k stars 2.03k forks source link

token: Support interest bearing tokens #1207

Closed jstarry closed 2 years ago

jstarry commented 3 years ago

Problem

There is no clear solution to implementing interest bearing tokens with the SPL Token program. Interest bearing tokens are tokens whose balance changes over time due to interest accrual and have a pegged exchange rate to the underlying asset.

For example, on a borrow lending protocol, after lending out a token like USDC, you will receive equivalent amount of intUSDC to represent your lending position. Over time, the amount of intUSDC increases and the amount is exchangeable for equivalent amount of USDC.

Proposed Changes

TBD. Discussion needed!

jstarry commented 3 years ago

Potential Approaches:

1) Create new interest bearing token program 2) Adopt a programming model similar to ERC20 where programs can implement a token interface for full customization 3) Add programmable hooks into the existing SPL Token program which can be configured per mint 4) Find a way to support this feature by composing the existing SPL Token program 5) Add interest functionality into the existing SPL Token program

joncinque commented 3 years ago

We might be able to do this with composition if we follow the lending model for collateral tokens. We create a special program which manages interest-bearing tokens, containing two SPL token mints and an owner:

struct InterestBearingMint {
  non_fungible_token_mint: SplMint, // non-fungible tokens that actually gain interest, OR a copy of SplMint that only exposes a few operations
  percent_of_ownership_fungible_token_mint: SplMint, // fungible tokens that can be freely transfered / traded
  internal_mint_authority: Pubkey, // program address that has authority over both SPL mints  
  owner: Pubkey // the owner who can mint new tokens of either type, since the two mints will be controlled by internal_mint_authority
}

The concept is that at any point, to transfer interest-bearing tokens, they need to be converted to the fungible version, which represents your overall share in the pool of interest-bearing tokens. Interest accrues on all interest-bearing tokens at the same rate, so the amount of tokens is constantly changing, but your share of ownership remains constant.

For example, let's say I have 50 interest-bearing tokens out of 1000, and that 5% stake gets me 5 fungible tokens out of 100. If the interest is 2%, after one year I should have 51 out of 1020 tokens interest-bearing tokens, still 5% of the total, worth 5 fungible tokens. At any point, I can trade those 5 fungible tokens for anything else without worrying about interest accrual. As soon as I want to use the interest-bearing token again, I convert the fungible tokens back to interest-bearing NFTs.

If I mint 102 interest-bearing tokens, the total amount increases by 10%, so the total number of possible fungible tokens will go up by 10.

OR

If I mint 10 fungible tokens, the total amount increases by 10%, so the total of non-fungible tokens increases by 102.

There are few questions with this model:

Our solution might end up being a combination of a special interest-bearing token and a separate fungible conversion program. Thoughts on this approach?

jstarry commented 3 years ago

@joncinque nice proposal! I like the notion of having higher order tokens which can be mapped to fractional spl tokens.

This approach allows us to stick with a clearly defined minimal behavior for spl tokens while providing a pattern for creating higher order tokens which could be explicitly supported by future defi programs if desired. It feels that interest bearing, yield bearing, and rebase-type tokens should always have explicit opt-in if their balance can drift over time.

Not sure whether you were thinking of using SPL token NFT's but it could be interesting to make that work. Treating derivative tokens as SPL token NFT's seems difficult given how we currently recommend devs to implement NFT's with a unique mint + single minted token. NFT's would need to track when interest was last accrued on their balance so they would need to be stateful. Since SPL NFT's don't allow custom data, this would need to be a separate account. Maybe we could make some changes there.. it feels useful to group NFT's under a common mint despite not being fungible.

It would be cooler if those interest-bearing tokens were actually fungible though and it would be nice if it followed a similar interface to the SPL token program for transfers, mints, burns, etc. In an ideal world, I imagine defi programs explicitly enabling different types of programmable tokens with minimal code changes but it feels difficult when you consider that different types of tokens would need to reference additional accounts to inform how interest is accrued, among other things.

what operations do we permit with the interest-bearing NFTs?

I imagine this is what people hold by default since they are easiest to reason about.. if they follow some standard interface, wallets could provide a nice experience which shows live balance, for example. That feels difficult if people just hold the fractional spl token equivalent.

why would anyone ever hold the interest-bearing version? is it just used for certain transactions?

I imagine some protocols would be created to explicitly support them (imagine swapping various interest bearing tokens in a pool). It could also help prevent rounding errors whenever you convert to the fungible token equivalent.

I also imagine that contracts like the lending protocol would exclusively deal in interest bearing tokens for nice book-keeping. It wouldn't need to know about the fractional spl tokens.

how do wallets integrate with these? Maybe the frontend does a slick conversion to show the value of fungible tokens and doesn't show interest-bearing tokens

I propose first-class support for interest bearing tokens over the fungible ones since we would presumably be able to pack everything a wallet needs to know into those accounts / mint.

If we did some slick conversion, we would probably want some way to have the fractional token mint somehow indicate its relationship to the interest bearing mint. I don't think we have a good answer for that yet.

joncinque commented 3 years ago

Not sure whether you were thinking of using SPL token NFT's but it could be interesting to make that work.

if they follow some standard interface, wallets could provide a nice experience which shows live balance, for example.

Probably a modded version of SPL NFTs would be best then. It could use the SPL Token ABI for as many instructions as possible, and even the account structure could look similar for first X bytes, and then define additional instructions / fields as needed for things like interest accrual. Clients / wallets need to be careful when deserializing accounts to support all kinds of tokens.

I propose first-class support for interest bearing tokens over the fungible ones since we would presumably be able to pack everything a wallet needs to know into those accounts / mint.

You're right, that should be the top priority

If we did some slick conversion, we would probably want some way to have the fractional token mint somehow indicate its relationship to the interest bearing mint.

Yeah that part does get tricky. We might need to adopt a separate standard over SPL token in that case to provide any token metadata, which I know has been on the to-do list for awhile.

jstarry commented 3 years ago

Yeah that part does get tricky. We might need to adopt a separate standard over SPL token in that case to provide any token metadata, which I know has been on the to-do list for awhile.

Yeah, this seems like the main problem to solve actually. Just discussed this a bit with @bartosz-lipinski and we're thinking that if we have multiple token programs that we want wallets and programs to call transparently, we'll need an ABI for defining the accounts needed. Otherwise the complexity of adding support for different token types will be unruly.

Also, clients need to know how to construct an instruction which has all dependent accounts and this is difficult when you start trying to provide accounts to a program which itself needs to pass different accounts depending on the token type being used. This account dependency tree becomes a big challenge to manage manually and likely necessitates having an ABI standard which provides all the info needed for both off-chain clients and on-chain programs.

defactojob commented 3 years ago

Coming from the ETH world, it seems that having the ABI on top of the EVM is what makes DeFi composability possible. The designers of ERC20 most likely never thought of rebase tokens, yet today they are getting traction in the form of algo stables and interest bearing tokens.

Also, clients need to know how to construct an instruction which has all dependent accounts and this is difficult when you start trying to provide accounts to a program which itself needs to pass different accounts depending on the token type being used. This account dependency tree becomes a big challenge to manage manually and likely necessitates having an ABI standard which provides all the info needed for both off-chain clients and on-chain programs.

Adding my 2c to this discussion, a preliminary proposal for Solana Program ABI:

https://github.com/solana-labs/solana-program-library/issues/1247

ryoqun commented 3 years ago

@jstarry @joncinque hehe, higher order token sounds cool. engineers love abstraction. :)

Here's my wild/jot-down proposal out of blue.

Sadly, I don't follow all these discussed derived token mechanism... But as a different angle in the design space, how about supporting these as a runtime feature? Please pardon my ignorance.

Well, I just noticed we could revisit the currently-not-so-useful eager rent collection mechanism to support these periodic account update.

(context: eager rent collection is a rent-collection mechanism where we scan whole accounts on the chain ordered by pubkey. what we can exploit here is that we can update each account per an epoch in loaded-spreaded way.) Put differentally, this is like gc-managed programming language's runFinalizer. Or general runtime-triggered hooks into objects (accounts in our case). Dunno there is ethereum equivalent...

detail:

Basicallly, at each slot, runtime synthesizes artifical transactions with registered program id for hooks for small subset of accoutns as writable. Then the hooked program are free to update the account as it likes referencing fixed set of read-only accounts.

pros:

cons:

If this sounds worth to chase, I'm happy to write more properly... ;)

joncinque commented 3 years ago

This is a really great write-up, thanks @ryoqun !

We did wonder about the possibility of pre/post effects whenever an account is touched or written by adding transactions. It would be a really big runtime advantage, since often people want to automatically run instructions. If we go down this path, I like the idea of limiting these to run once per epoch as a start. We could even allow a program writer to define an instruction to be run on every epoch through an entrypoint!-like macro. They can register the instruction directly with the runtime on program deployment and put funds into an account which pays for all of the transactions. Maybe this can all be done through another version of the BPF loader.

I know there's a desire to avoid complicating the runtime, but this would be a great feature, and it's tough to imagine putting it anywhere else.

jstarry commented 3 years ago

For interest bearing tokens, I don't find epoch-frequency updates to be nearly as useful as slot-frequency updates. Feels to me that a lazy hook-style approach that runs at most once a slot would be sufficiently powerful.

They can register the instruction directly with the runtime on program deployment and put funds into an account which pays for all of the transactions. Maybe this can all be done through another version of the BPF loader.

This seems like a cool idea but it feels inevitable that those funds are going to be depleted and cause issues. I think it'd be cooler if programs with hooks just had higher transaction fees to cover the cost of the lazy hook.

ryoqun commented 3 years ago

if we go down this path, I like the idea of limiting these to run once per epoch as a start.

For interest bearing tokens, I don't find epoch-frequency updates to be nearly as useful as slot-frequency updates. Feels to me that a lazy hook-style approach that runs at most once a slot would be sufficiently powerful.

Yeah, I've looked into ethereum counterparts; and now think per epoch update is too slow (or not fun) So, my current position is that solana's stake rewards are too infrequent. xD

And per slot or other variants of less-than-epoch hooks might have unpredictable runtime perf impact... If this is the real concern, we are might be better off just exposing tick instruction (which anyone can submit transaction at that and will be executed at-most per N slots).

ryoqun commented 3 years ago

Also, I heard about a desire to customize spl-token instructions for custom transferability logic for NFT.

Adopt a programming model similar to ERC20 where programs can implement a token interface for full customization

So, this might be more proper solution.

Sorry for dispersion of discussion but I'm regarding we're now at the crux of our spl-token composition future. :)

aeyakovenko commented 3 years ago

The actual balance doesn't change, just a global reference that is used by wallets. The tokens on ethereum still maintain the same balance, and during a transfer the actual bits moved just represent shares.

joncinque commented 3 years ago

Proposal added at https://github.com/solana-labs/solana/pull/15927

joncinque commented 2 years ago

Closed with #3013