From 828a9a9c5388bb5bd5a5d76f6d53ca402abcb07f Mon Sep 17 00:00:00 2001 From: David Melendez Date: Mon, 13 Oct 2025 12:49:23 -0600 Subject: [PATCH 1/3] Enhance ClaimPrize function in Lottery contract with reentrancy protection and improved validation checks. Added steps for draw existence verification, ticket ownership validation, and prize transfer logic. Updated event emission to reflect accurate prize amounts upon claim. Streamlined code for clarity and security. --- .../snfoundry/contracts/src/Lottery.cairo | 68 ++++++++++++------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/packages/snfoundry/contracts/src/Lottery.cairo b/packages/snfoundry/contracts/src/Lottery.cairo index 221f5447..ab81fd4e 100644 --- a/packages/snfoundry/contracts/src/Lottery.cairo +++ b/packages/snfoundry/contracts/src/Lottery.cairo @@ -657,41 +657,59 @@ pub mod Lottery { } //======================================================================================= fn ClaimPrize(ref self: ContractState, drawId: u64, ticketId: felt252) { - // Validate that draw exists + // 1. Start reentrancy protection + self.reentrancy_guard.start(); + + // 2. Validate that draw exists self.AssertDrawExists(drawId, 'ClaimPrize'); + // 3. Get draw and validate state let draw = self.draws.entry(drawId).read(); - let ticket = self.tickets.entry((drawId, ticketId)).read(); - assert(!ticket.claimed, 'Prize already claimed'); assert(!draw.isActive, 'Draw still active'); + assert(draw.distribution_done, 'Distribution not done'); - let matches = self - .CheckMatches( - drawId, - ticket.number1, - ticket.number2, - ticket.number3, - ticket.number4, - ticket.number5, - ); - let prize = self.GetFixedPrize(drawId, matches); + // 4. Get ticket and validate ownership and prize + let mut ticket = self.tickets.entry((drawId, ticketId)).read(); + let caller = get_caller_address(); + + assert(ticket.player == caller, 'Not ticket owner'); + assert(!ticket.claimed, 'Prize already claimed'); + assert(ticket.prize_assigned, 'No prize assigned'); + assert(ticket.prize_amount > 0, 'No prize amount'); + + // 5. Get contract addresses + let vault_address = self.strkPlayVaultContractAddress.read(); + let token_address = self.strkPlayContractAddress.read(); + + // 6. Transfer tokens from vault to player + let token_dispatcher = IERC20Dispatcher { + contract_address: token_address, + }; - let mut ticket = ticket; + // Note: transfer_from will revert if it fails (no need to check return value) + // The vault must have previously approved this contract + token_dispatcher.transfer_from( + vault_address, + caller, + ticket.prize_amount + ); + + // 7. Mark ticket as claimed ticket.claimed = true; self.tickets.entry((drawId, ticketId)).write(ticket); - if prize > 0 { - //TODO: We need to process the payment of the prize + // 8. Emit event with correct prize amount + self.emit( + PrizeClaimed { + drawId, + player: caller, + ticketId, + prizeAmount: ticket.prize_amount, + }, + ); - self - .emit( - PrizeClaimed { - drawId, player: ticket.player, ticketId, prizeAmount: prize, - }, - ); - } else { - self.emit(PrizeClaimed { drawId, player: ticket.player, ticketId, prizeAmount: 0 }); - } + // 9. Release reentrancy guard + self.reentrancy_guard.end(); } //======================================================================================= From c5c4ca28581bc593c3dddf7cff70df9a6f9f9202 Mon Sep 17 00:00:00 2001 From: David Melendez Date: Mon, 13 Oct 2025 13:53:00 -0600 Subject: [PATCH 2/3] Update deployed contracts and enhance Lottery functionality with new GetUserWinningTickets method. Refactor prize assignment to mark_as_prize for clarity. Adjust tests and deployment scripts to reflect these changes and improve role assignments for the Lottery contract. --- .../nextjs/contracts/deployedContracts.ts | 32 ++++++++++--- .../snfoundry/contracts/src/Lottery.cairo | 45 ++++++++++++++++--- .../contracts/src/StarkPlayERC20.cairo | 5 +-- .../snfoundry/contracts/tests/test_CU02.cairo | 16 +++---- packages/snfoundry/scripts-ts/deploy.ts | 5 +++ 5 files changed, 81 insertions(+), 22 deletions(-) diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts index 77c284a8..5a01a7b6 100644 --- a/packages/nextjs/contracts/deployedContracts.ts +++ b/packages/nextjs/contracts/deployedContracts.ts @@ -7,7 +7,7 @@ const deployedContracts = { devnet: { StarkPlayERC20: { address: - "0x5183c0e5ae2579f5c541c6c5d69d89d7d71b80fe583a90bb1e41c383adadbc1", + "0x67c92804c52bf84d814b5ca866e3e6b9ed21634b15bc64234d2ffc1458c8d47", abi: [ { type: "impl", @@ -234,7 +234,7 @@ const deployedContracts = { items: [ { type: "function", - name: "assign_prize_tokens", + name: "mark_as_prize", inputs: [ { name: "recipient", @@ -1185,11 +1185,11 @@ const deployedContracts = { }, ], classHash: - "0x1db26668046de7c00fed257f34a0dad314a19a1fc8cc6816381ab7defec22b0", + "0x6f7fbd824c9dcff98cf62eba37adcbb8698a9aef6e034181a91d5f0e508bb0f", }, StarkPlayVault: { address: - "0x621b858ef40bbc6a8716025f6be83e2334647069aaf10d98b567ea26414c691", + "0x5308b4f0dbca99d7692f951e4c30682a54acf1c2c49aad5d2f6853127bbbfc2", abi: [ { type: "impl", @@ -2077,7 +2077,7 @@ const deployedContracts = { }, Lottery: { address: - "0x6e9f2cc499c9a1ce19be05a0373e1a4f7f6f44400d37bffb128a37cef58d08f", + "0x4fc81e6d7f3b7b05f40547a96312287104b173c12218468caab4bcdf64c601a", abi: [ { type: "impl", @@ -2546,6 +2546,26 @@ const deployedContracts = { ], state_mutability: "external", }, + { + type: "function", + name: "GetUserWinningTickets", + inputs: [ + { + name: "drawId", + type: "core::integer::u64", + }, + { + name: "player", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [ + { + type: "core::array::Array::", + }, + ], + state_mutability: "view", + }, { type: "function", name: "GetUserTicketsCount", @@ -3479,7 +3499,7 @@ const deployedContracts = { }, ], classHash: - "0x7dbe308ba32779b910b9ab4400caf084dc545c476eb36be6f65f98307a6c088", + "0x72bd355d5c5afa59f8372d324cee47f5e7e4ae679c9de99ab07973a07412b27", }, }, } as const; diff --git a/packages/snfoundry/contracts/src/Lottery.cairo b/packages/snfoundry/contracts/src/Lottery.cairo index ab81fd4e..26895c8f 100644 --- a/packages/snfoundry/contracts/src/Lottery.cairo +++ b/packages/snfoundry/contracts/src/Lottery.cairo @@ -113,6 +113,9 @@ pub trait ILottery { fn GetUserTickets( ref self: TContractState, drawId: u64, player: ContractAddress, ) -> Array; + fn GetUserWinningTickets( + self: @TContractState, drawId: u64, player: ContractAddress, + ) -> Array; fn GetUserTicketsCount(self: @TContractState, drawId: u64, player: ContractAddress) -> u32; fn GetTicketInfo( self: @TContractState, drawId: u64, ticketId: felt252, player: ContractAddress, @@ -171,6 +174,7 @@ pub mod Lottery { Draw, ILottery, IRandomnessLotteryDispatcher, IRandomnessLotteryDispatcherTrait, JackpotEntry, Ticket, }; + use contracts::StarkPlayERC20::{IPrizeTokenDispatcher, IPrizeTokenDispatcherTrait}; // ownable component by openzeppelin component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); @@ -686,19 +690,23 @@ pub mod Lottery { contract_address: token_address, }; - // Note: transfer_from will revert if it fails (no need to check return value) - // The vault must have previously approved this contract token_dispatcher.transfer_from( vault_address, caller, ticket.prize_amount ); - // 7. Mark ticket as claimed + // 7. Mark transferred tokens as prize tokens + let prize_dispatcher = IPrizeTokenDispatcher { + contract_address: token_address, + }; + prize_dispatcher.mark_as_prize(caller, ticket.prize_amount); + + // 8. Mark ticket as claimed ticket.claimed = true; self.tickets.entry((drawId, ticketId)).write(ticket); - // 8. Emit event with correct prize amount + // 9. Emit event with correct prize amount self.emit( PrizeClaimed { drawId, @@ -708,7 +716,7 @@ pub mod Lottery { }, ); - // 9. Release reentrancy guard + // 10. Release reentrancy guard self.reentrancy_guard.end(); } @@ -969,6 +977,33 @@ pub mod Lottery { user_tickets_data } + //======================================================================================= + fn GetUserWinningTickets( + self: @ContractState, drawId: u64, player: ContractAddress, + ) -> Array { + // Validate that draw exists (need to create snapshot for immutable self) + + let draw = self.draws.entry(drawId).read(); + assert(draw.drawId > 0, 'Draw does not exist'); + + let ticket_ids = self.GetUserTicketIds(drawId, player); + let mut winning_tickets = ArrayTrait::new(); + let mut i: usize = 0; + + while i != ticket_ids.len() { + let ticket_id = *ticket_ids.at(i); + let ticket = self.tickets.entry((drawId, ticket_id)).read(); + + // Filter: prize_assigned=true AND prize_amount>0 AND NOT claimed + if ticket.prize_assigned && ticket.prize_amount > 0 && !ticket.claimed { + winning_tickets.append(ticket); + } + i += 1; + }; + + winning_tickets + } + //======================================================================================= fn GetTicketInfo( self: @ContractState, drawId: u64, ticketId: felt252, player: ContractAddress, diff --git a/packages/snfoundry/contracts/src/StarkPlayERC20.cairo b/packages/snfoundry/contracts/src/StarkPlayERC20.cairo index 26dbb79b..a1fa008c 100644 --- a/packages/snfoundry/contracts/src/StarkPlayERC20.cairo +++ b/packages/snfoundry/contracts/src/StarkPlayERC20.cairo @@ -37,7 +37,7 @@ pub trait IBurnable { #[starknet::interface] pub trait IPrizeToken { - fn assign_prize_tokens(ref self: TContractState, recipient: ContractAddress, amount: u256); + fn mark_as_prize(ref self: TContractState, recipient: ContractAddress, amount: u256); fn get_prize_balance(self: @TContractState, account: ContractAddress) -> u256; fn grant_prize_assigner_role(ref self: TContractState, assigner: ContractAddress); fn revoke_prize_assigner_role(ref self: TContractState, assigner: ContractAddress); @@ -328,12 +328,11 @@ pub mod StarkPlayERC20 { #[abi(embed_v0)] impl PrizeTokenImpl of IPrizeToken { - fn assign_prize_tokens(ref self: ContractState, recipient: ContractAddress, amount: u256) { + fn mark_as_prize(ref self: ContractState, recipient: ContractAddress, amount: u256) { self.pausable.assert_not_paused(); self.accesscontrol.assert_only_role(PRIZE_ASSIGNER_ROLE); let current_prize_balance = self.prize_balances.entry(recipient).read(); self.prize_balances.entry(recipient).write(current_prize_balance + amount); - self.erc20.mint(recipient, amount); self.emit(PrizeTokensAssigned { recipient, amount }); } diff --git a/packages/snfoundry/contracts/tests/test_CU02.cairo b/packages/snfoundry/contracts/tests/test_CU02.cairo index c1938178..2a16377c 100644 --- a/packages/snfoundry/contracts/tests/test_CU02.cairo +++ b/packages/snfoundry/contracts/tests/test_CU02.cairo @@ -111,26 +111,26 @@ fn deploy_vault_contract() -> (IStarkPlayVaultDispatcher, IMintableDispatcher) { starkplay_token.set_minter_allowance(vault_address, EXCEEDS_MINT_LIMIT().into() * 10); starkplay_token_burn.grant_burner_role(vault_address); stop_cheat_caller_address(starkplay_token.contract_address); - // โœ… VERIFICAR que el rol se asignรณ correctamente + // โœ… Verify that the role was assigned correctly let starkplay_access = IAccessControlDispatcher { contract_address: starkplay_token.contract_address, }; let burner_role = selector!("BURNER_ROLE"); assert(starkplay_access.has_role(burner_role, vault_address), 'Vault should have BURNER_ROLE'); - // ๐Ÿ† ASIGNAR PRIZE_ASSIGNER_ROLE al OWNER (no al vault) + // ๐Ÿ† Assign PRIZE_ASSIGNER_ROLE to OWNER (not to vault) let prize_dispatcher = IPrizeTokenDispatcher { contract_address: starkplay_address }; start_cheat_caller_address(prize_dispatcher.contract_address, OWNER()); prize_dispatcher.grant_prize_assigner_role(vault_address); stop_cheat_caller_address(prize_dispatcher.contract_address); - // ๐Ÿ† MINTEAR StarkPlay tokens a USER1 + // ๐Ÿ† Mint StarkPlay tokens to USER1 start_cheat_caller_address(starkplay_token.contract_address, vault_address); starkplay_token .mint(USER1(), 1000_000_000_000_000_000_000_u256); // 1000 tokens with 18 decimals - // ๐Ÿ† REGISTRAR esos tokens como premios usando assign_prize_tokens + // ๐Ÿ† Mark those tokens as prizes using mark_as_prize (without additional minting) prize_dispatcher - .assign_prize_tokens( + .mark_as_prize( USER1(), 1000_000_000_000_000_000_000_u256, ); // 1000 tokens with 18 decimals stop_cheat_caller_address(starkplay_token.contract_address); @@ -168,7 +168,7 @@ fn setup_user_balance( } fn setup_vault_strk_balance(vault_address: ContractAddress, amount: u256) { - // Set up vault with STRK balance usando OWNER() como en test_CU01.cairo + // Set up vault with STRK balance using OWNER() as in test_CU01.cairo let strk_token = IMintableDispatcher { contract_address: 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d .try_into() @@ -221,7 +221,7 @@ fn test_convert_to_strk_burn_limit_validation() { // Verify burn limit was set assert(vault.get_burn_limit() == small_burn_limit, 'Burn limit should be set'); - // Set up vault with STRK balance (para dar cambio) + // Set up vault with STRK balance (to provide change) setup_vault_strk_balance( vault.contract_address, 1000_000_000_000_000_000_000_u256, ); // 1000 tokens @@ -278,7 +278,7 @@ fn test_convert_to_strk_exceeds_burn_limit() { vault.setBurnLimit(burn_limit); stop_cheat_caller_address(vault.contract_address); - // Set up vault with STRK balance (para dar cambio) + // Set up vault with STRK balance (to provide change) setup_vault_strk_balance( vault.contract_address, 1000_000_000_000_000_000_000_u256, ); // 1000 tokens diff --git a/packages/snfoundry/scripts-ts/deploy.ts b/packages/snfoundry/scripts-ts/deploy.ts index e805e38b..1eba8f07 100644 --- a/packages/snfoundry/scripts-ts/deploy.ts +++ b/packages/snfoundry/scripts-ts/deploy.ts @@ -168,7 +168,12 @@ const deployScript = async (): Promise => { const burn_allowance = 1_000_000_000n * 1000000000000000000n; // 1B tokens with 18 decimals await starkPlayTokenContract.set_burner_allowance(starkPlayVaultAddress, burn_allowance); + // Owner (deployer) assigns PRIZE_ASSIGNER_ROLE to Lottery contract + console.log("Granting PRIZE_ASSIGNER_ROLE to Lottery..."); + await starkPlayTokenContract.grant_prize_assigner_role(lotteryAddress); + console.log("StarkPlayVault roles assigned successfully by owner"); + console.log("Lottery PRIZE_ASSIGNER_ROLE assigned successfully"); } catch (error) { console.error("Failed to assign vault roles:", error); throw new Error(`Vault role assignment failed: ${error}`); From 2379279a9a6ee4ee86faa67843e7b59a2cc5c488 Mon Sep 17 00:00:00 2001 From: David Melendez Date: Mon, 13 Oct 2025 13:56:57 -0600 Subject: [PATCH 3/3] run scarb fmt --- .../snfoundry/contracts/src/Lottery.cairo | 42 +++++++------------ .../snfoundry/contracts/tests/test_CU02.cairo | 4 +- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/packages/snfoundry/contracts/src/Lottery.cairo b/packages/snfoundry/contracts/src/Lottery.cairo index 26895c8f..55e2e4a6 100644 --- a/packages/snfoundry/contracts/src/Lottery.cairo +++ b/packages/snfoundry/contracts/src/Lottery.cairo @@ -157,6 +157,7 @@ pub trait ILottery { //======================================================================================= #[starknet::contract] pub mod Lottery { + use contracts::StarkPlayERC20::{IPrizeTokenDispatcher, IPrizeTokenDispatcherTrait}; use core::array::{Array, ArrayTrait}; use core::dict::{Felt252Dict, Felt252DictTrait}; use core::traits::TryInto; @@ -174,7 +175,6 @@ pub mod Lottery { Draw, ILottery, IRandomnessLotteryDispatcher, IRandomnessLotteryDispatcherTrait, JackpotEntry, Ticket, }; - use contracts::StarkPlayERC20::{IPrizeTokenDispatcher, IPrizeTokenDispatcherTrait}; // ownable component by openzeppelin component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); @@ -675,7 +675,7 @@ pub mod Lottery { // 4. Get ticket and validate ownership and prize let mut ticket = self.tickets.entry((drawId, ticketId)).read(); let caller = get_caller_address(); - + assert(ticket.player == caller, 'Not ticket owner'); assert(!ticket.claimed, 'Prize already claimed'); assert(ticket.prize_assigned, 'No prize assigned'); @@ -684,22 +684,14 @@ pub mod Lottery { // 5. Get contract addresses let vault_address = self.strkPlayVaultContractAddress.read(); let token_address = self.strkPlayContractAddress.read(); - + // 6. Transfer tokens from vault to player - let token_dispatcher = IERC20Dispatcher { - contract_address: token_address, - }; + let token_dispatcher = IERC20Dispatcher { contract_address: token_address }; - token_dispatcher.transfer_from( - vault_address, - caller, - ticket.prize_amount - ); + token_dispatcher.transfer_from(vault_address, caller, ticket.prize_amount); // 7. Mark transferred tokens as prize tokens - let prize_dispatcher = IPrizeTokenDispatcher { - contract_address: token_address, - }; + let prize_dispatcher = IPrizeTokenDispatcher { contract_address: token_address }; prize_dispatcher.mark_as_prize(caller, ticket.prize_amount); // 8. Mark ticket as claimed @@ -707,14 +699,12 @@ pub mod Lottery { self.tickets.entry((drawId, ticketId)).write(ticket); // 9. Emit event with correct prize amount - self.emit( - PrizeClaimed { - drawId, - player: caller, - ticketId, - prizeAmount: ticket.prize_amount, - }, - ); + self + .emit( + PrizeClaimed { + drawId, player: caller, ticketId, prizeAmount: ticket.prize_amount, + }, + ); // 10. Release reentrancy guard self.reentrancy_guard.end(); @@ -982,24 +972,24 @@ pub mod Lottery { self: @ContractState, drawId: u64, player: ContractAddress, ) -> Array { // Validate that draw exists (need to create snapshot for immutable self) - + let draw = self.draws.entry(drawId).read(); assert(draw.drawId > 0, 'Draw does not exist'); let ticket_ids = self.GetUserTicketIds(drawId, player); let mut winning_tickets = ArrayTrait::new(); let mut i: usize = 0; - + while i != ticket_ids.len() { let ticket_id = *ticket_ids.at(i); let ticket = self.tickets.entry((drawId, ticket_id)).read(); - + // Filter: prize_assigned=true AND prize_amount>0 AND NOT claimed if ticket.prize_assigned && ticket.prize_amount > 0 && !ticket.claimed { winning_tickets.append(ticket); } i += 1; - }; + } winning_tickets } diff --git a/packages/snfoundry/contracts/tests/test_CU02.cairo b/packages/snfoundry/contracts/tests/test_CU02.cairo index 2a16377c..8cbe4121 100644 --- a/packages/snfoundry/contracts/tests/test_CU02.cairo +++ b/packages/snfoundry/contracts/tests/test_CU02.cairo @@ -130,9 +130,7 @@ fn deploy_vault_contract() -> (IStarkPlayVaultDispatcher, IMintableDispatcher) { // ๐Ÿ† Mark those tokens as prizes using mark_as_prize (without additional minting) prize_dispatcher - .mark_as_prize( - USER1(), 1000_000_000_000_000_000_000_u256, - ); // 1000 tokens with 18 decimals + .mark_as_prize(USER1(), 1000_000_000_000_000_000_000_u256); // 1000 tokens with 18 decimals stop_cheat_caller_address(starkplay_token.contract_address); start_cheat_caller_address(starkplay_token.contract_address, OWNER()); // Set a large allowance for the vault to mint and burn tokens