Skip to content

Commit 2107eaf

Browse files
committed
fix: Enhance contract validation and error handling for voting and proposal processes
1 parent fdeb7d0 commit 2107eaf

File tree

9 files changed

+264
-71
lines changed

9 files changed

+264
-71
lines changed

contracts/tansu/src/contract_dao.rs

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ impl DaoTrait for Tansu {
116116
votes: Vec<u128>,
117117
seeds: Vec<u128>,
118118
) -> 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+
119124
let vote_config = Self::get_anonymous_voting_config(env.clone(), project_key);
120125

121126
let bls12_381 = env.crypto().bls12_381();
@@ -248,12 +253,18 @@ impl DaoTrait for Tansu {
248253
};
249254

250255
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+
251263
env.storage().persistent().set(
252264
&types::ProjectKey::DaoTotalProposals(project_key.clone()),
253265
&next_id,
254266
);
255267

256-
let page = proposal_id / MAX_PROPOSALS_PER_PAGE;
257268
let mut dao_page = Self::get_dao(env.clone(), project_key.clone(), page);
258269
dao_page.proposals.push_back(proposal.clone());
259270

@@ -359,6 +370,12 @@ impl DaoTrait for Tansu {
359370
_ => panic_with_error!(&env, &errors::ContractErrors::NoProposalorPageFound),
360371
};
361372

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+
362379
// Check vote limits for DoS protection
363380
if proposal.vote_data.votes.len() >= MAX_VOTES_PER_PROPOSAL {
364381
panic_with_error!(&env, &errors::ContractErrors::VoteLimitExceeded);
@@ -381,14 +398,16 @@ impl DaoTrait for Tansu {
381398
panic_with_error!(&env, &errors::ContractErrors::WrongVoteType);
382399
}
383400

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+
}
390410
}
391-
}
392411

