Skip to content

ISSUE-CU05-002: Tests and Gas Report for Prize Distribution #545

Description

@davidmelendez

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:

  1. Precondition validation: Tests that verify correct rejection of distributions when the lottery is not finalized.
  2. Idempotency: Verify that distribution cannot be executed twice for the same lottery.
  3. Correct calculations: Validate that prizes are calculated and distributed correctly according to defined percentages for each level.
  4. Equitable division: Confirm that prizes are divided equitably among all winners of the same level.
  5. Event emission: Verify that all events are emitted with correct data.
  6. Levels without winners: Validate behavior when a level has no winners.
  7. State update: Confirm that storage fields are updated correctly.
  8. 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

  1. 🔹 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
  2. 🔹 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
  3. 🔹 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

  1. 🔹 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
  2. 🔹 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)
  3. 🔹 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
  4. 🔹 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

  1. 🔹 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)
  2. 🔹 Test: Distribution completion mark

    • Given: A finalized lottery with successful distribution
    • When: distribute_prizes completes
    • Then: lottery.distribution_done == true
  3. 🔹 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
  4. 🔹 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

  1. 🔹 Test: Single winner in a level

    • Given: Only 1 winner with 4 matches
    • When: Distribution occurs
    • Then: Receives the complete 60% of the pool
  2. 🔹 Test: All tickets win

    • Given: All sold tickets have at least 1 match
    • When: Distribution occurs
    • Then: All receive prize according to their level
  3. 🔹 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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions