Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
111 changes: 111 additions & 0 deletions contracts/predictify-hybrid/src/disputes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1415,6 +1415,21 @@ impl DisputeManager {
// Require authentication from the user
user.require_auth();

// Reject self-vote: the dispute opener cannot vote on their own dispute
let market = MarketStateManager::get_market(env, &market_id)?;
if market.dispute_stakes.contains_key(user.clone()) {
crate::events::EventEmitter::emit_dispute_vote_rejected(
env,
&dispute_id,
&user,
&soroban_sdk::String::from_str(
env,
"Dispute opener cannot vote on their own dispute",
),
);
return Err(Error::DisputerCannotVote);
}

// Validate dispute voting conditions
DisputeValidator::validate_dispute_voting_conditions(env, &market_id, &dispute_id)?;

Expand Down Expand Up @@ -3245,4 +3260,100 @@ mod tests {
assert_eq!(analytics.is_expired, false);
assert_eq!(analytics.status, DisputeTimeoutStatus::Active);
}

#[test]
fn test_disputer_cannot_vote_on_own_dispute() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(crate::PredictifyHybrid, ());
let admin = Address::generate(&env);
crate::PredictifyHybridClient::new(&env, &contract_id)
.initialize(&admin, &Some(200i128), &None);

let market_id = Symbol::new(&env, "market_self_vote");
let disputer = Address::generate(&env);

env.as_contract(&contract_id, || {
let mut market = create_test_market(&env, env.ledger().timestamp().saturating_sub(1));
market.oracle_result = Some(String::from_str(&env, "yes"));
market.dispute_stakes.set(disputer.clone(), 1000);
MarketStateManager::update_market(&env, &market_id, &market);

let voting = DisputeVoting {
dispute_id: market_id.clone(),
voting_start: 0,
voting_end: env.ledger().timestamp() + 86400,
total_votes: 0,
support_votes: 0,
against_votes: 0,
total_support_stake: 0,
total_against_stake: 0,
status: DisputeVotingStatus::Active,
};
DisputeUtils::store_dispute_voting(&env, &market_id, &voting).unwrap();
});

let result = env.as_contract(&contract_id, || {
DisputeManager::vote_on_dispute(
&env,
disputer.clone(),
market_id.clone(),
market_id.clone(),
false,
500,
Some(String::from_str(&env, "Voting against own dispute")),
)
});

assert_eq!(result, Err(Error::DisputerCannotVote));
}

#[test]
fn test_non_disputer_can_pass_self_vote_check() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(crate::PredictifyHybrid, ());
let admin = Address::generate(&env);
crate::PredictifyHybridClient::new(&env, &contract_id)
.initialize(&admin, &Some(200i128), &None);

let market_id = Symbol::new(&env, "market_non_disputer");
let disputer = Address::generate(&env);
let voter = Address::generate(&env);

env.as_contract(&contract_id, || {
let mut market = create_test_market(&env, env.ledger().timestamp().saturating_sub(1));
market.oracle_result = Some(String::from_str(&env, "yes"));
market.dispute_stakes.set(disputer.clone(), 1000);
MarketStateManager::update_market(&env, &market_id, &market);

let voting = DisputeVoting {
dispute_id: market_id.clone(),
voting_start: 0,
voting_end: env.ledger().timestamp() + 86400,
total_votes: 0,
support_votes: 0,
against_votes: 0,
total_support_stake: 0,
total_against_stake: 0,
status: DisputeVotingStatus::Active,
};
DisputeUtils::store_dispute_voting(&env, &market_id, &voting).unwrap();
});

let result = env.as_contract(&contract_id, || {
DisputeManager::vote_on_dispute(
&env,
voter.clone(),
market_id.clone(),
market_id.clone(),
false,
500,
Some(String::from_str(&env, "Voting against")),
)
});