393412
// can only vote for yourself so address must match
394413
let vote_address = match &vote {
@@ -411,6 +430,10 @@ impl DaoTrait for Tansu {
411430
vote_address.clone(),
412431
);
413432

433+
if voter_max_weight == 0 {
434+
panic_with_error!(&env, &errors::ContractErrors::UnknownMember);
435+
}
436+
414437
if vote_weight > &voter_max_weight {
415438
panic_with_error!(&env, &errors::ContractErrors::VoterWeight);
416439
}
@@ -525,22 +548,28 @@ impl DaoTrait for Tansu {
525548
// tally to results
526549
proposal.status = match proposal.vote_data.public_voting {
527550
true => {
528-
if tallies.is_some() | seeds.is_some() {
551+
if tallies.is_some() || seeds.is_some() {
529552
panic_with_error!(&env, &errors::ContractErrors::TallySeedError);
530553
}
531554
public_execute(&proposal)
532555
}
533556
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 {
535564
panic_with_error!(&env, &errors::ContractErrors::TallySeedError);
536565
}
537-
let tallies_ = tallies.unwrap();
566+
538567
if !Self::proof(
539568
env.clone(),
540569
project_key.clone(),
541570
proposal.clone(),
542571
tallies_.clone(),
543-
seeds.unwrap(),
572+
seeds_,
544573
) {
545574
panic_with_error!(&env, &errors::ContractErrors::InvalidProof)
546575
}
@@ -604,7 +633,7 @@ impl DaoTrait for Tansu {
604633
tallies: Vec<u128>,
605634
seeds: Vec<u128>,
606635
) -> bool {
607-
// only allow to proof if proposal is not active
636+
// Proof validation only applies to active proposals (before execution)
608637
if proposal.status != types::ProposalStatus::Active {
609638
panic_with_error!(&env, &errors::ContractErrors::ProposalActive);
610639
}
@@ -620,7 +649,9 @@ impl DaoTrait for Tansu {
620649
.storage()
621650
.instance()
622651
.get(&types::ProjectKey::AnonymousVoteConfig(project_key))
623-
.unwrap();
652+
.unwrap_or_else(|| {
653+
panic_with_error!(&env, &errors::ContractErrors::NoAnonymousVotingConfig);
654+
});
624655

625656
let seed_generator_point = G1Affine::from_bytes(vote_config.seed_generator_point);
626657
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 {
777808
/// # Returns
778809
/// * `types::ProposalStatus` - The final status (Approved if approve > reject, Rejected if reject > approve, Cancelled if equal)
779810
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");
785821

786822
tallies_to_result(voted_approve, voted_reject, voted_abstain)
787823
}
@@ -804,10 +840,14 @@ fn tallies_to_result(
804840
voted_reject: u128,
805841
voted_abstain: u128,
806842
) -> 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) {
809849
types::ProposalStatus::Approved
810-
} else if voted_reject > (voted_abstain + voted_approve) {
850+
} else if voted_reject > (voted_approve + voted_abstain) {
811851
types::ProposalStatus::Rejected
812852
} else {
813853
types::ProposalStatus::Cancelled

contracts/tansu/src/contract_membership.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,13 @@ impl MembershipTrait for Tansu {
101101
// a project
102102
'member_projects_badges: {
103103
for i in 0..member_.projects.len() {
104-
if member_.projects.get(i).unwrap().project == key {
105-
let mut project_badges = member_.projects.get(i).unwrap().clone();
106-
project_badges.badges = badges.clone();
107-
member_.projects.set(i, project_badges);
108-
break 'member_projects_badges;
109-
}
104+
if let Some(project_badge) = member_.projects.get(i)
105+
&& project_badge.project == key {
106+
let mut project_badges = project_badge.clone();
107+
project_badges.badges = badges.clone();
108+
member_.projects.set(i, project_badges);
109+
break 'member_projects_badges;
110+
}
110111
}
111112
let project_badges = types::ProjectBadges {
112113
project: key.clone(),

contracts/tansu/src/contract_tansu.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ impl TansuTrait for Tansu {
8080
env.storage()
8181
.instance()
8282
.get(&types::DataKey::AdminsConfig)
83-
.unwrap()
83+
.unwrap_or_else(|| {
84+
panic_with_error!(&env, &crate::errors::ContractErrors::UnexpectedError);
85+
})
8486
}
8587

8688
/// Set the Soroban Domain contract.
@@ -164,6 +166,11 @@ impl TansuTrait for Tansu {
164166
let admins_config =
165167
new_admins_config.unwrap_or_else(|| Self::get_admins_config(env.clone()));
166168

169+
// Validate that threshold is reasonable
170+
if admins_config.threshold == 0 || admins_config.threshold > admins_config.admins.len() {
171+
panic_with_error!(&env, &crate::errors::ContractErrors::UpgradeError);
172+
}
173+
167174
let upgrade_proposal = types::UpgradeProposal {
168175
wasm_hash: new_wasm_hash.clone(),
169176
executable_at,

contracts/tansu/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,7 @@ fn validate_contract(env: &Env, contract: &types::Contract) {
195195
let contract_executable = contract.address.executable();
196196

197197
if let Some(wasm_hash) = contract.clone().wasm_hash {
198-
if let Some(wasm_hash) = contract.clone().wasm_hash
199-
&& contract_executable != Some(Executable::Wasm(wasm_hash)) {
198+
if contract_executable != Some(Executable::Wasm(wasm_hash)) {
200199
panic_with_error!(&env, &errors::ContractErrors::ContractValidation)
201200
}
202201
}

contracts/tansu/src/tests/test_commit.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,72 @@ fn commit_unregistered_maintainer_error() {
5656
.unwrap();
5757
assert_eq!(err, ContractErrors::UnauthorizedSigner.into());
5858
}
59+
60+
#[test]
61+
fn test_anonymous_vote_commitment_validation() {
62+
let setup = create_test_data();
63+
let id = init_contract(&setup);
64+
65+
// Setup anonymous voting first
66+
setup.contract.anonymous_voting_setup(
67+
&setup.grogu,
68+
&id,
69+
&String::from_str(&setup.env, "test_public_key"),
70+
);
71+
72+
// Test mismatched votes and seeds length
73+
let result = setup.contract.try_build_commitments_from_votes(
74+
&id,
75+
&vec![&setup.env, 1u128, 2u128], // 2 votes
76+
&vec![&setup.env, 1u128], // 1 seed - mismatch!
77+
);
78+
79+
assert_eq!(
80+
result,
81+
Err(Ok(soroban_sdk::Error::from_contract_error(
82+
ContractErrors::TallySeedError as u32
83+
)))
84+
);
85+
}
86+
87+
/// Test malformed inputs for build_commitments_from_votes with mismatched lengths
88+
#[test]
89+
fn test_malformed_commitments_mismatched_lengths() {
90+
let setup = create_test_data();
91+
let id = init_contract(&setup);
92+
93+
// Setup anonymous voting
94+
setup.contract.anonymous_voting_setup(
95+
&setup.grogu,
96+
&id,
97+
&String::from_str(&setup.env, "test_public_key"),
98+
);
99+
100+
// Test mismatched votes and seeds length - votes longer
101+
let result = setup.contract.try_build_commitments_from_votes(
102+
&id,
103+
&vec![&setup.env, 1u128, 2u128, 3u128, 4u128], // 4 votes
104+
&vec![&setup.env, 1u128, 2u128, 3u128], // 3 seeds - mismatch!
105+
);
106+
107+
assert_eq!(
108+
result,
109+
Err(Ok(soroban_sdk::Error::from_contract_error(
110+
ContractErrors::TallySeedError as u32
111+
)))
112+
);
113+
114+
// Test mismatched votes and seeds length - seeds longer
115+
let result2 = setup.contract.try_build_commitments_from_votes(
116+
&id,
117+
&vec![&setup.env, 1u128, 2u128], // 2 votes
118+
&vec![&setup.env, 1u128, 2u128, 3u128], // 3 seeds - mismatch!
119+
);
120+
121+
assert_eq!(
122+
result2,
123+
Err(Ok(soroban_sdk::Error::from_contract_error(
124+
ContractErrors::TallySeedError as u32
125+
)))
126+
);
127+
}

contracts/tansu/src/tests/test_dao.rs

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -116,39 +116,39 @@ fn proposal_flow() {
116116
.contract
117117
.execute(&setup.mando, &id, &proposal_id, &None, &None);
118118

119-
// // Verify proposal executed event
120-
// let mut all_events = setup.env.events().all();
119+
// Verify proposal executed event
120+
let mut all_events = setup.env.events().all();
121121
all_events.pop_front();
122122
all_events.pop_front();
123123
all_events.pop_front();
124-
// assert_eq!(
125-
// all_events,
126-
// vec![
127-
// &setup.env,
128-
// (
129-
// setup.contract_id.clone(),
130-
// (Symbol::new(&setup.env, "proposal_executed"), id.clone()).into_val(&setup.env),
131-
// Map::<Symbol, Val>::from_array(
132-
// &setup.env,
133-
// [
134-
// (
135-
// Symbol::new(&setup.env, "proposal_id"),
136-
// proposal_id.into_val(&setup.env)
137-
// ),
138-
// (
139-
// Symbol::new(&setup.env, "status"),
140-
// String::from_str(&setup.env, "Cancelled").into_val(&setup.env)
141-
// ),
142-
// (
143-
// Symbol::new(&setup.env, "maintainer"),
144-
// setup.mando.clone().into_val(&setup.env)
145-
// ),
146-
// ],
147-
// )
148-
// .into_val(&setup.env),
149-
// ),
150-
// ]
151-
// );
124+
assert_eq!(
125+
all_events,
126+
vec![
127+
&setup.env,
128+
(
129+
setup.contract_id.clone(),
130+
(Symbol::new(&setup.env, "proposal_executed"), id.clone()).into_val(&setup.env),
131+
Map::<Symbol, Val>::from_array(
132+
&setup.env,
133+
[
134+
(
135+
Symbol::new(&setup.env, "proposal_id"),
136+
proposal_id.into_val(&setup.env)
137+
),
138+
(
139+
Symbol::new(&setup.env, "status"),
140+
String::from_str(&setup.env, "Cancelled").into_val(&setup.env)
141+
),
142+
(
143+
Symbol::new(&setup.env, "maintainer"),
144+
setup.mando.clone().into_val(&setup.env)
145+
),
146+
],
147+
)
148+
.into_val(&setup.env),
149+
),
150+
]
151+
);
152152

153153
assert_eq!(result, ProposalStatus::Cancelled);
154154

@@ -748,3 +748,4 @@ fn voter_weight_validation() {
748748
assert_eq!(public_vote.vote_choice, VoteChoice::Approve);
749749
}
750750
}
751+

0 commit comments

Comments
 (0)