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 221f5447..55e2e4a6 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, @@ -154,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; @@ -657,41 +661,53 @@ 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(); - let mut ticket = ticket; + 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 }; + + 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 }; + prize_dispatcher.mark_as_prize(caller, ticket.prize_amount); + + // 8. 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 + // 9. 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 }); - } + // 10. Release reentrancy guard + self.reentrancy_guard.end(); } //======================================================================================= @@ -951,6 +967,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..8cbe4121 100644 --- a/packages/snfoundry/contracts/tests/test_CU02.cairo +++ b/packages/snfoundry/contracts/tests/test_CU02.cairo @@ -111,28 +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( - 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 @@ -168,7 +166,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 +219,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 +276,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}`);