-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: main
Are you sure you want to change the base?
Conversation
@@ -0,0 +1,788 @@ | |||
import { Abi, Bytes, encodeCall, decodeResult } from '@subsquid/ink-abi' |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
src/mappings/points/helpers.ts
Outdated
block.hash | ||
) | ||
|
||
const poolAssetPrice = (await oracleContract.stateCall('0xb3596f07', [ |
There was a problem hiding this comment.
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>() |
There was a problem hiding this comment.
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)
@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. |
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. |
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 I'll try that. |
Okay done, added caches per block and fetch the price from the 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). |
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. |
Ah! I thought it always used whatever it could, local or not. The we are good. |
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. |
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). We can do this if it turns out it scales poorly with many addresses. |
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? |
There was a problem hiding this 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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?
src/mappings/points/helpers.ts
Outdated
ss58ToHex(router.id), | ||
block.hash | ||
) | ||
const relevantOracleHexAddress = await routerContract.oracleByAsset( |
There was a problem hiding this comment.
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.
src/mappings/points/helpers.ts
Outdated
) | ||
const totalValue = await contract.getTotalPoolWorth() | ||
|
||
const { priceUsdUnits, decimals } = await getBackstopPoolTokenPrice( |
There was a problem hiding this comment.
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
.
@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 |
Added more optimizations. Now we are closer to Never mind I did some profiling, getting the info from So now we fetch all pairs and filter what we need, this should have a constant |
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)? |
It was taking 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. |
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). |
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:
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
andBurn
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.