Feat(crowdfund): financial penalties for malicious campaigns #409
Open
goodness-cpu wants to merge 2 commits into
Open
Feat(crowdfund): financial penalties for malicious campaigns #409goodness-cpu wants to merge 2 commits into
goodness-cpu wants to merge 2 commits into
Conversation
…otocol#360) Closes ShadeProtocol#360 Adds a self-imposed penalty rate (organizer commits up to 50% of payout) combined with a 7-day backer-governance vote window on any malicious- campaign report. If backers vote the campaign malicious by strict majority, the slashed portion of any withdrawal is detained in an on- chain penalty pool and distributed pro-rata to backers via claim_penalty_refund. Unclaimed remainder becomes sweepable 6 months after resolution for treasury consolidation. New entry points: - set_penalty_bps: organizer-only, locked once the first pledge lands - report_malicious: backer-only with anti-griefing 1% stake floor - vote_on_malice: one weighted vote per backer over a 7-day window - resolve_malice_report: strict-majority approval of penalty - claim_penalty_refund: pro-rata refund, idempotent - sweep_unclaimed_penalty: anyone, after a 6-month grace window Modifies execute_campaign and release_milestone to invoke compute_and_lock_penalty and emit PenaltySlashedEvent when the malice proposal has been approved. Both pathways also block while the malice vote window is still open to prevent the organizer from front-running the penalty vote. Storage uses 17 new DataKey variants and 13 new error variants (28..40). Happy-path and negative tests cover locking, voting, pro-rata math, milestone slashing, execute-blocking, sweep gating.
|
@goodness-cpu 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! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
closes #360
Summary
Implements an opt-in, self-imposed financial-penalty mechanism for the
crowdfundcontract that lets backers recover a slice of a successful campaign's payout if
the organizer is later judged to have acted maliciously. The organizer
voluntarily commits to a slashable rate (≤ 50%) before any pledge lands; backers
can then open a 7-day, pledge-weighted governance vote to accuse the campaign;
on majority approval, the slashed slice is detained on-chain at the moment of
execute_campaign/release_milestoneand distributed pro-rata to backersunder claim — with a 6-month unclaimed sweep as a fallback.
Why
Successful crowdfund campaigns have no recourse today if the organizer
disappears, never delivers, or acts in bad faith. Existing commitment devices
(refunds on failed goals, milestone voting) only protect against delivery
risk, not malice risk. This PR adds a credible threat of partial recovery
that does not require a centralized arbiter.
Design highlights
PenaltyBpsis set voluntarily by theorganizer. It locks automatically after the first pledge lands so the
disclosed rate is guaranteed for the entire campaign.
hold at least 1 % of
Raised, so a single sock-puppet cannot spam votes.execute_campaignandrelease_milestonerefuse to run while a malice vote window is still open — the organizer
cannot race to withdraw funds before backers finalize the vote.
votes > raised / 2,using each backer's pledge as weight. Same governance model as the existing
milestone voting.
Raisedis frozen intoPenaltySnapshotRaisedat resolution, so claim math stays correct even if the campaign somehow
accrues more pledges after the report.
token transfer in
claim_penalty_refund.routed to a configurable recipient (treasury / governance); no funds are
permanently stuck.
Constants
MAX_PENALTY_BPSPENALTY_VOTE_WINDOWMIN_REPORTER_PLEDGE_BPSPENALTY_SWEEP_AFTER_SECSPublic API
Configuration & status
set_penalty_bps(bps)MAX_PENALTY_BPS. Locked after first pledge.get_penalty_bps()penalty_locked()is_malice_report_active()is_penalty_approved()resolve_malice_reportresolved in favour of backers.penalty_pool_balance()penalty_snapshot_raised()Raisedat resolution time — claim denominator.malice_vote_start() / deadline() / reporter() / reason()penalty_vote_counts()→(approval, rejection)backer_penalty_claimed(backer)penalty_sweep_unlock()max_penalty_bps() / penalty_vote_window() / penalty_sweep_after()Report, vote, resolution
report_malicious(reporter, reason)Raisedand may not be the organizer.vote_on_malice(voter, approve)resolve_malice_report()approval > raised / 2. SnapshotsRaisedand arms the sweep timer.Payout integration (existing flows now penalty-aware)
execute_campaign()—compute_and_lock_penaltydeducts the slashed slicefrom the organizer payout when
is_penalty_approvedis true; no-opotherwise.
release_milestone(index)— samecompute_and_lock_penaltyapplication permilestone slice, so a malicious milestone does not break the rest of the
release schedule.
Claim & sweep
claim_penalty_refund(backer)sweep_unclaimed_penalty(recipient)penalty_sweep_unlock.Events
PenaltyConfiguredEvent { bps }MaliciousReportFiledEvent { reporter, reason, vote_deadline }MaliceVoteCastEvent { voter, approve, weight }MaliceReportResolvedEvent { approved, approval_weight, rejection_weight, snapshot_raised }PenaltySlashedEvent { amount, source, pool_balance }PenaltyRefundClaimedEvent { backer, amount, remaining_pool }PenaltySweptEvent { recipient, amount }Errors added (
contracts/crowdfund/src/errors.rs)PenaltyBpsInvalid,PenaltyLocked,MaliceReportActive,NoMaliceReport,MaliceVoteWindowActive,MaliceVoteWindowExpired,PenaltyNotApproved,InsufficientPenaltyPool,NotOrganizer,InsufficientReporterStake,PenaltySweepTooEarly,NoPenaltyShareAvailable,PenaltyVoteAlreadyCast.Files touched
Tests added (
contracts/crowdfund/src/test.rs)Happy path:
test_set_penalty_bps_records_and_emitstest_penalty_bps_locks_after_contributiontest_report_malicious_opens_vote_windowtest_vote_on_malice_records_weight_oncetest_resolve_malice_approves_on_majoritytest_execute_campaign_keeps_full_payout_when_no_penaltytest_execute_campaign_slashes_after_malice_approval— full end-to-end flow including pro-rata backer claimstest_release_milestone_does_not_apply_penalty_when_unapprovedtest_release_milestone_slashes_after_malice_approvaltest_claim_penalty_refund_returns_no_share_when_pool_emptytest_sweep_unclaimed_penalty_before_window_panicsNegative / guard paths:
test_set_penalty_bps_above_max_panicstest_set_penalty_bps_blocked_after_first_pledgetest_report_malicious_requires_min_staketest_report_malicious_cannot_self_reporttest_vote_on_malice_double_vote_panicstest_vote_on_malice_after_window_panicstest_resolve_malice_requires_window_closetest_claim_penalty_refund_before_approval_panicstest_execute_campaign_blocked_during_vote_windowSecurity review notes
saturating_*arithmetic — no overflow.compute_and_lock_penaltyis the single source of truth for slashing mathand is invoked at every payout site, so milestone mode and direct-execute
mode stay consistent.
token::transfercall is needed (the contract already holds the funds),so there is no extra cross-contract surface.
sweep_unclaimed_penaltyzeroes the pool by setting it toPenaltyTotalClaimed, not by resetting it to zero — this preserves theinvariant that
pool >= total_claimedeven after sweep.Notes for reviewer
keys are additive under the existing
DataKeyenum.Raisedtaken at resolution (PenaltySnapshotRaised) keepspro-rata math stable even if more pledges accrue before
execute_campaign/
release_milestoneactually runs.without recompilation.