How do I calculate TVL on-chain?

I am working on a anchor / solana program that provides liquidity to a number of pools, including saber.so and invariant.app. During the swap, I need to calculate the TVL, to provision a token at a fair exchange rate.

My question is: what is the best way to calculate TVL onchain?

The following are some approaches that I have in mind, each one with its shortcomings:

(1) Calculate off-chain, and provide this as an oracle:

We could calculate the TVL off-chain, and then provide this TVL as an oracle. The shortcomings are: chainlink (an oracle provider) on solana does not seem to support custom data-feeds, as is the case with ethereum. Further more this solution increases the centralization of the app, it would be nice to have it on-chain. also there could be oracle-attacks which drain the reserves of the protocol.

(2) Have a giant list of liquidity-positions:

Another approach would be to keep track of all liquidity-positions that we as a protocol have provided liquidity in. Although this is possible, I believe that this would (very quickly), reach solana's account limit.

In this case, we would have a huge "state-"account, which tracks the following variables per pool:

  • token1_mint: Pubkey
  • token2_mint: Pubkey
  • token1_amount: u64
  • token2_amount: u64
  • token1_to_currency_pyth_feed_address: Pubkey
  • token2_to_currency_pyth_feed_address: Pubkey
  • provider: u8

Given that we have 4 * 32 + 2 * 64 bytes + 8 bytes = 264 bytes, we can have around 20 pools that we can deposit at any given point in time (because of a 4KB account limit on solana)

The second option seems like the way to go, as the first one if off-chain and prone to oracle-attacks. However, the second option still seems a bit hacky, as I would have to include this data-structure anytime I intend to calculate the total TVL.

Is there any other design ideas that come to your mind or that you have seen, that would be appropriate?


Solution 1:

I don't know much about the overall design of your program to provide you with a good solution. I also don't know what invariant is, maybe that breaks what I'm about to describe below.

I assume that you have some instruction in your program which cpi calls into Saber etc and opens a liquidity position. Assuming that that instruction creates an account on the chain with the following information:

pool_address,
token_1_mint_address
token_2_mint_address
amount_token_1
amount_token_2
...

One simple solution is to loop through all those accounts, and since you have the amount and mint of each token, you can calculate the value using something like the pyth price oracle. I wouldn't do this on chain though since it can become pretty expensive fast! Perhaps is best to do it on the client side and write this information back to the chain. The recent solana bootcamp videos actually have a tutorial on bringing off chain info back to the chain! https://www.youtube.com/watch?v=GwhRWde3Ckw&t=385s

Below is a demo of the runtime limitations of on chain programs, perhaps you could do the loop through the pool accounts, if you use some indexing and PDA to find the account address and assuming that you have a limited number of liquidity positions! However I wouldn't hardcode all the information into a single account, seems like an unsustainable approach that might cause a lot of issues down the line. Might give you superior performance however, not sure.

https://www.youtube.com/watch?v=5IrfSecDPeA&t=1191s

Anyways GL!