Skip to content

Commit 3984606

Browse files
committed
fix: do not retry disabled ones
1 parent 3d8dc6b commit 3984606

File tree

5 files changed

+378
-1
lines changed

5 files changed

+378
-1
lines changed
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
---
2+
name: auto-voting-relayers
3+
description: "Complete domain knowledge for VeBetterDAO's auto-voting and relayer system. Use when working on relayer dashboard, relayer node, auto-voting contracts (XAllocationVoting, VoterRewards, RelayerRewardsPool), or anything related to relayers, auto-voting, gasless voting, or relayer rewards. Triggers on: relayer, auto-voting, autovoting, gasless voting, relayer rewards, RelayerRewardsPool, castVoteOnBehalfOf, relayer dashboard, relayer node, veDelegate comparison."
4+
allowed-tools: []
5+
license: MIT
6+
metadata:
7+
author: VeChain
8+
version: "0.1.0"
9+
---
10+
11+
# VeBetterDAO Auto-Voting & Relayer System
12+
13+
Complete domain knowledge for the auto-voting and relayer ecosystem. This skill provides context for working on any component of the system: smart contracts, relayer node, relayer dashboard, or documentation.
14+
15+
## System Overview
16+
17+
VeBetterDAO's auto-voting system lets users automate their weekly X Allocation voting. Users pick favorite apps once, toggle auto-voting on, and relayers (off-chain services) handle the rest: casting votes, claiming rewards, all gasless. Relayers earn a fee from the reward pool. Tokens never leave the user's wallet.
18+
19+
Auto-voting applies **only to X Allocation rounds**. Governance proposals remain manual-only.
20+
21+
## Architecture
22+
23+
```text
24+
Users (toggle auto-voting, set preferences)
25+
|
26+
v
27+
Smart Contracts (on-chain logic)
28+
- XAllocationVoting (v8): auto-voting state, vote execution
29+
- VoterRewards (v6): reward claiming, fee deduction
30+
- RelayerRewardsPool (v1): relayer registration, reward distribution
31+
|
32+
v
33+
Relayer Nodes (off-chain execution)
34+
- relayer-node/ (standalone CLI, no monorepo dependency)
35+
- Monitor rounds, batch vote/claim, loop every 5 min
36+
|
37+
v
38+
Relayer Dashboard (monitoring/analytics)
39+
- apps/relayer-dashboard/ (static Next.js, GitHub Pages)
40+
- Round analytics, relayer stats, ROI tracking
41+
```
42+
43+
## How It Works (Non-Technical)
44+
45+
### For Users
46+
47+
1. Hold 1+ VOT3, complete 3+ sustainable actions, pass VeBetterPassport
48+
2. Choose up to 15 apps, toggle auto-voting on
49+
3. Takes effect next round (not current)
50+
4. Each week: relayer votes for you, claims your rewards to your wallet
51+
5. Fee: 10% of rewards (max 100 B3TR/week) - covers all gas costs
52+
6. While active: no manual voting/claiming allowed
53+
7. Manual claim fallback: available 5 days after round end if relayer hasn't processed
54+
55+
### Auto-Disable Triggers
56+
57+
- VOT3 drops below 1
58+
- All selected apps become ineligible
59+
- Sustainable action threshold not met
60+
- Bot detection by app owner
61+
62+
### For Relayers
63+
64+
1. Get registered on-chain (POOL_ADMIN_ROLE during MVP)
65+
2. Run relayer-node with wallet (MNEMONIC or RELAYER_PRIVATE_KEY)
66+
3. Node auto-discovers users, batches votes + claims
67+
4. Earn weighted points: vote = 3 pts, claim = 1 pt
68+
5. After ALL users served, claim proportional share of reward pool
69+
6. All-or-nothing: if any user missed, nobody gets paid
70+
71+
### Apps as Relayers
72+
73+
Apps can register as relayers, ask users to set them as preference, and run a node. They earn relayer fees instead of paying veDelegate for votes. Important: apps should ADD themselves to preference lists, not replace other apps.
74+
75+
## vs veDelegate
76+
77+
| Feature | veDelegate | VeBetterDAO Auto-Voting |
78+
| --- | --- | --- |
79+
| X Allocation voting | Yes | Yes |
80+
| Governance voting | Yes (always "abstain") | No (manual only) |
81+
| Compounding (B3TR->VOT3) | Auto | Manual |
82+
| Token custody | Leaves wallet | Stays in wallet |
83+
| Centralization | Single entity | Many relayers |
84+
| Cost to apps | Apps pay veDelegate | Apps earn fees |
85+
86+
veDelegate: docs.vedelegate.vet / github.com/vechain-energy/vedelegate-for-dapps
87+
88+
## Smart Contracts Detail
89+
90+
### XAllocationVoting.sol (v8)
91+
92+
Auto-voting added in v8 via `AutoVotingLogicUpgradeable` module.
93+
94+
**Storage** (in AutoVotingLogic library):
95+
96+
- `_autoVotingEnabled`: Checkpointed per-user status (changes take effect next round)
97+
- `_userVotingPreferences`: Array of app IDs per user (max 15, validated, no duplicates)
98+
- `_totalAutoVotingUsers`: Checkpointed total count
99+
100+
**Key functions:**
101+
102+
```solidity
103+
toggleAutoVoting(address user) // Enable/disable
104+
setUserVotingPreferences(bytes32[] memory appIds) // Set apps (1-15)
105+
castVoteOnBehalfOf(address voter, uint256 roundId) // Relayer executes vote
106+
getUserVotingPreferences(address) // View preferences
107+
isUserAutoVotingEnabled(address) // Current status
108+
isUserAutoVotingEnabledForRound(address, uint256) // Status at round snapshot
109+
getTotalAutoVotingUsersAtRoundStart() // Count at last emission
110+
getTotalAutoVotingUsersAtTimepoint(uint48) // Historical count
111+
```
112+
113+
**Vote execution** (`castVoteOnBehalfOf`):
114+
115+
1. Validate early access (registered relayer during window)
116+
2. Get user preferences, filter eligible apps
117+
3. Split voting power equally across eligible apps
118+
4. Cast via internal `_countVote()`
119+
5. Register VOTE action on RelayerRewardsPool (3 weight points)
120+
121+
When the user is ineligible (e.g. VOT3 < 1, no eligible apps, threshold not met), the contract does **not** revert; it skips the vote and emits **AutoVoteSkipped(voter, roundId, isPerson, appCount, votingPower)**. The user is not marked as having voted (`hasVoted` stays false), so relayers must not retry them every cycle.
122+
123+
**Events relevant to relayers:**
124+
125+
- `AutoVotingToggled(account, enabled)` — used to discover who had auto-voting on at round snapshot
126+
- `AllocationAutoVoteCast(voter, roundId, appsIds, voteWeights)` — successful relayer vote
127+
- `AutoVoteSkipped(voter, roundId, isPerson, appCount, votingPower)` — ineligible user, processed but no vote cast; relayer should treat as "already processed" for this round
128+
129+
### VoterRewards.sol (v6)
130+
131+
V6 added relayer fee integration. Fee deduction happens HERE during `claimReward()`.
132+
133+
**V6 storage:** `xAllocationVoting`, `relayerRewardsPool`
134+
135+
**Fee flow in `claimReward(uint256 cycle, address voter)`:**
136+
137+
1. Check user had auto-voting at round start (checkpointed)
138+
2. Calculate raw rewards (voting + GM reward)
139+
3. If auto-voting: `fee = min(totalReward * 10/100, 100 B3TR)`
140+
4. Fee proportionally split between voting and GM reward
141+
5. Fee approved + deposited to `RelayerRewardsPool.deposit(fee, cycle)`
142+
6. `registerRelayerAction(msg.sender, voter, cycle, CLAIM)` - credits caller with 1 weight point
143+
7. Net reward transferred to voter wallet
144+
145+
**Important:** `msg.sender` calling `claimReward()` IS the relayer credited for CLAIM action.
146+
147+
**Early access:** During window, `validateClaimDuringEarlyAccess()` reverts if caller is the voter or not a registered relayer.
148+
149+
### RelayerRewardsPool.sol (v1)
150+
151+
Manages registration, action tracking, reward distribution.
152+
153+
**Storage:**
154+
155+
```text
156+
totalRewards[roundId] // Pool amount (funded by fees)
157+
relayerWeightedActions[roundId][relayer] // Per-relayer weighted work
158+
totalWeightedActions[roundId] // Expected weighted total
159+
completedWeightedActions[roundId] // Completed weighted total
160+
registeredRelayers[address] // Registration mapping
161+
relayerAddresses[] // All registered addresses
162+
voteWeight = 3 // Points per vote action
163+
claimWeight = 1 // Points per claim action
164+
earlyAccessBlocks = 432,000 // ~5 days on VeChain
165+
relayerFeePercentage = 10 // 10%
166+
feeCap = 100 ether // 100 B3TR
167+
```
168+
169+
**Reward formula:**
170+
171+
```text
172+
relayerShare = (relayerWeightedActions / completedWeightedActions) * totalRewards
173+
```
174+
175+
**Claimability:** `isRewardClaimable(roundId)` requires:
176+
177+
- Round ended (`emissions.isCycleEnded(roundId)`)
178+
- All work done (`completedWeightedActions >= totalWeightedActions`)
179+
180+
**Key admin functions (POOL_ADMIN_ROLE):**
181+
182+
- `registerRelayer(address)` / `unregisterRelayer(address)`
183+
- `setTotalActionsForRound(roundId, userCount)` - sets expected = userCount x 2 actions, userCount x 4 weighted
184+
- `reduceExpectedActionsForRound(roundId, userCount)` - for ineligible users
185+
- `registerRelayerAction(relayer, voter, roundId, action)` - record work
186+
- `deposit(amount, roundId)` - fund pool
187+
188+
**Early access:**
189+
190+
- Vote window: `roundSnapshot + earlyAccessBlocks`
191+
- Claim window: `roundDeadline + earlyAccessBlocks`
192+
- During: only registered relayers, user can't self-act
193+
- After: anyone can act
194+
195+
## Auto-Voting Lifecycle (Per Round)
196+
197+
```text
198+
Round N: User enables auto-voting + sets preferences (checkpointed)
199+
Round N+1:
200+
1. startNewRound() - snapshot locks auto-voting status
201+
2. setTotalActionsForRound(roundId, userCount)
202+
3. Relayers call castVoteOnBehalfOf() for each user
203+
- Ineligible users: reduceExpectedActionsForRound()
204+
- Each successful vote: +3 weighted points to relayer
205+
4. Round ends (deadline block)
206+
5. Relayers call VoterRewards.claimReward() for each user
207+
- Fee deducted and deposited to pool
208+
- Each successful claim: +1 weighted point to relayer
209+
6. All actions complete -> pool unlocked
210+
7. Relayers call RelayerRewardsPool.claimRewards()
211+
```
212+
213+
## Relayer Node (relayer-node/)
214+
215+
Standalone CLI tool. No monorepo dependency.
216+
217+
**Deps:** `@vechain/sdk-core`, `@vechain/sdk-network`, `@vechain/vebetterdao-contracts`
218+
219+
```text
220+
relayer-node/src/
221+
index.ts # Entry, env parsing, main loop, SIGINT
222+
config.ts # Mainnet + testnet-staging addresses
223+
contracts.ts # 26 view functions + event pagination
224+
relayer.ts # Batch vote/claim with isolation/retry
225+
display.ts # Terminal UI (box drawing + chalk)
226+
types.ts # Shared interfaces
227+
```
228+
229+
**Env vars:** `MNEMONIC` / `RELAYER_PRIVATE_KEY`, `RELAYER_NETWORK`, `RUN_ONCE`, `DRY_RUN`
230+
231+
**Cycle:** Discover users from events -> filter voted + already skipped -> batch castVoteOnBehalfOf -> batch claimReward -> loop 5min
232+
233+
**Vote-cycle "already processed" strategy:** Users who had auto-voting on at snapshot but are ineligible (balance < 1 VOT3, etc.) cause the contract to emit `AutoVoteSkipped` instead of reverting; `hasVoted(roundId, user)` stays false, so without extra logic the relayer would retry them every cycle. The relayer uses **AutoVoteSkipped events** to avoid that: it fetches all `AutoVoteSkipped` logs for the current round (from round snapshot block to latest block), builds the set of voter addresses already skipped, and excludes them from the "needs vote" list. So each ineligible user is effectively processed once (one `castVoteOnBehalfOf` call that results in a skip) and never reconsidered for that round. Implemented in `contracts.ts` via `getAlreadySkippedVotersForRound()`, and in `relayer.ts` by filtering `needsVote` against that set before batching.
234+
235+
## Relayer Dashboard (apps/relayer-dashboard/)
236+
237+
Static Next.js 14 (output: "export"), Chakra UI v3, VeChain Kit, Recharts. GitHub Pages under /b3tr.
238+
239+
**Data:** Static `report.json` (hourly GH Action, temporary) + on-chain reads via `useCallClause`
240+
241+
**Pages** (state-based nav, not file routing):
242+
243+
- Home: StatsCards (2x2), RoundsChart, RoundsList, info cards
244+
- My Relayer: ConnectedWallet view
245+
- Info: BecomeRelayer + AppsAsRelayers
246+
- Round detail: `/round?roundId=X` - 2-col layout, summary/actions/financials
247+
248+
**Hooks:**
249+
250+
- `contracts.ts` - ABIs + addresses from `@repo/config`
251+
- `useCurrentRoundId` - XAllocationVoting.currentRoundId()
252+
- `useTotalAutoVotingUsers` - getTotalAutoVotingUsersAtTimepoint()
253+
- `useRegisteredRelayers` - getRegisteredRelayers()
254+
- `useRoundRewardStatus` - isRewardClaimable() + getTotalRewards()
255+
- `useReportData` - fetches /data/report.json
256+
- `useB3trToVthoRate` - oracle exchange rate
257+
258+
**Commands:** `yarn relayer:dev:staging`, `yarn relayer:dev:mainnet`, `yarn relayer:build:staging`, `yarn relayer:build:mainnet`
259+
260+
## Gas Cost Analysis
261+
262+
| Action | Gas | VTHO | B3TR equiv |
263+
| --- | --- | --- | --- |
264+
| Vote (5-8 apps) | ~441K | ~4.41 | ~0.075 |
265+
| Claim | ~208K | ~2.08 | ~0.035 |
266+
| **Total/user/round** | | **~6.49** | **~0.11** |
267+
268+
Average user: ~10.8k-22.6k VOT3, earns ~90-190 B3TR/round.
269+
At 10% fee: ~9-19 B3TR per user into pool. Relayer cost: ~0.11 B3TR. Margin: ~8.9-18.9 B3TR/user.
270+
271+
## External Resources
272+
273+
- Docs: https://docs.vebetterdao.org/vebetter/automation
274+
- Governance proposal: https://governance.vebetterdao.org/proposals/93450486232994296830196736391400835825360450263361422145364815974754963306849
275+
- Discourse: https://vechain.discourse.group/t/vebetterdao-proposal-auto-voting-for-x-allocation-with-gasless-voting-and-relayer-rewards/559
276+
- NPM: `@vechain/vebetterdao-contracts`
277+
- Contracts source: https://github.com/vechain/vebetterdao-contracts

