Skip to content

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

@davidmelendez

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

    No fields configured for Task.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions