Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DO NOT MERGE] - Calculate rewards points #75

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

gianfra-t
Copy link
Contributor

@gianfra-t gianfra-t commented Oct 18, 2024

Issue: tasks/#411.

The idea of this PR is NOT to be merged, but to serve as a place to keep and discuss the special purpose indexer code to calculate the points for the campaign.

Overall idea

We only handle contract events, shutting down everything else for this use case.

The overall idea is to integrate the number of points obtained per block. We use the following formula:

points_this_block = (lp_price / total_supply)  * asset_price_usd * (1 / 216000) * (amount_lped / lp_decimals) * (1 / 100 or 50)

To keep the sync time lower, we save 3 entities in memory only and not in the schema. These will keep track of the user's points and mapping between lp tokens and pools. So far we only save Mint, Burn events, and although we could derive the current total lp from them, it would become costly to do so on each block.

The relevant addition and subtraction of the liquidity is handled on the same Mint and Burn events (example).

Similarly, we can replace the in-memory values and put them into the store, yet it seems overkill to implement more schema changes. The only downside is that to get the total current points, the squid must not stop during a run. I believe this is the expected use case for this script/indexer. We do, however, implement a schema to hold the points that will be updated every X blocks. The intention is to be able to easily query the results.

I'll leave relevant comments on the code.

@gianfra-t gianfra-t changed the title Calculate rewards points [DO NOT MERGE] - Calculate rewards points Oct 22, 2024
@gianfra-t gianfra-t marked this pull request as ready for review October 22, 2024 14:15
@gianfra-t gianfra-t requested a review from a team October 22, 2024 14:17
@@ -0,0 +1,788 @@
import { Abi, Bytes, encodeCall, decodeResult } from '@subsquid/ink-abi'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need this abi to fetch the prices given a pool token.

@@ -143,6 +151,12 @@ export async function handleMint(
event.amountPrincipleDeposited,
backstopPool
)
addBackstopLP(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

On this implementation, this is how we keep track of the LP tokens per pool and user and avoid storing in the database.

])

// Accumulate points from both swap LPs and backstop LPs
for (const address of addresses) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Iterate through addresses to more easily return total points this block for each user.

block.hash
)

