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.

SeverityCount
CRITICAL3
HIGH4
MEDIUM4
LOW / INFO3

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

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.