.agents/skills/grill-me/SKILL.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
name: grill-me
3+
description: Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one.
4+
license: MIT
5+
---
6+
7+
# Grill Me
8+
9+
Interview the user relentlessly about every aspect of their plan until you reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one.
10+
11+
## How It Works
12+
13+
When this skill is invoked, switch into interviewer mode:
14+
15+
1. **Read the plan** — understand what the user has described so far.
16+
2. **Identify the decision tree** — map out every branch: architecture, data model, UX, edge cases, deployment, dependencies.
17+
3. **Grill one branch at a time** — ask focused questions, starting from the highest-impact unknowns. Don't move on until the branch is resolved.
18+
4. **Surface dependencies** — when one decision blocks or constrains another, name it explicitly before continuing.
19+
5. **Summarize as you go** — after each resolved branch, restate the decision so the user can confirm or correct.
20+
6. **Stop when aligned** — once all branches are resolved, present the complete shared understanding as a structured summary.
21+
22+
## Rules
23+
24+
- **Never assume.** If something is ambiguous, ask.
25+
- **One topic at a time.** Don't bundle unrelated questions.
26+
- **Push back.** If a decision seems risky or contradictory, say so.
27+
- **No implementation.** This skill is for planning only. Don't write code.
28+
- **Be direct.** Skip pleasantries. Get to the point.
29+
- **Track progress.** Keep a mental map of resolved vs. open branches so the user knows how much is left.

