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
304 changes: 112 additions & 192 deletions contracts/raffle/src/instance/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ fn verify_randomness_proof_internal(
seed: u64,
proof: &BytesN<64>,
) {
let message: Bytes = seed.to_xdr(env);
let message = Bytes::from_array(env, &seed.to_be_bytes());
// ed25519_verify traps on invalid signature, rejecting the randomness submit.
env.crypto().ed25519_verify(public_key, &message, proof);
}
Expand Down Expand Up @@ -250,6 +250,114 @@ fn require_not_paused(env: &Env) -> Result<(), Error> {
Ok(())
}

fn build_internal_seed(env: &Env) -> u64 {
let xdr = (
env.ledger().timestamp(),
env.ledger().sequence(),
env.current_contract_address(),
)
.to_xdr(env);
let hash: BytesN<32> = env.crypto().sha256(&xdr).into();

// Convert first 8 bytes of hash to u64
let mut bytes = [0u8; 8];
for i in 0..8 {
bytes[i] = hash.get(i as u32).unwrap();
}
u64::from_be_bytes(bytes)
}

fn do_finalize_with_seed(
env: &Env,
mut raffle: Raffle,
seed: u64,
randomness_type: RandomnessType,
) -> Result<(), Error> {
let total_tickets = get_ticket_count(env);
if total_tickets == 0 {
return Err(Error::NoTicketsSold);
}

let selector = OracleSeedWinnerSelection::new(seed);
let winning_ticket_ids =
selector.select_winner_indices(env, total_tickets, raffle.prizes.len() as u32);
let mut winners = Vec::new(env);

for i in 0..winning_ticket_ids.len() {
let winner_index = winning_ticket_ids.get(i).unwrap();
let ticket_id = winner_index + 1;
let winner = get_ticket_owner(env, ticket_id).ok_or(Error::TicketNotFound)?;
winners.push_back(winner.clone());

env.events().publish(
(
Symbol::new(env, "WinnerDrawn"),
winner.clone(),
winner_index,
),
WinnerDrawn {
winner: winner.clone(),
ticket_id: winner_index,
tier_index: i,
timestamp: env.ledger().timestamp(),
},
);
}

let mut claimed_winners = Vec::new(env);
for _ in 0..raffle.prizes.len() {
claimed_winners.push_back(false);
}

let fairness_metadata = FairnessMetadata {
seed,
randomness_source: raffle.randomness_source.clone(),
winning_ticket_indices: winning_ticket_ids.clone(),
draw_timestamp: env.ledger().timestamp(),
draw_sequence: env.ledger().sequence(),
};
env.storage()
.instance()
.set(&DataKey::RandomnessSeed, &fairness_metadata);

raffle.status = RaffleStatus::Finalized;
raffle.winners = winners.clone();
raffle.claimed_winners = claimed_winners;
raffle.finalized_at = Some(env.ledger().timestamp());
write_raffle(env, &raffle);

if !env.storage().persistent().has(&DataKey::FinishTime) {
env.storage()
.persistent()
.set(&DataKey::FinishTime, &env.ledger().timestamp());
}

publish_event(
env,
"raffle_finalized",
RaffleFinalized {
winners,
winning_ticket_ids,
total_tickets_sold: raffle.tickets_sold,
randomness_source: raffle.randomness_source.clone(),
randomness_type,
finalized_at: env.ledger().timestamp(),
},
);

publish_event(
env,
"status_changed",
RaffleStatusChanged {
old_status: RaffleStatus::Drawing,
new_status: RaffleStatus::Finalized,
timestamp: env.ledger().timestamp(),
},
);

Ok(())
}