const poolAssetPrice = (await oracleContract.stateCall('0xb3596f07', [
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For some reason, the method getAssetPrice is not being generated. So we call it manually.

const DUMP_BLOCK_HEIGHT = 3795124 // whenever the campaign finishes.

// global vars to count points
export const pointsCount = new Map<address, Big>()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need points to be a Big. The points per block obtained by the current equation can be very small (10**-7)

@gianfra-t
Copy link
Contributor Author

@pendulum-chain/devs I've noticed that the code takes a VERY long term to run. Probably due to the fact that many contract calls are done each block (iteration even) to fetch the price.

Although caching will be super easy to do, still to be 100% correct we would need to call at least once per block the contracts to get the price. If the running time ends up being too long, I propose we used cached values for the prices for N blocks.

@TorstenStueber
Copy link
Member

TorstenStueber commented Oct 24, 2024

Can you give an estimate of the running time? I guess we can just deploy a squid and let it execute on the Subsquid cloud. Even if it takes a day or so, that shouldn't matter too much at the end.

I think we can at least speed up the oracle call. The contract is just a wrapper around a chain extension to read the state from the (dia) oracle pallet. Subsquid should be able to directly read the pallet storage state without making a runtime call, let alone executing a smart contract.

@gianfra-t
Copy link
Contributor Author

The running time was around 1 second per block.. so quite slow. I thought about calling the oracle directly for a different reason but now that you say it that would be way faster.

Since this is indexed by symbol and blockchain and not by address we need to read the contracts the first time and then cache that value for the corresponding pool since that never changes.

I'll try that.

@gianfra-t
Copy link
Contributor Author

gianfra-t commented Oct 24, 2024

Okay done, added caches per block and fetch the price from the diaOracle and not the contract. Still 1 second per block (actually I lied to you before, it was a bit more than 1 second/block before).

I don't know how much else to optimize it if we want exact prices each block. Right now for a 30 day campaign will take 1 hour exactly, as more addresses need to be processed it should scale better since we are caching the prices (in the range I tested, is only 1 address and 3 swap pools).

@TorstenStueber
Copy link
Member

Do you run it locally? Then it is super slow because it uses the RPC to request information block by block. When running in the Subsquid cloud, it should be much faster as it uses archives and loads information from historical data much faster.

@gianfra-t
Copy link
Contributor Author

Ah! I thought it always used whatever it could, local or not. The we are good.

@ebma
Copy link
Member

ebma commented Oct 24, 2024

Do you run it locally? Then it is super slow because it uses the RPC to request information block by block.

Are you referring to running the squid locally or indexing a local runtime @TorstenStueber? Indexing a local runtime is slower, yes, but if @gianfra-t was running the squid locally to index pendulum/amplitude/foucoco, then it would also first connect to the archive and eventually switch to the RPC connection. The comment here refers to a local runtime, I might have phrased it poorly. If you check the config, you'll see that the archive field of the local config is undefined. Note that it's also undefined for Foucoco but we can re-enable that. It was just a temporary thing because the subsquid team needed to migrate the foucoco archive after the migration to Paseo.

@gianfra-t
Copy link
Contributor Author

So I ran it on the cloud and we still get 1 block/sec, can see the logs here (need to scroll up a bit).
The normal rate before a liquidity position is minted, is 30 blocks/sec. We could do some profiling to identify what are the slowest operations but I think it is overkill for a process that will only run a couple of times.

We can do this if it turns out it scales poorly with many addresses.

@TorstenStueber
Copy link
Member

This is rather unexpected. If you run a squid on the Subsquid cloud, then you can see that it catches up very quickly and it is fully synced in I think way less than an hour.

Calling into a contract to request data is slower but the fraction of blocks where interaction with the Forex AMM is taking place is so minimal that I shouldn't have noticeable impact on the total runtime.

Does it make any difference whether you start at a specific block vs. starting from genesis as we usually do with a squid?

Copy link
Member

@TorstenStueber TorstenStueber left a comment

Choose a reason for hiding this comment

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

The algorithm itself looks nice and good to me.

I think the reason for the slow runtime is that you actually do call into smart contracts for each block because you need to get current prices. Although you now query the oracle price directly from the blockchain state, which should be way faster, you still call oracleByAsset of the router contract each block multiple times.

You can either just hard code this information or cache it similar to how you cache getBlockchainSymbolForToken. I assume that if you avoid calling contracts for each single block and only when necessary (possibly not at all), then you will be able to speed up this squid.

const input = _abi.encodeMessageInput(selector, args)
const data = encodeCall(this.address, input)
const result = await this.ctx._chain.rpc.call('state_call', [
let input = _abi.encodeMessageInput(selector, args)
Copy link
Member

Choose a reason for hiding this comment

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

This is generated code, right? Strange that they changed this to let statements.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I thought so too. I did update at some point squid dependencies. They must have changed this.

@@ -188,8 +202,11 @@ export async function updateBackstopCoverageAndSupply(
const contractHexAddress = ss58ToHex(backstopPool.id)
const contract = new BackstopPoolContract(ctx, contractHexAddress)
const poolTokenAddress = await contract.asset()
const poolTokenContract = new Erc20Contract(ctx, poolTokenAddress)

const poolTokenContract = new Erc20Contract(
Copy link
Member

Choose a reason for hiding this comment

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

Can you merge the latest changes from the main branch or rebase on the latest main branch?

ss58ToHex(router.id),
block.hash
)
const relevantOracleHexAddress = await routerContract.oracleByAsset(
Copy link
Member

Choose a reason for hiding this comment

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

This code is really nice and general. But if we want to optimize for speed, it's totally fine to just hardcode all these contracts and addresses as – for now – we will only use it for this use one time use case.

)
const totalValue = await contract.getTotalPoolWorth()

const { priceUsdUnits, decimals } = await getBackstopPoolTokenPrice(
Copy link
Member

Choose a reason for hiding this comment

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

The decimals are not used, correct? Same for getSwapPoolLPPrice.

@gianfra-t
Copy link
Contributor Author

@TorstenStueber Thanks for the review. Well in theory I'm caching the LP price on the main loop (which involves the contract-calling) to avoid calling it more than once each block. Also have a token cache in case different Nabla's had pools with shared tokens.

I didn't know we had the possibility to know in advance the pools that would be used. In that case once we have them, I can manually map the symbol and blockchain for each pool to get the price directly.

@gianfra-t
Copy link
Contributor Author

gianfra-t commented Oct 25, 2024

Added more optimizations. Now we are closer to 10 blocks/sec.

Never mind I did some profiling, getting the info from coinStorage already takes 200ms, that is per pool unless they share token which will not happen for a single deployment of course.

So now we fetch all pairs and filter what we need, this should have a constant 500ms per block regardless of how many pools there are.

@TorstenStueber
Copy link
Member

TorstenStueber commented Oct 26, 2024

getting the info from coinStorage already takes 200ms

So slow. I had the impression that the squids optimize access to historical data as they sync really fast. But it seems that they request block by block from the archive here.

So if we want to be precise I also don't see how we can speed this up any further. That means that running this script will pretty much exactly take one hour per day of the campaign. It's fine, we can just deploy it to the cloud and then come back two days later and check the logs (?). Thanks for all the optimization effort.

Nevertheless, I don't think we need this level of precision. It would be totally fine to cache prices for a couple of blocks as they don't change that much and at the end everything is just for calculating some one-time rewards. If we even just update prices every two minutes we can speed this up 10 times.

Would that speed up block processing? Why does it still take another 300ms to process a single block? Is database access so slow? Is it possible to cache everything just in memory and avoid using the database altogether (apart from processing specific events)?

@gianfra-t
Copy link
Contributor Author

Why does it still take another 300ms to process a single block

It was taking 200ms per coin info (per pool), so reading all pairs together takes 500ms but then it will be constant for any number of pools of course.

This is the real bottleneck. If we cache and update the prices every 2 minutes like you say, or more, then it will be fine. There is also something that will take even longer, which is the call to the backstop-pool contract to get the liabilities. I didn't account for this on the profiling/test because for the range I was testing there was no deposit into backstop pool. So we should probably want to cache that too and update every so often, as prices.

Beyond that, there is nothing else to do. I may be missing something, but I don't know how to fetch the prices faster.

@TorstenStueber
Copy link
Member

The backstop pool liability can only change when the backstop pool executes some action. We already track that in the event handlers of the indexer and store the liability in the database. That way there should never be a need to request it from the contract (apart from the event handlers).

@gianfra-t
Copy link
Contributor Author

Added two changes to speed up:

  • Store in the backstop pool entity the totalPoolValue, so that we call the contract only on a relevant event.
  • Cache the prices not per block, but per a number of blocks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants