@@ -116,6 +116,11 @@ impl DaoTrait for Tansu {
116
116
votes : Vec < u128 > ,
117
117
seeds : Vec < u128 > ,
118
118
) -> Vec < BytesN < 96 > > {
119
+ // Validate that votes and seeds have the same length
120
+ if votes. len ( ) != seeds. len ( ) {
121
+ panic_with_error ! ( & env, & errors:: ContractErrors :: TallySeedError ) ;
122
+ }
123
+
119
124
let vote_config = Self :: get_anonymous_voting_config ( env. clone ( ) , project_key) ;
120
125
121
126
let bls12_381 = env. crypto ( ) . bls12_381 ( ) ;
@@ -248,12 +253,18 @@ impl DaoTrait for Tansu {
248
253
} ;
249
254
250
255
let next_id = proposal_id + 1 ;
256
+ let page = proposal_id / MAX_PROPOSALS_PER_PAGE ;
257
+
258
+ // Prevent exceeding maximum page limit
259
+ if page >= MAX_PAGES {
260
+ panic_with_error ! ( & env, & errors:: ContractErrors :: NoProposalorPageFound ) ;
261
+ }
262
+
251
263
env. storage ( ) . persistent ( ) . set (
252
264
& types:: ProjectKey :: DaoTotalProposals ( project_key. clone ( ) ) ,
253
265
& next_id,
254
266
) ;
255
267
256
- let page = proposal_id / MAX_PROPOSALS_PER_PAGE ;
257
268
let mut dao_page = Self :: get_dao ( env. clone ( ) , project_key. clone ( ) , page) ;
258
269
dao_page. proposals . push_back ( proposal. clone ( ) ) ;
259
270
@@ -359,6 +370,12 @@ impl DaoTrait for Tansu {
359
370
_ => panic_with_error ! ( & env, & errors:: ContractErrors :: NoProposalorPageFound ) ,
360
371
} ;
361
372
373
+ // Check that voting period has not ended
374
+ let curr_timestamp = env. ledger ( ) . timestamp ( ) ;
375
+ if curr_timestamp >= proposal. vote_data . voting_ends_at {
376
+ panic_with_error ! ( & env, & errors:: ContractErrors :: ProposalVotingTime ) ;
377
+ }
378
+
362
379
// Check vote limits for DoS protection
363
380
if proposal. vote_data . votes . len ( ) >= MAX_VOTES_PER_PROPOSAL {
364
381
panic_with_error ! ( & env, & errors:: ContractErrors :: VoteLimitExceeded ) ;
@@ -381,14 +398,16 @@ impl DaoTrait for Tansu {
381
398
panic_with_error ! ( & env, & errors:: ContractErrors :: WrongVoteType ) ;
382
399
}
383
400
384
- if !is_public_vote && let types:: Vote :: AnonymousVote ( vote_choice) = & vote {
385
- if vote_choice. commitments . len ( ) != 3 {
386
- panic_with_error ! ( & env, & errors:: ContractErrors :: BadCommitment )
387
- }
388
- for commitment in & vote_choice. commitments {
389
- G1Affine :: from_bytes ( commitment) ;
401
+ // For anonymous votes, validate commitment structure
402
+ if !is_public_vote
403
+ && let types:: Vote :: AnonymousVote ( vote_choice) = & vote {
404
+ if vote_choice. commitments . len ( ) != 3 {
405
+ panic_with_error ! ( & env, & errors:: ContractErrors :: BadCommitment )
406
+ }
407
+ for commitment in & vote_choice. commitments {
408
+ G1Affine :: from_bytes ( commitment) ;
409
+ }
390
410
}
391
- }
392
411
393
412
// can only vote for yourself so address must match
394
413
let vote_address = match & vote {
@@ -411,6 +430,10 @@ impl DaoTrait for Tansu {
411
430
vote_address. clone ( ) ,
412
431
) ;
413
432
433
+ if voter_max_weight == 0 {
434
+ panic_with_error ! ( & env, & errors:: ContractErrors :: UnknownMember ) ;
435
+ }
436
+
414
437
if vote_weight > & voter_max_weight {
415
438
panic_with_error ! ( & env, & errors:: ContractErrors :: VoterWeight ) ;
416
439
}
@@ -525,22 +548,28 @@ impl DaoTrait for Tansu {
525
548
// tally to results
526
549
proposal. status = match proposal. vote_data . public_voting {
527
550
true => {
528
- if tallies. is_some ( ) | seeds. is_some ( ) {
551
+ if tallies. is_some ( ) || seeds. is_some ( ) {
529
552
panic_with_error ! ( & env, & errors:: ContractErrors :: TallySeedError ) ;
530
553
}
531
554
public_execute ( & proposal)
532
555
}
533
556
false => {
534
- if !( tallies. is_some ( ) & seeds. is_some ( ) ) {
557
+ let ( tallies_, seeds_) = match ( tallies, seeds) {
558
+ ( Some ( t) , Some ( s) ) => ( t, s) ,
559
+ _ => panic_with_error ! ( & env, & errors:: ContractErrors :: TallySeedError ) ,
560
+ } ;
561
+
562
+ // Validate tallies and seeds have expected length (3: approve, reject, abstain)
563
+ if tallies_. len ( ) != 3 || seeds_. len ( ) != 3 {
535
564
panic_with_error ! ( & env, & errors:: ContractErrors :: TallySeedError ) ;
536
565
}
537
- let tallies_ = tallies . unwrap ( ) ;
566
+
538
567
if !Self :: proof (
539
568
env. clone ( ) ,
540
569
project_key. clone ( ) ,
541
570
proposal. clone ( ) ,
542
571
tallies_. clone ( ) ,
543
- seeds . unwrap ( ) ,
572
+ seeds_ ,
544
573
) {
545
574
panic_with_error ! ( & env, & errors:: ContractErrors :: InvalidProof )
546
575
}
@@ -604,7 +633,7 @@ impl DaoTrait for Tansu {
604
633
tallies : Vec < u128 > ,
605
634
seeds : Vec < u128 > ,
606
635
) -> bool {
607
- // only allow to proof if proposal is not active
636
+ // Proof validation only applies to active proposals (before execution)
608
637
if proposal. status != types:: ProposalStatus :: Active {
609
638
panic_with_error ! ( & env, & errors:: ContractErrors :: ProposalActive ) ;
610
639
}
@@ -620,7 +649,9 @@ impl DaoTrait for Tansu {
620
649
. storage ( )
621
650
. instance ( )
622
651
. get ( & types:: ProjectKey :: AnonymousVoteConfig ( project_key) )
623
- . unwrap ( ) ;
652
+ . unwrap_or_else ( || {
653
+ panic_with_error ! ( & env, & errors:: ContractErrors :: NoAnonymousVotingConfig ) ;
654
+ } ) ;
624
655
625
656
let seed_generator_point = G1Affine :: from_bytes ( vote_config. seed_generator_point ) ;
626
657
let vote_generator_point = G1Affine :: from_bytes ( vote_config. vote_generator_point ) ;
@@ -777,11 +808,16 @@ pub fn public_execute(proposal: &types::Proposal) -> types::ProposalStatus {
777
808
/// # Returns
778
809
/// * `types::ProposalStatus` - The final status (Approved if approve > reject, Rejected if reject > approve, Cancelled if equal)
779
810
pub fn anonymous_execute ( tallies : & Vec < u128 > ) -> types:: ProposalStatus {
780
- let mut iter = tallies. iter ( ) ;
781
-
782
- let voted_approve = iter. next ( ) . unwrap ( ) ;
783
- let voted_reject = iter. next ( ) . unwrap ( ) ;
784
- let voted_abstain = iter. next ( ) . unwrap ( ) ;
811
+ // Use get() method to access elements safely
812
+ let voted_approve = tallies
813
+ . get ( 0 )
814
+ . expect ( "anonymous_execute missing approve tally entry" ) ;
815
+ let voted_reject = tallies
816
+ . get ( 1 )
817
+ . expect ( "anonymous_execute missing reject tally entry" ) ;
818
+ let voted_abstain = tallies
819
+ . get ( 2 )
820
+ . expect ( "anonymous_execute missing abstain tally entry" ) ;
785
821
786
822
tallies_to_result ( voted_approve, voted_reject, voted_abstain)
787
823
}
@@ -804,10 +840,14 @@ fn tallies_to_result(
804
840
voted_reject : u128 ,
805
841
voted_abstain : u128 ,
806
842
) -> types:: ProposalStatus {
807
- // accept or reject if we have a majority
808
- if voted_approve > ( voted_abstain + voted_reject) {
843
+ // Supermajority governance: requires more than half of all votes (including abstains)
844
+ // This ensures broad consensus before passing any proposal
845
+ // Approve needs: approve > (reject + abstain)
846
+ // Reject needs: reject > (approve + abstain)
847
+ // Otherwise: cancelled (tie or no clear supermajority)
848
+ if voted_approve > ( voted_reject + voted_abstain) {
809
849
types:: ProposalStatus :: Approved
810
- } else if voted_reject > ( voted_abstain + voted_approve ) {
850
+ } else if voted_reject > ( voted_approve + voted_abstain ) {
811
851
types:: ProposalStatus :: Rejected
812
852
} else {
813
853
types:: ProposalStatus :: Cancelled
0 commit comments