From c9dd0152072c1d83c1afeb44257ce974a2fa457f Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Mon, 2 Dec 2024 11:10:55 -0800 Subject: [PATCH 1/8] add yield delegation rebasing options and pull balances via RPC when an account is involved in yield delegation. doing it this way dramatically simplifies the processing. still need to handle the erc20 processor --- ...46488522-Data.js => 1733162399493-Data.js} | 8 +- schema.graphql | 14 +- schema/general.graphql | 5 - schema/otoken.graphql | 9 + src/model/generated/_rebasingOption.ts | 2 + src/model/generated/oTokenAddress.model.ts | 5 +- .../generated/oTokenRebaseOption.model.ts | 5 +- src/templates/otoken/otoken.ts | 311 +++++++++++++----- src/templates/otoken/utils.ts | 1 + 9 files changed, 265 insertions(+), 95 deletions(-) rename db/migrations/{1732646488522-Data.js => 1733162399493-Data.js} (99%) diff --git a/db/migrations/1732646488522-Data.js b/db/migrations/1733162399493-Data.js similarity index 99% rename from db/migrations/1732646488522-Data.js rename to db/migrations/1733162399493-Data.js index 23b5167c..08cb1ec0 100644 --- a/db/migrations/1732646488522-Data.js +++ b/db/migrations/1733162399493-Data.js @@ -1,5 +1,5 @@ -module.exports = class Data1732646488522 { - name = 'Data1732646488522' +module.exports = class Data1733162399493 { + name = 'Data1733162399493' async up(db) { await db.query(`CREATE TABLE "aero_cl_gauge_claim_fees" ("id" character varying NOT NULL, "chain_id" integer NOT NULL, "block_number" integer NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "address" text NOT NULL, "from" text NOT NULL, "claimed0" numeric NOT NULL, "claimed1" numeric NOT NULL, CONSTRAINT "PK_324db7f817fe71a6a8dfc04701a" PRIMARY KEY ("id"))`) @@ -640,7 +640,7 @@ module.exports = class Data1732646488522 { await db.query(`CREATE INDEX "IDX_2f1457755464ec5951d1e96542" ON "o_token_history" ("address_id") `) await db.query(`CREATE INDEX "IDX_42142d191ea0408fb511f9f576" ON "o_token_history" ("block_number") `) await db.query(`CREATE INDEX "IDX_f87d86cfca9ef211ba1b18d2bc" ON "o_token_history" ("tx_hash") `) - await db.query(`CREATE TABLE "o_token_address" ("id" character varying NOT NULL, "chain_id" integer NOT NULL, "otoken" text NOT NULL, "address" text NOT NULL, "is_contract" boolean NOT NULL, "rebasing_option" character varying(6) NOT NULL, "balance" numeric NOT NULL, "earned" numeric NOT NULL, "credits" numeric NOT NULL, "last_updated" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_5d5d2b6f8a94da6ed63aac85194" PRIMARY KEY ("id"))`) + await db.query(`CREATE TABLE "o_token_address" ("id" character varying NOT NULL, "chain_id" integer NOT NULL, "otoken" text NOT NULL, "address" text NOT NULL, "is_contract" boolean NOT NULL, "rebasing_option" character varying(21) NOT NULL, "balance" numeric NOT NULL, "earned" numeric NOT NULL, "credits" numeric NOT NULL, "delegated_to" text, "last_updated" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_5d5d2b6f8a94da6ed63aac85194" PRIMARY KEY ("id"))`) await db.query(`CREATE INDEX "IDX_7cbc465ce1e9ae06dfe3a8c625" ON "o_token_address" ("chain_id") `) await db.query(`CREATE INDEX "IDX_5342c499e930e396bade7faeb6" ON "o_token_address" ("otoken") `) await db.query(`CREATE INDEX "IDX_75c7d29bf71b393e99c4407885" ON "o_token_address" ("address") `) @@ -658,7 +658,7 @@ module.exports = class Data1732646488522 { await db.query(`CREATE INDEX "IDX_b0c6feb890a83dcca572302371" ON "o_token_rebase" ("block_number") `) await db.query(`CREATE INDEX "IDX_7170f89052507f34d8563f7016" ON "o_token_rebase" ("tx_hash") `) await db.query(`CREATE INDEX "IDX_b8653270b96fc932f077b26441" ON "o_token_rebase" ("apy_id") `) - await db.query(`CREATE TABLE "o_token_rebase_option" ("id" character varying NOT NULL, "chain_id" integer NOT NULL, "otoken" text NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "block_number" integer NOT NULL, "tx_hash" text NOT NULL, "status" character varying(6) NOT NULL, "address_id" character varying, CONSTRAINT "PK_8b52df258c40e8347a66922f63e" PRIMARY KEY ("id"))`) + await db.query(`CREATE TABLE "o_token_rebase_option" ("id" character varying NOT NULL, "chain_id" integer NOT NULL, "otoken" text NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "block_number" integer NOT NULL, "tx_hash" text NOT NULL, "status" character varying(21) NOT NULL, "delegated_to" text, "address_id" character varying, CONSTRAINT "PK_8b52df258c40e8347a66922f63e" PRIMARY KEY ("id"))`) await db.query(`CREATE INDEX "IDX_5dfc53108b110d42994d02a832" ON "o_token_rebase_option" ("chain_id") `) await db.query(`CREATE INDEX "IDX_5936af713ee8131983812703b2" ON "o_token_rebase_option" ("otoken") `) await db.query(`CREATE INDEX "IDX_cb07bc901206c5da63eacff7df" ON "o_token_rebase_option" ("timestamp") `) diff --git a/schema.graphql b/schema.graphql index 7c5e4159..85b78c7d 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1082,11 +1082,6 @@ type ExchangeRate @entity { rate: BigInt! } -enum RebasingOption { - OptIn - OptOut -} - enum HistoryType { Sent Received @@ -1626,6 +1621,7 @@ type OTokenAddress @entity { balance: BigInt! earned: BigInt! credits: BigInt! + delegatedTo: String lastUpdated: DateTime! history: [OTokenHistory!]! @derivedFrom(field: "address") } @@ -1671,6 +1667,7 @@ type OTokenRebaseOption @entity { txHash: String! @index address: OTokenAddress! status: RebasingOption! + delegatedTo: String } type OTokenAPY @entity { @@ -1813,6 +1810,13 @@ type OTokenWithdrawalRequest @entity { claimed: Boolean! txHash: String! @index } + +enum RebasingOption { + OptIn + OptOut + YieldDelegationSource + YieldDelegationTarget +} type MorphoMarketState @entity { id: ID! chainId: Int! diff --git a/schema/general.graphql b/schema/general.graphql index 50f6a9d7..7b535dd4 100644 --- a/schema/general.graphql +++ b/schema/general.graphql @@ -23,11 +23,6 @@ type ExchangeRate @entity { rate: BigInt! } -enum RebasingOption { - OptIn - OptOut -} - enum HistoryType { Sent Received diff --git a/schema/otoken.graphql b/schema/otoken.graphql index 98c89ef6..bead411c 100644 --- a/schema/otoken.graphql +++ b/schema/otoken.graphql @@ -27,6 +27,7 @@ type OTokenAddress @entity { balance: BigInt! earned: BigInt! credits: BigInt! + delegatedTo: String lastUpdated: DateTime! history: [OTokenHistory!]! @derivedFrom(field: "address") } @@ -72,6 +73,7 @@ type OTokenRebaseOption @entity { txHash: String! @index address: OTokenAddress! status: RebasingOption! + delegatedTo: String } type OTokenAPY @entity { @@ -214,3 +216,10 @@ type OTokenWithdrawalRequest @entity { claimed: Boolean! txHash: String! @index } + +enum RebasingOption { + OptIn + OptOut + YieldDelegationSource + YieldDelegationTarget +} diff --git a/src/model/generated/_rebasingOption.ts b/src/model/generated/_rebasingOption.ts index ea77587b..b0666e60 100644 --- a/src/model/generated/_rebasingOption.ts +++ b/src/model/generated/_rebasingOption.ts @@ -1,4 +1,6 @@ export enum RebasingOption { OptIn = "OptIn", OptOut = "OptOut", + YieldDelegationSource = "YieldDelegationSource", + YieldDelegationTarget = "YieldDelegationTarget", } diff --git a/src/model/generated/oTokenAddress.model.ts b/src/model/generated/oTokenAddress.model.ts index 22b2c9e3..7fac885c 100644 --- a/src/model/generated/oTokenAddress.model.ts +++ b/src/model/generated/oTokenAddress.model.ts @@ -26,7 +26,7 @@ export class OTokenAddress { @BooleanColumn_({nullable: false}) isContract!: boolean - @Column_("varchar", {length: 6, nullable: false}) + @Column_("varchar", {length: 21, nullable: false}) rebasingOption!: RebasingOption @BigIntColumn_({nullable: false}) @@ -38,6 +38,9 @@ export class OTokenAddress { @BigIntColumn_({nullable: false}) credits!: bigint + @StringColumn_({nullable: true}) + delegatedTo!: string | undefined | null + @DateTimeColumn_({nullable: false}) lastUpdated!: Date diff --git a/src/model/generated/oTokenRebaseOption.model.ts b/src/model/generated/oTokenRebaseOption.model.ts index 31170fa7..d2768361 100644 --- a/src/model/generated/oTokenRebaseOption.model.ts +++ b/src/model/generated/oTokenRebaseOption.model.ts @@ -35,6 +35,9 @@ export class OTokenRebaseOption { @ManyToOne_(() => OTokenAddress, {nullable: true}) address!: OTokenAddress - @Column_("varchar", {length: 6, nullable: false}) + @Column_("varchar", {length: 21, nullable: false}) status!: RebasingOption + + @StringColumn_({nullable: true}) + delegatedTo!: string | undefined | null } diff --git a/src/templates/otoken/otoken.ts b/src/templates/otoken/otoken.ts index 24ba5dc8..7820fc0d 100644 --- a/src/templates/otoken/otoken.ts +++ b/src/templates/otoken/otoken.ts @@ -24,7 +24,7 @@ import { OTokenVault, RebasingOption, } from '@model' -import { Block, Context } from '@processor' +import { Block, Context, Log } from '@processor' import { ensureExchangeRate } from '@shared/post-processors/exchange-rates' import { CurrencyAddress, CurrencySymbol } from '@shared/post-processors/exchange-rates/mainnetCurrencies' import { EvmBatchProcessor } from '@subsquid/evm-processor' @@ -75,6 +75,17 @@ export const createOTokenProcessor = (params: { range: { from: params.harvester.from }, }) : undefined + const yieldDelegatedFilter = logFilter({ + address: [params.otokenAddress], + topic0: [otoken.events.YieldDelegated.topic], + range: { from: params.from }, + }) + const yieldUndelegatedFilter = logFilter({ + address: [params.otokenAddress], + topic0: [otoken.events.YieldUndelegated.topic], + range: { from: params.from }, + }) + const setup = (processor: EvmBatchProcessor) => { if (params.upgrades?.rebaseOptEvents !== false) { processor.addTrace({ @@ -96,6 +107,8 @@ export const createOTokenProcessor = (params: { transaction: true, range: { from: params.from }, }) + processor.addLog(yieldDelegatedFilter.value) + processor.addLog(yieldUndelegatedFilter.value) if (params.wotoken) { processor.addLog({ address: [params.wotoken.address], @@ -197,7 +210,7 @@ export const createOTokenProcessor = (params: { for (const block of ctx.blocks) { await getOTokenDailyStat(ctx, result, block) for (const trace of block.traces) { - await processRebaseOpt(ctx, result, block, trace) + await processRebaseOptTrace(ctx, result, block, trace) } for (const log of block.logs) { await processTransfer(ctx, result, block, log) @@ -205,6 +218,8 @@ export const createOTokenProcessor = (params: { await processTotalSupplyUpdatedHighres(ctx, result, block, log) await processRebaseOptEvent(ctx, result, block, log) await processHarvesterYieldSent(ctx, result, block, log) + await processYieldDelegated(ctx, result, block, log) + await processYieldUndelegated(ctx, result, block, log) } } @@ -432,8 +447,20 @@ export const createOTokenProcessor = (params: { * "0017708038-000327-29fec:0xd2cdf18b60a5cdb634180d5615df7a58a597247c:Sent","0","49130257489166670","2023-07-16T19:50:11.000Z",17708038,"0x0e3ac28945d45993e3d8e1f716b6e9ec17bfc000418a1091a845b7a00c7e3280","Sent","0xd2cdf18b60a5cdb634180d5615df7a58a597247c", */ - const updateAddressBalance = ({ address, credits }: { address: OTokenAddress; credits: [bigint, bigint] }) => { - const newBalance = (credits[0] * DECIMALS_18) / credits[1] + const updateAddressBalance = async ({ + address, + credits, + }: { + address: OTokenAddress + credits: [bigint, bigint] + }) => { + const otokenContract = new otoken.Contract(ctx, block.header, params.otokenAddress) + const involvedInYieldDelegation = + address.rebasingOption === RebasingOption.YieldDelegationSource || + address.rebasingOption === RebasingOption.YieldDelegationTarget + const newBalance = involvedInYieldDelegation + ? await otokenContract.balanceOf(address.address)! // It should exist. + : (credits[0] * DECIMALS_18) / credits[1] const change = newBalance - address.balance if (change === 0n) return const type = addressSub === address ? HistoryType.Sent : HistoryType.Received @@ -455,14 +482,16 @@ export const createOTokenProcessor = (params: { address.balance = newBalance // token balance } - updateAddressBalance({ - address: addressSub, - credits: fromCreditsBalanceOf, - }) - updateAddressBalance({ - address: addressAdd, - credits: toCreditsBalanceOf, - }) + await Promise.all([ + updateAddressBalance({ + address: addressSub, + credits: fromCreditsBalanceOf, + }), + updateAddressBalance({ + address: addressAdd, + credits: toCreditsBalanceOf, + }), + ]) if (addressAdd.rebasingOption === RebasingOption.OptOut && data.from === ADDRESS_ZERO) { // If it's a mint and minter has opted out of rebasing, @@ -522,12 +551,18 @@ export const createOTokenProcessor = (params: { data, result.lastYieldDistributionEvent, ) + const yieldDelegationBalances = await getYieldDelegationBalances(ctx, block) for (const address of owners!.values()) { if (!address.credits || address.rebasingOption === RebasingOption.OptOut) { continue } - const newBalance = (address.credits * DECIMALS_18) / data.rebasingCreditsPerToken + const involvedInYieldDelegation = + address.rebasingOption === RebasingOption.YieldDelegationSource || + address.rebasingOption === RebasingOption.YieldDelegationTarget + const newBalance = involvedInYieldDelegation + ? yieldDelegationBalances.get(address.address)! // It should exist. + : (address.credits * DECIMALS_18) / data.rebasingCreditsPerToken const earned = newBalance - address.balance if (earned === 0n) continue @@ -567,7 +602,7 @@ export const createOTokenProcessor = (params: { result.lastYieldDistributionEvent = { yield: _yield, fee: _fee } } - const processRebaseOpt = async ( + const processRebaseOptTrace = async ( ctx: Context, result: ProcessResult, block: Context['blocks']['0'], @@ -582,53 +617,23 @@ export const createOTokenProcessor = (params: { ) { await result.initialize() const timestamp = new Date(block.header.timestamp) - const blockNumber = block.header.height const address = trace.action.sighash === otoken.functions.governanceRebaseOptIn.selector ? otoken.functions.governanceRebaseOptIn.decode(trace.action.input)._account : trace.action.from.toLowerCase() - const otokenObject = await getLatestOTokenObject(ctx, result, block) - let owner = owners!.get(address) - if (!owner) { - owner = await createAddress(ctx, params.otokenAddress, address, timestamp) - owners!.set(address, owner) - } - - const rebaseOption = new OTokenRebaseOption({ - id: getUniqueId(`${ctx.chain.id}-${params.otokenAddress}-${trace.transaction?.hash!}-${owner.address}`), - chainId: ctx.chain.id, - otoken: params.otokenAddress, - timestamp, - blockNumber, - txHash: trace.transaction?.hash, - address: owner, - status: owner.rebasingOption, + const option = + trace.action.sighash === otoken.functions.rebaseOptIn.selector ? RebasingOption.OptIn : RebasingOption.OptOut + await processRebaseOpt({ + ctx, + result, + block, + address, + hash: trace.transaction?.hash ?? timestamp.toString(), + option, }) - result.rebaseOptions.push(rebaseOption) - if (trace.action.sighash === otoken.functions.rebaseOptIn.selector) { - const afterHighResUpgrade = block.header.height >= (params.Upgrade_CreditsBalanceOfHighRes ?? 0) - const otokenContract = new otoken.Contract(ctx, block.header, params.otokenAddress) - owner.credits = afterHighResUpgrade - ? await otokenContract.creditsBalanceOfHighres(owner.address).then((c) => c._0) - : await otokenContract.creditsBalanceOf(owner.address).then((c) => c._0 * 1000000000n) - owner.rebasingOption = RebasingOption.OptIn - rebaseOption.status = RebasingOption.OptIn - otokenObject.nonRebasingSupply -= owner.balance - otokenObject.rebasingSupply = otokenObject.totalSupply - otokenObject.nonRebasingSupply - } - if (trace.action.sighash === otoken.functions.rebaseOptOut.selector) { - owner.rebasingOption = RebasingOption.OptOut - rebaseOption.status = RebasingOption.OptOut - otokenObject.nonRebasingSupply += owner.balance - otokenObject.rebasingSupply = otokenObject.totalSupply - otokenObject.nonRebasingSupply - } } } - const rebaseEventTopics = { - [otoken.events.AccountRebasingEnabled.topic]: otoken.events.AccountRebasingEnabled, - [otoken.events.AccountRebasingDisabled.topic]: otoken.events.AccountRebasingDisabled, - } const processRebaseOptEvent = async ( ctx: Context, result: ProcessResult, @@ -636,43 +641,173 @@ export const createOTokenProcessor = (params: { log: Context['blocks']['0']['logs']['0'], ) => { if (log.address !== params.otokenAddress) return + const rebaseEventTopics = { + [otoken.events.AccountRebasingEnabled.topic]: otoken.events.AccountRebasingEnabled, + [otoken.events.AccountRebasingDisabled.topic]: otoken.events.AccountRebasingDisabled, + } if (rebaseEventTopics[log.topics[0]]) { await result.initialize() - const timestamp = new Date(block.header.timestamp) - const blockNumber = block.header.height const data = rebaseEventTopics[log.topics[0]].decode(log) - const otokenObject = await getLatestOTokenObject(ctx, result, block) + const address = data.account.toLowerCase() - let owner = owners!.get(address) - if (!owner) { - owner = await createAddress(ctx, params.otokenAddress, address, timestamp) - owners!.set(address, owner) - } + const option = + log.topics[0] === otoken.events.AccountRebasingEnabled.topic ? RebasingOption.OptIn : RebasingOption.OptOut + await processRebaseOpt({ ctx, result, block, address, hash: log.transactionHash, option }) + } + } + + const processRebaseOpt = async ({ + ctx, + result, + block, + address, + hash, + option, + delegate, + }: { + ctx: Context + result: ProcessResult + block: Context['blocks']['0'] + address: string + hash: string + option: RebasingOption + delegate?: string + }) => { + const timestamp = new Date(block.header.timestamp) + const blockNumber = block.header.height + const otokenObject = await getLatestOTokenObject(ctx, result, block) + let owner = owners!.get(address) + if (!owner) { + owner = await createAddress(ctx, params.otokenAddress, address, timestamp) + owners!.set(address, owner) + } + const rebaseOption = new OTokenRebaseOption({ + id: getUniqueId(`${ctx.chain.id}-${params.otokenAddress}-${hash}-${owner.address}`), + chainId: ctx.chain.id, + otoken: params.otokenAddress, + timestamp, + blockNumber, + txHash: hash, + address: owner, + status: owner.rebasingOption, + delegatedTo: null, + }) + result.rebaseOptions.push(rebaseOption) + + owner.delegatedTo = null + if (option === RebasingOption.OptIn) { + rebaseOption.status = RebasingOption.OptIn + owner.rebasingOption = RebasingOption.OptIn + otokenObject.nonRebasingSupply -= owner.balance + otokenObject.rebasingSupply = otokenObject.totalSupply - otokenObject.nonRebasingSupply + } else { + rebaseOption.status = RebasingOption.OptOut + owner.rebasingOption = RebasingOption.OptOut + otokenObject.nonRebasingSupply += owner.balance + otokenObject.rebasingSupply = otokenObject.totalSupply - otokenObject.nonRebasingSupply + } + } - const rebaseOption = new OTokenRebaseOption({ - id: getUniqueId(`${ctx.chain.id}-${params.otokenAddress}-${log.transactionHash!}-${owner.address}`), + const processYieldDelegated = async (ctx: Context, result: ProcessResult, block: Block, log: Log) => { + if (!yieldDelegatedFilter.matches(log)) return + const timestamp = new Date(block.header.timestamp) + const blockNumber = block.header.height + const data = otoken.events.YieldDelegated.decode(log) + const sourceAddress = data.source.toLowerCase() + const targetAddress = data.target.toLowerCase() + // Source + let sourceOwner = owners!.get(sourceAddress) + if (!sourceOwner) { + sourceOwner = await createAddress(ctx, params.otokenAddress, sourceAddress, timestamp) + owners!.set(sourceAddress, sourceOwner) + } + sourceOwner.rebasingOption = RebasingOption.YieldDelegationSource + sourceOwner.delegatedTo = targetAddress + result.rebaseOptions.push( + new OTokenRebaseOption({ + id: getUniqueId(`${ctx.chain.id}-${params.otokenAddress}-${log.transactionHash}-${sourceAddress}`), chainId: ctx.chain.id, otoken: params.otokenAddress, timestamp, blockNumber, txHash: log.transactionHash, - address: owner, - status: owner.rebasingOption, - }) - result.rebaseOptions.push(rebaseOption) - if (log.topics[0] === otoken.events.AccountRebasingEnabled.topic) { - owner.rebasingOption = RebasingOption.OptIn - rebaseOption.status = RebasingOption.OptIn - otokenObject.nonRebasingSupply -= owner.balance - otokenObject.rebasingSupply = otokenObject.totalSupply - otokenObject.nonRebasingSupply - } - if (log.topics[0] === otoken.events.AccountRebasingDisabled.topic) { - owner.rebasingOption = RebasingOption.OptOut - rebaseOption.status = RebasingOption.OptOut - otokenObject.nonRebasingSupply += owner.balance - otokenObject.rebasingSupply = otokenObject.totalSupply - otokenObject.nonRebasingSupply - } + address: sourceOwner, + status: RebasingOption.YieldDelegationSource, + delegatedTo: targetAddress, + }), + ) + // Target + let targetOwner = owners!.get(targetAddress) + if (!targetOwner) { + targetOwner = await createAddress(ctx, params.otokenAddress, targetAddress, timestamp) + owners!.set(targetAddress, targetOwner) } + targetOwner.rebasingOption = RebasingOption.YieldDelegationTarget + targetOwner.delegatedTo = null + result.rebaseOptions.push( + new OTokenRebaseOption({ + id: getUniqueId(`${ctx.chain.id}-${params.otokenAddress}-${log.transactionHash}-${targetAddress}`), + chainId: ctx.chain.id, + otoken: params.otokenAddress, + timestamp, + blockNumber, + txHash: log.transactionHash, + address: targetOwner, + status: RebasingOption.YieldDelegationTarget, + delegatedTo: null, + }), + ) + } + + const processYieldUndelegated = async (ctx: Context, result: ProcessResult, block: Block, log: Log) => { + if (!yieldUndelegatedFilter.matches(log)) return + const timestamp = new Date(block.header.timestamp) + const blockNumber = block.header.height + const data = otoken.events.YieldUndelegated.decode(log) + const sourceAddress = data.source.toLowerCase() + const targetAddress = data.target.toLowerCase() + // Source + let sourceOwner = owners!.get(sourceAddress) + if (!sourceOwner) { + sourceOwner = await createAddress(ctx, params.otokenAddress, sourceAddress, timestamp) + owners!.set(sourceAddress, sourceOwner) + } + sourceOwner.rebasingOption = RebasingOption.OptOut + sourceOwner.delegatedTo = null + result.rebaseOptions.push( + new OTokenRebaseOption({ + id: getUniqueId(`${ctx.chain.id}-${params.otokenAddress}-${log.transactionHash}-${sourceAddress}`), + chainId: ctx.chain.id, + otoken: params.otokenAddress, + timestamp, + blockNumber, + txHash: log.transactionHash, + address: sourceOwner, + status: RebasingOption.OptOut, + delegatedTo: null, + }), + ) + // Target + let targetOwner = owners!.get(targetAddress) + if (!targetOwner) { + targetOwner = await createAddress(ctx, params.otokenAddress, targetAddress, timestamp) + owners!.set(targetAddress, targetOwner) + } + targetOwner.rebasingOption = RebasingOption.OptIn + targetOwner.delegatedTo = null + result.rebaseOptions.push( + new OTokenRebaseOption({ + id: getUniqueId(`${ctx.chain.id}-${params.otokenAddress}-${log.transactionHash}-${targetAddress}`), + chainId: ctx.chain.id, + otoken: params.otokenAddress, + timestamp, + blockNumber, + txHash: log.transactionHash, + address: targetOwner, + status: RebasingOption.OptIn, + delegatedTo: null, + }), + ) } const processHarvesterYieldSent = async ( @@ -773,5 +908,23 @@ export const createOTokenProcessor = (params: { return entity } + const getYieldDelegationBalances = async (ctx: Context, block: Block) => { + const delegatedAddresses = Array.from(owners!.values()) + .filter( + (owner) => + owner.rebasingOption === RebasingOption.YieldDelegationSource || + owner.rebasingOption === RebasingOption.YieldDelegationTarget, + ) + .map((owner) => owner.address) + const delegateBalances = await multicall( + ctx, + block.header, + otoken.functions.balanceOf, + params.otokenAddress, + delegatedAddresses.map((_account) => ({ _account })), + ) + return new Map(delegatedAddresses.map((address, index) => [address, delegateBalances[index]])) + } + return { from: params.from, setup, process } } diff --git a/src/templates/otoken/utils.ts b/src/templates/otoken/utils.ts index eef51607..ac9a3037 100644 --- a/src/templates/otoken/utils.ts +++ b/src/templates/otoken/utils.ts @@ -33,6 +33,7 @@ export async function createAddress(ctx: Context, otoken: string, addr: string, credits: 0n, isContract, rebasingOption: isContract ? RebasingOption.OptOut : RebasingOption.OptIn, + delegatedTo: null, lastUpdated, }) } From 56edbea2cef8d69359c56394e46e71ca3f7965dd Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Mon, 2 Dec 2024 14:28:02 -0800 Subject: [PATCH 2/8] modify erc20-rebasing processor to be able to handle yield delegation --- src/base/erc20.ts | 22 ++++++++++++++ src/mainnet/processors/erc20s.ts | 22 ++++++++++++++ src/ousd/processors/erc20s.ts | 22 ++++++++++++++ src/templates/erc20/erc20-rebasing.ts | 43 +++++++++++++++++++++------ 4 files changed, 100 insertions(+), 9 deletions(-) diff --git a/src/base/erc20.ts b/src/base/erc20.ts index 7a35f79b..e02e5746 100644 --- a/src/base/erc20.ts +++ b/src/base/erc20.ts @@ -31,6 +31,28 @@ export const baseERC20s = [ const oToken = new otoken.Contract(ctx, block.header, baseAddresses.tokens.superOETHb) return oToken.rebasingCreditsPerTokenHighres() }, + enableRpcBalance: { + filter: logFilter({ + address: [baseAddresses.tokens.superOETHb], + topic0: [otoken.events.YieldDelegated.topic], + range: { from: 23192884 }, + }), + decode: (log) => { + const data = otoken.events.YieldDelegated.decode(log) + return { addresses: [data.source, data.target] } + }, + }, + disableRpcBalance: { + filter: logFilter({ + address: [baseAddresses.tokens.superOETHb], + topic0: [otoken.events.YieldUndelegated.topic], + range: { from: 23192884 }, + }), + decode: (log) => { + const data = otoken.events.YieldUndelegated.decode(log) + return { addresses: [data.source, data.target] } + }, + }, }, }), // wsuperOETHb diff --git a/src/mainnet/processors/erc20s.ts b/src/mainnet/processors/erc20s.ts index 037ed0fe..f8048fd5 100644 --- a/src/mainnet/processors/erc20s.ts +++ b/src/mainnet/processors/erc20s.ts @@ -62,6 +62,28 @@ const rebasingTracks: Record { + const data = otoken.events.YieldDelegated.decode(log) + return { addresses: [data.source, data.target] } + }, + }, + disableRpcBalance: { + filter: logFilter({ + address: [OETH_ADDRESS], + topic0: [otoken.events.YieldUndelegated.topic], + range: { from: 21317452 }, + }), + decode: (log) => { + const data = otoken.events.YieldUndelegated.decode(log) + return { addresses: [data.source, data.target] } + }, + }, }, }, } diff --git a/src/ousd/processors/erc20s.ts b/src/ousd/processors/erc20s.ts index 726cc2dc..a8c04f69 100644 --- a/src/ousd/processors/erc20s.ts +++ b/src/ousd/processors/erc20s.ts @@ -29,6 +29,28 @@ const rebasingTracks: Record { + const data = otoken.events.YieldDelegated.decode(log) + return { addresses: [data.source, data.target] } + }, + }, + disableRpcBalance: { + filter: logFilter({ + address: [OUSD_ADDRESS], + topic0: [otoken.events.YieldUndelegated.topic], + range: { from: 21317452 }, + }), + decode: (log) => { + const data = otoken.events.YieldUndelegated.decode(log) + return { addresses: [data.source, data.target] } + }, + }, }, }, } diff --git a/src/templates/erc20/erc20-rebasing.ts b/src/templates/erc20/erc20-rebasing.ts index 0d02e9cd..af0d16b9 100644 --- a/src/templates/erc20/erc20-rebasing.ts +++ b/src/templates/erc20/erc20-rebasing.ts @@ -1,6 +1,6 @@ import * as abi from '@abi/erc20' import { ERC20, ERC20Balance, ERC20Holder, ERC20State, ERC20Transfer } from '@model' -import { Block, Context } from '@processor' +import { Block, Context, Log } from '@processor' import { publishERC20State } from '@shared/erc20' import { EvmBatchProcessor } from '@subsquid/evm-processor' import { ADDRESS_ZERO } from '@utils/addresses' @@ -20,6 +20,8 @@ export const createRebasingERC20Tracker = ({ rebaseEventFilter: LogFilter getCredits: (ctx: Context, block: Block, account: string) => Promise getCreditsPerToken: (ctx: Context, block: Block) => Promise + enableRpcBalance?: { filter: LogFilter; decode: (log: Log) => { addresses: string[] } } + disableRpcBalance?: { filter: LogFilter; decode: (log: Log) => { addresses: string[] } } } }) => { if (duplicateTracker.has(address)) { @@ -32,7 +34,7 @@ export const createRebasingERC20Tracker = ({ // TODO: Consider doing this differently? // Eventually memory may become a constraint. let mostRecentState: ERC20State | undefined - let holders: Map + let holders: Map const transferLogFilter = logFilter({ address: [address.toLowerCase()], topic0: [abi.events.Transfer.topic], @@ -131,6 +133,14 @@ export const createRebasingERC20Tracker = ({ const account = accounts[i].toLowerCase() if (account === ADDRESS_ZERO) continue const id = `${ctx.chain.id}-${block.header.height}-${address}-${account}` + const useRpcBalance = holders.get(account) === null + const rebasingCredits = useRpcBalance ? null : credits[i] + const newBalance = + rebasingCredits === null + ? await contract.balanceOf(account) + : mostRecentState?.rebasingCreditsPerToken + ? (rebasingCredits * 10n ** 18n) / mostRecentState.rebasingCreditsPerToken + : 0n const balance = new ERC20Balance({ id, chainId: ctx.chain.id, @@ -138,10 +148,8 @@ export const createRebasingERC20Tracker = ({ blockNumber: block.header.height, address, account, - balance: mostRecentState?.rebasingCreditsPerToken - ? (credits[i] * 10n ** 18n) / mostRecentState.rebasingCreditsPerToken - : 0n, - rebasingCredits: credits[i], + balance: newBalance, + rebasingCredits, }) result.balances.set(id, balance) if (balance.balance === 0n) { @@ -174,9 +182,12 @@ export const createRebasingERC20Tracker = ({ } const updateAllBalances = async () => { for (const [account, rebasingCredits] of holders.entries()) { - const balance = mostRecentState?.rebasingCreditsPerToken - ? (rebasingCredits * 10n ** 18n) / mostRecentState.rebasingCreditsPerToken - : 0n + const balance = + rebasingCredits === null + ? await contract.balanceOf(account) + : mostRecentState?.rebasingCreditsPerToken + ? (rebasingCredits * 10n ** 18n) / mostRecentState.rebasingCreditsPerToken + : 0n result.holders.set( account, new ERC20Holder({ @@ -235,6 +246,20 @@ export const createRebasingERC20Tracker = ({ await updateAllBalances() time('rebase log') } + const isEnableRpcBalanceLog = rebasing?.enableRpcBalance?.filter.matches(log) + if (isEnableRpcBalanceLog) { + const data = rebasing.enableRpcBalance?.decode(log) + for (const account of data?.addresses ?? []) { + holders.set(account, null) + } + } + const isDisableRpcBalanceLog = rebasing?.disableRpcBalance?.filter.matches(log) + if (isDisableRpcBalanceLog) { + const data = rebasing.disableRpcBalance?.decode(log) + for (const account of data?.addresses ?? []) { + holders.set(account, await rebasing.getCredits(ctx, block, account)) + } + } } } await Promise.all([ From 2f1badb32f21cbdb5e756248296774ab6337e516 Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Tue, 3 Dec 2024 14:40:16 -0800 Subject: [PATCH 3/8] testing erc20 processors, looking at discrepancies brought up by domen - fixes a few issues found --- ...62399493-Data.js => 1733248174412-Data.js} | 6 +- schema.graphql | 20 ++-- schema/general.graphql | 20 ++-- src/base/erc20.ts | 25 +--- src/mainnet/processors/erc20s.ts | 25 +--- src/model/generated/erc20.model.ts | 3 - src/model/generated/erc20Balance.model.ts | 3 - src/model/generated/erc20Holder.model.ts | 3 - src/model/generated/erc20State.model.ts | 3 - src/model/generated/index.ts | 1 + .../generated/processorMetadata.model.ts | 17 +++ src/ousd/processors/erc20s.ts | 25 +--- .../validators/validate-ousd/validate-ousd.ts | 3 +- src/templates/erc20/erc20-rebasing.ts | 109 ++++++++++++++++-- src/templates/erc20/erc20.ts | 6 +- src/validation/entities.ts | 6 + src/validation/manual-entities.json | 24 ++++ 17 files changed, 183 insertions(+), 116 deletions(-) rename db/migrations/{1733162399493-Data.js => 1733248174412-Data.js} (99%) create mode 100644 src/model/generated/processorMetadata.model.ts create mode 100644 src/validation/manual-entities.json diff --git a/db/migrations/1733162399493-Data.js b/db/migrations/1733248174412-Data.js similarity index 99% rename from db/migrations/1733162399493-Data.js rename to db/migrations/1733248174412-Data.js index 08cb1ec0..8d096948 100644 --- a/db/migrations/1733162399493-Data.js +++ b/db/migrations/1733248174412-Data.js @@ -1,5 +1,5 @@ -module.exports = class Data1733162399493 { - name = 'Data1733162399493' +module.exports = class Data1733248174412 { + name = 'Data1733248174412' async up(db) { await db.query(`CREATE TABLE "aero_cl_gauge_claim_fees" ("id" character varying NOT NULL, "chain_id" integer NOT NULL, "block_number" integer NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "address" text NOT NULL, "from" text NOT NULL, "claimed0" numeric NOT NULL, "claimed1" numeric NOT NULL, CONSTRAINT "PK_324db7f817fe71a6a8dfc04701a" PRIMARY KEY ("id"))`) @@ -494,6 +494,7 @@ module.exports = class Data1733162399493 { await db.query(`CREATE INDEX "IDX_7f49f2839212126c3d42fb4bf1" ON "frrs_strategist_updated" ("block_number") `) await db.query(`CREATE INDEX "IDX_aab8ae0cb29505a7014438675b" ON "frrs_strategist_updated" ("tx_hash") `) await db.query(`CREATE TABLE "processing_status" ("id" character varying NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "block_number" integer NOT NULL, CONSTRAINT "PK_85f5e2467b74fb70fac1a053021" PRIMARY KEY ("id"))`) + await db.query(`CREATE TABLE "processor_metadata" ("id" character varying NOT NULL, "data" jsonb NOT NULL, CONSTRAINT "PK_6accadf4e3bd54d481c86cd810f" PRIMARY KEY ("id"))`) await db.query(`CREATE TABLE "exchange_rate" ("id" character varying NOT NULL, "chain_id" integer NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "block_number" integer NOT NULL, "pair" text NOT NULL, "base" text NOT NULL, "quote" text NOT NULL, "rate" numeric NOT NULL, CONSTRAINT "PK_5c5d27d2b900ef6cdeef0398472" PRIMARY KEY ("id"))`) await db.query(`CREATE INDEX "IDX_2b58051dcc72cf0f02aa41ff14" ON "exchange_rate" ("chain_id") `) await db.query(`CREATE INDEX "IDX_9e23a3f1bf3634820c873a0fe8" ON "exchange_rate" ("timestamp") `) @@ -1221,6 +1222,7 @@ module.exports = class Data1733162399493 { await db.query(`DROP INDEX "public"."IDX_7f49f2839212126c3d42fb4bf1"`) await db.query(`DROP INDEX "public"."IDX_aab8ae0cb29505a7014438675b"`) await db.query(`DROP TABLE "processing_status"`) + await db.query(`DROP TABLE "processor_metadata"`) await db.query(`DROP TABLE "exchange_rate"`) await db.query(`DROP INDEX "public"."IDX_2b58051dcc72cf0f02aa41ff14"`) await db.query(`DROP INDEX "public"."IDX_9e23a3f1bf3634820c873a0fe8"`) diff --git a/schema.graphql b/schema.graphql index 85b78c7d..41148c53 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1063,6 +1063,14 @@ type FRRSStrategistUpdated @entity { blockNumber: Int! } +""" +Simple KV store to store metadata for processors. +""" +type ProcessorMetadata @entity { + id: ID! + data: JSON! +} + """ Any entity which has a price associated with it should have that price go in here. Prices can change very frequently and we don't want those changes on the same track @@ -1147,9 +1155,6 @@ type NativeBalance @entity { } type ERC20 @entity { - """ - Format: 'address' - """ id: ID! chainId: Int! @index address: String! @index @@ -1159,9 +1164,6 @@ type ERC20 @entity { } type ERC20Holder @entity { - """ - Format: 'address:account' - """ id: ID! chainId: Int! @index address: String! @index @@ -1171,9 +1173,6 @@ type ERC20Holder @entity { } type ERC20State @entity { - """ - Format: 'address:blockNumber' - """ id: ID! chainId: Int! @index timestamp: DateTime! @index @@ -1185,9 +1184,6 @@ type ERC20State @entity { } type ERC20Balance @entity { - """ - Format: 'address:account:blockNumber' - """ id: ID! chainId: Int! @index timestamp: DateTime! @index diff --git a/schema/general.graphql b/schema/general.graphql index 7b535dd4..12cb48eb 100644 --- a/schema/general.graphql +++ b/schema/general.graphql @@ -4,6 +4,14 @@ type ProcessingStatus @entity { blockNumber: Int! } +""" +Simple KV store to store metadata for processors. +""" +type ProcessorMetadata @entity { + id: ID! + data: JSON! +} + """ Any entity which has a price associated with it should have that price go in here. Prices can change very frequently and we don't want those changes on the same track @@ -88,9 +96,6 @@ type NativeBalance @entity { } type ERC20 @entity { - """ - Format: 'address' - """ id: ID! chainId: Int! @index address: String! @index @@ -100,9 +105,6 @@ type ERC20 @entity { } type ERC20Holder @entity { - """ - Format: 'address:account' - """ id: ID! chainId: Int! @index address: String! @index @@ -112,9 +114,6 @@ type ERC20Holder @entity { } type ERC20State @entity { - """ - Format: 'address:blockNumber' - """ id: ID! chainId: Int! @index timestamp: DateTime! @index @@ -126,9 +125,6 @@ type ERC20State @entity { } type ERC20Balance @entity { - """ - Format: 'address:account:blockNumber' - """ id: ID! chainId: Int! @index timestamp: DateTime! @index diff --git a/src/base/erc20.ts b/src/base/erc20.ts index e02e5746..bc52ec01 100644 --- a/src/base/erc20.ts +++ b/src/base/erc20.ts @@ -1,7 +1,7 @@ import * as otoken from '@abi/otoken' import { createERC20Tracker } from '@templates/erc20' import { createERC20SimpleTracker } from '@templates/erc20-simple' -import { createRebasingERC20Tracker } from '@templates/erc20/erc20-rebasing' +import { createRebasingERC20Tracker, getErc20RebasingParams } from '@templates/erc20/erc20-rebasing' import { OGN_BASE_ADDRESS } from '@utils/addresses' import { baseAddresses } from '@utils/addresses-base' import { logFilter } from '@utils/logFilter' @@ -31,28 +31,7 @@ export const baseERC20s = [ const oToken = new otoken.Contract(ctx, block.header, baseAddresses.tokens.superOETHb) return oToken.rebasingCreditsPerTokenHighres() }, - enableRpcBalance: { - filter: logFilter({ - address: [baseAddresses.tokens.superOETHb], - topic0: [otoken.events.YieldDelegated.topic], - range: { from: 23192884 }, - }), - decode: (log) => { - const data = otoken.events.YieldDelegated.decode(log) - return { addresses: [data.source, data.target] } - }, - }, - disableRpcBalance: { - filter: logFilter({ - address: [baseAddresses.tokens.superOETHb], - topic0: [otoken.events.YieldUndelegated.topic], - range: { from: 23192884 }, - }), - decode: (log) => { - const data = otoken.events.YieldUndelegated.decode(log) - return { addresses: [data.source, data.target] } - }, - }, + ...getErc20RebasingParams({ from: 17819702, address: baseAddresses.tokens.superOETHb }), }, }), // wsuperOETHb diff --git a/src/mainnet/processors/erc20s.ts b/src/mainnet/processors/erc20s.ts index f8048fd5..cbc7a06f 100644 --- a/src/mainnet/processors/erc20s.ts +++ b/src/mainnet/processors/erc20s.ts @@ -1,7 +1,7 @@ import * as otoken from '@abi/otoken' import { createERC20Tracker } from '@templates/erc20' import { createERC20SimpleTracker } from '@templates/erc20-simple' -import { createRebasingERC20Tracker } from '@templates/erc20/erc20-rebasing' +import { createRebasingERC20Tracker, getErc20RebasingParams } from '@templates/erc20/erc20-rebasing' import { OETH_ADDRESS, OETH_DRIPPER_ADDRESS, @@ -62,28 +62,7 @@ const rebasingTracks: Record { - const data = otoken.events.YieldDelegated.decode(log) - return { addresses: [data.source, data.target] } - }, - }, - disableRpcBalance: { - filter: logFilter({ - address: [OETH_ADDRESS], - topic0: [otoken.events.YieldUndelegated.topic], - range: { from: 21317452 }, - }), - decode: (log) => { - const data = otoken.events.YieldUndelegated.decode(log) - return { addresses: [data.source, data.target] } - }, - }, + ...getErc20RebasingParams({ from: 16935276, address: OETH_ADDRESS }), }, }, } diff --git a/src/model/generated/erc20.model.ts b/src/model/generated/erc20.model.ts index bf8a20e7..29deb98b 100644 --- a/src/model/generated/erc20.model.ts +++ b/src/model/generated/erc20.model.ts @@ -6,9 +6,6 @@ export class ERC20 { Object.assign(this, props) } - /** - * Format: 'address' - */ @PrimaryColumn_() id!: string diff --git a/src/model/generated/erc20Balance.model.ts b/src/model/generated/erc20Balance.model.ts index 2db92474..43cf2344 100644 --- a/src/model/generated/erc20Balance.model.ts +++ b/src/model/generated/erc20Balance.model.ts @@ -6,9 +6,6 @@ export class ERC20Balance { Object.assign(this, props) } - /** - * Format: 'address:account:blockNumber' - */ @PrimaryColumn_() id!: string diff --git a/src/model/generated/erc20Holder.model.ts b/src/model/generated/erc20Holder.model.ts index ed76596b..ee9bf199 100644 --- a/src/model/generated/erc20Holder.model.ts +++ b/src/model/generated/erc20Holder.model.ts @@ -6,9 +6,6 @@ export class ERC20Holder { Object.assign(this, props) } - /** - * Format: 'address:account' - */ @PrimaryColumn_() id!: string diff --git a/src/model/generated/erc20State.model.ts b/src/model/generated/erc20State.model.ts index f10242db..370ffe9b 100644 --- a/src/model/generated/erc20State.model.ts +++ b/src/model/generated/erc20State.model.ts @@ -6,9 +6,6 @@ export class ERC20State { Object.assign(this, props) } - /** - * Format: 'address:blockNumber' - */ @PrimaryColumn_() id!: string diff --git a/src/model/generated/index.ts b/src/model/generated/index.ts index f4295264..8c8d27bf 100644 --- a/src/model/generated/index.ts +++ b/src/model/generated/index.ts @@ -80,6 +80,7 @@ export * from "./frrsRewardsPerSecondChanged.model" export * from "./frrsRewardsTargetChange.model" export * from "./frrsStrategistUpdated.model" export * from "./processingStatus.model" +export * from "./processorMetadata.model" export * from "./exchangeRate.model" export * from "./strategyBalance.model" export * from "./strategyYield.model" diff --git a/src/model/generated/processorMetadata.model.ts b/src/model/generated/processorMetadata.model.ts new file mode 100644 index 00000000..a4a4d9c3 --- /dev/null +++ b/src/model/generated/processorMetadata.model.ts @@ -0,0 +1,17 @@ +import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, JSONColumn as JSONColumn_} from "@subsquid/typeorm-store" + +/** + * Simple KV store to store metadata for processors. + */ +@Entity_() +export class ProcessorMetadata { + constructor(props?: Partial) { + Object.assign(this, props) + } + + @PrimaryColumn_() + id!: string + + @JSONColumn_({nullable: false}) + data!: unknown +} diff --git a/src/ousd/processors/erc20s.ts b/src/ousd/processors/erc20s.ts index a8c04f69..73515e24 100644 --- a/src/ousd/processors/erc20s.ts +++ b/src/ousd/processors/erc20s.ts @@ -1,6 +1,6 @@ import * as otoken from '@abi/otoken' import { createERC20Tracker } from '@templates/erc20' -import { createRebasingERC20Tracker } from '@templates/erc20/erc20-rebasing' +import { createRebasingERC20Tracker, getErc20RebasingParams } from '@templates/erc20/erc20-rebasing' import { OUSD_ADDRESS, OUSD_VAULT_ADDRESS, ousdStrategyArray, tokens } from '@utils/addresses' import { logFilter } from '@utils/logFilter' @@ -29,28 +29,7 @@ const rebasingTracks: Record { - const data = otoken.events.YieldDelegated.decode(log) - return { addresses: [data.source, data.target] } - }, - }, - disableRpcBalance: { - filter: logFilter({ - address: [OUSD_ADDRESS], - topic0: [otoken.events.YieldUndelegated.topic], - range: { from: 21317452 }, - }), - decode: (log) => { - const data = otoken.events.YieldUndelegated.decode(log) - return { addresses: [data.source, data.target] } - }, - }, + ...getErc20RebasingParams({ from: 11585978, address: OUSD_ADDRESS }), }, }, } diff --git a/src/ousd/validators/validate-ousd/validate-ousd.ts b/src/ousd/validators/validate-ousd/validate-ousd.ts index efbbd7ba..4f2962cf 100644 --- a/src/ousd/validators/validate-ousd/validate-ousd.ts +++ b/src/ousd/validators/validate-ousd/validate-ousd.ts @@ -1,7 +1,7 @@ import { ERC20Balance, OToken, OTokenAPY, OTokenDailyStat, OTokenHistory, OTokenRebase } from '@model' import { Context } from '@processor' import { env } from '@utils/env' -import { entities } from '@validation/entities' +import { entities, manualEntities } from '@validation/entities' import { validateExpectations } from '@validation/validate' export const name = 'validate-ousd' @@ -17,6 +17,7 @@ export const process = async (ctx: Context) => { await validateExpectations(ctx, block, OTokenRebase, firstBlock, entities.ousd_oTokenRebases) await validateExpectations(ctx, block, OTokenDailyStat, firstBlock, entities.ousd_oTokenDailyStats) await validateExpectations(ctx, block, ERC20Balance, firstBlock, entities.ousd_erc20Balances) + await validateExpectations(ctx, block, ERC20Balance, firstBlock, manualEntities.erc20_discrepancy_testing) firstBlock = false } } diff --git a/src/templates/erc20/erc20-rebasing.ts b/src/templates/erc20/erc20-rebasing.ts index af0d16b9..31e7ce24 100644 --- a/src/templates/erc20/erc20-rebasing.ts +++ b/src/templates/erc20/erc20-rebasing.ts @@ -1,5 +1,6 @@ import * as abi from '@abi/erc20' -import { ERC20, ERC20Balance, ERC20Holder, ERC20State, ERC20Transfer } from '@model' +import * as otoken from '@abi/otoken' +import { ERC20, ERC20Balance, ERC20Holder, ERC20State, ERC20Transfer, ProcessorMetadata } from '@model' import { Block, Context, Log } from '@processor' import { publishERC20State } from '@shared/erc20' import { EvmBatchProcessor } from '@subsquid/evm-processor' @@ -22,6 +23,11 @@ export const createRebasingERC20Tracker = ({ getCreditsPerToken: (ctx: Context, block: Block) => Promise enableRpcBalance?: { filter: LogFilter; decode: (log: Log) => { addresses: string[] } } disableRpcBalance?: { filter: LogFilter; decode: (log: Log) => { addresses: string[] } } + isEligibleForRebasing?: (ctx: Context, block: Block, account: string) => Promise + hooks?: { + filter: LogFilter + action: (ctx: Context, block: Block, log: Log) => Promise + }[] } }) => { if (duplicateTracker.has(address)) { @@ -91,7 +97,7 @@ export const createRebasingERC20Tracker = ({ const holdersArray = await ctx.store.findBy(ERC20Holder, { chainId: ctx.chain.id, address }) holders = new Map() for (const holder of holdersArray) { - holders.set(holder.account, holder.rebasingCredits ?? 0n) + holders.set(holder.account, holder.rebasingCredits ?? null) } time('initialize holders') } @@ -133,7 +139,10 @@ export const createRebasingERC20Tracker = ({ const account = accounts[i].toLowerCase() if (account === ADDRESS_ZERO) continue const id = `${ctx.chain.id}-${block.header.height}-${address}-${account}` - const useRpcBalance = holders.get(account) === null + let useRpcBalance = holders.get(account) === null + if (!holders.has(account) && rebasing.isEligibleForRebasing) { + useRpcBalance = !(await rebasing.isEligibleForRebasing(ctx, block, account)) + } const rebasingCredits = useRpcBalance ? null : credits[i] const newBalance = rebasingCredits === null @@ -168,7 +177,7 @@ export const createRebasingERC20Tracker = ({ }) if (!holders.has(account)) { doStateUpdate = true - holders.set(account, credits[i]) + holders.set(account, rebasingCredits) } result.holders.set(holder.account, holder) result.removedHolders.delete(holder.account) @@ -180,8 +189,9 @@ export const createRebasingERC20Tracker = ({ await updateState() } } - const updateAllBalances = async () => { + const updateAllBalances = async (rebasingOnly: boolean = false) => { for (const [account, rebasingCredits] of holders.entries()) { + if (rebasingOnly && rebasingCredits === null) continue const balance = rebasingCredits === null ? await contract.balanceOf(account) @@ -243,7 +253,7 @@ export const createRebasingERC20Tracker = ({ const isRebaseLog = rebasing?.rebaseEventFilter.matches(log) if (isRebaseLog) { await updateState() - await updateAllBalances() + await updateAllBalances(true) time('rebase log') } const isEnableRpcBalanceLog = rebasing?.enableRpcBalance?.filter.matches(log) @@ -260,6 +270,11 @@ export const createRebasingERC20Tracker = ({ holders.set(account, await rebasing.getCredits(ctx, block, account)) } } + for (const hook of rebasing.hooks ?? []) { + if (hook.filter.matches(log)) { + await hook.action(ctx, block, log) + } + } } } await Promise.all([ @@ -267,7 +282,11 @@ export const createRebasingERC20Tracker = ({ ctx.store.insert([...result.states.values()]), ctx.store.insert([...result.balances.values()]), ctx.store.insert([...result.transfers.values()]), - ctx.store.remove([...result.removedHolders.values()].map((id) => new ERC20Holder({ id }))), + ctx.store.remove( + [...result.removedHolders.values()].map( + (account) => new ERC20Holder({ id: `${ctx.chain.id}-${address}-${account}` }), + ), + ), ]) time('save') publishERC20State(ctx, address, result) @@ -275,3 +294,79 @@ export const createRebasingERC20Tracker = ({ }, } } + +export const getErc20RebasingParams = ({ from, address }: { from: number; address: string }) => { + const data: Pick< + Parameters[0]['rebasing'], + 'enableRpcBalance' | 'disableRpcBalance' | 'isEligibleForRebasing' | 'hooks' + > = { + enableRpcBalance: { + filter: logFilter({ + address: [address], + topic0: [otoken.events.YieldDelegated.topic], + range: { from: 23192884 }, + }), + decode: (log) => { + const data = otoken.events.YieldDelegated.decode(log) + return { addresses: [data.source, data.target] } + }, + }, + disableRpcBalance: { + filter: logFilter({ + address: [address], + topic0: [otoken.events.YieldUndelegated.topic], + range: { from: 23192884 }, + }), + decode: (log) => { + const data = otoken.events.YieldUndelegated.decode(log) + return { addresses: [data.source, data.target] } + }, + }, + isEligibleForRebasing: async (ctx, block, account: string) => { + const metadata = await ctx.store.get(ProcessorMetadata, `erc20-rebasing:${address}:${account}`) + if (metadata) { + const data = metadata.data as { rebasing: boolean } + return data.rebasing + } + let isContract: boolean = false + if (account !== '0x0000000000000000000000000000000000000000') { + isContract = + (await ctx._chain.client.call('eth_getCode', [account, `0x${block.header.height.toString(16)}`])) !== '0x' + } + return !isContract + }, + hooks: [ + { + filter: logFilter({ + address: [address], + topic0: [otoken.events.AccountRebasingEnabled.topic], + range: { from }, + }), + action: async (ctx, block, log) => { + const data = otoken.events.AccountRebasingEnabled.decode(log) + const metadata = new ProcessorMetadata({ + id: `erc20-rebasing:${address}:${data.account}`, + data: { rebasing: true }, + }) + await ctx.store.upsert(metadata) + }, + }, + { + filter: logFilter({ + address: [address], + topic0: [otoken.events.AccountRebasingDisabled.topic], + range: { from }, + }), + action: async (ctx, block, log) => { + const data = otoken.events.AccountRebasingDisabled.decode(log) + const metadata = new ProcessorMetadata({ + id: `erc20-rebasing:${address}:${data.account}`, + data: { rebasing: false }, + }) + await ctx.store.upsert(metadata) + }, + }, + ], + } + return data +} diff --git a/src/templates/erc20/erc20.ts b/src/templates/erc20/erc20.ts index 9d2d7009..083a4f27 100644 --- a/src/templates/erc20/erc20.ts +++ b/src/templates/erc20/erc20.ts @@ -205,7 +205,11 @@ export const createERC20Tracker = ({ ctx.store.insert([...result.states.values()]), ctx.store.insert([...result.balances.values()]), ctx.store.insert([...result.transfers.values()]), - ctx.store.remove([...result.removedHolders.values()].map((id) => new ERC20Holder({ id }))), + ctx.store.remove( + [...result.removedHolders.values()].map( + (account) => new ERC20Holder({ id: `${ctx.chain.id}-${address}-${account}` }), + ), + ), ]) publishERC20State(ctx, address, result) }, diff --git a/src/validation/entities.ts b/src/validation/entities.ts index 23ccae66..5ed498e2 100644 --- a/src/validation/entities.ts +++ b/src/validation/entities.ts @@ -1,6 +1,7 @@ import { sortBy } from 'lodash' import entitiesData from './entities.json' +import manualEntitiesData from './manual-entities.json' const e = (arr: any[]) => { return sortBy(arr, (v) => v.blockNumber) @@ -10,3 +11,8 @@ export const entities: Record = entitiesData for (const key of Object.keys(entities)) { entities[key as keyof typeof entities] = e(entities[key as keyof typeof entities]) } + +export const manualEntities: Record = manualEntitiesData +for (const key of Object.keys(manualEntities)) { + manualEntities[key as keyof typeof manualEntities] = e(manualEntities[key as keyof typeof manualEntities]) +} diff --git a/src/validation/manual-entities.json b/src/validation/manual-entities.json new file mode 100644 index 00000000..7fe8e985 --- /dev/null +++ b/src/validation/manual-entities.json @@ -0,0 +1,24 @@ +{ + "erc20_discrepancy_testing": [ + { + "id": "1-20329461-0x2a8e1e676ec238d8a992307b495b45b3feaa5e86-0x00000000009726632680fb29d3f7a9734e3010e2", + "chainId": 1, + "timestamp": "2024-07-17T23:13:59.000000Z", + "blockNumber": 20329461, + "address": "0x2a8e1e676ec238d8a992307b495b45b3feaa5e86", + "account": "0x00000000009726632680fb29d3f7a9734e3010e2", + "balance": "0", + "rebasingCredits": null + }, + { + "id": "1-15933628-0x2a8e1e676ec238d8a992307b495b45b3feaa5e86-0x00000000009726632680fb29d3f7a9734e3010e2", + "chainId": 1, + "timestamp": "2022-11-09T16:22:11.000000Z", + "blockNumber": 15933628, + "address": "0x2a8e1e676ec238d8a992307b495b45b3feaa5e86", + "account": "0x00000000009726632680fb29d3f7a9734e3010e2", + "balance": "81689765193240680895", + "rebasingCredits": null + } + ] +} From e107af07c749fb66913fc01a89752acf8193a653 Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Tue, 3 Dec 2024 14:50:43 -0800 Subject: [PATCH 4/8] wip rebasing switch on generic erc20 processor --- src/templates/erc20/erc20-rebasing.ts | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/templates/erc20/erc20-rebasing.ts b/src/templates/erc20/erc20-rebasing.ts index 31e7ce24..33b846fb 100644 --- a/src/templates/erc20/erc20-rebasing.ts +++ b/src/templates/erc20/erc20-rebasing.ts @@ -26,7 +26,15 @@ export const createRebasingERC20Tracker = ({ isEligibleForRebasing?: (ctx: Context, block: Block, account: string) => Promise hooks?: { filter: LogFilter - action: (ctx: Context, block: Block, log: Log) => Promise + action: ( + ctx: Context, + block: Block, + log: Log, + hooks: { + enableRebasing: (account: string) => Promise + disableRebasing: (account: string) => Promise + }, + ) => Promise }[] } }) => { @@ -272,7 +280,16 @@ export const createRebasingERC20Tracker = ({ } for (const hook of rebasing.hooks ?? []) { if (hook.filter.matches(log)) { - await hook.action(ctx, block, log) + await hook.action(ctx, block, log, { + async enableRebasing(account: string) { + holders.set(account, await rebasing.getCredits(ctx, block, account)) + await updateBalances([account]) + }, + async disableRebasing(account: string) { + holders.set(account, null) + await updateBalances([account]) + }, + }) } } } @@ -342,13 +359,14 @@ export const getErc20RebasingParams = ({ from, address }: { from: number; addres topic0: [otoken.events.AccountRebasingEnabled.topic], range: { from }, }), - action: async (ctx, block, log) => { + action: async (ctx, block, log, actions) => { const data = otoken.events.AccountRebasingEnabled.decode(log) const metadata = new ProcessorMetadata({ id: `erc20-rebasing:${address}:${data.account}`, data: { rebasing: true }, }) await ctx.store.upsert(metadata) + await actions.enableRebasing(data.account) }, }, { @@ -357,13 +375,14 @@ export const getErc20RebasingParams = ({ from, address }: { from: number; addres topic0: [otoken.events.AccountRebasingDisabled.topic], range: { from }, }), - action: async (ctx, block, log) => { + action: async (ctx, block, log, actions) => { const data = otoken.events.AccountRebasingDisabled.decode(log) const metadata = new ProcessorMetadata({ id: `erc20-rebasing:${address}:${data.account}`, data: { rebasing: false }, }) await ctx.store.upsert(metadata) + await actions.disableRebasing(data.account) }, }, ], From c6fad31a5b5f460e442ca1f191c88ea43d93aa61 Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Tue, 3 Dec 2024 15:40:56 -0800 Subject: [PATCH 5/8] also watch the old `TotalSupplyUpdated` for ousd --- abi/otoken-old.json | 27 +++++++++++++++++++++++++++ src/abi/otoken-old.ts | 13 +++++++++++++ src/ousd/processors/erc20s.ts | 3 ++- src/templates/erc20/erc20-rebasing.ts | 6 ++---- 4 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 abi/otoken-old.json create mode 100644 src/abi/otoken-old.ts diff --git a/abi/otoken-old.json b/abi/otoken-old.json new file mode 100644 index 00000000..1c129703 --- /dev/null +++ b/abi/otoken-old.json @@ -0,0 +1,27 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "rebasingCredits", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "rebasingCreditsPerToken", + "type": "uint256" + } + ], + "name": "TotalSupplyUpdated", + "type": "event" + } +] diff --git a/src/abi/otoken-old.ts b/src/abi/otoken-old.ts new file mode 100644 index 00000000..b4661cf9 --- /dev/null +++ b/src/abi/otoken-old.ts @@ -0,0 +1,13 @@ +import * as p from '@subsquid/evm-codec' +import { event, fun, viewFun, indexed, ContractBase } from '@subsquid/evm-abi' +import type { EventParams as EParams, FunctionArguments, FunctionReturn } from '@subsquid/evm-abi' + +export const events = { + TotalSupplyUpdated: event("0x99e56f783b536ffacf422d59183ea321dd80dcd6d23daa13023e8afea38c3df1", "TotalSupplyUpdated(uint256,uint256,uint256)", {"totalSupply": p.uint256, "rebasingCredits": p.uint256, "rebasingCreditsPerToken": p.uint256}), +} + +export class Contract extends ContractBase { +} + +/// Event types +export type TotalSupplyUpdatedEventArgs = EParams diff --git a/src/ousd/processors/erc20s.ts b/src/ousd/processors/erc20s.ts index 73515e24..32d6447f 100644 --- a/src/ousd/processors/erc20s.ts +++ b/src/ousd/processors/erc20s.ts @@ -1,4 +1,5 @@ import * as otoken from '@abi/otoken' +import * as otokenOld from '@abi/otoken-old' import { createERC20Tracker } from '@templates/erc20' import { createRebasingERC20Tracker, getErc20RebasingParams } from '@templates/erc20/erc20-rebasing' import { OUSD_ADDRESS, OUSD_VAULT_ADDRESS, ousdStrategyArray, tokens } from '@utils/addresses' @@ -11,7 +12,7 @@ const rebasingTracks: Record Date: Wed, 4 Dec 2024 07:53:41 -0800 Subject: [PATCH 6/8] debug & fix / improve --- ...48174412-Data.js => 1733327488810-Data.js} | 6 +- schema.graphql | 8 - schema/general.graphql | 8 - src/base/erc20.ts | 6 +- src/mainnet/processors/erc20s.ts | 2 +- src/model/generated/index.ts | 1 - .../generated/processorMetadata.model.ts | 17 -- src/ousd/processors/erc20s.ts | 2 +- src/templates/erc20/erc20-rebasing.ts | 152 +++++++++++++----- 9 files changed, 119 insertions(+), 83 deletions(-) rename db/migrations/{1733248174412-Data.js => 1733327488810-Data.js} (99%) delete mode 100644 src/model/generated/processorMetadata.model.ts diff --git a/db/migrations/1733248174412-Data.js b/db/migrations/1733327488810-Data.js similarity index 99% rename from db/migrations/1733248174412-Data.js rename to db/migrations/1733327488810-Data.js index 8d096948..6e15fe65 100644 --- a/db/migrations/1733248174412-Data.js +++ b/db/migrations/1733327488810-Data.js @@ -1,5 +1,5 @@ -module.exports = class Data1733248174412 { - name = 'Data1733248174412' +module.exports = class Data1733327488810 { + name = 'Data1733327488810' async up(db) { await db.query(`CREATE TABLE "aero_cl_gauge_claim_fees" ("id" character varying NOT NULL, "chain_id" integer NOT NULL, "block_number" integer NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "address" text NOT NULL, "from" text NOT NULL, "claimed0" numeric NOT NULL, "claimed1" numeric NOT NULL, CONSTRAINT "PK_324db7f817fe71a6a8dfc04701a" PRIMARY KEY ("id"))`) @@ -494,7 +494,6 @@ module.exports = class Data1733248174412 { await db.query(`CREATE INDEX "IDX_7f49f2839212126c3d42fb4bf1" ON "frrs_strategist_updated" ("block_number") `) await db.query(`CREATE INDEX "IDX_aab8ae0cb29505a7014438675b" ON "frrs_strategist_updated" ("tx_hash") `) await db.query(`CREATE TABLE "processing_status" ("id" character varying NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "block_number" integer NOT NULL, CONSTRAINT "PK_85f5e2467b74fb70fac1a053021" PRIMARY KEY ("id"))`) - await db.query(`CREATE TABLE "processor_metadata" ("id" character varying NOT NULL, "data" jsonb NOT NULL, CONSTRAINT "PK_6accadf4e3bd54d481c86cd810f" PRIMARY KEY ("id"))`) await db.query(`CREATE TABLE "exchange_rate" ("id" character varying NOT NULL, "chain_id" integer NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "block_number" integer NOT NULL, "pair" text NOT NULL, "base" text NOT NULL, "quote" text NOT NULL, "rate" numeric NOT NULL, CONSTRAINT "PK_5c5d27d2b900ef6cdeef0398472" PRIMARY KEY ("id"))`) await db.query(`CREATE INDEX "IDX_2b58051dcc72cf0f02aa41ff14" ON "exchange_rate" ("chain_id") `) await db.query(`CREATE INDEX "IDX_9e23a3f1bf3634820c873a0fe8" ON "exchange_rate" ("timestamp") `) @@ -1222,7 +1221,6 @@ module.exports = class Data1733248174412 { await db.query(`DROP INDEX "public"."IDX_7f49f2839212126c3d42fb4bf1"`) await db.query(`DROP INDEX "public"."IDX_aab8ae0cb29505a7014438675b"`) await db.query(`DROP TABLE "processing_status"`) - await db.query(`DROP TABLE "processor_metadata"`) await db.query(`DROP TABLE "exchange_rate"`) await db.query(`DROP INDEX "public"."IDX_2b58051dcc72cf0f02aa41ff14"`) await db.query(`DROP INDEX "public"."IDX_9e23a3f1bf3634820c873a0fe8"`) diff --git a/schema.graphql b/schema.graphql index 41148c53..35801018 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1063,14 +1063,6 @@ type FRRSStrategistUpdated @entity { blockNumber: Int! } -""" -Simple KV store to store metadata for processors. -""" -type ProcessorMetadata @entity { - id: ID! - data: JSON! -} - """ Any entity which has a price associated with it should have that price go in here. Prices can change very frequently and we don't want those changes on the same track diff --git a/schema/general.graphql b/schema/general.graphql index 12cb48eb..c75c6b6a 100644 --- a/schema/general.graphql +++ b/schema/general.graphql @@ -4,14 +4,6 @@ type ProcessingStatus @entity { blockNumber: Int! } -""" -Simple KV store to store metadata for processors. -""" -type ProcessorMetadata @entity { - id: ID! - data: JSON! -} - """ Any entity which has a price associated with it should have that price go in here. Prices can change very frequently and we don't want those changes on the same track diff --git a/src/base/erc20.ts b/src/base/erc20.ts index bc52ec01..a070ab65 100644 --- a/src/base/erc20.ts +++ b/src/base/erc20.ts @@ -31,7 +31,11 @@ export const baseERC20s = [ const oToken = new otoken.Contract(ctx, block.header, baseAddresses.tokens.superOETHb) return oToken.rebasingCreditsPerTokenHighres() }, - ...getErc20RebasingParams({ from: 17819702, address: baseAddresses.tokens.superOETHb }), + ...getErc20RebasingParams({ + from: 17819702, + yieldDelegationFrom: 23192884, + address: baseAddresses.tokens.superOETHb, + }), }, }), // wsuperOETHb diff --git a/src/mainnet/processors/erc20s.ts b/src/mainnet/processors/erc20s.ts index cbc7a06f..4e0e49e0 100644 --- a/src/mainnet/processors/erc20s.ts +++ b/src/mainnet/processors/erc20s.ts @@ -62,7 +62,7 @@ const rebasingTracks: Record) { - Object.assign(this, props) - } - - @PrimaryColumn_() - id!: string - - @JSONColumn_({nullable: false}) - data!: unknown -} diff --git a/src/ousd/processors/erc20s.ts b/src/ousd/processors/erc20s.ts index 32d6447f..2aa3f4a1 100644 --- a/src/ousd/processors/erc20s.ts +++ b/src/ousd/processors/erc20s.ts @@ -30,7 +30,7 @@ const rebasingTracks: Record() @@ -26,10 +27,11 @@ export const createRebasingERC20Tracker = ({ isEligibleForRebasing?: (ctx: Context, block: Block, account: string) => Promise hooks?: { filter: LogFilter + traceFilter?: TraceFilter action: ( ctx: Context, block: Block, - log: Log, + params: { log: Log } | { trace: Trace }, hooks: { enableRebasing: (account: string) => Promise disableRebasing: (account: string) => Promise @@ -82,6 +84,12 @@ export const createRebasingERC20Tracker = ({ setup(processor: EvmBatchProcessor) { processor.addLog(transferLogFilter.value) processor.addLog(rebasing.rebaseEventFilter.value) + for (const hook of rebasing.hooks ?? []) { + processor.addLog(hook.filter.value) + if (hook.traceFilter) { + processor.addTrace(hook.traceFilter.value) + } + } }, async process(ctx: Context) { const debugLogging = global.process.env.DEBUG_PERF === 'true' @@ -233,6 +241,18 @@ export const createRebasingERC20Tracker = ({ time('update all balances') } + const hookActions = { + async enableRebasing(account: string) { + holders.set(account, await rebasing.getCredits(ctx, block, account)) + await updateBalances([account]) + }, + async disableRebasing(account: string) { + holders.set(account, null) + await updateBalances([account]) + }, + } + + // Iterate Logs for (const log of block.logs) { const isTransferLog = transferLogFilter.matches(log) if (isTransferLog) { @@ -276,18 +296,19 @@ export const createRebasingERC20Tracker = ({ holders.set(account, await rebasing.getCredits(ctx, block, account)) } } + for (const hook of rebasing.hooks ?? []) { if (hook.filter.matches(log)) { - await hook.action(ctx, block, log, { - async enableRebasing(account: string) { - holders.set(account, await rebasing.getCredits(ctx, block, account)) - await updateBalances([account]) - }, - async disableRebasing(account: string) { - holders.set(account, null) - await updateBalances([account]) - }, - }) + await hook.action(ctx, block, { log }, hookActions) + } + } + } + + // Iterate Traces + for (const trace of block.traces) { + for (const hook of rebasing.hooks ?? []) { + if (hook.traceFilter?.matches(trace)) { + await hook.action(ctx, block, { trace }, hookActions) } } } @@ -306,11 +327,34 @@ export const createRebasingERC20Tracker = ({ time('save') publishERC20State(ctx, address, result) time('publish') + + // const lastBlock = ctx.blocks[ctx.blocks.length - 1] + // if (lastBlock.header.height === 23192884) { + // for (const account of holders.keys()) { + // const contract = new otoken.Contract(ctx, lastBlock.header, address) + // const balance = await contract.balanceOf(account) + // const state = await contract.rebaseState(account) + // const blah = holders.get(account) + // const holder = await ctx.store.get(ERC20Holder, `${ctx.chain.id}-${address}-${account}`) + // if (holder?.balance !== balance) { + // console.log('mismatch', account, holder?.balance, balance, state, blah) + // process.exit(1) + // } + // } + // } }, } } -export const getErc20RebasingParams = ({ from, address }: { from: number; address: string }) => { +export const getErc20RebasingParams = ({ + from, + yieldDelegationFrom, + address, +}: { + from: number + yieldDelegationFrom: number + address: string +}) => { const data: Pick< Parameters[0]['rebasing'], 'enableRpcBalance' | 'disableRpcBalance' | 'isEligibleForRebasing' | 'hooks' @@ -319,7 +363,7 @@ export const getErc20RebasingParams = ({ from, address }: { from: number; addres filter: logFilter({ address: [address], topic0: [otoken.events.YieldDelegated.topic], - range: { from: 23192884 }, + range: { from: yieldDelegationFrom }, }), decode: (log) => { const data = otoken.events.YieldDelegated.decode(log) @@ -330,7 +374,7 @@ export const getErc20RebasingParams = ({ from, address }: { from: number; addres filter: logFilter({ address: [address], topic0: [otoken.events.YieldUndelegated.topic], - range: { from: 23192884 }, + range: { from: yieldDelegationFrom }, }), decode: (log) => { const data = otoken.events.YieldUndelegated.decode(log) @@ -338,17 +382,17 @@ export const getErc20RebasingParams = ({ from, address }: { from: number; addres }, }, isEligibleForRebasing: async (ctx, block, account: string) => { - const metadata = await ctx.store.get(ProcessorMetadata, `erc20-rebasing:${address}:${account}`) - if (metadata) { - const data = metadata.data as { rebasing: boolean } - return data.rebasing - } - let isContract: boolean = false - if (account !== '0x0000000000000000000000000000000000000000') { - isContract = - (await ctx._chain.client.call('eth_getCode', [account, `0x${block.header.height.toString(16)}`])) !== '0x' + const contract = new otoken.Contract(ctx, block.header, address) + const rebaseState = await contract.rebaseState(account) + if (rebaseState === 0) { + let isContract: boolean = false + if (account !== '0x0000000000000000000000000000000000000000') { + isContract = + (await ctx._chain.client.call('eth_getCode', [account, `0x${block.header.height.toString(16)}`])) !== '0x' + } + return !isContract } - return !isContract + return rebaseState === 2 }, hooks: [ { @@ -357,14 +401,26 @@ export const getErc20RebasingParams = ({ from, address }: { from: number; addres topic0: [otoken.events.AccountRebasingEnabled.topic], range: { from }, }), - action: async (ctx, block, log, actions) => { - const data = otoken.events.AccountRebasingEnabled.decode(log) - const metadata = new ProcessorMetadata({ - id: `erc20-rebasing:${address}:${data.account}`, - data: { rebasing: true }, - }) - await ctx.store.upsert(metadata) - await actions.enableRebasing(data.account) + traceFilter: traceFilter({ + callTo: [address], + type: ['call'], + callSighash: [otoken.functions.rebaseOptIn.selector], + range: { from }, + }), + action: async (ctx, block, params, actions) => { + let account: string | undefined = undefined + if ('log' in params) { + const data = otoken.events.AccountRebasingEnabled.decode(params.log) + account = data.account + } + if ('trace' in params && params.trace.type === 'call') { + account = params.trace.action.from + } + if (account) { + await actions.enableRebasing(account) + } else { + throw new Error('No account found for rebasing opt-in') + } }, }, { @@ -373,14 +429,26 @@ export const getErc20RebasingParams = ({ from, address }: { from: number; addres topic0: [otoken.events.AccountRebasingDisabled.topic], range: { from }, }), - action: async (ctx, block, log, actions) => { - const data = otoken.events.AccountRebasingDisabled.decode(log) - const metadata = new ProcessorMetadata({ - id: `erc20-rebasing:${address}:${data.account}`, - data: { rebasing: false }, - }) - await ctx.store.upsert(metadata) - await actions.disableRebasing(data.account) + traceFilter: traceFilter({ + callTo: [address], + type: ['call'], + callSighash: [otoken.functions.rebaseOptOut.selector], + range: { from }, + }), + action: async (ctx, block, params, actions) => { + let account: string | undefined = undefined + if ('log' in params) { + const data = otoken.events.AccountRebasingDisabled.decode(params.log) + account = data.account + } + if ('trace' in params && params.trace.type === 'call') { + account = params.trace.action.from + } + if (account) { + await actions.disableRebasing(account) + } else { + throw new Error('No account found for rebasing opt-out') + } }, }, ], From 7a3da362bf40c62dc570e9f768a04f6b95cfd755 Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Thu, 5 Dec 2024 11:54:48 -0800 Subject: [PATCH 7/8] debug & fix / improve --- src/oeth/processors/ccip.ts | 2 +- src/oeth/processors/oeth.ts | 1 + src/oeth/processors/strategies.ts | 1 + src/processor.ts | 2 + src/templates/erc20/erc20-rebasing.ts | 40 ++++++++++++------- .../processor-status/processor-status.ts | 1 + 6 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/oeth/processors/ccip.ts b/src/oeth/processors/ccip.ts index 5734e855..46c3dee6 100644 --- a/src/oeth/processors/ccip.ts +++ b/src/oeth/processors/ccip.ts @@ -181,5 +181,5 @@ export const ccip = (params: { chainId: 1 | 42161 }) => { await ctx.store.upsert([...result.bridgeTransferStates.values()]) } - return { from, setup, process } + return { name: `ccip-${params.chainId}`, from, setup, process } } diff --git a/src/oeth/processors/oeth.ts b/src/oeth/processors/oeth.ts index 4c92d5d9..e0526d14 100644 --- a/src/oeth/processors/oeth.ts +++ b/src/oeth/processors/oeth.ts @@ -92,6 +92,7 @@ const otokenActivityProcessor = createOTokenActivityProcessor({ }, }) +export const name = 'oeth' export const from = Math.min(otokenProcessor.from, otokenActivityProcessor.from) export const setup = (processor: EvmBatchProcessor) => { otokenProcessor.setup(processor) diff --git a/src/oeth/processors/strategies.ts b/src/oeth/processors/strategies.ts index ea986265..51a13003 100644 --- a/src/oeth/processors/strategies.ts +++ b/src/oeth/processors/strategies.ts @@ -192,6 +192,7 @@ const eventProcessors = [ }), ] +export const name = 'strategies' export const from = Math.min(...strategies.map((s) => s.from)) export const setup = (processor: EvmBatchProcessor) => { diff --git a/src/processor.ts b/src/processor.ts index cfe5bc1a..1ef15bd3 100644 --- a/src/processor.ts +++ b/src/processor.ts @@ -128,6 +128,8 @@ export const run = ({ chainId = 1, stateSchema, processors, postProcessors, vali processors = processors.filter((p) => p.name?.includes(process.env.PROCESSOR!)) } + console.log('Processors:\n - ', processors.map((p) => p.name).join('\n - ')) + const config = chainConfigs[chainId] if (!config) throw new Error('No chain configuration found.') const evmBatchProcessor = createEvmBatchProcessor(config) diff --git a/src/templates/erc20/erc20-rebasing.ts b/src/templates/erc20/erc20-rebasing.ts index 84642cbe..e0d31050 100644 --- a/src/templates/erc20/erc20-rebasing.ts +++ b/src/templates/erc20/erc20-rebasing.ts @@ -79,7 +79,11 @@ export const createRebasingERC20Tracker = ({ ctx.log.error({ height: block.header.height, err }, `Failed to get contract name for ${address}`) } } + + let checkBalances = 0 + return { + name: `rebasing-erc20-${address}`, from, setup(processor: EvmBatchProcessor) { processor.addLog(transferLogFilter.value) @@ -328,20 +332,28 @@ export const createRebasingERC20Tracker = ({ publishERC20State(ctx, address, result) time('publish') - // const lastBlock = ctx.blocks[ctx.blocks.length - 1] - // if (lastBlock.header.height === 23192884) { - // for (const account of holders.keys()) { - // const contract = new otoken.Contract(ctx, lastBlock.header, address) - // const balance = await contract.balanceOf(account) - // const state = await contract.rebaseState(account) - // const blah = holders.get(account) - // const holder = await ctx.store.get(ERC20Holder, `${ctx.chain.id}-${address}-${account}`) - // if (holder?.balance !== balance) { - // console.log('mismatch', account, holder?.balance, balance, state, blah) - // process.exit(1) - // } - // } - // } + const lastBlock = ctx.blocks[ctx.blocks.length - 1] + if (checkBalances < Math.floor(lastBlock.header.height / 100000) && lastBlock.header.height > 14085199) { + checkBalances = Math.floor(lastBlock.header.height / 100000) + console.time('Checking balances') + let correctBalances = 0 + const holderEntities = await ctx.store.findBy(ERC20Holder, { chainId: ctx.chain.id, address }) + for (const holder of holderEntities) { + const account = holder.account + const contract = new otoken.Contract(ctx, lastBlock.header, address) + const balance = await contract.balanceOf(account) + if (holder.balance === balance) { + correctBalances++ + } + } + console.timeEnd('Checking balances') + console.log( + `Correct balances: ${correctBalances}/${holderEntities.length} (${( + (correctBalances / holderEntities.length) * + 100 + ).toFixed(2)}%)`, + ) + } }, } } diff --git a/src/templates/processor-status/processor-status.ts b/src/templates/processor-status/processor-status.ts index a078eb89..9470d161 100644 --- a/src/templates/processor-status/processor-status.ts +++ b/src/templates/processor-status/processor-status.ts @@ -11,6 +11,7 @@ export const processStatus = (id: string) => { } return { + name: `processor-status-${id}`, async process(ctx: Context) { const header = ctx.blocks[ctx.blocks.length - 1].header if (header) { From eec0ab7e8fcaadee311aa07e6a50c746b71fe614 Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Thu, 5 Dec 2024 11:58:28 -0800 Subject: [PATCH 8/8] have to process these otherwise yield delegation accounts will not get updates - means non rebasing accounts will have more entries in balances - could be improved later --- src/templates/erc20/erc20-rebasing.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/templates/erc20/erc20-rebasing.ts b/src/templates/erc20/erc20-rebasing.ts index e0d31050..28f1d1b4 100644 --- a/src/templates/erc20/erc20-rebasing.ts +++ b/src/templates/erc20/erc20-rebasing.ts @@ -207,9 +207,8 @@ export const createRebasingERC20Tracker = ({ await updateState() } } - const updateAllBalances = async (rebasingOnly: boolean = false) => { + const updateAllBalances = async () => { for (const [account, rebasingCredits] of holders.entries()) { - if (rebasingOnly && rebasingCredits === null) continue const balance = rebasingCredits === null ? await contract.balanceOf(account) @@ -283,7 +282,7 @@ export const createRebasingERC20Tracker = ({ const isRebaseLog = rebasing?.rebaseEventFilter.matches(log) if (isRebaseLog) { await updateState() - await updateAllBalances(true) + await updateAllBalances() time('rebase log') } const isEnableRpcBalanceLog = rebasing?.enableRpcBalance?.filter.matches(log)