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
7 changes: 7 additions & 0 deletions .claude/skills/onchain-verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,10 @@ Or leave the script in `scripts/` if it's useful as a recurring diagnostic — b
- Tracked event signatures live in `subgraph.yaml.mustache`
- If a chain contract emits a topic0 that matches neither known deployer nor known event sig, that's a clue you have an **untracked deployer** — flag it for the user
- Graft block lives in `networks.json` under `<network>.grafting.block`; use it as the BEFORE/after divider

## BSC specifics (learned 2026-08-20)

- `bsc-rpc.publicnode.com` refuses `eth_getLogs` / historical `eth_getCode` without a token ("Archive requests require a personal token"); `bsc-mainnet.public.blastapi.io` serves archive but rate-limits fast. The Alchemy BNB endpoint in `~/projects/reserve-api/.env` (`RPC_URL_BSC`) handles both, including address-filtered `eth_getLogs` over the full range in one call.
- `cast` beats a viem script for quick checks: `cast call`, `cast logs --address X --from-block N --to-block latest --json`, `cast receipt TX --json`, `cast block N --field timestamp`. `cast logs` wants `--json`, not `-j`.
- BSC block time is ~0.45 s (measured over 20k blocks); mainnet ~12 s, base 2 s. Binary-searching a deploy block needs an archive RPC or the "deployed at" result is garbage.
- The `ReserveOptimisticGovernorDeployer` topic0 `0x92f46afc…` = `ReserveOptimisticGovernorSystemDeployed(address,address,address,address)` (reserve-governor repo), now tracked via `networks.json` → `OptimisticGovernorDeployer`.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ All notable changes to the DTF Index Subgraph will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.10.0] - 2026-08-20

Requires a clean reindex (no graft): the UnstakingManager subscription and the exchange-rate / position entities are created at staking-token discovery and from historical Deposit/Withdraw events.

### Fixed
- **Vaults discovered outside a tracked deployer event never had their `UnstakingManager` subscribed, so their locks were never indexed.** Concretely the shared BSC vlRSR `0xE744C8157c346B2931807F42552c8CBc0BB6D34f` (deployed by the untracked `ReserveOptimisticGovernorDeployer` at block 102317233) had `unstakingManager: null` and `locks: []` on Goldsky while two `LockCreated` events (blocks 116930184 / 116931302) sat on chain. The subscription now lives in `getOrCreateStakingToken` (`subscribeUnstakingManager`), i.e. on every discovery path — deployer events, `Governor.token()`, timelock role grants. Verified by replaying the two real events in `tests/unstaking-manager-fallback.test.ts`.
- `Lock.account` / `RewardClaim.account` referenced an `Account` that was never saved when the address had not held the vault token before (e.g. a withdraw to a different receiver).

