Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,28 @@ pnpm dev

Visit http://localhost:8080 to see the GraphQL Playground, local password is `testing`.

### Tokenized strategy lifecycle

`YearnV3Strategy.NewTokenizedStrategy(address,address,string)` is indexed with a
wildcard subscription on each configured chain. Initialization runs through
delegatecall and emits from the new strategy address, so discovery must work
before that address appears in a vault's `StrategyChanged` event.

The emitter must match the event's `strategy` parameter. Matching events register
the strategy for `Reported` and `StrategyShutdown`, which remain restricted to
discovered addresses. Existing vault and splitter discovery paths still apply.

`V3TokenizedStrategyDeployed` records the emitter as `strategyAddress`, the event's
`strategy`, `asset`, and `apiVersion`, and full block/transaction/log provenance.
The initialization event supplies no factory address. `V3StrategyShutdown`
records the strategy emitter and the same provenance fields. These are observed
events, not an endorsement or verification of a strategy's implementation.

Run the lifecycle routing regression tests with
`corepack pnpm exec vitest run test/tokenized-strategy.test.ts`.
Deploying this schema/config change requires the normal database reset and
backfill; verify deployment, report, and shutdown coverage at a fixed block.

### Environment Variables

Copy `.env.example` to `.env` and fill in values as needed:
Expand Down
4 changes: 3 additions & 1 deletion config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ contracts:
- name: YearnV3Strategy
handler: src/EventHandlers.ts
events:
- event: "NewTokenizedStrategy(address indexed strategy, address indexed asset, string apiVersion)"
- event: "Reported(uint256 profit, uint256 loss, uint256 protocolFees, uint256 performanceFees)"
- event: "StrategyShutdown()"
- name: YearnV3SplitterFactory
handler: src/EventHandlers.ts
events:
Expand Down Expand Up @@ -278,7 +280,7 @@ chains:
- name: YearnV3Accountant
address: []
- name: YearnV3Strategy
# No static addresses: strategies are discovered from vault/role-manager events.
# Discovered from strategy initialization and existing vault/splitter events.
address: []
- name: YearnV3SplitterFactory
address:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"start": "envio start",
"start:envio": "envio start",
"sync:vaults": "node scripts/update_kong_vaults.js",
"test": "vitest run && node --test monitoring/test/monitoring.test.js",
"test": "vitest run",
"calc-fees": "tsc -p tsconfig.calc.json && node build/calc/calculate-depositor-fees.js",
"jwt:generate": "node scripts/generate_hasura_jwt.js"
},
Expand Down
29 changes: 29 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,35 @@ type V3StrategyReported {
performanceFees: BigInt!
}

type V3TokenizedStrategyDeployed {
id: ID!
strategyAddress: String! @index
chainId: Int! @index
blockNumber: Int! @index
blockTimestamp: Int! @index
blockHash: String!
transactionHash: String! @index
transactionIndex: Int!
transactionFrom: String @index
logIndex: Int!
strategy: String! @index
asset: String! @index
apiVersion: String!
}

type V3StrategyShutdown {
id: ID!
strategyAddress: String! @index
chainId: Int! @index
blockNumber: Int! @index
blockTimestamp: Int! @index
blockHash: String!
transactionHash: String! @index
transactionIndex: Int!
transactionFrom: String @index
logIndex: Int!
}

