solana-labs / solana-program-library

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

[Token-2022] [Feature] YieldBearing extension #5170

Closed wjthieme closed 2 months ago

wjthieme commented 1 year ago

The problem

Right now when tokens want to distribute yield, dividends, profits, airdrops, etc. to their token holders they will need to do so off-chain (not very decentralized). It would be great to have a mechanism to be able to automatically distribute yield to holders of a token in a transparent and decentralized manner.

Some potential examples (that I came up with):

The high level solution

What if we add an extension to spl-token-2022 that will make this possible. This extension will take the address of another spl-token in which the yield is denoted (could be wSol, usdc, etc.) [after this: yieldToken] and add two instructions to the spl-token-2022 program.

Low level design

In order for this to work we require a couple of things:

A YieldBearing mint extension that keeps track of the following data.

publicKey of yieldToken
u64 of yieldPerUnit
u64 of pendingYield

The yieldPerUnit and pendingYield together keep track of the cumulative yield that has been distributed into the token. yieldPerUnit basically states how much yield a single unit of token is allowed to withdraw. pendingYield is basically the partial yieldPerUnit that is left after depositing. This will probably all make more sense when you see the math behind withdrawing and depositing

A YieldBearingAccount account extension that keeps track of how much dividend a user has withdrawn. It will contain the following data

i128 redeemedYield

The redeemedYield states how much yield this particular holder has already withdrawn. It is a i128 type because it needs to as large as possible (as we keep track of cumulative yield and cumulative redeemed yield) and be able to go negative (more on that later).

A initializeYieldBearingMint Instruction on spl-token-2022

This is pretty straightforward. When initializing the mint we need to populate the publicKey of yieldToken. This will be the publicKey of the spl-token in which the dividends are denoted. Potentially we would also need to know if it is an normal spl-token or an spl-token-2022. If needed we can add that here as well.

Unfortunately the way this is designed it is not possible to change the yieldToken after initializing (as this would mess up pretty much all the stored data, etc.). Therefore there is also no yieldTokenAuthority and updateYieldBearingMint or anything like that.

A depositYield instruction on spl-token-2022

This instruction will transfer x amount of yieldToken and allocate that to the current token holders. The way this works is as follows:

transfer x tokens of yieldToken to the ata of the token mint
let yield = x + pendingYield;
yieldPerUnit += yield / totalSupply
pendingYield = yield % totalSupply;

Each time yield is deposited we add that to the cumulative yield (yieldPerUnit + pendingYield).

A withdrawYield instruction on spl-token-2022

This instruction allows any token holder to withdraw their portion of the yield. The way this works is as follows:

transfer x tokens of yieldToken from the mint ata to the token holder
user.redeemedYield += x

To calculate the allowance for a specific token holder we can use the following math

let allowance = yieldPerUnit * tokenBalance - redeemedYield

Now there will be a couple cases where we need to alter redeemedYield when certain actions happen

When tokens are transferred we need to update redeemedYield

When tokens are transferred we need to make sure that the user receiving the token cannot withdraw the same yield again. There are two potential ways we can go with this.

Transfer the yield together with the tokens which. Downside: users might unknowingly transfer their tokens without having redeemed their yield.

let allowance = yieldPerUnit * yieldTokenBalance - redeemedYield
let requiredTransfer = allowance * transferAmount / tokenBalance
let automaticTransfer =  transferAmount * yieldPerUnit
let manualTransfer = requiredTransfer - automaticTransfer
sender.redeemedYield += manualTransfer
receiver.redeemedYield -= manualTransfer

Leave the yield with the original token holder which has a simpler implementation. Downside: might cause problems when tokens are staked, in LPs, etc. as yield will accumulate there and get stuck.

let yieldTransfer = transferAmount * yieldPerUnit
sender.redeemedYield -= yieldTransfer
receiver.redeemedYield += yieldTransfer

I'm leaning towards option one as this would allow users to keep accumulating yield even while their tokens are staked, in LPs, etc.

When tokens are minted we need to update redeemedYield

Newly minted tokens should come without yield. For each minted token we need to increase the redeemedYield of the receiver by yieldPerUnit.

receiver.redeemedYield += mintedAmount * yieldPerUnit

When tokens are burned we need to update redeemedYield

When tokens are burned we need to make sure that the user can still withdraw their yield that they have accumulated. For that we need to decrease the redeemedYield.

let allowance = yieldPerUnit * yieldTokenBalance - redeemedYield
let allowancePerToken = allowance * transferAmount / tokenBalance
user.redeemedYield -= burnAmount * allowancePerToken

Block closing a token account when there is still yield to withdraw

It doesn't make sense to allow a user to close their token account if they still have yield that they can withdraw since they will loose their yield if they close the token account. We can block closing if there is still yield to withdraw.

Closing note

Most of this will / should be possible when using a TransferHook extension but having a dedicated extension will make it a lot easier for teams to implement a feature like this. The only issue that someone implementing it that way might run into is the update of redeemedYield when tokens are burned (as there is currently no way to intercept that I believe).

