Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 72 additions & 10 deletions contracts/crowdfund_registry/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ use crate::error::CrowdfundError;
use crate::events::{
CampaignApproved, CampaignCancelled, CampaignCreated, CampaignFailed, CampaignFunded,
CampaignRejected, CampaignSubmittedForReview, CampaignTerminated, CampaignValidated,
DisputeResolved, MilestoneApproved, MilestoneDisputed, MilestoneOverdue,
CampaignVoteRejected, DisputeResolved, MilestoneApproved, MilestoneDisputed, MilestoneOverdue,
MilestoneRevisionRequested, MilestoneSubmitted, PledgeRecorded, RefundBatchProcessed,
};
use crate::storage::{
Campaign, CampaignStatus, CrowdfundDataKey, CrowdfundMilestoneStatus, DisputeResolution,
Milestone, VoteContext,
Milestone, VoteContext, VoteOption, VotingSession,
};
use boundless_types::ttl::{
INSTANCE_TTL_EXTEND, INSTANCE_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND, PERSISTENT_TTL_THRESHOLD,
Expand Down Expand Up @@ -441,17 +441,79 @@ impl CrowdfundRegistry {
.ok_or(CrowdfundError::NoVoteSession)?;

let gov_addr = Self::get_gov_addr(&env);
let args: Vec<Val> = Vec::from_array(&env, [session_id.into_val(&env)]);
let reached: bool = env.invoke_contract(&gov_addr, &sym(&env, "threshold_reached"), args);

if !reached {
return Err(CrowdfundError::VoteThresholdNotMet);
}
// Check if vote threshold has been reached
let threshold_args: Vec<Val> =
Vec::from_array(&env, [session_id.clone().into_val(&env)]);
let reached: bool = env.invoke_contract(
&gov_addr,
&sym(&env, "threshold_reached"),
threshold_args,
);

campaign.status = CampaignStatus::Campaigning;
env.storage().persistent().set(&key, &campaign);
if reached {
// Threshold reached — check which option won
let approve_args: Vec<Val> = Vec::from_array(
&env,
[
session_id.clone().into_val(&env),
0u32.into_val(&env), // option 0 = Approve
],
);
let reject_args: Vec<Val> = Vec::from_array(
&env,
[
session_id.into_val(&env),
1u32.into_val(&env), // option 1 = Reject
],
);

let approve_option: VoteOption = env.invoke_contract(
&gov_addr,
&sym(&env, "get_option"),
approve_args,
);
let reject_option: VoteOption = env.invoke_contract(
&gov_addr,
&sym(&env, "get_option"),
reject_args,
);

let approve_votes = approve_option.votes;
let reject_votes = reject_option.votes;

if approve_votes > reject_votes {
campaign.status = CampaignStatus::Campaigning;
env.storage().persistent().set(&key, &campaign);
CampaignValidated { id: campaign_id }.publish(&env);
} else {
campaign.status = CampaignStatus::Draft;
campaign.vote_session_id = None;
env.storage().persistent().set(&key, &campaign);
CampaignVoteRejected { id: campaign_id }.publish(&env);
}
} else {
// Threshold not reached — check if voting period has expired
let session_args: Vec<Val> =
Vec::from_array(&env, [session_id.into_val(&env)]);
let session: VotingSession = env.invoke_contract(
&gov_addr,
&sym(&env, "get_session"),
session_args,
);

if env.ledger().timestamp() <= session.end_at {
// Voting still open, threshold not met yet
return Err(CrowdfundError::VoteThresholdNotMet);
}

// Voting expired without reaching threshold — reject
campaign.status = CampaignStatus::Draft;
campaign.vote_session_id = None;
env.storage().persistent().set(&key, &campaign);
CampaignVoteRejected { id: campaign_id }.publish(&env);
}

CampaignValidated { id: campaign_id }.publish(&env);
Ok(())
}

Expand Down
7 changes: 7 additions & 0 deletions contracts/crowdfund_registry/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,13 @@ pub struct MilestoneRevisionRequested {
pub milestone_id: u32,
}

#[contractevent]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CampaignVoteRejected {
#[topic]
pub id: u64,
}

#[contractevent]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DisputeResolved {
Expand Down
37 changes: 36 additions & 1 deletion contracts/crowdfund_registry/src/storage/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use soroban_sdk::{contracttype, Address, BytesN, String};

