dao-governance
DAO Governance — veToken Model
serayd61/stacks-dao-governance · 5 contracts · ~929 lines · February 21, 2026
Overview
A Curve-inspired veToken DAO governance system with five contracts: governance token (SIP-010 with delegation), proposal manager, timelock, treasury, and voting escrow. The architecture aims for a full governance stack — lock tokens for time-weighted voting power, create and vote on proposals, queue through a timelock, and manage treasury spending.
The system is non-functional. The voting escrow contains an infinitely recursive function that aborts every lock attempt. The proposal manager has its threshold check commented out and uses hardcoded voting power. The five contracts are not integrated with each other.
| Severity | Count |
|---|---|
| CRITICAL | 3 |
| HIGH | 4 |
| MEDIUM | 4 |
| LOW / INFO | 3 |
Critical Findings
C-1 Infinite Recursion in to-int — Voting Escrow Completely Broken
Contract: voting-escrow.clar
The to-int helper function shadows the Clarity built-in and calls itself unconditionally:
(define-private (to-int (value uint))
(if (< value u9223372036854775807)
(to-int value) ;; ← recursive call to itself, NOT the built-in
0))
This function is called by checkpoint, which is called by create-lock, increase-amount, and increase-unlock-time. Every attempt to lock tokens hits the runtime recursion limit and aborts. No locks can ever be created, making the entire veToken model non-functional.
Fix: Remove the wrapper entirely and use the built-in to-int directly in checkpoint, or rename the helper to avoid shadowing.
C-2 Hardcoded Voting Power — Governance Token Meaningless
Contract: proposal-manager.clar
Every voter receives exactly 1,000,000 votes regardless of actual token holdings:
(define-public (vote (proposal-id uint) (support bool))
(let (
(voter-power u1000000) ;; Simplified - would call governance-token
)
This makes the governance token pointless. One address = one million votes. A sybil attack with N addresses yields N million votes, trivially overwhelming any legitimate governance.
C-3 Proposal Threshold Completely Disabled
Contract: proposal-manager.clar
The minimum token requirement to create proposals is commented out:
;; (asserts! (>= (contract-call? .governance-token get-voting-power tx-sender) ;; (var-get proposal-threshold)) ;; ERR_INSUFFICIENT_VOTING_POWER)
Anyone — with zero tokens — can create unlimited proposals. Combined with C-2, a single attacker with no tokens can create and pass proposals at will.
High Findings
H-1 Delegation Double-Counting Inflates Voting Power
Contract: governance-token.clar
update-voting-power adds the delegator's balance to the delegate's power but never subtracts it when re-delegating:
(map-set voting-power delegate-to (+ (get-voting-power delegate-to) balance))
If Alice (100 tokens) delegates to Bob, Bob gets +100. If Alice later re-delegates to Carol, Carol gets +100 but Bob's extra 100 is never removed. After N re-delegations, total voting power inflates by N × balance. This is trivially exploitable: delegate in a loop to manufacture unlimited voting power.
H-2 Quorum Check Missing from Finalization
Contract: proposal-manager.clar
Despite configuring a quorum-percentage of 10%, finalize-proposal only checks vote direction:
(if (> (get for-votes proposal) (get against-votes proposal))
(map-set proposals proposal-id (merge proposal { state: STATE_SUCCEEDED }))
...)
A single vote can pass any proposal. Combined with C-2 (hardcoded 1M votes), one address can unilaterally control governance.
H-3 Permissionless Proposal Queue and Execution
Contract: proposal-manager.clar
queue-proposal and execute-proposal have no access control checks. While sometimes intentional, combined with C-2, C-3, and H-2, a single attacker can run the full governance lifecycle: create → vote → finalize → queue → execute in ~1,442 blocks with no tokens.
H-4 Treasury Balance Tracking Desync
Contract: treasury-manager.clar
The treasury tracks its balance via a data-var separate from the actual STX balance held by the contract:
(var-set treasury-balance (+ (var-get treasury-balance) amount))
Direct STX transfers to the contract address, or partial failures during stx-transfer?, cause the tracked balance to diverge from reality. The spending-check against treasury-balance can then block valid withdrawals or approve impossible ones.
Medium Findings
M-1 Voting Escrow total-supply Stale Immediately After Checkpoint
Contract: voting-escrow.clar
total-supply is a point-in-time snapshot set during checkpoint, but voting power decays linearly with every block. The stored value becomes stale immediately, making get-total-voting-power and get-voting-power-percentage inaccurate. In a real veToken system, this must be recomputed or tracked with slope changes.
M-2 execute-proposal Is a No-Op
Contract: proposal-manager.clar
;; Execute actions would go here
The entire governance lifecycle — create, vote, finalize, queue, execute — results in a state flag set to STATE_EXECUTED with zero on-chain effect. There's no mechanism to attach executable actions (contract calls, parameter changes, transfers) to proposals. The governance system governs nothing.
M-3 Timelock Not Connected to DAO
Contract: timelock.clar
The timelock only accepts transactions from CONTRACT_OWNER, not from the proposal manager. The governance flow and the timelock are completely independent systems. Proposals don't create timelock entries, and timelock execution doesn't verify proposal approval. Two disconnected security mechanisms.
M-4 get-voting-power-at Uses Current State, Not Historical
Contract: voting-escrow.clar
(define-read-only (get-voting-power-at (user principal) (target-block uint)) ... (calculate-voting-power (get amount lock) (get end lock)))
This reads the current lock state rather than the state at target-block. It also calls calculate-voting-power which uses the current block-height for decay, not the target block. Historical queries return present-day power, making snapshot-based governance impossible.
Low / Informational
L-1 Checkpoint Requires Exact Block Match
Contract: governance-token.clar
get-past-votes uses a map keyed on exact block numbers. Unless a checkpoint was created at that precise block, it returns u0. There's no nearest-checkpoint search. Real governance systems need binary search over epoch arrays.
L-2 No Bounds on Governance Parameters
Contract: proposal-manager.clar
set-voting-period and set-timelock-delay accept any uint with no minimum or maximum bounds. The owner can set the voting period to u0 (instant voting) or timelock delay to u0 (no delay), silently disabling both safeguards.
L-3 Implicit Self-Delegation Never Checkpointed
Contract: governance-token.clar
get-delegate returns the account itself when no delegation is set, and update-voting-power handles this case correctly. However, the initial voting power is never checkpointed until the user explicitly acts (delegates or transfers), creating a gap in historical records.
Positive Observations
- GOOD veToken concept (lock duration × amount = voting power) is architecturally sound when functional
- GOOD Clean separation into 5 focused contracts with clear responsibilities
- GOOD Timelock has proper grace period and expiry — a good security pattern
- GOOD Treasury spending requires explicit approval → execution two-step flow
- GOOD Linear decay formula
amount × time_remaining / max_timeis correct
Verdict
This DAO governance system is non-functional at every layer. The voting escrow — the architectural centerpiece — cannot create locks due to infinite recursion in a helper function. The proposal manager bypasses its own token-gating (commented out) and uses hardcoded voting power, making governance trivially attackable by a single address. The five contracts exist as isolated modules: the timelock is owner-only, proposals don't execute actions, and the treasury operates independently of governance approval.
The conceptual architecture (veToken + proposals + timelock + treasury) is sound and follows well-known patterns from Curve/Compound. But every integration point is missing or broken. Not safe for any use. Requires: fix to-int recursion, implement actual cross-contract calls for voting power, add quorum enforcement, connect timelock to proposal flow, and implement proposal execution.