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
16 changes: 16 additions & 0 deletions contracts/raffle-instance/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,19 @@ pub struct AdminChanged {
pub changed_by: Address,
pub timestamp: u64,
}

/// Emitted once per ticket after an NFT receipt is successfully minted
/// by the configured `nft_contract`.
#[derive(Clone)]
#[contractevent]
pub struct TicketNftMinted {
/// The address that received the NFT (the ticket buyer).
pub recipient: Address,
/// The ticket ID within this raffle (1-indexed).
pub ticket_id: u32,
/// The raffle instance contract address (NFT namespace).
pub raffle_id: Address,
/// The NFT contract that performed the mint.
pub nft_contract: Address,
pub timestamp: u64,
}
95 changes: 70 additions & 25 deletions contracts/raffle-instance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ use crate::events::{
OracleAddressUpdated, PrizeClaimed, PrizeDeposited, PrizeRefunded, ProtocolFeeUpdated,
RaffleCancelled, RaffleCreated, RaffleFailed, RaffleFinalized, RaffleStatusChanged,
RandomnessFallbackTriggered, RandomnessReceived, RandomnessRequested, SwapDeadlineUpdated,
TicketPurchased, TicketRefunded, TicketSalesPaused, TicketSalesResumed, TokensRescued,
WinnerDrawn,
TicketNftMinted, TicketPurchased, TicketRefunded, TicketSalesPaused, TicketSalesResumed,
TokensRescued, WinnerDrawn,
};

