Skip to content
Closed
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
7 changes: 7 additions & 0 deletions contracts/tholos-v2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,13 @@ impl TholosV2 {
position.revealed = true;
position.agrees_with_outcome = Some(agrees_with_asserter);
Self::set_position(env, id, fixed_voter, &position, &assertion.policy);

Revealed {
id,
voter: fixed_voter.clone(),
choice: agrees_with_asserter,
}
.publish(env);
}

Self::set_resolution(env, id, &resolution, &assertion.policy);
Expand Down
21 changes: 20 additions & 1 deletion contracts/tholos-v2/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

use super::*;
use soroban_sdk::testutils::storage::Persistent as _;
use soroban_sdk::testutils::{Address as _, Ledger};
use soroban_sdk::testutils::{Address as _, Events, Ledger};
use soroban_sdk::xdr;

const DEFAULT_BOND: i128 = 100;
const DEFAULT_CHALLENGE_WINDOW: u64 = 3600;
Expand Down Expand Up @@ -1444,6 +1445,24 @@ fn test_reveal_opens_phase_counts_fixed_positions_and_verifies_commitment() {
f.advance_past_registration_deadline(id);
f.client.reveal(&voter, &id, &true, &s);

// Auto-reveal of the asserter and disputer fixed positions must each emit
// a Revealed event, in addition to the explicit Revealed event from the
// external voter's reveal() call.
let contract_events = f.env.events().all();
let all_events = contract_events.events();
let mut revealed_count: u32 = 0;
for event in all_events.iter() {
let xdr::ContractEventBody::V0(event_data) = &event.body;
if event_data.topics.len() == 2 {
if let xdr::ScVal::Symbol(topic0) = &event_data.topics[0] {
if topic0.to_utf8_string().unwrap_or_default() == "revealed" {
revealed_count += 1;
}
}
}
}
assert_eq!(revealed_count, 3, "expected asserter, disputer, and voter Revealed events");

let assertion = f.client.get_assertion(&id);
assert_eq!(assertion.phase, PhaseV2::Reveal);

Expand Down
9 changes: 9 additions & 0 deletions contracts/tholos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ pub enum Error {
/// slot without any economic risk (they receive both bonds back regardless
/// of the resolver vote), nullifying the bond-forfeiture deterrent.
SelfDispute = 22,
/// A resolver attempted to vote on an assertion where they are also the
/// asserter or the disputer. Voting in their own case is a conflict of
/// interest and, in a size-1 committee, lets them guarantee the outcome.
SelfVote = 23,
}

const DAY_IN_LEDGERS: u32 = 17280;
Expand Down Expand Up @@ -777,6 +781,11 @@ impl Tholos {
return Err(Error::AlreadyVoted);
}

// A resolver may not vote on their own case.
if resolver == assertion.asserter || assertion.disputer.as_ref() == Some(&resolver) {
return Err(Error::SelfVote);
}

assertion.voted.push_back(resolver);
if agrees_with_asserter {
assertion.votes_for_outcome += 1;
Expand Down
68 changes: 68 additions & 0 deletions contracts/tholos/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,74 @@ fn test_resolve_records_asserted_outcome_when_asserter_wins() {
assert_eq!(f.client.get_assertion_state(&id).final_outcome, Some(false));
}

#[test]
fn test_asserter_resolver_cannot_vote_on_own_case() {
let env = Env::default();
env.mock_all_auths();

let (token_id, mut resolvers) = setup(&env);
let _token = token::Client::new(&env, &token_id);
let asserter = Address::generate(&env);
let disputer = Address::generate(&env);
// Make the asserter the first resolver so they are in the committee.
resolvers.set(0, asserter.clone());

let contract_id = env.register(Tholos, ());
let client = TholosClient::new(&env, &contract_id);
let admin = Address::generate(&env);
client.initialize(
&admin,
&token_id,
&DEFAULT_BOND,
&DEFAULT_WINDOW,
&resolvers,
&0u32,
);

token::StellarAssetClient::new(&env, &token_id).mint(&asserter, &DEFAULT_MINT);
token::StellarAssetClient::new(&env, &token_id).mint(&disputer, &DEFAULT_MINT);

let id = client.assert_outcome(&asserter, &true);
client.dispute(&disputer, &id);

let result = client.try_resolve(&asserter, &id, &true);
assert_eq!(result, Err(Ok(Error::SelfVote)));
}

#[test]
fn test_disputer_resolver_cannot_vote_on_own_case() {
let env = Env::default();
env.mock_all_auths();

let (token_id, mut resolvers) = setup(&env);
let _token = token::Client::new(&env, &token_id);
let asserter = Address::generate(&env);
let disputer = Address::generate(&env);
// Make the disputer the first resolver so they are in the committee.
resolvers.set(0, disputer.clone());

let contract_id = env.register(Tholos, ());
let client = TholosClient::new(&env, &contract_id);
let admin = Address::generate(&env);
client.initialize(
&admin,
&token_id,
&DEFAULT_BOND,
&DEFAULT_WINDOW,
&resolvers,
&0u32,
);

token::StellarAssetClient::new(&env, &token_id).mint(&asserter, &DEFAULT_MINT);
token::StellarAssetClient::new(&env, &token_id).mint(&disputer, &DEFAULT_MINT);

let id = client.assert_outcome(&asserter, &true);
client.dispute(&disputer, &id);

let result = client.try_resolve(&disputer, &id, &false);
assert_eq!(result, Err(Ok(Error::SelfVote)));
}

#[test]
fn test_cannot_initialize_with_even_resolver_count() {
let env = Env::default();
Expand Down
Loading