type V3SplitterNewSplitter {
id: ID!
factoryAddress: String! @index
Expand Down
36 changes: 36 additions & 0 deletions src/EventHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ import type {
V3RoleManagerRemovedVault,
V3SplitterNewSplitter,
V3StrategyReported,
V3StrategyShutdown,
V3TokenizedStrategyDeployed,
V3VaultFactoryNewVault,
V3YieldSplitterNewYieldSplitter,
VeyfiGaugeRegistered,
Expand Down Expand Up @@ -1946,6 +1948,40 @@ indexer.onEvent({ contract: "YearnV3Strategy", event: "Reported" }, async ({ eve
context.V3StrategyReported.set(entity);
});

indexer.onEvent({ contract: "YearnV3Strategy", event: "StrategyShutdown" }, async ({ event, context }) => {
const entity: V3StrategyShutdown = {
...eventCore(event),
strategyAddress: getAddress(event.srcAddress),
};
context.V3StrategyShutdown.set(entity);
});

// Initialization is emitted by the strategy itself, before its address is known.
indexer.contractRegister(
{ contract: "YearnV3Strategy", event: "NewTokenizedStrategy", wildcard: true },
async ({ event, context }) => {
const strategyAddress = getAddress(event.srcAddress);
if (strategyAddress !== getAddress(event.params.strategy)) return;
context.chain.YearnV3Strategy.add(strategyAddress);
},
);

indexer.onEvent(
{ contract: "YearnV3Strategy", event: "NewTokenizedStrategy", wildcard: true },
async ({ event, context }) => {
const strategyAddress = getAddress(event.srcAddress);
if (strategyAddress !== getAddress(event.params.strategy)) return;
const entity: V3TokenizedStrategyDeployed = {
...eventCore(event),
strategyAddress,
strategy: getAddress(event.params.strategy),
asset: getAddress(event.params.asset),
apiVersion: event.params.apiVersion,
};
context.V3TokenizedStrategyDeployed.set(entity);
},
);

indexer.onEvent({ contract: "YearnV3SplitterFactory", event: "NewSplitter" }, async ({ event, context }) => {
const entity: V3SplitterNewSplitter = {
id: eventId(event),
Expand Down
152 changes: 152 additions & 0 deletions test/tokenized-strategy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { createTestIndexer } from "envio";
import { getAddress } from "viem";
import { describe, expect, it } from "vitest";

const rawStrategy = "0x1234567890abcdef1234567890abcdef12345678";
const rawAsset = "0xabcdef1234567890abcdef1234567890abcdef12";
const strategy = getAddress(rawStrategy);
const asset = getAddress(rawAsset);
const sender = getAddress("0x1111111111111111111111111111111111111111");
const factory = "0xE9E8C89c8Fc7E8b8F23425688eb68987231178e5";
const vault = "0x2222222222222222222222222222222222222222";
const block = {
number: 30_000_000,
timestamp: 1_788_000_000,
hash: `0x${"ab".repeat(32)}`,
};
const transaction = { hash: `0x${"cd".repeat(32)}`, transactionIndex: 3, from: sender };
const provenance = {
blockNumber: block.number,
blockTimestamp: block.timestamp,
blockHash: block.hash,
transactionHash: transaction.hash,
transactionIndex: transaction.transactionIndex,
transactionFrom: sender,
};
const deployment = {
contract: "YearnV3Strategy",
event: "NewTokenizedStrategy",
srcAddress: rawStrategy,
params: { strategy, asset: rawAsset, apiVersion: "3.0.4" },
block,
transaction,
logIndex: 0,
} as const;
const report = {
contract: "YearnV3Strategy",
event: "Reported",
srcAddress: strategy,
params: { profit: 100n, loss: 2n, protocolFees: 3n, performanceFees: 4n },
block,
transaction,
logIndex: 1,
} as const;
const shutdown = {
contract: "YearnV3Strategy",
event: "StrategyShutdown",
srcAddress: strategy,
block: { ...block, number: block.number + 1 },
transaction,
logIndex: 2,
} as const;

describe("tokenized strategy lifecycle routing", () => {
it.each(createTestIndexer().chainIds)(
"discovers a standalone strategy and tracks its lifecycle on chain %i",
async (chainId) => {
const indexer = createTestIndexer();
await indexer.process({
chains: { [chainId]: { simulate: [deployment, report, shutdown] } },
});

expect(await indexer.V3TokenizedStrategyDeployed.getAll()).toEqual([{
...provenance,
id: `${chainId}_${block.number}_0`,
chainId,
logIndex: 0,
strategyAddress: strategy,
strategy,
asset,
apiVersion: "3.0.4",
}]);
expect(await indexer.V3StrategyReported.getAll()).toEqual([{
...provenance,
id: `${chainId}_${block.number}_1`,
chainId,
logIndex: 1,
strategyAddress: strategy,
...report.params,
}]);
expect(await indexer.V3StrategyShutdown.getAll()).toEqual([{
...provenance,
id: `${chainId}_${block.number + 1}_2`,
chainId,
blockNumber: block.number + 1,
logIndex: 2,
strategyAddress: strategy,
}]);
},
);

it("preserves factory-to-vault strategy discovery, reporting, and shutdowns", async () => {
const indexer = createTestIndexer();
await indexer.process({
chains: {
1: {
simulate: [
{
contract: "YearnV3VaultFactory",
event: "NewVault",
srcAddress: factory,
params: { vault_address: vault, asset },
block,
logIndex: 0,
},
{
contract: "YearnV3Vault",
event: "StrategyChanged",
srcAddress: vault,
params: { strategy, change_type: 1n },
block,
logIndex: 1,
},
{ ...report, logIndex: 2 },
shutdown,
],
},
},
});

expect(await indexer.V3VaultFactoryNewVault.getAll()).toHaveLength(1);
expect(await indexer.StrategyChanged.getAll()).toHaveLength(1);
expect(await indexer.V3TokenizedStrategyDeployed.getAll()).toEqual([]);
expect(await indexer.V3StrategyReported.getAll()).toHaveLength(1);
expect(await indexer.V3StrategyShutdown.getAll()).toHaveLength(1);
});

it("does not record or register a strategy named by a different emitter", async () => {
const indexer = createTestIndexer();
await expect(indexer.process({
chains: {
1: {
simulate: [
{ ...deployment, srcAddress: sender },
shutdown,
{ ...shutdown, srcAddress: sender, logIndex: 3 },
],
},
},
})).rejects.toThrow("never reached a handler");
expect(await indexer.V3TokenizedStrategyDeployed.getAll()).toEqual([]);
expect(await indexer.V3StrategyShutdown.getAll()).toEqual([]);
});

it.each([report, shutdown])("keeps $event restricted to discovered strategies", async (event) => {
const indexer = createTestIndexer();
await expect(indexer.process({
chains: { 1: { simulate: [event] } },
})).rejects.toThrow("never reached a handler");
expect(await indexer.V3StrategyReported.getAll()).toEqual([]);
expect(await indexer.V3StrategyShutdown.getAll()).toEqual([]);
});
});