Skip to content

Commit c40857e

Browse files
moose-codeclaude
andcommitted
feat: veCRV locks + FeeDistributor protocol revenue
- veCRV vote-escrow: per-user Lock (amount, unlock time) + VeCrvState (total locked, lock count). Validated: 68.67M CRV locked across 719 locks. - FeeDistributor (3CRV + crvUSD): weekly FeeDistribution (CheckpointToken) + FeeClaim. Validated: 21 weekly distributions, 1,319 claims (first week $3.18M). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 76c8f05 commit c40857e

3 files changed

Lines changed: 219 additions & 0 deletions

File tree

config.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,27 @@ contracts:
208208
- hash
209209
- event: UpdateLiquidityLimit(address user, uint256 original_balance, uint256 original_supply, uint256 working_balance, uint256 working_supply)
210210

211+
# veCRV vote-escrow + fee distribution (protocol revenue).
212+
- name: VotingEscrow
213+
events:
214+
- event: Deposit(address indexed provider, uint256 value, uint256 indexed locktime, int128 type, uint256 ts)
215+
field_selection:
216+
transaction_fields:
217+
- hash
218+
- event: Withdraw(address indexed provider, uint256 value, uint256 ts)
219+
field_selection:
220+
transaction_fields:
221+
- hash
222+
- event: Supply(uint256 prevSupply, uint256 supply)
223+
224+
- name: FeeDistributor
225+
events:
226+
- event: CheckpointToken(uint256 time, uint256 tokens)
227+
- event: Claimed(address indexed recipient, uint256 amount, uint256 claim_epoch, uint256 max_epoch)
228+
field_selection:
229+
transaction_fields:
230+
- hash
231+
211232
# scrvUSD savings vault (ERC4626).
212233
- name: ScrvUsdVault
213234
events:
@@ -229,6 +250,15 @@ chains:
229250
address:
230251
- 0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB
231252
- name: Gauge
253+
- name: VotingEscrow
254+
address:
255+
- 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2
256+
start_block: 10650387
257+
- name: FeeDistributor
258+
address:
259+
- 0xA464e6DCda8AC41e03616F95f4BC98a13b8922Dc
260+
- 0xD16d5eC345Dd86Fb63C6a9C43c517210F1027914
261+
start_block: 11362396
232262
- name: TricryptoFactoryNG
233263
address:
234264
- 0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963

schema.graphql

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,55 @@ type GaugeVote {
295295
txHash: String!
296296
}
297297