const ORACLE_TIMEOUT_LEDGERS: u32 = 200;
Expand Down Expand Up @@ -66,6 +66,9 @@ pub struct Raffle {
pub allow_multiple: bool,
pub ticket_price: i128,
pub payment_token: Address,
/// The token used for prize deposit and claims.
/// Defaults to `payment_token` when not explicitly set by the creator.
pub prize_token: Address,
pub prize_amount: i128,
pub prizes: Vec<u32>,
pub tickets_sold: u32,
Expand Down Expand Up @@ -208,9 +211,6 @@ fn require_admin(env: &Env) -> Result<Address, Error> {
Ok(admin)
}

/// Maximum protocol fee in basis points (20%) for per-raffle admin updates.
pub const MAX_PROTOCOL_FEE_BP: u32 = 2_000;

fn get_ticket_owner(env: &Env, ticket_id: u32) -> Option<Address> {
env.storage()
.persistent()
Expand Down Expand Up @@ -427,7 +427,7 @@ fn calculate_tier_prize(raffle: &Raffle, tier_index: u32) -> Result<i128, Error>
}

#[contractimpl]
impl Contract {
impl RaffleInstance {
pub fn init(
env: Env,
factory: Address,
Expand Down Expand Up @@ -513,6 +513,19 @@ impl Contract {
// Validate that the payment_token is a valid token contract
validate_token_address(&env, &config.payment_token)?;

// Validate prize_token if it differs from payment_token.
if let Some(ref pt) = config.prize_token {
if *pt != config.payment_token {
validate_token_address(&env, pt)?;
}
}

// Resolve the prize token: use the explicit override, or fall back to payment_token.
let prize_token = config
.prize_token
.clone()
.unwrap_or_else(|| config.payment_token.clone());

// Resolve default values for fields that use 0 as "use default"
let config = config.resolve_defaults();

Expand Down Expand Up @@ -545,6 +558,7 @@ impl Contract {
allow_multiple: config.allow_multiple,
ticket_price: config.ticket_price,
payment_token: config.payment_token.clone(),
prize_token: prize_token.clone(),
prize_amount: config.prize_amount,
prizes: config.prizes.clone(),
tickets_sold: 0,
Expand Down Expand Up @@ -603,7 +617,7 @@ impl Contract {

// Move tokens first. If the transfer fails we want the contract state
// (prize_deposited flag, raffle.status) to remain untouched.
let token_client = token::Client::new(&env, &raffle.payment_token);
let token_client = token::Client::new(&env, &raffle.prize_token);
let contract_address = env.current_contract_address();

let _ = token_client
Expand Down Expand Up @@ -878,8 +892,8 @@ impl Contract {
}

TicketPurchased {
buyer,
ticket_ids,
buyer: buyer.clone(),
ticket_ids: ticket_ids.clone(),
quantity,
ticket_price: raffle.ticket_price,
effective_ticket_price: effective_price,
Expand All @@ -889,6 +903,26 @@ impl Contract {
}
.publish(&env);

// NFT minting: issue an on-chain NFT receipt for each ticket purchased.
// This is best-effort — a failing NFT contract panics the whole call, so
// the NFT contract is assumed to be trusted and correctly implemented.
if let Some(ref nft_addr) = raffle.nft_contract {
let nft_client = NftTicketClient::new(&env, nft_addr);
let raffle_id = env.current_contract_address();
for i in 0..ticket_ids.len() {
let tid = ticket_ids.get(i).unwrap();
nft_client.mint(&buyer, &tid, &raffle_id);
TicketNftMinted {
recipient: buyer.clone(),
ticket_id: tid,
raffle_id: raffle_id.clone(),
nft_contract: nft_addr.clone(),
timestamp,
}
.publish(&env);
}
}

Ok(raffle.tickets_sold)
}

Expand Down Expand Up @@ -1202,15 +1236,15 @@ impl Contract {
}
write_raffle(&env, &raffle);

let token_client = token::Client::new(&env, &raffle.payment_token);
let token_client = token::Client::new(&env, &raffle.prize_token);
let _ = token_client
.try_transfer(&env.current_contract_address(), &winner, &amount)
.map_err(|_| Error::TokenTransferFailed)?;

PrizeClaimed {
winner,
tier_index,
payment_token: raffle.payment_token.clone(),
payment_token: raffle.prize_token.clone(),
gross_amount: amount,
net_amount: amount,
platform_fee: 0,
Expand Down Expand Up @@ -1361,7 +1395,7 @@ impl Contract {
PrizeRefunded {
creator: raffle.creator.clone(),
amount: raffle.prize_amount,
token: raffle.payment_token.clone(),
token: raffle.prize_token.clone(),
timestamp: env.ledger().timestamp(),
}
.publish(&env);
Expand Down Expand Up @@ -1423,7 +1457,7 @@ impl Contract {
raffle.status = RaffleStatus::Cancelled;
write_raffle(&env, &raffle);

let token_client = token::Client::new(&env, &raffle.payment_token);
let token_client = token::Client::new(&env, &raffle.prize_token);
token_client.transfer(
&env.current_contract_address(),
&raffle.creator,
Expand All @@ -1434,7 +1468,7 @@ impl Contract {
withdrawn_by: caller,
to: raffle.creator.clone(),
amount: raffle.prize_amount,
token: raffle.payment_token.clone(),
token: raffle.prize_token.clone(),
timestamp: env.ledger().timestamp(),
}
.publish(&env);
Expand Down Expand Up @@ -1785,10 +1819,13 @@ impl Contract {
return Err(Error::InvalidParameters);
}

// Protect active escrow: block sweeping the raffle payment token while
// the prize is deposited (i.e. the escrow is live).
// Protect active escrow: block sweeping the prize token while the prize
// is deposited. Also block the payment token if it equals the prize token
// to prevent draining the fee pool via a mis-directed rescue.
if let Ok(raffle) = read_raffle(&env) {
if token == raffle.payment_token && raffle.prize_deposited {
if raffle.prize_deposited
&& (token == raffle.prize_token || token == raffle.payment_token)
{
return Err(Error::InvalidParameters);
}
}
Expand Down Expand Up @@ -1939,8 +1976,8 @@ mod test {
env.mock_all_auths();
env.ledger().set_timestamp(1_000);

let contract_id = env.register(Contract, ());
let client = ContractClient::new(&env, &contract_id);
let contract_id = env.register(RaffleInstance, ());
let client = RaffleInstanceClient::new(&env, &contract_id);

// Players
let factory = env.register(MockFactory, ());
Expand Down Expand Up @@ -2219,8 +2256,8 @@ mod test {
env.mock_all_auths();
env.ledger().set_timestamp(1_000);

let contract_id = env.register(Contract, ());
let client = ContractClient::new(&env, &contract_id);
let contract_id = env.register(RaffleInstance, ());
let client = RaffleInstanceClient::new(&env, &contract_id);

let factory = env.register(MockFactory, ());
let admin = Address::generate(&env);
Expand Down Expand Up @@ -2255,6 +2292,8 @@ mod test {
metadata_hash: BytesN::from_array(&env, &[9u8; 32]),
claim_lockup_seconds: 0,
swap_deadline_seconds: 0,
prize_token: None,
nft_contract: None,
};

client.init(&factory, &admin, &creator, &config);
Expand Down Expand Up @@ -2309,8 +2348,8 @@ mod test {
env.mock_all_auths();
env.ledger().set_timestamp(1_000);

let contract_id = env.register(Contract, ());
let client = ContractClient::new(&env, &contract_id);
let contract_id = env.register(RaffleInstance, ());
let client = RaffleInstanceClient::new(&env, &contract_id);

let factory = env.register(MockFactory, ());
let admin = Address::generate(&env);
Expand Down Expand Up @@ -2342,6 +2381,8 @@ mod test {
metadata_hash: BytesN::from_array(&env, &[3u8; 32]),
claim_lockup_seconds: 0,
swap_deadline_seconds: 0,
prize_token: None,
nft_contract: None,
};

client.init(&factory, &admin, &creator, &config);
Expand Down Expand Up @@ -2373,8 +2414,8 @@ mod test {
env.mock_all_auths();
env.ledger().set_timestamp(1_000);

let contract_id = env.register(Contract, ());
let client = ContractClient::new(&env, &contract_id);
let contract_id = env.register(RaffleInstance, ());
let client = RaffleInstanceClient::new(&env, &contract_id);

let factory = env.register(MockFactory, ());
let admin = Address::generate(&env);
Expand Down Expand Up @@ -2407,6 +2448,8 @@ mod test {
metadata_hash: BytesN::from_array(&env, &[4u8; 32]),
claim_lockup_seconds: 0,
swap_deadline_seconds: 0,
prize_token: None,
nft_contract: None,
};

client.init(&factory, &admin, &creator, &config);
Expand Down Expand Up @@ -2466,6 +2509,8 @@ mod test {
metadata_hash: BytesN::from_array(env, &[5u8; 32]),
claim_lockup_seconds: 0,
swap_deadline_seconds: 0,
prize_token: None,
nft_contract: None,
};

client.init(&factory, &admin, &creator, &config);
Expand Down
22 changes: 12 additions & 10 deletions contracts/raffle-instance/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ fn test_oracle_fallback_with_ledger_delays() {
let token_client = StellarAssetClient::new(&env, &payment_token);
token_client.mint(&creator, &100_000_000);

let contract_id = env.register(Contract, ());
let client = ContractClient::new(&env, &contract_id);
let contract_id = env.register(RaffleInstance, ());
let client = RaffleInstanceClient::new(&env, &contract_id);

// 2. Initialize Raffle with External Randomness
let config = RaffleConfig {
Expand Down Expand Up @@ -122,8 +122,8 @@ fn test_admin_updates_oracle_address() {
let oracle = Address::generate(&env);
let new_oracle = Address::generate(&env);

let contract_id = env.register(Contract, ());
let client = ContractClient::new(&env, &contract_id);
let contract_id = env.register(RaffleInstance, ());
let client = RaffleInstanceClient::new(&env, &contract_id);

let config = RaffleConfig {
description: String::from_str(&env, "Oracle migration"),
Expand Down Expand Up @@ -171,8 +171,8 @@ fn non_winner_cannot_claim() {
env.mock_all_auths();
env.ledger().set_timestamp(1_000);

let contract_id = env.register(Contract, ());
let client = ContractClient::new(&env, &contract_id);
let contract_id = env.register(RaffleInstance, ());
let client = RaffleInstanceClient::new(&env, &contract_id);

let config = RaffleConfig {
description: String::from_str(&env, "Fee update"),
Expand Down Expand Up @@ -547,8 +547,8 @@ fn test_refund_ticket_after_cancel() {
let token_client = StellarAssetClient::new(&env, &payment_token);
token_client.mint(&creator, &1_000_000);

let contract_id = env.register(Contract, ());
let client = ContractClient::new(&env, &contract_id);
let contract_id = env.register(RaffleInstance, ());
let client = RaffleInstanceClient::new(&env, &contract_id);

let config = RaffleConfig {
description: String::from_str(&env, "Test"),
Expand Down Expand Up @@ -650,8 +650,8 @@ fn emergency_withdraw_fails_for_no_deadline_raffle_before_timeout() {
let token_client = StellarAssetClient::new(&env, &payment_token);
token_client.mint(&creator, &1_000_000);

let contract_id = env.register(Contract, ());
let client = ContractClient::new(&env, &contract_id);
let contract_id = env.register(RaffleInstance, ());
let client = RaffleInstanceClient::new(&env, &contract_id);

let end_time = 5_000u64;
let config = RaffleConfig {
Expand Down Expand Up @@ -860,6 +860,8 @@ fn emergency_withdraw_fails_if_prize_not_deposited() {
metadata_hash: BytesN::from_array(&env, &[5; 32]),
claim_lockup_seconds: 0,
swap_deadline_seconds: 0,
prize_token: None,
nft_contract: None,
};

client.init(&factory, &admin, &creator, &config);
Expand Down
Loading