// Should NOT be DisputerCannotVote (though it will likely fail on token transfer)
assert_ne!(result, Err(Error::DisputerCannotVote));
}
}
4 changes: 4 additions & 0 deletions contracts/predictify-hybrid/src/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ pub enum Error {
DisputeFeeFailed = 409,
/// Generic dispute subsystem error. Check dispute state and configuration.
DisputeError = 410,
/// The dispute opener cannot vote on their own dispute.
DisputerCannotVote = 419,
/// Unclaimed winnings have already been swept for this market. Repeat sweeps are not allowed.
SweepAlreadyDone = 411,
/// Fee arithmetic overflowed during checked platform-fee calculation.
Expand Down Expand Up @@ -1380,6 +1382,7 @@ impl Error {
Error::DisputeCondNotMet => "Dispute resolution conditions not met",
Error::DisputeFeeFailed => "Dispute fee distribution failed",
Error::DisputeError => "Generic dispute subsystem error",
Error::DisputerCannotVote => "Dispute opener cannot vote on their own dispute",
Error::SweepAlreadyDone => "Unclaimed winnings already swept for this market",
Error::FeeArithmeticOverflow => "Fee arithmetic overflowed",
Error::FeeAlreadyCollected => "Platform fee already collected",
Expand Down Expand Up @@ -1474,6 +1477,7 @@ impl Error {
Error::DisputeCondNotMet => "DISPUTE_RESOLUTION_CONDITIONS_NOT_MET",
Error::DisputeFeeFailed => "DISPUTE_FEE_DISTRIBUTION_FAILED",
Error::DisputeError => "DISPUTE_ERROR",
Error::DisputerCannotVote => "DISPUTER_CANNOT_VOTE",
Error::SweepAlreadyDone => "SWEEP_ALREADY_DONE",
Error::FeeArithmeticOverflow => "FEE_ARITHMETIC_OVERFLOW",
Error::FeeAlreadyCollected => "FEE_ALREADY_COLLECTED",
Expand Down
37 changes: 37 additions & 0 deletions contracts/predictify-hybrid/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1254,6 +1254,24 @@ pub struct DisputeTimeoutExtendedEvent {
pub timestamp: u64,
}

/// Event emitted when a vote on a dispute is rejected because the voter
/// is the same address that opened the dispute.
///
/// This prevents the dispute opener from biasing the tally by voting
/// on their own dispute.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DisputeVoteRejectedEvent {
/// Dispute ID
pub dispute_id: Symbol,
/// Voter address (same as the dispute opener)
pub voter: Address,
/// Reason for rejection
pub reason: String,
/// Rejection timestamp
pub timestamp: u64,
}

/// Dispute auto-resolved event
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
Expand Down Expand Up @@ -2844,6 +2862,25 @@ impl EventEmitter {
.publish((symbol_short!("tout_ext"), dispute_id.clone()), event);
}

/// Emit dispute vote rejected event
pub fn emit_dispute_vote_rejected(
env: &Env,
dispute_id: &Symbol,
voter: &Address,
reason: &String,
) {
let event = DisputeVoteRejectedEvent {
dispute_id: dispute_id.clone(),
voter: voter.clone(),
reason: reason.clone(),
timestamp: env.ledger().timestamp(),
};

Self::store_event(env, &symbol_short!("d_v_rej"), &event);
env.events()
.publish((symbol_short!("d_v_rej"), dispute_id.clone()), event);
}

/// Emit dispute auto-resolved event
pub fn emit_dispute_auto_resolved(
env: &Env,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,7 @@ fn test_set_event_bet_limits_forged_admin_rejected() {
#[test]
fn test_set_oracle_val_cfg_global_authorized_admin_succeeds() {
let (env, cid, admin) = setup();
let result = client(&env, &cid).try_set_oracle_val_cfg_global(&admin, &300u64, &9500u32);
let result = client(&env, &cid).try_set_oracle_val_cfg_global(&admin, &300u64, &9500u32, &None::<u32>);
assert_auth_ok_contract!(result, "set_oracle_val_cfg_global rejected authorized admin");
}

Expand All @@ -1014,7 +1014,7 @@ fn test_set_oracle_val_cfg_global_authorized_admin_succeeds() {
fn test_set_oracle_val_cfg_global_forged_admin_rejected() {
let (env, cid, _admin) = setup();
let attacker = Address::generate(&env);
let result = client(&env, &cid).try_set_oracle_val_cfg_global(&attacker, &300u64, &9500u32);
let result = client(&env, &cid).try_set_oracle_val_cfg_global(&attacker, &300u64, &9500u32, &None::<u32>);
assert_unauthorized_contract!(result);
}

Expand All @@ -1026,7 +1026,7 @@ fn test_set_oracle_val_cfg_event_authorized_admin_succeeds() {
let (env, cid, admin) = setup();
let market_id = make_market(&env, &cid, &admin);
let result = client(&env, &cid).try_set_oracle_val_cfg_event(
&admin, &market_id, &300u64, &9500u32,
&admin, &market_id, &300u64, &9500u32, &None::<u32>,
);
assert_auth_ok_contract!(result, "set_oracle_val_cfg_event rejected authorized admin");
}
Expand All @@ -1038,7 +1038,7 @@ fn test_set_oracle_val_cfg_event_forged_admin_rejected() {
let market_id = make_market(&env, &cid, &admin);
let attacker = Address::generate(&env);
let result = client(&env, &cid).try_set_oracle_val_cfg_event(
&attacker, &market_id, &300u64, &9500u32,
&attacker, &market_id, &300u64, &9500u32, &None::<u32>,
);
assert_unauthorized_contract!(result);
}
Expand Down
Loading