Skip to content

fix: use 1-address-1-vote model to prevent flash-loan voting (#469) - #689

Open
0dillon wants to merge 4 commits into
Iris-IV:mainfrom
0dillon:fix/flash-loan-voting-snapshot
Open

fix: use 1-address-1-vote model to prevent flash-loan voting (#469)#689
0dillon wants to merge 4 commits into
Iris-IV:mainfrom
0dillon:fix/flash-loan-voting-snapshot

Conversation

@0dillon

@0dillon 0dillon commented Jul 30, 2026

Copy link
Copy Markdown

Closes #469

Summary

Replace token-weighted voting with a 1-address-1-vote model to close the flash-loan attack vector where an attacker borrows a large token balance, votes with inflated weight, and returns the tokens before verification.

What Changed

  • cast_vote: Each vote now contributes exactly 1 to the weight sum instead of the voter's live token balance. The token-holder gate (min_voting_balance) is preserved — you still need tokens to vote, but your balance doesn't affect your voting power.
  • verify_with_votes: Threshold calculation now uses vote counts (approve_votes / reject_votes) instead of token weights. This is safe because the quorum check already guarantees total_votes > 0.
  • Backward compat: approve_weight and reject_weight storage keys are still incremented (by 1 per vote) so existing campaigns with stored voting state remain valid.

Design Decision

Three options were considered from the issue: snapshot at creation, minimum holding period, or 1-address-1-vote. 1-address-1-vote was chosen because it's the simplest, most gas-efficient fix that fully eliminates the flash-loan vector without requiring expensive on-chain iteration of all token holders at campaign creation time.

Acceptance Criteria

  • cast_vote no longer uses live balance for voting weight
  • verify_with_votes threshold immune to flash-loan inflation
  • Token-holder gate preserved (must hold >= min_voting_balance)
  • All tests pass (393/395, 2 pre-existing unrelated failures in test_campaign_update)
  • Clippy clean, formatting clean
  • Proptests updated for 1-address-1-vote invariant

Test Output

test result: ok. 393 passed; 2 failed (pre-existing in test_campaign_update)

Follow-ups (out of scope)

  • Consider removing approve_weight/reject_weight storage entirely since they now equal vote counts (backward-compat cleanup)
  • Update EVENT_PAYLOADS.md to reflect changed event shape

Security Note

This fix prevents flash-loan governance attacks. The 1-address-1-vote model is Sybil-resistant only to the extent that obtaining token balances across many addresses costs real capital. The min_voting_balance admin parameter can be raised to increase this cost.

…#469)

Replace token-weighted voting with 1-address-1-vote in cast_vote and
verify_with_votes. Each eligible token holder now contributes exactly 1 to
the vote count regardless of their balance, closing the flash-loan attack
vector where an attacker borrows a large balance, votes with inflated
weight, and returns the tokens before verification.

Changes:
- cast_vote: weight = 1 instead of weight = balance
- verify_with_votes: threshold computed from vote counts, not token weights
- Updated tests and proptests for the new 1-address-1-vote model
- Updated overflow regression test (Iris-IV#354) for safe weight addition

Closes Iris-IV#469
@drips-wave

drips-wave Bot commented Jul 30, 2026

Copy link
Copy Markdown

@0dillon Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

0dillon added 3 commits July 30, 2026 07:24
)

Tests were unpacking the campaign_metadata_updated event as a 2-tuple
(String, String) but the event was updated to emit 4 strings
(old_title, old_desc, new_title, new_desc) for consistent indexer shape.
@davidmaronio davidmaronio mentioned this pull request Aug 3, 2026
9 tasks
@davidmaronio

Copy link
Copy Markdown
Contributor

solid, minimal fix for #469. incrementing the weight sums by 1 instead of the voter's balance (src/voting.rs:393, 403) and computing approval bps from counts in verify_with_votes closes the flash-loan vector while keeping the storage layout and the 3-tuple event shape intact, so indexers do not break. adapting the #354 overflow test into test_vote_weight_does_not_overflow_with_1_address_1_vote instead of deleting it is a nice touch, and the proptest migration to u32 counts with saturating adds is careful.

a few notes:

  1. src/voting.rs:388-406 - after this change ApproveWeight/RejectWeight are exact duplicates of approve_votes/reject_votes, so every vote pays for two redundant persistent writes. consider skipping the weight writes entirely and leaving the keys dormant (see Fix/448 flash loan voting #709, which does this with #[expect(dead_code)] on the helpers), unless you want the mirror kept deliberately for indexer continuity; if so, a comment saying that would help the next reader.

  2. src/voting.rs:429-432 - the comment says total_votes > 0 is guaranteed by the quorum check, but that only holds when min_votes_quorum > 0. the unwrap_or(0) already protects the division, so just soften the comment.

  3. src/voting.rs:414 - the event now emits a constant (approve, 1i128, 1i128), dropping the voter's actual balance which was previously informational for indexers. emitting (approve, balance, 1i128) would keep that signal while still recording unit weight.

  4. src/tests/test_voting.rs:180 - the half-approval tolerance widened to 4900..=5000; with votes >= 10 the true lower bound is tighter, so a narrower band keeps the property useful.

heads up that #709 solves the same issue (#448/#469) with a fuller cleanup; maintainers should pick one and close the other. this branch is in conflict with main, so please resolve conflicts and rebase, then this is mergeable from my side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] Token-weighted voting uses live balance — flash-loan inflates approval weight

2 participants