fn read_raffle(env: &Env) -> Result<Raffle, Error> {
env.storage()
.instance()
Expand Down Expand Up @@ -716,10 +824,8 @@ impl Contract {
.ok_or(Error::OracleNotSet)?
.clone();

// Save the Drawing state so the transition is durable
write_raffle(&env, &raffle);

// Guard against duplicate requests
let already: bool = env
.storage()
.instance()
Expand All @@ -746,91 +852,8 @@ impl Contract {
return Ok(());
}

let total_tickets = get_ticket_count(&env);
let selector = PrngWinnerSelection::new(
env.ledger().timestamp(),
env.ledger().sequence(),
env.current_contract_address(),
raffle.tickets_sold,
);
let winning_ticket_ids =
selector.select_winner_indices(&env, total_tickets, raffle.prizes.len() as u32);
let mut winners = Vec::new(&env);

for i in 0..winning_ticket_ids.len() {
let winner_index = winning_ticket_ids.get(i).unwrap();
let ticket_id = winner_index + 1;
let winner = get_ticket_owner(&env, ticket_id).ok_or(Error::TicketNotFound)?;
winners.push_back(winner.clone());

env.events().publish(
(
Symbol::new(&env, "WinnerDrawn"),
winner.clone(),
winner_index,
),
WinnerDrawn {
winner: winner.clone(),
ticket_id: winner_index,
tier_index: i,
timestamp: env.ledger().timestamp(),
},
);
}

let mut claimed_winners = Vec::new(&env);
for _ in 0..raffle.prizes.len() {
claimed_winners.push_back(false);
}

// Store fairness metadata for transparency
let fairness_metadata = FairnessMetadata {
seed: selector.seed_fingerprint(&env),
randomness_source: raffle.randomness_source.clone(),
winning_ticket_indices: winning_ticket_ids.clone(),
draw_timestamp: env.ledger().timestamp(),
draw_sequence: env.ledger().sequence(),
};
env.storage()
.instance()
.set(&DataKey::RandomnessSeed, &fairness_metadata);

raffle.status = RaffleStatus::Finalized;
raffle.winners = winners.clone();
raffle.claimed_winners = claimed_winners;
raffle.finalized_at = Some(env.ledger().timestamp());
write_raffle(&env, &raffle);

if !env.storage().persistent().has(&DataKey::FinishTime) {
env.storage()
.persistent()
.set(&DataKey::FinishTime, &env.ledger().timestamp());
}

publish_event(
&env,
"raffle_finalized",
RaffleFinalized {
winners,
winning_ticket_ids,
total_tickets_sold: raffle.tickets_sold,
randomness_source: RandomnessSource::Internal,
randomness_type: RandomnessType::Prng,
finalized_at: env.ledger().timestamp(),
},
);

publish_event(
&env,
"status_changed",
RaffleStatusChanged {
old_status: RaffleStatus::Drawing,
new_status: RaffleStatus::Finalized,
timestamp: env.ledger().timestamp(),
},
);

Ok(())
let seed = build_internal_seed(&env);
do_finalize_with_seed(&env, raffle, seed, RandomnessType::Prng)
}

/// Explicitly request winner selection from the configured oracle.
Expand Down Expand Up @@ -938,7 +961,6 @@ impl Contract {
) -> Result<Address, Error> {
let mut raffle = read_raffle(&env)?;

// Verify the caller is the authorised oracle
let oracle = match &raffle.oracle_address {
Some(addr) => {
addr.require_auth();
Expand All @@ -947,15 +969,13 @@ impl Contract {
None => return Err(Error::OracleNotSet),
};

// State guards
if raffle.status != RaffleStatus::Drawing {
return Err(Error::InvalidStateTransition);
}
if raffle.randomness_source != RandomnessSource::External {
return Err(Error::InvalidStateTransition);
}

// Ensure a request was explicitly made — rejects unsolicited callbacks
let request_pending: bool = env
.storage()
.instance()
Expand All @@ -967,114 +987,14 @@ impl Contract {

verify_randomness_proof_internal(&env, &public_key, random_seed, &proof);

// Optimize: Use NextTicketId as count instead of loading all tickets into Vec
let total_tickets = get_ticket_count(&env);
if total_tickets == 0 {
return Err(Error::NoTicketsSold);
}

// Clear the pending request and its ledger timestamp before selecting winners
env.storage()
.instance()
.remove(&DataKey::RandomnessRequested);
env.storage()
.instance()
.remove(&DataKey::RandomnessRequestLedger);

let selector = OracleSeedWinnerSelection::new(random_seed);
let winning_ticket_ids =
selector.select_winner_indices(&env, total_tickets, raffle.prizes.len() as u32);
let mut winners = Vec::new(&env);

for i in 0..winning_ticket_ids.len() {
let winner_index = winning_ticket_ids.get(i).unwrap();
let ticket_id = winner_index + 1; // ticket IDs start at 1
let winner = get_ticket_owner(&env, ticket_id).ok_or(Error::TicketNotFound)?;
winners.push_back(winner.clone());

env.events().publish(
(
Symbol::new(&env, "WinnerDrawn"),
winner.clone(),
winner_index,
),
WinnerDrawn {
winner: winner.clone(),
ticket_id: winner_index,
tier_index: i,
timestamp: env.ledger().timestamp(),
},
);
}

if raffle.status != RaffleStatus::Drawing
|| raffle.randomness_source != RandomnessSource::External
{
return Err(Error::InvalidStateTransition);
}

let mut claimed_winners = Vec::new(&env);
for _ in 0..raffle.prizes.len() {
claimed_winners.push_back(false);
}

// Store fairness metadata for transparency
let fairness_metadata = FairnessMetadata {
seed: random_seed,
randomness_source: raffle.randomness_source.clone(),
winning_ticket_indices: winning_ticket_ids.clone(),
draw_timestamp: env.ledger().timestamp(),
draw_sequence: env.ledger().sequence(),
};
env.storage()
.instance()
.set(&DataKey::RandomnessSeed, &fairness_metadata);

raffle.status = RaffleStatus::Finalized;
raffle.winners = winners.clone();
raffle.claimed_winners = claimed_winners;
raffle.finalized_at = Some(env.ledger().timestamp());
write_raffle(&env, &raffle);

if !env.storage().persistent().has(&DataKey::FinishTime) {
env.storage()
.persistent()
.set(&DataKey::FinishTime, &env.ledger().timestamp());
}

publish_event(
&env,
"randomness_received",
RandomnessReceived {
oracle: oracle.clone(),
seed: random_seed,
timestamp: env.ledger().timestamp(),
},
);

publish_event(
&env,
"raffle_finalized",
RaffleFinalized {
winners: winners.clone(),
winning_ticket_ids,
total_tickets_sold: raffle.tickets_sold,
randomness_source: RandomnessSource::External,
randomness_type: RandomnessType::Vrf,
finalized_at: env.ledger().timestamp(),
},
);

publish_event(
&env,
"status_changed",
RaffleStatusChanged {
old_status: RaffleStatus::Drawing,
new_status: RaffleStatus::Finalized,
timestamp: env.ledger().timestamp(),
},
);

do_finalize_with_seed(&env, raffle, random_seed, RandomnessType::Vrf)?;
Ok(oracle)
}

Expand Down
Loading
Loading