skills-lock.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"version": 1,
3+
"skills": {
4+
"auto-voting-relayers": {
5+
"source": "vechain/vechain-ai-skills",
6+
"sourceType": "github",
7+
"computedHash": "52147fdab413b0946248b514f47f2019f1297fa7251645ad9b2c5efb9ce147e1"
8+
},
9+
"grill-me": {
10+
"source": "vechain/vechain-ai-skills",
11+
"sourceType": "github",
12+
"computedHash": "a096bc3a1b5f73b889f688b82cf806cc9ffc278551b47efee4465265280c555f"
13+
}
14+
}
15+
}

src/contracts.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,45 @@ export async function getAutoVotingUsers(
183183
return [...userState.entries()].filter(([, on]) => on).map(([a]) => a)
184184
}
185185

186+
/**
187+
* Returns the set of voter addresses that already emitted AutoVoteSkipped for the given round.
188+
* Used so the relayer does not retry castVoteOnBehalfOf for ineligible users (e.g. balance < 1 VOT3).
189+
*/
190+
export async function getAlreadySkippedVotersForRound(
191+
thor: ThorClient,
192+
contractAddress: string,
193+
roundId: number,
194+
fromBlock: number,
195+
toBlock: number,
196+
): Promise<Set<string>> {
197+
const event = xavAbi.getEvent("AutoVoteSkipped") as any
198+
const topics = event.encodeFilterTopicsNoNull({})
199+
const skipped = new Set<string>()
200+
let offset = 0
201+
202+
while (true) {
203+
const logs = await thor.logs.filterEventLogs({
204+
range: { unit: "block" as const, from: fromBlock, to: toBlock },
205+
options: { offset, limit: MAX_EVENTS },
206+
order: "asc",
207+
criteriaSet: [{ criteria: { address: contractAddress, topic0: topics[0] }, eventAbi: event }],
208+
})
209+
for (const log of logs) {
210+
const decoded = event.decodeEventLog({
211+
topics: log.topics.map((t: string) => Hex.of(t)),
212+
data: Hex.of(log.data),
213+
})
214+
if (Number(decoded.args.roundId) === roundId) {
215+
skipped.add((decoded.args.voter as string).toLowerCase())
216+
}
217+
}
218+
if (logs.length < MAX_EVENTS) break
219+
offset += MAX_EVENTS
220+
}
221+
222+
return skipped
223+
}
224+
186225
// ── Full summary fetch ──────────────────────────────────────
187226

188227
export async function fetchSummary(

0 commit comments

Comments
 (0)