|
| 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 |
0 commit comments