ISSUE-CU05-002: Tests and Gas Report for Prize Distribution
✨ Issue Request
Implement comprehensive test suite for the distribute_prizes function of the lottery contract, ensuring correct prize distribution, idempotency, error handling, and generate a basic gas report for the hackathon version.
📌 Description
This issue covers the creation of unit and integration tests to validate the correct functioning of the prize distribution system implemented in SL-CU05-001. It must guarantee:
- Precondition validation: Tests that verify correct rejection of distributions when the lottery is not finalized.
- Idempotency: Verify that distribution cannot be executed twice for the same lottery.
- Correct calculations: Validate that prizes are calculated and distributed correctly according to defined percentages for each level.
- Equitable division: Confirm that prizes are divided equitably among all winners of the same level.
- Event emission: Verify that all events are emitted with correct data.
- Levels without winners: Validate behavior when a level has no winners.
- State update: Confirm that storage fields are updated correctly.
- Gas reporting: Generate basic gas consumption metrics for different scenarios.
🛠️ Steps to Reproduce (if applicable)
Not applicable - this is a test implementation issue.
🖼️ Screenshots (if applicable)
Not applicable - smart contract test implementation.
🎯 Expected Behavior
The test suite must meet the following criteria:
Validation Tests
-
🔹 Test: LOTTERY_NOT_FINALIZED
- Given: A lottery in
InProgress or Pending state
- When: Attempting to call
distribute_prizes(lottery_id)
- Then: Transaction reverts with error
LOTTERY_NOT_FINALIZED
-
🔹 Test: ALREADY_DISTRIBUTED
- Given: A finalized lottery with
distribution_done == true
- When: Attempting to call
distribute_prizes(lottery_id) again
- Then: Transaction reverts with error
ALREADY_DISTRIBUTED
-
🔹 Test: NO_TICKETS
- Given: A finalized lottery with no sold tickets
- When: Calling
distribute_prizes(lottery_id)
- Then: Transaction reverts with error
NO_TICKETS
Calculation and Distribution Tests
-
🔹 Test: Equitable division per level
- Given: 10 winners with 1 match and 50 StarkPlay tokens assigned to level 1 (5% of 1000 SP pool)
- When:
distribute_prizes(lottery_id) is executed
- Then: Each winner receives exactly 5 SP
- And: 10
PrizeAssigned events are emitted with amount == 5 SP and level == 1
-
🔹 Test: Multiple levels with winners
- Given: A lottery with:
- 2 winners with 4 matches (60% = 600 SP)
- 5 winners with 3 matches (25% = 250 SP)
- 10 winners with 2 matches (10% = 100 SP)
- 20 winners with 1 match (5% = 50 SP)
- When: Prizes are distributed
- Then:
- Level 4: each winner receives 300 SP (600/2)
- Level 3: each winner receives 50 SP (250/5)
- Level 2: each winner receives 10 SP (100/10)
- Level 1: each winner receives 2.5 SP (50/20)
-
🔹 Test: Level without winners
- Given: A lottery where there are no winners with 4 matches
- When:
pool_for_level is calculated for level 4
- Then: No prizes are assigned at that level
- And: Other levels are calculated normally
-
🔹 Test: Correct match counting
- Given:
- Winning combination: [5, 12, 23, 30]
- Ticket 1: [5, 12, 23, 30] → 4 matches
- Ticket 2: [5, 12, 23, 45] → 3 matches
- Ticket 3: [5, 12, 88, 99] → 2 matches
- Ticket 4: [5, 77, 88, 99] → 1 match
- Ticket 5: [1, 2, 3, 4] → 0 matches
- When: Matches are counted
- Then: Each ticket is classified in the correct level
State and Event Tests
-
🔹 Test: State update per ticket
- Given: A winning ticket at any level
- When: Prizes are distributed
- Then:
ticket.prize_assigned == true
ticket.prize_amount > 0
ticket.claimed == false (claimed in CU-06)
-
🔹 Test: Distribution completion mark
- Given: A finalized lottery with successful distribution
- When:
distribute_prizes completes
- Then:
lottery.distribution_done == true
-
🔹 Test: PrizeAssigned events
- Given: A lottery with N winners
- When: Prizes are distributed
- Then: Exactly N
PrizeAssigned events are emitted
- And: Each event contains:
- Correct
lottery_id
- Correct
ticket_id
- Correct
level (1-4)
- Correct
amount
-
🔹 Test: PrizesDistributed event
- Given: A completed distribution
- When: Function finishes
- Then: A
PrizesDistributed event is emitted with:
- Correct
lottery_id
winners_total = total number of winners
total_distributed = sum of all assigned prizes
Edge Case Tests
-
🔹 Test: Single winner in a level
- Given: Only 1 winner with 4 matches
- When: Distribution occurs
- Then: Receives the complete 60% of the pool
-
🔹 Test: All tickets win
- Given: All sold tickets have at least 1 match
- When: Distribution occurs
- Then: All receive prize according to their level
-
🔹 Test: No tickets win
- Given: No tickets have matches
- When: Distribution occurs
- Then: No prizes are assigned but transaction completes successfully
🚀 Suggested Solution / Feature Request
1. Test File Structure
Create the following files in packages/snfoundry/contracts/tests/:
tests/
├── test_CU05.cairo
2. Main Tests Implementation
// test_distribute_prizes.cairo
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address,
stop_cheat_caller_address, spy_events, EventSpyAssertionsTrait
};
#[test]
fn test_distribute_prizes_success() {
// Setup
let (lottery_contract, lottery_id) = setup_finalized_lottery();
let tickets = create_test_tickets(lottery_id, 10);
// Act
lottery_contract.distribute_prizes(lottery_id);
// Assert
let lottery = lottery_contract.get_lottery(lottery_id);
assert!(lottery.distribution_done, "Distribution should be marked done");
// Verify prizes were assigned
for ticket_id in tickets {
let ticket = lottery_contract.get_ticket(ticket_id);
if ticket.matches > 0 {
assert!(ticket.prize_assigned, "Prize should be assigned");
assert!(ticket.prize_amount > 0, "Prize amount should be positive");
}
}
}
#[test]
#[should_panic(expected: ('LOTTERY_NOT_FINALIZED',))]
fn test_distribute_prizes_not_finalized() {
// Setup
let (lottery_contract, lottery_id) = setup_lottery_in_progress();
// Act - should panic
lottery_contract.distribute_prizes(lottery_id);
}
#[test]
#[should_panic(expected: ('ALREADY_DISTRIBUTED',))]
fn test_distribute_prizes_idempotency() {
// Setup
let (lottery_contract, lottery_id) = setup_finalized_lottery();
// First distribution - should succeed
lottery_contract.distribute_prizes(lottery_id);
// Second distribution - should panic
lottery_contract.distribute_prizes(lottery_id);
}
#[test]
#[should_panic(expected: ('NO_TICKETS',))]
fn test_distribute_prizes_no_tickets() {
// Setup
let (lottery_contract, lottery_id) = setup_finalized_lottery_no_tickets();
// Act - should panic
lottery_contract.distribute_prizes(lottery_id);
}
3. Prize Calculation Tests
// test_prize_calculations.cairo
#[test]
fn test_equal_distribution_per_level() {
// Setup: 10 winners with 1 match, pool = 1000 SP, level 1 = 5% = 50 SP
let (lottery_contract, lottery_id) = setup_lottery_with_level_1_winners(10, 1000);
// Act
lottery_contract.distribute_prizes(lottery_id);
// Assert: Each winner gets 50/10 = 5 SP
let winners = lottery_contract.get_winners_by_level(lottery_id, 1);
for winner_id in winners {
let ticket = lottery_contract.get_ticket(winner_id);
assert!(ticket.prize_amount == 5_u128, "Each winner should get 5 SP");
}
}
#[test]
fn test_multiple_levels_distribution() {
// Setup
let total_pool = 1000_u128;
let (lottery_contract, lottery_id) = setup_complex_lottery(total_pool);
// Create winners:
// - 2 tickets with 4 matches (level 4)
// - 5 tickets with 3 matches (level 3)
// - 10 tickets with 2 matches (level 2)
// - 20 tickets with 1 match (level 1)
create_winners_distribution(lottery_contract, lottery_id);
// Act
lottery_contract.distribute_prizes(lottery_id);
// Assert
// Level 4: 60% = 600 SP / 2 = 300 SP each
let level_4_winners = lottery_contract.get_winners_by_level(lottery_id, 4);
assert!(level_4_winners.len() == 2, "Should have 2 level 4 winners");
for winner_id in level_4_winners {
let ticket = lottery_contract.get_ticket(winner_id);
assert!(ticket.prize_amount == 300_u128, "Level 4 winner should get 300 SP");
}
// Level 3: 25% = 250 SP / 5 = 50 SP each
let level_3_winners = lottery_contract.get_winners_by_level(lottery_id, 3);
assert!(level_3_winners.len() == 5, "Should have 5 level 3 winners");
for winner_id in level_3_winners {
let ticket = lottery_contract.get_ticket(winner_id);
assert!(ticket.prize_amount == 50_u128, "Level 3 winner should get 50 SP");
}
// Level 2: 10% = 100 SP / 10 = 10 SP each
let level_2_winners = lottery_contract.get_winners_by_level(lottery_id, 2);
assert!(level_2_winners.len() == 10, "Should have 10 level 2 winners");
for winner_id in level_2_winners {
let ticket = lottery_contract.get_ticket(winner_id);
assert!(ticket.prize_amount == 10_u128, "Level 2 winner should get 10 SP");
}
// Level 1: 5% = 50 SP / 20 = 2.5 SP each (or 2 if using integer division)
let level_1_winners = lottery_contract.get_winners_by_level(lottery_id, 1);
assert!(level_1_winners.len() == 20, "Should have 20 level 1 winners");
}
#[test]
fn test_level_without_winners() {
// Setup: Only level 1, 2, 3 have winners, level 4 has none
let (lottery_contract, lottery_id) = setup_lottery_no_level_4_winners();
// Act
lottery_contract.distribute_prizes(lottery_id);
// Assert: Level 4 should have no prizes assigned
let level_4_winners = lottery_contract.get_winners_by_level(lottery_id, 4);
assert!(level_4_winners.len() == 0, "Level 4 should have no winners");
// Other levels should work normally
let level_3_winners = lottery_contract.get_winners_by_level(lottery_id, 3);
assert!(level_3_winners.len() > 0, "Level 3 should have winners");
}
4. Match Counting Tests
// test_match_counting.cairo
#[test]
fn test_count_matches_all_correct() {
let winning_numbers = array![5, 12, 23, 30].span();
let ticket_numbers = array![5, 12, 23, 30].span();
let matches = count_matches(ticket_numbers, winning_numbers);
assert!(matches == 4, "Should have 4 matches");
}
#[test]
fn test_count_matches_three_correct() {
let winning_numbers = array![5, 12, 23, 30].span();
let ticket_numbers = array![5, 12, 23, 45].span();
let matches = count_matches(ticket_numbers, winning_numbers);
assert!(matches == 3, "Should have 3 matches");
}
#[test]
fn test_count_matches_two_correct() {
let winning_numbers = array![5, 12, 23, 30].span();
let ticket_numbers = array![5, 12, 88, 99].span();
let matches = count_matches(ticket_numbers, winning_numbers);
assert!(matches == 2, "Should have 2 matches");
}
#[test]
fn test_count_matches_one_correct() {
let winning_numbers = array![5, 12, 23, 30].span();
let ticket_numbers = array![5, 77, 88, 99].span();
let matches = count_matches(ticket_numbers, winning_numbers);
assert!(matches == 1, "Should have 1 match");
}
#[test]
fn test_count_matches_none_correct() {
let winning_numbers = array![5, 12, 23, 30].span();
let ticket_numbers = array![1, 2, 3, 4].span();
let matches = count_matches(ticket_numbers, winning_numbers);
assert!(matches == 0, "Should have 0 matches");
}
5. Event Tests
#[test]
fn test_prize_assigned_events() {
// Setup
let (lottery_contract, lottery_id) = setup_finalized_lottery();
let mut spy = spy_events();
// Create 3 winners
let winners = create_test_winners(lottery_contract, lottery_id, 3);
// Act
lottery_contract.distribute_prizes(lottery_id);
// Assert: Should emit 3 PrizeAssigned events
spy.assert_emitted(@array![
(lottery_contract.contract_address,
Event::PrizeAssigned(PrizeAssigned {
lottery_id,
ticket_id: winners[0],
level: 1,
amount: /* expected amount */
}))
]);
// Verify event count
let events = spy.get_events();
let prize_assigned_count = count_events_by_type(events, "PrizeAssigned");
assert!(prize_assigned_count == 3, "Should emit 3 PrizeAssigned events");
}
#[test]
fn test_prizes_distributed_event() {
// Setup
let (lottery_contract, lottery_id) = setup_finalized_lottery();
let mut spy = spy_events();
let total_winners = 10_u32;
// Act
lottery_contract.distribute_prizes(lottery_id);
// Assert: Should emit PrizesDistributed event
spy.assert_emitted(@array![
(lottery_contract.contract_address,
Event::PrizesDistributed(PrizesDistributed {
lottery_id,
winners_total: total_winners,
total_distributed: /* sum of all prizes */
}))
]);
}
6. State Tests
#[test]
fn test_ticket_state_after_distribution() {
// Setup
let (lottery_contract, lottery_id) = setup_finalized_lottery();
let ticket_id = create_winning_ticket(lottery_contract, lottery_id);
// Act
lottery_contract.distribute_prizes(lottery_id);
// Assert
let ticket = lottery_contract.get_ticket(ticket_id);
assert!(ticket.prize_assigned == true, "prize_assigned should be true");
assert!(ticket.prize_amount > 0, "prize_amount should be positive");
assert!(ticket.claimed == false, "claimed should still be false");
}
#[test]
fn test_lottery_distribution_done_flag() {
// Setup
let (lottery_contract, lottery_id) = setup_finalized_lottery();
// Before distribution
let lottery_before = lottery_contract.get_lottery(lottery_id);
assert!(lottery_before.distribution_done == false, "Should not be done before");
// Act
lottery_contract.distribute_prizes(lottery_id);
// After distribution
let lottery_after = lottery_contract.get_lottery(lottery_id);
assert!(lottery_after.distribution_done == true, "Should be done after");
}
7. Helpers and Fixtures
// helpers/prize_distribution_fixtures.cairo
fn setup_finalized_lottery() -> (ILotteryDispatcher, u64) {
let contract = declare("Lottery").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
let lottery_contract = ILotteryDispatcher { contract_address };
// Create and finalize lottery
let lottery_id = lottery_contract.create_lottery(/* params */);
lottery_contract.finalize_lottery(lottery_id);
(lottery_contract, lottery_id)
}
fn create_test_tickets(
lottery_contract: ILotteryDispatcher,
lottery_id: u64,
count: u32
) -> Array<u64> {
let mut ticket_ids = array![];
for i in 0..count {
let ticket_id = lottery_contract.buy_ticket(
lottery_id,
generate_random_numbers()
);
ticket_ids.append(ticket_id);
}
ticket_ids
}
fn create_winners_distribution(
lottery_contract: ILotteryDispatcher,
lottery_id: u64
) {
let winning_numbers = array![5, 12, 23, 30];
// Create 2 tickets with 4 matches
for i in 0..2 {
lottery_contract.buy_ticket(lottery_id, winning_numbers.span());
}
// Create 5 tickets with 3 matches
for i in 0..5 {
lottery_contract.buy_ticket(lottery_id, array![5, 12, 23, 45].span());
}
// ... similar for other levels
}
8. Gas Report
Add at the end of the main test file or in a README:
// Gas consumption report (approximate values for hackathon demo)
#[test]
fn gas_report_distribute_prizes() {
// Test with 10 tickets
let gas_10 = measure_gas_distribute_prizes(10);
println!("Gas for 10 tickets: {}", gas_10);
// Test with 50 tickets
let gas_50 = measure_gas_distribute_prizes(50);
println!("Gas for 50 tickets: {}", gas_50);
// Test with 100 tickets
let gas_100 = measure_gas_distribute_prizes(100);
println!("Gas for 100 tickets: {}", gas_100);
// Generate report
println!("\n=== Gas Report ===");
println!("Distribution scales approximately linearly with ticket count");
println!("Average gas per ticket: ~{} gas", (gas_10 + gas_50 + gas_100) / 160);
}
9. Gas Report Documentation
Create a GAS_REPORT.md file in the contract directory:
# Gas Report - Prize Distribution (CU-05)
## Test Scenarios
| Scenario | Tickets | Winners | Gas Used | Gas/Ticket |
|----------|---------|---------|----------|------------|
| Small | 10 | 5 | ~XXX | ~XX |
| Medium | 50 | 25 | ~XXX | ~XX |
| Large | 100 | 50 | ~XXX | ~XX |
## Notes for Hackathon Version
- Distribution is performed in a single transaction
- Gas cost scales linearly with ticket count
- Future optimization: Batch processing or Merkle proof claims
## Optimization Opportunities
1. Implement pagination for large lotteries
2. Use Merkle trees for claim-based distribution
3. Cache intermediate calculations
📌 Additional Notes
Testing Framework
- Use Starknet Foundry (snforge) as testing framework
- Version: snforge 0.41.0 (according to CLAUDE.md)
- Run tests with:
yarn test or scarb test
Coverage Expectations
- Minimum expected coverage: 90% of distribution code lines
- All defined errors must have tests that trigger them
- All events must have emission tests
Self-Contained Fixtures
- Tests should not depend on external state
- Each test should set up its own initial state
- Use helpers to reduce code duplication
Gas Reporting
- Gas report is indicative for hackathon version
- Extreme optimization is not necessary at this stage
- Clearly document measurements for future reference
Dependencies
- Requires SL-CU05-001 to be implemented and compiling
- Requires access to Lottery and Ticket structures
- Requires events to be correctly defined
⚠️ Before Applying
Please read this guide: Contributor Guidelines
ISSUE-CU05-002: Tests and Gas Report for Prize Distribution
✨ Issue Request
Implement comprehensive test suite for the
distribute_prizesfunction of the lottery contract, ensuring correct prize distribution, idempotency, error handling, and generate a basic gas report for the hackathon version.📌 Description
This issue covers the creation of unit and integration tests to validate the correct functioning of the prize distribution system implemented in SL-CU05-001. It must guarantee:
🛠️ Steps to Reproduce (if applicable)
Not applicable - this is a test implementation issue.
🖼️ Screenshots (if applicable)
Not applicable - smart contract test implementation.
🎯 Expected Behavior
The test suite must meet the following criteria:
Validation Tests
🔹 Test: LOTTERY_NOT_FINALIZED
InProgressorPendingstatedistribute_prizes(lottery_id)LOTTERY_NOT_FINALIZED🔹 Test: ALREADY_DISTRIBUTED
distribution_done == truedistribute_prizes(lottery_id)againALREADY_DISTRIBUTED🔹 Test: NO_TICKETS
distribute_prizes(lottery_id)NO_TICKETSCalculation and Distribution Tests
🔹 Test: Equitable division per level
distribute_prizes(lottery_id)is executedPrizeAssignedevents are emitted withamount == 5 SPandlevel == 1🔹 Test: Multiple levels with winners
🔹 Test: Level without winners
pool_for_levelis calculated for level 4🔹 Test: Correct match counting
State and Event Tests
🔹 Test: State update per ticket
ticket.prize_assigned == trueticket.prize_amount > 0ticket.claimed == false(claimed in CU-06)🔹 Test: Distribution completion mark
distribute_prizescompleteslottery.distribution_done == true🔹 Test: PrizeAssigned events
PrizeAssignedevents are emittedlottery_idticket_idlevel(1-4)amount🔹 Test: PrizesDistributed event
PrizesDistributedevent is emitted with:lottery_idwinners_total= total number of winnerstotal_distributed= sum of all assigned prizesEdge Case Tests
🔹 Test: Single winner in a level
🔹 Test: All tickets win
🔹 Test: No tickets win
🚀 Suggested Solution / Feature Request
1. Test File Structure
Create the following files in
packages/snfoundry/contracts/tests/:2. Main Tests Implementation
3. Prize Calculation Tests
4. Match Counting Tests
5. Event Tests
6. State Tests
7. Helpers and Fixtures
8. Gas Report
Add at the end of the main test file or in a README:
9. Gas Report Documentation
Create a
GAS_REPORT.mdfile in the contract directory:📌 Additional Notes
Testing Framework
yarn testorscarb testCoverage Expectations
Self-Contained Fixtures
Gas Reporting
Dependencies
Please read this guide: Contributor Guidelines