From 3737f6d48d30387656dd4bb92040fb25b7a27682 Mon Sep 17 00:00:00 2001 From: "Alexandro T. Netto" Date: Fri, 20 Mar 2026 08:35:18 +0000 Subject: [PATCH 1/4] fix: load root .env for all services and fix OTel ESM/CJS compat Add `dotenv --` to indexer, api, gateway, and gateful scripts (matching dashboard) so the root .env is injected into turbo child processes. Bundle OpenTelemetry dependencies into the observability package to avoid the ESM/CJS resolution conflict in @opentelemetry/resources@1.30.x that broke ponder and other consumers. Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 8 ++++---- packages/observability/package.json | 4 ++-- packages/observability/tsup.config.ts | 28 +++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 packages/observability/tsup.config.ts diff --git a/package.json b/package.json index 6e73f6ae8..c92e108d8 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,10 @@ "description": "Anticapture Monorepo", "scripts": { "dashboard": "dotenv -- turbo run --filter=@anticapture/dashboard", - "indexer": "turbo run --filter=@anticapture/indexer", - "gateway": "turbo run --filter=@anticapture/api-gateway", - "gateful": "turbo run --filter=@anticapture/gateful", - "api": "turbo run --filter=@anticapture/api", + "indexer": "dotenv -- turbo run --filter=@anticapture/indexer", + "gateway": "dotenv -- turbo run --filter=@anticapture/api-gateway", + "gateful": "dotenv -- turbo run --filter=@anticapture/gateful", + "api": "dotenv -- turbo run --filter=@anticapture/api", "client": "turbo run --filter=@anticapture/graphql-client", "offchain-indexer": "turbo run --filter=@anticapture/offchain-indexer", "address": "turbo run --filter=@anticapture/address-enrichment", diff --git a/packages/observability/package.json b/packages/observability/package.json index 546b1fd6e..94cd7fd7b 100644 --- a/packages/observability/package.json +++ b/packages/observability/package.json @@ -13,8 +13,8 @@ "typecheck": "tsc --noEmit", "lint": "eslint src", "lint:fix": "eslint src --fix", - "build": "tsup src/index.ts --format esm,cjs --dts --out-dir dist", - "dev": "tsup src/index.ts --format esm,cjs --dts --out-dir dist --watch" + "build": "tsup", + "dev": "tsup --watch" }, "devDependencies": { "tsup": "^8.5.1", diff --git a/packages/observability/tsup.config.ts b/packages/observability/tsup.config.ts new file mode 100644 index 000000000..c04a779cd --- /dev/null +++ b/packages/observability/tsup.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from "tsup"; + +const shared = { + entry: ["src/index.ts"], + dts: true, + outDir: "dist", + // Bundle all OTel packages to avoid ESM/CJS resolution issues in consumers + // like ponder. Keep @opentelemetry/api external so it remains a singleton. + noExternal: [/^@opentelemetry\/(?!api$)/], + external: ["@opentelemetry/api"], +} as const; + +export default defineConfig([ + { + ...shared, + format: ["esm"], + // The bundled OTel CJS packages use require() internally. Provide a shim + // so the ESM output can resolve those calls at runtime. + banner: { + js: 'import { createRequire } from "module"; const require = createRequire(import.meta.url);', + }, + }, + { + ...shared, + format: ["cjs"], + dts: false, // only emit .d.ts once from the ESM build + }, +]); From b73e17925594f4f17cb287472c507705cc8d7a77 Mon Sep 17 00:00:00 2001 From: "Alexandro T. Netto" Date: Fri, 20 Mar 2026 08:55:22 +0000 Subject: [PATCH 2/4] feat: add dao-integration skill for onboarding new DAOs Step-by-step guide covering all five components (indexer, API, gateway, dashboard, enum sync) with concrete file paths, code patterns, and a verification checklist. Co-Authored-By: Claude Opus 4.6 (1M context) --- .agents/skills/dao-integration/SKILL.md | 259 ++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 .agents/skills/dao-integration/SKILL.md diff --git a/.agents/skills/dao-integration/SKILL.md b/.agents/skills/dao-integration/SKILL.md new file mode 100644 index 000000000..d6819186e --- /dev/null +++ b/.agents/skills/dao-integration/SKILL.md @@ -0,0 +1,259 @@ +--- +name: dao-integration +description: Use when adding a new DAO to the Anticapture platform. Covers all five components — indexer, API, gateway, dashboard, and enum sync — with a step-by-step checklist. +--- + +# DAO Integration Guide + +## Use This Skill When + +- You are adding a new DAO to the platform. +- You need to understand what files to create/modify for a new DAO. +- You are debugging why a DAO is missing from a component. + +## Prerequisites + +Before starting, gather these details about the DAO: + +- **DAO ID**: Short uppercase identifier (e.g. `ENS`, `UNI`, `AAVE`) +- **Chain**: Which EVM chain (mainnet, arbitrum, optimism, etc.) +- **Token contract**: Address, decimals, deploy block, type (ERC20/ERC721) +- **Governor contract**: Address, deploy block, governor type (Compound-style, Azorius, etc.) +- **Timelock contract**: Address (if applicable) +- **Treasury addresses**: DAO multisigs, vesting contracts +- **CEX/DEX/Lending addresses**: Known exchange wallets, LP pools, lending contracts holding the token +- **Governance rules**: Voting delay, period, quorum calculation, cancel function, vote logic + +## Integration Checklist + +The integration touches 5 components. Work through them in order. + +### Step 1: Enum Sync + +Add the DAO ID to the enum in **all three locations** (they must match): + +| File | Package | +| ------------------------------------- | --------- | +| `apps/indexer/src/lib/enums.ts` | Indexer | +| `apps/api/src/lib/enums.ts` | API | +| `apps/dashboard/shared/types/daos.ts` | Dashboard | + +```typescript +export enum DaoIdEnum { + // ... existing entries ... + NEW_DAO = "NEW_DAO", +} +``` + +### Step 2: Indexer + +#### 2a. Constants (`apps/indexer/src/lib/constants.ts`) + +Add entry to `CONTRACT_ADDRESSES[DaoIdEnum.NEW_DAO]`: + +```typescript +[DaoIdEnum.NEW_DAO]: { + blockTime: 12, // seconds per block on the chain + token: { + address: "0x..." as Address, + decimals: 18, + startBlock: 12345678, + }, + governor: { + address: "0x..." as Address, + startBlock: 12345678, + }, +}, +``` + +Add entries to `TreasuryAddresses`, `CEXAddresses`, `DEXAddresses`, `LendingAddresses`, `BurningAddresses`. + +#### 2b. ABIs (`apps/indexer/src/indexer//abi/`) + +Create ABI files for the token and governor contracts. Use viem's built-in ABIs where possible, or extract from block explorer. + +``` +apps/indexer/src/indexer// +├── abi/ +│ └── index.ts # exports TokenAbi, GovernorAbi +├── erc20.ts # token event handlers (Transfer, DelegateChanged, DelegateVotesChanged) +├── governor.ts # governor event handlers (ProposalCreated, VoteCast, etc.) +└── index.ts # re-exports everything +``` + +#### 2c. Event Handlers + +**Token handler** (`erc20.ts`): Follow the pattern in `apps/indexer/src/indexer/ens/erc20.ts`: + +- `setup` event: Insert token record +- `Transfer`: Track balances, CEX/DEX/Lending/Treasury flows +- `DelegateChanged`: Track delegation changes +- `DelegateVotesChanged`: Track voting power changes + +**Governor handler** (`governor.ts`): Follow the pattern in `apps/indexer/src/indexer/ens/governor.ts`: + +- `ProposalCreated`: Insert proposal +- `VoteCast` / `VoteCastWithParams`: Record votes +- `ProposalQueued`, `ProposalExecuted`, `ProposalCanceled`: Update proposal status + +#### 2d. Ponder Config (`apps/indexer/config/.config.ts`) + +Follow the pattern in `apps/indexer/config/ens.config.ts`: + +```typescript +import { createConfig } from "ponder"; +import { CONTRACT_ADDRESSES } from "@/lib/constants"; +import { DaoIdEnum } from "@/lib/enums"; +import { env } from "@/env"; +import { TokenAbi, GovernorAbi } from "@/indexer//abi"; + +const CONTRACTS = CONTRACT_ADDRESSES[DaoIdEnum.NEW_DAO]; + +export default createConfig({ + database: { kind: "postgres", connectionString: env.DATABASE_URL }, + chains: { + ethereum_mainnet: { + id: 1, + rpc: env.RPC_URL, + maxRequestsPerSecond: env.MAX_REQUESTS_PER_SECOND, + pollingInterval: env.POLLING_INTERVAL, + }, + }, + contracts: { + NEW_DAOToken: { + abi: TokenAbi, + chain: "ethereum_mainnet", + address: CONTRACTS.token.address, + startBlock: CONTRACTS.token.startBlock, + }, + NEW_DAOGovernor: { + abi: GovernorAbi, + chain: "ethereum_mainnet", + address: CONTRACTS.governor.address, + startBlock: CONTRACTS.governor.startBlock, + }, + }, +}); +``` + +#### 2e. Wire into Ponder (`apps/indexer/ponder.config.ts`) + +Import the new config and spread its chains/contracts into the merged config. + +#### 2f. Wire into entry point (`apps/indexer/src/index.ts`) + +Add import and switch case: + +```typescript +import { NEW_DAOTokenIndexer, NEW_DAOGovernorIndexer } from "@/indexer/"; + +case DaoIdEnum.NEW_DAO: { + NEW_DAOTokenIndexer(token.address, token.decimals); + NEW_DAOGovernorIndexer(blockTime); + break; +} +``` + +### Step 3: API + +#### 3a. Constants (`apps/api/src/lib/constants.ts`) + +Mirror the same `CONTRACT_ADDRESSES[DaoIdEnum.NEW_DAO]` entry from the indexer. + +#### 3b. Client (`apps/api/src/clients//index.ts`) + +Create a client class extending `GovernorBase` and implementing `DAOClient`: + +```typescript +export class NEW_DAOClient extends GovernorBase implements DAOClient { + // Implement: getDaoId, getQuorum, getTimelockDelay, + // alreadySupportCalldataReview, calculateQuorum +} +``` + +Follow the pattern in `apps/api/src/clients/ens/index.ts`. + +#### 3c. Register client (`apps/api/src/clients/index.ts`) + +Add `export * from "./";` + +### Step 4: Gateway + +The gateway auto-discovers DAOs from `DAO_API_*` environment variables. Add: + +``` +DAO_API_NEW_DAO= +``` + +No code changes needed unless the API exposes new endpoint patterns. + +### Step 5: Dashboard + +#### 5a. DAO Config (`apps/dashboard/shared/dao-config/.ts`) + +Create a `DaoConfiguration` object. Follow `apps/dashboard/shared/dao-config/ens.ts` as a template. Required fields: + +```typescript +export const NEW_DAO: DaoConfiguration = { + name: "New DAO", + decimals: 18, + color: { svgColor: "#...", svgBgColor: "#..." }, + ogIcon: NewDaoOgIcon, + daoOverview: { + token: "ERC20", + chain: { ...mainnet, icon: MainnetIcon }, + contracts: { governor: "0x...", token: "0x...", timelock: "0x..." }, + rules: { + delay: true, + changeVote: false, + timelock: true, + cancelFunction: false, + logic: "For", + quorumCalculation: "...", + }, + }, + // Feature flags + resilienceStages: true, + tokenDistribution: true, + dataTables: true, + governancePage: true, +}; +``` + +#### 5b. Register config (`apps/dashboard/shared/dao-config/index.ts`) + +Import and add to the default export object. + +#### 5c. Icons (optional) + +Add DAO icon component in `apps/dashboard/shared/components/icons/` and OG icon in `apps/dashboard/shared/og/dao-og-icons/`. + +## Verification + +After all changes, run typecheck and lint on each affected package: + +```bash +pnpm indexer typecheck && pnpm indexer lint +pnpm api typecheck && pnpm api lint +pnpm gateway typecheck && pnpm gateway lint +pnpm dashboard typecheck && pnpm dashboard lint +``` + +## Common Patterns & Variations + +| Variation | Example DAO | Key Difference | +| ---------------------------------- | ----------------- | --------------------------------------------------- | +| Standard ERC20 + Compound Governor | ENS, UNI, GTC, OP | Straightforward, follow ENS pattern | +| ERC721 (NFT) token | NOUNS | Token is NFT, auto-delegates on transfer | +| Multi-token tracking | AAVE | Tracks AAVE + stkAAVE + aAAVE separately | +| Azorius governance (Fractal) | SHU | Different governor events, custom proposal handling | +| Multi-chain | ARB, OP, SCR | Config needs chain-specific RPC and chain ID | +| No governor (token-only) | ARB | Only token indexer, no governor handler | + +## Guardrails + +- Enums **must** be identical across indexer, API, and dashboard +- Constants (contract addresses) **must** match between indexer and API +- ABIs **must** match the deployed contracts — verify on block explorer +- Do **not** run the indexer unless explicitly asked (reindexing is expensive) +- Test the API client against the real chain before deploying From cbd44cc2587f0e6991832a474c1d36d9d639d93f Mon Sep 17 00:00:00 2001 From: "Alexandro T. Netto" Date: Fri, 20 Mar 2026 11:48:56 +0000 Subject: [PATCH 3/4] feat: add governance architecture discovery step to dao-integration skill Add mandatory Step 0 that verifies the governor's voting token on-chain before writing code. This catches non-standard governance patterns like vote-escrow (veToken) DAOs where the ERC20 token differs from the voting token. Also adds an INTEGRATION.md template for per-DAO status tracking and a new veToken variation row. Co-Authored-By: Claude Opus 4.6 (1M context) --- .agents/skills/dao-integration/SKILL.md | 86 +++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/.agents/skills/dao-integration/SKILL.md b/.agents/skills/dao-integration/SKILL.md index d6819186e..c22ebcda7 100644 --- a/.agents/skills/dao-integration/SKILL.md +++ b/.agents/skills/dao-integration/SKILL.md @@ -28,6 +28,54 @@ Before starting, gather these details about the DAO: The integration touches 5 components. Work through them in order. +### Step 0: Governance Architecture Discovery + +**This step is mandatory before writing any code.** Many DAOs have non-standard governance architectures. Skipping this step risks building an integration that misses the core governance mechanism. + +#### 0a. Verify the governor's voting token + +Call `governor.token()` on-chain to find what contract the governor actually uses for voting power: + +```bash +cast call "token()(address)" --rpc-url +``` + +Compare the result against the token address the user provided. They may differ — e.g. the governor may point to a vote-escrow wrapper, not the ERC20. + +#### 0b. Classify the voting token + +Check what the voting token actually is: + +| Check | Command | What it tells you | +|---|---|---| +| Is it the same as the ERC20? | Compare addresses | If different, there's an intermediary | +| Does it have `delegates()`? | `cast call "delegates(address)(address)" ` | If reverts → no delegation, voting power comes from elsewhere | +| Does it emit `DelegateChanged`? | Check ABI on block explorer | If missing → `delegatedSupply` and `accountPower` will be empty | +| Is it a vote-escrow (veToken)? | Check contract name/source on block explorer | veTokens use lock-based voting power with `Deposit`/`Withdraw` events | +| Is it a wrapper? | Check if it references another contract | Wrappers (like wveOLAS) proxy reads to an underlying contract | + +#### 0c. Determine integration scope + +Based on the findings, classify the integration: + +| Architecture | Token events available | Delegation tracking | Example | +|---|---|---|---| +| **Standard ERC20Votes** | Transfer, DelegateChanged, DelegateVotesChanged | Full | ENS, UNI, OBOL | +| **Plain ERC20 + veToken** | Transfer only (on ERC20); Deposit/Withdraw (on veToken) | Requires custom veToken indexing | OLAS | +| **ERC721 (NFT)** | Transfer (minting = delegation) | Via transfer events | NOUNS | +| **Multi-token** | Transfer + delegation per token | Aggregated across tokens | AAVE | + +#### 0d. Document findings in INTEGRATION.md + +Create `apps/indexer/src/indexer//INTEGRATION.md` documenting: + +1. **Architecture**: What contracts exist and how they connect +2. **What's integrated**: Which events and metrics are covered +3. **What's pending**: Gaps that need follow-up work (e.g. veToken indexing) +4. **Addresses provided vs discovered**: Any discrepancies from user-provided info + +This file is the source of truth for the integration status of each DAO. See the template below in "INTEGRATION.md Template". + ### Step 1: Enum Sync Add the DAO ID to the enum in **all three locations** (they must match): @@ -249,6 +297,43 @@ pnpm dashboard typecheck && pnpm dashboard lint | Azorius governance (Fractal) | SHU | Different governor events, custom proposal handling | | Multi-chain | ARB, OP, SCR | Config needs chain-specific RPC and chain ID | | No governor (token-only) | ARB | Only token indexer, no governor handler | +| Vote-escrow (veToken) governance | OLAS | ERC20 has no delegation; voting power from veToken lock. Requires custom veToken indexer for `Deposit`/`Withdraw` events to track `delegatedSupply` and `accountPower`. Governor events are standard. | + +## INTEGRATION.md Template + +Every DAO integration **must** include an `INTEGRATION.md` file at `apps/indexer/src/indexer//INTEGRATION.md`. This is the source of truth for what's integrated and what's pending. + +```markdown +# Integration Status + +## Architecture + +| Contract | Address | Type | Events used | +|---|---|---|---| +| Token | 0x... | ERC20 / ERC721 / veToken | Transfer, DelegateChanged, ... | +| Governor | 0x... | OZ Governor / Azorius / ... | ProposalCreated, VoteCast, ... | +| Timelock | 0x... | TimelockController | (not indexed) | + +Governor voting token: `
` (same as token / veToken wrapper / other) + +## What's Integrated + +- [ ] Token supply tracking (Transfer events) +- [ ] Delegation tracking (DelegateChanged / DelegateVotesChanged) +- [ ] Voting power tracking (accountPower, votingPowerHistory) +- [ ] Governor proposals (ProposalCreated, status updates) +- [ ] Governor votes (VoteCast) +- [ ] CEX/DEX/Lending address classification +- [ ] Treasury tracking + +## What's Pending + +List gaps with context on why and what's needed to close them. + +## Notes + +Any DAO-specific quirks, discrepancies, or decisions made during integration. +``` ## Guardrails @@ -257,3 +342,4 @@ pnpm dashboard typecheck && pnpm dashboard lint - ABIs **must** match the deployed contracts — verify on block explorer - Do **not** run the indexer unless explicitly asked (reindexing is expensive) - Test the API client against the real chain before deploying +- **Always** run Step 0 (Governance Architecture Discovery) before writing code — never assume the token the user provides is the voting token From 006aa5277fcc79d163cfa764dd87aaaeba5e62e2 Mon Sep 17 00:00:00 2001 From: "Alexandro T. Netto" Date: Fri, 20 Mar 2026 12:04:11 +0000 Subject: [PATCH 4/4] feat(indexer): add Olas DAO integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Index the OLAS ERC20 token (0x0001A500...E45CB0) for supply metrics and Governor OLAS (0x8E84B505...3b401) for proposals and votes. OLAS uses a vote-escrow (veOLAS) governance model — voting power comes from locking tokens, not delegation. The ERC20 token has no DelegateChanged/DelegateVotesChanged events. veOLAS indexing for per-account voting power tracking is documented as pending work in INTEGRATION.md. Verified against local RPC: 350k transfers, 50 proposals, 188 votes, 42k accounts indexed successfully. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/indexer/config/olas.config.ts | 37 ++++ apps/indexer/ponder.config.ts | 3 + apps/indexer/src/index.ts | 6 + apps/indexer/src/indexer/olas/INTEGRATION.md | 56 ++++++ apps/indexer/src/indexer/olas/abi/governor.ts | 179 ++++++++++++++++++ apps/indexer/src/indexer/olas/abi/index.ts | 2 + apps/indexer/src/indexer/olas/abi/token.ts | 108 +++++++++++ apps/indexer/src/indexer/olas/erc20.ts | 151 +++++++++++++++ apps/indexer/src/indexer/olas/governor.ts | 68 +++++++ apps/indexer/src/indexer/olas/index.ts | 4 + apps/indexer/src/lib/constants.ts | 25 +++ apps/indexer/src/lib/enums.ts | 1 + 12 files changed, 640 insertions(+) create mode 100644 apps/indexer/config/olas.config.ts create mode 100644 apps/indexer/src/indexer/olas/INTEGRATION.md create mode 100644 apps/indexer/src/indexer/olas/abi/governor.ts create mode 100644 apps/indexer/src/indexer/olas/abi/index.ts create mode 100644 apps/indexer/src/indexer/olas/abi/token.ts create mode 100644 apps/indexer/src/indexer/olas/erc20.ts create mode 100644 apps/indexer/src/indexer/olas/governor.ts create mode 100644 apps/indexer/src/indexer/olas/index.ts diff --git a/apps/indexer/config/olas.config.ts b/apps/indexer/config/olas.config.ts new file mode 100644 index 000000000..2a944bd7d --- /dev/null +++ b/apps/indexer/config/olas.config.ts @@ -0,0 +1,37 @@ +import { createConfig } from "ponder"; +import { CONTRACT_ADDRESSES } from "@/lib/constants"; +import { DaoIdEnum } from "@/lib/enums"; + +import { env } from "@/env"; +import { OlasGovernorAbi, OlasTokenAbi } from "@/indexer/olas/abi"; + +const OLAS_CONTRACTS = CONTRACT_ADDRESSES[DaoIdEnum.OLAS]; + +export default createConfig({ + database: { + kind: "postgres", + connectionString: env.DATABASE_URL, + }, + chains: { + ethereum_mainnet: { + id: 1, + rpc: env.RPC_URL, + maxRequestsPerSecond: env.MAX_REQUESTS_PER_SECOND, + pollingInterval: env.POLLING_INTERVAL, + }, + }, + contracts: { + OlasToken: { + abi: OlasTokenAbi, + chain: "ethereum_mainnet", + address: OLAS_CONTRACTS.token.address, + startBlock: OLAS_CONTRACTS.token.startBlock, + }, + OlasGovernor: { + abi: OlasGovernorAbi, + chain: "ethereum_mainnet", + address: OLAS_CONTRACTS.governor.address, + startBlock: OLAS_CONTRACTS.governor.startBlock, + }, + }, +}); diff --git a/apps/indexer/ponder.config.ts b/apps/indexer/ponder.config.ts index 0317f2c17..cfb7c7b25 100644 --- a/apps/indexer/ponder.config.ts +++ b/apps/indexer/ponder.config.ts @@ -5,6 +5,7 @@ import ensConfig from "./config/ens.config"; import gitcoinConfig from "./config/gitcoin.config"; import nounsConfig from "./config/nouns.config"; import obolConfig from "./config/obol.config"; +import olasConfig from "./config/olas.config"; import optimismConfig from "./config/optimism.config"; import scrollConfig from "./config/scroll.config"; import shutterConfig from "./config/shutter.config"; @@ -23,6 +24,7 @@ export default { ...scrollConfig.chains, ...compoundConfig.chains, ...obolConfig.chains, + ...olasConfig.chains, ...zkConfig.chains, ...shutterConfig.chains, }, @@ -37,6 +39,7 @@ export default { ...scrollConfig.contracts, ...compoundConfig.contracts, ...obolConfig.contracts, + ...olasConfig.contracts, ...zkConfig.contracts, ...shutterConfig.contracts, }, diff --git a/apps/indexer/src/index.ts b/apps/indexer/src/index.ts index b38c1676b..de9aec67d 100644 --- a/apps/indexer/src/index.ts +++ b/apps/indexer/src/index.ts @@ -39,6 +39,7 @@ import { ZKTokenIndexer, GovernorIndexer as ZKGovernorIndexer, } from "./indexer/zk"; +import { OlasTokenIndexer, OlasGovernorIndexer } from "./indexer/olas"; const { DAO_ID: daoId } = env; @@ -105,6 +106,11 @@ switch (daoId) { SHUGovernorIndexer(blockTime); break; } + case DaoIdEnum.OLAS: { + OlasTokenIndexer(token.address, token.decimals); + OlasGovernorIndexer(blockTime); + break; + } case DaoIdEnum.AAVE: { const { aave, stkAAVE, aAAVE } = CONTRACT_ADDRESSES[DaoIdEnum.AAVE]; AAVETokenIndexer(aave.address, aave.decimals); diff --git a/apps/indexer/src/indexer/olas/INTEGRATION.md b/apps/indexer/src/indexer/olas/INTEGRATION.md new file mode 100644 index 000000000..f8f0b1e9e --- /dev/null +++ b/apps/indexer/src/indexer/olas/INTEGRATION.md @@ -0,0 +1,56 @@ +# Olas (OLAS) Integration Status + +## Architecture + +| Contract | Address | Type | Events used | +| ------------- | -------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------- | +| OLAS Token | `0x0001A500A6B18995B03f44bb040A5fFc28E45CB0` | Plain ERC20 (no delegation) | Transfer | +| veOLAS | `0x7e01A500805f8A52Fad229b3015AD130A332B7b3` | Vote-escrow (Curve-style) | **Not indexed** — emits Deposit, Withdraw, Supply | +| wveOLAS | `0x4039B809E0C0Ad04F6Fc880193366b251dDf4B40` | Read-only wrapper for veOLAS | Not indexed | +| Governor OLAS | `0x8E84B5055492901988B831817e4Ace5275A3b401` | OZ Governor v4.8.3 + GovernorCompatibilityBravo | ProposalCreated, VoteCast, ProposalCanceled, ProposalExecuted, ProposalQueued | +| Timelock | `0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE` | TimelockController | Not indexed (registered as treasury) | + +Governor voting token: `0x4039B809E0C0Ad04F6Fc880193366b251dDf4B40` (wveOLAS — a wrapper around veOLAS, **not** the ERC20 token). + +Governance power is earned by locking OLAS tokens in veOLAS for up to 4 years. Voting power decays linearly over the lock period. There is no delegation — each user's voting power is determined solely by their lock amount and remaining lock time. + +## What's Integrated + +- [x] Token supply tracking (Transfer events on OLAS ERC20) +- [ ] Delegation tracking — **N/A**: OLAS ERC20 has no DelegateChanged/DelegateVotesChanged +- [ ] Voting power tracking (accountPower, votingPowerHistory) — requires veOLAS indexing +- [x] Governor proposals (ProposalCreated, status updates) +- [x] Governor votes (VoteCast) +- [ ] CEX/DEX/Lending address classification — no addresses provided yet +- [x] Treasury tracking (timelock address registered) + +## What's Pending + +### 1. veOLAS indexing (high priority) + +The core governance mechanism — who has voting power and how much — is not tracked. veOLAS emits: + +- `Deposit(address provider, uint256 amount, uint256 locktime, uint256 depositType, uint256 ts)` — user locks OLAS +- `Withdraw(address provider, uint256 amount, uint256 ts)` — user unlocks after expiry +- `Supply(uint256 prevSupply, uint256 supply)` — total locked supply changes + +To integrate: + +- Add veOLAS ABI and contract to `olas.config.ts` +- Write custom event handlers mapping Deposit/Withdraw to `accountPower` and `votingPowerHistory` +- Map total locked supply (from `Supply` events) to `delegatedSupply` on the token record +- This is a new pattern not used by any other DAO — requires custom handler logic + +### 2. CEX/DEX/Lending addresses + +User stated no CEX or DEX addresses available yet. When provided, add to `CEXAddresses[DaoIdEnum.OLAS]`, `DEXAddresses[DaoIdEnum.OLAS]`, and `LendingAddresses[DaoIdEnum.OLAS]` in constants.ts. + +### 3. API, Gateway, Dashboard integration + +Only the indexer component is complete. Steps 3–5 from the dao-integration skill remain. + +## Notes + +- The user provided `0x7e01A500805f8A52Fad229b3015AD130A332B7b3` (veOLAS) as the "token" address. veOLAS is non-transferable and doesn't emit Transfer events. The actual tradeable OLAS ERC20 is at `0x0001A500A6B18995B03f44bb040A5fFc28E45CB0` — this is what we index for supply metrics. +- Governor uses OZ v4.8.3 naming convention: ProposalCreated uses `startBlock`/`endBlock` (not `voteStart`/`voteEnd` like OZ v5). +- `delegatedSupply` will remain 0 until veOLAS indexing is implemented. This is expected — it does not indicate a bug. diff --git a/apps/indexer/src/indexer/olas/abi/governor.ts b/apps/indexer/src/indexer/olas/abi/governor.ts new file mode 100644 index 000000000..167d189b6 --- /dev/null +++ b/apps/indexer/src/indexer/olas/abi/governor.ts @@ -0,0 +1,179 @@ +export const GovernorAbi = [ + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + ], + name: "ProposalCanceled", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "proposer", + type: "address", + }, + { + indexed: false, + internalType: "address[]", + name: "targets", + type: "address[]", + }, + { + indexed: false, + internalType: "uint256[]", + name: "values", + type: "uint256[]", + }, + { + indexed: false, + internalType: "string[]", + name: "signatures", + type: "string[]", + }, + { + indexed: false, + internalType: "bytes[]", + name: "calldatas", + type: "bytes[]", + }, + { + indexed: false, + internalType: "uint256", + name: "startBlock", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "endBlock", + type: "uint256", + }, + { + indexed: false, + internalType: "string", + name: "description", + type: "string", + }, + ], + name: "ProposalCreated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + ], + name: "ProposalExecuted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "eta", + type: "uint256", + }, + ], + name: "ProposalQueued", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "voter", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + { + indexed: false, + internalType: "uint8", + name: "support", + type: "uint8", + }, + { + indexed: false, + internalType: "uint256", + name: "weight", + type: "uint256", + }, + { + indexed: false, + internalType: "string", + name: "reason", + type: "string", + }, + ], + name: "VoteCast", + type: "event", + }, + { + inputs: [], + name: "name", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "proposalId", type: "uint256" }], + name: "state", + outputs: [ + { + internalType: "enum IGovernor.ProposalState", + name: "", + type: "uint8", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "votingDelay", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "votingPeriod", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, +] as const; diff --git a/apps/indexer/src/indexer/olas/abi/index.ts b/apps/indexer/src/indexer/olas/abi/index.ts new file mode 100644 index 000000000..884efae34 --- /dev/null +++ b/apps/indexer/src/indexer/olas/abi/index.ts @@ -0,0 +1,2 @@ +export { GovernorAbi as OlasGovernorAbi } from "./governor"; +export { TokenAbi as OlasTokenAbi } from "./token"; diff --git a/apps/indexer/src/indexer/olas/abi/token.ts b/apps/indexer/src/indexer/olas/abi/token.ts new file mode 100644 index 000000000..2684ec58c --- /dev/null +++ b/apps/indexer/src/indexer/olas/abi/token.ts @@ -0,0 +1,108 @@ +export const TokenAbi = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "spender", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "from", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "to", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "Transfer", + type: "event", + }, + { + inputs: [{ internalType: "address", name: "account", type: "address" }], + name: "balanceOf", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "decimals", + outputs: [{ internalType: "uint8", name: "", type: "uint8" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "name", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "symbol", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "totalSupply", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "transfer", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "transferFrom", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/apps/indexer/src/indexer/olas/erc20.ts b/apps/indexer/src/indexer/olas/erc20.ts new file mode 100644 index 000000000..962ace8e4 --- /dev/null +++ b/apps/indexer/src/indexer/olas/erc20.ts @@ -0,0 +1,151 @@ +import { ponder } from "ponder:registry"; +import { token } from "ponder:schema"; +import { Address } from "viem"; + +import { tokenTransfer } from "@/eventHandlers"; +import { + updateCirculatingSupply, + updateSupplyMetric, + updateTotalSupply, +} from "@/eventHandlers/metrics"; +import { handleTransaction } from "@/eventHandlers/shared"; +import { + MetricTypesEnum, + BurningAddresses, + CEXAddresses, + DEXAddresses, + LendingAddresses, + TreasuryAddresses, +} from "@/lib/constants"; +import { DaoIdEnum } from "@/lib/enums"; + +export function OlasTokenIndexer( + address: Address, + decimals: number, + daoId: DaoIdEnum = DaoIdEnum.OLAS, +) { + ponder.on("OlasToken:setup", async ({ context }) => { + await context.db.insert(token).values({ + id: address, + name: daoId, + decimals, + }); + }); + + ponder.on("OlasToken:Transfer", async ({ event, context }) => { + const { from, to, value } = event.args; + const { timestamp } = event.block; + + const cexAddressList = Object.values(CEXAddresses[daoId]); + const dexAddressList = Object.values(DEXAddresses[daoId]); + const lendingAddressList = Object.values(LendingAddresses[daoId]); + const burningAddressList = Object.values(BurningAddresses[daoId]); + const treasuryAddressList = Object.values(TreasuryAddresses[daoId]); + + await tokenTransfer( + context, + daoId, + { + from, + to, + value, + token: address, + transactionHash: event.transaction.hash, + timestamp: event.block.timestamp, + logIndex: event.log.logIndex, + }, + { + cex: cexAddressList, + dex: dexAddressList, + lending: lendingAddressList, + burning: burningAddressList, + }, + ); + + await updateSupplyMetric( + context, + "lendingSupply", + lendingAddressList, + MetricTypesEnum.LENDING_SUPPLY, + from, + to, + value, + daoId, + address, + timestamp, + ); + + await updateSupplyMetric( + context, + "cexSupply", + cexAddressList, + MetricTypesEnum.CEX_SUPPLY, + from, + to, + value, + daoId, + address, + timestamp, + ); + + await updateSupplyMetric( + context, + "dexSupply", + dexAddressList, + MetricTypesEnum.DEX_SUPPLY, + from, + to, + value, + daoId, + address, + timestamp, + ); + + await updateSupplyMetric( + context, + "treasury", + treasuryAddressList, + MetricTypesEnum.TREASURY, + from, + to, + value, + daoId, + address, + timestamp, + ); + + await updateTotalSupply( + context, + burningAddressList, + MetricTypesEnum.TOTAL_SUPPLY, + from, + to, + value, + daoId, + address, + timestamp, + ); + + await updateCirculatingSupply(context, daoId, address, timestamp); + + if (!event.transaction.to) return; + + await handleTransaction( + context, + event.transaction.hash, + event.transaction.from, + event.transaction.to, + event.block.timestamp, + [event.args.from, event.args.to], + { + cex: cexAddressList, + dex: dexAddressList, + lending: lendingAddressList, + burning: burningAddressList, + }, + ); + }); + + // NOTE: OLAS token does not have DelegateChanged or DelegateVotesChanged events. + // Governance power comes from veOLAS (voting escrow), not token delegation. +} diff --git a/apps/indexer/src/indexer/olas/governor.ts b/apps/indexer/src/indexer/olas/governor.ts new file mode 100644 index 000000000..993fdc766 --- /dev/null +++ b/apps/indexer/src/indexer/olas/governor.ts @@ -0,0 +1,68 @@ +import { ponder } from "ponder:registry"; + +import { + updateProposalStatus, + proposalCreated, + voteCast, +} from "@/eventHandlers"; +import { DaoIdEnum } from "@/lib/enums"; +import { ProposalStatus } from "@/lib/constants"; + +export function OlasGovernorIndexer(blockTime: number) { + const daoId = DaoIdEnum.OLAS; + + ponder.on(`OlasGovernor:VoteCast`, async ({ event, context }) => { + await voteCast(context, daoId, { + proposalId: event.args.proposalId.toString(), + voter: event.args.voter, + reason: event.args.reason, + support: event.args.support, + timestamp: event.block.timestamp, + txHash: event.transaction.hash, + votingPower: event.args.weight, + logIndex: event.log.logIndex, + }); + }); + + ponder.on(`OlasGovernor:ProposalCreated`, async ({ event, context }) => { + await proposalCreated(context, daoId, blockTime, { + proposalId: event.args.proposalId.toString(), + txHash: event.transaction.hash, + proposer: event.args.proposer, + targets: [...event.args.targets], + values: [...event.args.values], + signatures: [...event.args.signatures], + calldatas: [...event.args.calldatas], + startBlock: event.args.startBlock.toString(), + endBlock: event.args.endBlock.toString(), + description: event.args.description, + timestamp: event.block.timestamp, + blockNumber: event.block.number, + logIndex: event.log.logIndex, + }); + }); + + ponder.on(`OlasGovernor:ProposalCanceled`, async ({ event, context }) => { + await updateProposalStatus( + context, + event.args.proposalId.toString(), + ProposalStatus.CANCELED, + ); + }); + + ponder.on(`OlasGovernor:ProposalExecuted`, async ({ event, context }) => { + await updateProposalStatus( + context, + event.args.proposalId.toString(), + ProposalStatus.EXECUTED, + ); + }); + + ponder.on(`OlasGovernor:ProposalQueued`, async ({ event, context }) => { + await updateProposalStatus( + context, + event.args.proposalId.toString(), + ProposalStatus.QUEUED, + ); + }); +} diff --git a/apps/indexer/src/indexer/olas/index.ts b/apps/indexer/src/indexer/olas/index.ts new file mode 100644 index 000000000..324693e85 --- /dev/null +++ b/apps/indexer/src/indexer/olas/index.ts @@ -0,0 +1,4 @@ +export * from "./abi"; + +export * from "./governor"; +export * from "./erc20"; diff --git a/apps/indexer/src/lib/constants.ts b/apps/indexer/src/lib/constants.ts index 9471b4b0e..7e2191d5f 100644 --- a/apps/indexer/src/lib/constants.ts +++ b/apps/indexer/src/lib/constants.ts @@ -183,6 +183,20 @@ export const CONTRACT_ADDRESSES = { startBlock: 19021698, }, }, + [DaoIdEnum.OLAS]: { + blockTime: 12, + // https://etherscan.io/address/0x0001A500A6B18995B03f44bb040A5fFc28E45CB0 + token: { + address: "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0", + decimals: 18, + startBlock: 15049891, + }, + // https://etherscan.io/address/0x8E84B5055492901988B831817e4Ace5275A3b401 + governor: { + address: "0x8E84B5055492901988B831817e4Ace5275A3b401", + startBlock: 17527057, + }, + }, [DaoIdEnum.AAVE]: { blockTime: 1, token: { @@ -349,6 +363,9 @@ export const TreasuryAddresses: Record> = { [DaoIdEnum.SHU]: { timelock: "0x36bD3044ab68f600f6d3e081056F34f2a58432c4", }, + [DaoIdEnum.OLAS]: { + timelock: "0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE", + }, }; export const CEXAddresses: Record> = { @@ -562,6 +579,7 @@ export const CEXAddresses: Record> = { OKX3: "0xecf17c7f6a6090f1edd21e0beb2268197270fb44", }, [DaoIdEnum.SHU]: {}, + [DaoIdEnum.OLAS]: {}, }; export const DEXAddresses: Record> = { @@ -631,6 +649,7 @@ export const DEXAddresses: Record> = { [DaoIdEnum.SHU]: { "Uniswap V3": "0x7A922aea89288d8c91777BeECc68DF4A17151df1", }, + [DaoIdEnum.OLAS]: {}, }; export const LendingAddresses: Record> = { @@ -679,6 +698,7 @@ export const LendingAddresses: Record> = { Venus: "0x697a70779c1a03ba2bd28b7627a902bff831b616", }, [DaoIdEnum.SHU]: {}, + [DaoIdEnum.OLAS]: {}, }; export const BurningAddresses: Record< @@ -756,6 +776,11 @@ export const BurningAddresses: Record< Dead: "0x000000000000000000000000000000000000dEaD", TokenContract: "0xe485E2f1bab389C08721B291f6b59780feC83Fd7", }, + [DaoIdEnum.OLAS]: { + ZeroAddress: zeroAddress, + Dead: "0x000000000000000000000000000000000000dEaD", + TokenContract: "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0", + }, }; export enum ProposalStatus { diff --git a/apps/indexer/src/lib/enums.ts b/apps/indexer/src/lib/enums.ts index b29e2d240..60276d45a 100644 --- a/apps/indexer/src/lib/enums.ts +++ b/apps/indexer/src/lib/enums.ts @@ -12,6 +12,7 @@ export enum DaoIdEnum { OBOL = "OBOL", ZK = "ZK", SHU = "SHU", + OLAS = "OLAS", } export const SECONDS_IN_DAY = 24 * 60 * 60;