// Local copy of governance_voting VoteContext for cross-contract serialization.
// Local copies of governance_voting types for cross-contract serialization.
#[contracttype]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum VoteContext {
Expand All @@ -10,6 +10,41 @@ pub enum VoteContext {
HackathonJudging,
}

#[contracttype]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum VoteStatus {
Pending,
Active,
Concluded,
Cancelled,
}

#[contracttype]
#[derive(Clone, Debug)]
pub struct VoteOption {
pub id: u32,
pub label: String,
pub votes: u32,
pub weighted_votes: u64,
}

#[contracttype]
#[derive(Clone, Debug)]
pub struct VotingSession {
pub session_id: BytesN<32>,
pub context: VoteContext,
pub module_id: u64,
pub created_at: u64,
pub start_at: u64,
pub end_at: u64,
pub status: VoteStatus,
pub threshold: Option<u32>,
pub threshold_reached: bool,
pub total_votes: u32,
pub quorum: Option<u32>,
pub weight_by_reputation: bool,
}

#[contracttype]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum CampaignStatus {
Expand Down
109 changes: 109 additions & 0 deletions contracts/crowdfund_registry/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -580,3 +580,112 @@ fn test_resolve_dispute_not_disputed_fails() {
.try_resolve_dispute(&cid, &0, &DisputeResolution::ApproveCreator);
assert!(result.is_err());
}

#[test]
fn test_vote_reject_returns_to_draft() {
let t = setup();
let owner = t.admin.clone();

let cid = t.client.create_campaign(
&owner,
&String::from_str(&t.env, "Vote reject"),
&10000i128,
&t.token_addr,
&(t.env.ledger().timestamp() + 86400),
&make_milestones(&t.env),
&100i128,
&false,
);

t.client.submit_for_review(&cid);

// Admin approves → creates vote session (threshold=1)
t.client.approve_campaign(&cid, &1000, &1);
assert_eq!(
t.client.get_campaign(&cid).status,
CampaignStatus::Submitted
);

// Voter votes "Reject" (option 1)
let voter = Address::generate(&t.env);
t.client.vote_campaign(&voter, &cid, &1);

// Check threshold → should reject back to Draft
t.client.check_vote_threshold(&cid);

let campaign = t.client.get_campaign(&cid);
assert_eq!(campaign.status, CampaignStatus::Draft);
assert!(campaign.vote_session_id.is_none());
}

#[test]
fn test_vote_expired_without_quorum_returns_to_draft() {
let t = setup();
let owner = t.admin.clone();

let cid = t.client.create_campaign(
&owner,
&String::from_str(&t.env, "Vote expire"),
&10000i128,
&t.token_addr,
&(t.env.ledger().timestamp() + 86400),
&make_milestones(&t.env),
&100i128,
&false,
);

t.client.submit_for_review(&cid);

// Admin approves → creates vote session (threshold=5, duration=1000)
t.client.approve_campaign(&cid, &1000, &5);
assert_eq!(
t.client.get_campaign(&cid).status,
CampaignStatus::Submitted
);

// Only 1 vote cast (threshold is 5), so threshold not reached
let voter = Address::generate(&t.env);
t.client.vote_campaign(&voter, &cid, &0);

// Advance past voting deadline
t.env.ledger().with_mut(|l| {
l.timestamp += 1001;
});

// Check threshold → voting expired, should reject back to Draft
t.client.check_vote_threshold(&cid);

let campaign = t.client.get_campaign(&cid);
assert_eq!(campaign.status, CampaignStatus::Draft);
assert!(campaign.vote_session_id.is_none());
}

#[test]
fn test_vote_threshold_not_met_while_active() {
let t = setup();
let owner = t.admin.clone();

let cid = t.client.create_campaign(
&owner,
&String::from_str(&t.env, "Still voting"),
&10000i128,
&t.token_addr,
&(t.env.ledger().timestamp() + 86400),
&make_milestones(&t.env),
&100i128,
&false,
);

t.client.submit_for_review(&cid);
t.client.approve_campaign(&cid, &1000, &5);

// No votes yet, voting still active → should error
let result = t.client.try_check_vote_threshold(&cid);
assert!(result.is_err());

// Campaign stays in Submitted
assert_eq!(
t.client.get_campaign(&cid).status,
CampaignStatus::Submitted
);
}
Loading