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/db/migrations/1732646488522-Data.js b/db/migrations/1733327488810-Data.js similarity index 99% rename from db/migrations/1732646488522-Data.js rename to db/migrations/1733327488810-Data.js index 23b5167c..6e15fe65 100644 --- a/db/migrations/1732646488522-Data.js +++ b/db/migrations/1733327488810-Data.js @@ -1,5 +1,5 @@ -module.exports = class Data1732646488522 { - name = 'Data1732646488522' +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"))`) @@ -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..35801018 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1082,11 +1082,6 @@ type ExchangeRate @entity { rate: BigInt! } -enum RebasingOption { - OptIn - OptOut -} - enum HistoryType { Sent Received @@ -1152,9 +1147,6 @@ type NativeBalance @entity { } type ERC20 @entity { - """ - Format: 'address' - """ id: ID! chainId: Int! @index address: String! @index @@ -1164,9 +1156,6 @@ type ERC20 @entity { } type ERC20Holder @entity { - """ - Format: 'address:account' - """ id: ID! chainId: Int! @index address: String! @index @@ -1176,9 +1165,6 @@ type ERC20Holder @entity { } type ERC20State @entity { - """ - Format: 'address:blockNumber' - """ id: ID! chainId: Int! @index timestamp: DateTime! @index @@ -1190,9 +1176,6 @@ type ERC20State @entity { } type ERC20Balance @entity { - """ - Format: 'address:account:blockNumber' - """ id: ID! chainId: Int! @index timestamp: DateTime! @index @@ -1626,6 +1609,7 @@ type OTokenAddress @entity { balance: BigInt! earned: BigInt! credits: BigInt! + delegatedTo: String lastUpdated: DateTime! history: [OTokenHistory!]! @derivedFrom(field: "address") } @@ -1671,6 +1655,7 @@ type OTokenRebaseOption @entity { txHash: String! @index address: OTokenAddress! status: RebasingOption! + delegatedTo: String } type OTokenAPY @entity { @@ -1813,6 +1798,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..c75c6b6a 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 @@ -93,9 +88,6 @@ type NativeBalance @entity { } type ERC20 @entity { - """ - Format: 'address' - """ id: ID! chainId: Int! @index address: String! @index @@ -105,9 +97,6 @@ type ERC20 @entity { } type ERC20Holder @entity { - """ - Format: 'address:account' - """ id: ID! chainId: Int! @index address: String! @index @@ -117,9 +106,6 @@ type ERC20Holder @entity { } type ERC20State @entity { - """ - Format: 'address:blockNumber' - """ id: ID! chainId: Int! @index timestamp: DateTime! @index @@ -131,9 +117,6 @@ type ERC20State @entity { } type ERC20Balance @entity { - """ - Format: 'address:account:blockNumber' - """ id: ID! chainId: Int! @index timestamp: DateTime! @index 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/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/base/erc20.ts b/src/base/erc20.ts index 7a35f79b..a070ab65 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,6 +31,11 @@ export const baseERC20s = [ const oToken = new otoken.Contract(ctx, block.header, baseAddresses.tokens.superOETHb) return oToken.rebasingCreditsPerTokenHighres() }, + ...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 037ed0fe..4e0e49e0 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,6 +62,7 @@ const rebasingTracks: Record 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/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/ousd/processors/erc20s.ts b/src/ousd/processors/erc20s.ts index 726cc2dc..2aa3f4a1 100644 --- a/src/ousd/processors/erc20s.ts +++ b/src/ousd/processors/erc20s.ts @@ -1,6 +1,7 @@ import * as otoken from '@abi/otoken' +import * as otokenOld from '@abi/otoken-old' 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' @@ -11,7 +12,7 @@ const rebasingTracks: Record { 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/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 0d02e9cd..28f1d1b4 100644 --- a/src/templates/erc20/erc20-rebasing.ts +++ b/src/templates/erc20/erc20-rebasing.ts @@ -1,11 +1,13 @@ import * as abi from '@abi/erc20' +import * as otoken from '@abi/otoken' import { ERC20, ERC20Balance, ERC20Holder, ERC20State, ERC20Transfer } from '@model' -import { Block, Context } from '@processor' +import { Block, Context, Log, Trace } from '@processor' import { publishERC20State } from '@shared/erc20' import { EvmBatchProcessor } from '@subsquid/evm-processor' import { ADDRESS_ZERO } from '@utils/addresses' import { LogFilter, logFilter } from '@utils/logFilter' import { TokenAddress } from '@utils/symbols' +import { TraceFilter, traceFilter } from '@utils/traceFilter' const duplicateTracker = new Set() @@ -20,6 +22,22 @@ 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[] } } + isEligibleForRebasing?: (ctx: Context, block: Block, account: string) => Promise + hooks?: { + filter: LogFilter + traceFilter?: TraceFilter + action: ( + ctx: Context, + block: Block, + params: { log: Log } | { trace: Trace }, + hooks: { + enableRebasing: (account: string) => Promise + disableRebasing: (account: string) => Promise + }, + ) => Promise + }[] } }) => { if (duplicateTracker.has(address)) { @@ -32,7 +50,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], @@ -61,11 +79,21 @@ 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) 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' @@ -89,7 +117,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') } @@ -131,6 +159,17 @@ export const createRebasingERC20Tracker = ({ const account = accounts[i].toLowerCase() if (account === ADDRESS_ZERO) continue const id = `${ctx.chain.id}-${block.header.height}-${address}-${account}` + 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 + ? await contract.balanceOf(account) + : mostRecentState?.rebasingCreditsPerToken + ? (rebasingCredits * 10n ** 18n) / mostRecentState.rebasingCreditsPerToken + : 0n const balance = new ERC20Balance({ id, chainId: ctx.chain.id, @@ -138,10 +177,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) { @@ -158,10 +195,8 @@ export const createRebasingERC20Tracker = ({ balance: balance.balance, rebasingCredits: balance.rebasingCredits, }) - if (!holders.has(account)) { - doStateUpdate = true - holders.set(account, credits[i]) - } + doStateUpdate = true + holders.set(account, rebasingCredits) result.holders.set(holder.account, holder) result.removedHolders.delete(holder.account) } @@ -174,9 +209,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({ @@ -206,6 +244,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) { @@ -235,6 +285,35 @@ 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)) + } + } + + for (const hook of rebasing.hooks ?? []) { + if (hook.filter.matches(log)) { + 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) + } + } } } await Promise.all([ @@ -242,11 +321,148 @@ 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) time('publish') + + 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)}%)`, + ) + } + }, + } +} + +export const getErc20RebasingParams = ({ + from, + yieldDelegationFrom, + address, +}: { + from: number + yieldDelegationFrom: 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: yieldDelegationFrom }, + }), + 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: yieldDelegationFrom }, + }), + decode: (log) => { + const data = otoken.events.YieldUndelegated.decode(log) + return { addresses: [data.source, data.target] } + }, + }, + isEligibleForRebasing: async (ctx, block, account: string) => { + 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 rebaseState === 2 + }, + hooks: [ + { + filter: logFilter({ + address: [address], + topic0: [otoken.events.AccountRebasingEnabled.topic], + range: { from }, + }), + 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') + } + }, + }, + { + filter: logFilter({ + address: [address], + topic0: [otoken.events.AccountRebasingDisabled.topic], + range: { from }, + }), + 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') + } + }, + }, + ], } + 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/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, }) } 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) { 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 + } + ] +}