### Added
- **`OptimisticGovernorDeployer` data source** (networks.json; mainnet `0x2acc45e1…` from block 25245938, base `0x604d70d1…` from 46904843, bsc `0x1c10e68b…` from 102317103 — the mainnet/base vlRSR vaults `0xABbDD9AC…` / `0x2F0D6538…` had the same missing-manager bug) handling `ReserveOptimisticGovernorSystemDeployed`. `deployWithNewStakingVault` indexes the vault, its manager, governance and timelock (the AccessControl vault emits no `OwnershipTransferred`, so the timelock entity is created here); `deployWithExistingStakingVault` (`stakingVault = 0x0`) only discovers the vault through `Governor.token()`; the DTF governance keeps being created by the DTF role grant, where its timelock and `optimisticProposers` are known.
- **Vault exchange-rate tracking.** The new StakingVault streams native RSR rewards into `totalAssets()`, so the share → asset rate drifts every block. A hardcoded `TrackedVault` data source per chain (networks.json: the vlRSR vault — mainnet `0xABbDD9AC…`, base `0x2F0D6538…`, bsc `0xE744C815…`) runs a polling block handler (`handleExchangeRatePoll`) that samples `convertToAssets(10^decimals)` + `totalAssets()` roughly every 10 minutes — `every` is derived per network from `networks.json.secondsPerBlock` (mainnet 12s → 50 blocks, base 2s → 300, bsc 0.45s → 1333). New fields `StakingToken.exchangeRate / totalAssets / exchangeRateBlock / exchangeRateTimestamp`, `StakingTokenExchangeRateDailySnapshot` (last sample per day, upsert) and `StakingTokenExchangeRateSnapshot` (immutable, written on the first poll and whenever the rate moves — legacy 1:1 vaults get a single baseline row). Only the tracked vault is polled (two eth_calls per poll); the cadence is one constant in `scripts/parse-template.js`, adding a vault is one `TrackedVault` entry.
- **Per-account staking accounting** for the "locked 100 RSR → 96 vlRSR, now worth 120 RSR" view: `StakingPosition` (shares, average-cost `principal`, `totalDeposited`, `totalWithdrawn`, `realizedRewards`), `StakingPositionRecord` per `Deposit` / `Withdraw` / wallet `Transfer` (with the implied exchange rate), `StakingPositionDailySnapshot`. Withdrawals release principal pro rata and book the excess as realized rewards; wallet transfers move principal pro rata; a cancelled lock re-enters as a `LOCK_CANCEL` record (principal at today's rate, lifetime totals untouched). Validated against the full real history of the BSC locker `0xb0bd…6e18` with an independent BigInt oracle (`tests/staking-position.test.ts`). See `docs/staking-vaults.md` for the query recipe.
- `StakingToken.unstakingManager`, `unstakingDelay`, `rewardRatio`, `rewardHalfLife` (from `UnstakingDelaySet` / `RewardRatioSet` and resolved at discovery).

### Changed
- `abis/staking-vault.json` gains `versionRegistry()`, `rewardTokenRegistry()`, `VersionRegistrySet`, `RewardTokenRegistrySet`; new `abis/optimistic-governor-deployer.json`.
- `scripts/parse-template.js` requires `secondsPerBlock` per network and renders `OptimisticGovernorDeployer` and `TrackedVault` data sources.
- Test helpers: `tests/helpers/mock-staking-vault.ts` mocks every vault / governor / timelock read the discovery path performs.

## [1.9.8] - 2026-06-23

### Fixed
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@ The DTF Index Subgraph tracks and indexes:
This subgraph uses a Mustache-based template system to generate network-specific configurations:

1. **Template File**: `subgraph.yaml.mustache` contains the base configuration with placeholders
2. **Network Configuration**: `networks.json` stores deployment addresses and block numbers for each network
2. **Network Configuration**: `networks.json` stores deployment addresses and block numbers for each network, plus `secondsPerBlock` (drives the exchange-rate polling cadence, see below)
3. **Parser Script**: `scripts/parse-template.js` generates the final `subgraph.yaml`

### How It Works

The template parsing process:
1. Takes a network argument (e.g., "mainnet", "base", "bsc")
2. Loads network-specific addresses and block numbers from `networks.json`
3. Handles multiple protocol versions (1.0.0, 2.0.0, 4.0.0) with different deployer contracts
3. Handles multiple protocol versions (1.0.0, 2.0.0, 4.0.0, 5.0.0) with different deployer contracts, plus the standalone `ReserveOptimisticGovernorDeployer` (`OptimisticGovernorDeployer` in `networks.json`)
4. Converts the 10-minute exchange-rate polling cadence into a per-network block count (`every: round(600 / secondsPerBlock)`)
4. Generates `subgraph.yaml` with all network-specific values filled in

### Building for Different Networks
Expand Down Expand Up @@ -123,6 +124,7 @@ The deployment process automatically:
- **Token**: ERC20 tokens with transfer history and holder analytics
- **Governance**: Proposals, votes, delegates, and timelock operations
- **VoteLockedToken**: stDAO tokens with delegation and vote-locking mechanisms
- **StakingPosition / exchange rate**: per-account principal vs. current value for appreciating vaults — see `docs/staking-vaults.md`

## Version Support

Expand Down
137 changes: 137 additions & 0 deletions abis/optimistic-governor-deployer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
[
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "stakingVault",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "governor",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "timelock",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "optimisticSelectorRegistry",
"type": "address"
}
],
"name": "ReserveOptimisticGovernorSystemDeployed",
"type": "event"
},
{
"inputs": [],
"name": "versionRegistry",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "rewardTokenRegistry",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "guardian",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "stakingVaultImpl",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "governorImpl",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "timelockImpl",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "selectorRegistryImpl",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "version",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "pure",
"type": "function"
}
]
82 changes: 77 additions & 5 deletions abis/staking-vault.json
Original file line number Diff line number Diff line change
Expand Up @@ -1964,22 +1964,94 @@
"name": "getRoleMember",
"stateMutability": "view",
"inputs": [
{ "name": "role", "type": "bytes32", "internalType": "bytes32" },
{ "name": "index", "type": "uint256", "internalType": "uint256" }
{
"name": "role",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "index",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{ "name": "", "type": "address", "internalType": "address" }
{
"name": "",
"type": "address",
"internalType": "address"
}
]
},
{
"type": "function",
"name": "getRoleMemberCount",
"stateMutability": "view",
"inputs": [
{ "name": "role", "type": "bytes32", "internalType": "bytes32" }
{
"name": "role",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{ "name": "", "type": "uint256", "internalType": "uint256" }
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"inputs": [],
"name": "versionRegistry",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "rewardTokenRegistry",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "versionRegistry",
"type": "address"
}
],
"name": "VersionRegistrySet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "rewardTokenRegistry",
"type": "address"
}
],
"name": "RewardTokenRegistrySet",
"type": "event"
}
]
Loading