298+
# =========================================================================
299+
# veCRV vote-escrow (locks) + fee distribution (protocol revenue to lockers).
300+
# =========================================================================
301+
302+
type Lock {
303+
id: ID!
304+
chainId: Int!
305+
user: String! @index
306+
lockedAmount: BigInt!
307+
unlockTime: BigInt!
308+
isActive: Boolean!
309+
createdBlock: Int!
310+
createdTimestamp: BigInt!
311+
lastUpdatedBlock: Int!
312+
lastUpdatedTimestamp: BigInt!
313+
}
314+
315+
type VeCrvState {
316+
id: ID!
317+
chainId: Int!
318+
totalLocked: BigInt!
319+
lockCount: Int!
320+
lastUpdatedBlock: Int!
321+
lastUpdatedTimestamp: BigInt!
322+
}
323+
324+
type FeeDistribution {
325+
id: ID!
326+
chainId: Int!
327+
distributor: String! @index
328+
token: String!
329+
weekTime: BigInt!
330+
tokens: BigInt!
331+
blockNumber: Int!
332+
timestamp: BigInt! @index
333+
}
334+
335+
type FeeClaim {
336+
id: ID!
337+
chainId: Int!
338+
distributor: String! @index
339+
recipient: String! @index
340+
amount: BigInt!
341+
epoch: BigInt!
342+
blockNumber: Int!
343+
timestamp: BigInt! @index
344+
txHash: String!
345+
}
346+
298347
type Loan {
299348
id: ID!
300349
chainId: Int!

src/handlers/VeCrvFees.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { indexer } from "envio";
2+
3+
// veCRV vote-escrow locks + FeeDistributor protocol revenue (the fees paid out
4+
// to veCRV lockers each week — 3CRV historically, crvUSD since 2024).
5+
6+
const FEE_TOKEN: Record<string, string> = {
7+
"0xa464e6dcda8ac41e03616f95f4bc98a13b8922dc": "3CRV",
8+
"0xd16d5ec345dd86fb63c6a9c43c517210f1027914": "crvUSD",
9+
};
10+
11+
async function ensureVeCrvState(
12+
context: any,
13+
chainId: number,
14+
block: { number: number; timestamp: number },
15+
): Promise<any> {
16+
const id = `${chainId}`;
17+
const existing = await context.VeCrvState.get(id);
18+
if (existing) return existing;
19+
const created = {
20+
id,
21+
chainId,
22+
totalLocked: 0n,
23+
lockCount: 0,
24+
lastUpdatedBlock: block.number,
25+
lastUpdatedTimestamp: BigInt(block.timestamp),
26+
};
27+
context.VeCrvState.set(created);
28+
return created;
29+
}
30+
31+
indexer.onEvent(
32+
{ contract: "VotingEscrow", event: "Deposit" },
33+
async ({ event, context }) => {
34+
const chainId = event.chainId;
35+
const user = event.params.provider.toLowerCase();
36+
const id = `${chainId}_${user}`;
37+
const existing = await context.Lock.get(id);
38+
const isNew = !existing || !existing.isActive;
39+
const prevUnlock = existing?.unlockTime ?? 0n;
40+
context.Lock.set({
41+
id,
42+
chainId,
43+
user,
44+
lockedAmount: (existing?.lockedAmount ?? 0n) + event.params.value,
45+
unlockTime:
46+
event.params.locktime > prevUnlock ? event.params.locktime : prevUnlock,
47+
isActive: true,
48+
createdBlock: existing?.createdBlock ?? event.block.number,
49+
createdTimestamp: existing?.createdTimestamp ?? BigInt(event.block.timestamp),
50+
lastUpdatedBlock: event.block.number,
51+
lastUpdatedTimestamp: BigInt(event.block.timestamp),
52+
});
53+
if (isNew) {
54+
const s = await ensureVeCrvState(context, chainId, event.block);
55+
context.VeCrvState.set({
56+
...s,
57+
lockCount: s.lockCount + 1,
58+
lastUpdatedBlock: event.block.number,
59+
lastUpdatedTimestamp: BigInt(event.block.timestamp),
60+
});
61+
}
62+
},
63+
);
64+
65+
indexer.onEvent(
66+
{ contract: "VotingEscrow", event: "Withdraw" },
67+
async ({ event, context }) => {
68+
const chainId = event.chainId;
69+
const user = event.params.provider.toLowerCase();
70+
const id = `${chainId}_${user}`;
71+
const existing = await context.Lock.get(id);
72+
if (!existing) return;
73+
context.Lock.set({
74+
...existing,
75+
lockedAmount: 0n,
76+
isActive: false,
77+
lastUpdatedBlock: event.block.number,
78+
lastUpdatedTimestamp: BigInt(event.block.timestamp),
79+
});
80+
if (existing.isActive) {
81+
const s = await ensureVeCrvState(context, chainId, event.block);
82+
context.VeCrvState.set({
83+
...s,
84+
lockCount: s.lockCount > 0 ? s.lockCount - 1 : 0,
85+
lastUpdatedBlock: event.block.number,
86+
lastUpdatedTimestamp: BigInt(event.block.timestamp),
87+
});
88+
}
89+
},
90+
);
91+
92+
indexer.onEvent(
93+
{ contract: "VotingEscrow", event: "Supply" },
94+
async ({ event, context }) => {
95+
const s = await ensureVeCrvState(context, event.chainId, event.block);
96+
context.VeCrvState.set({
97+
...s,
98+
totalLocked: event.params.supply,
99+
lastUpdatedBlock: event.block.number,
100+
lastUpdatedTimestamp: BigInt(event.block.timestamp),
101+
});
102+
},
103+
);
104+
105+
indexer.onEvent(
106+
{ contract: "FeeDistributor", event: "CheckpointToken" },
107+
async ({ event, context }) => {
108+
const chainId = event.chainId;
109+
const dist = event.srcAddress.toLowerCase();
110+
context.FeeDistribution.set({
111+
id: `${chainId}_${event.block.number}_${event.logIndex}`,
112+
chainId,
113+
distributor: dist,
114+
token: FEE_TOKEN[dist] ?? "?",
115+
weekTime: event.params.time,
116+
tokens: event.params.tokens,
117+
blockNumber: event.block.number,
118+
timestamp: BigInt(event.block.timestamp),
119+
});
120+
},
121+
);
122+
123+
indexer.onEvent(
124+
{ contract: "FeeDistributor", event: "Claimed" },
125+
async ({ event, context }) => {
126+
const chainId = event.chainId;
127+
const dist = event.srcAddress.toLowerCase();
128+
context.FeeClaim.set({
129+
id: `${chainId}_${event.block.number}_${event.logIndex}`,
130+
chainId,
131+
distributor: dist,
132+
recipient: event.params.recipient.toLowerCase(),
133+
amount: event.params.amount,
134+
epoch: event.params.claim_epoch,
135+
blockNumber: event.block.number,
136+
timestamp: BigInt(event.block.timestamp),
137+
txHash: event.transaction.hash,
138+
});
139+
},
140+
);

0 commit comments

Comments
 (0)