joncinque commented 1 year ago

I might be missing something at the fundamental level. You say:

This extension will take the address of another spl-token in which the yield is denoted (could be wSol, usdc, etc.) [after this: yieldToken] and add two instructions to the spl-token-2022 program.

But then I don't see how the new token (ie not the yieldToken) fits into this. The solution seems to involve wrapping yieldToken with the new token, but the new token is never mentioned, only yieldToken.

If you are wrapping a token, you're much better off wrapping with the existing interest-bearing extension and modifying the interest-rate properly whenever you want to "distribute" tokens. For example, if there are 1_000_000 yieldTokens in your pool, and 1_000_000 new tokens, and you want to transfer 1_000 tokens yieldTokens into the pool (add 0.1%), you increase the interest rate to a huge number for a short period of time to "create" those 1_000 new tokens, then set it back to 0.

Users should always be able to redeem the correct number when they go through your protocol. This just adds the UI trick and eliminates all complexity!

wjthieme commented 1 year ago

But then I don't see how the new token (ie not the yieldToken) fits into this. The solution seems to involve wrapping yieldToken with the new token, but the new token is never mentioned, only yieldToken.

Maybe an analogy could help clear it up. Then again I am not fully up to date on the InterestBearing extension but from what I understand it sounds like a different thing.

Let's say you own 10 shares of AAPL (which will be your original token) and there are in total 1000 shares outstanding (total supply). Each year/month/week/whatever Apple distributes dividends to their shareholders. These dividends are given to each shareholder based on how much of the percentage of shares they own. The dividends won't be given in the form of new AAPL shares but instead as US dollars. (most brokers allow you to instantly buy new AAPL shares with your dividends to make it look like you get paid your dividends in AAPL but that is not important here).

Let's say Apple wants to distribute 10.000 USD dividends to their shareholders. You would then get 100 USD as you own 1% of the supply.

If you were to relate this to the above using a YieldBearing token, AAPL shares would be the original token and the yieldToken would be US dollars. For every 1 AAPL you own you would get 10 USD in dividends.

Behavior of the original token itself won't change much with a YieldBearing token. The thing that gets added is that you can easily distribute dividends / yield (denoted in another currency / token) to the holders of the original token.

I'm not sure if this would make sense to have in spl-token-2022 but I can imagine this would be a nice thing to have for anyone who wants to create a 'security' or 'equity' token on-chain.

buffalojoec commented 1 year ago

One thing that sort of jumps out at me is how you intend to increase the supply (or debit some account in denomination) of one mint at the behest of another mint. For this reason, it starts to sound a bit more like a protocol than something Token2022 should provide.

In your analogy, AAPL can pay dividends because it's a firm that generates USD revenue and thus peels profits for shareholder dividends. In other words, the dividend USD comes from in-house and is within their control as to how much is generated in revenue and/or allocated to dividends. Put simply, AAPL doesn't increase the supply of shares to pay dividends, it's facilitating business in another mint (USD) that it intends to reward shareholders with.

This behavior could be mimicked by a protocol and could just invoke Token2022 to perform the transactions. I tend to lean towards @joncinque's suggestion possibly combined with some more already-implemented Token2022 functionality.

However, would love to explore more!

I'm not sure if this would make sense to have in spl-token-2022 but I can imagine this would be a nice thing to have for anyone who wants to create a 'security' or 'equity' token on-chain.

cc @valentinmadrid

wjthieme commented 1 year ago

One thing that sort of jumps out at me is how you intend to increase the supply (or debit some account in denomination) of one mint at the behest of another mint.

Let's take a dex protocol as an example. Users swap through a dex which will generate returns for the LP holders and optionally for the holders of a governance / security / equity token of the dex.

Most likely teams would implement this using a treasury and then maybe airdrop to governance token holders from the treasury. This requires human intervention.

If you want to do this in a fully decentralized and autonomous way the YieldBearing token extension could come in. Instead of the LP pushing a little bit of the profits to a treasury wallet it could instead directly go to the YieldBearing token where token holders can withdraw their part of the profit.

(then you can still have a treasury wallet that gets payed out as well by just having it hold x% of the total supply of the governance token which entitles it to a share of the profit as well).

This behavior could be mimicked by a protocol and could just invoke Token2022 to perform the transactions

Most of this should be possible using the transfer hook so definitely think this could be implemented fully decentralized at the protocol level, but only for fixed supply tokens (as minting and burning might be difficult to intercept without needing a proxy).

The main point I am trying to make here is that while it is possible to implement at the protocol level, there are some hurdles that won't be there if implemented as an extension. I would argue that because security/equity tokens will become more prevalent as more things move on-chain, it might be worth to explore making this as an extension.

Added benefit is that it only has to be implemented once at extension level as opposed to each team having to create their own implementation of this (provided this is what the teams are after of course).