From d67dc296bf1bd8133656244a7a2bc22d9b70dd23 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 9 Oct 2025 20:44:19 +0300 Subject: [PATCH 01/39] Enhance RandomLottery: transfer invocation rewards on ticket purchase failures --- src/contracts/RandomLottery.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 7a1a109ad..58a0ccf81 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -368,6 +368,8 @@ struct RL : public ContractBase if (state.players.contains(qpi.invocator())) { output.returnCode = static_cast(EReturnCode::TICKET_ALREADY_PURCHASED); + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; } @@ -375,6 +377,8 @@ struct RL : public ContractBase if (state.players.add(qpi.invocator()) == NULL_INDEX) { output.returnCode = static_cast(EReturnCode::TICKET_ALL_SOLD_OUT); + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; } From a7cc0b212ab15c903f88547539eabe7da2313c5a Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 15 Oct 2025 19:51:41 +0300 Subject: [PATCH 02/39] Fixes increment winnersInfoNextEmptyIndex --- src/contracts/RandomLottery.h | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 58a0ccf81..448cf0c96 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -404,10 +404,9 @@ struct RL : public ContractBase { return; } - if (RL_MAX_NUMBER_OF_WINNERS_IN_HISTORY >= state.winners.capacity() - 1) - { - state.winnersInfoNextEmptyIndex = 0; - } + + state.winnersInfoNextEmptyIndex = mod(state.winnersInfoNextEmptyIndex, state.winners.capacity()); + locals.winnerInfo.winnerAddress = input.winnerAddress; locals.winnerInfo.revenue = input.revenue; locals.winnerInfo.epoch = qpi.epoch(); From 4dbf07ddd7a25dd45d979123f5a8f25f16a4933d Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 22 Oct 2025 19:13:43 +0300 Subject: [PATCH 03/39] Removes the limit on buying one ticket --- src/contracts/RandomLottery.h | 170 +++++++++++++++++++--------------- 1 file changed, 95 insertions(+), 75 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 448cf0c96..e7a01b2cb 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -18,6 +18,14 @@ constexpr uint16 RL_MAX_NUMBER_OF_PLAYERS = 1024; /// Maximum number of winners kept in the on-chain winners history buffer. constexpr uint16 RL_MAX_NUMBER_OF_WINNERS_IN_HISTORY = 1024; +constexpr uint64 RL_TICKET_PRICE = 1000000; + +constexpr uint8 RL_TEAM_FEE_PERCENT = 10; + +constexpr uint8 RL_SHAREHOLDER_FEE_PERCENT = 20; + +constexpr uint8 RL_BURN_PERCENT = 2; + /// Placeholder structure for future extensions. struct RL2 { @@ -44,7 +52,9 @@ struct RL : public ContractBase enum class EState : uint8 { SELLING, - LOCKED + LOCKED, + + INVALID = 255 }; /** @@ -55,13 +65,12 @@ struct RL : public ContractBase SUCCESS = 0, // Ticket-related errors TICKET_INVALID_PRICE = 1, - TICKET_ALREADY_PURCHASED = 2, - TICKET_ALL_SOLD_OUT = 3, - TICKET_SELLING_CLOSED = 4, + TICKET_ALL_SOLD_OUT = 2, + TICKET_SELLING_CLOSED = 3, // Access-related errors - ACCESS_DENIED = 5, + ACCESS_DENIED = 4, // Fee-related errors - FEE_INVALID_PERCENT_VALUE = 6, + FEE_INVALID_PERCENT_VALUE = 5, // Fallback UNKNOW_ERROR = UINT8_MAX }; @@ -97,16 +106,10 @@ struct RL : public ContractBase struct GetPlayers_output { Array players; - uint16 numberOfPlayers = 0; + uint16 playerCounter = 0; uint8 returnCode = static_cast(EReturnCode::SUCCESS); }; - struct GetPlayers_locals - { - uint64 arrayIndex = 0; - sint64 i = 0; - }; - /** * @brief Stored winner snapshot for an epoch. */ @@ -157,10 +160,37 @@ struct RL : public ContractBase struct GetWinners_output { Array winners; - uint64 numberOfWinners = 0; + uint64 winnersCounter = 0; uint8 returnCode = static_cast(EReturnCode::SUCCESS); }; + struct GetTicketPrice_input + { + }; + + struct GetTicketPrice_output + { + uint64 ticketPrice = 0; + }; + + struct GetMaxNumberOfPlayers_input + { + }; + + struct GetMaxNumberOfPlayers_output + { + uint16 numberOfPlayers = 0; + }; + + struct GetState_input + { + }; + + struct GetState_output + { + uint8 currentState = static_cast(EState::INVALID); + }; + struct ReturnAllTickets_input { }; @@ -208,6 +238,9 @@ struct RL : public ContractBase REGISTER_USER_FUNCTION(GetFees, 1); REGISTER_USER_FUNCTION(GetPlayers, 2); REGISTER_USER_FUNCTION(GetWinners, 3); + REGISTER_USER_FUNCTION(GetTicketPrice, 4); + REGISTER_USER_FUNCTION(GetMaxNumberOfPlayers, 5); + REGISTER_USER_FUNCTION(GetState, 6); REGISTER_USER_PROCEDURE(BuyTicket, 1); } @@ -218,22 +251,25 @@ struct RL : public ContractBase INITIALIZE() { // Addresses - state.teamAddress = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, - _R, _L, _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); + state.teamAddress = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, _R, _L, + _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); // Owner address (currently identical to developer address; can be split in future revisions). state.ownerAddress = state.teamAddress; // Default fee percentages (sum <= 100; winner percent derived) - state.teamFeePercent = 10; - state.distributionFeePercent = 20; - state.burnPercent = 2; + state.teamFeePercent = RL_TEAM_FEE_PERCENT; + state.distributionFeePercent = RL_SHAREHOLDER_FEE_PERCENT; + state.burnPercent = RL_BURN_PERCENT; state.winnerFeePercent = 100 - state.teamFeePercent - state.distributionFeePercent - state.burnPercent; // Default ticket price - state.ticketPrice = 1000000; + state.ticketPrice = RL_TICKET_PRICE; // Start locked state.currentState = EState::LOCKED; + + // Initialize Player index + state.playerCounter = 0; } /** @@ -250,11 +286,11 @@ struct RL : public ContractBase state.currentState = EState::LOCKED; // Single-player edge case: refund instead of drawing. - if (state.players.population() == 1) + if (state.playerCounter == 1) { ReturnAllTickets(qpi, state, locals.returnAllTicketsInput, locals.returnAllTicketsOutput, locals.returnAllTicketsLocals); } - else if (state.players.population() > 1) + else if (state.playerCounter > 1) { qpi.getEntity(SELF, locals.entity); locals.revenue = locals.entity.incomingAmount - locals.entity.outgoingAmount; @@ -307,7 +343,7 @@ struct RL : public ContractBase } // Prepare for next epoch. - state.players.reset(); + state.playerCounter = 0; } /** @@ -324,18 +360,10 @@ struct RL : public ContractBase /** * @brief Retrieves the active players list for the ongoing epoch. */ - PUBLIC_FUNCTION_WITH_LOCALS(GetPlayers) + PUBLIC_FUNCTION(GetPlayers) { - locals.arrayIndex = 0; - - locals.i = state.players.nextElementIndex(NULL_INDEX); - while (locals.i != NULL_INDEX) - { - output.players.set(locals.arrayIndex++, state.players.key(locals.i)); - locals.i = state.players.nextElementIndex(locals.i); - }; - - output.numberOfPlayers = static_cast(locals.arrayIndex); + output.players = state.players; + output.playerCounter = state.playerCounter; } /** @@ -344,9 +372,13 @@ struct RL : public ContractBase PUBLIC_FUNCTION(GetWinners) { output.winners = state.winners; - output.numberOfWinners = state.winnersInfoNextEmptyIndex; + output.winnersCounter = state.winnersCounter; } + PUBLIC_FUNCTION(GetTicketPrice) { output.ticketPrice = state.ticketPrice; } + PUBLIC_FUNCTION(GetMaxNumberOfPlayers) { output.numberOfPlayers = RL_MAX_NUMBER_OF_PLAYERS; } + PUBLIC_FUNCTION(GetState) { output.currentState = static_cast(state.currentState); } + /** * @brief Attempts to buy a ticket (must send exact price unless zero is forbidden; state must * be SELLING). Reverts with proper return codes for invalid cases. @@ -356,41 +388,40 @@ struct RL : public ContractBase // Selling closed if (state.currentState == EState::LOCKED) { - output.returnCode = static_cast(EReturnCode::TICKET_SELLING_CLOSED); if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + + output.returnCode = static_cast(EReturnCode::TICKET_SELLING_CLOSED); return; } - // Already purchased - if (state.players.contains(qpi.invocator())) + // Price mismatch (validate before any state mutation) + if (qpi.invocationReward() != state.ticketPrice && qpi.invocationReward() > 0) { - output.returnCode = static_cast(EReturnCode::TICKET_ALREADY_PURCHASED); qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.returnCode = static_cast(EReturnCode::TICKET_INVALID_PRICE); return; } // Capacity full - if (state.players.add(qpi.invocator()) == NULL_INDEX) + if (state.playerCounter >= state.players.capacity()) { output.returnCode = static_cast(EReturnCode::TICKET_ALL_SOLD_OUT); - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } return; } - // Price mismatch - if (qpi.invocationReward() != state.ticketPrice && qpi.invocationReward() > 0) + // Protect against rewriting existing players (should not happen due to prior checks). + if (state.playerCounter < state.players.capacity()) { - output.returnCode = static_cast(EReturnCode::TICKET_INVALID_PRICE); - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - state.players.remove(qpi.invocator()); - - state.players.cleanupIfNeeded(80); - return; + state.players.set(state.playerCounter, qpi.invocator()); + state.playerCounter = min(++state.playerCounter, state.players.capacity()); } } @@ -405,13 +436,13 @@ struct RL : public ContractBase return; } - state.winnersInfoNextEmptyIndex = mod(state.winnersInfoNextEmptyIndex, state.winners.capacity()); + state.winnersCounter = mod(state.winnersCounter, state.winners.capacity()); locals.winnerInfo.winnerAddress = input.winnerAddress; locals.winnerInfo.revenue = input.revenue; locals.winnerInfo.epoch = qpi.epoch(); locals.winnerInfo.tick = qpi.tick(); - state.winners.set(state.winnersInfoNextEmptyIndex++, locals.winnerInfo); + state.winners.set(state.winnersCounter++, locals.winnerInfo); } /** @@ -419,37 +450,24 @@ struct RL : public ContractBase */ PRIVATE_PROCEDURE_WITH_LOCALS(GetWinner) { - if (state.players.population() == 0) + if (state.playerCounter == 0) { return; } - locals.randomNum = mod(qpi.K12(qpi.getPrevSpectrumDigest()).u64._0, state.players.population()); + locals.randomNum = mod(qpi.K12(qpi.getPrevSpectrumDigest()).u64._0, state.playerCounter); - locals.j = 0; - locals.i = state.players.nextElementIndex(NULL_INDEX); - while (locals.i != NULL_INDEX) - { - if (locals.j++ == locals.randomNum) - { - output.winnerAddress = state.players.key(locals.i); - output.index = locals.i; - break; - } - - locals.i = state.players.nextElementIndex(locals.i); - }; + // Direct indexing for Array + output.winnerAddress = state.players.get(locals.randomNum); + output.index = locals.randomNum; } PRIVATE_PROCEDURE_WITH_LOCALS(ReturnAllTickets) { - locals.i = state.players.nextElementIndex(NULL_INDEX); - while (locals.i != NULL_INDEX) + for (locals.i = 0; locals.i < state.playerCounter; ++locals.i) { - qpi.transfer(state.players.key(locals.i), state.ticketPrice); - - locals.i = state.players.nextElementIndex(locals.i); - }; + qpi.transfer(state.players.get(locals.i), state.ticketPrice); + } } protected: @@ -495,11 +513,13 @@ struct RL : public ContractBase */ uint64 ticketPrice = 0; + uint64 playerCounter = 0; + /** * @brief Set of players participating in the current lottery epoch. * Maximum capacity is defined by RL_MAX_NUMBER_OF_PLAYERS. */ - HashSet players = {}; + Array players = {}; /** * @brief Circular buffer storing the history of winners. @@ -511,7 +531,7 @@ struct RL : public ContractBase * @brief Index pointing to the next empty slot in the winners array. * Used for maintaining the circular buffer of winners. */ - uint64 winnersInfoNextEmptyIndex = 0; + uint64 winnersCounter = 0; /** * @brief Current state of the lottery contract. From f3d696d50a402f59a183bda8e9725d89f0988674 Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 22 Oct 2025 22:30:35 +0300 Subject: [PATCH 04/39] Add new public functions to retrieve ticket price, max number of players, contract state, and balance --- src/contracts/RandomLottery.h | 32 ++++- test/contract_rl.cpp | 263 ++++++++++++++++++++++++++-------- 2 files changed, 232 insertions(+), 63 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index e7a01b2cb..cf56299ef 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -106,7 +106,7 @@ struct RL : public ContractBase struct GetPlayers_output { Array players; - uint16 playerCounter = 0; + uint64 playerCounter = 0; uint8 returnCode = static_cast(EReturnCode::SUCCESS); }; @@ -179,7 +179,7 @@ struct RL : public ContractBase struct GetMaxNumberOfPlayers_output { - uint16 numberOfPlayers = 0; + uint64 numberOfPlayers = 0; }; struct GetState_input @@ -191,6 +191,20 @@ struct RL : public ContractBase uint8 currentState = static_cast(EState::INVALID); }; + struct GetBalance_input + { + }; + + struct GetBalance_output + { + uint64 balance = 0; + }; + + struct GetBalance_locals + { + Entity entity = {}; + }; + struct ReturnAllTickets_input { }; @@ -241,6 +255,7 @@ struct RL : public ContractBase REGISTER_USER_FUNCTION(GetTicketPrice, 4); REGISTER_USER_FUNCTION(GetMaxNumberOfPlayers, 5); REGISTER_USER_FUNCTION(GetState, 6); + REGISTER_USER_FUNCTION(GetBalance, 7); REGISTER_USER_PROCEDURE(BuyTicket, 1); } @@ -363,7 +378,7 @@ struct RL : public ContractBase PUBLIC_FUNCTION(GetPlayers) { output.players = state.players; - output.playerCounter = state.playerCounter; + output.playerCounter = min(state.playerCounter, state.players.capacity()); } /** @@ -372,12 +387,16 @@ struct RL : public ContractBase PUBLIC_FUNCTION(GetWinners) { output.winners = state.winners; - output.winnersCounter = state.winnersCounter; + output.winnersCounter = min(state.winnersCounter, state.winners.capacity()); } PUBLIC_FUNCTION(GetTicketPrice) { output.ticketPrice = state.ticketPrice; } PUBLIC_FUNCTION(GetMaxNumberOfPlayers) { output.numberOfPlayers = RL_MAX_NUMBER_OF_PLAYERS; } PUBLIC_FUNCTION(GetState) { output.currentState = static_cast(state.currentState); } + PUBLIC_FUNCTION_WITH_LOCALS(GetBalance) + { + output.balance = qpi.getEntity(SELF, locals.entity) ? locals.entity.incomingAmount - locals.entity.outgoingAmount : 0; + } /** * @brief Attempts to buy a ticket (must send exact price unless zero is forbidden; state must @@ -421,7 +440,7 @@ struct RL : public ContractBase if (state.playerCounter < state.players.capacity()) { state.players.set(state.playerCounter, qpi.invocator()); - state.playerCounter = min(++state.playerCounter, state.players.capacity()); + state.playerCounter = min(state.playerCounter + 1, state.players.capacity()); } } @@ -538,4 +557,7 @@ struct RL : public ContractBase * Can be either SELLING (tickets available) or LOCKED (epoch closed). */ EState currentState = EState::LOCKED; + +protected: + template static constexpr const T& min(const T& a, const T& b) { return (a < b) ? a : b; } }; diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index 5c0bd008b..30e263656 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -7,6 +7,10 @@ constexpr uint16 PROCEDURE_INDEX_BUY_TICKET = 1; constexpr uint16 FUNCTION_INDEX_GET_FEES = 1; constexpr uint16 FUNCTION_INDEX_GET_PLAYERS = 2; constexpr uint16 FUNCTION_INDEX_GET_WINNERS = 3; +constexpr uint16 FUNCTION_INDEX_GET_TICKET_PRICE = 4; +constexpr uint16 FUNCTION_INDEX_GET_MAX_NUM_PLAYERS = 5; +constexpr uint16 FUNCTION_INDEX_GET_STATE = 6; +constexpr uint16 FUNCTION_INDEX_GET_BALANCE = 7; static const id RL_DEV_ADDRESS = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, _R, _L, _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); @@ -37,14 +41,11 @@ class RLChecker : public RL { EXPECT_EQ(output.returnCode, static_cast(EReturnCode::SUCCESS)); EXPECT_EQ(output.players.capacity(), players.capacity()); - EXPECT_EQ(static_cast(output.numberOfPlayers), players.population()); + EXPECT_EQ(output.playerCounter, playerCounter); - for (uint64 i = 0, playerArrayIndex = 0; i < players.capacity(); ++i) + for (uint64 i = 0; i < playerCounter; ++i) { - if (!players.isEmptySlot(i)) - { - EXPECT_EQ(output.players.get(playerArrayIndex++), players.key(i)); - } + EXPECT_EQ(output.players.get(i), players.get(i)); } } @@ -52,9 +53,9 @@ class RLChecker : public RL { EXPECT_EQ(output.returnCode, static_cast(EReturnCode::SUCCESS)); EXPECT_EQ(output.winners.capacity(), winners.capacity()); - EXPECT_EQ(output.numberOfWinners, winnersInfoNextEmptyIndex); + EXPECT_EQ(output.winnersCounter, winnersCounter); - for (uint64 i = 0; i < output.numberOfWinners; ++i) + for (uint64 i = 0; i < winnersCounter; ++i) { EXPECT_EQ(output.winners.get(i), winners.get(i)); } @@ -62,10 +63,10 @@ class RLChecker : public RL void randomlyAddPlayers(uint64 maxNewPlayers) { - const uint64 newPlayerCount = mod(maxNewPlayers, players.capacity()); - for (uint64 i = 0; i < newPlayerCount; ++i) + playerCounter = mod(maxNewPlayers, players.capacity()); + for (uint64 i = 0; i < playerCounter; ++i) { - players.add(id::randomValue()); + players.set(i, id::randomValue()); } } @@ -73,7 +74,7 @@ class RLChecker : public RL { const uint64 newWinnerCount = mod(maxNewWinners, winners.capacity()); - winnersInfoNextEmptyIndex = 0; + winnersCounter = 0; WinnerInfo wi; for (uint64 i = 0; i < newWinnerCount; ++i) @@ -82,15 +83,11 @@ class RLChecker : public RL wi.tick = 1; wi.revenue = 1000000; wi.winnerAddress = id::randomValue(); - winners.set(winnersInfoNextEmptyIndex++, wi); + winners.set(winnersCounter++, wi); } } - void setSelling() { currentState = EState::SELLING; } - - void setLocked() { currentState = EState::LOCKED; } - - uint64 playersPopulation() const { return players.population(); } + uint64 getPlayerCounter() const { return playerCounter; } uint64 getTicketPrice() const { return ticketPrice; } }; @@ -106,7 +103,8 @@ class ContractTestingRL : protected ContractTesting callSystemProcedure(RL_CONTRACT_INDEX, INITIALIZE); } - RLChecker* getState() { return reinterpret_cast(contractStates[RL_CONTRACT_INDEX]); } + // Access internal contract state for assertions + RLChecker* state() { return reinterpret_cast(contractStates[RL_CONTRACT_INDEX]); } RL::GetFees_output getFees() { @@ -135,6 +133,42 @@ class ContractTestingRL : protected ContractTesting return output; } + // Wrapper for public function RL::GetTicketPrice + RL::GetTicketPrice_output getTicketPrice() + { + RL::GetTicketPrice_input input; + RL::GetTicketPrice_output output; + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_TICKET_PRICE, input, output); + return output; + } + + // Wrapper for public function RL::GetMaxNumberOfPlayers + RL::GetMaxNumberOfPlayers_output getMaxNumberOfPlayers() + { + RL::GetMaxNumberOfPlayers_input input; + RL::GetMaxNumberOfPlayers_output output; + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_MAX_NUM_PLAYERS, input, output); + return output; + } + + // Wrapper for public function RL::GetState + RL::GetState_output getStateInfo() + { + RL::GetState_input input; + RL::GetState_output output; + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_STATE, input, output); + return output; + } + + // Wrapper for public function RL::GetBalance + RL::GetBalance_output getBalanceInfo() + { + RL::GetBalance_input input; + RL::GetBalance_output output; + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_BALANCE, input, output); + return output; + } + RL::BuyTicket_output buyTicket(const id& user, uint64 reward) { RL::BuyTicket_input input; @@ -146,13 +180,43 @@ class ContractTestingRL : protected ContractTesting void BeginEpoch() { callSystemProcedure(RL_CONTRACT_INDEX, BEGIN_EPOCH); } void EndEpoch() { callSystemProcedure(RL_CONTRACT_INDEX, END_EPOCH); } + + // Returns the SELF contract account address + id rlSelf() { return id(RL_CONTRACT_INDEX, 0, 0, 0); } + + // Computes remaining contract balance after winner/team/distribution/burn payouts + // Distribution is floored to a multiple of NUMBER_OF_COMPUTORS + uint64 expectedRemainingAfterPayout(uint64 before, const RL::GetFees_output& fees) + { + const uint64 burn = (before * fees.burnPercent) / 100; + const uint64 distribPer = ((before * fees.distributionFeePercent) / 100) / NUMBER_OF_COMPUTORS; + const uint64 distrib = distribPer * NUMBER_OF_COMPUTORS; // floor to a multiple + const uint64 team = (before * fees.teamFeePercent) / 100; + const uint64 winner = (before * fees.winnerFeePercent) / 100; + return before - burn - distrib - team - winner; + } + + // Fund user and buy a ticket, asserting success + void increaseAndBuy(ContractTestingRL& ctl, const id& user, uint64 ticketPrice) + { + increaseEnergy(user, ticketPrice * 2); + const RL::BuyTicket_output out = ctl.buyTicket(user, ticketPrice); + EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + } + + // Assert contract account balance equals the value returned by RL::GetBalance + void expectContractBalanceEqualsGetBalance(ContractTestingRL& ctl, const id& contractAddress) + { + const RL::GetBalance_output out = ctl.getBalanceInfo(); + EXPECT_EQ(out.balance, getBalance(contractAddress)); + } }; TEST(ContractRandomLottery, GetFees) { ContractTestingRL ctl; RL::GetFees_output output = ctl.getFees(); - ctl.getState()->checkFees(output); + ctl.state()->checkFees(output); } TEST(ContractRandomLottery, GetPlayers) @@ -161,13 +225,13 @@ TEST(ContractRandomLottery, GetPlayers) // Initially empty RL::GetPlayers_output output = ctl.getPlayers(); - ctl.getState()->checkPlayers(output); + ctl.state()->checkPlayers(output); // Add random players directly to state (test helper) constexpr uint64 maxPlayersToAdd = 10; - ctl.getState()->randomlyAddPlayers(maxPlayersToAdd); + ctl.state()->randomlyAddPlayers(maxPlayersToAdd); output = ctl.getPlayers(); - ctl.getState()->checkPlayers(output); + ctl.state()->checkPlayers(output); } TEST(ContractRandomLottery, GetWinners) @@ -176,16 +240,16 @@ TEST(ContractRandomLottery, GetWinners) // Populate winners history artificially constexpr uint64 maxNewWinners = 10; - ctl.getState()->randomlyAddWinners(maxNewWinners); + ctl.state()->randomlyAddWinners(maxNewWinners); RL::GetWinners_output winnersOutput = ctl.getWinners(); - ctl.getState()->checkWinners(winnersOutput); + ctl.state()->checkWinners(winnersOutput); } TEST(ContractRandomLottery, BuyTicket) { ContractTestingRL ctl; - const uint64 ticketPrice = ctl.getState()->getTicketPrice(); + const uint64 ticketPrice = ctl.state()->getTicketPrice(); // 1. Attempt when state is LOCKED (should fail and refund invocation reward) { @@ -193,11 +257,11 @@ TEST(ContractRandomLottery, BuyTicket) increaseEnergy(userLocked, ticketPrice * 2); RL::BuyTicket_output out = ctl.buyTicket(userLocked, ticketPrice); EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::TICKET_SELLING_CLOSED)); - EXPECT_EQ(ctl.getState()->playersPopulation(), 0); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0); } // Switch to SELLING to allow purchases - ctl.getState()->setSelling(); + ctl.BeginEpoch(); // 2. Loop over several users and test invalid price, success, duplicate constexpr uint64 userCount = 5; @@ -212,7 +276,7 @@ TEST(ContractRandomLottery, BuyTicket) { const RL::BuyTicket_output outInvalid = ctl.buyTicket(user, ticketPrice - 1); EXPECT_EQ(outInvalid.returnCode, static_cast(RL::EReturnCode::TICKET_INVALID_PRICE)); - EXPECT_EQ(ctl.getState()->playersPopulation(), expectedPlayers); + EXPECT_EQ(ctl.state()->getPlayerCounter(), expectedPlayers); } // (b) Valid purchase — player added @@ -220,28 +284,28 @@ TEST(ContractRandomLottery, BuyTicket) const RL::BuyTicket_output outOk = ctl.buyTicket(user, ticketPrice); EXPECT_EQ(outOk.returnCode, static_cast(RL::EReturnCode::SUCCESS)); ++expectedPlayers; - EXPECT_EQ(ctl.getState()->playersPopulation(), expectedPlayers); + EXPECT_EQ(ctl.state()->getPlayerCounter(), expectedPlayers); } - // (c) Duplicate purchase — rejected + // (c) Duplicate purchase — allowed, increases count { const RL::BuyTicket_output outDup = ctl.buyTicket(user, ticketPrice); - EXPECT_EQ(outDup.returnCode, static_cast(RL::EReturnCode::TICKET_ALREADY_PURCHASED)); - EXPECT_EQ(ctl.getState()->playersPopulation(), expectedPlayers); + EXPECT_EQ(outDup.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + ++expectedPlayers; + EXPECT_EQ(ctl.state()->getPlayerCounter(), expectedPlayers); } } - // 3. Sanity check: number of unique players matches expectations - EXPECT_EQ(expectedPlayers, userCount); + // 3. Sanity check: number of tickets equals twice the number of users (due to duplicate buys) + EXPECT_EQ(expectedPlayers, userCount * 2); } TEST(ContractRandomLottery, EndEpoch) { ContractTestingRL ctl; - // Helper: contract balance holder (SELF account) - const id contractAddress = id(RL_CONTRACT_INDEX, 0, 0, 0); - const uint64 ticketPrice = ctl.getState()->getTicketPrice(); + const id contractAddress = ctl.rlSelf(); + const uint64 ticketPrice = ctl.state()->getTicketPrice(); // Current fee configuration (set in INITIALIZE) const RL::GetFees_output fees = ctl.getFees(); @@ -253,16 +317,16 @@ TEST(ContractRandomLottery, EndEpoch) // --- Scenario 1: No players (should just lock and clear silently) --- { ctl.BeginEpoch(); - EXPECT_EQ(ctl.getState()->playersPopulation(), 0u); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); RL::GetWinners_output before = ctl.getWinners(); - EXPECT_EQ(before.numberOfWinners, 0u); + EXPECT_EQ(before.winnersCounter, 0u); ctl.EndEpoch(); RL::GetWinners_output after = ctl.getWinners(); - EXPECT_EQ(after.numberOfWinners, 0u); - EXPECT_EQ(ctl.getState()->playersPopulation(), 0u); + EXPECT_EQ(after.winnersCounter, 0u); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); } // --- Scenario 2: Exactly one player (ticket refunded, no winner recorded) --- @@ -275,17 +339,17 @@ TEST(ContractRandomLottery, EndEpoch) const RL::BuyTicket_output out = ctl.buyTicket(solo, ticketPrice); EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::SUCCESS)); - EXPECT_EQ(ctl.getState()->playersPopulation(), 1u); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 1u); EXPECT_EQ(getBalance(solo), balanceBefore - ticketPrice); ctl.EndEpoch(); // Refund happened EXPECT_EQ(getBalance(solo), balanceBefore); - EXPECT_EQ(ctl.getState()->playersPopulation(), 0u); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); const RL::GetWinners_output winners = ctl.getWinners(); - EXPECT_EQ(winners.numberOfWinners, 0u); + EXPECT_EQ(winners.winnersCounter, 0u); } // --- Scenario 3: Multiple players (winner chosen, fees processed, remainder burned) --- @@ -306,15 +370,12 @@ TEST(ContractRandomLottery, EndEpoch) for (uint32 i = 0; i < N; ++i) { const id randomUser = id::randomValue(); - increaseEnergy(randomUser, ticketPrice * 2); + ctl.increaseAndBuy(ctl, randomUser, ticketPrice); const uint64 bBefore = getBalance(randomUser); - const RL::BuyTicket_output out = ctl.buyTicket(randomUser, ticketPrice); - EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::SUCCESS)); - EXPECT_EQ(getBalance(randomUser), bBefore - ticketPrice); - infos.push_back({randomUser, bBefore, bBefore - ticketPrice}); + infos.push_back({randomUser, bBefore + ticketPrice, bBefore}); // account for ticket deduction } - EXPECT_EQ(ctl.getState()->playersPopulation(), N); + EXPECT_EQ(ctl.state()->getPlayerCounter(), N); const uint64 contractBalanceBefore = getBalance(contractAddress); EXPECT_EQ(contractBalanceBefore, ticketPrice * N); @@ -322,15 +383,15 @@ TEST(ContractRandomLottery, EndEpoch) const uint64 teamBalanceBefore = getBalance(RL_DEV_ADDRESS); const RL::GetWinners_output winnersBefore = ctl.getWinners(); - const uint64 winnersCountBefore = winnersBefore.numberOfWinners; + const uint64 winnersCountBefore = winnersBefore.winnersCounter; ctl.EndEpoch(); // Players reset after epoch end - EXPECT_EQ(ctl.getState()->playersPopulation(), 0u); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); const RL::GetWinners_output winnersAfter = ctl.getWinners(); - EXPECT_EQ(winnersAfter.numberOfWinners, winnersCountBefore + 1); + EXPECT_EQ(winnersAfter.winnersCounter, winnersCountBefore + 1); // Newly appended winner info const RL::WinnerInfo wi = winnersAfter.winners.get(winnersCountBefore); @@ -361,10 +422,96 @@ TEST(ContractRandomLottery, EndEpoch) const uint64 teamFeeExpected = (ticketPrice * N * teamPercent) / 100; EXPECT_EQ(getBalance(RL_DEV_ADDRESS), teamBalanceBefore + teamFeeExpected); - // Burn - const uint64 burnExpected = contractBalanceBefore - ((contractBalanceBefore * burnPercent) / 100) - - ((((contractBalanceBefore * distributionPercent) / 100) / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS) - - ((contractBalanceBefore * teamPercent) / 100) - ((contractBalanceBefore * winnerPercent) / 100); + // Burn (remaining on contract) + const uint64 burnExpected = ctl.expectedRemainingAfterPayout(contractBalanceBefore, fees); EXPECT_EQ(getBalance(contractAddress), burnExpected); } } + +TEST(ContractRandomLottery, GetBalance) +{ + ContractTestingRL ctl; + + const id contractAddress = ctl.rlSelf(); + const uint64 ticketPrice = ctl.state()->getTicketPrice(); + + // Initially, contract balance is 0 + { + const RL::GetBalance_output out0 = ctl.getBalanceInfo(); + EXPECT_EQ(out0.balance, 0u); + EXPECT_EQ(out0.balance, getBalance(contractAddress)); + } + + // Open selling and perform several purchases + ctl.BeginEpoch(); + + constexpr uint32 K = 3; + for (uint32 i = 0; i < K; ++i) + { + const id user = id::randomValue(); + ctl.increaseAndBuy(ctl, user, ticketPrice); + ctl.expectContractBalanceEqualsGetBalance(ctl, contractAddress); + } + + // Before ending the epoch, balance equals the total cost of tickets + { + const RL::GetBalance_output outBefore = ctl.getBalanceInfo(); + EXPECT_EQ(outBefore.balance, ticketPrice * K); + } + + // End epoch and verify expected remaining amount against contract balance and function output + const uint64 contractBalanceBefore = getBalance(contractAddress); + const RL::GetFees_output fees = ctl.getFees(); + + ctl.EndEpoch(); + + const RL::GetBalance_output outAfter = ctl.getBalanceInfo(); + const uint64 envAfter = getBalance(contractAddress); + EXPECT_EQ(outAfter.balance, envAfter); + + const uint64 expectedRemaining = ctl.expectedRemainingAfterPayout(contractBalanceBefore, fees); + EXPECT_EQ(outAfter.balance, expectedRemaining); +} + +TEST(ContractRandomLottery, GetTicketPrice) +{ + ContractTestingRL ctl; + + const RL::GetTicketPrice_output out = ctl.getTicketPrice(); + EXPECT_EQ(out.ticketPrice, ctl.state()->getTicketPrice()); +} + +TEST(ContractRandomLottery, GetMaxNumberOfPlayers) +{ + ContractTestingRL ctl; + + const RL::GetMaxNumberOfPlayers_output out = ctl.getMaxNumberOfPlayers(); + // Compare against the players array capacity, fetched via GetPlayers + const RL::GetPlayers_output playersOut = ctl.getPlayers(); + EXPECT_EQ(static_cast(out.numberOfPlayers), static_cast(playersOut.players.capacity())); +} + +TEST(ContractRandomLottery, GetState) +{ + ContractTestingRL ctl; + + // Initially LOCKED + { + const RL::GetState_output out0 = ctl.getStateInfo(); + EXPECT_EQ(out0.currentState, static_cast(RL::EState::LOCKED)); + } + + // After BeginEpoch — SELLING + ctl.BeginEpoch(); + { + const RL::GetState_output out1 = ctl.getStateInfo(); + EXPECT_EQ(out1.currentState, static_cast(RL::EState::SELLING)); + } + + // After EndEpoch — back to LOCKED + ctl.EndEpoch(); + { + const RL::GetState_output out2 = ctl.getStateInfo(); + EXPECT_EQ(out2.currentState, static_cast(RL::EState::LOCKED)); + } +} From 1f9e39dffa4102d9db3ca9040d992887dc3371ac Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 23 Oct 2025 12:13:29 +0300 Subject: [PATCH 05/39] Fix ticket purchase validation and adjust player count expectations --- src/contracts/RandomLottery.h | 2 +- test/contract_rl.cpp | 27 +++++++++++++++++++++------ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index cf56299ef..e614e6928 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -417,7 +417,7 @@ struct RL : public ContractBase } // Price mismatch (validate before any state mutation) - if (qpi.invocationReward() != state.ticketPrice && qpi.invocationReward() > 0) + if (qpi.invocationReward() != state.ticketPrice) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index 30e263656..63aac8774 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -173,7 +173,10 @@ class ContractTestingRL : protected ContractTesting { RL::BuyTicket_input input; RL::BuyTicket_output output; - invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_BUY_TICKET, input, output, user, reward); + if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_BUY_TICKET, input, output, user, reward)) + { + output.returnCode = static_cast(RL::EReturnCode::UNKNOW_ERROR); + } return output; } @@ -274,9 +277,20 @@ TEST(ContractRandomLottery, BuyTicket) // (a) Invalid price (wrong reward sent) — player not added { - const RL::BuyTicket_output outInvalid = ctl.buyTicket(user, ticketPrice - 1); + // < ticketPrice + RL::BuyTicket_output outInvalid = ctl.buyTicket(user, ticketPrice - 1); + EXPECT_EQ(outInvalid.returnCode, static_cast(RL::EReturnCode::TICKET_INVALID_PRICE)); + EXPECT_EQ(ctl.state()->getPlayerCounter(), expectedPlayers); + + // == 0 + outInvalid = ctl.buyTicket(user, 0); EXPECT_EQ(outInvalid.returnCode, static_cast(RL::EReturnCode::TICKET_INVALID_PRICE)); EXPECT_EQ(ctl.state()->getPlayerCounter(), expectedPlayers); + + // < 0 + outInvalid = ctl.buyTicket(user, -ticketPrice); + EXPECT_NE(outInvalid.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(ctl.state()->getPlayerCounter(), expectedPlayers); } // (b) Valid purchase — player added @@ -297,7 +311,7 @@ TEST(ContractRandomLottery, BuyTicket) } // 3. Sanity check: number of tickets equals twice the number of users (due to duplicate buys) - EXPECT_EQ(expectedPlayers, userCount * 2); + EXPECT_EQ(ctl.state()->getPlayerCounter(), userCount * 2); } TEST(ContractRandomLottery, EndEpoch) @@ -356,7 +370,7 @@ TEST(ContractRandomLottery, EndEpoch) { ctl.BeginEpoch(); - constexpr uint32 N = 5; + constexpr uint32 N = 5 * 2; struct PlayerInfo { id addr; @@ -367,12 +381,13 @@ TEST(ContractRandomLottery, EndEpoch) infos.reserve(N); // Add N distinct players with valid purchases - for (uint32 i = 0; i < N; ++i) + for (uint32 i = 0; i < N; i+=2) { const id randomUser = id::randomValue(); ctl.increaseAndBuy(ctl, randomUser, ticketPrice); + ctl.increaseAndBuy(ctl, randomUser, ticketPrice); const uint64 bBefore = getBalance(randomUser); - infos.push_back({randomUser, bBefore + ticketPrice, bBefore}); // account for ticket deduction + infos.push_back({randomUser, bBefore + (ticketPrice * 2), bBefore}); // account for ticket deduction } EXPECT_EQ(ctl.state()->getPlayerCounter(), N); From a3cda953040fb40842a3698f6473353b5c057636 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 23 Oct 2025 13:14:32 +0300 Subject: [PATCH 06/39] Refactor code formatting and add tests for multiple consecutive epochs in lottery contract --- src/contracts/RandomLottery.h | 2 +- test/contract_rl.cpp | 93 +++++++++++++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index e614e6928..05d39e4be 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -72,7 +72,7 @@ struct RL : public ContractBase // Fee-related errors FEE_INVALID_PERCENT_VALUE = 5, // Fallback - UNKNOW_ERROR = UINT8_MAX + UNKNOWN_ERROR = UINT8_MAX }; //---- User-facing I/O structures ------------------------------------------------------------- diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index 63aac8774..00dbea2d2 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -13,7 +13,7 @@ constexpr uint16 FUNCTION_INDEX_GET_STATE = 6; constexpr uint16 FUNCTION_INDEX_GET_BALANCE = 7; static const id RL_DEV_ADDRESS = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, - _R, _L, _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); + _R, _L, _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); // Equality operator for comparing WinnerInfo objects bool operator==(const RL::WinnerInfo& left, const RL::WinnerInfo& right) @@ -175,7 +175,7 @@ class ContractTestingRL : protected ContractTesting RL::BuyTicket_output output; if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_BUY_TICKET, input, output, user, reward)) { - output.returnCode = static_cast(RL::EReturnCode::UNKNOW_ERROR); + output.returnCode = static_cast(RL::EReturnCode::UNKNOWN_ERROR); } return output; } @@ -380,8 +380,8 @@ TEST(ContractRandomLottery, EndEpoch) std::vector infos; infos.reserve(N); - // Add N distinct players with valid purchases - for (uint32 i = 0; i < N; i+=2) + // Add N/2 distinct players, each making two valid purchases + for (uint32 i = 0; i < N; i += 2) { const id randomUser = id::randomValue(); ctl.increaseAndBuy(ctl, randomUser, ticketPrice); @@ -441,6 +441,91 @@ TEST(ContractRandomLottery, EndEpoch) const uint64 burnExpected = ctl.expectedRemainingAfterPayout(contractBalanceBefore, fees); EXPECT_EQ(getBalance(contractAddress), burnExpected); } + + // --- Scenario 4: Several consecutive epochs (winners accumulate, balances consistent) --- + { + const uint32 rounds = 3; + const uint32 playersPerRound = 6 * 2; // even number to mimic duplicates if desired + + // Remember starting winners count and team balance + const uint64 winnersStart = ctl.getWinners().winnersCounter; + const uint64 teamStartBal = getBalance(RL_DEV_ADDRESS); + + uint64 teamAccrued = 0; + + for (uint32 r = 0; r < rounds; ++r) + { + ctl.BeginEpoch(); + + struct P + { + id addr; + uint64 balAfterBuy; + }; + std::vector

roundPlayers; + roundPlayers.reserve(playersPerRound); + + // Each player buys two tickets in this round + for (uint32 i = 0; i < playersPerRound; i += 2) + { + const id u = id::randomValue(); + ctl.increaseAndBuy(ctl, u, ticketPrice); + ctl.increaseAndBuy(ctl, u, ticketPrice); + const uint64 balAfter = getBalance(u); + roundPlayers.push_back({u, balAfter}); + } + + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersPerRound); + + const uint64 winnersBefore = ctl.getWinners().winnersCounter; + const uint64 contractBefore = getBalance(contractAddress); + const uint64 teamBalBeforeRound = getBalance(RL_DEV_ADDRESS); + + ctl.EndEpoch(); + + // Winners should increase by exactly one + const RL::GetWinners_output wOut = ctl.getWinners(); + EXPECT_EQ(wOut.winnersCounter, winnersBefore + 1); + + // Validate winner entry + const RL::WinnerInfo newWi = wOut.winners.get(winnersBefore); + EXPECT_NE(newWi.winnerAddress, id::zero()); + EXPECT_EQ(newWi.revenue, (contractBefore * winnerPercent) / 100); + + // Winner must be one of the current round players + bool inRound = false; + for (const auto& p : roundPlayers) + { + if (p.addr == newWi.winnerAddress) + { + inRound = true; + break; + } + } + EXPECT_TRUE(inRound); + + // Check players' balances after payout + for (const auto& p : roundPlayers) + { + const uint64 b = getBalance(p.addr); + const uint64 expected = (p.addr == newWi.winnerAddress) ? (p.balAfterBuy + newWi.revenue) : p.balAfterBuy; + EXPECT_EQ(b, expected); + } + + // Team fee for the whole contract balance of the round + const uint64 teamFee = (contractBefore * teamPercent) / 100; + teamAccrued += teamFee; + EXPECT_EQ(getBalance(RL_DEV_ADDRESS), teamBalBeforeRound + teamFee); + + // Contract remaining should match expected + const uint64 expectedRemaining = ctl.expectedRemainingAfterPayout(contractBefore, fees); + EXPECT_EQ(getBalance(contractAddress), expectedRemaining); + } + + // After all rounds winners increased by rounds and team received cumulative fees + EXPECT_EQ(ctl.getWinners().winnersCounter, winnersStart + rounds); + EXPECT_EQ(getBalance(RL_DEV_ADDRESS), teamStartBal + teamAccrued); + } } TEST(ContractRandomLottery, GetBalance) From 0727963d4f531597ad39b7d9f9f677fedef0c87c Mon Sep 17 00:00:00 2001 From: N-010 Date: Sun, 26 Oct 2025 21:17:24 +0300 Subject: [PATCH 07/39] Add SetPrice procedure and corresponding tests for ticket price management --- src/contracts/RandomLottery.h | 279 ++++++++++++++++++++++++---------- test/contract_rl.cpp | 235 ++++++++++++++++++++++++++++ 2 files changed, 434 insertions(+), 80 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 05d39e4be..df325100f 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -1,4 +1,5 @@ -/** +#pragma once +/** * @file RandomLottery.h * @brief Random Lottery contract definition: state, data structures, and user / internal * procedures. @@ -8,22 +9,31 @@ * - Draws a pseudo-random winner when the epoch ends. * - Distributes fees (team, distribution, burn, winner). * - Records winners' history in a ring-like buffer. + * + * Notes: + * - Percentages must sum to <= 100; the remainder goes to the winner. + * - Players array stores one entry per ticket, so a single address can appear multiple times. + * - When only one player bought a ticket in the epoch, funds are refunded instead of drawing. */ using namespace QPI; -/// Maximum number of players allowed in the lottery. +/// Maximum number of players allowed in the lottery for a single epoch (one entry == one ticket). constexpr uint16 RL_MAX_NUMBER_OF_PLAYERS = 1024; -/// Maximum number of winners kept in the on-chain winners history buffer. +/// Maximum number of winners stored in the on-chain winners history ring buffer. constexpr uint16 RL_MAX_NUMBER_OF_WINNERS_IN_HISTORY = 1024; +/// Default ticket price (denominated in the smallest currency unit). constexpr uint64 RL_TICKET_PRICE = 1000000; +/// Team fee percent of epoch revenue (0..100). constexpr uint8 RL_TEAM_FEE_PERCENT = 10; +/// Distribution (shareholders/validators) fee percent of epoch revenue (0..100). constexpr uint8 RL_SHAREHOLDER_FEE_PERCENT = 20; +/// Burn percent of epoch revenue (0..100). constexpr uint8 RL_BURN_PERCENT = 2; /// Placeholder structure for future extensions. @@ -51,8 +61,8 @@ struct RL : public ContractBase */ enum class EState : uint8 { - SELLING, - LOCKED, + SELLING, // Ticket selling is open + LOCKED, // Ticket selling is closed INVALID = 255 }; @@ -64,17 +74,20 @@ struct RL : public ContractBase { SUCCESS = 0, // Ticket-related errors - TICKET_INVALID_PRICE = 1, - TICKET_ALL_SOLD_OUT = 2, - TICKET_SELLING_CLOSED = 3, + TICKET_INVALID_PRICE = 1, // Not enough funds to buy at least one ticket / price mismatch + TICKET_ALL_SOLD_OUT = 2, // No free slots left in players array + TICKET_SELLING_CLOSED = 3, // Attempted to buy while state is LOCKED // Access-related errors - ACCESS_DENIED = 4, - // Fee-related errors - FEE_INVALID_PERCENT_VALUE = 5, - // Fallback + ACCESS_DENIED = 4, // Caller is not authorized to perform the action + UNKNOWN_ERROR = UINT8_MAX }; + struct NextEpochData + { + uint64 newPrice = 0; // Ticket price to apply after END_EPOCH; 0 means "no change queued" + }; + //---- User-facing I/O structures ------------------------------------------------------------- struct BuyTicket_input @@ -92,10 +105,10 @@ struct RL : public ContractBase struct GetFees_output { - uint8 teamFeePercent = 0; - uint8 distributionFeePercent = 0; - uint8 winnerFeePercent = 0; - uint8 burnPercent = 0; + uint8 teamFeePercent = 0; // Team share in percent + uint8 distributionFeePercent = 0; // Distribution/shareholders share in percent + uint8 winnerFeePercent = 0; // Winner share in percent + uint8 burnPercent = 0; // Burn share in percent uint8 returnCode = static_cast(EReturnCode::SUCCESS); }; @@ -105,8 +118,8 @@ struct RL : public ContractBase struct GetPlayers_output { - Array players; - uint64 playerCounter = 0; + Array players; // Current epoch ticket holders (duplicates allowed) + uint64 playerCounter = 0; // Actual count of filled entries uint8 returnCode = static_cast(EReturnCode::SUCCESS); }; @@ -115,16 +128,16 @@ struct RL : public ContractBase */ struct WinnerInfo { - id winnerAddress = id::zero(); - uint64 revenue = 0; - uint16 epoch = 0; - uint32 tick = 0; + id winnerAddress = id::zero(); // Winner address + uint64 revenue = 0; // Payout value sent to the winner for that epoch + uint16 epoch = 0; // Epoch number when winner was recorded + uint32 tick = 0; // Tick when the decision was made }; struct FillWinnersInfo_input { - id winnerAddress = id::zero(); - uint64 revenue = 0; + id winnerAddress = id::zero(); // Winner address to store + uint64 revenue = 0; // Winner payout to store }; struct FillWinnersInfo_output @@ -133,7 +146,7 @@ struct RL : public ContractBase struct FillWinnersInfo_locals { - WinnerInfo winnerInfo = {}; + WinnerInfo winnerInfo = {}; // Temporary buffer to compose a WinnerInfo record }; struct GetWinner_input @@ -142,15 +155,15 @@ struct RL : public ContractBase struct GetWinner_output { - id winnerAddress = id::zero(); - uint64 index = 0; + id winnerAddress = id::zero(); // Selected winner address (id::zero if none) + uint64 index = 0; // Index into players array }; struct GetWinner_locals { - uint64 randomNum = 0; - sint64 i = 0; - uint64 j = 0; + uint64 randomNum = 0; // Random index candidate in [0, playerCounter) + sint64 i = 0; // Reserved for future iteration logic + uint64 j = 0; // Reserved for future iteration logic }; struct GetWinners_input @@ -159,8 +172,8 @@ struct RL : public ContractBase struct GetWinners_output { - Array winners; - uint64 winnersCounter = 0; + Array winners; // Ring buffer snapshot + uint64 winnersCounter = 0; // Number of valid entries (bounded by capacity) uint8 returnCode = static_cast(EReturnCode::SUCCESS); }; @@ -170,7 +183,7 @@ struct RL : public ContractBase struct GetTicketPrice_output { - uint64 ticketPrice = 0; + uint64 ticketPrice = 0; // Current ticket price }; struct GetMaxNumberOfPlayers_input @@ -179,7 +192,7 @@ struct RL : public ContractBase struct GetMaxNumberOfPlayers_output { - uint64 numberOfPlayers = 0; + uint64 numberOfPlayers = 0; // Max capacity of players array }; struct GetState_input @@ -188,7 +201,7 @@ struct RL : public ContractBase struct GetState_output { - uint8 currentState = static_cast(EState::INVALID); + uint8 currentState = static_cast(EState::INVALID); // Current finite state of the lottery }; struct GetBalance_input @@ -197,12 +210,28 @@ struct RL : public ContractBase struct GetBalance_output { - uint64 balance = 0; + uint64 balance = 0; // Net balance (incoming - outgoing) for current epoch }; + // Local variables for GetBalance procedure struct GetBalance_locals { - Entity entity = {}; + Entity entity = {}; // Entity accounting snapshot for SELF + }; + + // Local variables for BuyTicket procedure + struct BuyTicket_locals + { + uint64 price = 0; // Current ticket price + uint64 reward = 0; // Funds sent with call (invocationReward) + uint64 capacity = 0; // Max capacity of players array + uint64 slotsLeft = 0; // Remaining slots available to fill this epoch + uint64 desired = 0; // How many tickets the caller wants to buy + uint64 remainder = 0; // Change to return (reward % price) + uint64 toBuy = 0; // Actual number of tickets to purchase (bounded by slotsLeft) + uint64 unfilled = 0; // Portion of desired tickets not purchased due to capacity limit + uint64 refundAmount = 0; // Total refund: remainder + unfilled * price + uint64 i = 0; // Loop counter }; struct ReturnAllTickets_input @@ -214,7 +243,7 @@ struct RL : public ContractBase struct ReturnAllTickets_locals { - sint64 i = 0; + uint64 i = 0; // Loop counter for mass-refund }; struct END_EPOCH_locals @@ -231,15 +260,25 @@ struct RL : public ContractBase ReturnAllTickets_output returnAllTicketsOutput = {}; ReturnAllTickets_locals returnAllTicketsLocals = {}; - uint64 teamFee = 0; - uint64 distributionFee = 0; - uint64 winnerAmount = 0; - uint64 burnedAmount = 0; + uint64 teamFee = 0; // Team payout portion + uint64 distributionFee = 0; // Distribution/shared payout portion + uint64 winnerAmount = 0; // Winner payout portion + uint64 burnedAmount = 0; // Burn portion + + uint64 revenue = 0; // Epoch revenue = incoming - outgoing + Entity entity = {}; // Accounting snapshot - uint64 revenue = 0; - Entity entity = {}; + sint32 i = 0; // Reserved + }; - sint32 i = 0; + struct SetPrice_input + { + uint64 newPrice = 0; // New ticket price to be applied at the end of the epoch + }; + + struct SetPrice_output + { + uint8 returnCode = static_cast(EReturnCode::SUCCESS); }; public: @@ -257,6 +296,7 @@ struct RL : public ContractBase REGISTER_USER_FUNCTION(GetState, 6); REGISTER_USER_FUNCTION(GetBalance, 7); REGISTER_USER_PROCEDURE(BuyTicket, 1); + REGISTER_USER_PROCEDURE(SetPrice, 2); } /** @@ -265,25 +305,24 @@ struct RL : public ContractBase */ INITIALIZE() { - // Addresses + // Set team/developer address (owner and team are the same for now) state.teamAddress = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, _R, _L, _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); - // Owner address (currently identical to developer address; can be split in future revisions). state.ownerAddress = state.teamAddress; - // Default fee percentages (sum <= 100; winner percent derived) + // Fee configuration (winner gets the remainder) state.teamFeePercent = RL_TEAM_FEE_PERCENT; state.distributionFeePercent = RL_SHAREHOLDER_FEE_PERCENT; state.burnPercent = RL_BURN_PERCENT; state.winnerFeePercent = 100 - state.teamFeePercent - state.distributionFeePercent - state.burnPercent; - // Default ticket price + // Initial ticket price state.ticketPrice = RL_TICKET_PRICE; - // Start locked + // Start in LOCKED state; selling must be explicitly opened with BEGIN_EPOCH state.currentState = EState::LOCKED; - // Initialize Player index + // Reset player counter state.playerCounter = 0; } @@ -295,6 +334,13 @@ struct RL : public ContractBase /** * @brief Closes epoch: computes revenue, selects winner (if >1 player), * distributes fees, burns leftover, records winner, then clears players. + * + * Behavior: + * - If exactly 1 player, refund ticket price (no draw). + * - If >1 players, compute revenue, select winner with K12-based randomness, + * split revenue into winner/team/distribution/burn, perform transfers/burn, + * and store winner snapshot in the ring buffer. + * - Apply deferred price change for the next epoch if queued. */ END_EPOCH_WITH_LOCALS() { @@ -307,27 +353,28 @@ struct RL : public ContractBase } else if (state.playerCounter > 1) { + // Epoch revenue = incoming - outgoing for this contract qpi.getEntity(SELF, locals.entity); locals.revenue = locals.entity.incomingAmount - locals.entity.outgoingAmount; - // Winner selection (pseudo-random). + // Winner selection (pseudo-random using K12(prevSpectrumDigest)). GetWinner(qpi, state, locals.getWinnerInput, locals.getWinnerOutput, locals.getWinnerLocals); if (locals.getWinnerOutput.winnerAddress != id::zero()) { - // Fee splits + // Split revenue by configured percentages locals.winnerAmount = div(locals.revenue * state.winnerFeePercent, 100ULL); locals.teamFee = div(locals.revenue * state.teamFeePercent, 100ULL); locals.distributionFee = div(locals.revenue * state.distributionFeePercent, 100ULL); locals.burnedAmount = div(locals.revenue * state.burnPercent, 100ULL); - // Team fee + // Team payout if (locals.teamFee > 0) { qpi.transfer(state.teamAddress, locals.teamFee); } - // Distribution fee + // Distribution payout (equal per validator) if (locals.distributionFee > 0) { qpi.distributeDividends(div(locals.distributionFee, uint64(NUMBER_OF_COMPUTORS))); @@ -339,26 +386,32 @@ struct RL : public ContractBase qpi.transfer(locals.getWinnerOutput.winnerAddress, locals.winnerAmount); } - // Burn remainder + // Burn configured portion if (locals.burnedAmount > 0) { qpi.burn(locals.burnedAmount); } - // Persist winner record + // Store winner snapshot into history (ring buffer) locals.fillWinnersInfoInput.winnerAddress = locals.getWinnerOutput.winnerAddress; locals.fillWinnersInfoInput.revenue = locals.winnerAmount; FillWinnersInfo(qpi, state, locals.fillWinnersInfoInput, locals.fillWinnersInfoOutput, locals.fillWinnersInfoLocals); } else { - // Return funds to players if no winner could be selected (should be impossible). + // Fallback: if winner couldn't be selected (should not happen), refund all tickets ReturnAllTickets(qpi, state, locals.returnAllTicketsInput, locals.returnAllTicketsOutput, locals.returnAllTicketsLocals); } } - // Prepare for next epoch. + // Prepare for next epoch: clear players and apply deferred price if any state.playerCounter = 0; + + if (state.nexEpochData.newPrice != 0) + { + state.ticketPrice = state.nexEpochData.newPrice; + state.nexEpochData.newPrice = 0; + } } /** @@ -395,53 +448,107 @@ struct RL : public ContractBase PUBLIC_FUNCTION(GetState) { output.currentState = static_cast(state.currentState); } PUBLIC_FUNCTION_WITH_LOCALS(GetBalance) { + // Returns balance for current epoch (incoming - outgoing) output.balance = qpi.getEntity(SELF, locals.entity) ? locals.entity.incomingAmount - locals.entity.outgoingAmount : 0; } + PUBLIC_PROCEDURE(SetPrice) + { + // Only team/owner can queue a price change + if (qpi.invocator() != state.teamAddress) + { + output.returnCode = static_cast(EReturnCode::ACCESS_DENIED); + return; + } + + // Zero price is invalid + if (input.newPrice == 0) + { + output.returnCode = static_cast(EReturnCode::TICKET_INVALID_PRICE); + return; + } + + // Defer application until END_EPOCH + state.nexEpochData.newPrice = input.newPrice; + output.returnCode = static_cast(EReturnCode::SUCCESS); + } + /** - * @brief Attempts to buy a ticket (must send exact price unless zero is forbidden; state must - * be SELLING). Reverts with proper return codes for invalid cases. + * @brief Attempts to buy tickets while SELLING state is active. + * Logic: + * - If locked: refund full invocationReward and return TICKET_SELLING_CLOSED. + * - If reward < price: refund full reward and return TICKET_INVALID_PRICE. + * - If no capacity left: refund full reward and return TICKET_ALL_SOLD_OUT. + * - Otherwise: add up to slotsLeft tickets; refund remainder and unfilled part. */ - PUBLIC_PROCEDURE(BuyTicket) + PUBLIC_PROCEDURE_WITH_LOCALS(BuyTicket) { - // Selling closed + locals.reward = qpi.invocationReward(); + + // Selling closed: refund any attached funds and exit if (state.currentState == EState::LOCKED) { - if (qpi.invocationReward() > 0) + if (locals.reward > 0) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + qpi.transfer(qpi.invocator(), locals.reward); } output.returnCode = static_cast(EReturnCode::TICKET_SELLING_CLOSED); return; } - // Price mismatch (validate before any state mutation) - if (qpi.invocationReward() != state.ticketPrice) + locals.price = state.ticketPrice; + + // Not enough to buy even a single ticket: refund everything + if (locals.reward < locals.price) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + if (locals.reward > 0) + { + qpi.transfer(qpi.invocator(), locals.reward); + } output.returnCode = static_cast(EReturnCode::TICKET_INVALID_PRICE); return; } - // Capacity full - if (state.playerCounter >= state.players.capacity()) + // Capacity check + locals.capacity = state.players.capacity(); + locals.slotsLeft = (state.playerCounter < locals.capacity) ? (locals.capacity - state.playerCounter) : 0; + if (locals.slotsLeft == 0) { - output.returnCode = static_cast(EReturnCode::TICKET_ALL_SOLD_OUT); - if (qpi.invocationReward() > 0) + // All sold out: refund full amount + if (locals.reward > 0) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + qpi.transfer(qpi.invocator(), locals.reward); } + output.returnCode = static_cast(EReturnCode::TICKET_ALL_SOLD_OUT); return; } - // Protect against rewriting existing players (should not happen due to prior checks). - if (state.playerCounter < state.players.capacity()) + // Compute desired number of tickets and change + locals.desired = locals.reward / locals.price; // How many tickets the caller attempts to buy + locals.remainder = locals.reward % locals.price; // Change to return + locals.toBuy = min(locals.desired, locals.slotsLeft); // Do not exceed available slots + + // Add tickets (the same address may be inserted multiple times) + for (locals.i = 0; locals.i < locals.toBuy; ++locals.i) + { + if (state.playerCounter < locals.capacity) + { + state.players.set(state.playerCounter, qpi.invocator()); + state.playerCounter = min(state.playerCounter + 1, locals.capacity); + } + } + + // Refund change and unfilled portion (if desired > slotsLeft) + locals.unfilled = locals.desired - locals.toBuy; + locals.refundAmount = locals.remainder + (locals.unfilled * locals.price); + if (locals.refundAmount > 0) { - state.players.set(state.playerCounter, qpi.invocator()); - state.playerCounter = min(state.playerCounter + 1, state.players.capacity()); + qpi.transfer(qpi.invocator(), locals.refundAmount); } + + output.returnCode = static_cast(EReturnCode::SUCCESS); } private: @@ -452,16 +559,18 @@ struct RL : public ContractBase { if (input.winnerAddress == id::zero()) { - return; + return; // Nothing to store } + // Use ring-buffer indexing to avoid overflow (overwrite oldest entries) state.winnersCounter = mod(state.winnersCounter, state.winners.capacity()); locals.winnerInfo.winnerAddress = input.winnerAddress; locals.winnerInfo.revenue = input.revenue; locals.winnerInfo.epoch = qpi.epoch(); locals.winnerInfo.tick = qpi.tick(); - state.winners.set(state.winnersCounter++, locals.winnerInfo); + state.winners.set(state.winnersCounter, locals.winnerInfo); + ++state.winnersCounter; } /** @@ -474,15 +583,17 @@ struct RL : public ContractBase return; } + // Compute pseudo-random index based on K12(prevSpectrumDigest) locals.randomNum = mod(qpi.K12(qpi.getPrevSpectrumDigest()).u64._0, state.playerCounter); - // Direct indexing for Array + // Index directly into players array output.winnerAddress = state.players.get(locals.randomNum); output.index = locals.randomNum; } PRIVATE_PROCEDURE_WITH_LOCALS(ReturnAllTickets) { + // Refund ticket price to each recorded player (one transfer per ticket entry) for (locals.i = 0; locals.i < state.playerCounter; ++locals.i) { qpi.transfer(state.players.get(locals.i), state.ticketPrice); @@ -532,8 +643,16 @@ struct RL : public ContractBase */ uint64 ticketPrice = 0; + /** + * @brief Number of players (tickets sold) in the current epoch. + */ uint64 playerCounter = 0; + /** + * @brief Data structure for deferred changes to apply at the end of the epoch. + */ + NextEpochData nexEpochData = {}; + /** * @brief Set of players participating in the current lottery epoch. * Maximum capacity is defined by RL_MAX_NUMBER_OF_PLAYERS. diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index 00dbea2d2..6fd466e96 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -4,6 +4,7 @@ #include "contract_testing.h" constexpr uint16 PROCEDURE_INDEX_BUY_TICKET = 1; +constexpr uint16 PROCEDURE_INDEX_SET_PRICE = 2; constexpr uint16 FUNCTION_INDEX_GET_FEES = 1; constexpr uint16 FUNCTION_INDEX_GET_PLAYERS = 2; constexpr uint16 FUNCTION_INDEX_GET_WINNERS = 3; @@ -180,6 +181,19 @@ class ContractTestingRL : protected ContractTesting return output; } + // Added: wrapper for SetPrice procedure + RL::SetPrice_output setPrice(const id& invocator, uint64 newPrice) + { + RL::SetPrice_input input; + input.newPrice = newPrice; + RL::SetPrice_output output; + if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_SET_PRICE, input, output, invocator, 0)) + { + output.returnCode = static_cast(RL::EReturnCode::UNKNOWN_ERROR); + } + return output; + } + void BeginEpoch() { callSystemProcedure(RL_CONTRACT_INDEX, BEGIN_EPOCH); } void EndEpoch() { callSystemProcedure(RL_CONTRACT_INDEX, END_EPOCH); } @@ -615,3 +629,224 @@ TEST(ContractRandomLottery, GetState) EXPECT_EQ(out2.currentState, static_cast(RL::EState::LOCKED)); } } + +// --- New tests for SetPrice --- + +TEST(ContractRandomLottery, SetPrice_AccessControl) +{ + ContractTestingRL ctl; + + const uint64 oldPrice = ctl.state()->getTicketPrice(); + const uint64 newPrice = oldPrice * 2; + + // Random user must not have permission + const id randomUser = id::randomValue(); + increaseEnergy(randomUser, 1); + + const RL::SetPrice_output outDenied = ctl.setPrice(randomUser, newPrice); + EXPECT_EQ(outDenied.returnCode, static_cast(RL::EReturnCode::ACCESS_DENIED)); + + // Price doesn't change immediately nor after EndEpoch + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); + ctl.EndEpoch(); + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); +} + +TEST(ContractRandomLottery, SetPrice_ZeroNotAllowed) +{ + ContractTestingRL ctl; + + increaseEnergy(RL_DEV_ADDRESS, 1); + + const uint64 oldPrice = ctl.state()->getTicketPrice(); + + const RL::SetPrice_output outInvalid = ctl.setPrice(RL_DEV_ADDRESS, 0); + EXPECT_EQ(outInvalid.returnCode, static_cast(RL::EReturnCode::TICKET_INVALID_PRICE)); + + // Price remains unchanged even after EndEpoch + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); + ctl.EndEpoch(); + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); +} + +TEST(ContractRandomLottery, SetPrice_AppliesAfterEndEpoch) +{ + ContractTestingRL ctl; + + increaseEnergy(RL_DEV_ADDRESS, 1); + + const uint64 oldPrice = ctl.state()->getTicketPrice(); + const uint64 newPrice = oldPrice * 2; + + const RL::SetPrice_output outOk = ctl.setPrice(RL_DEV_ADDRESS, newPrice); + EXPECT_EQ(outOk.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + + // Until EndEpoch the price remains unchanged + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); + + // Applied after EndEpoch + ctl.EndEpoch(); + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, newPrice); + + // Another EndEpoch without a new SetPrice doesn't change the price + ctl.EndEpoch(); + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, newPrice); +} + +TEST(ContractRandomLottery, SetPrice_OverrideBeforeEndEpoch) +{ + ContractTestingRL ctl; + + increaseEnergy(RL_DEV_ADDRESS, 1); + + const uint64 oldPrice = ctl.state()->getTicketPrice(); + const uint64 firstPrice = oldPrice + 1000; + const uint64 secondPrice = oldPrice + 7777; + + // Two SetPrice calls before EndEpoch — the last one should apply + EXPECT_EQ(ctl.setPrice(RL_DEV_ADDRESS, firstPrice).returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(ctl.setPrice(RL_DEV_ADDRESS, secondPrice).returnCode, static_cast(RL::EReturnCode::SUCCESS)); + + // Until EndEpoch the old price remains + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); + + ctl.EndEpoch(); + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, secondPrice); +} + +TEST(ContractRandomLottery, SetPrice_AffectsNextEpochBuys) +{ + ContractTestingRL ctl; + + increaseEnergy(RL_DEV_ADDRESS, 1); + + const uint64 oldPrice = ctl.state()->getTicketPrice(); + const uint64 newPrice = oldPrice * 3; + + // Open selling and buy at the old price + ctl.BeginEpoch(); + const id u1 = id::randomValue(); + increaseEnergy(u1, oldPrice * 2); + { + const RL::BuyTicket_output out1 = ctl.buyTicket(u1, oldPrice); + EXPECT_EQ(out1.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + } + + // Set a new price, but before EndEpoch purchases should use the old price + { + const RL::SetPrice_output setOut = ctl.setPrice(RL_DEV_ADDRESS, newPrice); + EXPECT_EQ(setOut.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + } + + const id u2 = id::randomValue(); + increaseEnergy(u2, newPrice * 2); + { + const uint64 balBefore = getBalance(u2); + const uint64 playersBefore = ctl.state()->getPlayerCounter(); + const RL::BuyTicket_output outNow = ctl.buyTicket(u2, newPrice); + EXPECT_EQ(outNow.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + // floor(newPrice/oldPrice) tickets were bought, the remainder was refunded + const uint64 bought = newPrice / oldPrice; + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersBefore + bought); + EXPECT_EQ(getBalance(u2), balBefore - bought * oldPrice); + } + + // End the epoch: new price will apply + ctl.EndEpoch(); + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, newPrice); + + // In the next epoch, a purchase at the new price should succeed + ctl.BeginEpoch(); + { + const uint64 balBefore = getBalance(u2); + const uint64 playersBefore = ctl.state()->getPlayerCounter(); + const RL::BuyTicket_output outOk = ctl.buyTicket(u2, newPrice); + EXPECT_EQ(outOk.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersBefore + 1); + EXPECT_EQ(getBalance(u2), balBefore - newPrice); + } +} + +TEST(ContractRandomLottery, BuyMultipleTickets_ExactMultiple_NoRemainder) +{ + ContractTestingRL ctl; + ctl.BeginEpoch(); + const uint64 price = ctl.state()->getTicketPrice(); + const id user = id::randomValue(); + const uint64 k = 7; + increaseEnergy(user, price * k); + const uint64 playersBefore = ctl.state()->getPlayerCounter(); + const RL::BuyTicket_output out = ctl.buyTicket(user, price * k); + EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersBefore + k); +} + +TEST(ContractRandomLottery, BuyMultipleTickets_WithRemainder_Refunded) +{ + ContractTestingRL ctl; + ctl.BeginEpoch(); + const uint64 price = ctl.state()->getTicketPrice(); + const id user = id::randomValue(); + const uint64 k = 5; + const uint64 r = price / 3; // partial remainder + increaseEnergy(user, price * k + r); + const uint64 balBefore = getBalance(user); + const uint64 playersBefore = ctl.state()->getPlayerCounter(); + const RL::BuyTicket_output out = ctl.buyTicket(user, price * k + r); + EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersBefore + k); + // Remainder refunded, only k * price spent + EXPECT_EQ(getBalance(user), balBefore - k * price); +} + +TEST(ContractRandomLottery, BuyMultipleTickets_CapacityPartialRefund) +{ + ContractTestingRL ctl; + ctl.BeginEpoch(); + const uint64 price = ctl.state()->getTicketPrice(); + const uint64 capacity = ctl.getPlayers().players.capacity(); + + // Fill almost up to capacity + const uint64 toFill = (capacity > 5) ? (capacity - 5) : 0; + for (uint64 i = 0; i < toFill; ++i) + { + const id u = id::randomValue(); + increaseEnergy(u, price); + EXPECT_EQ(ctl.buyTicket(u, price).returnCode, static_cast(RL::EReturnCode::SUCCESS)); + } + EXPECT_EQ(ctl.state()->getPlayerCounter(), toFill); + + // Try to buy 10 tickets — only remaining 5 accepted, the rest refunded + const id buyer = id::randomValue(); + increaseEnergy(buyer, price * 10); + const uint64 balBefore = getBalance(buyer); + const RL::BuyTicket_output out = ctl.buyTicket(buyer, price * 10); + EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(ctl.state()->getPlayerCounter(), capacity); + EXPECT_EQ(getBalance(buyer), balBefore - price * 5); +} + +TEST(ContractRandomLottery, BuyMultipleTickets_AllSoldOut) +{ + ContractTestingRL ctl; + ctl.BeginEpoch(); + const uint64 price = ctl.state()->getTicketPrice(); + const uint64 capacity = ctl.getPlayers().players.capacity(); + + // Fill to capacity + for (uint64 i = 0; i < capacity; ++i) + { + const id u = id::randomValue(); + increaseEnergy(u, price); + EXPECT_EQ(ctl.buyTicket(u, price).returnCode, static_cast(RL::EReturnCode::SUCCESS)); + } + EXPECT_EQ(ctl.state()->getPlayerCounter(), capacity); + + // Any purchase refunds the full amount and returns ALL_SOLD_OUT code + const id buyer = id::randomValue(); + increaseEnergy(buyer, price * 3); + const uint64 balBefore = getBalance(buyer); + const RL::BuyTicket_output out = ctl.buyTicket(buyer, price * 3); + EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::TICKET_ALL_SOLD_OUT)); + EXPECT_EQ(getBalance(buyer), balBefore); +} From 07bad874839b318ff3e67b6ea15a558b3fc60db0 Mon Sep 17 00:00:00 2001 From: N-010 Date: Sun, 26 Oct 2025 21:27:58 +0300 Subject: [PATCH 08/39] Removes #pragma once --- src/contracts/RandomLottery.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index df325100f..359abf6ed 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -1,5 +1,4 @@ -#pragma once -/** +/** * @file RandomLottery.h * @brief Random Lottery contract definition: state, data structures, and user / internal * procedures. From 81e47e65a284024775210c043d150c5838689475 Mon Sep 17 00:00:00 2001 From: N-010 Date: Sun, 26 Oct 2025 21:38:11 +0300 Subject: [PATCH 09/39] Fixes division operator --- src/contracts/RandomLottery.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 359abf6ed..e839f7a04 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -525,8 +525,8 @@ struct RL : public ContractBase } // Compute desired number of tickets and change - locals.desired = locals.reward / locals.price; // How many tickets the caller attempts to buy - locals.remainder = locals.reward % locals.price; // Change to return + locals.desired = div(locals.reward, locals.price); // How many tickets the caller attempts to buy + locals.remainder = mod(locals.reward, locals.price); // Change to return locals.toBuy = min(locals.desired, locals.slotsLeft); // Do not exceed available slots // Add tickets (the same address may be inserted multiple times) From 43ba4bd7a4e97c3e5a51721110c0e4cef15d740d Mon Sep 17 00:00:00 2001 From: N-010 Date: Tue, 28 Oct 2025 22:01:52 +0300 Subject: [PATCH 10/39] Add utility functions for date handling and revenue calculation; enhance state management for lottery epochs --- src/contracts/RandomLottery.h | 508 ++++++++++++++++++++++------------ 1 file changed, 334 insertions(+), 174 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index e839f7a04..a9712b41a 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -35,6 +35,34 @@ constexpr uint8 RL_SHAREHOLDER_FEE_PERCENT = 20; /// Burn percent of epoch revenue (0..100). constexpr uint8 RL_BURN_PERCENT = 2; +constexpr uint8 RL_INVALID_DAY = 255; + +constexpr uint8 RL_INVALID_HOUR = 255; + +namespace RLUtils +{ + static void getCurrentDayOfWeek(const QpiContextFunctionCall& qpi, uint8& dayOfWeek) + { + dayOfWeek = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); + } + + static void makeDateStamp(const QpiContextFunctionCall& qpi, uint32& res) + { + res = static_cast(qpi.year() << 9 | qpi.month() << 5 | qpi.day()); + } + + static void getSCRevenue(const QpiContextFunctionCall& qpi, Entity& entity, uint64& revenue) + { + qpi.getEntity(SELF, entity); + revenue = entity.incomingAmount - entity.outgoingAmount; + } + + template static constexpr const T& min(const T& a, const T& b) + { + return (a < b) ? a : b; + } +}; // namespace RLUtils + /// Placeholder structure for future extensions. struct RL2 { @@ -63,7 +91,7 @@ struct RL : public ContractBase SELLING, // Ticket selling is open LOCKED, // Ticket selling is closed - INVALID = 255 + INVALID = UINT8_MAX }; /** @@ -71,13 +99,17 @@ struct RL : public ContractBase */ enum class EReturnCode : uint8 { - SUCCESS = 0, + SUCCESS, + // Ticket-related errors - TICKET_INVALID_PRICE = 1, // Not enough funds to buy at least one ticket / price mismatch - TICKET_ALL_SOLD_OUT = 2, // No free slots left in players array - TICKET_SELLING_CLOSED = 3, // Attempted to buy while state is LOCKED + TICKET_INVALID_PRICE, // Not enough funds to buy at least one ticket / price mismatch + TICKET_ALL_SOLD_OUT, // No free slots left in players array + TICKET_SELLING_CLOSED, // Attempted to buy while state is LOCKED // Access-related errors - ACCESS_DENIED = 4, // Caller is not authorized to perform the action + ACCESS_DENIED, // Caller is not authorized to perform the action + + // Value-related errors + INVALID_VALUE, // Input value is not acceptable UNKNOWN_ERROR = UINT8_MAX }; @@ -85,6 +117,7 @@ struct RL : public ContractBase struct NextEpochData { uint64 newPrice = 0; // Ticket price to apply after END_EPOCH; 0 means "no change queued" + uint8 schedule = 0; }; //---- User-facing I/O structures ------------------------------------------------------------- @@ -127,10 +160,11 @@ struct RL : public ContractBase */ struct WinnerInfo { - id winnerAddress = id::zero(); // Winner address - uint64 revenue = 0; // Payout value sent to the winner for that epoch - uint16 epoch = 0; // Epoch number when winner was recorded - uint32 tick = 0; // Tick when the decision was made + id winnerAddress = id::zero(); // Winner address + uint64 revenue = 0; // Payout value sent to the winner for that epoch + uint32 tick = 0; // Tick when the decision was made + uint16 epoch = 0; // Epoch number when winner was recorded + uint8 dayOfWeek = RL_INVALID_DAY; // Day of week when the winner was drawn [0..6] 0 = WEDNESDAY }; struct FillWinnersInfo_input @@ -148,23 +182,6 @@ struct RL : public ContractBase WinnerInfo winnerInfo = {}; // Temporary buffer to compose a WinnerInfo record }; - struct GetWinner_input - { - }; - - struct GetWinner_output - { - id winnerAddress = id::zero(); // Selected winner address (id::zero if none) - uint64 index = 0; // Index into players array - }; - - struct GetWinner_locals - { - uint64 randomNum = 0; // Random index candidate in [0, playerCounter) - sint64 i = 0; // Reserved for future iteration logic - uint64 j = 0; // Reserved for future iteration logic - }; - struct GetWinners_input { }; @@ -245,39 +262,65 @@ struct RL : public ContractBase uint64 i = 0; // Loop counter for mass-refund }; - struct END_EPOCH_locals + struct SetPrice_input { - GetWinner_input getWinnerInput = {}; - GetWinner_output getWinnerOutput = {}; - GetWinner_locals getWinnerLocals = {}; + uint64 newPrice = 0; // New ticket price to be applied at the end of the epoch + }; - FillWinnersInfo_input fillWinnersInfoInput = {}; - FillWinnersInfo_output fillWinnersInfoOutput = {}; - FillWinnersInfo_locals fillWinnersInfoLocals = {}; + struct SetPrice_output + { + uint8 returnCode = static_cast(EReturnCode::SUCCESS); + }; + struct SetSchedule_input + { + uint8 newSchedule = 0; // New schedule bitmask to be applied at the end of the epoch + }; + + struct SetSchedule_output + { + uint8 returnCode = static_cast(EReturnCode::SUCCESS); + }; + + struct BEGIN_TICK_locals + { + id winnerAddress = id::zero(); + Entity entity = {}; + uint64 revenue = 0; + uint64 randomNum = 0; + uint64 winnerAmount = 0; + uint64 teamFee = 0; + uint64 distributionFee = 0; + uint64 burnedAmount = 0; + FillWinnersInfo_locals fillWinnersInfoLocals = {}; + FillWinnersInfo_input fillWinnersInfoInput = {}; + uint32 currentDateStamp = 0; + uint8 currentDayOfWeek = RL_INVALID_DAY; + uint8 currentHour = RL_INVALID_HOUR; + uint8 isWednesday = 0; + uint8 isScheduledToday = 0; + ReturnAllTickets_locals returnAllTicketsLocals = {}; ReturnAllTickets_input returnAllTicketsInput = {}; ReturnAllTickets_output returnAllTicketsOutput = {}; - ReturnAllTickets_locals returnAllTicketsLocals = {}; - - uint64 teamFee = 0; // Team payout portion - uint64 distributionFee = 0; // Distribution/shared payout portion - uint64 winnerAmount = 0; // Winner payout portion - uint64 burnedAmount = 0; // Burn portion + FillWinnersInfo_output fillWinnersInfoOutput = {}; + }; - uint64 revenue = 0; // Epoch revenue = incoming - outgoing - Entity entity = {}; // Accounting snapshot + struct GetNextEpochData_input + { + }; - sint32 i = 0; // Reserved + struct GetNextEpochData_output + { + NextEpochData nextEpochData = {}; }; - struct SetPrice_input + struct GetDrawHour_input { - uint64 newPrice = 0; // New ticket price to be applied at the end of the epoch }; - struct SetPrice_output + struct GetDrawHour_output { - uint8 returnCode = static_cast(EReturnCode::SUCCESS); + uint8 drawHour = RL_INVALID_HOUR; }; public: @@ -294,8 +337,11 @@ struct RL : public ContractBase REGISTER_USER_FUNCTION(GetMaxNumberOfPlayers, 5); REGISTER_USER_FUNCTION(GetState, 6); REGISTER_USER_FUNCTION(GetBalance, 7); + REGISTER_USER_FUNCTION(GetNextEpochData, 8); + REGISTER_USER_FUNCTION(GetDrawHour, 9); REGISTER_USER_PROCEDURE(BuyTicket, 1); REGISTER_USER_PROCEDURE(SetPrice, 2); + REGISTER_USER_PROCEDURE(SetSchedule, 3); } /** @@ -323,94 +369,154 @@ struct RL : public ContractBase // Reset player counter state.playerCounter = 0; + + // Default schedule: WEDNESDAY + state.schedule = 1 << WEDNESDAY; } /** * @brief Opens ticket selling for a new epoch. */ - BEGIN_EPOCH() { state.currentState = EState::SELLING; } + BEGIN_EPOCH() + { + if (state.schedule == 0) + { + // Default to WEDNESDAY if no schedule is set + state.schedule = 1 << WEDNESDAY; + } - /** - * @brief Closes epoch: computes revenue, selects winner (if >1 player), - * distributes fees, burns leftover, records winner, then clears players. - * - * Behavior: - * - If exactly 1 player, refund ticket price (no draw). - * - If >1 players, compute revenue, select winner with K12-based randomness, - * split revenue into winner/team/distribution/burn, perform transfers/burn, - * and store winner snapshot in the ring buffer. - * - Apply deferred price change for the next epoch if queued. - */ - END_EPOCH_WITH_LOCALS() + if (state.drawHour == 0) + { + state.drawHour = 11; // Default to 11 UTC + } + + // Mark the current day as already processed to avoid immediate toggling on the same day + RLUtils::getCurrentDayOfWeek(qpi, state.lastDrawDay); + state.lastDrawHour = state.drawHour; + // Force lastDrawDateStamp to today's date to prevent reprocessing + RLUtils::makeDateStamp(qpi, state.lastDrawDateStamp); + + enableBuyTicket(state, true); + } + + END_EPOCH() { - state.currentState = EState::LOCKED; + enableBuyTicket(state, false); - // Single-player edge case: refund instead of drawing. - if (state.playerCounter == 1) + clearStateOnEndEpoch(state); + applyNextEpochData(state); + } + + BEGIN_TICK_WITH_LOCALS() + { + // Only process once every 100 ticks + if (mod(qpi.tick(), 100u) != 0) { - ReturnAllTickets(qpi, state, locals.returnAllTicketsInput, locals.returnAllTicketsOutput, locals.returnAllTicketsLocals); + return; } - else if (state.playerCounter > 1) + + // Compute current day and hour + RLUtils::getCurrentDayOfWeek(qpi, locals.currentDayOfWeek); + locals.currentHour = qpi.hour(); + + // Only consider actions at or after the configured draw hour + if (locals.currentHour < state.drawHour) { - // Epoch revenue = incoming - outgoing for this contract - qpi.getEntity(SELF, locals.entity); - locals.revenue = locals.entity.incomingAmount - locals.entity.outgoingAmount; + return; + } - // Winner selection (pseudo-random using K12(prevSpectrumDigest)). - GetWinner(qpi, state, locals.getWinnerInput, locals.getWinnerOutput, locals.getWinnerLocals); + // Allow only one state change action per calendar day + RLUtils::makeDateStamp(qpi, locals.currentDateStamp); + if (state.lastDrawDateStamp == locals.currentDateStamp) + { + return; + } - if (locals.getWinnerOutput.winnerAddress != id::zero()) - { - // Split revenue by configured percentages - locals.winnerAmount = div(locals.revenue * state.winnerFeePercent, 100ULL); - locals.teamFee = div(locals.revenue * state.teamFeePercent, 100ULL); - locals.distributionFee = div(locals.revenue * state.distributionFeePercent, 100ULL); - locals.burnedAmount = div(locals.revenue * state.burnPercent, 100ULL); - - // Team payout - if (locals.teamFee > 0) - { - qpi.transfer(state.teamAddress, locals.teamFee); - } + locals.isWednesday = (locals.currentDayOfWeek == WEDNESDAY); + locals.isScheduledToday = ((state.schedule & (1u << locals.currentDayOfWeek)) != 0); - // Distribution payout (equal per validator) - if (locals.distributionFee > 0) - { - qpi.distributeDividends(div(locals.distributionFee, uint64(NUMBER_OF_COMPUTORS))); - } + // Two-Wednesdays rule takes precedence over schedule: + // - First Wednesday (epoch start) is handled in BEGIN_EPOCH (we mark the day as processed), + // - Second (and any subsequent) Wednesday always performs draw and closes selling, + // - On other days, we draw only if the day is in schedule and re-open selling afterwards. + if (!locals.isWednesday && !locals.isScheduledToday) + { + return; // Non-Wednesday day that is not in schedule: nothing to do + } - // Winner payout - if (locals.winnerAmount > 0) - { - qpi.transfer(locals.getWinnerOutput.winnerAddress, locals.winnerAmount); - } + // Mark as processed for this calendar day and snapshot time + state.lastDrawDay = locals.currentDayOfWeek; + state.lastDrawHour = locals.currentHour; + state.lastDrawDateStamp = locals.currentDateStamp; - // Burn configured portion - if (locals.burnedAmount > 0) - { - qpi.burn(locals.burnedAmount); - } + // Disable for current draw period + enableBuyTicket(state, false); - // Store winner snapshot into history (ring buffer) - locals.fillWinnersInfoInput.winnerAddress = locals.getWinnerOutput.winnerAddress; - locals.fillWinnersInfoInput.revenue = locals.winnerAmount; - FillWinnersInfo(qpi, state, locals.fillWinnersInfoInput, locals.fillWinnersInfoOutput, locals.fillWinnersInfoLocals); + // Draw + { + if (state.playerCounter <= 1) + { + ReturnAllTickets(qpi, state, locals.returnAllTicketsInput, locals.returnAllTicketsOutput, locals.returnAllTicketsLocals); } else { - // Fallback: if winner couldn't be selected (should not happen), refund all tickets - ReturnAllTickets(qpi, state, locals.returnAllTicketsInput, locals.returnAllTicketsOutput, locals.returnAllTicketsLocals); + // Epoch revenue = incoming - outgoing for this contract + qpi.getEntity(SELF, locals.entity); + RLUtils::getSCRevenue(qpi, locals.entity, locals.revenue); + + // Winner selection (pseudo-random using K12(prevSpectrumDigest)). + getRandomPlayer(state, qpi, locals.randomNum, locals.winnerAddress); + + if (locals.winnerAddress != id::zero()) + { + // Split revenue by configured percentages + locals.winnerAmount = div(locals.revenue * state.winnerFeePercent, 100ULL); + locals.teamFee = div(locals.revenue * state.teamFeePercent, 100ULL); + locals.distributionFee = div(locals.revenue * state.distributionFeePercent, 100ULL); + locals.burnedAmount = div(locals.revenue * state.burnPercent, 100ULL); + + // Team payout + if (locals.teamFee > 0) + { + qpi.transfer(state.teamAddress, locals.teamFee); + } + + // Distribution payout (equal per validator) + if (locals.distributionFee > 0) + { + qpi.distributeDividends(div(locals.distributionFee, static_cast(NUMBER_OF_COMPUTORS))); + } + + // Winner payout + if (locals.winnerAmount > 0) + { + qpi.transfer(locals.winnerAddress, locals.winnerAmount); + } + + // Burn configured portion + if (locals.burnedAmount > 0) + { + qpi.burn(locals.burnedAmount); + } + + // Store winner snapshot into history (ring buffer) + locals.fillWinnersInfoInput.winnerAddress = locals.winnerAddress; + locals.fillWinnersInfoInput.revenue = locals.winnerAmount; + FillWinnersInfo(qpi, state, locals.fillWinnersInfoInput, locals.fillWinnersInfoOutput, locals.fillWinnersInfoLocals); + } + else + { + // Fallback: if winner couldn't be selected (should not happen), refund all tickets + ReturnAllTickets(qpi, state, locals.returnAllTicketsInput, locals.returnAllTicketsOutput, locals.returnAllTicketsLocals); + } } } - // Prepare for next epoch: clear players and apply deferred price if any - state.playerCounter = 0; + clearStateOnEndDraw(state); - if (state.nexEpochData.newPrice != 0) - { - state.ticketPrice = state.nexEpochData.newPrice; - state.nexEpochData.newPrice = 0; - } + // Re-enable ticket buying if today is not Wednesday + // On Wednesdays, selling remains closed until next BEGIN_EPOCH + enableBuyTicket(state, !locals.isWednesday); } /** @@ -430,7 +536,7 @@ struct RL : public ContractBase PUBLIC_FUNCTION(GetPlayers) { output.players = state.players; - output.playerCounter = min(state.playerCounter, state.players.capacity()); + output.playerCounter = RLUtils::min(state.playerCounter, state.players.capacity()); } /** @@ -439,17 +545,15 @@ struct RL : public ContractBase PUBLIC_FUNCTION(GetWinners) { output.winners = state.winners; - output.winnersCounter = min(state.winnersCounter, state.winners.capacity()); + output.winnersCounter = RLUtils::min(state.winnersCounter, state.winners.capacity()); } PUBLIC_FUNCTION(GetTicketPrice) { output.ticketPrice = state.ticketPrice; } PUBLIC_FUNCTION(GetMaxNumberOfPlayers) { output.numberOfPlayers = RL_MAX_NUMBER_OF_PLAYERS; } PUBLIC_FUNCTION(GetState) { output.currentState = static_cast(state.currentState); } - PUBLIC_FUNCTION_WITH_LOCALS(GetBalance) - { - // Returns balance for current epoch (incoming - outgoing) - output.balance = qpi.getEntity(SELF, locals.entity) ? locals.entity.incomingAmount - locals.entity.outgoingAmount : 0; - } + PUBLIC_FUNCTION(GetNextEpochData) { output.nextEpochData = state.nexEpochData; } + PUBLIC_FUNCTION(GetDrawHour) { output.drawHour = state.drawHour; } + PUBLIC_FUNCTION_WITH_LOCALS(GetBalance) { RLUtils::getSCRevenue(qpi, locals.entity, output.balance); } PUBLIC_PROCEDURE(SetPrice) { @@ -472,6 +576,24 @@ struct RL : public ContractBase output.returnCode = static_cast(EReturnCode::SUCCESS); } + PUBLIC_PROCEDURE(SetSchedule) + { + if (qpi.invocator() != state.teamAddress) + { + output.returnCode = static_cast(EReturnCode::ACCESS_DENIED); + return; + } + + if (input.newSchedule == 0) + { + output.returnCode = static_cast(EReturnCode::INVALID_VALUE); + return; + } + + state.nexEpochData.schedule = input.newSchedule; + output.returnCode = static_cast(EReturnCode::SUCCESS); + } + /** * @brief Attempts to buy tickets while SELLING state is active. * Logic: @@ -525,9 +647,9 @@ struct RL : public ContractBase } // Compute desired number of tickets and change - locals.desired = div(locals.reward, locals.price); // How many tickets the caller attempts to buy - locals.remainder = mod(locals.reward, locals.price); // Change to return - locals.toBuy = min(locals.desired, locals.slotsLeft); // Do not exceed available slots + locals.desired = div(locals.reward, locals.price); // How many tickets the caller attempts to buy + locals.remainder = mod(locals.reward, locals.price); // Change to return + locals.toBuy = RLUtils::min(locals.desired, locals.slotsLeft); // Do not exceed available slots // Add tickets (the same address may be inserted multiple times) for (locals.i = 0; locals.i < locals.toBuy; ++locals.i) @@ -535,7 +657,7 @@ struct RL : public ContractBase if (state.playerCounter < locals.capacity) { state.players.set(state.playerCounter, qpi.invocator()); - state.playerCounter = min(state.playerCounter + 1, locals.capacity); + state.playerCounter = RLUtils::min(state.playerCounter + 1, locals.capacity); } } @@ -562,34 +684,17 @@ struct RL : public ContractBase } // Use ring-buffer indexing to avoid overflow (overwrite oldest entries) - state.winnersCounter = mod(state.winnersCounter, state.winners.capacity()); + state.winnersCounter = mod(state.winnersCounter, state.winners.capacity()); locals.winnerInfo.winnerAddress = input.winnerAddress; locals.winnerInfo.revenue = input.revenue; locals.winnerInfo.epoch = qpi.epoch(); locals.winnerInfo.tick = qpi.tick(); + RLUtils::getCurrentDayOfWeek(qpi, locals.winnerInfo.dayOfWeek); state.winners.set(state.winnersCounter, locals.winnerInfo); ++state.winnersCounter; } - /** - * @brief Internal: pseudo-random selection of a winner index using hardware RNG. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(GetWinner) - { - if (state.playerCounter == 0) - { - return; - } - - // Compute pseudo-random index based on K12(prevSpectrumDigest) - locals.randomNum = mod(qpi.K12(qpi.getPrevSpectrumDigest()).u64._0, state.playerCounter); - - // Index directly into players array - output.winnerAddress = state.players.get(locals.randomNum); - output.index = locals.randomNum; - } - PRIVATE_PROCEDURE_WITH_LOCALS(ReturnAllTickets) { // Refund ticket price to each recorded player (one transfer per ticket entry) @@ -601,40 +706,33 @@ struct RL : public ContractBase protected: /** - * @brief Address of the team managing the lottery contract. - * Initialized to a zero address. - */ - id teamAddress = id::zero(); - - /** - * @brief Address of the owner of the lottery contract. - * Initialized to a zero address. + * @brief Circular buffer storing the history of winners. + * Maximum capacity is defined by RL_MAX_NUMBER_OF_WINNERS_IN_HISTORY. */ - id ownerAddress = id::zero(); + Array winners = {}; /** - * @brief Percentage of the revenue allocated to the team. - * Value is between 0 and 100. + * @brief Set of players participating in the current lottery epoch. + * Maximum capacity is defined by RL_MAX_NUMBER_OF_PLAYERS. */ - uint8 teamFeePercent = 0; + Array players = {}; /** - * @brief Percentage of the revenue allocated for distribution. - * Value is between 0 and 100. + * @brief Address of the team managing the lottery contract. + * Initialized to a zero address. */ - uint8 distributionFeePercent = 0; + id teamAddress = id::zero(); /** - * @brief Percentage of the revenue allocated to the winner. - * Automatically calculated as the remainder after other fees. + * @brief Address of the owner of the lottery contract. + * Initialized to a zero address. */ - uint8 winnerFeePercent = 0; + id ownerAddress = id::zero(); /** - * @brief Percentage of the revenue to be burned. - * Value is between 0 and 100. + * @brief Data structure for deferred changes to apply at the end of the epoch. */ - uint8 burnPercent = 0; + NextEpochData nexEpochData = {}; /** * @brief Price of a single lottery ticket. @@ -648,27 +746,42 @@ struct RL : public ContractBase uint64 playerCounter = 0; /** - * @brief Data structure for deferred changes to apply at the end of the epoch. + * @brief Index pointing to the next empty slot in the winners array. + * Used for maintaining the circular buffer of winners. */ - NextEpochData nexEpochData = {}; + uint64 winnersCounter = 0; + + uint8 lastDrawDay = RL_INVALID_DAY; + uint8 lastDrawHour = RL_INVALID_HOUR; + uint32 lastDrawDateStamp = 0; // calendar day marker to prevent multiple actions per day /** - * @brief Set of players participating in the current lottery epoch. - * Maximum capacity is defined by RL_MAX_NUMBER_OF_PLAYERS. + * @brief Percentage of the revenue allocated to the team. + * Value is between 0 and 100. */ - Array players = {}; + uint8 teamFeePercent = 0; /** - * @brief Circular buffer storing the history of winners. - * Maximum capacity is defined by RL_MAX_NUMBER_OF_WINNERS_IN_HISTORY. + * @brief Percentage of the revenue allocated for distribution. + * Value is between 0 and 100. */ - Array winners = {}; + uint8 distributionFeePercent = 0; /** - * @brief Index pointing to the next empty slot in the winners array. - * Used for maintaining the circular buffer of winners. + * @brief Percentage of the revenue allocated to the winner. + * Automatically calculated as the remainder after other fees. */ - uint64 winnersCounter = 0; + uint8 winnerFeePercent = 0; + + /** + * @brief Percentage of the revenue to be burned. + * Value is between 0 and 100. + */ + uint8 burnPercent = 0; + + uint8 schedule = 0; + + uint8 drawHour = 0; /** * @brief Current state of the lottery contract. @@ -677,5 +790,52 @@ struct RL : public ContractBase EState currentState = EState::LOCKED; protected: - template static constexpr const T& min(const T& a, const T& b) { return (a < b) ? a : b; } + static void clearStateOnEndEpoch(RL& state) + { + // Prepare for next epoch: clear players and apply deferred price if any + state.playerCounter = 0; + + state.lastDrawHour = RL_INVALID_HOUR; + state.lastDrawDay = RL_INVALID_DAY; + state.lastDrawDateStamp = 0; + } + + static void clearStateOnEndDraw(RL& state) + { + // Prepare for next draw period: clear players + state.playerCounter = 0; + } + + static void applyNextEpochData(RL& state) + { + if (state.nexEpochData.newPrice != 0) + { + state.ticketPrice = state.nexEpochData.newPrice; + state.nexEpochData.newPrice = 0; + } + + if (state.nexEpochData.schedule != 0) + { + state.schedule = state.nexEpochData.schedule; + state.nexEpochData.schedule = 0; + } + } + + static void enableBuyTicket(RL& state, bool bEnable) { state.currentState = bEnable ? EState::SELLING : EState::LOCKED; } + + static void getRandomPlayer(const RL& state, const QpiContextFunctionCall& qpi, uint64& randomNum, id& winnerAddress) + { + winnerAddress = id::zero(); + + if (state.playerCounter == 0) + { + return; + } + + // Compute pseudo-random index based on K12(prevSpectrumDigest) + randomNum = mod(qpi.K12(qpi.getPrevSpectrumDigest()).u64._0, state.playerCounter); + + // Index directly into players array + winnerAddress = state.players.get(randomNum); + } }; From 96b7e2957d825b3cc48fa14a604a4b06b674e2dd Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 30 Oct 2025 00:00:29 +0300 Subject: [PATCH 11/39] Add schedule management and draw hour functionality; implement GetSchedule and SetSchedule procedures --- src/contracts/RandomLottery.h | 15 ++- test/contract_rl.cpp | 238 +++++++++++++++++++++++++++++----- 2 files changed, 222 insertions(+), 31 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index a9712b41a..f326424b4 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -39,6 +39,8 @@ constexpr uint8 RL_INVALID_DAY = 255; constexpr uint8 RL_INVALID_HOUR = 255; +constexpr uint8 RL_TICK_UPDATE_PERIOD = 100; + namespace RLUtils { static void getCurrentDayOfWeek(const QpiContextFunctionCall& qpi, uint8& dayOfWeek) @@ -323,6 +325,15 @@ struct RL : public ContractBase uint8 drawHour = RL_INVALID_HOUR; }; + // New: expose current schedule mask + struct GetSchedule_input + { + }; + struct GetSchedule_output + { + uint8 schedule = 0; + }; + public: /** * @brief Registers all externally callable functions and procedures with their numeric @@ -339,6 +350,7 @@ struct RL : public ContractBase REGISTER_USER_FUNCTION(GetBalance, 7); REGISTER_USER_FUNCTION(GetNextEpochData, 8); REGISTER_USER_FUNCTION(GetDrawHour, 9); + REGISTER_USER_FUNCTION(GetSchedule, 10); REGISTER_USER_PROCEDURE(BuyTicket, 1); REGISTER_USER_PROCEDURE(SetPrice, 2); REGISTER_USER_PROCEDURE(SetSchedule, 3); @@ -410,7 +422,7 @@ struct RL : public ContractBase BEGIN_TICK_WITH_LOCALS() { // Only process once every 100 ticks - if (mod(qpi.tick(), 100u) != 0) + if (mod(qpi.tick(), static_cast(RL_TICK_UPDATE_PERIOD)) != 0) { return; } @@ -553,6 +565,7 @@ struct RL : public ContractBase PUBLIC_FUNCTION(GetState) { output.currentState = static_cast(state.currentState); } PUBLIC_FUNCTION(GetNextEpochData) { output.nextEpochData = state.nexEpochData; } PUBLIC_FUNCTION(GetDrawHour) { output.drawHour = state.drawHour; } + PUBLIC_FUNCTION(GetSchedule) { output.schedule = state.schedule; } PUBLIC_FUNCTION_WITH_LOCALS(GetBalance) { RLUtils::getSCRevenue(qpi, locals.entity, output.balance); } PUBLIC_PROCEDURE(SetPrice) diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index 6fd466e96..774c45093 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -5,6 +5,7 @@ constexpr uint16 PROCEDURE_INDEX_BUY_TICKET = 1; constexpr uint16 PROCEDURE_INDEX_SET_PRICE = 2; +constexpr uint16 PROCEDURE_INDEX_SET_SCHEDULE = 3; constexpr uint16 FUNCTION_INDEX_GET_FEES = 1; constexpr uint16 FUNCTION_INDEX_GET_PLAYERS = 2; constexpr uint16 FUNCTION_INDEX_GET_WINNERS = 3; @@ -12,14 +13,20 @@ constexpr uint16 FUNCTION_INDEX_GET_TICKET_PRICE = 4; constexpr uint16 FUNCTION_INDEX_GET_MAX_NUM_PLAYERS = 5; constexpr uint16 FUNCTION_INDEX_GET_STATE = 6; constexpr uint16 FUNCTION_INDEX_GET_BALANCE = 7; +constexpr uint16 FUNCTION_INDEX_GET_NEXT_EPOCH_DATA = 8; +constexpr uint16 FUNCTION_INDEX_GET_DRAW_HOUR = 9; +constexpr uint16 FUNCTION_INDEX_GET_SCHEDULE = 10; static const id RL_DEV_ADDRESS = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, _R, _L, _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); +constexpr uint8 RL_ANY_DAY_DRAW_SCHEDULE = 0xFF; + // Equality operator for comparing WinnerInfo objects bool operator==(const RL::WinnerInfo& left, const RL::WinnerInfo& right) { - return left.winnerAddress == right.winnerAddress && left.revenue == right.revenue && left.epoch == right.epoch && left.tick == right.tick; + return left.winnerAddress == right.winnerAddress && left.revenue == right.revenue && left.epoch == right.epoch && left.tick == right.tick && + left.dayOfWeek == right.dayOfWeek; } // Test helper that exposes internal state assertions @@ -88,9 +95,15 @@ class RLChecker : public RL } } + void setScheduleMask(uint8 newMask) { schedule = newMask; } + uint64 getPlayerCounter() const { return playerCounter; } uint64 getTicketPrice() const { return ticketPrice; } + + uint8 getScheduleMask() const { return schedule; } + + uint8 getDrawHourInternal() const { return drawHour; } }; class ContractTestingRL : protected ContractTesting @@ -170,6 +183,33 @@ class ContractTestingRL : protected ContractTesting return output; } + // Wrapper for public function RL::GetNextEpochData + RL::GetNextEpochData_output getNextEpochData() + { + RL::GetNextEpochData_input input; + RL::GetNextEpochData_output output; + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_NEXT_EPOCH_DATA, input, output); + return output; + } + + // Wrapper for public function RL::GetDrawHour + RL::GetDrawHour_output getDrawHour() + { + RL::GetDrawHour_input input; + RL::GetDrawHour_output output; + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_DRAW_HOUR, input, output); + return output; + } + + // Wrapper for public function RL::GetSchedule + RL::GetSchedule_output getSchedule() + { + RL::GetSchedule_input input; + RL::GetSchedule_output output; + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_SCHEDULE, input, output); + return output; + } + RL::BuyTicket_output buyTicket(const id& user, uint64 reward) { RL::BuyTicket_input input; @@ -194,10 +234,25 @@ class ContractTestingRL : protected ContractTesting return output; } + // Added: wrapper for SetSchedule procedure + RL::SetSchedule_output setSchedule(const id& invocator, uint8 newSchedule) + { + RL::SetSchedule_input input; + input.newSchedule = newSchedule; + RL::SetSchedule_output output; + if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_SET_SCHEDULE, input, output, invocator, 0)) + { + output.returnCode = static_cast(RL::EReturnCode::UNKNOWN_ERROR); + } + return output; + } + void BeginEpoch() { callSystemProcedure(RL_CONTRACT_INDEX, BEGIN_EPOCH); } void EndEpoch() { callSystemProcedure(RL_CONTRACT_INDEX, END_EPOCH); } + void BeginTick() { callSystemProcedure(RL_CONTRACT_INDEX, BEGIN_TICK); } + // Returns the SELF contract account address id rlSelf() { return id(RL_CONTRACT_INDEX, 0, 0, 0); } @@ -227,6 +282,61 @@ class ContractTestingRL : protected ContractTesting const RL::GetBalance_output out = ctl.getBalanceInfo(); EXPECT_EQ(out.balance, getBalance(contractAddress)); } + + void setCurrentHour(uint8 hour) + { + updateTime(); + utcTime.Hour = hour; + updateQpiTime(); + } + + // New: set full date and hour + void setDateTime(uint16 year, uint8 month, uint8 day, uint8 hour) + { + updateTime(); + utcTime.Year = year; + utcTime.Month = month; + utcTime.Day = day; + utcTime.Hour = hour; + utcTime.Minute = 0; + utcTime.Second = 0; + utcTime.Nanosecond = 0; + updateQpiTime(); + } + + // New: perform many BEGIN_TICK calls to ensure one execution when tick % 100 == 0 + void forceBeginTick() + { + system.tick = system.tick + (RL_TICK_UPDATE_PERIOD - mod(system.tick, static_cast(RL_TICK_UPDATE_PERIOD))); + + BeginTick(); + } + + // New: helper to advance one day ahead and try to draw at 12:00 + void advanceOneDayAndDraw() + { + // Use a safe base month to avoid invalid dates: January 2025 + static uint16 y = 2025; + static uint8 m = 1; + static uint8 d = 10; // start from 10th + // advance one day within January bounds + d = static_cast(d + 1); + if (d > 31) + { + d = 1; // wrap within month for simplicity in tests + } + setDateTime(y, m, d, 12); + forceBeginTick(); + } + + void forceSchedule(uint8 scheduleMask) + { + state()->setScheduleMask(scheduleMask); + // increaseEnergy(RL_DEV_ADDRESS, 1); + // BeginEpoch(); + // EXPECT_EQ(setSchedule(RL_DEV_ADDRESS, scheduleMask).returnCode, static_cast(RL::EReturnCode::SUCCESS)); + // EndEpoch(); + } }; TEST(ContractRandomLottery, GetFees) @@ -328,7 +438,8 @@ TEST(ContractRandomLottery, BuyTicket) EXPECT_EQ(ctl.state()->getPlayerCounter(), userCount * 2); } -TEST(ContractRandomLottery, EndEpoch) +// Updated: payout is triggered by BEGIN_TICK with schedule/time, not by END_EPOCH +TEST(ContractRandomLottery, DrawAndPayout_BeginTick) { ContractTestingRL ctl; @@ -342,18 +453,22 @@ TEST(ContractRandomLottery, EndEpoch) const uint8 burnPercent = fees.burnPercent; // Burn percent const uint8 winnerPercent = fees.winnerFeePercent; // Winner payout percent - // --- Scenario 1: No players (should just lock and clear silently) --- + // Ensure schedule allows draw any day + ctl.forceSchedule(RL_ANY_DAY_DRAW_SCHEDULE); + + // --- Scenario 1: No players (should just clear silently) --- { ctl.BeginEpoch(); EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); RL::GetWinners_output before = ctl.getWinners(); - EXPECT_EQ(before.winnersCounter, 0u); + const uint64 winnersBefore = before.winnersCounter; - ctl.EndEpoch(); + // Need to move to a new day and call BEGIN_TICK to allow draw + ctl.advanceOneDayAndDraw(); RL::GetWinners_output after = ctl.getWinners(); - EXPECT_EQ(after.winnersCounter, 0u); + EXPECT_EQ(after.winnersCounter, winnersBefore); EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); } @@ -370,14 +485,17 @@ TEST(ContractRandomLottery, EndEpoch) EXPECT_EQ(ctl.state()->getPlayerCounter(), 1u); EXPECT_EQ(getBalance(solo), balanceBefore - ticketPrice); - ctl.EndEpoch(); + const uint64 winnersBeforeCount = ctl.getWinners().winnersCounter; + + ctl.advanceOneDayAndDraw(); // Refund happened EXPECT_EQ(getBalance(solo), balanceBefore); EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); const RL::GetWinners_output winners = ctl.getWinners(); - EXPECT_EQ(winners.winnersCounter, 0u); + // No new winners appended + EXPECT_EQ(winners.winnersCounter, winnersBeforeCount); } // --- Scenario 3: Multiple players (winner chosen, fees processed, remainder burned) --- @@ -414,16 +532,16 @@ TEST(ContractRandomLottery, EndEpoch) const RL::GetWinners_output winnersBefore = ctl.getWinners(); const uint64 winnersCountBefore = winnersBefore.winnersCounter; - ctl.EndEpoch(); + ctl.advanceOneDayAndDraw(); - // Players reset after epoch end + // Players reset after draw EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); const RL::GetWinners_output winnersAfter = ctl.getWinners(); EXPECT_EQ(winnersAfter.winnersCounter, winnersCountBefore + 1); // Newly appended winner info - const RL::WinnerInfo wi = winnersAfter.winners.get(winnersCountBefore); + const RL::WinnerInfo wi = winnersAfter.winners.get(mod(winnersCountBefore, winnersAfter.winners.capacity())); EXPECT_NE(wi.winnerAddress, id::zero()); EXPECT_EQ(wi.revenue, (ticketPrice * N * winnerPercent) / 100); @@ -456,7 +574,7 @@ TEST(ContractRandomLottery, EndEpoch) EXPECT_EQ(getBalance(contractAddress), burnExpected); } - // --- Scenario 4: Several consecutive epochs (winners accumulate, balances consistent) --- + // --- Scenario 4: Several consecutive draws (winners accumulate, balances consistent) --- { const uint32 rounds = 3; const uint32 playersPerRound = 6 * 2; // even number to mimic duplicates if desired @@ -495,14 +613,14 @@ TEST(ContractRandomLottery, EndEpoch) const uint64 contractBefore = getBalance(contractAddress); const uint64 teamBalBeforeRound = getBalance(RL_DEV_ADDRESS); - ctl.EndEpoch(); + ctl.advanceOneDayAndDraw(); // Winners should increase by exactly one const RL::GetWinners_output wOut = ctl.getWinners(); EXPECT_EQ(wOut.winnersCounter, winnersBefore + 1); // Validate winner entry - const RL::WinnerInfo newWi = wOut.winners.get(winnersBefore); + const RL::WinnerInfo newWi = wOut.winners.get(mod(winnersBefore, wOut.winners.capacity())); EXPECT_NE(newWi.winnerAddress, id::zero()); EXPECT_EQ(newWi.revenue, (contractBefore * winnerPercent) / 100); @@ -567,17 +685,19 @@ TEST(ContractRandomLottery, GetBalance) ctl.expectContractBalanceEqualsGetBalance(ctl, contractAddress); } - // Before ending the epoch, balance equals the total cost of tickets + // Before draw, balance equals the total cost of tickets { const RL::GetBalance_output outBefore = ctl.getBalanceInfo(); EXPECT_EQ(outBefore.balance, ticketPrice * K); } - // End epoch and verify expected remaining amount against contract balance and function output + // Trigger draw and verify expected remaining amount against contract balance and function output const uint64 contractBalanceBefore = getBalance(contractAddress); const RL::GetFees_output fees = ctl.getFees(); - ctl.EndEpoch(); + // Ensure schedule allows draw and perform it + ctl.forceSchedule(RL_ANY_DAY_DRAW_SCHEDULE); + ctl.advanceOneDayAndDraw(); const RL::GetBalance_output outAfter = ctl.getBalanceInfo(); const uint64 envAfter = getBalance(contractAddress); @@ -600,7 +720,7 @@ TEST(ContractRandomLottery, GetMaxNumberOfPlayers) ContractTestingRL ctl; const RL::GetMaxNumberOfPlayers_output out = ctl.getMaxNumberOfPlayers(); - // Compare against the players array capacity, fetched via GetPlayers + // Compare against the known constant via GetPlayers capacity const RL::GetPlayers_output playersOut = ctl.getPlayers(); EXPECT_EQ(static_cast(out.numberOfPlayers), static_cast(playersOut.players.capacity())); } @@ -622,7 +742,7 @@ TEST(ContractRandomLottery, GetState) EXPECT_EQ(out1.currentState, static_cast(RL::EState::SELLING)); } - // After EndEpoch — back to LOCKED + // After END_EPOCH — back to LOCKED (selling disabled until next epoch) ctl.EndEpoch(); { const RL::GetState_output out2 = ctl.getStateInfo(); @@ -630,7 +750,7 @@ TEST(ContractRandomLottery, GetState) } } -// --- New tests for SetPrice --- +// --- New tests for SetPrice and NextEpochData --- TEST(ContractRandomLottery, SetPrice_AccessControl) { @@ -646,7 +766,7 @@ TEST(ContractRandomLottery, SetPrice_AccessControl) const RL::SetPrice_output outDenied = ctl.setPrice(randomUser, newPrice); EXPECT_EQ(outDenied.returnCode, static_cast(RL::EReturnCode::ACCESS_DENIED)); - // Price doesn't change immediately nor after EndEpoch + // Price doesn't change immediately nor after END_EPOCH implicitly EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); ctl.EndEpoch(); EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); @@ -663,7 +783,7 @@ TEST(ContractRandomLottery, SetPrice_ZeroNotAllowed) const RL::SetPrice_output outInvalid = ctl.setPrice(RL_DEV_ADDRESS, 0); EXPECT_EQ(outInvalid.returnCode, static_cast(RL::EReturnCode::TICKET_INVALID_PRICE)); - // Price remains unchanged even after EndEpoch + // Price remains unchanged even after END_EPOCH EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); ctl.EndEpoch(); EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); @@ -681,14 +801,20 @@ TEST(ContractRandomLottery, SetPrice_AppliesAfterEndEpoch) const RL::SetPrice_output outOk = ctl.setPrice(RL_DEV_ADDRESS, newPrice); EXPECT_EQ(outOk.returnCode, static_cast(RL::EReturnCode::SUCCESS)); - // Until EndEpoch the price remains unchanged + // Check NextEpochData reflects pending change + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.newPrice, newPrice); + + // Until END_EPOCH the price remains unchanged EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); - // Applied after EndEpoch + // Applied after END_EPOCH ctl.EndEpoch(); EXPECT_EQ(ctl.getTicketPrice().ticketPrice, newPrice); - // Another EndEpoch without a new SetPrice doesn't change the price + // NextEpochData cleared + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.newPrice, 0u); + + // Another END_EPOCH without a new SetPrice doesn't change the price ctl.EndEpoch(); EXPECT_EQ(ctl.getTicketPrice().ticketPrice, newPrice); } @@ -703,11 +829,14 @@ TEST(ContractRandomLottery, SetPrice_OverrideBeforeEndEpoch) const uint64 firstPrice = oldPrice + 1000; const uint64 secondPrice = oldPrice + 7777; - // Two SetPrice calls before EndEpoch — the last one should apply + // Two SetPrice calls before END_EPOCH — the last one should apply EXPECT_EQ(ctl.setPrice(RL_DEV_ADDRESS, firstPrice).returnCode, static_cast(RL::EReturnCode::SUCCESS)); EXPECT_EQ(ctl.setPrice(RL_DEV_ADDRESS, secondPrice).returnCode, static_cast(RL::EReturnCode::SUCCESS)); - // Until EndEpoch the old price remains + // NextEpochData shows the last queued value + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.newPrice, secondPrice); + + // Until END_EPOCH the old price remains EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); ctl.EndEpoch(); @@ -732,10 +861,11 @@ TEST(ContractRandomLottery, SetPrice_AffectsNextEpochBuys) EXPECT_EQ(out1.returnCode, static_cast(RL::EReturnCode::SUCCESS)); } - // Set a new price, but before EndEpoch purchases should use the old price + // Set a new price, but before END_EPOCH purchases should use the old price logic (split by old price) { const RL::SetPrice_output setOut = ctl.setPrice(RL_DEV_ADDRESS, newPrice); EXPECT_EQ(setOut.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.newPrice, newPrice); } const id u2 = id::randomValue(); @@ -751,11 +881,11 @@ TEST(ContractRandomLottery, SetPrice_AffectsNextEpochBuys) EXPECT_EQ(getBalance(u2), balBefore - bought * oldPrice); } - // End the epoch: new price will apply + // END_EPOCH: new price will apply ctl.EndEpoch(); EXPECT_EQ(ctl.getTicketPrice().ticketPrice, newPrice); - // In the next epoch, a purchase at the new price should succeed + // In the next epoch, a purchase at the new price should succeed exactly once per price ctl.BeginEpoch(); { const uint64 balBefore = getBalance(u2); @@ -850,3 +980,51 @@ TEST(ContractRandomLottery, BuyMultipleTickets_AllSoldOut) EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::TICKET_ALL_SOLD_OUT)); EXPECT_EQ(getBalance(buyer), balBefore); } + +// functions related to schedule and draw hour + +TEST(ContractRandomLottery, GetSchedule_And_SetSchedule) +{ + ContractTestingRL ctl; + + // Default schedule set on initialize: Wednesday bit must be set + const RL::GetSchedule_output s0 = ctl.getSchedule(); + EXPECT_NE(s0.schedule, 0u); + + // Access control: random user cannot set schedule + const id rnd = id::randomValue(); + increaseEnergy(rnd, 1); + const RL::SetSchedule_output outDenied = ctl.setSchedule(rnd, RL_ANY_DAY_DRAW_SCHEDULE); + EXPECT_EQ(outDenied.returnCode, static_cast(RL::EReturnCode::ACCESS_DENIED)); + + // Invalid value: zero mask not allowed + increaseEnergy(RL_DEV_ADDRESS, 1); + const RL::SetSchedule_output outInvalid = ctl.setSchedule(RL_DEV_ADDRESS, 0); + EXPECT_EQ(outInvalid.returnCode, static_cast(RL::EReturnCode::INVALID_VALUE)); + + // Valid update queues into NextEpochData and applies after END_EPOCH + const uint8 newMask = 0x5A; // some non-zero mask + const RL::SetSchedule_output outOk = ctl.setSchedule(RL_DEV_ADDRESS, newMask); + EXPECT_EQ(outOk.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.schedule, newMask); + + // Not applied yet + EXPECT_NE(ctl.getSchedule().schedule, newMask); + + // Apply + ctl.EndEpoch(); + EXPECT_EQ(ctl.getSchedule().schedule, newMask); + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.schedule, 0u); +} + +TEST(ContractRandomLottery, GetDrawHour_DefaultAfterBeginEpoch) +{ + ContractTestingRL ctl; + + // Initially drawHour is 0 + EXPECT_EQ(ctl.getDrawHour().drawHour, 0u); + + // After BeginEpoch default is 11 + ctl.BeginEpoch(); + EXPECT_EQ(ctl.getDrawHour().drawHour, 11u); +} From 8b278edec0123555a420dfd816fda8887b9a06c1 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 30 Oct 2025 00:11:48 +0300 Subject: [PATCH 12/39] Enhance lottery contract with detailed comments and clarify draw scheduling logic; update state management for draw hour and schedule --- src/contracts/RandomLottery.h | 75 +++++++++++++++++++++++------------ test/contract_rl.cpp | 34 ++++++++-------- 2 files changed, 67 insertions(+), 42 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index f326424b4..07d7a1e32 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -5,7 +5,7 @@ * * This header declares the RL (Random Lottery) contract which: * - Sells tickets during a SELLING epoch. - * - Draws a pseudo-random winner when the epoch ends. + * - Draws a pseudo-random winner when the epoch ends or at scheduled intra-epoch draws. * - Distributes fees (team, distribution, burn, winner). * - Records winners' history in a ring-like buffer. * @@ -13,6 +13,8 @@ * - Percentages must sum to <= 100; the remainder goes to the winner. * - Players array stores one entry per ticket, so a single address can appear multiple times. * - When only one player bought a ticket in the epoch, funds are refunded instead of drawing. + * - Day-of-week mapping used here is 0..6 where 0 = WEDNESDAY, 1 = THURSDAY, ..., 6 = TUESDAY. + * - Schedule uses a 7-bit mask aligned to the mapping above (bit 0 -> WEDNESDAY, bit 6 -> TUESDAY). */ using namespace QPI; @@ -35,24 +37,33 @@ constexpr uint8 RL_SHAREHOLDER_FEE_PERCENT = 20; /// Burn percent of epoch revenue (0..100). constexpr uint8 RL_BURN_PERCENT = 2; +/// Sentinel for "no valid day". constexpr uint8 RL_INVALID_DAY = 255; +/// Sentinel for "no valid hour". constexpr uint8 RL_INVALID_HOUR = 255; +/// Throttling period: process BEGIN_TICK logic once per this many ticks. constexpr uint8 RL_TICK_UPDATE_PERIOD = 100; +/// Default draw hour (UTC). +constexpr uint8 RL_DEFAULT_DRAW_HOUR = 11; // 11:00 UTC + namespace RLUtils { + // Returns current day-of-week in range [0..6], with 0 = WEDNESDAY according to platform mapping. static void getCurrentDayOfWeek(const QpiContextFunctionCall& qpi, uint8& dayOfWeek) { dayOfWeek = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); } + // Packs current date into a compact stamp (Y/M/D) used to ensure a single action per calendar day. static void makeDateStamp(const QpiContextFunctionCall& qpi, uint32& res) { res = static_cast(qpi.year() << 9 | qpi.month() << 5 | qpi.day()); } + // Reads current net on-chain balance of SELF (incoming - outgoing). static void getSCRevenue(const QpiContextFunctionCall& qpi, Entity& entity, uint64& revenue) { qpi.getEntity(SELF, entity); @@ -119,7 +130,7 @@ struct RL : public ContractBase struct NextEpochData { uint64 newPrice = 0; // Ticket price to apply after END_EPOCH; 0 means "no change queued" - uint8 schedule = 0; + uint8 schedule = 0; // Schedule bitmask (bit 0 = WEDNESDAY, ..., bit 6 = TUESDAY); applied after END_EPOCH }; //---- User-facing I/O structures ------------------------------------------------------------- @@ -228,7 +239,7 @@ struct RL : public ContractBase struct GetBalance_output { - uint64 balance = 0; // Net balance (incoming - outgoing) for current epoch + uint64 balance = 0; // Current contract net balance (incoming - outgoing) }; // Local variables for GetBalance procedure @@ -393,21 +404,21 @@ struct RL : public ContractBase { if (state.schedule == 0) { - // Default to WEDNESDAY if no schedule is set + // Default to WEDNESDAY if no schedule is set (bit 0) state.schedule = 1 << WEDNESDAY; } if (state.drawHour == 0) { - state.drawHour = 11; // Default to 11 UTC + state.drawHour = RL_DEFAULT_DRAW_HOUR; // Default draw hour (UTC) } - // Mark the current day as already processed to avoid immediate toggling on the same day + // Mark the current date as already processed to avoid immediate draw on the same calendar day RLUtils::getCurrentDayOfWeek(qpi, state.lastDrawDay); state.lastDrawHour = state.drawHour; - // Force lastDrawDateStamp to today's date to prevent reprocessing RLUtils::makeDateStamp(qpi, state.lastDrawDateStamp); + // Open selling for the new epoch enableBuyTicket(state, true); } @@ -421,23 +432,23 @@ struct RL : public ContractBase BEGIN_TICK_WITH_LOCALS() { - // Only process once every 100 ticks + // Throttle: run logic only once per RL_TICK_UPDATE_PERIOD ticks if (mod(qpi.tick(), static_cast(RL_TICK_UPDATE_PERIOD)) != 0) { return; } - // Compute current day and hour + // Snapshot current day/hour RLUtils::getCurrentDayOfWeek(qpi, locals.currentDayOfWeek); locals.currentHour = qpi.hour(); - // Only consider actions at or after the configured draw hour + // Do nothing before the configured draw hour if (locals.currentHour < state.drawHour) { return; } - // Allow only one state change action per calendar day + // Ensure only one action per calendar day (UTC) RLUtils::makeDateStamp(qpi, locals.currentDateStamp); if (state.lastDrawDateStamp == locals.currentDateStamp) { @@ -447,21 +458,21 @@ struct RL : public ContractBase locals.isWednesday = (locals.currentDayOfWeek == WEDNESDAY); locals.isScheduledToday = ((state.schedule & (1u << locals.currentDayOfWeek)) != 0); - // Two-Wednesdays rule takes precedence over schedule: - // - First Wednesday (epoch start) is handled in BEGIN_EPOCH (we mark the day as processed), - // - Second (and any subsequent) Wednesday always performs draw and closes selling, - // - On other days, we draw only if the day is in schedule and re-open selling afterwards. + // Two-Wednesdays rule: + // - First Wednesday (epoch start) is "consumed" in BEGIN_EPOCH (we set lastDrawDateStamp), + // - Any subsequent Wednesday performs a draw and leaves selling CLOSED until next BEGIN_EPOCH, + // - Any other day performs a draw only if included in schedule and then re-opens selling. if (!locals.isWednesday && !locals.isScheduledToday) { - return; // Non-Wednesday day that is not in schedule: nothing to do + return; // Non-Wednesday day that is not scheduled: nothing to do } - // Mark as processed for this calendar day and snapshot time + // Mark today's action and timestamp state.lastDrawDay = locals.currentDayOfWeek; state.lastDrawHour = locals.currentHour; state.lastDrawDateStamp = locals.currentDateStamp; - // Disable for current draw period + // Temporarily close selling for the draw enableBuyTicket(state, false); // Draw @@ -526,8 +537,7 @@ struct RL : public ContractBase clearStateOnEndDraw(state); - // Re-enable ticket buying if today is not Wednesday - // On Wednesdays, selling remains closed until next BEGIN_EPOCH + // Resume selling unless today is Wednesday (remains closed until next epoch) enableBuyTicket(state, !locals.isWednesday); } @@ -688,6 +698,7 @@ struct RL : public ContractBase private: /** * @brief Internal: records a winner into the cyclic winners array. + * Overwrites oldest entries when capacity is exceeded (ring buffer). */ PRIVATE_PROCEDURE_WITH_LOCALS(FillWinnersInfo) { @@ -710,7 +721,7 @@ struct RL : public ContractBase PRIVATE_PROCEDURE_WITH_LOCALS(ReturnAllTickets) { - // Refund ticket price to each recorded player (one transfer per ticket entry) + // Refund ticket price to each recorded ticket entry (1 transfer per entry) for (locals.i = 0; locals.i < state.playerCounter; ++locals.i) { qpi.transfer(state.players.get(locals.i), state.ticketPrice); @@ -764,9 +775,13 @@ struct RL : public ContractBase */ uint64 winnersCounter = 0; + /** + * @brief Date/time guard for draw operations. + * lastDrawDateStamp prevents more than one action per calendar day (UTC). + */ uint8 lastDrawDay = RL_INVALID_DAY; uint8 lastDrawHour = RL_INVALID_HOUR; - uint32 lastDrawDateStamp = 0; // calendar day marker to prevent multiple actions per day + uint32 lastDrawDateStamp = 0; // Compact YYYY/MM/DD marker /** * @brief Percentage of the revenue allocated to the team. @@ -792,20 +807,28 @@ struct RL : public ContractBase */ uint8 burnPercent = 0; + /** + * @brief Schedule bitmask: bit 0 = WEDNESDAY, 1 = THURSDAY, ..., 6 = TUESDAY. + * If a bit is set, a draw may occur on that day (subject to drawHour and daily guard). + * Wednesday also follows the "Two-Wednesdays rule" (selling stays closed after Wednesday draw). + */ uint8 schedule = 0; + /** + * @brief UTC hour [0..23] when a draw is allowed to run (daily time gate). + */ uint8 drawHour = 0; /** * @brief Current state of the lottery contract. - * Can be either SELLING (tickets available) or LOCKED (epoch closed). + * SELLING: tickets available; LOCKED: selling closed. */ EState currentState = EState::LOCKED; protected: static void clearStateOnEndEpoch(RL& state) { - // Prepare for next epoch: clear players and apply deferred price if any + // Prepare for next epoch: clear players and reset daily guards state.playerCounter = 0; state.lastDrawHour = RL_INVALID_HOUR; @@ -815,18 +838,20 @@ struct RL : public ContractBase static void clearStateOnEndDraw(RL& state) { - // Prepare for next draw period: clear players + // After each draw period, clear current tickets state.playerCounter = 0; } static void applyNextEpochData(RL& state) { + // Apply deferred ticket price (if any) if (state.nexEpochData.newPrice != 0) { state.ticketPrice = state.nexEpochData.newPrice; state.nexEpochData.newPrice = 0; } + // Apply deferred schedule (if any) if (state.nexEpochData.schedule != 0) { state.schedule = state.nexEpochData.schedule; diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index 774c45093..d262ac4da 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -23,13 +23,14 @@ static const id RL_DEV_ADDRESS = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, constexpr uint8 RL_ANY_DAY_DRAW_SCHEDULE = 0xFF; // Equality operator for comparing WinnerInfo objects +// Compares all fields (address, revenue, epoch, tick, dayOfWeek) bool operator==(const RL::WinnerInfo& left, const RL::WinnerInfo& right) { return left.winnerAddress == right.winnerAddress && left.revenue == right.revenue && left.epoch == right.epoch && left.tick == right.tick && left.dayOfWeek == right.dayOfWeek; } -// Test helper that exposes internal state assertions +// Test helper that exposes internal state assertions and utilities class RLChecker : public RL { public: @@ -175,6 +176,7 @@ class ContractTestingRL : protected ContractTesting } // Wrapper for public function RL::GetBalance + // Returns current contract on-chain balance (incoming - outgoing) RL::GetBalance_output getBalanceInfo() { RL::GetBalance_input input; @@ -290,7 +292,7 @@ class ContractTestingRL : protected ContractTesting updateQpiTime(); } - // New: set full date and hour + // New: set full date and hour (UTC), then sync QPI time void setDateTime(uint16 year, uint8 month, uint8 day, uint8 hour) { updateTime(); @@ -304,7 +306,7 @@ class ContractTestingRL : protected ContractTesting updateQpiTime(); } - // New: perform many BEGIN_TICK calls to ensure one execution when tick % 100 == 0 + // New: advance to the next tick boundary where tick % RL_TICK_UPDATE_PERIOD == 0 and run BEGIN_TICK once void forceBeginTick() { system.tick = system.tick + (RL_TICK_UPDATE_PERIOD - mod(system.tick, static_cast(RL_TICK_UPDATE_PERIOD))); @@ -312,7 +314,7 @@ class ContractTestingRL : protected ContractTesting BeginTick(); } - // New: helper to advance one day ahead and try to draw at 12:00 + // New: helper to advance one calendar day and perform a scheduled draw at 12:00 UTC void advanceOneDayAndDraw() { // Use a safe base month to avoid invalid dates: January 2025 @@ -329,13 +331,11 @@ class ContractTestingRL : protected ContractTesting forceBeginTick(); } + // Force schedule mask directly in state (bypasses external call, suitable for tests) void forceSchedule(uint8 scheduleMask) { state()->setScheduleMask(scheduleMask); - // increaseEnergy(RL_DEV_ADDRESS, 1); - // BeginEpoch(); - // EXPECT_EQ(setSchedule(RL_DEV_ADDRESS, scheduleMask).returnCode, static_cast(RL::EReturnCode::SUCCESS)); - // EndEpoch(); + // NOTE: we do not call SetSchedule here to avoid epoch transitions in tests. } }; @@ -438,7 +438,7 @@ TEST(ContractRandomLottery, BuyTicket) EXPECT_EQ(ctl.state()->getPlayerCounter(), userCount * 2); } -// Updated: payout is triggered by BEGIN_TICK with schedule/time, not by END_EPOCH +// Updated: payout is triggered by BEGIN_TICK with schedule/time gating, not by END_EPOCH TEST(ContractRandomLottery, DrawAndPayout_BeginTick) { ContractTestingRL ctl; @@ -456,7 +456,7 @@ TEST(ContractRandomLottery, DrawAndPayout_BeginTick) // Ensure schedule allows draw any day ctl.forceSchedule(RL_ANY_DAY_DRAW_SCHEDULE); - // --- Scenario 1: No players (should just clear silently) --- + // --- Scenario 1: No players (nothing to payout, no winner recorded) --- { ctl.BeginEpoch(); EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); @@ -498,7 +498,7 @@ TEST(ContractRandomLottery, DrawAndPayout_BeginTick) EXPECT_EQ(winners.winnersCounter, winnersBeforeCount); } - // --- Scenario 3: Multiple players (winner chosen, fees processed, remainder burned) --- + // --- Scenario 3: Multiple players (winner chosen, fees processed, correct remaining on contract) --- { ctl.BeginEpoch(); @@ -644,7 +644,7 @@ TEST(ContractRandomLottery, DrawAndPayout_BeginTick) EXPECT_EQ(b, expected); } - // Team fee for the whole contract balance of the round + // Team fee for the whole round's contract balance const uint64 teamFee = (contractBefore * teamPercent) / 100; teamAccrued += teamFee; EXPECT_EQ(getBalance(RL_DEV_ADDRESS), teamBalBeforeRound + teamFee); @@ -987,7 +987,7 @@ TEST(ContractRandomLottery, GetSchedule_And_SetSchedule) { ContractTestingRL ctl; - // Default schedule set on initialize: Wednesday bit must be set + // Default schedule set on initialize must include Wednesday (bit 0) const RL::GetSchedule_output s0 = ctl.getSchedule(); EXPECT_NE(s0.schedule, 0u); @@ -1003,7 +1003,7 @@ TEST(ContractRandomLottery, GetSchedule_And_SetSchedule) EXPECT_EQ(outInvalid.returnCode, static_cast(RL::EReturnCode::INVALID_VALUE)); // Valid update queues into NextEpochData and applies after END_EPOCH - const uint8 newMask = 0x5A; // some non-zero mask + const uint8 newMask = 0x5A; // some non-zero mask (bits set for selected days) const RL::SetSchedule_output outOk = ctl.setSchedule(RL_DEV_ADDRESS, newMask); EXPECT_EQ(outOk.returnCode, static_cast(RL::EReturnCode::SUCCESS)); EXPECT_EQ(ctl.getNextEpochData().nextEpochData.schedule, newMask); @@ -1021,10 +1021,10 @@ TEST(ContractRandomLottery, GetDrawHour_DefaultAfterBeginEpoch) { ContractTestingRL ctl; - // Initially drawHour is 0 + // Initially drawHour is 0 (not configured) EXPECT_EQ(ctl.getDrawHour().drawHour, 0u); - // After BeginEpoch default is 11 + // After BeginEpoch default is 11 UTC ctl.BeginEpoch(); - EXPECT_EQ(ctl.getDrawHour().drawHour, 11u); + EXPECT_EQ(ctl.getDrawHour().drawHour, RL_DEFAULT_DRAW_HOUR); } From 57a3fc0b409d4327502e60156700bf44e24fd4ee Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 30 Oct 2025 00:23:36 +0300 Subject: [PATCH 13/39] Refactor winner management logic; improve comments and introduce getWinnerCounter utility function for better clarity and maintainability --- src/contracts/RandomLottery.h | 17 ++++++++++------- test/contract_rl.cpp | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 07d7a1e32..8f3759737 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -193,6 +193,7 @@ struct RL : public ContractBase struct FillWinnersInfo_locals { WinnerInfo winnerInfo = {}; // Temporary buffer to compose a WinnerInfo record + uint64 insertIdx = 0; // Index in ring buffer where to insert new winner }; struct GetWinners_input @@ -483,8 +484,7 @@ struct RL : public ContractBase } else { - // Epoch revenue = incoming - outgoing for this contract - qpi.getEntity(SELF, locals.entity); + // Current contract net balance = incoming - outgoing for this contract RLUtils::getSCRevenue(qpi, locals.entity, locals.revenue); // Winner selection (pseudo-random using K12(prevSpectrumDigest)). @@ -567,7 +567,7 @@ struct RL : public ContractBase PUBLIC_FUNCTION(GetWinners) { output.winners = state.winners; - output.winnersCounter = RLUtils::min(state.winnersCounter, state.winners.capacity()); + getWinnerCounter(state, output.winnersCounter); } PUBLIC_FUNCTION(GetTicketPrice) { output.ticketPrice = state.ticketPrice; } @@ -707,16 +707,17 @@ struct RL : public ContractBase return; // Nothing to store } - // Use ring-buffer indexing to avoid overflow (overwrite oldest entries) - state.winnersCounter = mod(state.winnersCounter, state.winners.capacity()); + // Compute ring-buffer index without clamping the total counter + getWinnerCounter(state, locals.insertIdx); + ++state.winnersCounter; locals.winnerInfo.winnerAddress = input.winnerAddress; locals.winnerInfo.revenue = input.revenue; locals.winnerInfo.epoch = qpi.epoch(); locals.winnerInfo.tick = qpi.tick(); RLUtils::getCurrentDayOfWeek(qpi, locals.winnerInfo.dayOfWeek); - state.winners.set(state.winnersCounter, locals.winnerInfo); - ++state.winnersCounter; + + state.winners.set(locals.insertIdx, locals.winnerInfo); } PRIVATE_PROCEDURE_WITH_LOCALS(ReturnAllTickets) @@ -876,4 +877,6 @@ struct RL : public ContractBase // Index directly into players array winnerAddress = state.players.get(randomNum); } + + static void getWinnerCounter(const RL& state, uint64& outCounter) { outCounter = mod(state.winnersCounter, state.winners.capacity()); } }; diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index d262ac4da..c213f1601 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -20,7 +20,7 @@ constexpr uint16 FUNCTION_INDEX_GET_SCHEDULE = 10; static const id RL_DEV_ADDRESS = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, _R, _L, _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); -constexpr uint8 RL_ANY_DAY_DRAW_SCHEDULE = 0xFF; +constexpr uint8 RL_ANY_DAY_DRAW_SCHEDULE = 0xFF; // 0xFF sets bits 0..6 (WED..TUE); bit 7 is unused/ignored by logic // Equality operator for comparing WinnerInfo objects // Compares all fields (address, revenue, epoch, tick, dayOfWeek) From 8a6d47eb87cd92508a8b3e5e38c837ea89609a34 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 30 Oct 2025 00:31:25 +0300 Subject: [PATCH 14/39] Update winnersCounter calculation to reflect valid entries based on capacity; enhance test expectations for accuracy --- src/contracts/RandomLottery.h | 2 +- test/contract_rl.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 8f3759737..b285d67f7 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -203,7 +203,7 @@ struct RL : public ContractBase struct GetWinners_output { Array winners; // Ring buffer snapshot - uint64 winnersCounter = 0; // Number of valid entries (bounded by capacity) + uint64 winnersCounter = 0; // Number of valid entries = (totalWinners % capacity) uint8 returnCode = static_cast(EReturnCode::SUCCESS); }; diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index c213f1601..539623a4f 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -62,9 +62,11 @@ class RLChecker : public RL { EXPECT_EQ(output.returnCode, static_cast(EReturnCode::SUCCESS)); EXPECT_EQ(output.winners.capacity(), winners.capacity()); - EXPECT_EQ(output.winnersCounter, winnersCounter); - for (uint64 i = 0; i < winnersCounter; ++i) + const uint64 expectedCount = mod(winnersCounter, winners.capacity()); + EXPECT_EQ(output.winnersCounter, expectedCount); + + for (uint64 i = 0; i < expectedCount; ++i) { EXPECT_EQ(output.winners.get(i), winners.get(i)); } From 4b20e8d6e9645b7c048b44a5bc3cb4bab3670cca Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 30 Oct 2025 00:40:04 +0300 Subject: [PATCH 15/39] Fix parameter type in getRandomPlayer function to use QpiContextProcedureCall for consistency --- src/contracts/RandomLottery.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index b285d67f7..0c513f457 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -862,7 +862,7 @@ struct RL : public ContractBase static void enableBuyTicket(RL& state, bool bEnable) { state.currentState = bEnable ? EState::SELLING : EState::LOCKED; } - static void getRandomPlayer(const RL& state, const QpiContextFunctionCall& qpi, uint64& randomNum, id& winnerAddress) + static void getRandomPlayer(const RL& state, const QpiContextProcedureCall& qpi, uint64& randomNum, id& winnerAddress) { winnerAddress = id::zero(); From 80db11807f0943af922be985da6137c97f35cbf8 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 30 Oct 2025 00:46:32 +0300 Subject: [PATCH 16/39] Refactor winner selection logic; inline getRandomPlayer functionality for improved clarity and maintainability --- src/contracts/RandomLottery.h | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 0c513f457..7e85a977b 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -488,7 +488,18 @@ struct RL : public ContractBase RLUtils::getSCRevenue(qpi, locals.entity, locals.revenue); // Winner selection (pseudo-random using K12(prevSpectrumDigest)). - getRandomPlayer(state, qpi, locals.randomNum, locals.winnerAddress); + { + locals.winnerAddress = id::zero(); + + if (state.playerCounter != 0) + { + // Compute pseudo-random index based on K12(prevSpectrumDigest) + locals.randomNum = mod(qpi.K12(qpi.getPrevSpectrumDigest()).u64._0, state.playerCounter); + + // Index directly into players array + locals.winnerAddress = state.players.get(locals.randomNum); + } + } if (locals.winnerAddress != id::zero()) { @@ -862,21 +873,5 @@ struct RL : public ContractBase static void enableBuyTicket(RL& state, bool bEnable) { state.currentState = bEnable ? EState::SELLING : EState::LOCKED; } - static void getRandomPlayer(const RL& state, const QpiContextProcedureCall& qpi, uint64& randomNum, id& winnerAddress) - { - winnerAddress = id::zero(); - - if (state.playerCounter == 0) - { - return; - } - - // Compute pseudo-random index based on K12(prevSpectrumDigest) - randomNum = mod(qpi.K12(qpi.getPrevSpectrumDigest()).u64._0, state.playerCounter); - - // Index directly into players array - winnerAddress = state.players.get(randomNum); - } - static void getWinnerCounter(const RL& state, uint64& outCounter) { outCounter = mod(state.winnersCounter, state.winners.capacity()); } }; From 7324060e6eeda9a31b0c456c8515f4a7e947e676 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 30 Oct 2025 01:13:34 +0300 Subject: [PATCH 17/39] Refactor date and revenue utility functions; simplify parameters and improve clarity in usage --- src/contracts/RandomLottery.h | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 7e85a977b..fe10ebf37 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -51,22 +51,15 @@ constexpr uint8 RL_DEFAULT_DRAW_HOUR = 11; // 11:00 UTC namespace RLUtils { - // Returns current day-of-week in range [0..6], with 0 = WEDNESDAY according to platform mapping. - static void getCurrentDayOfWeek(const QpiContextFunctionCall& qpi, uint8& dayOfWeek) - { - dayOfWeek = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); - } - // Packs current date into a compact stamp (Y/M/D) used to ensure a single action per calendar day. - static void makeDateStamp(const QpiContextFunctionCall& qpi, uint32& res) + static void makeDateStamp(uint8 year, uint8 month, uint8 day, uint32& res) { - res = static_cast(qpi.year() << 9 | qpi.month() << 5 | qpi.day()); + res = static_cast(year << 9 | month << 5 | day); } // Reads current net on-chain balance of SELF (incoming - outgoing). - static void getSCRevenue(const QpiContextFunctionCall& qpi, Entity& entity, uint64& revenue) + static void getSCRevenue(const Entity& entity, uint64& revenue) { - qpi.getEntity(SELF, entity); revenue = entity.incomingAmount - entity.outgoingAmount; } @@ -415,9 +408,9 @@ struct RL : public ContractBase } // Mark the current date as already processed to avoid immediate draw on the same calendar day - RLUtils::getCurrentDayOfWeek(qpi, state.lastDrawDay); + state.lastDrawDay = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); state.lastDrawHour = state.drawHour; - RLUtils::makeDateStamp(qpi, state.lastDrawDateStamp); + RLUtils::makeDateStamp(qpi.year(), qpi.month(), qpi.day(), state.lastDrawDateStamp); // Open selling for the new epoch enableBuyTicket(state, true); @@ -440,7 +433,7 @@ struct RL : public ContractBase } // Snapshot current day/hour - RLUtils::getCurrentDayOfWeek(qpi, locals.currentDayOfWeek); + locals.currentDayOfWeek = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); locals.currentHour = qpi.hour(); // Do nothing before the configured draw hour @@ -450,7 +443,7 @@ struct RL : public ContractBase } // Ensure only one action per calendar day (UTC) - RLUtils::makeDateStamp(qpi, locals.currentDateStamp); + RLUtils::makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentDateStamp); if (state.lastDrawDateStamp == locals.currentDateStamp) { return; @@ -485,7 +478,8 @@ struct RL : public ContractBase else { // Current contract net balance = incoming - outgoing for this contract - RLUtils::getSCRevenue(qpi, locals.entity, locals.revenue); + qpi.getEntity(SELF, locals.entity); + RLUtils::getSCRevenue(locals.entity, locals.revenue); // Winner selection (pseudo-random using K12(prevSpectrumDigest)). { @@ -587,7 +581,11 @@ struct RL : public ContractBase PUBLIC_FUNCTION(GetNextEpochData) { output.nextEpochData = state.nexEpochData; } PUBLIC_FUNCTION(GetDrawHour) { output.drawHour = state.drawHour; } PUBLIC_FUNCTION(GetSchedule) { output.schedule = state.schedule; } - PUBLIC_FUNCTION_WITH_LOCALS(GetBalance) { RLUtils::getSCRevenue(qpi, locals.entity, output.balance); } + PUBLIC_FUNCTION_WITH_LOCALS(GetBalance) + { + qpi.getEntity(SELF, locals.entity); + RLUtils::getSCRevenue(locals.entity, output.balance); + } PUBLIC_PROCEDURE(SetPrice) { @@ -726,7 +724,7 @@ struct RL : public ContractBase locals.winnerInfo.revenue = input.revenue; locals.winnerInfo.epoch = qpi.epoch(); locals.winnerInfo.tick = qpi.tick(); - RLUtils::getCurrentDayOfWeek(qpi, locals.winnerInfo.dayOfWeek); + locals.winnerInfo.dayOfWeek = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); state.winners.set(locals.insertIdx, locals.winnerInfo); } From 27e1275deb6028785d1aaff26b6e4913a8252ab4 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 30 Oct 2025 01:19:49 +0300 Subject: [PATCH 18/39] Refactor RLUtils namespace; inline utility functions for date stamping and revenue calculation to improve clarity and maintainability --- src/contracts/RandomLottery.h | 42 +++++++++++++---------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index fe10ebf37..e322d9990 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -49,26 +49,6 @@ constexpr uint8 RL_TICK_UPDATE_PERIOD = 100; /// Default draw hour (UTC). constexpr uint8 RL_DEFAULT_DRAW_HOUR = 11; // 11:00 UTC -namespace RLUtils -{ - // Packs current date into a compact stamp (Y/M/D) used to ensure a single action per calendar day. - static void makeDateStamp(uint8 year, uint8 month, uint8 day, uint32& res) - { - res = static_cast(year << 9 | month << 5 | day); - } - - // Reads current net on-chain balance of SELF (incoming - outgoing). - static void getSCRevenue(const Entity& entity, uint64& revenue) - { - revenue = entity.incomingAmount - entity.outgoingAmount; - } - - template static constexpr const T& min(const T& a, const T& b) - { - return (a < b) ? a : b; - } -}; // namespace RLUtils - /// Placeholder structure for future extensions. struct RL2 { @@ -410,7 +390,7 @@ struct RL : public ContractBase // Mark the current date as already processed to avoid immediate draw on the same calendar day state.lastDrawDay = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); state.lastDrawHour = state.drawHour; - RLUtils::makeDateStamp(qpi.year(), qpi.month(), qpi.day(), state.lastDrawDateStamp); + makeDateStamp(qpi.year(), qpi.month(), qpi.day(), state.lastDrawDateStamp); // Open selling for the new epoch enableBuyTicket(state, true); @@ -443,7 +423,7 @@ struct RL : public ContractBase } // Ensure only one action per calendar day (UTC) - RLUtils::makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentDateStamp); + makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentDateStamp); if (state.lastDrawDateStamp == locals.currentDateStamp) { return; @@ -479,7 +459,7 @@ struct RL : public ContractBase { // Current contract net balance = incoming - outgoing for this contract qpi.getEntity(SELF, locals.entity); - RLUtils::getSCRevenue(locals.entity, locals.revenue); + getSCRevenue(locals.entity, locals.revenue); // Winner selection (pseudo-random using K12(prevSpectrumDigest)). { @@ -563,7 +543,7 @@ struct RL : public ContractBase PUBLIC_FUNCTION(GetPlayers) { output.players = state.players; - output.playerCounter = RLUtils::min(state.playerCounter, state.players.capacity()); + output.playerCounter = min(state.playerCounter, state.players.capacity()); } /** @@ -584,7 +564,7 @@ struct RL : public ContractBase PUBLIC_FUNCTION_WITH_LOCALS(GetBalance) { qpi.getEntity(SELF, locals.entity); - RLUtils::getSCRevenue(locals.entity, output.balance); + getSCRevenue(locals.entity, output.balance); } PUBLIC_PROCEDURE(SetPrice) @@ -681,7 +661,7 @@ struct RL : public ContractBase // Compute desired number of tickets and change locals.desired = div(locals.reward, locals.price); // How many tickets the caller attempts to buy locals.remainder = mod(locals.reward, locals.price); // Change to return - locals.toBuy = RLUtils::min(locals.desired, locals.slotsLeft); // Do not exceed available slots + locals.toBuy = min(locals.desired, locals.slotsLeft); // Do not exceed available slots // Add tickets (the same address may be inserted multiple times) for (locals.i = 0; locals.i < locals.toBuy; ++locals.i) @@ -689,7 +669,7 @@ struct RL : public ContractBase if (state.playerCounter < locals.capacity) { state.players.set(state.playerCounter, qpi.invocator()); - state.playerCounter = RLUtils::min(state.playerCounter + 1, locals.capacity); + state.playerCounter = min(state.playerCounter + 1, locals.capacity); } } @@ -872,4 +852,12 @@ struct RL : public ContractBase static void enableBuyTicket(RL& state, bool bEnable) { state.currentState = bEnable ? EState::SELLING : EState::LOCKED; } static void getWinnerCounter(const RL& state, uint64& outCounter) { outCounter = mod(state.winnersCounter, state.winners.capacity()); } + + // Packs current date into a compact stamp (Y/M/D) used to ensure a single action per calendar day. + static void makeDateStamp(uint8 year, uint8 month, uint8 day, uint32& res) { res = static_cast(year << 9 | month << 5 | day); } + + // Reads current net on-chain balance of SELF (incoming - outgoing). + static void getSCRevenue(const Entity& entity, uint64& revenue) { revenue = entity.incomingAmount - entity.outgoingAmount; } + + template static constexpr const T& min(const T& a, const T& b) { return (a < b) ? a : b; } }; From 5ed2168744446058c5486ac1fcf0aaf9fd6f0487 Mon Sep 17 00:00:00 2001 From: N-010 Date: Fri, 31 Oct 2025 19:01:55 +0300 Subject: [PATCH 19/39] Refactor data structures in RandomLottery.h; remove default initializations for clarity and consistency --- src/contracts/RandomLottery.h | 130 +++++++++++++++++----------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index e322d9990..becf4f3e9 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -102,8 +102,8 @@ struct RL : public ContractBase struct NextEpochData { - uint64 newPrice = 0; // Ticket price to apply after END_EPOCH; 0 means "no change queued" - uint8 schedule = 0; // Schedule bitmask (bit 0 = WEDNESDAY, ..., bit 6 = TUESDAY); applied after END_EPOCH + uint64 newPrice; // Ticket price to apply after END_EPOCH; 0 means "no change queued" + uint8 schedule; // Schedule bitmask (bit 0 = WEDNESDAY, ..., bit 6 = TUESDAY); applied after END_EPOCH }; //---- User-facing I/O structures ------------------------------------------------------------- @@ -114,7 +114,7 @@ struct RL : public ContractBase struct BuyTicket_output { - uint8 returnCode = static_cast(EReturnCode::SUCCESS); + uint8 returnCode; }; struct GetFees_input @@ -123,11 +123,11 @@ struct RL : public ContractBase struct GetFees_output { - uint8 teamFeePercent = 0; // Team share in percent - uint8 distributionFeePercent = 0; // Distribution/shareholders share in percent - uint8 winnerFeePercent = 0; // Winner share in percent - uint8 burnPercent = 0; // Burn share in percent - uint8 returnCode = static_cast(EReturnCode::SUCCESS); + uint8 teamFeePercent; // Team share in percent + uint8 distributionFeePercent; // Distribution/shareholders share in percent + uint8 winnerFeePercent; // Winner share in percent + uint8 burnPercent; // Burn share in percent + uint8 returnCode; }; struct GetPlayers_input @@ -137,8 +137,8 @@ struct RL : public ContractBase struct GetPlayers_output { Array players; // Current epoch ticket holders (duplicates allowed) - uint64 playerCounter = 0; // Actual count of filled entries - uint8 returnCode = static_cast(EReturnCode::SUCCESS); + uint64 playerCounter; // Actual count of filled entries + uint8 returnCode; }; /** @@ -146,17 +146,17 @@ struct RL : public ContractBase */ struct WinnerInfo { - id winnerAddress = id::zero(); // Winner address - uint64 revenue = 0; // Payout value sent to the winner for that epoch - uint32 tick = 0; // Tick when the decision was made - uint16 epoch = 0; // Epoch number when winner was recorded - uint8 dayOfWeek = RL_INVALID_DAY; // Day of week when the winner was drawn [0..6] 0 = WEDNESDAY + id winnerAddress; // Winner address + uint64 revenue; // Payout value sent to the winner for that epoch + uint32 tick; // Tick when the decision was made + uint16 epoch; // Epoch number when winner was recorded + uint8 dayOfWeek; // Day of week when the winner was drawn [0..6] 0 = WEDNESDAY }; struct FillWinnersInfo_input { - id winnerAddress = id::zero(); // Winner address to store - uint64 revenue = 0; // Winner payout to store + id winnerAddress; // Winner address to store + uint64 revenue; // Winner payout to store }; struct FillWinnersInfo_output @@ -165,8 +165,8 @@ struct RL : public ContractBase struct FillWinnersInfo_locals { - WinnerInfo winnerInfo = {}; // Temporary buffer to compose a WinnerInfo record - uint64 insertIdx = 0; // Index in ring buffer where to insert new winner + WinnerInfo winnerInfo; // Temporary buffer to compose a WinnerInfo record + uint64 insertIdx; // Index in ring buffer where to insert new winner }; struct GetWinners_input @@ -176,8 +176,8 @@ struct RL : public ContractBase struct GetWinners_output { Array winners; // Ring buffer snapshot - uint64 winnersCounter = 0; // Number of valid entries = (totalWinners % capacity) - uint8 returnCode = static_cast(EReturnCode::SUCCESS); + uint64 winnersCounter; // Number of valid entries = (totalWinners % capacity) + uint8 returnCode; }; struct GetTicketPrice_input @@ -186,7 +186,7 @@ struct RL : public ContractBase struct GetTicketPrice_output { - uint64 ticketPrice = 0; // Current ticket price + uint64 ticketPrice; // Current ticket price }; struct GetMaxNumberOfPlayers_input @@ -195,7 +195,7 @@ struct RL : public ContractBase struct GetMaxNumberOfPlayers_output { - uint64 numberOfPlayers = 0; // Max capacity of players array + uint64 numberOfPlayers; // Max capacity of players array }; struct GetState_input @@ -204,7 +204,7 @@ struct RL : public ContractBase struct GetState_output { - uint8 currentState = static_cast(EState::INVALID); // Current finite state of the lottery + uint8 currentState; // Current finite state of the lottery }; struct GetBalance_input @@ -213,28 +213,28 @@ struct RL : public ContractBase struct GetBalance_output { - uint64 balance = 0; // Current contract net balance (incoming - outgoing) + uint64 balance; // Current contract net balance (incoming - outgoing) }; // Local variables for GetBalance procedure struct GetBalance_locals { - Entity entity = {}; // Entity accounting snapshot for SELF + Entity entity; // Entity accounting snapshot for SELF }; // Local variables for BuyTicket procedure struct BuyTicket_locals { - uint64 price = 0; // Current ticket price - uint64 reward = 0; // Funds sent with call (invocationReward) - uint64 capacity = 0; // Max capacity of players array - uint64 slotsLeft = 0; // Remaining slots available to fill this epoch - uint64 desired = 0; // How many tickets the caller wants to buy - uint64 remainder = 0; // Change to return (reward % price) - uint64 toBuy = 0; // Actual number of tickets to purchase (bounded by slotsLeft) - uint64 unfilled = 0; // Portion of desired tickets not purchased due to capacity limit - uint64 refundAmount = 0; // Total refund: remainder + unfilled * price - uint64 i = 0; // Loop counter + uint64 price; // Current ticket price + uint64 reward; // Funds sent with call (invocationReward) + uint64 capacity; // Max capacity of players array + uint64 slotsLeft; // Remaining slots available to fill this epoch + uint64 desired; // How many tickets the caller wants to buy + uint64 remainder; // Change to return (reward % price) + uint64 toBuy; // Actual number of tickets to purchase (bounded by slotsLeft) + uint64 unfilled; // Portion of desired tickets not purchased due to capacity limit + uint64 refundAmount; // Total refund: remainder + unfilled * price + uint64 i; // Loop counter }; struct ReturnAllTickets_input @@ -246,50 +246,50 @@ struct RL : public ContractBase struct ReturnAllTickets_locals { - uint64 i = 0; // Loop counter for mass-refund + uint64 i; // Loop counter for mass-refund }; struct SetPrice_input { - uint64 newPrice = 0; // New ticket price to be applied at the end of the epoch + uint64 newPrice; // New ticket price to be applied at the end of the epoch }; struct SetPrice_output { - uint8 returnCode = static_cast(EReturnCode::SUCCESS); + uint8 returnCode; }; struct SetSchedule_input { - uint8 newSchedule = 0; // New schedule bitmask to be applied at the end of the epoch + uint8 newSchedule; // New schedule bitmask to be applied at the end of the epoch }; struct SetSchedule_output { - uint8 returnCode = static_cast(EReturnCode::SUCCESS); + uint8 returnCode; }; struct BEGIN_TICK_locals { - id winnerAddress = id::zero(); - Entity entity = {}; - uint64 revenue = 0; - uint64 randomNum = 0; - uint64 winnerAmount = 0; - uint64 teamFee = 0; - uint64 distributionFee = 0; - uint64 burnedAmount = 0; - FillWinnersInfo_locals fillWinnersInfoLocals = {}; - FillWinnersInfo_input fillWinnersInfoInput = {}; - uint32 currentDateStamp = 0; - uint8 currentDayOfWeek = RL_INVALID_DAY; - uint8 currentHour = RL_INVALID_HOUR; - uint8 isWednesday = 0; - uint8 isScheduledToday = 0; - ReturnAllTickets_locals returnAllTicketsLocals = {}; - ReturnAllTickets_input returnAllTicketsInput = {}; - ReturnAllTickets_output returnAllTicketsOutput = {}; - FillWinnersInfo_output fillWinnersInfoOutput = {}; + id winnerAddress; + Entity entity; + uint64 revenue; + uint64 randomNum; + uint64 winnerAmount; + uint64 teamFee; + uint64 distributionFee; + uint64 burnedAmount; + FillWinnersInfo_locals fillWinnersInfoLocals; + FillWinnersInfo_input fillWinnersInfoInput; + uint32 currentDateStamp; + uint8 currentDayOfWeek; + uint8 currentHour; + uint8 isWednesday; + uint8 isScheduledToday; + ReturnAllTickets_locals returnAllTicketsLocals; + ReturnAllTickets_input returnAllTicketsInput; + ReturnAllTickets_output returnAllTicketsOutput; + FillWinnersInfo_output fillWinnersInfoOutput; }; struct GetNextEpochData_input @@ -298,7 +298,7 @@ struct RL : public ContractBase struct GetNextEpochData_output { - NextEpochData nextEpochData = {}; + NextEpochData nextEpochData; }; struct GetDrawHour_input @@ -307,7 +307,7 @@ struct RL : public ContractBase struct GetDrawHour_output { - uint8 drawHour = RL_INVALID_HOUR; + uint8 drawHour; }; // New: expose current schedule mask @@ -316,7 +316,7 @@ struct RL : public ContractBase }; struct GetSchedule_output { - uint8 schedule = 0; + uint8 schedule; }; public: @@ -659,8 +659,8 @@ struct RL : public ContractBase } // Compute desired number of tickets and change - locals.desired = div(locals.reward, locals.price); // How many tickets the caller attempts to buy - locals.remainder = mod(locals.reward, locals.price); // Change to return + locals.desired = div(locals.reward, locals.price); // How many tickets the caller attempts to buy + locals.remainder = mod(locals.reward, locals.price); // Change to return locals.toBuy = min(locals.desired, locals.slotsLeft); // Do not exceed available slots // Add tickets (the same address may be inserted multiple times) From 7ef9f4b4806c1e27ecd46f6c23507081201fe28e Mon Sep 17 00:00:00 2001 From: N-010 Date: Fri, 31 Oct 2025 23:30:38 +0300 Subject: [PATCH 20/39] Add default schedule for lottery draws; replace hardcoded values with RL_DEFAULT_SCHEDULE for clarity and maintainability --- src/contracts/RandomLottery.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index becf4f3e9..3758498f5 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -49,6 +49,8 @@ constexpr uint8 RL_TICK_UPDATE_PERIOD = 100; /// Default draw hour (UTC). constexpr uint8 RL_DEFAULT_DRAW_HOUR = 11; // 11:00 UTC +constexpr uint8 RL_DEFAULT_SCHEDULE = 1 << WEDNESDAY | 1 << FRIDAY | 1 << SUNDAY; // Draws on WED, FRI, SUN + /// Placeholder structure for future extensions. struct RL2 { @@ -368,7 +370,7 @@ struct RL : public ContractBase state.playerCounter = 0; // Default schedule: WEDNESDAY - state.schedule = 1 << WEDNESDAY; + state.schedule = RL_DEFAULT_SCHEDULE; } /** @@ -379,7 +381,7 @@ struct RL : public ContractBase if (state.schedule == 0) { // Default to WEDNESDAY if no schedule is set (bit 0) - state.schedule = 1 << WEDNESDAY; + state.schedule = RL_DEFAULT_SCHEDULE; } if (state.drawHour == 0) From 6a68ce8501d4794e99025af69b80a803a6006b18 Mon Sep 17 00:00:00 2001 From: N-010 Date: Mon, 3 Nov 2025 13:59:06 +0300 Subject: [PATCH 21/39] Update RandomLottery.h Remove initialize --- src/contracts/RandomLottery.h | 36 +++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 3758498f5..74fbf9047 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -725,97 +725,97 @@ struct RL : public ContractBase * @brief Circular buffer storing the history of winners. * Maximum capacity is defined by RL_MAX_NUMBER_OF_WINNERS_IN_HISTORY. */ - Array winners = {}; + Array winners; /** * @brief Set of players participating in the current lottery epoch. * Maximum capacity is defined by RL_MAX_NUMBER_OF_PLAYERS. */ - Array players = {}; + Array players; /** * @brief Address of the team managing the lottery contract. * Initialized to a zero address. */ - id teamAddress = id::zero(); + id teamAddress; /** * @brief Address of the owner of the lottery contract. * Initialized to a zero address. */ - id ownerAddress = id::zero(); + id ownerAddress; /** * @brief Data structure for deferred changes to apply at the end of the epoch. */ - NextEpochData nexEpochData = {}; + NextEpochData nexEpochData; /** * @brief Price of a single lottery ticket. * Value is in the smallest currency unit (e.g., cents). */ - uint64 ticketPrice = 0; + uint64 ticketPrice; /** * @brief Number of players (tickets sold) in the current epoch. */ - uint64 playerCounter = 0; + uint64 playerCounter; /** * @brief Index pointing to the next empty slot in the winners array. * Used for maintaining the circular buffer of winners. */ - uint64 winnersCounter = 0; + uint64 winnersCounter; /** * @brief Date/time guard for draw operations. * lastDrawDateStamp prevents more than one action per calendar day (UTC). */ - uint8 lastDrawDay = RL_INVALID_DAY; - uint8 lastDrawHour = RL_INVALID_HOUR; - uint32 lastDrawDateStamp = 0; // Compact YYYY/MM/DD marker + uint8 lastDrawDay; + uint8 lastDrawHour; + uint32 lastDrawDateStamp; // Compact YYYY/MM/DD marker /** * @brief Percentage of the revenue allocated to the team. * Value is between 0 and 100. */ - uint8 teamFeePercent = 0; + uint8 teamFeePercent; /** * @brief Percentage of the revenue allocated for distribution. * Value is between 0 and 100. */ - uint8 distributionFeePercent = 0; + uint8 distributionFeePercent; /** * @brief Percentage of the revenue allocated to the winner. * Automatically calculated as the remainder after other fees. */ - uint8 winnerFeePercent = 0; + uint8 winnerFeePercent; /** * @brief Percentage of the revenue to be burned. * Value is between 0 and 100. */ - uint8 burnPercent = 0; + uint8 burnPercent; /** * @brief Schedule bitmask: bit 0 = WEDNESDAY, 1 = THURSDAY, ..., 6 = TUESDAY. * If a bit is set, a draw may occur on that day (subject to drawHour and daily guard). * Wednesday also follows the "Two-Wednesdays rule" (selling stays closed after Wednesday draw). */ - uint8 schedule = 0; + uint8 schedule; /** * @brief UTC hour [0..23] when a draw is allowed to run (daily time gate). */ - uint8 drawHour = 0; + uint8 drawHour; /** * @brief Current state of the lottery contract. * SELLING: tickets available; LOCKED: selling closed. */ - EState currentState = EState::LOCKED; + EState currentState; protected: static void clearStateOnEndEpoch(RL& state) From 4b15a0e32a8c278df5f21a73e3f78ca7e2ef34ef Mon Sep 17 00:00:00 2001 From: N-010 Date: Tue, 4 Nov 2025 11:32:09 +0300 Subject: [PATCH 22/39] Reset player states before the next epoch in RandomLottery --- src/contracts/RandomLottery.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 74fbf9047..92a973f6b 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -822,6 +822,7 @@ struct RL : public ContractBase { // Prepare for next epoch: clear players and reset daily guards state.playerCounter = 0; + state.players.setAll(id::zero()); state.lastDrawHour = RL_INVALID_HOUR; state.lastDrawDay = RL_INVALID_DAY; From 2c5d894b8bbaeb59596fd97df7d92feec640626c Mon Sep 17 00:00:00 2001 From: N-010 Date: Sun, 9 Nov 2025 17:09:50 +0300 Subject: [PATCH 23/39] Add POST_INCOMING_TRANSFER function to handle standard transaction transfers in RandomLottery --- src/contracts/RandomLottery.h | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 92a973f6b..0ab6cdd11 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -528,6 +528,20 @@ struct RL : public ContractBase enableBuyTicket(state, !locals.isWednesday); } + POST_INCOMING_TRANSFER() + { + switch (input.type) + { + case TransferType::standardTransaction: + // Return any funds sent via standard transaction + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + default: break; + } + } + /** * @brief Returns currently configured fee percentages. */ From 4c3b3f84998a149c5bee13e8118f6ae10e4bdaef Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 13 Nov 2025 23:18:55 +0300 Subject: [PATCH 24/39] Fixes PostIncomingTransfer --- src/contracts/RandomLottery.h | 7 ++++--- test/contract_rl.cpp | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 0ab6cdd11..2a3504496 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -534,9 +534,9 @@ struct RL : public ContractBase { case TransferType::standardTransaction: // Return any funds sent via standard transaction - if (qpi.invocationReward() > 0) + if (input.amount > 0) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + qpi.transfer(input.sourceId, input.amount); } default: break; } @@ -836,7 +836,7 @@ struct RL : public ContractBase { // Prepare for next epoch: clear players and reset daily guards state.playerCounter = 0; - state.players.setAll(id::zero()); + setMemory(state.players, 0); state.lastDrawHour = RL_INVALID_HOUR; state.lastDrawDay = RL_INVALID_DAY; @@ -847,6 +847,7 @@ struct RL : public ContractBase { // After each draw period, clear current tickets state.playerCounter = 0; + setMemory(state.players, 0); } static void applyNextEpochData(RL& state) diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index 539623a4f..34d40ddbd 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -117,6 +117,7 @@ class ContractTestingRL : protected ContractTesting initEmptySpectrum(); initEmptyUniverse(); INIT_CONTRACT(RL); + system.epoch = contractDescriptions[RL_CONTRACT_INDEX].constructionEpoch; callSystemProcedure(RL_CONTRACT_INDEX, INITIALIZE); } @@ -341,6 +342,24 @@ class ContractTestingRL : protected ContractTesting } }; +TEST(ContractRandomLottery, PostIncomingTransfer) +{ + ContractTestingRL ctl; + static constexpr uint64 transferAmount = 123456789; + + const id sender = id::randomValue(); + increaseEnergy(sender, transferAmount); + EXPECT_EQ(getBalance(sender), transferAmount); + + const id contractAddress = ctl.rlSelf(); + EXPECT_EQ(getBalance(contractAddress), 0); + + notifyContractOfIncomingTransfer(sender, contractAddress, transferAmount, QPI::TransferType::standardTransaction); + + EXPECT_EQ(getBalance(sender), transferAmount); + EXPECT_EQ(getBalance(contractAddress), 0); +} + TEST(ContractRandomLottery, GetFees) { ContractTestingRL ctl; From 50b2ef108609a42e9ca77933da713e56743de56b Mon Sep 17 00:00:00 2001 From: N-010 Date: Sun, 16 Nov 2025 22:09:56 +0300 Subject: [PATCH 25/39] Adds handling for the event when the time has not yet been initialized to the current date. --- src/contracts/RandomLottery.h | 34 +++++++++++++++++------ test/contract_rl.cpp | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 2a3504496..06da0a0ef 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -51,6 +51,8 @@ constexpr uint8 RL_DEFAULT_DRAW_HOUR = 11; // 11:00 UTC constexpr uint8 RL_DEFAULT_SCHEDULE = 1 << WEDNESDAY | 1 << FRIDAY | 1 << SUNDAY; // Draws on WED, FRI, SUN +constexpr uint32 RL_DEFAULT_INIT_TIME = 22 << 9 | 4 << 5 | 13; + /// Placeholder structure for future extensions. struct RL2 { @@ -390,12 +392,10 @@ struct RL : public ContractBase } // Mark the current date as already processed to avoid immediate draw on the same calendar day - state.lastDrawDay = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); - state.lastDrawHour = state.drawHour; makeDateStamp(qpi.year(), qpi.month(), qpi.day(), state.lastDrawDateStamp); // Open selling for the new epoch - enableBuyTicket(state, true); + enableBuyTicket(state, state.lastDrawDateStamp != RL_DEFAULT_INIT_TIME); } END_EPOCH() @@ -414,8 +414,7 @@ struct RL : public ContractBase return; } - // Snapshot current day/hour - locals.currentDayOfWeek = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); + // Snapshot current hour locals.currentHour = qpi.hour(); // Do nothing before the configured draw hour @@ -426,11 +425,32 @@ struct RL : public ContractBase // Ensure only one action per calendar day (UTC) makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentDateStamp); + + if (locals.currentDateStamp == RL_DEFAULT_INIT_TIME) + { + enableBuyTicket(state, false); + + // Safety check: avoid processing on uninitialized time but remember that this date was encountered + state.lastDrawDateStamp = RL_DEFAULT_INIT_TIME; + return; + } + + // Set lastDrawDateStamp on first valid date processed + if (state.lastDrawDateStamp == RL_DEFAULT_INIT_TIME) + { + enableBuyTicket(state, true); + + // First valid date processed: just mark it and return + state.lastDrawDateStamp = locals.currentDateStamp; + return; + } + if (state.lastDrawDateStamp == locals.currentDateStamp) { return; } + locals.currentDayOfWeek = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); locals.isWednesday = (locals.currentDayOfWeek == WEDNESDAY); locals.isScheduledToday = ((state.schedule & (1u << locals.currentDayOfWeek)) != 0); @@ -444,8 +464,6 @@ struct RL : public ContractBase } // Mark today's action and timestamp - state.lastDrawDay = locals.currentDayOfWeek; - state.lastDrawHour = locals.currentHour; state.lastDrawDateStamp = locals.currentDateStamp; // Temporarily close selling for the draw @@ -838,8 +856,6 @@ struct RL : public ContractBase state.playerCounter = 0; setMemory(state.players, 0); - state.lastDrawHour = RL_INVALID_HOUR; - state.lastDrawDay = RL_INVALID_DAY; state.lastDrawDateStamp = 0; } diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index 34d40ddbd..706182015 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -107,6 +107,8 @@ class RLChecker : public RL uint8 getScheduleMask() const { return schedule; } uint8 getDrawHourInternal() const { return drawHour; } + + uint32 getLastDrawDateStamp() const { return lastDrawDateStamp; } }; class ContractTestingRL : protected ContractTesting @@ -342,6 +344,56 @@ class ContractTestingRL : protected ContractTesting } }; +TEST(ContractRandomLottery, DefaultInitTimeGuardSkipsPlaceholderDate) +{ + ContractTestingRL ctl; + + const uint64 ticketPrice = ctl.state()->getTicketPrice(); + + // Allow draws every day so weekday logic does not block BEGIN_TICK + ctl.forceSchedule(RL_ANY_DAY_DRAW_SCHEDULE); + + // Simulate the placeholder 2022-04-13 QPI date during initialization + ctl.setDateTime(2022, 4, 13, RL_DEFAULT_DRAW_HOUR + 1); + ctl.BeginEpoch(); + EXPECT_EQ(ctl.getStateInfo().currentState, static_cast(RL::EState::LOCKED)); + + // Selling is blocked until a valid date arrives + const id blockedBuyer = id::randomValue(); + increaseEnergy(blockedBuyer, ticketPrice); + const RL::BuyTicket_output denied = ctl.buyTicket(blockedBuyer, ticketPrice); + EXPECT_EQ(denied.returnCode, static_cast(RL::EReturnCode::TICKET_SELLING_CLOSED)); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); + + const uint64 winnersBefore = ctl.getWinners().winnersCounter; + + // BEGIN_TICK should detect the placeholder date and skip processing, but remember the sentinel day + ctl.forceBeginTick(); + EXPECT_EQ(ctl.state()->getLastDrawDateStamp(), RL_DEFAULT_INIT_TIME); + EXPECT_EQ(ctl.getStateInfo().currentState, static_cast(RL::EState::LOCKED)); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); + EXPECT_EQ(ctl.getWinners().winnersCounter, winnersBefore); + + // First valid day re-opens selling but still skips the draw + ctl.setDateTime(2025, 1, 10, RL_DEFAULT_DRAW_HOUR + 1); + ctl.forceBeginTick(); + EXPECT_EQ(ctl.getStateInfo().currentState, static_cast(RL::EState::SELLING)); + EXPECT_NE(ctl.state()->getLastDrawDateStamp(), RL_DEFAULT_INIT_TIME); + + const id playerA = id::randomValue(); + const id playerB = id::randomValue(); + ctl.increaseAndBuy(ctl, playerA, ticketPrice); + ctl.increaseAndBuy(ctl, playerB, ticketPrice); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 2u); + + // The immediate next valid day should run the actual draw + ctl.setDateTime(2025, 1, 11, RL_DEFAULT_DRAW_HOUR + 1); + ctl.forceBeginTick(); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); + EXPECT_EQ(ctl.getWinners().winnersCounter, winnersBefore + 1); + EXPECT_NE(ctl.state()->getLastDrawDateStamp(), RL_DEFAULT_INIT_TIME); +} + TEST(ContractRandomLottery, PostIncomingTransfer) { ContractTestingRL ctl; From 9059addfcab70d66536f22d460836f5aabf431e8 Mon Sep 17 00:00:00 2001 From: N-010 Date: Mon, 17 Nov 2025 17:52:41 +0300 Subject: [PATCH 26/39] Implements date handling and epoch initialization for the lottery contract --- src/contracts/RandomLottery.h | 16 ++++-- test/contract_rl.cpp | 93 +++++++++++++++++++++++++++++------ 2 files changed, 90 insertions(+), 19 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 06da0a0ef..7c9dd31da 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -288,6 +288,7 @@ struct RL : public ContractBase uint32 currentDateStamp; uint8 currentDayOfWeek; uint8 currentHour; + uint8 wednesdayDay; uint8 isWednesday; uint8 isScheduledToday; ReturnAllTickets_locals returnAllTicketsLocals; @@ -416,6 +417,8 @@ struct RL : public ContractBase // Snapshot current hour locals.currentHour = qpi.hour(); + locals.currentDayOfWeek = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); + locals.isWednesday = locals.currentDayOfWeek == WEDNESDAY; // Do nothing before the configured draw hour if (locals.currentHour < state.drawHour) @@ -440,9 +443,14 @@ struct RL : public ContractBase { enableBuyTicket(state, true); - // First valid date processed: just mark it and return - state.lastDrawDateStamp = locals.currentDateStamp; - return; + if (locals.isWednesday) + { + state.lastDrawDateStamp = locals.currentDateStamp; + } + else + { + state.lastDrawDateStamp = 0; + } } if (state.lastDrawDateStamp == locals.currentDateStamp) @@ -450,8 +458,6 @@ struct RL : public ContractBase return; } - locals.currentDayOfWeek = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); - locals.isWednesday = (locals.currentDayOfWeek == WEDNESDAY); locals.isScheduledToday = ((state.schedule & (1u << locals.currentDayOfWeek)) != 0); // Two-Wednesdays rule: diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index 706182015..9f6e8b54f 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -22,6 +22,12 @@ static const id RL_DEV_ADDRESS = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, constexpr uint8 RL_ANY_DAY_DRAW_SCHEDULE = 0xFF; // 0xFF sets bits 0..6 (WED..TUE); bit 7 is unused/ignored by logic +static uint32 makeDateStamp(uint16 year, uint8 month, uint8 day) +{ + const uint8 shortYear = static_cast(year - 2000); + return static_cast(shortYear << 9 | month << 5 | day); +} + // Equality operator for comparing WinnerInfo objects // Compares all fields (address, revenue, epoch, tick, dayOfWeek) bool operator==(const RL::WinnerInfo& left, const RL::WinnerInfo& right) @@ -342,6 +348,14 @@ class ContractTestingRL : protected ContractTesting state()->setScheduleMask(scheduleMask); // NOTE: we do not call SetSchedule here to avoid epoch transitions in tests. } + + void beginEpochWithDate(uint16 year, uint8 month, uint8 day, uint8 hour = static_cast(RL_DEFAULT_DRAW_HOUR + 1)) + { + setDateTime(year, month, day, hour); + BeginEpoch(); + } + + void beginEpochWithValidTime() { beginEpochWithDate(2025, 1, 20); } }; TEST(ContractRandomLottery, DefaultInitTimeGuardSkipsPlaceholderDate) @@ -394,6 +408,57 @@ TEST(ContractRandomLottery, DefaultInitTimeGuardSkipsPlaceholderDate) EXPECT_NE(ctl.state()->getLastDrawDateStamp(), RL_DEFAULT_INIT_TIME); } +TEST(ContractRandomLottery, SellingUnlocksWhenTimeSetBeforeScheduledDay) +{ + ContractTestingRL ctl; + + const uint64 ticketPrice = ctl.state()->getTicketPrice(); + + ctl.setDateTime(2022, 4, 13, RL_DEFAULT_DRAW_HOUR + 1); + ctl.BeginEpoch(); + + const id deniedBuyer = id::randomValue(); + increaseEnergy(deniedBuyer, ticketPrice); + EXPECT_EQ(ctl.buyTicket(deniedBuyer, ticketPrice).returnCode, static_cast(RL::EReturnCode::TICKET_SELLING_CLOSED)); + + ctl.setDateTime(2025, 1, 14, RL_DEFAULT_DRAW_HOUR + 2); // Tuesday, not scheduled by default + ctl.forceBeginTick(); + + EXPECT_EQ(ctl.getStateInfo().currentState, static_cast(RL::EState::SELLING)); + EXPECT_EQ(ctl.state()->getLastDrawDateStamp(), 0u); + + const id allowedBuyer = id::randomValue(); + increaseEnergy(allowedBuyer, ticketPrice); + const RL::BuyTicket_output allowed = ctl.buyTicket(allowedBuyer, ticketPrice); + EXPECT_EQ(allowed.returnCode, static_cast(RL::EReturnCode::SUCCESS)); +} + +TEST(ContractRandomLottery, SellingUnlocksWhenTimeSetOnDrawDay) +{ + ContractTestingRL ctl; + + const uint64 ticketPrice = ctl.state()->getTicketPrice(); + + ctl.setDateTime(2022, 4, 13, RL_DEFAULT_DRAW_HOUR + 1); + ctl.BeginEpoch(); + + const id deniedBuyer = id::randomValue(); + increaseEnergy(deniedBuyer, ticketPrice); + EXPECT_EQ(ctl.buyTicket(deniedBuyer, ticketPrice).returnCode, static_cast(RL::EReturnCode::TICKET_SELLING_CLOSED)); + + ctl.setDateTime(2025, 1, 15, RL_DEFAULT_DRAW_HOUR + 2); // Wednesday draw day + ctl.forceBeginTick(); + + const uint32 expectedStamp = makeDateStamp(2025, 1, 15); + EXPECT_EQ(ctl.state()->getLastDrawDateStamp(), expectedStamp); + EXPECT_EQ(ctl.getStateInfo().currentState, static_cast(RL::EState::SELLING)); + + const id allowedBuyer = id::randomValue(); + increaseEnergy(allowedBuyer, ticketPrice); + const RL::BuyTicket_output allowed = ctl.buyTicket(allowedBuyer, ticketPrice); + EXPECT_EQ(allowed.returnCode, static_cast(RL::EReturnCode::SUCCESS)); +} + TEST(ContractRandomLottery, PostIncomingTransfer) { ContractTestingRL ctl; @@ -461,7 +526,7 @@ TEST(ContractRandomLottery, BuyTicket) } // Switch to SELLING to allow purchases - ctl.BeginEpoch(); + ctl.beginEpochWithValidTime(); // 2. Loop over several users and test invalid price, success, duplicate constexpr uint64 userCount = 5; @@ -531,7 +596,7 @@ TEST(ContractRandomLottery, DrawAndPayout_BeginTick) // --- Scenario 1: No players (nothing to payout, no winner recorded) --- { - ctl.BeginEpoch(); + ctl.beginEpochWithValidTime(); EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); RL::GetWinners_output before = ctl.getWinners(); @@ -547,7 +612,7 @@ TEST(ContractRandomLottery, DrawAndPayout_BeginTick) // --- Scenario 2: Exactly one player (ticket refunded, no winner recorded) --- { - ctl.BeginEpoch(); + ctl.beginEpochWithValidTime(); const id solo = id::randomValue(); increaseEnergy(solo, ticketPrice); @@ -573,7 +638,7 @@ TEST(ContractRandomLottery, DrawAndPayout_BeginTick) // --- Scenario 3: Multiple players (winner chosen, fees processed, correct remaining on contract) --- { - ctl.BeginEpoch(); + ctl.beginEpochWithValidTime(); constexpr uint32 N = 5 * 2; struct PlayerInfo @@ -660,7 +725,7 @@ TEST(ContractRandomLottery, DrawAndPayout_BeginTick) for (uint32 r = 0; r < rounds; ++r) { - ctl.BeginEpoch(); + ctl.beginEpochWithValidTime(); struct P { @@ -748,7 +813,7 @@ TEST(ContractRandomLottery, GetBalance) } // Open selling and perform several purchases - ctl.BeginEpoch(); + ctl.beginEpochWithValidTime(); constexpr uint32 K = 3; for (uint32 i = 0; i < K; ++i) @@ -809,7 +874,7 @@ TEST(ContractRandomLottery, GetState) } // After BeginEpoch — SELLING - ctl.BeginEpoch(); + ctl.beginEpochWithValidTime(); { const RL::GetState_output out1 = ctl.getStateInfo(); EXPECT_EQ(out1.currentState, static_cast(RL::EState::SELLING)); @@ -926,7 +991,7 @@ TEST(ContractRandomLottery, SetPrice_AffectsNextEpochBuys) const uint64 newPrice = oldPrice * 3; // Open selling and buy at the old price - ctl.BeginEpoch(); + ctl.beginEpochWithValidTime(); const id u1 = id::randomValue(); increaseEnergy(u1, oldPrice * 2); { @@ -959,7 +1024,7 @@ TEST(ContractRandomLottery, SetPrice_AffectsNextEpochBuys) EXPECT_EQ(ctl.getTicketPrice().ticketPrice, newPrice); // In the next epoch, a purchase at the new price should succeed exactly once per price - ctl.BeginEpoch(); + ctl.beginEpochWithValidTime(); { const uint64 balBefore = getBalance(u2); const uint64 playersBefore = ctl.state()->getPlayerCounter(); @@ -973,7 +1038,7 @@ TEST(ContractRandomLottery, SetPrice_AffectsNextEpochBuys) TEST(ContractRandomLottery, BuyMultipleTickets_ExactMultiple_NoRemainder) { ContractTestingRL ctl; - ctl.BeginEpoch(); + ctl.beginEpochWithValidTime(); const uint64 price = ctl.state()->getTicketPrice(); const id user = id::randomValue(); const uint64 k = 7; @@ -987,7 +1052,7 @@ TEST(ContractRandomLottery, BuyMultipleTickets_ExactMultiple_NoRemainder) TEST(ContractRandomLottery, BuyMultipleTickets_WithRemainder_Refunded) { ContractTestingRL ctl; - ctl.BeginEpoch(); + ctl.beginEpochWithValidTime(); const uint64 price = ctl.state()->getTicketPrice(); const id user = id::randomValue(); const uint64 k = 5; @@ -1005,7 +1070,7 @@ TEST(ContractRandomLottery, BuyMultipleTickets_WithRemainder_Refunded) TEST(ContractRandomLottery, BuyMultipleTickets_CapacityPartialRefund) { ContractTestingRL ctl; - ctl.BeginEpoch(); + ctl.beginEpochWithValidTime(); const uint64 price = ctl.state()->getTicketPrice(); const uint64 capacity = ctl.getPlayers().players.capacity(); @@ -1032,7 +1097,7 @@ TEST(ContractRandomLottery, BuyMultipleTickets_CapacityPartialRefund) TEST(ContractRandomLottery, BuyMultipleTickets_AllSoldOut) { ContractTestingRL ctl; - ctl.BeginEpoch(); + ctl.beginEpochWithValidTime(); const uint64 price = ctl.state()->getTicketPrice(); const uint64 capacity = ctl.getPlayers().players.capacity(); @@ -1098,6 +1163,6 @@ TEST(ContractRandomLottery, GetDrawHour_DefaultAfterBeginEpoch) EXPECT_EQ(ctl.getDrawHour().drawHour, 0u); // After BeginEpoch default is 11 UTC - ctl.BeginEpoch(); + ctl.beginEpochWithValidTime(); EXPECT_EQ(ctl.getDrawHour().drawHour, RL_DEFAULT_DRAW_HOUR); } From 90ef5271c3036e34f5f0037e64e199ff6860268f Mon Sep 17 00:00:00 2001 From: N-010 Date: Mon, 17 Nov 2025 18:02:33 +0300 Subject: [PATCH 27/39] Removes unused wednesdayDay variable from RandomLottery struct --- src/contracts/RandomLottery.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 7c9dd31da..0e8e1a1b9 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -288,7 +288,6 @@ struct RL : public ContractBase uint32 currentDateStamp; uint8 currentDayOfWeek; uint8 currentHour; - uint8 wednesdayDay; uint8 isWednesday; uint8 isScheduledToday; ReturnAllTickets_locals returnAllTicketsLocals; From abf49bde8abfc83d860a14bdf0caf03f2773c0dd Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 19 Nov 2025 14:29:07 +0300 Subject: [PATCH 28/39] Adds unit test for setting price and scheduling draws in the Random Lottery contract --- src/contracts/RandomLottery.h | 8 +++- test/contract_rl.cpp | 84 +++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 0e8e1a1b9..f88141938 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -276,6 +276,7 @@ struct RL : public ContractBase struct BEGIN_TICK_locals { id winnerAddress; + m256i mixedSpectrumValue; Entity entity; uint64 revenue; uint64 randomNum; @@ -492,8 +493,11 @@ struct RL : public ContractBase if (state.playerCounter != 0) { - // Compute pseudo-random index based on K12(prevSpectrumDigest) - locals.randomNum = mod(qpi.K12(qpi.getPrevSpectrumDigest()).u64._0, state.playerCounter); + locals.mixedSpectrumValue = qpi.getPrevSpectrumDigest(); + locals.mixedSpectrumValue.u64._0 ^= qpi.tick(); + locals.mixedSpectrumValue.u64._1 ^= state.playerCounter; + // Compute pseudo-random index based on K12(prevSpectrumDigest ^ tick) + locals.randomNum = mod(qpi.K12(locals.mixedSpectrumValue).u64._0, state.playerCounter); // Index directly into players array locals.winnerAddress = state.players.get(locals.randomNum); diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index 9f6e8b54f..798fa8d59 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -358,6 +358,90 @@ class ContractTestingRL : protected ContractTesting void beginEpochWithValidTime() { beginEpochWithDate(2025, 1, 20); } }; +TEST(ContractRandomLottery, SetPriceAndScheduleApplyNextEpoch) +{ + ContractTestingRL ctl; + ctl.beginEpochWithValidTime(); + + // Default epoch configuration: draws 3 times per week at the default price + const uint64 oldPrice = ctl.state()->getTicketPrice(); + EXPECT_EQ(ctl.getSchedule().schedule, RL_DEFAULT_SCHEDULE); + + // Queue a new price (5,000,000) and limit draws to only Wednesday + constexpr uint64 newPrice = 5000000; + constexpr uint8 wednesdayOnly = static_cast(1 << WEDNESDAY); + increaseEnergy(RL_DEV_ADDRESS, 3); + EXPECT_EQ(ctl.setPrice(RL_DEV_ADDRESS, newPrice).returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(ctl.setSchedule(RL_DEV_ADDRESS, wednesdayOnly).returnCode, static_cast(RL::EReturnCode::SUCCESS)); + + const RL::NextEpochData nextDataBefore = ctl.getNextEpochData().nextEpochData; + EXPECT_EQ(nextDataBefore.newPrice, newPrice); + EXPECT_EQ(nextDataBefore.schedule, wednesdayOnly); + + // Until END_EPOCH the old settings remain active + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); + EXPECT_EQ(ctl.getSchedule().schedule, RL_DEFAULT_SCHEDULE); + + // Transition closes the epoch and applies both pending changes + ctl.EndEpoch(); + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, newPrice); + EXPECT_EQ(ctl.getSchedule().schedule, wednesdayOnly); + + const RL::NextEpochData nextDataAfter = ctl.getNextEpochData().nextEpochData; + EXPECT_EQ(nextDataAfter.newPrice, 0u); + EXPECT_EQ(nextDataAfter.schedule, 0u); + + // In the next epoch tickets must sell at the updated price + ctl.beginEpochWithDate(2025, 1, 15); // Wednesday + const id buyer = id::randomValue(); + increaseEnergy(buyer, newPrice * 2); + const uint64 balBefore = getBalance(buyer); + const uint64 playersBefore = ctl.state()->getPlayerCounter(); + const RL::BuyTicket_output buyOut = ctl.buyTicket(buyer, newPrice); + EXPECT_EQ(buyOut.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + const uint64 playersAfterFirstBuy = playersBefore + 1; + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersAfterFirstBuy); + EXPECT_EQ(getBalance(buyer), balBefore - newPrice); + + // Second user also buys a ticket at the new price + const id secondBuyer = id::randomValue(); + increaseEnergy(secondBuyer, newPrice * 2); + const uint64 secondBalBefore = getBalance(secondBuyer); + const RL::BuyTicket_output secondBuyOut = ctl.buyTicket(secondBuyer, newPrice); + EXPECT_EQ(secondBuyOut.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + const uint64 playersAfterBuy = playersAfterFirstBuy + 1; + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersAfterBuy); + EXPECT_EQ(getBalance(secondBuyer), secondBalBefore - newPrice); + + // Draws should only trigger on Wednesdays now: starting on Wednesday means the draw + // is deferred until the next Wednesday in the schedule. + const uint64 winnersBefore = ctl.getWinners().winnersCounter; + ctl.setDateTime(2025, 1, 15, RL_DEFAULT_DRAW_HOUR + 1); // current Wednesday + ctl.forceBeginTick(); + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersAfterBuy); + EXPECT_EQ(ctl.getStateInfo().currentState, static_cast(RL::EState::SELLING)); + EXPECT_EQ(ctl.getWinners().winnersCounter, winnersBefore); + + // No draw on non-scheduled days between Wednesdays + ctl.setDateTime(2025, 1, 21, RL_DEFAULT_DRAW_HOUR + 1); // Tuesday next week + ctl.forceBeginTick(); + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersAfterBuy); + EXPECT_EQ(ctl.getWinners().winnersCounter, winnersBefore); + + // Next Wednesday processes the draw + ctl.setDateTime(2025, 1, 22, RL_DEFAULT_DRAW_HOUR + 1); // next Wednesday + ctl.forceBeginTick(); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); + EXPECT_EQ(ctl.getWinners().winnersCounter, winnersBefore + 1); + EXPECT_EQ(ctl.getStateInfo().currentState, static_cast(RL::EState::LOCKED)); + + // After the draw and before the next epoch begins, ticket purchases are blocked + const id lockedBuyer = id::randomValue(); + increaseEnergy(lockedBuyer, newPrice); + const RL::BuyTicket_output lockedOut = ctl.buyTicket(lockedBuyer, newPrice); + EXPECT_EQ(lockedOut.returnCode, static_cast(RL::EReturnCode::TICKET_SELLING_CLOSED)); +} + TEST(ContractRandomLottery, DefaultInitTimeGuardSkipsPlaceholderDate) { ContractTestingRL ctl; From da4f64646e3a4e62f2047b9e2dd3c7e4f5246c87 Mon Sep 17 00:00:00 2001 From: N-010 Date: Mon, 1 Dec 2025 00:58:19 +0300 Subject: [PATCH 29/39] Returns tickets if multiple tickets were purchased by one player Issues RLT tokens Gives tokens for every ticket Purchasing a ticket with tokens --- src/contracts/RandomLottery.h | 455 ++++++++++++++++++++++++++++-- test/contract_rl.cpp | 516 ++++++++++++++++++++++++++++++---- 2 files changed, 888 insertions(+), 83 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index f88141938..1d14cb38a 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -53,6 +53,14 @@ constexpr uint8 RL_DEFAULT_SCHEDULE = 1 << WEDNESDAY | 1 << FRIDAY | 1 << SUNDAY constexpr uint32 RL_DEFAULT_INIT_TIME = 22 << 9 | 4 << 5 | 13; +constexpr uint64 RL_TOKEN_NAME = 0x00544c52ull; // "RLT" little-endian + +constexpr uint64 RL_DEFAULT_TOKEN_PRICE = 12; + +constexpr uint64 RL_TOKEN_REWARD_DIVISOR = 3; + +constexpr uint16 RL_MAX_ALLOWED_TOKENS = 1024; + /// Placeholder structure for future extensions. struct RL2 { @@ -78,12 +86,19 @@ struct RL : public ContractBase */ enum class EState : uint8 { - SELLING, // Ticket selling is open - LOCKED, // Ticket selling is closed - - INVALID = UINT8_MAX + SELLING = 1 << 0, // Ticket selling is open }; + friend EState operator|(const EState& a, const EState& b) { return static_cast(static_cast(a) | static_cast(b)); } + + friend EState operator&(const EState& a, const EState& b) { return static_cast(static_cast(a) & static_cast(b)); } + + friend EState operator~(const EState& a) { return static_cast(~static_cast(a)); } + + template friend bool operator==(const EState& a, const T& b) { return static_cast(a) == b; } + + template friend bool operator!=(const EState& a, const T& b) { return !(a == b); } + /** * @brief Standardized return / error codes for procedures. */ @@ -101,6 +116,10 @@ struct RL : public ContractBase // Value-related errors INVALID_VALUE, // Input value is not acceptable + // Token-related errors + TOKEN_NOT_ALLOWED, // Attempted to pay with a token that is not supported + TOKEN_TRANSFER_FAILED, // Token transfer to/from the contract failed + UNKNOWN_ERROR = UINT8_MAX }; @@ -110,6 +129,17 @@ struct RL : public ContractBase uint8 schedule; // Schedule bitmask (bit 0 = WEDNESDAY, ..., bit 6 = TUESDAY); applied after END_EPOCH }; + struct TokenData + { + uint64 pricePerTicket; + }; + + struct OutTokenData + { + uint64 tokenName; + TokenData tokenData; + }; + //---- User-facing I/O structures ------------------------------------------------------------- struct BuyTicket_input @@ -118,7 +148,18 @@ struct RL : public ContractBase struct BuyTicket_output { - uint8 returnCode; + EReturnCode returnCode; + }; + + struct BuyTicketWithToken_input + { + uint64 tokenName; + uint64 tokenAmount; + }; + + struct BuyTicketWithToken_output + { + EReturnCode returnCode; }; struct GetFees_input @@ -131,7 +172,7 @@ struct RL : public ContractBase uint8 distributionFeePercent; // Distribution/shareholders share in percent uint8 winnerFeePercent; // Winner share in percent uint8 burnPercent; // Burn share in percent - uint8 returnCode; + EReturnCode returnCode; }; struct GetPlayers_input @@ -142,7 +183,7 @@ struct RL : public ContractBase { Array players; // Current epoch ticket holders (duplicates allowed) uint64 playerCounter; // Actual count of filled entries - uint8 returnCode; + EReturnCode returnCode; }; /** @@ -181,7 +222,7 @@ struct RL : public ContractBase { Array winners; // Ring buffer snapshot uint64 winnersCounter; // Number of valid entries = (totalWinners % capacity) - uint8 returnCode; + EReturnCode returnCode; }; struct GetTicketPrice_input @@ -241,6 +282,19 @@ struct RL : public ContractBase uint64 i; // Loop counter }; + struct BuyTicketWithToken_locals + { + uint64 price; // Price per ticket in the chosen token + uint64 slotsLeft; // Remaining slots available to fill this epoch + uint64 desired; // How many tickets the caller wants to buy + uint64 remainder; // Change to return (tokenAmount % price) + uint64 toBuy; // Actual number of tickets to purchase (bounded by slotsLeft) + uint64 unfilled; // Portion of desired tickets not purchased due to capacity limit + uint64 refundAmount; // Total refund in tokens: remainder + unfilled * price + uint64 i; // Loop counter + TokenData tokenData; + }; + struct ReturnAllTickets_input { }; @@ -260,7 +314,7 @@ struct RL : public ContractBase struct SetPrice_output { - uint8 returnCode; + EReturnCode returnCode; }; struct SetSchedule_input @@ -270,7 +324,33 @@ struct RL : public ContractBase struct SetSchedule_output { - uint8 returnCode; + EReturnCode returnCode; + }; + + struct AddAllowedToken_input + { + uint64 tokenName; + uint64 tokenPrice; + }; + + struct AddAllowedToken_output + { + EReturnCode returnCode; + }; + + struct AddAllowedToken_locals + { + TokenData tokenData; + }; + + struct RemoveAllowedToken_input + { + uint64 tokenName; + }; + + struct RemoveAllowedToken_output + { + EReturnCode returnCode; }; struct BEGIN_TICK_locals @@ -284,6 +364,8 @@ struct RL : public ContractBase uint64 teamFee; uint64 distributionFee; uint64 burnedAmount; + uint64 index; + sint64 rewardPerTicket; FillWinnersInfo_locals fillWinnersInfoLocals; FillWinnersInfo_input fillWinnersInfoInput; uint32 currentDateStamp; @@ -291,10 +373,12 @@ struct RL : public ContractBase uint8 currentHour; uint8 isWednesday; uint8 isScheduledToday; + bit hasMultipleParticipants; ReturnAllTickets_locals returnAllTicketsLocals; ReturnAllTickets_input returnAllTicketsInput; ReturnAllTickets_output returnAllTicketsOutput; FillWinnersInfo_output fillWinnersInfoOutput; + TokenData tokenData; }; struct GetNextEpochData_input @@ -324,6 +408,46 @@ struct RL : public ContractBase uint8 schedule; }; + struct GetTokenData_input + { + }; + struct GetTokenData_output + { + Array tokenData; + }; + struct GetTokenData_locals + { + OutTokenData outTokenData; + sint64 hashMapIndex; + uint64 arrayIndex; + }; + + struct SetTokenRewardDivisor_input + { + uint64 tokenRewardDivisor; + }; + + struct SetTokenRewardDivisor_output + { + EReturnCode returnCode; + }; + + struct TransferToken_input + { + id newOwner; + uint64 amount; + }; + + struct TransferToken_output + { + EReturnCode returnCode; + }; + + struct TransferToken_locals + { + id ownerAndPossessor; + }; + public: /** * @brief Registers all externally callable functions and procedures with their numeric @@ -341,9 +465,16 @@ struct RL : public ContractBase REGISTER_USER_FUNCTION(GetNextEpochData, 8); REGISTER_USER_FUNCTION(GetDrawHour, 9); REGISTER_USER_FUNCTION(GetSchedule, 10); + REGISTER_USER_FUNCTION(GetTokenData, 11); + REGISTER_USER_PROCEDURE(BuyTicket, 1); REGISTER_USER_PROCEDURE(SetPrice, 2); REGISTER_USER_PROCEDURE(SetSchedule, 3); + REGISTER_USER_PROCEDURE(BuyTicketWithToken, 4); + REGISTER_USER_PROCEDURE(AddAllowedToken, 5); + REGISTER_USER_PROCEDURE(RemoveAllowedToken, 6); + REGISTER_USER_PROCEDURE(SetTokenRewardDivisor, 7); + REGISTER_USER_PROCEDURE(TransferToken, 8); } /** @@ -367,7 +498,7 @@ struct RL : public ContractBase state.ticketPrice = RL_TICKET_PRICE; // Start in LOCKED state; selling must be explicitly opened with BEGIN_EPOCH - state.currentState = EState::LOCKED; + enableBuyTicket(state, false); // Reset player counter state.playerCounter = 0; @@ -397,6 +528,21 @@ struct RL : public ContractBase // Open selling for the new epoch enableBuyTicket(state, state.lastDrawDateStamp != RL_DEFAULT_INIT_TIME); + + if (!qpi.isAssetIssued(SELF, RL_TOKEN_NAME)) + { + qpi.issueAsset(RL_TOKEN_NAME, SELF, 0, MAX_AMOUNT, 0); + } + + if (state.allowedTokens.getElementIndex(RL_TOKEN_NAME) == NULL_INDEX) + { + state.allowedTokens.set(RL_TOKEN_NAME, {RL_DEFAULT_TOKEN_PRICE}); + } + + if (state.tokenRewardDivisor == 0) + { + state.tokenRewardDivisor = RL_TOKEN_REWARD_DIVISOR; + } } END_EPOCH() @@ -477,7 +623,21 @@ struct RL : public ContractBase // Draw { - if (state.playerCounter <= 1) + locals.hasMultipleParticipants = false; + if (state.playerCounter >= 2) + { + const id firstPlayer = state.players.get(0); + for (uint64 i = 1; i < state.playerCounter; ++i) + { + if (state.players.get(i) != firstPlayer) + { + locals.hasMultipleParticipants = true; + break; + } + } + } + + if (!locals.hasMultipleParticipants) { ReturnAllTickets(qpi, state, locals.returnAllTicketsInput, locals.returnAllTicketsOutput, locals.returnAllTicketsLocals); } @@ -494,10 +654,8 @@ struct RL : public ContractBase if (state.playerCounter != 0) { locals.mixedSpectrumValue = qpi.getPrevSpectrumDigest(); - locals.mixedSpectrumValue.u64._0 ^= qpi.tick(); - locals.mixedSpectrumValue.u64._1 ^= state.playerCounter; - // Compute pseudo-random index based on K12(prevSpectrumDigest ^ tick) - locals.randomNum = mod(qpi.K12(locals.mixedSpectrumValue).u64._0, state.playerCounter); + getRandomU64(state, qpi, locals.mixedSpectrumValue, locals.randomNum); + locals.randomNum = mod(locals.randomNum, state.playerCounter); // Index directly into players array locals.winnerAddress = state.players.get(locals.randomNum); @@ -549,6 +707,8 @@ struct RL : public ContractBase } } + rewardParticipantsWithRLT(qpi, state, locals); + clearStateOnEndDraw(state); // Resume selling unless today is Wednesday (remains closed until next epoch) @@ -604,6 +764,20 @@ struct RL : public ContractBase PUBLIC_FUNCTION(GetNextEpochData) { output.nextEpochData = state.nexEpochData; } PUBLIC_FUNCTION(GetDrawHour) { output.drawHour = state.drawHour; } PUBLIC_FUNCTION(GetSchedule) { output.schedule = state.schedule; } + PUBLIC_FUNCTION_WITH_LOCALS(GetTokenData) + { + locals.arrayIndex = 0; + + locals.hashMapIndex = state.allowedTokens.nextElementIndex(NULL_INDEX); + while (locals.hashMapIndex != NULL_INDEX) + { + locals.outTokenData.tokenName = state.allowedTokens.key(locals.hashMapIndex); + locals.outTokenData.tokenData = state.allowedTokens.value(locals.hashMapIndex); + output.tokenData.set(locals.arrayIndex++, locals.outTokenData); + + locals.hashMapIndex = state.allowedTokens.nextElementIndex(locals.hashMapIndex); + } + } PUBLIC_FUNCTION_WITH_LOCALS(GetBalance) { qpi.getEntity(SELF, locals.entity); @@ -612,41 +786,145 @@ struct RL : public ContractBase PUBLIC_PROCEDURE(SetPrice) { + returnInvocationReward(qpi); + // Only team/owner can queue a price change if (qpi.invocator() != state.teamAddress) { - output.returnCode = static_cast(EReturnCode::ACCESS_DENIED); + output.returnCode = EReturnCode::ACCESS_DENIED; return; } // Zero price is invalid if (input.newPrice == 0) { - output.returnCode = static_cast(EReturnCode::TICKET_INVALID_PRICE); + output.returnCode = EReturnCode::TICKET_INVALID_PRICE; return; } // Defer application until END_EPOCH state.nexEpochData.newPrice = input.newPrice; - output.returnCode = static_cast(EReturnCode::SUCCESS); + output.returnCode = EReturnCode::SUCCESS; } PUBLIC_PROCEDURE(SetSchedule) { + returnInvocationReward(qpi); + if (qpi.invocator() != state.teamAddress) { - output.returnCode = static_cast(EReturnCode::ACCESS_DENIED); + output.returnCode = EReturnCode::ACCESS_DENIED; return; } if (input.newSchedule == 0) { - output.returnCode = static_cast(EReturnCode::INVALID_VALUE); + output.returnCode = EReturnCode::INVALID_VALUE; return; } state.nexEpochData.schedule = input.newSchedule; - output.returnCode = static_cast(EReturnCode::SUCCESS); + output.returnCode = EReturnCode::SUCCESS; + } + + PUBLIC_PROCEDURE_WITH_LOCALS(AddAllowedToken) + { + returnInvocationReward(qpi); + + if (qpi.invocator() != state.ownerAddress) + { + output.returnCode = EReturnCode::ACCESS_DENIED; + return; + } + + if (input.tokenPrice == 0) + { + output.returnCode = EReturnCode::TICKET_INVALID_PRICE; + return; + } + + if (getAllowedToken(state, input.tokenName, locals.tokenData)) + { + locals.tokenData.pricePerTicket = input.tokenPrice; + state.allowedTokens.set(input.tokenName, locals.tokenData); + output.returnCode = EReturnCode::SUCCESS; + return; + } + + if (state.allowedTokens.population() >= state.allowedTokens.capacity()) + { + output.returnCode = EReturnCode::INVALID_VALUE; + return; + } + + locals.tokenData.pricePerTicket = input.tokenPrice; + state.allowedTokens.set(input.tokenName, locals.tokenData); + + output.returnCode = EReturnCode::SUCCESS; + } + + PUBLIC_PROCEDURE(RemoveAllowedToken) + { + returnInvocationReward(qpi); + + if (qpi.invocator() != state.ownerAddress) + { + output.returnCode = EReturnCode::ACCESS_DENIED; + return; + } + + state.allowedTokens.removeByKey(input.tokenName); + output.returnCode = EReturnCode::SUCCESS; + } + + PUBLIC_PROCEDURE(SetTokenRewardDivisor) + { + returnInvocationReward(qpi); + + if (qpi.invocator() != state.ownerAddress) + { + output.returnCode = EReturnCode::ACCESS_DENIED; + return; + } + + if (input.tokenRewardDivisor == 0) + { + output.returnCode = EReturnCode::INVALID_VALUE; + return; + } + + state.tokenRewardDivisor = input.tokenRewardDivisor; + output.returnCode = EReturnCode::SUCCESS; + } + + PUBLIC_PROCEDURE_WITH_LOCALS(TransferToken) + { + returnInvocationReward(qpi); + + if (input.amount == 0) + { + output.returnCode = EReturnCode::INVALID_VALUE; + return; + } + + if (isZero(input.newOwner)) + { + output.returnCode = EReturnCode::INVALID_VALUE; + return; + } + + locals.ownerAndPossessor = qpi.invocator() == state.ownerAddress ? SELF : qpi.invocator(); + + if (getNumberOfToken(qpi, locals.ownerAndPossessor) < input.amount) + { + output.returnCode = EReturnCode::TOKEN_TRANSFER_FAILED; + return; + } + + qpi.transferShareOwnershipAndPossession(RL_TOKEN_NAME, SELF, locals.ownerAndPossessor, locals.ownerAndPossessor, input.amount, + input.newOwner); + + output.returnCode = EReturnCode::SUCCESS; } /** @@ -662,14 +940,14 @@ struct RL : public ContractBase locals.reward = qpi.invocationReward(); // Selling closed: refund any attached funds and exit - if (state.currentState == EState::LOCKED) + if (!isSellingOpen(state)) { if (locals.reward > 0) { qpi.transfer(qpi.invocator(), locals.reward); } - output.returnCode = static_cast(EReturnCode::TICKET_SELLING_CLOSED); + output.returnCode = EReturnCode::TICKET_SELLING_CLOSED; return; } @@ -683,7 +961,7 @@ struct RL : public ContractBase qpi.transfer(qpi.invocator(), locals.reward); } - output.returnCode = static_cast(EReturnCode::TICKET_INVALID_PRICE); + output.returnCode = EReturnCode::TICKET_INVALID_PRICE; return; } @@ -697,7 +975,7 @@ struct RL : public ContractBase { qpi.transfer(qpi.invocator(), locals.reward); } - output.returnCode = static_cast(EReturnCode::TICKET_ALL_SOLD_OUT); + output.returnCode = EReturnCode::TICKET_ALL_SOLD_OUT; return; } @@ -724,7 +1002,69 @@ struct RL : public ContractBase qpi.transfer(qpi.invocator(), locals.refundAmount); } - output.returnCode = static_cast(EReturnCode::SUCCESS); + output.returnCode = EReturnCode::SUCCESS; + } + + PUBLIC_PROCEDURE_WITH_LOCALS(BuyTicketWithToken) + { + // Refund any QU mistakenly attached to a token purchase + returnInvocationReward(qpi); + + if (!isSellingOpen(state)) + { + output.returnCode = EReturnCode::TICKET_SELLING_CLOSED; + return; + } + + if (!getAllowedToken(state, input.tokenName, locals.tokenData)) + { + output.returnCode = EReturnCode::TOKEN_NOT_ALLOWED; + return; + } + + locals.price = locals.tokenData.pricePerTicket; + + if (locals.price == 0 || input.tokenAmount < locals.price) + { + output.returnCode = EReturnCode::TICKET_INVALID_PRICE; + return; + } + + locals.slotsLeft = (state.playerCounter < state.players.capacity()) ? (state.players.capacity() - state.playerCounter) : 0; + if (locals.slotsLeft == 0) + { + output.returnCode = EReturnCode::TICKET_ALL_SOLD_OUT; + return; + } + + // Move tokens from buyer to contract + if (qpi.transferShareOwnershipAndPossession(input.tokenName, SELF, qpi.invocator(), qpi.invocator(), input.tokenAmount, SELF) < 0) + { + output.returnCode = EReturnCode::TOKEN_TRANSFER_FAILED; + return; + } + + locals.desired = div(input.tokenAmount, locals.price); + locals.remainder = mod(input.tokenAmount, locals.price); + locals.toBuy = min(locals.desired, locals.slotsLeft); + + for (locals.i = 0; locals.i < locals.toBuy; ++locals.i) + { + if (state.playerCounter < state.players.capacity()) + { + state.players.set(state.playerCounter, qpi.invocator()); + state.playerCounter = min(state.playerCounter + 1, state.players.capacity()); + } + } + + locals.unfilled = locals.desired - locals.toBuy; + locals.refundAmount = locals.remainder + (locals.unfilled * locals.price); + if (locals.refundAmount > 0) + { + qpi.transferShareOwnershipAndPossession(input.tokenName, SELF, SELF, SELF, locals.refundAmount, qpi.invocator()); + } + + output.returnCode = EReturnCode::SUCCESS; } private: @@ -858,6 +1198,10 @@ struct RL : public ContractBase */ EState currentState; + HashMap allowedTokens; + + uint16 tokenRewardDivisor; + protected: static void clearStateOnEndEpoch(RL& state) { @@ -892,7 +1236,12 @@ struct RL : public ContractBase } } - static void enableBuyTicket(RL& state, bool bEnable) { state.currentState = bEnable ? EState::SELLING : EState::LOCKED; } + static void enableBuyTicket(RL& state, bool bEnable) + { + state.currentState = bEnable ? state.currentState | EState::SELLING : state.currentState & ~EState::SELLING; + } + + static bool isSellingOpen(const RL& state) { return (state.currentState & EState::SELLING) != 0; } static void getWinnerCounter(const RL& state, uint64& outCounter) { outCounter = mod(state.winnersCounter, state.winners.capacity()); } @@ -903,4 +1252,54 @@ struct RL : public ContractBase static void getSCRevenue(const Entity& entity, uint64& revenue) { revenue = entity.incomingAmount - entity.outgoingAmount; } template static constexpr const T& min(const T& a, const T& b) { return (a < b) ? a : b; } + + template static constexpr const T& max(const T& a, const T& b) { return (a > b) ? a : b; } + + static bool getAllowedToken(const RL& state, uint64 tokenName, TokenData& outToken) { return state.allowedTokens.get(tokenName, outToken); } + + static void rewardParticipantsWithRLT(const QpiContextProcedureCall& qpi, const RL& state, BEGIN_TICK_locals& locals) + { + if (state.playerCounter == 0) + { + return; + } + + if (state.tokenRewardDivisor == 0) + { + return; + } + + if (!getAllowedToken(state, RL_TOKEN_NAME, locals.tokenData) || locals.tokenData.pricePerTicket == 0) + { + return; + } + + locals.rewardPerTicket = max(1, div(locals.tokenData.pricePerTicket, state.tokenRewardDivisor)); + + for (locals.index = 0; locals.index < state.playerCounter; ++locals.index) + { + qpi.transferShareOwnershipAndPossession(RL_TOKEN_NAME, SELF, SELF, SELF, locals.rewardPerTicket, state.players.get(locals.index)); + } + } + + static void returnInvocationReward(const QpiContextProcedureCall& qpi) + { + // Refund any QU mistakenly attached to a token purchase + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + + static sint64 getNumberOfToken(const QpiContextFunctionCall& qpi, const id& ownerAndPossessor) + { + return qpi.numberOfPossessedShares(RL_TOKEN_NAME, SELF, ownerAndPossessor, ownerAndPossessor, SELF_INDEX, SELF_INDEX); + } + + static void getRandomU64(const RL& state, const QpiContextFunctionCall& qpi, m256i& spectrumData, uint64& outRandomValue) + { + spectrumData.u64._0 ^= qpi.tick(); + spectrumData.u64._1 ^= state.playerCounter; + outRandomValue = qpi.K12(spectrumData).u64._0; + } }; diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index 798fa8d59..2586fb6f2 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -6,6 +6,11 @@ constexpr uint16 PROCEDURE_INDEX_BUY_TICKET = 1; constexpr uint16 PROCEDURE_INDEX_SET_PRICE = 2; constexpr uint16 PROCEDURE_INDEX_SET_SCHEDULE = 3; +constexpr uint16 PROCEDURE_INDEX_BUY_TICKET_WITH_TOKEN = 4; +constexpr uint16 PROCEDURE_INDEX_ADD_ALLOWED_TOKEN = 5; +constexpr uint16 PROCEDURE_INDEX_REMOVE_ALLOWED_TOKEN = 6; +constexpr uint16 PROCEDURE_INDEX_SET_TOKEN_REWARD_DIVISOR = 7; +constexpr uint16 PROCEDURE_INDEX_TRANSFER_TOKEN = 8; constexpr uint16 FUNCTION_INDEX_GET_FEES = 1; constexpr uint16 FUNCTION_INDEX_GET_PLAYERS = 2; constexpr uint16 FUNCTION_INDEX_GET_WINNERS = 3; @@ -16,6 +21,9 @@ constexpr uint16 FUNCTION_INDEX_GET_BALANCE = 7; constexpr uint16 FUNCTION_INDEX_GET_NEXT_EPOCH_DATA = 8; constexpr uint16 FUNCTION_INDEX_GET_DRAW_HOUR = 9; constexpr uint16 FUNCTION_INDEX_GET_SCHEDULE = 10; +constexpr uint16 FUNCTION_INDEX_GET_TOKEN_DATA = 11; +constexpr uint8 STATE_SELLING = static_cast(RL::EState::SELLING); +constexpr uint8 STATE_LOCKED = 0u; static const id RL_DEV_ADDRESS = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, _R, _L, _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); @@ -36,6 +44,11 @@ bool operator==(const RL::WinnerInfo& left, const RL::WinnerInfo& right) left.dayOfWeek == right.dayOfWeek; } +static void encodeTokenName(uint64 tokenName, char (&out)[8]) +{ + copyMem(out, &tokenName, sizeof(tokenName)); +} + // Test helper that exposes internal state assertions and utilities class RLChecker : public RL { @@ -44,7 +57,7 @@ class RLChecker : public RL void checkFees(const GetFees_output& fees) { - EXPECT_EQ(fees.returnCode, static_cast(EReturnCode::SUCCESS)); + EXPECT_EQ(fees.returnCode, EReturnCode::SUCCESS); EXPECT_EQ(fees.distributionFeePercent, distributionFeePercent); EXPECT_EQ(fees.teamFeePercent, teamFeePercent); @@ -54,7 +67,7 @@ class RLChecker : public RL void checkPlayers(const GetPlayers_output& output) const { - EXPECT_EQ(output.returnCode, static_cast(EReturnCode::SUCCESS)); + EXPECT_EQ(output.returnCode, EReturnCode::SUCCESS); EXPECT_EQ(output.players.capacity(), players.capacity()); EXPECT_EQ(output.playerCounter, playerCounter); @@ -66,7 +79,7 @@ class RLChecker : public RL void checkWinners(const GetWinners_output& output) const { - EXPECT_EQ(output.returnCode, static_cast(EReturnCode::SUCCESS)); + EXPECT_EQ(output.returnCode, EReturnCode::SUCCESS); EXPECT_EQ(output.winners.capacity(), winners.capacity()); const uint64 expectedCount = mod(winnersCounter, winners.capacity()); @@ -106,6 +119,37 @@ class RLChecker : public RL void setScheduleMask(uint8 newMask) { schedule = newMask; } + void expectAllowedToken(uint64 tokenName, uint64 expectedPrice) const + { + RL::TokenData tokenData{}; + EXPECT_TRUE(getAllowedToken(*this, tokenName, tokenData)); + EXPECT_EQ(tokenData.pricePerTicket, expectedPrice); + } + + bool hasAllowedToken(uint64 tokenName) const + { + RL::TokenData tokenData{}; + return getAllowedToken(*this, tokenName, tokenData); + } + + bool tokenDataOutputContains(const RL::GetTokenData_output& out, uint64 tokenName, uint64 expectedPrice) const + { + for (uint64 i = 0; i < out.tokenData.capacity(); ++i) + { + const auto entry = out.tokenData.get(i); + if (entry.tokenName == 0) + { + continue; + } + + if (entry.tokenName == tokenName) + { + return entry.tokenData.pricePerTicket == expectedPrice; + } + } + return false; + } + uint64 getPlayerCounter() const { return playerCounter; } uint64 getTicketPrice() const { return ticketPrice; } @@ -115,6 +159,8 @@ class RLChecker : public RL uint8 getDrawHourInternal() const { return drawHour; } uint32 getLastDrawDateStamp() const { return lastDrawDateStamp; } + + uint64 getTokenRewardDivisor() const { return tokenRewardDivisor; } }; class ContractTestingRL : protected ContractTesting @@ -223,13 +269,32 @@ class ContractTestingRL : protected ContractTesting return output; } + RL::GetTokenData_output getTokenData() + { + RL::GetTokenData_input input{}; + RL::GetTokenData_output output{}; + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_TOKEN_DATA, input, output); + return output; + } + RL::BuyTicket_output buyTicket(const id& user, uint64 reward) { RL::BuyTicket_input input; RL::BuyTicket_output output; if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_BUY_TICKET, input, output, user, reward)) { - output.returnCode = static_cast(RL::EReturnCode::UNKNOWN_ERROR); + output.returnCode = RL::EReturnCode::UNKNOWN_ERROR; + } + return output; + } + + RL::BuyTicketWithToken_output buyTicketWithToken(const id& user, uint64 tokenName, uint64 tokenAmount) + { + RL::BuyTicketWithToken_input input{tokenName, tokenAmount}; + RL::BuyTicketWithToken_output output; + if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_BUY_TICKET_WITH_TOKEN, input, output, user, 0)) + { + output.returnCode = RL::EReturnCode::UNKNOWN_ERROR; } return output; } @@ -242,7 +307,7 @@ class ContractTestingRL : protected ContractTesting RL::SetPrice_output output; if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_SET_PRICE, input, output, invocator, 0)) { - output.returnCode = static_cast(RL::EReturnCode::UNKNOWN_ERROR); + output.returnCode = RL::EReturnCode::UNKNOWN_ERROR; } return output; } @@ -255,7 +320,51 @@ class ContractTestingRL : protected ContractTesting RL::SetSchedule_output output; if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_SET_SCHEDULE, input, output, invocator, 0)) { - output.returnCode = static_cast(RL::EReturnCode::UNKNOWN_ERROR); + output.returnCode = RL::EReturnCode::UNKNOWN_ERROR; + } + return output; + } + + RL::AddAllowedToken_output addAllowedToken(const id& invocator, uint64 tokenName, uint64 tokenPrice) + { + RL::AddAllowedToken_input input{tokenName, tokenPrice}; + RL::AddAllowedToken_output output; + if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_ADD_ALLOWED_TOKEN, input, output, invocator, 0)) + { + output.returnCode = RL::EReturnCode::UNKNOWN_ERROR; + } + return output; + } + + RL::RemoveAllowedToken_output removeAllowedToken(const id& invocator, uint64 tokenName, uint64 reward = 0) + { + RL::RemoveAllowedToken_input input{tokenName}; + RL::RemoveAllowedToken_output output{}; + if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_REMOVE_ALLOWED_TOKEN, input, output, invocator, reward)) + { + output.returnCode = RL::EReturnCode::UNKNOWN_ERROR; + } + return output; + } + + RL::SetTokenRewardDivisor_output setTokenRewardDivisor(const id& invocator, uint64 divisor) + { + RL::SetTokenRewardDivisor_input input{divisor}; + RL::SetTokenRewardDivisor_output output{}; + if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_SET_TOKEN_REWARD_DIVISOR, input, output, invocator, 0)) + { + output.returnCode = RL::EReturnCode::UNKNOWN_ERROR; + } + return output; + } + + RL::TransferToken_output transferToken(const id& invocator, const id& newOwner, uint64 amount) + { + RL::TransferToken_input input{newOwner, amount}; + RL::TransferToken_output output{}; + if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_TRANSFER_TOKEN, input, output, invocator, 0)) + { + output.returnCode = RL::EReturnCode::UNKNOWN_ERROR; } return output; } @@ -267,7 +376,7 @@ class ContractTestingRL : protected ContractTesting void BeginTick() { callSystemProcedure(RL_CONTRACT_INDEX, BEGIN_TICK); } // Returns the SELF contract account address - id rlSelf() { return id(RL_CONTRACT_INDEX, 0, 0, 0); } + id rlSelf() const { return id(RL_CONTRACT_INDEX, 0, 0, 0); } // Computes remaining contract balance after winner/team/distribution/burn payouts // Distribution is floored to a multiple of NUMBER_OF_COMPUTORS @@ -286,7 +395,31 @@ class ContractTestingRL : protected ContractTesting { increaseEnergy(user, ticketPrice * 2); const RL::BuyTicket_output out = ctl.buyTicket(user, ticketPrice); - EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(out.returnCode, RL::EReturnCode::SUCCESS); + } + + uint64 issueManagedTokenToUser(uint64 tokenName, const id& user, uint64 amount) + { + int issuanceIdx = -1; + int ownershipIdx = -1; + int possessionIdx = -1; + + char tokenNameBuf[8]; + encodeTokenName(tokenName, tokenNameBuf); + EXPECT_EQ(issueAsset(rlSelf(), tokenNameBuf, 0, CONTRACT_ASSET_UNIT_OF_MEASUREMENT, amount, RL_CONTRACT_INDEX, &issuanceIdx, &ownershipIdx, + &possessionIdx), + amount); + + int dstOwnershipIdx = -1; + int dstPossessionIdx = -1; + EXPECT_TRUE(transferShareOwnershipAndPossession(ownershipIdx, possessionIdx, user, amount, &dstOwnershipIdx, &dstPossessionIdx, true)); + + return tokenName; + } + + sint64 tokenBalance(uint64 assetName, const id& owner, const id& issuer) const + { + return numberOfPossessedShares(assetName, issuer, owner, owner, RL_CONTRACT_INDEX, RL_CONTRACT_INDEX); } // Assert contract account balance equals the value returned by RL::GetBalance @@ -371,8 +504,8 @@ TEST(ContractRandomLottery, SetPriceAndScheduleApplyNextEpoch) constexpr uint64 newPrice = 5000000; constexpr uint8 wednesdayOnly = static_cast(1 << WEDNESDAY); increaseEnergy(RL_DEV_ADDRESS, 3); - EXPECT_EQ(ctl.setPrice(RL_DEV_ADDRESS, newPrice).returnCode, static_cast(RL::EReturnCode::SUCCESS)); - EXPECT_EQ(ctl.setSchedule(RL_DEV_ADDRESS, wednesdayOnly).returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(ctl.setPrice(RL_DEV_ADDRESS, newPrice).returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.setSchedule(RL_DEV_ADDRESS, wednesdayOnly).returnCode, RL::EReturnCode::SUCCESS); const RL::NextEpochData nextDataBefore = ctl.getNextEpochData().nextEpochData; EXPECT_EQ(nextDataBefore.newPrice, newPrice); @@ -398,7 +531,7 @@ TEST(ContractRandomLottery, SetPriceAndScheduleApplyNextEpoch) const uint64 balBefore = getBalance(buyer); const uint64 playersBefore = ctl.state()->getPlayerCounter(); const RL::BuyTicket_output buyOut = ctl.buyTicket(buyer, newPrice); - EXPECT_EQ(buyOut.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(buyOut.returnCode, RL::EReturnCode::SUCCESS); const uint64 playersAfterFirstBuy = playersBefore + 1; EXPECT_EQ(ctl.state()->getPlayerCounter(), playersAfterFirstBuy); EXPECT_EQ(getBalance(buyer), balBefore - newPrice); @@ -408,7 +541,7 @@ TEST(ContractRandomLottery, SetPriceAndScheduleApplyNextEpoch) increaseEnergy(secondBuyer, newPrice * 2); const uint64 secondBalBefore = getBalance(secondBuyer); const RL::BuyTicket_output secondBuyOut = ctl.buyTicket(secondBuyer, newPrice); - EXPECT_EQ(secondBuyOut.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(secondBuyOut.returnCode, RL::EReturnCode::SUCCESS); const uint64 playersAfterBuy = playersAfterFirstBuy + 1; EXPECT_EQ(ctl.state()->getPlayerCounter(), playersAfterBuy); EXPECT_EQ(getBalance(secondBuyer), secondBalBefore - newPrice); @@ -419,7 +552,7 @@ TEST(ContractRandomLottery, SetPriceAndScheduleApplyNextEpoch) ctl.setDateTime(2025, 1, 15, RL_DEFAULT_DRAW_HOUR + 1); // current Wednesday ctl.forceBeginTick(); EXPECT_EQ(ctl.state()->getPlayerCounter(), playersAfterBuy); - EXPECT_EQ(ctl.getStateInfo().currentState, static_cast(RL::EState::SELLING)); + EXPECT_EQ(ctl.getStateInfo().currentState, STATE_SELLING); EXPECT_EQ(ctl.getWinners().winnersCounter, winnersBefore); // No draw on non-scheduled days between Wednesdays @@ -433,13 +566,13 @@ TEST(ContractRandomLottery, SetPriceAndScheduleApplyNextEpoch) ctl.forceBeginTick(); EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); EXPECT_EQ(ctl.getWinners().winnersCounter, winnersBefore + 1); - EXPECT_EQ(ctl.getStateInfo().currentState, static_cast(RL::EState::LOCKED)); + EXPECT_EQ(ctl.getStateInfo().currentState, STATE_LOCKED); // After the draw and before the next epoch begins, ticket purchases are blocked const id lockedBuyer = id::randomValue(); increaseEnergy(lockedBuyer, newPrice); const RL::BuyTicket_output lockedOut = ctl.buyTicket(lockedBuyer, newPrice); - EXPECT_EQ(lockedOut.returnCode, static_cast(RL::EReturnCode::TICKET_SELLING_CLOSED)); + EXPECT_EQ(lockedOut.returnCode, RL::EReturnCode::TICKET_SELLING_CLOSED); } TEST(ContractRandomLottery, DefaultInitTimeGuardSkipsPlaceholderDate) @@ -454,13 +587,13 @@ TEST(ContractRandomLottery, DefaultInitTimeGuardSkipsPlaceholderDate) // Simulate the placeholder 2022-04-13 QPI date during initialization ctl.setDateTime(2022, 4, 13, RL_DEFAULT_DRAW_HOUR + 1); ctl.BeginEpoch(); - EXPECT_EQ(ctl.getStateInfo().currentState, static_cast(RL::EState::LOCKED)); + EXPECT_EQ(ctl.getStateInfo().currentState, STATE_LOCKED); // Selling is blocked until a valid date arrives const id blockedBuyer = id::randomValue(); increaseEnergy(blockedBuyer, ticketPrice); const RL::BuyTicket_output denied = ctl.buyTicket(blockedBuyer, ticketPrice); - EXPECT_EQ(denied.returnCode, static_cast(RL::EReturnCode::TICKET_SELLING_CLOSED)); + EXPECT_EQ(denied.returnCode, RL::EReturnCode::TICKET_SELLING_CLOSED); EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); const uint64 winnersBefore = ctl.getWinners().winnersCounter; @@ -468,14 +601,14 @@ TEST(ContractRandomLottery, DefaultInitTimeGuardSkipsPlaceholderDate) // BEGIN_TICK should detect the placeholder date and skip processing, but remember the sentinel day ctl.forceBeginTick(); EXPECT_EQ(ctl.state()->getLastDrawDateStamp(), RL_DEFAULT_INIT_TIME); - EXPECT_EQ(ctl.getStateInfo().currentState, static_cast(RL::EState::LOCKED)); + EXPECT_EQ(ctl.getStateInfo().currentState, STATE_LOCKED); EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); EXPECT_EQ(ctl.getWinners().winnersCounter, winnersBefore); // First valid day re-opens selling but still skips the draw ctl.setDateTime(2025, 1, 10, RL_DEFAULT_DRAW_HOUR + 1); ctl.forceBeginTick(); - EXPECT_EQ(ctl.getStateInfo().currentState, static_cast(RL::EState::SELLING)); + EXPECT_EQ(ctl.getStateInfo().currentState, STATE_SELLING); EXPECT_NE(ctl.state()->getLastDrawDateStamp(), RL_DEFAULT_INIT_TIME); const id playerA = id::randomValue(); @@ -503,18 +636,18 @@ TEST(ContractRandomLottery, SellingUnlocksWhenTimeSetBeforeScheduledDay) const id deniedBuyer = id::randomValue(); increaseEnergy(deniedBuyer, ticketPrice); - EXPECT_EQ(ctl.buyTicket(deniedBuyer, ticketPrice).returnCode, static_cast(RL::EReturnCode::TICKET_SELLING_CLOSED)); + EXPECT_EQ(ctl.buyTicket(deniedBuyer, ticketPrice).returnCode, RL::EReturnCode::TICKET_SELLING_CLOSED); ctl.setDateTime(2025, 1, 14, RL_DEFAULT_DRAW_HOUR + 2); // Tuesday, not scheduled by default ctl.forceBeginTick(); - EXPECT_EQ(ctl.getStateInfo().currentState, static_cast(RL::EState::SELLING)); + EXPECT_EQ(ctl.getStateInfo().currentState, STATE_SELLING); EXPECT_EQ(ctl.state()->getLastDrawDateStamp(), 0u); const id allowedBuyer = id::randomValue(); increaseEnergy(allowedBuyer, ticketPrice); const RL::BuyTicket_output allowed = ctl.buyTicket(allowedBuyer, ticketPrice); - EXPECT_EQ(allowed.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(allowed.returnCode, RL::EReturnCode::SUCCESS); } TEST(ContractRandomLottery, SellingUnlocksWhenTimeSetOnDrawDay) @@ -528,19 +661,19 @@ TEST(ContractRandomLottery, SellingUnlocksWhenTimeSetOnDrawDay) const id deniedBuyer = id::randomValue(); increaseEnergy(deniedBuyer, ticketPrice); - EXPECT_EQ(ctl.buyTicket(deniedBuyer, ticketPrice).returnCode, static_cast(RL::EReturnCode::TICKET_SELLING_CLOSED)); + EXPECT_EQ(ctl.buyTicket(deniedBuyer, ticketPrice).returnCode, RL::EReturnCode::TICKET_SELLING_CLOSED); ctl.setDateTime(2025, 1, 15, RL_DEFAULT_DRAW_HOUR + 2); // Wednesday draw day ctl.forceBeginTick(); const uint32 expectedStamp = makeDateStamp(2025, 1, 15); EXPECT_EQ(ctl.state()->getLastDrawDateStamp(), expectedStamp); - EXPECT_EQ(ctl.getStateInfo().currentState, static_cast(RL::EState::SELLING)); + EXPECT_EQ(ctl.getStateInfo().currentState, STATE_SELLING); const id allowedBuyer = id::randomValue(); increaseEnergy(allowedBuyer, ticketPrice); const RL::BuyTicket_output allowed = ctl.buyTicket(allowedBuyer, ticketPrice); - EXPECT_EQ(allowed.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(allowed.returnCode, RL::EReturnCode::SUCCESS); } TEST(ContractRandomLottery, PostIncomingTransfer) @@ -605,7 +738,7 @@ TEST(ContractRandomLottery, BuyTicket) const id userLocked = id::randomValue(); increaseEnergy(userLocked, ticketPrice * 2); RL::BuyTicket_output out = ctl.buyTicket(userLocked, ticketPrice); - EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::TICKET_SELLING_CLOSED)); + EXPECT_EQ(out.returnCode, RL::EReturnCode::TICKET_SELLING_CLOSED); EXPECT_EQ(ctl.state()->getPlayerCounter(), 0); } @@ -625,24 +758,24 @@ TEST(ContractRandomLottery, BuyTicket) { // < ticketPrice RL::BuyTicket_output outInvalid = ctl.buyTicket(user, ticketPrice - 1); - EXPECT_EQ(outInvalid.returnCode, static_cast(RL::EReturnCode::TICKET_INVALID_PRICE)); + EXPECT_EQ(outInvalid.returnCode, RL::EReturnCode::TICKET_INVALID_PRICE); EXPECT_EQ(ctl.state()->getPlayerCounter(), expectedPlayers); // == 0 outInvalid = ctl.buyTicket(user, 0); - EXPECT_EQ(outInvalid.returnCode, static_cast(RL::EReturnCode::TICKET_INVALID_PRICE)); + EXPECT_EQ(outInvalid.returnCode, RL::EReturnCode::TICKET_INVALID_PRICE); EXPECT_EQ(ctl.state()->getPlayerCounter(), expectedPlayers); // < 0 outInvalid = ctl.buyTicket(user, -ticketPrice); - EXPECT_NE(outInvalid.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_NE(outInvalid.returnCode, RL::EReturnCode::SUCCESS); EXPECT_EQ(ctl.state()->getPlayerCounter(), expectedPlayers); } // (b) Valid purchase — player added { const RL::BuyTicket_output outOk = ctl.buyTicket(user, ticketPrice); - EXPECT_EQ(outOk.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(outOk.returnCode, RL::EReturnCode::SUCCESS); ++expectedPlayers; EXPECT_EQ(ctl.state()->getPlayerCounter(), expectedPlayers); } @@ -650,7 +783,7 @@ TEST(ContractRandomLottery, BuyTicket) // (c) Duplicate purchase — allowed, increases count { const RL::BuyTicket_output outDup = ctl.buyTicket(user, ticketPrice); - EXPECT_EQ(outDup.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(outDup.returnCode, RL::EReturnCode::SUCCESS); ++expectedPlayers; EXPECT_EQ(ctl.state()->getPlayerCounter(), expectedPlayers); } @@ -703,7 +836,7 @@ TEST(ContractRandomLottery, DrawAndPayout_BeginTick) const uint64 balanceBefore = getBalance(solo); const RL::BuyTicket_output out = ctl.buyTicket(solo, ticketPrice); - EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(out.returnCode, RL::EReturnCode::SUCCESS); EXPECT_EQ(ctl.state()->getPlayerCounter(), 1u); EXPECT_EQ(getBalance(solo), balanceBefore - ticketPrice); @@ -720,6 +853,31 @@ TEST(ContractRandomLottery, DrawAndPayout_BeginTick) EXPECT_EQ(winners.winnersCounter, winnersBeforeCount); } + // --- Scenario 2b: Multiple tickets from the same player are treated as single participant --- + { + ctl.beginEpochWithValidTime(); + + const id solo = id::randomValue(); + increaseEnergy(solo, ticketPrice * 10); + const uint64 balanceBefore = getBalance(solo); + + for (int i = 0; i < 5; ++i) + { + EXPECT_EQ(ctl.buyTicket(solo, ticketPrice).returnCode, RL::EReturnCode::SUCCESS); + } + EXPECT_EQ(ctl.state()->getPlayerCounter(), 5u); + + const uint64 winnersBeforeCount = ctl.getWinners().winnersCounter; + + ctl.advanceOneDayAndDraw(); + + // All tickets refunded, no winner recorded + EXPECT_EQ(getBalance(solo), balanceBefore); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); + EXPECT_EQ(ctl.getWinners().winnersCounter, winnersBeforeCount); + EXPECT_EQ(getBalance(contractAddress), 0u); + } + // --- Scenario 3: Multiple players (winner chosen, fees processed, correct remaining on contract) --- { ctl.beginEpochWithValidTime(); @@ -882,6 +1040,46 @@ TEST(ContractRandomLottery, DrawAndPayout_BeginTick) } } +TEST(ContractRandomLottery, ParticipantsReceiveRLTPerTicketOnDraw) +{ + ContractTestingRL ctl; + ctl.forceSchedule(RL_ANY_DAY_DRAW_SCHEDULE); + + const uint64 ticketPrice = ctl.state()->getTicketPrice(); + constexpr uint64 tokenPrice = 15; + constexpr uint64 rewardDivisor = 5; + ctl.beginEpochWithValidTime(); + + increaseEnergy(RL_DEV_ADDRESS, 1); + EXPECT_EQ(ctl.addAllowedToken(RL_DEV_ADDRESS, RL_TOKEN_NAME, tokenPrice).returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.setTokenRewardDivisor(RL_DEV_ADDRESS, rewardDivisor).returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.state()->getTokenRewardDivisor(), rewardDivisor); + + constexpr uint64 rewardPerTicket = std::max(1, tokenPrice / rewardDivisor); + const id playerA = id::randomValue(); + const id playerB = id::randomValue(); + + increaseEnergy(playerA, ticketPrice * 3); + increaseEnergy(playerB, ticketPrice * 2); + + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, playerA, ctl.rlSelf()), 0); + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, playerB, ctl.rlSelf()), 0); + + EXPECT_EQ(ctl.buyTicket(playerA, ticketPrice).returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.buyTicket(playerA, ticketPrice).returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.buyTicket(playerB, ticketPrice).returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 3u); + + const sint64 contractTokensBefore = ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); + + ctl.advanceOneDayAndDraw(); + + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, playerA, ctl.rlSelf()), rewardPerTicket * 2); + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, playerB, ctl.rlSelf()), rewardPerTicket); + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore - (rewardPerTicket * 3)); +} + TEST(ContractRandomLottery, GetBalance) { ContractTestingRL ctl; @@ -954,21 +1152,21 @@ TEST(ContractRandomLottery, GetState) // Initially LOCKED { const RL::GetState_output out0 = ctl.getStateInfo(); - EXPECT_EQ(out0.currentState, static_cast(RL::EState::LOCKED)); + EXPECT_EQ(out0.currentState, STATE_LOCKED); } // After BeginEpoch — SELLING ctl.beginEpochWithValidTime(); { const RL::GetState_output out1 = ctl.getStateInfo(); - EXPECT_EQ(out1.currentState, static_cast(RL::EState::SELLING)); + EXPECT_EQ(out1.currentState, STATE_SELLING); } // After END_EPOCH — back to LOCKED (selling disabled until next epoch) ctl.EndEpoch(); { const RL::GetState_output out2 = ctl.getStateInfo(); - EXPECT_EQ(out2.currentState, static_cast(RL::EState::LOCKED)); + EXPECT_EQ(out2.currentState, STATE_LOCKED); } } @@ -986,7 +1184,7 @@ TEST(ContractRandomLottery, SetPrice_AccessControl) increaseEnergy(randomUser, 1); const RL::SetPrice_output outDenied = ctl.setPrice(randomUser, newPrice); - EXPECT_EQ(outDenied.returnCode, static_cast(RL::EReturnCode::ACCESS_DENIED)); + EXPECT_EQ(outDenied.returnCode, RL::EReturnCode::ACCESS_DENIED); // Price doesn't change immediately nor after END_EPOCH implicitly EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); @@ -1003,7 +1201,7 @@ TEST(ContractRandomLottery, SetPrice_ZeroNotAllowed) const uint64 oldPrice = ctl.state()->getTicketPrice(); const RL::SetPrice_output outInvalid = ctl.setPrice(RL_DEV_ADDRESS, 0); - EXPECT_EQ(outInvalid.returnCode, static_cast(RL::EReturnCode::TICKET_INVALID_PRICE)); + EXPECT_EQ(outInvalid.returnCode, RL::EReturnCode::TICKET_INVALID_PRICE); // Price remains unchanged even after END_EPOCH EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); @@ -1021,7 +1219,7 @@ TEST(ContractRandomLottery, SetPrice_AppliesAfterEndEpoch) const uint64 newPrice = oldPrice * 2; const RL::SetPrice_output outOk = ctl.setPrice(RL_DEV_ADDRESS, newPrice); - EXPECT_EQ(outOk.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(outOk.returnCode, RL::EReturnCode::SUCCESS); // Check NextEpochData reflects pending change EXPECT_EQ(ctl.getNextEpochData().nextEpochData.newPrice, newPrice); @@ -1052,8 +1250,8 @@ TEST(ContractRandomLottery, SetPrice_OverrideBeforeEndEpoch) const uint64 secondPrice = oldPrice + 7777; // Two SetPrice calls before END_EPOCH — the last one should apply - EXPECT_EQ(ctl.setPrice(RL_DEV_ADDRESS, firstPrice).returnCode, static_cast(RL::EReturnCode::SUCCESS)); - EXPECT_EQ(ctl.setPrice(RL_DEV_ADDRESS, secondPrice).returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(ctl.setPrice(RL_DEV_ADDRESS, firstPrice).returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.setPrice(RL_DEV_ADDRESS, secondPrice).returnCode, RL::EReturnCode::SUCCESS); // NextEpochData shows the last queued value EXPECT_EQ(ctl.getNextEpochData().nextEpochData.newPrice, secondPrice); @@ -1080,13 +1278,13 @@ TEST(ContractRandomLottery, SetPrice_AffectsNextEpochBuys) increaseEnergy(u1, oldPrice * 2); { const RL::BuyTicket_output out1 = ctl.buyTicket(u1, oldPrice); - EXPECT_EQ(out1.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(out1.returnCode, RL::EReturnCode::SUCCESS); } // Set a new price, but before END_EPOCH purchases should use the old price logic (split by old price) { const RL::SetPrice_output setOut = ctl.setPrice(RL_DEV_ADDRESS, newPrice); - EXPECT_EQ(setOut.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(setOut.returnCode, RL::EReturnCode::SUCCESS); EXPECT_EQ(ctl.getNextEpochData().nextEpochData.newPrice, newPrice); } @@ -1096,7 +1294,7 @@ TEST(ContractRandomLottery, SetPrice_AffectsNextEpochBuys) const uint64 balBefore = getBalance(u2); const uint64 playersBefore = ctl.state()->getPlayerCounter(); const RL::BuyTicket_output outNow = ctl.buyTicket(u2, newPrice); - EXPECT_EQ(outNow.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(outNow.returnCode, RL::EReturnCode::SUCCESS); // floor(newPrice/oldPrice) tickets were bought, the remainder was refunded const uint64 bought = newPrice / oldPrice; EXPECT_EQ(ctl.state()->getPlayerCounter(), playersBefore + bought); @@ -1113,7 +1311,7 @@ TEST(ContractRandomLottery, SetPrice_AffectsNextEpochBuys) const uint64 balBefore = getBalance(u2); const uint64 playersBefore = ctl.state()->getPlayerCounter(); const RL::BuyTicket_output outOk = ctl.buyTicket(u2, newPrice); - EXPECT_EQ(outOk.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(outOk.returnCode, RL::EReturnCode::SUCCESS); EXPECT_EQ(ctl.state()->getPlayerCounter(), playersBefore + 1); EXPECT_EQ(getBalance(u2), balBefore - newPrice); } @@ -1125,11 +1323,11 @@ TEST(ContractRandomLottery, BuyMultipleTickets_ExactMultiple_NoRemainder) ctl.beginEpochWithValidTime(); const uint64 price = ctl.state()->getTicketPrice(); const id user = id::randomValue(); - const uint64 k = 7; + constexpr uint64 k = 7; increaseEnergy(user, price * k); const uint64 playersBefore = ctl.state()->getPlayerCounter(); const RL::BuyTicket_output out = ctl.buyTicket(user, price * k); - EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(out.returnCode, RL::EReturnCode::SUCCESS); EXPECT_EQ(ctl.state()->getPlayerCounter(), playersBefore + k); } @@ -1139,13 +1337,13 @@ TEST(ContractRandomLottery, BuyMultipleTickets_WithRemainder_Refunded) ctl.beginEpochWithValidTime(); const uint64 price = ctl.state()->getTicketPrice(); const id user = id::randomValue(); - const uint64 k = 5; + constexpr uint64 k = 5; const uint64 r = price / 3; // partial remainder increaseEnergy(user, price * k + r); const uint64 balBefore = getBalance(user); const uint64 playersBefore = ctl.state()->getPlayerCounter(); const RL::BuyTicket_output out = ctl.buyTicket(user, price * k + r); - EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(out.returnCode, RL::EReturnCode::SUCCESS); EXPECT_EQ(ctl.state()->getPlayerCounter(), playersBefore + k); // Remainder refunded, only k * price spent EXPECT_EQ(getBalance(user), balBefore - k * price); @@ -1164,7 +1362,7 @@ TEST(ContractRandomLottery, BuyMultipleTickets_CapacityPartialRefund) { const id u = id::randomValue(); increaseEnergy(u, price); - EXPECT_EQ(ctl.buyTicket(u, price).returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(ctl.buyTicket(u, price).returnCode, RL::EReturnCode::SUCCESS); } EXPECT_EQ(ctl.state()->getPlayerCounter(), toFill); @@ -1173,7 +1371,7 @@ TEST(ContractRandomLottery, BuyMultipleTickets_CapacityPartialRefund) increaseEnergy(buyer, price * 10); const uint64 balBefore = getBalance(buyer); const RL::BuyTicket_output out = ctl.buyTicket(buyer, price * 10); - EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(out.returnCode, RL::EReturnCode::SUCCESS); EXPECT_EQ(ctl.state()->getPlayerCounter(), capacity); EXPECT_EQ(getBalance(buyer), balBefore - price * 5); } @@ -1190,7 +1388,7 @@ TEST(ContractRandomLottery, BuyMultipleTickets_AllSoldOut) { const id u = id::randomValue(); increaseEnergy(u, price); - EXPECT_EQ(ctl.buyTicket(u, price).returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(ctl.buyTicket(u, price).returnCode, RL::EReturnCode::SUCCESS); } EXPECT_EQ(ctl.state()->getPlayerCounter(), capacity); @@ -1199,7 +1397,7 @@ TEST(ContractRandomLottery, BuyMultipleTickets_AllSoldOut) increaseEnergy(buyer, price * 3); const uint64 balBefore = getBalance(buyer); const RL::BuyTicket_output out = ctl.buyTicket(buyer, price * 3); - EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::TICKET_ALL_SOLD_OUT)); + EXPECT_EQ(out.returnCode, RL::EReturnCode::TICKET_ALL_SOLD_OUT); EXPECT_EQ(getBalance(buyer), balBefore); } @@ -1217,17 +1415,17 @@ TEST(ContractRandomLottery, GetSchedule_And_SetSchedule) const id rnd = id::randomValue(); increaseEnergy(rnd, 1); const RL::SetSchedule_output outDenied = ctl.setSchedule(rnd, RL_ANY_DAY_DRAW_SCHEDULE); - EXPECT_EQ(outDenied.returnCode, static_cast(RL::EReturnCode::ACCESS_DENIED)); + EXPECT_EQ(outDenied.returnCode, RL::EReturnCode::ACCESS_DENIED); // Invalid value: zero mask not allowed increaseEnergy(RL_DEV_ADDRESS, 1); const RL::SetSchedule_output outInvalid = ctl.setSchedule(RL_DEV_ADDRESS, 0); - EXPECT_EQ(outInvalid.returnCode, static_cast(RL::EReturnCode::INVALID_VALUE)); + EXPECT_EQ(outInvalid.returnCode, RL::EReturnCode::INVALID_VALUE); // Valid update queues into NextEpochData and applies after END_EPOCH const uint8 newMask = 0x5A; // some non-zero mask (bits set for selected days) const RL::SetSchedule_output outOk = ctl.setSchedule(RL_DEV_ADDRESS, newMask); - EXPECT_EQ(outOk.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(outOk.returnCode, RL::EReturnCode::SUCCESS); EXPECT_EQ(ctl.getNextEpochData().nextEpochData.schedule, newMask); // Not applied yet @@ -1250,3 +1448,211 @@ TEST(ContractRandomLottery, GetDrawHour_DefaultAfterBeginEpoch) ctl.beginEpochWithValidTime(); EXPECT_EQ(ctl.getDrawHour().drawHour, RL_DEFAULT_DRAW_HOUR); } + +TEST(ContractRandomLottery, DefaultTokenAddedOnBeginEpochAndListed) +{ + ContractTestingRL ctl; + + EXPECT_FALSE(ctl.state()->hasAllowedToken(RL_TOKEN_NAME)); + + ctl.beginEpochWithValidTime(); + EXPECT_TRUE(ctl.state()->hasAllowedToken(RL_TOKEN_NAME)); + ctl.state()->expectAllowedToken(RL_TOKEN_NAME, RL_DEFAULT_TOKEN_PRICE); + + const RL::GetTokenData_output out = ctl.getTokenData(); + EXPECT_TRUE(ctl.state()->tokenDataOutputContains(out, RL_TOKEN_NAME, RL_DEFAULT_TOKEN_PRICE)); +} + +TEST(ContractRandomLottery, AllowedTokens_AddUpdateAndRemove) +{ + ContractTestingRL ctl; + + const uint64 tokenName = assetNameFromString("TOKADD"); + constexpr uint64 initialPrice = 111; + constexpr uint64 updatedPrice = 222; + const id randomUser = id::randomValue(); + + increaseEnergy(randomUser, 1); + const RL::AddAllowedToken_output denied = ctl.addAllowedToken(randomUser, tokenName, initialPrice); + EXPECT_EQ(denied.returnCode, RL::EReturnCode::ACCESS_DENIED); + EXPECT_FALSE(ctl.state()->hasAllowedToken(tokenName)); + + increaseEnergy(RL_DEV_ADDRESS, 1); + const RL::AddAllowedToken_output invalid = ctl.addAllowedToken(RL_DEV_ADDRESS, tokenName, 0); + EXPECT_EQ(invalid.returnCode, RL::EReturnCode::TICKET_INVALID_PRICE); + EXPECT_FALSE(ctl.state()->hasAllowedToken(tokenName)); + + increaseEnergy(RL_DEV_ADDRESS, 1); + const RL::AddAllowedToken_output ok = ctl.addAllowedToken(RL_DEV_ADDRESS, tokenName, initialPrice); + EXPECT_EQ(ok.returnCode, RL::EReturnCode::SUCCESS); + ctl.state()->expectAllowedToken(tokenName, initialPrice); + + increaseEnergy(RL_DEV_ADDRESS, 1); + const RL::AddAllowedToken_output updated = ctl.addAllowedToken(RL_DEV_ADDRESS, tokenName, updatedPrice); + EXPECT_EQ(updated.returnCode, RL::EReturnCode::SUCCESS); + ctl.state()->expectAllowedToken(tokenName, updatedPrice); + const RL::GetTokenData_output outAfterUpdate = ctl.getTokenData(); + EXPECT_TRUE(ctl.state()->tokenDataOutputContains(outAfterUpdate, tokenName, updatedPrice)); + + constexpr uint64 invocationReward = 12345; + increaseEnergy(randomUser, invocationReward); + const uint64 randomBalanceBefore = getBalance(randomUser); + const RL::RemoveAllowedToken_output removeDenied = ctl.removeAllowedToken(randomUser, tokenName, invocationReward); + EXPECT_EQ(removeDenied.returnCode, RL::EReturnCode::ACCESS_DENIED); + EXPECT_EQ(getBalance(randomUser), randomBalanceBefore); + ctl.state()->expectAllowedToken(tokenName, updatedPrice); + + increaseEnergy(RL_DEV_ADDRESS, invocationReward); + const uint64 devBalanceBefore = getBalance(RL_DEV_ADDRESS); + const RL::RemoveAllowedToken_output removeOk = ctl.removeAllowedToken(RL_DEV_ADDRESS, tokenName, invocationReward); + EXPECT_EQ(removeOk.returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(getBalance(RL_DEV_ADDRESS), devBalanceBefore); + EXPECT_FALSE(ctl.state()->hasAllowedToken(tokenName)); + const RL::GetTokenData_output outAfterRemove = ctl.getTokenData(); + EXPECT_FALSE(ctl.state()->tokenDataOutputContains(outAfterRemove, tokenName, updatedPrice)); +} + +TEST(ContractRandomLottery, TokenRewardDivisor_SetAccessAndValidation) +{ + ContractTestingRL ctl; + ctl.beginEpochWithValidTime(); + + EXPECT_EQ(ctl.state()->getTokenRewardDivisor(), RL_TOKEN_REWARD_DIVISOR); + + const id randomUser = id::randomValue(); + increaseEnergy(randomUser, 1); + const RL::SetTokenRewardDivisor_output denied = ctl.setTokenRewardDivisor(randomUser, 5); + EXPECT_EQ(denied.returnCode, RL::EReturnCode::ACCESS_DENIED); + EXPECT_EQ(ctl.state()->getTokenRewardDivisor(), RL_TOKEN_REWARD_DIVISOR); + + increaseEnergy(RL_DEV_ADDRESS, 1); + const RL::SetTokenRewardDivisor_output invalid = ctl.setTokenRewardDivisor(RL_DEV_ADDRESS, 0); + EXPECT_EQ(invalid.returnCode, RL::EReturnCode::INVALID_VALUE); + EXPECT_EQ(ctl.state()->getTokenRewardDivisor(), RL_TOKEN_REWARD_DIVISOR); + + increaseEnergy(RL_DEV_ADDRESS, 1); + constexpr uint64 newDivisor = 7; + const RL::SetTokenRewardDivisor_output ok = ctl.setTokenRewardDivisor(RL_DEV_ADDRESS, newDivisor); + EXPECT_EQ(ok.returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.state()->getTokenRewardDivisor(), newDivisor); +} + +TEST(ContractRandomLottery, TransferToken_OwnerCanDistributeContractTokens) +{ + ContractTestingRL ctl; + ctl.beginEpochWithValidTime(); + + const sint64 contractTokensBefore = ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); + ASSERT_GT(contractTokensBefore, 0); + + const id recipient = id::randomValue(); + const uint64 transferAmount = std::min(5ul, contractTokensBefore); + + increaseEnergy(RL_DEV_ADDRESS, 1); + ctl.transferToken(RL_DEV_ADDRESS, recipient, transferAmount); + + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, recipient, ctl.rlSelf()), transferAmount); + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore - transferAmount); +} + +TEST(ContractRandomLottery, TransferToken_UserCanTransferOwnedTokensAndFailsWhenInsufficient) +{ + ContractTestingRL ctl; + ctl.beginEpochWithValidTime(); + + const id user = id::randomValue(); + const id target = id::randomValue(); + + increaseEnergy(RL_DEV_ADDRESS, 1); + constexpr uint64 seedAmount = 3; + ctl.transferToken(RL_DEV_ADDRESS, user, seedAmount); + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, user, ctl.rlSelf()), seedAmount); + + increaseEnergy(user, 1); + ctl.transferToken(user, target, seedAmount + 1); // insufficient balance + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, user, ctl.rlSelf()), seedAmount); + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, target, ctl.rlSelf()), 0); + + increaseEnergy(user, 1); + constexpr uint64 sendAmount = seedAmount - 1; + ctl.transferToken(user, target, sendAmount); + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, user, ctl.rlSelf()), seedAmount - sendAmount); + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, target, ctl.rlSelf()), sendAmount); +} + +TEST(ContractRandomLottery, TransferToken_NoOpForZeroAmountOrRecipient) +{ + ContractTestingRL ctl; + ctl.beginEpochWithValidTime(); + + const sint64 contractTokensBefore = ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); + ASSERT_GT(contractTokensBefore, 0); + + const id recipient = id::randomValue(); + + increaseEnergy(RL_DEV_ADDRESS, 1); + ctl.transferToken(RL_DEV_ADDRESS, recipient, 0); + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore); + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, recipient, ctl.rlSelf()), 0); + + increaseEnergy(RL_DEV_ADDRESS, 1); + ctl.transferToken(RL_DEV_ADDRESS, id::zero(), 1); + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore); +} + +TEST(ContractRandomLottery, BuyTicketWithToken_ClosedAndNotAllowed) +{ + ContractTestingRL ctl; + + const uint64 allowedToken = assetNameFromString("TOKCLO"); + const uint64 notAllowedToken = assetNameFromString("TOKBAD"); + const id buyer = id::randomValue(); + constexpr uint64 tokenPrice = 10; + constexpr uint64 allowedAmount = tokenPrice * 2; + constexpr uint64 disallowedAmount = tokenPrice * 3; + + increaseEnergy(buyer, 1); + increaseEnergy(RL_DEV_ADDRESS, 1); + const uint64 allowedAssetName = ctl.issueManagedTokenToUser(allowedToken, buyer, allowedAmount); + const uint64 disallowedAssetName = ctl.issueManagedTokenToUser(notAllowedToken, buyer, disallowedAmount); + const sint64 allowedBefore = ctl.tokenBalance(allowedAssetName, buyer, ctl.rlSelf()); + const sint64 disallowedBefore = ctl.tokenBalance(disallowedAssetName, buyer, ctl.rlSelf()); + + EXPECT_EQ(ctl.addAllowedToken(RL_DEV_ADDRESS, allowedToken, 10).returnCode, RL::EReturnCode::SUCCESS); + + const RL::BuyTicketWithToken_output closedOut = ctl.buyTicketWithToken(buyer, allowedToken, tokenPrice); + EXPECT_EQ(closedOut.returnCode, RL::EReturnCode::TICKET_SELLING_CLOSED); + EXPECT_EQ(ctl.tokenBalance(allowedAssetName, buyer, ctl.rlSelf()), allowedBefore); + + ctl.beginEpochWithValidTime(); + const RL::BuyTicketWithToken_output notAllowedOut = ctl.buyTicketWithToken(buyer, notAllowedToken, disallowedAmount); + EXPECT_EQ(notAllowedOut.returnCode, RL::EReturnCode::TOKEN_NOT_ALLOWED); + EXPECT_EQ(ctl.tokenBalance(disallowedAssetName, buyer, ctl.rlSelf()), disallowedBefore); +} + +TEST(ContractRandomLottery, BuyTicketWithToken_SuccessAndRefundsRemainder) +{ + ContractTestingRL ctl; + ctl.beginEpochWithValidTime(); + + const uint64 tokenName = assetNameFromString("TOKSUC"); + constexpr uint64 tokenPrice = 5; + increaseEnergy(RL_DEV_ADDRESS, 1); + EXPECT_EQ(ctl.addAllowedToken(RL_DEV_ADDRESS, tokenName, tokenPrice).returnCode, RL::EReturnCode::SUCCESS); + + const id buyer = id::randomValue(); + increaseEnergy(buyer, 1); + constexpr uint64 tokenAmount = tokenPrice * 3 + 2; // buy 3 tickets, refund 2 tokens + const uint64 assetName = ctl.issueManagedTokenToUser(tokenName, buyer, tokenAmount); + const uint64 playersBefore = ctl.state()->getPlayerCounter(); + const sint64 buyerTokensBefore = ctl.tokenBalance(assetName, buyer, ctl.rlSelf()); + const sint64 contractTokensBefore = ctl.tokenBalance(assetName, ctl.rlSelf(), ctl.rlSelf()); + + const RL::BuyTicketWithToken_output out = ctl.buyTicketWithToken(buyer, tokenName, tokenAmount); + EXPECT_EQ(out.returnCode, RL::EReturnCode::SUCCESS); + + constexpr uint64 expectedBought = 3; + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersBefore + expectedBought); + EXPECT_EQ(ctl.tokenBalance(assetName, buyer, ctl.rlSelf()), buyerTokensBefore - expectedBought * tokenPrice); + EXPECT_EQ(ctl.tokenBalance(assetName, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore + expectedBought * tokenPrice); +} From 549bce1dab6cf0c5ac9731958ba1dd8054ca2fd0 Mon Sep 17 00:00:00 2001 From: N-010 Date: Mon, 1 Dec 2025 01:31:48 +0300 Subject: [PATCH 30/39] Fixes Contract verify --- src/contracts/RandomLottery.h | 84 ++++++++++++++++++----------------- test/contract_rl.cpp | 21 +++++---- 2 files changed, 56 insertions(+), 49 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 1d14cb38a..dd98dbeb5 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -123,6 +123,8 @@ struct RL : public ContractBase UNKNOWN_ERROR = UINT8_MAX }; + static constexpr uint8 toReturnCode(EReturnCode code) { return static_cast(code); } + struct NextEpochData { uint64 newPrice; // Ticket price to apply after END_EPOCH; 0 means "no change queued" @@ -148,7 +150,7 @@ struct RL : public ContractBase struct BuyTicket_output { - EReturnCode returnCode; + uint8 returnCode; }; struct BuyTicketWithToken_input @@ -159,7 +161,7 @@ struct RL : public ContractBase struct BuyTicketWithToken_output { - EReturnCode returnCode; + uint8 returnCode; }; struct GetFees_input @@ -172,7 +174,7 @@ struct RL : public ContractBase uint8 distributionFeePercent; // Distribution/shareholders share in percent uint8 winnerFeePercent; // Winner share in percent uint8 burnPercent; // Burn share in percent - EReturnCode returnCode; + uint8 returnCode; }; struct GetPlayers_input @@ -183,7 +185,7 @@ struct RL : public ContractBase { Array players; // Current epoch ticket holders (duplicates allowed) uint64 playerCounter; // Actual count of filled entries - EReturnCode returnCode; + uint8 returnCode; }; /** @@ -222,7 +224,7 @@ struct RL : public ContractBase { Array winners; // Ring buffer snapshot uint64 winnersCounter; // Number of valid entries = (totalWinners % capacity) - EReturnCode returnCode; + uint8 returnCode; }; struct GetTicketPrice_input @@ -314,7 +316,7 @@ struct RL : public ContractBase struct SetPrice_output { - EReturnCode returnCode; + uint8 returnCode; }; struct SetSchedule_input @@ -324,7 +326,7 @@ struct RL : public ContractBase struct SetSchedule_output { - EReturnCode returnCode; + uint8 returnCode; }; struct AddAllowedToken_input @@ -335,7 +337,7 @@ struct RL : public ContractBase struct AddAllowedToken_output { - EReturnCode returnCode; + uint8 returnCode; }; struct AddAllowedToken_locals @@ -350,7 +352,7 @@ struct RL : public ContractBase struct RemoveAllowedToken_output { - EReturnCode returnCode; + uint8 returnCode; }; struct BEGIN_TICK_locals @@ -429,7 +431,7 @@ struct RL : public ContractBase struct SetTokenRewardDivisor_output { - EReturnCode returnCode; + uint8 returnCode; }; struct TransferToken_input @@ -440,7 +442,7 @@ struct RL : public ContractBase struct TransferToken_output { - EReturnCode returnCode; + uint8 returnCode; }; struct TransferToken_locals @@ -791,20 +793,20 @@ struct RL : public ContractBase // Only team/owner can queue a price change if (qpi.invocator() != state.teamAddress) { - output.returnCode = EReturnCode::ACCESS_DENIED; + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); return; } // Zero price is invalid if (input.newPrice == 0) { - output.returnCode = EReturnCode::TICKET_INVALID_PRICE; + output.returnCode = toReturnCode(EReturnCode::TICKET_INVALID_PRICE); return; } // Defer application until END_EPOCH state.nexEpochData.newPrice = input.newPrice; - output.returnCode = EReturnCode::SUCCESS; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); } PUBLIC_PROCEDURE(SetSchedule) @@ -813,18 +815,18 @@ struct RL : public ContractBase if (qpi.invocator() != state.teamAddress) { - output.returnCode = EReturnCode::ACCESS_DENIED; + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); return; } if (input.newSchedule == 0) { - output.returnCode = EReturnCode::INVALID_VALUE; + output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); return; } state.nexEpochData.schedule = input.newSchedule; - output.returnCode = EReturnCode::SUCCESS; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); } PUBLIC_PROCEDURE_WITH_LOCALS(AddAllowedToken) @@ -833,13 +835,13 @@ struct RL : public ContractBase if (qpi.invocator() != state.ownerAddress) { - output.returnCode = EReturnCode::ACCESS_DENIED; + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); return; } if (input.tokenPrice == 0) { - output.returnCode = EReturnCode::TICKET_INVALID_PRICE; + output.returnCode = toReturnCode(EReturnCode::TICKET_INVALID_PRICE); return; } @@ -847,20 +849,20 @@ struct RL : public ContractBase { locals.tokenData.pricePerTicket = input.tokenPrice; state.allowedTokens.set(input.tokenName, locals.tokenData); - output.returnCode = EReturnCode::SUCCESS; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); return; } if (state.allowedTokens.population() >= state.allowedTokens.capacity()) { - output.returnCode = EReturnCode::INVALID_VALUE; + output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); return; } locals.tokenData.pricePerTicket = input.tokenPrice; state.allowedTokens.set(input.tokenName, locals.tokenData); - output.returnCode = EReturnCode::SUCCESS; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); } PUBLIC_PROCEDURE(RemoveAllowedToken) @@ -869,12 +871,12 @@ struct RL : public ContractBase if (qpi.invocator() != state.ownerAddress) { - output.returnCode = EReturnCode::ACCESS_DENIED; + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); return; } state.allowedTokens.removeByKey(input.tokenName); - output.returnCode = EReturnCode::SUCCESS; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); } PUBLIC_PROCEDURE(SetTokenRewardDivisor) @@ -883,18 +885,18 @@ struct RL : public ContractBase if (qpi.invocator() != state.ownerAddress) { - output.returnCode = EReturnCode::ACCESS_DENIED; + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); return; } if (input.tokenRewardDivisor == 0) { - output.returnCode = EReturnCode::INVALID_VALUE; + output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); return; } state.tokenRewardDivisor = input.tokenRewardDivisor; - output.returnCode = EReturnCode::SUCCESS; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); } PUBLIC_PROCEDURE_WITH_LOCALS(TransferToken) @@ -903,13 +905,13 @@ struct RL : public ContractBase if (input.amount == 0) { - output.returnCode = EReturnCode::INVALID_VALUE; + output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); return; } if (isZero(input.newOwner)) { - output.returnCode = EReturnCode::INVALID_VALUE; + output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); return; } @@ -917,14 +919,14 @@ struct RL : public ContractBase if (getNumberOfToken(qpi, locals.ownerAndPossessor) < input.amount) { - output.returnCode = EReturnCode::TOKEN_TRANSFER_FAILED; + output.returnCode = toReturnCode(EReturnCode::TOKEN_TRANSFER_FAILED); return; } qpi.transferShareOwnershipAndPossession(RL_TOKEN_NAME, SELF, locals.ownerAndPossessor, locals.ownerAndPossessor, input.amount, input.newOwner); - output.returnCode = EReturnCode::SUCCESS; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); } /** @@ -947,7 +949,7 @@ struct RL : public ContractBase qpi.transfer(qpi.invocator(), locals.reward); } - output.returnCode = EReturnCode::TICKET_SELLING_CLOSED; + output.returnCode = toReturnCode(EReturnCode::TICKET_SELLING_CLOSED); return; } @@ -961,7 +963,7 @@ struct RL : public ContractBase qpi.transfer(qpi.invocator(), locals.reward); } - output.returnCode = EReturnCode::TICKET_INVALID_PRICE; + output.returnCode = toReturnCode(EReturnCode::TICKET_INVALID_PRICE); return; } @@ -975,7 +977,7 @@ struct RL : public ContractBase { qpi.transfer(qpi.invocator(), locals.reward); } - output.returnCode = EReturnCode::TICKET_ALL_SOLD_OUT; + output.returnCode = toReturnCode(EReturnCode::TICKET_ALL_SOLD_OUT); return; } @@ -1002,7 +1004,7 @@ struct RL : public ContractBase qpi.transfer(qpi.invocator(), locals.refundAmount); } - output.returnCode = EReturnCode::SUCCESS; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); } PUBLIC_PROCEDURE_WITH_LOCALS(BuyTicketWithToken) @@ -1012,13 +1014,13 @@ struct RL : public ContractBase if (!isSellingOpen(state)) { - output.returnCode = EReturnCode::TICKET_SELLING_CLOSED; + output.returnCode = toReturnCode(EReturnCode::TICKET_SELLING_CLOSED); return; } if (!getAllowedToken(state, input.tokenName, locals.tokenData)) { - output.returnCode = EReturnCode::TOKEN_NOT_ALLOWED; + output.returnCode = toReturnCode(EReturnCode::TOKEN_NOT_ALLOWED); return; } @@ -1026,21 +1028,21 @@ struct RL : public ContractBase if (locals.price == 0 || input.tokenAmount < locals.price) { - output.returnCode = EReturnCode::TICKET_INVALID_PRICE; + output.returnCode = toReturnCode(EReturnCode::TICKET_INVALID_PRICE); return; } locals.slotsLeft = (state.playerCounter < state.players.capacity()) ? (state.players.capacity() - state.playerCounter) : 0; if (locals.slotsLeft == 0) { - output.returnCode = EReturnCode::TICKET_ALL_SOLD_OUT; + output.returnCode = toReturnCode(EReturnCode::TICKET_ALL_SOLD_OUT); return; } // Move tokens from buyer to contract if (qpi.transferShareOwnershipAndPossession(input.tokenName, SELF, qpi.invocator(), qpi.invocator(), input.tokenAmount, SELF) < 0) { - output.returnCode = EReturnCode::TOKEN_TRANSFER_FAILED; + output.returnCode = toReturnCode(EReturnCode::TOKEN_TRANSFER_FAILED); return; } @@ -1064,7 +1066,7 @@ struct RL : public ContractBase qpi.transferShareOwnershipAndPossession(input.tokenName, SELF, SELF, SELF, locals.refundAmount, qpi.invocator()); } - output.returnCode = EReturnCode::SUCCESS; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); } private: diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index 2586fb6f2..449992a99 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -36,6 +36,11 @@ static uint32 makeDateStamp(uint16 year, uint8 month, uint8 day) return static_cast(shortYear << 9 | month << 5 | day); } +inline bool operator==(uint8 left, RL::EReturnCode right) { return left == RL::toReturnCode(right); } +inline bool operator==(RL::EReturnCode left, uint8 right) { return right == left; } +inline bool operator!=(uint8 left, RL::EReturnCode right) { return !(left == right); } +inline bool operator!=(RL::EReturnCode left, uint8 right) { return !(right == left); } + // Equality operator for comparing WinnerInfo objects // Compares all fields (address, revenue, epoch, tick, dayOfWeek) bool operator==(const RL::WinnerInfo& left, const RL::WinnerInfo& right) @@ -283,7 +288,7 @@ class ContractTestingRL : protected ContractTesting RL::BuyTicket_output output; if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_BUY_TICKET, input, output, user, reward)) { - output.returnCode = RL::EReturnCode::UNKNOWN_ERROR; + output.returnCode = RL::toReturnCode(RL::EReturnCode::UNKNOWN_ERROR); } return output; } @@ -294,7 +299,7 @@ class ContractTestingRL : protected ContractTesting RL::BuyTicketWithToken_output output; if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_BUY_TICKET_WITH_TOKEN, input, output, user, 0)) { - output.returnCode = RL::EReturnCode::UNKNOWN_ERROR; + output.returnCode = RL::toReturnCode(RL::EReturnCode::UNKNOWN_ERROR); } return output; } @@ -307,7 +312,7 @@ class ContractTestingRL : protected ContractTesting RL::SetPrice_output output; if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_SET_PRICE, input, output, invocator, 0)) { - output.returnCode = RL::EReturnCode::UNKNOWN_ERROR; + output.returnCode = RL::toReturnCode(RL::EReturnCode::UNKNOWN_ERROR); } return output; } @@ -320,7 +325,7 @@ class ContractTestingRL : protected ContractTesting RL::SetSchedule_output output; if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_SET_SCHEDULE, input, output, invocator, 0)) { - output.returnCode = RL::EReturnCode::UNKNOWN_ERROR; + output.returnCode = RL::toReturnCode(RL::EReturnCode::UNKNOWN_ERROR); } return output; } @@ -331,7 +336,7 @@ class ContractTestingRL : protected ContractTesting RL::AddAllowedToken_output output; if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_ADD_ALLOWED_TOKEN, input, output, invocator, 0)) { - output.returnCode = RL::EReturnCode::UNKNOWN_ERROR; + output.returnCode = RL::toReturnCode(RL::EReturnCode::UNKNOWN_ERROR); } return output; } @@ -342,7 +347,7 @@ class ContractTestingRL : protected ContractTesting RL::RemoveAllowedToken_output output{}; if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_REMOVE_ALLOWED_TOKEN, input, output, invocator, reward)) { - output.returnCode = RL::EReturnCode::UNKNOWN_ERROR; + output.returnCode = RL::toReturnCode(RL::EReturnCode::UNKNOWN_ERROR); } return output; } @@ -353,7 +358,7 @@ class ContractTestingRL : protected ContractTesting RL::SetTokenRewardDivisor_output output{}; if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_SET_TOKEN_REWARD_DIVISOR, input, output, invocator, 0)) { - output.returnCode = RL::EReturnCode::UNKNOWN_ERROR; + output.returnCode = RL::toReturnCode(RL::EReturnCode::UNKNOWN_ERROR); } return output; } @@ -364,7 +369,7 @@ class ContractTestingRL : protected ContractTesting RL::TransferToken_output output{}; if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_TRANSFER_TOKEN, input, output, invocator, 0)) { - output.returnCode = RL::EReturnCode::UNKNOWN_ERROR; + output.returnCode = RL::toReturnCode(RL::EReturnCode::UNKNOWN_ERROR); } return output; } From 52c6a29da32bcc53a1b98302f19b9a52294d0e47 Mon Sep 17 00:00:00 2001 From: N-010 Date: Mon, 1 Dec 2025 01:39:43 +0300 Subject: [PATCH 31/39] Rename RLT to LOTTO --- src/contracts/RandomLottery.h | 6 +++--- test/contract_rl.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index dd98dbeb5..c68ca83ed 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -53,7 +53,7 @@ constexpr uint8 RL_DEFAULT_SCHEDULE = 1 << WEDNESDAY | 1 << FRIDAY | 1 << SUNDAY constexpr uint32 RL_DEFAULT_INIT_TIME = 22 << 9 | 4 << 5 | 13; -constexpr uint64 RL_TOKEN_NAME = 0x00544c52ull; // "RLT" little-endian +constexpr uint64 RL_TOKEN_NAME = 0x4f54544f4cull; // "LOTTO" little-endian constexpr uint64 RL_DEFAULT_TOKEN_PRICE = 12; @@ -709,7 +709,7 @@ struct RL : public ContractBase } } - rewardParticipantsWithRLT(qpi, state, locals); + rewardParticipantsWithLOTTO(qpi, state, locals); clearStateOnEndDraw(state); @@ -1259,7 +1259,7 @@ struct RL : public ContractBase static bool getAllowedToken(const RL& state, uint64 tokenName, TokenData& outToken) { return state.allowedTokens.get(tokenName, outToken); } - static void rewardParticipantsWithRLT(const QpiContextProcedureCall& qpi, const RL& state, BEGIN_TICK_locals& locals) + static void rewardParticipantsWithLOTTO(const QpiContextProcedureCall& qpi, const RL& state, BEGIN_TICK_locals& locals) { if (state.playerCounter == 0) { diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index 449992a99..767ecdfc1 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -1045,7 +1045,7 @@ TEST(ContractRandomLottery, DrawAndPayout_BeginTick) } } -TEST(ContractRandomLottery, ParticipantsReceiveRLTPerTicketOnDraw) +TEST(ContractRandomLottery, ParticipantsReceiveLOTTOPerTicketOnDraw) { ContractTestingRL ctl; ctl.forceSchedule(RL_ANY_DAY_DRAW_SCHEDULE); From c03c777fd4d26149c28f17f62c6f01d286d408bd Mon Sep 17 00:00:00 2001 From: N-010 Date: Mon, 1 Dec 2025 01:57:42 +0300 Subject: [PATCH 32/39] Removes local variable --- src/contracts/RandomLottery.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index c68ca83ed..56bc7e4f4 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -358,6 +358,7 @@ struct RL : public ContractBase struct BEGIN_TICK_locals { id winnerAddress; + id firstPlayer; m256i mixedSpectrumValue; Entity entity; uint64 revenue; @@ -628,10 +629,9 @@ struct RL : public ContractBase locals.hasMultipleParticipants = false; if (state.playerCounter >= 2) { - const id firstPlayer = state.players.get(0); for (uint64 i = 1; i < state.playerCounter; ++i) { - if (state.players.get(i) != firstPlayer) + if (state.players.get(i) != state.players.get(0)) { locals.hasMultipleParticipants = true; break; From 27502a085699135cabeed582a105f7f1d007e3f4 Mon Sep 17 00:00:00 2001 From: N-010 Date: Mon, 1 Dec 2025 02:04:25 +0300 Subject: [PATCH 33/39] Removes local variable --- src/contracts/RandomLottery.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 56bc7e4f4..9e9e8a718 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -629,9 +629,9 @@ struct RL : public ContractBase locals.hasMultipleParticipants = false; if (state.playerCounter >= 2) { - for (uint64 i = 1; i < state.playerCounter; ++i) + for (locals.index = 1; locals.index < state.playerCounter; ++locals.index) { - if (state.players.get(i) != state.players.get(0)) + if (state.players.get(locals.index) != state.players.get(0)) { locals.hasMultipleParticipants = true; break; From e1226966de1c9cbe269366e567e198f989b9ac42 Mon Sep 17 00:00:00 2001 From: N-010 Date: Mon, 1 Dec 2025 17:33:01 +0300 Subject: [PATCH 34/39] TransferShareManagementRights Fixes qpi --- src/contracts/RandomLottery.h | 214 ++++++++++++++++++++++++---------- test/contract_rl.cpp | 117 +++++++++++++++++++ 2 files changed, 268 insertions(+), 63 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 9e9e8a718..aa2285893 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -451,6 +451,27 @@ struct RL : public ContractBase id ownerAndPossessor; }; + struct TransferShareManagementRights_input + { + uint32 newManagingContractIndex; + sint64 numberOfShares; + }; + + struct TransferShareManagementRights_output + { + sint64 transferredNumberOfShares; + uint8 returnCode; + }; + + struct TransferShareManagementRights_locals + { + QX::Fees_input feesInput; + QX::Fees_output feesOutput; + Asset asset; + sint64 releaseResult; + uint64 refundableAmount; + }; + public: /** * @brief Registers all externally callable functions and procedures with their numeric @@ -478,6 +499,7 @@ struct RL : public ContractBase REGISTER_USER_PROCEDURE(RemoveAllowedToken, 6); REGISTER_USER_PROCEDURE(SetTokenRewardDivisor, 7); REGISTER_USER_PROCEDURE(TransferToken, 8); + REGISTER_USER_PROCEDURE(TransferShareManagementRights, 9); } /** @@ -656,7 +678,9 @@ struct RL : public ContractBase if (state.playerCounter != 0) { locals.mixedSpectrumValue = qpi.getPrevSpectrumDigest(); - getRandomU64(state, qpi, locals.mixedSpectrumValue, locals.randomNum); + locals.mixedSpectrumValue.u64._0 ^= qpi.tick(); + locals.mixedSpectrumValue.u64._1 ^= state.playerCounter; + locals.randomNum = qpi.K12(locals.mixedSpectrumValue).u64._0; locals.randomNum = mod(locals.randomNum, state.playerCounter); // Index directly into players array @@ -667,10 +691,10 @@ struct RL : public ContractBase if (locals.winnerAddress != id::zero()) { // Split revenue by configured percentages - locals.winnerAmount = div(locals.revenue * state.winnerFeePercent, 100ULL); - locals.teamFee = div(locals.revenue * state.teamFeePercent, 100ULL); - locals.distributionFee = div(locals.revenue * state.distributionFeePercent, 100ULL); - locals.burnedAmount = div(locals.revenue * state.burnPercent, 100ULL); + locals.winnerAmount = div(smul(locals.revenue, static_cast(state.winnerFeePercent)), 100ULL); + locals.teamFee = div(smul(locals.revenue, static_cast(state.teamFeePercent)), 100ULL); + locals.distributionFee = div(smul(locals.revenue, static_cast(state.distributionFeePercent)), 100ULL); + locals.burnedAmount = div(smul(locals.revenue, static_cast(state.burnPercent)), 100ULL); // Team payout if (locals.teamFee > 0) @@ -709,7 +733,22 @@ struct RL : public ContractBase } } - rewardParticipantsWithLOTTO(qpi, state, locals); + // Reward Participants With LOTTO + { + if (state.playerCounter != 0 && state.tokenRewardDivisor > 0) + { + if (getAllowedToken(state, RL_TOKEN_NAME, locals.tokenData) && locals.tokenData.pricePerTicket > 0) + { + locals.rewardPerTicket = max(1, div(locals.tokenData.pricePerTicket, state.tokenRewardDivisor)); + + for (locals.index = 0; locals.index < state.playerCounter; ++locals.index) + { + qpi.transferShareOwnershipAndPossession(RL_TOKEN_NAME, SELF, SELF, SELF, locals.rewardPerTicket, + state.players.get(locals.index)); + } + } + } + } clearStateOnEndDraw(state); @@ -788,7 +827,10 @@ struct RL : public ContractBase PUBLIC_PROCEDURE(SetPrice) { - returnInvocationReward(qpi); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } // Only team/owner can queue a price change if (qpi.invocator() != state.teamAddress) @@ -811,7 +853,10 @@ struct RL : public ContractBase PUBLIC_PROCEDURE(SetSchedule) { - returnInvocationReward(qpi); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } if (qpi.invocator() != state.teamAddress) { @@ -831,7 +876,10 @@ struct RL : public ContractBase PUBLIC_PROCEDURE_WITH_LOCALS(AddAllowedToken) { - returnInvocationReward(qpi); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } if (qpi.invocator() != state.ownerAddress) { @@ -867,7 +915,10 @@ struct RL : public ContractBase PUBLIC_PROCEDURE(RemoveAllowedToken) { - returnInvocationReward(qpi); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } if (qpi.invocator() != state.ownerAddress) { @@ -881,7 +932,10 @@ struct RL : public ContractBase PUBLIC_PROCEDURE(SetTokenRewardDivisor) { - returnInvocationReward(qpi); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } if (qpi.invocator() != state.ownerAddress) { @@ -901,7 +955,10 @@ struct RL : public ContractBase PUBLIC_PROCEDURE_WITH_LOCALS(TransferToken) { - returnInvocationReward(qpi); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } if (input.amount == 0) { @@ -916,8 +973,8 @@ struct RL : public ContractBase } locals.ownerAndPossessor = qpi.invocator() == state.ownerAddress ? SELF : qpi.invocator(); - - if (getNumberOfToken(qpi, locals.ownerAndPossessor) < input.amount) + if (qpi.numberOfPossessedShares(RL_TOKEN_NAME, SELF, locals.ownerAndPossessor, locals.ownerAndPossessor, SELF_INDEX, SELF_INDEX) < + input.amount) { output.returnCode = toReturnCode(EReturnCode::TOKEN_TRANSFER_FAILED); return; @@ -929,6 +986,80 @@ struct RL : public ContractBase output.returnCode = toReturnCode(EReturnCode::SUCCESS); } + PUBLIC_PROCEDURE_WITH_LOCALS(TransferShareManagementRights) + { + if (qpi.invocator() != state.ownerAddress) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); + return; + } + + if (input.numberOfShares <= 0) + { + output.transferredNumberOfShares = 0; + output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + locals.asset.assetName = RL_TOKEN_NAME; + locals.asset.issuer = SELF; + + CALL_OTHER_CONTRACT_FUNCTION(QX, Fees, locals.feesInput, locals.feesOutput); + + if (qpi.invocationReward() < locals.feesOutput.transferFee) + { + output.transferredNumberOfShares = 0; + output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + if (qpi.numberOfPossessedShares(locals.asset.assetName, locals.asset.issuer, SELF, SELF, SELF_INDEX, SELF_INDEX) < input.numberOfShares) + { + output.transferredNumberOfShares = 0; + output.returnCode = toReturnCode(EReturnCode::TOKEN_TRANSFER_FAILED); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + locals.releaseResult = qpi.releaseShares(locals.asset, SELF, SELF, input.numberOfShares, input.newManagingContractIndex, + input.newManagingContractIndex, locals.feesOutput.transferFee); + + if (locals.releaseResult < 0) + { + output.transferredNumberOfShares = 0; + output.returnCode = toReturnCode(EReturnCode::TOKEN_TRANSFER_FAILED); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + output.transferredNumberOfShares = input.numberOfShares; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + + if (qpi.invocationReward() > locals.feesOutput.transferFee) + { + locals.refundableAmount = qpi.invocationReward() - locals.feesOutput.transferFee; + qpi.transfer(qpi.invocator(), locals.refundableAmount); + } + } + /** * @brief Attempts to buy tickets while SELLING state is active. * Logic: @@ -998,7 +1129,7 @@ struct RL : public ContractBase // Refund change and unfilled portion (if desired > slotsLeft) locals.unfilled = locals.desired - locals.toBuy; - locals.refundAmount = locals.remainder + (locals.unfilled * locals.price); + locals.refundAmount = locals.remainder + (smul(locals.unfilled, locals.price)); if (locals.refundAmount > 0) { qpi.transfer(qpi.invocator(), locals.refundAmount); @@ -1010,7 +1141,10 @@ struct RL : public ContractBase PUBLIC_PROCEDURE_WITH_LOCALS(BuyTicketWithToken) { // Refund any QU mistakenly attached to a token purchase - returnInvocationReward(qpi); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } if (!isSellingOpen(state)) { @@ -1060,7 +1194,7 @@ struct RL : public ContractBase } locals.unfilled = locals.desired - locals.toBuy; - locals.refundAmount = locals.remainder + (locals.unfilled * locals.price); + locals.refundAmount = locals.remainder + (smul(locals.unfilled, locals.price)); if (locals.refundAmount > 0) { qpi.transferShareOwnershipAndPossession(input.tokenName, SELF, SELF, SELF, locals.refundAmount, qpi.invocator()); @@ -1258,50 +1392,4 @@ struct RL : public ContractBase template static constexpr const T& max(const T& a, const T& b) { return (a > b) ? a : b; } static bool getAllowedToken(const RL& state, uint64 tokenName, TokenData& outToken) { return state.allowedTokens.get(tokenName, outToken); } - - static void rewardParticipantsWithLOTTO(const QpiContextProcedureCall& qpi, const RL& state, BEGIN_TICK_locals& locals) - { - if (state.playerCounter == 0) - { - return; - } - - if (state.tokenRewardDivisor == 0) - { - return; - } - - if (!getAllowedToken(state, RL_TOKEN_NAME, locals.tokenData) || locals.tokenData.pricePerTicket == 0) - { - return; - } - - locals.rewardPerTicket = max(1, div(locals.tokenData.pricePerTicket, state.tokenRewardDivisor)); - - for (locals.index = 0; locals.index < state.playerCounter; ++locals.index) - { - qpi.transferShareOwnershipAndPossession(RL_TOKEN_NAME, SELF, SELF, SELF, locals.rewardPerTicket, state.players.get(locals.index)); - } - } - - static void returnInvocationReward(const QpiContextProcedureCall& qpi) - { - // Refund any QU mistakenly attached to a token purchase - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - } - - static sint64 getNumberOfToken(const QpiContextFunctionCall& qpi, const id& ownerAndPossessor) - { - return qpi.numberOfPossessedShares(RL_TOKEN_NAME, SELF, ownerAndPossessor, ownerAndPossessor, SELF_INDEX, SELF_INDEX); - } - - static void getRandomU64(const RL& state, const QpiContextFunctionCall& qpi, m256i& spectrumData, uint64& outRandomValue) - { - spectrumData.u64._0 ^= qpi.tick(); - spectrumData.u64._1 ^= state.playerCounter; - outRandomValue = qpi.K12(spectrumData).u64._0; - } }; diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index 767ecdfc1..ce0b1a05f 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -11,6 +11,7 @@ constexpr uint16 PROCEDURE_INDEX_ADD_ALLOWED_TOKEN = 5; constexpr uint16 PROCEDURE_INDEX_REMOVE_ALLOWED_TOKEN = 6; constexpr uint16 PROCEDURE_INDEX_SET_TOKEN_REWARD_DIVISOR = 7; constexpr uint16 PROCEDURE_INDEX_TRANSFER_TOKEN = 8; +constexpr uint16 PROCEDURE_INDEX_TRANSFER_SHARE_MANAGEMENT_RIGHTS = 9; constexpr uint16 FUNCTION_INDEX_GET_FEES = 1; constexpr uint16 FUNCTION_INDEX_GET_PLAYERS = 2; constexpr uint16 FUNCTION_INDEX_GET_WINNERS = 3; @@ -22,6 +23,7 @@ constexpr uint16 FUNCTION_INDEX_GET_NEXT_EPOCH_DATA = 8; constexpr uint16 FUNCTION_INDEX_GET_DRAW_HOUR = 9; constexpr uint16 FUNCTION_INDEX_GET_SCHEDULE = 10; constexpr uint16 FUNCTION_INDEX_GET_TOKEN_DATA = 11; +constexpr uint16 QX_FUNCTION_INDEX_FEES = 1; constexpr uint8 STATE_SELLING = static_cast(RL::EState::SELLING); constexpr uint8 STATE_LOCKED = 0u; @@ -175,6 +177,9 @@ class ContractTestingRL : protected ContractTesting { initEmptySpectrum(); initEmptyUniverse(); + INIT_CONTRACT(QX); + system.epoch = contractDescriptions[QX_CONTRACT_INDEX].constructionEpoch; + callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); INIT_CONTRACT(RL); system.epoch = contractDescriptions[RL_CONTRACT_INDEX].constructionEpoch; callSystemProcedure(RL_CONTRACT_INDEX, INITIALIZE); @@ -374,6 +379,26 @@ class ContractTestingRL : protected ContractTesting return output; } + RL::TransferShareManagementRights_output transferShareManagementRights( + const id& invocator, uint32 newManagingContractIndex, sint64 numberOfShares, uint64 invocationReward) + { + RL::TransferShareManagementRights_input input{newManagingContractIndex, numberOfShares}; + RL::TransferShareManagementRights_output output{}; + if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_TRANSFER_SHARE_MANAGEMENT_RIGHTS, input, output, invocator, invocationReward)) + { + output.returnCode = RL::toReturnCode(RL::EReturnCode::UNKNOWN_ERROR); + } + return output; + } + + uint32 qxTransferFee() + { + QX::Fees_input input{}; + QX::Fees_output output{}; + callFunction(QX_CONTRACT_INDEX, QX_FUNCTION_INDEX_FEES, input, output); + return output.transferFee; + } + void BeginEpoch() { callSystemProcedure(RL_CONTRACT_INDEX, BEGIN_EPOCH); } void EndEpoch() { callSystemProcedure(RL_CONTRACT_INDEX, END_EPOCH); } @@ -427,6 +452,11 @@ class ContractTestingRL : protected ContractTesting return numberOfPossessedShares(assetName, issuer, owner, owner, RL_CONTRACT_INDEX, RL_CONTRACT_INDEX); } + sint64 tokenBalanceWithManagers(uint64 assetName, const id& owner, const id& issuer, uint16 managerIndex) const + { + return numberOfPossessedShares(assetName, issuer, owner, owner, managerIndex, managerIndex); + } + // Assert contract account balance equals the value returned by RL::GetBalance void expectContractBalanceEqualsGetBalance(ContractTestingRL& ctl, const id& contractAddress) { @@ -1605,6 +1635,93 @@ TEST(ContractRandomLottery, TransferToken_NoOpForZeroAmountOrRecipient) EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore); } +TEST(ContractRandomLottery, TransferShareManagementRights_SucceedsAndRefundsRemainder) +{ + ContractTestingRL ctl; + ctl.beginEpochWithValidTime(); + + const uint32 transferFee = ctl.qxTransferFee(); + const sint64 available = ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); + ASSERT_GT(available, 10); + + constexpr sint64 transferAmount = 7; + constexpr uint64 extraReward = 25; + const uint64 invocationReward = transferFee + extraReward; + + increaseEnergy(RL_DEV_ADDRESS, invocationReward); + const uint64 ownerBalanceBefore = getBalance(RL_DEV_ADDRESS); + const sint64 contractBalanceBefore = ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); + + const RL::TransferShareManagementRights_output out = + ctl.transferShareManagementRights(RL_DEV_ADDRESS, QX_CONTRACT_INDEX, transferAmount, invocationReward); + EXPECT_EQ(out.returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(out.transferredNumberOfShares, transferAmount); + + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractBalanceBefore - transferAmount); + EXPECT_EQ(ctl.tokenBalanceWithManagers(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf(), QX_CONTRACT_INDEX), transferAmount); + EXPECT_EQ(getBalance(RL_DEV_ADDRESS), ownerBalanceBefore - transferFee); +} + +TEST(ContractRandomLottery, TransferShareManagementRights_FailsWhenRewardTooLow) +{ + ContractTestingRL ctl; + ctl.beginEpochWithValidTime(); + + const uint32 transferFee = ctl.qxTransferFee(); + const uint64 invocationReward = transferFee > 0 ? transferFee - 1 : 0; + + increaseEnergy(RL_DEV_ADDRESS, invocationReward); + const uint64 ownerBalanceBefore = getBalance(RL_DEV_ADDRESS); + const sint64 contractBalanceBefore = ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); + + const RL::TransferShareManagementRights_output out = + ctl.transferShareManagementRights(RL_DEV_ADDRESS, QX_CONTRACT_INDEX, 1, invocationReward); + EXPECT_EQ(out.transferredNumberOfShares, 0); + EXPECT_EQ(out.returnCode, RL::EReturnCode::INVALID_VALUE); + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractBalanceBefore); + EXPECT_EQ(getBalance(RL_DEV_ADDRESS), ownerBalanceBefore); +} + +TEST(ContractRandomLottery, TransferShareManagementRights_FailsWhenInsufficientShares) +{ + ContractTestingRL ctl; + ctl.beginEpochWithValidTime(); + + const uint32 transferFee = ctl.qxTransferFee(); + const sint64 available = ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); + ASSERT_GT(available, 0); + + const sint64 transferAmount = available + 1; + increaseEnergy(RL_DEV_ADDRESS, transferFee); + const uint64 ownerBalanceBefore = getBalance(RL_DEV_ADDRESS); + + const RL::TransferShareManagementRights_output out = + ctl.transferShareManagementRights(RL_DEV_ADDRESS, QX_CONTRACT_INDEX, transferAmount, transferFee); + EXPECT_EQ(out.transferredNumberOfShares, 0); + EXPECT_EQ(out.returnCode, RL::EReturnCode::TOKEN_TRANSFER_FAILED); + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), available); + EXPECT_EQ(getBalance(RL_DEV_ADDRESS), ownerBalanceBefore); +} + +TEST(ContractRandomLottery, TransferShareManagementRights_OnlyOwnerCanInvoke) +{ + ContractTestingRL ctl; + ctl.beginEpochWithValidTime(); + + const uint32 transferFee = ctl.qxTransferFee(); + const id user = id::randomValue(); + increaseEnergy(user, transferFee + 10); + const uint64 userBalanceBefore = getBalance(user); + const sint64 contractBalanceBefore = ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); + + const RL::TransferShareManagementRights_output out = + ctl.transferShareManagementRights(user, QX_CONTRACT_INDEX, 1, transferFee + 10); + EXPECT_EQ(out.transferredNumberOfShares, 0); + EXPECT_EQ(out.returnCode, RL::EReturnCode::ACCESS_DENIED); + EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractBalanceBefore); + EXPECT_EQ(getBalance(user), userBalanceBefore); +} + TEST(ContractRandomLottery, BuyTicketWithToken_ClosedAndNotAllowed) { ContractTestingRL ctl; From 82f10b681a7b9c233959b310f42f0afe1e235d3b Mon Sep 17 00:00:00 2001 From: N-010 Date: Fri, 5 Dec 2025 14:09:39 +0300 Subject: [PATCH 35/39] Adds mixe for players --- src/contracts/RandomLottery.h | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index aa2285893..df4bc0362 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -363,6 +363,8 @@ struct RL : public ContractBase Entity entity; uint64 revenue; uint64 randomNum; + uint64 shuffleIndex; + uint64 swapIndex; uint64 winnerAmount; uint64 teamFee; uint64 distributionFee; @@ -667,6 +669,27 @@ struct RL : public ContractBase } else { + // Deterministically shuffle players before drawing so all nodes observe the same order + locals.mixedSpectrumValue = qpi.getPrevSpectrumDigest(); + locals.mixedSpectrumValue.u64._0 ^= qpi.tick(); + locals.mixedSpectrumValue.u64._1 ^= state.playerCounter; + locals.randomNum = qpi.K12(locals.mixedSpectrumValue).u64._0; + + for (locals.shuffleIndex = state.playerCounter - 1; locals.shuffleIndex > 0; --locals.shuffleIndex) + { + locals.randomNum ^= locals.randomNum << 13; + locals.randomNum ^= locals.randomNum >> 7; + locals.randomNum ^= locals.randomNum << 17; + locals.swapIndex = mod(locals.randomNum, locals.shuffleIndex + 1); + + if (locals.swapIndex != locals.shuffleIndex) + { + locals.firstPlayer = state.players.get(locals.shuffleIndex); + state.players.set(locals.shuffleIndex, state.players.get(locals.swapIndex)); + state.players.set(locals.swapIndex, locals.firstPlayer); + } + } + // Current contract net balance = incoming - outgoing for this contract qpi.getEntity(SELF, locals.entity); getSCRevenue(locals.entity, locals.revenue); @@ -677,9 +700,6 @@ struct RL : public ContractBase if (state.playerCounter != 0) { - locals.mixedSpectrumValue = qpi.getPrevSpectrumDigest(); - locals.mixedSpectrumValue.u64._0 ^= qpi.tick(); - locals.mixedSpectrumValue.u64._1 ^= state.playerCounter; locals.randomNum = qpi.K12(locals.mixedSpectrumValue).u64._0; locals.randomNum = mod(locals.randomNum, state.playerCounter); From c180e1c92992604f2b1ce7b5c5e0e8f21c9ca552 Mon Sep 17 00:00:00 2001 From: N-010 Date: Fri, 5 Dec 2025 14:36:23 +0300 Subject: [PATCH 36/39] Removes unused variables --- src/contracts/RandomLottery.h | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index df4bc0362..fbaa9c463 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -37,12 +37,6 @@ constexpr uint8 RL_SHAREHOLDER_FEE_PERCENT = 20; /// Burn percent of epoch revenue (0..100). constexpr uint8 RL_BURN_PERCENT = 2; -/// Sentinel for "no valid day". -constexpr uint8 RL_INVALID_DAY = 255; - -/// Sentinel for "no valid hour". -constexpr uint8 RL_INVALID_HOUR = 255; - /// Throttling period: process BEGIN_TICK logic once per this many ticks. constexpr uint8 RL_TICK_UPDATE_PERIOD = 100; From 699e985ad97ff287e78d3a3d3a804ee4d8e5c969 Mon Sep 17 00:00:00 2001 From: N-010 Date: Fri, 5 Dec 2025 21:33:54 +0300 Subject: [PATCH 37/39] Removes ownership transfer to QX Burns tokens after payment --- src/contracts/RandomLottery.h | 98 +---------------------------------- 1 file changed, 1 insertion(+), 97 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index fbaa9c463..61f68a232 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -447,27 +447,6 @@ struct RL : public ContractBase id ownerAndPossessor; }; - struct TransferShareManagementRights_input - { - uint32 newManagingContractIndex; - sint64 numberOfShares; - }; - - struct TransferShareManagementRights_output - { - sint64 transferredNumberOfShares; - uint8 returnCode; - }; - - struct TransferShareManagementRights_locals - { - QX::Fees_input feesInput; - QX::Fees_output feesOutput; - Asset asset; - sint64 releaseResult; - uint64 refundableAmount; - }; - public: /** * @brief Registers all externally callable functions and procedures with their numeric @@ -495,7 +474,6 @@ struct RL : public ContractBase REGISTER_USER_PROCEDURE(RemoveAllowedToken, 6); REGISTER_USER_PROCEDURE(SetTokenRewardDivisor, 7); REGISTER_USER_PROCEDURE(TransferToken, 8); - REGISTER_USER_PROCEDURE(TransferShareManagementRights, 9); } /** @@ -1000,80 +978,6 @@ struct RL : public ContractBase output.returnCode = toReturnCode(EReturnCode::SUCCESS); } - PUBLIC_PROCEDURE_WITH_LOCALS(TransferShareManagementRights) - { - if (qpi.invocator() != state.ownerAddress) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); - return; - } - - if (input.numberOfShares <= 0) - { - output.transferredNumberOfShares = 0; - output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - - locals.asset.assetName = RL_TOKEN_NAME; - locals.asset.issuer = SELF; - - CALL_OTHER_CONTRACT_FUNCTION(QX, Fees, locals.feesInput, locals.feesOutput); - - if (qpi.invocationReward() < locals.feesOutput.transferFee) - { - output.transferredNumberOfShares = 0; - output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - - if (qpi.numberOfPossessedShares(locals.asset.assetName, locals.asset.issuer, SELF, SELF, SELF_INDEX, SELF_INDEX) < input.numberOfShares) - { - output.transferredNumberOfShares = 0; - output.returnCode = toReturnCode(EReturnCode::TOKEN_TRANSFER_FAILED); - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - - locals.releaseResult = qpi.releaseShares(locals.asset, SELF, SELF, input.numberOfShares, input.newManagingContractIndex, - input.newManagingContractIndex, locals.feesOutput.transferFee); - - if (locals.releaseResult < 0) - { - output.transferredNumberOfShares = 0; - output.returnCode = toReturnCode(EReturnCode::TOKEN_TRANSFER_FAILED); - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - - output.transferredNumberOfShares = input.numberOfShares; - output.returnCode = toReturnCode(EReturnCode::SUCCESS); - - if (qpi.invocationReward() > locals.feesOutput.transferFee) - { - locals.refundableAmount = qpi.invocationReward() - locals.feesOutput.transferFee; - qpi.transfer(qpi.invocator(), locals.refundableAmount); - } - } - /** * @brief Attempts to buy tickets while SELLING state is active. * Logic: @@ -1188,7 +1092,7 @@ struct RL : public ContractBase } // Move tokens from buyer to contract - if (qpi.transferShareOwnershipAndPossession(input.tokenName, SELF, qpi.invocator(), qpi.invocator(), input.tokenAmount, SELF) < 0) + if (qpi.transferShareOwnershipAndPossession(input.tokenName, SELF, qpi.invocator(), qpi.invocator(), input.tokenAmount, NULL_ID) < 0) { output.returnCode = toReturnCode(EReturnCode::TOKEN_TRANSFER_FAILED); return; From a67599c0f85db699c6791a3a218e646b06c213ef Mon Sep 17 00:00:00 2001 From: N-010 Date: Fri, 5 Dec 2025 23:25:02 +0300 Subject: [PATCH 38/39] Removed the token list mechanics (only LOTTO purchases remain), removed Add/RemoveAllowedToken, added burning of paid LOTTOs when purchasing tickets, and updated tests to accommodate the new logic. --- src/contracts/RandomLottery.h | 183 +++--------------- test/contract_rl.cpp | 352 ++++++++-------------------------- 2 files changed, 107 insertions(+), 428 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 61f68a232..82adb2056 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -47,14 +47,12 @@ constexpr uint8 RL_DEFAULT_SCHEDULE = 1 << WEDNESDAY | 1 << FRIDAY | 1 << SUNDAY constexpr uint32 RL_DEFAULT_INIT_TIME = 22 << 9 | 4 << 5 | 13; -constexpr uint64 RL_TOKEN_NAME = 0x4f54544f4cull; // "LOTTO" little-endian +constexpr uint64 RL_LOTTO_TOKEN_NAME = 0x4f54544f4cull; // "LOTTO" little-endian constexpr uint64 RL_DEFAULT_TOKEN_PRICE = 12; constexpr uint64 RL_TOKEN_REWARD_DIVISOR = 3; -constexpr uint16 RL_MAX_ALLOWED_TOKENS = 1024; - /// Placeholder structure for future extensions. struct RL2 { @@ -111,7 +109,6 @@ struct RL : public ContractBase INVALID_VALUE, // Input value is not acceptable // Token-related errors - TOKEN_NOT_ALLOWED, // Attempted to pay with a token that is not supported TOKEN_TRANSFER_FAILED, // Token transfer to/from the contract failed UNKNOWN_ERROR = UINT8_MAX @@ -130,12 +127,6 @@ struct RL : public ContractBase uint64 pricePerTicket; }; - struct OutTokenData - { - uint64 tokenName; - TokenData tokenData; - }; - //---- User-facing I/O structures ------------------------------------------------------------- struct BuyTicket_input @@ -149,7 +140,6 @@ struct RL : public ContractBase struct BuyTicketWithToken_input { - uint64 tokenName; uint64 tokenAmount; }; @@ -266,7 +256,6 @@ struct RL : public ContractBase // Local variables for BuyTicket procedure struct BuyTicket_locals { - uint64 price; // Current ticket price uint64 reward; // Funds sent with call (invocationReward) uint64 capacity; // Max capacity of players array uint64 slotsLeft; // Remaining slots available to fill this epoch @@ -280,15 +269,14 @@ struct RL : public ContractBase struct BuyTicketWithToken_locals { - uint64 price; // Price per ticket in the chosen token uint64 slotsLeft; // Remaining slots available to fill this epoch uint64 desired; // How many tickets the caller wants to buy uint64 remainder; // Change to return (tokenAmount % price) uint64 toBuy; // Actual number of tickets to purchase (bounded by slotsLeft) uint64 unfilled; // Portion of desired tickets not purchased due to capacity limit uint64 refundAmount; // Total refund in tokens: remainder + unfilled * price + uint64 burnAmount; // Tokens to burn corresponding to purchased tickets uint64 i; // Loop counter - TokenData tokenData; }; struct ReturnAllTickets_input @@ -323,32 +311,6 @@ struct RL : public ContractBase uint8 returnCode; }; - struct AddAllowedToken_input - { - uint64 tokenName; - uint64 tokenPrice; - }; - - struct AddAllowedToken_output - { - uint8 returnCode; - }; - - struct AddAllowedToken_locals - { - TokenData tokenData; - }; - - struct RemoveAllowedToken_input - { - uint64 tokenName; - }; - - struct RemoveAllowedToken_output - { - uint8 returnCode; - }; - struct BEGIN_TICK_locals { id winnerAddress; @@ -377,7 +339,7 @@ struct RL : public ContractBase ReturnAllTickets_input returnAllTicketsInput; ReturnAllTickets_output returnAllTicketsOutput; FillWinnersInfo_output fillWinnersInfoOutput; - TokenData tokenData; + uint64 tokenPrice; }; struct GetNextEpochData_input @@ -412,13 +374,7 @@ struct RL : public ContractBase }; struct GetTokenData_output { - Array tokenData; - }; - struct GetTokenData_locals - { - OutTokenData outTokenData; - sint64 hashMapIndex; - uint64 arrayIndex; + TokenData tokenData; }; struct SetTokenRewardDivisor_input @@ -470,8 +426,6 @@ struct RL : public ContractBase REGISTER_USER_PROCEDURE(SetPrice, 2); REGISTER_USER_PROCEDURE(SetSchedule, 3); REGISTER_USER_PROCEDURE(BuyTicketWithToken, 4); - REGISTER_USER_PROCEDURE(AddAllowedToken, 5); - REGISTER_USER_PROCEDURE(RemoveAllowedToken, 6); REGISTER_USER_PROCEDURE(SetTokenRewardDivisor, 7); REGISTER_USER_PROCEDURE(TransferToken, 8); } @@ -528,14 +482,14 @@ struct RL : public ContractBase // Open selling for the new epoch enableBuyTicket(state, state.lastDrawDateStamp != RL_DEFAULT_INIT_TIME); - if (!qpi.isAssetIssued(SELF, RL_TOKEN_NAME)) + if (!qpi.isAssetIssued(SELF, RL_LOTTO_TOKEN_NAME)) { - qpi.issueAsset(RL_TOKEN_NAME, SELF, 0, MAX_AMOUNT, 0); + qpi.issueAsset(RL_LOTTO_TOKEN_NAME, SELF, 0, MAX_AMOUNT, 0); } - if (state.allowedTokens.getElementIndex(RL_TOKEN_NAME) == NULL_INDEX) + if (state.lottoTokenPrice == 0) { - state.allowedTokens.set(RL_TOKEN_NAME, {RL_DEFAULT_TOKEN_PRICE}); + state.lottoTokenPrice = RL_DEFAULT_TOKEN_PRICE; } if (state.tokenRewardDivisor == 0) @@ -729,13 +683,13 @@ struct RL : public ContractBase { if (state.playerCounter != 0 && state.tokenRewardDivisor > 0) { - if (getAllowedToken(state, RL_TOKEN_NAME, locals.tokenData) && locals.tokenData.pricePerTicket > 0) + if (state.lottoTokenPrice > 0) { - locals.rewardPerTicket = max(1, div(locals.tokenData.pricePerTicket, state.tokenRewardDivisor)); + locals.rewardPerTicket = max(1, div(state.lottoTokenPrice, state.tokenRewardDivisor)); for (locals.index = 0; locals.index < state.playerCounter; ++locals.index) { - qpi.transferShareOwnershipAndPossession(RL_TOKEN_NAME, SELF, SELF, SELF, locals.rewardPerTicket, + qpi.transferShareOwnershipAndPossession(RL_LOTTO_TOKEN_NAME, SELF, SELF, SELF, locals.rewardPerTicket, state.players.get(locals.index)); } } @@ -797,20 +751,7 @@ struct RL : public ContractBase PUBLIC_FUNCTION(GetNextEpochData) { output.nextEpochData = state.nexEpochData; } PUBLIC_FUNCTION(GetDrawHour) { output.drawHour = state.drawHour; } PUBLIC_FUNCTION(GetSchedule) { output.schedule = state.schedule; } - PUBLIC_FUNCTION_WITH_LOCALS(GetTokenData) - { - locals.arrayIndex = 0; - - locals.hashMapIndex = state.allowedTokens.nextElementIndex(NULL_INDEX); - while (locals.hashMapIndex != NULL_INDEX) - { - locals.outTokenData.tokenName = state.allowedTokens.key(locals.hashMapIndex); - locals.outTokenData.tokenData = state.allowedTokens.value(locals.hashMapIndex); - output.tokenData.set(locals.arrayIndex++, locals.outTokenData); - - locals.hashMapIndex = state.allowedTokens.nextElementIndex(locals.hashMapIndex); - } - } + PUBLIC_FUNCTION(GetTokenData) { output.tokenData.pricePerTicket = state.lottoTokenPrice; } PUBLIC_FUNCTION_WITH_LOCALS(GetBalance) { qpi.getEntity(SELF, locals.entity); @@ -866,62 +807,6 @@ struct RL : public ContractBase output.returnCode = toReturnCode(EReturnCode::SUCCESS); } - PUBLIC_PROCEDURE_WITH_LOCALS(AddAllowedToken) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - - if (qpi.invocator() != state.ownerAddress) - { - output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); - return; - } - - if (input.tokenPrice == 0) - { - output.returnCode = toReturnCode(EReturnCode::TICKET_INVALID_PRICE); - return; - } - - if (getAllowedToken(state, input.tokenName, locals.tokenData)) - { - locals.tokenData.pricePerTicket = input.tokenPrice; - state.allowedTokens.set(input.tokenName, locals.tokenData); - output.returnCode = toReturnCode(EReturnCode::SUCCESS); - return; - } - - if (state.allowedTokens.population() >= state.allowedTokens.capacity()) - { - output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); - return; - } - - locals.tokenData.pricePerTicket = input.tokenPrice; - state.allowedTokens.set(input.tokenName, locals.tokenData); - - output.returnCode = toReturnCode(EReturnCode::SUCCESS); - } - - PUBLIC_PROCEDURE(RemoveAllowedToken) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - - if (qpi.invocator() != state.ownerAddress) - { - output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); - return; - } - - state.allowedTokens.removeByKey(input.tokenName); - output.returnCode = toReturnCode(EReturnCode::SUCCESS); - } - PUBLIC_PROCEDURE(SetTokenRewardDivisor) { if (qpi.invocationReward() > 0) @@ -965,14 +850,14 @@ struct RL : public ContractBase } locals.ownerAndPossessor = qpi.invocator() == state.ownerAddress ? SELF : qpi.invocator(); - if (qpi.numberOfPossessedShares(RL_TOKEN_NAME, SELF, locals.ownerAndPossessor, locals.ownerAndPossessor, SELF_INDEX, SELF_INDEX) < + if (qpi.numberOfPossessedShares(RL_LOTTO_TOKEN_NAME, SELF, locals.ownerAndPossessor, locals.ownerAndPossessor, SELF_INDEX, SELF_INDEX) < input.amount) { output.returnCode = toReturnCode(EReturnCode::TOKEN_TRANSFER_FAILED); return; } - qpi.transferShareOwnershipAndPossession(RL_TOKEN_NAME, SELF, locals.ownerAndPossessor, locals.ownerAndPossessor, input.amount, + qpi.transferShareOwnershipAndPossession(RL_LOTTO_TOKEN_NAME, SELF, locals.ownerAndPossessor, locals.ownerAndPossessor, input.amount, input.newOwner); output.returnCode = toReturnCode(EReturnCode::SUCCESS); @@ -1002,10 +887,8 @@ struct RL : public ContractBase return; } - locals.price = state.ticketPrice; - // Not enough to buy even a single ticket: refund everything - if (locals.reward < locals.price) + if (locals.reward < state.ticketPrice) { if (locals.reward > 0) { @@ -1031,8 +914,8 @@ struct RL : public ContractBase } // Compute desired number of tickets and change - locals.desired = div(locals.reward, locals.price); // How many tickets the caller attempts to buy - locals.remainder = mod(locals.reward, locals.price); // Change to return + locals.desired = div(locals.reward, state.ticketPrice); // How many tickets the caller attempts to buy + locals.remainder = mod(locals.reward, state.ticketPrice); // Change to return locals.toBuy = min(locals.desired, locals.slotsLeft); // Do not exceed available slots // Add tickets (the same address may be inserted multiple times) @@ -1047,7 +930,7 @@ struct RL : public ContractBase // Refund change and unfilled portion (if desired > slotsLeft) locals.unfilled = locals.desired - locals.toBuy; - locals.refundAmount = locals.remainder + (smul(locals.unfilled, locals.price)); + locals.refundAmount = locals.remainder + (smul(locals.unfilled, state.ticketPrice)); if (locals.refundAmount > 0) { qpi.transfer(qpi.invocator(), locals.refundAmount); @@ -1070,15 +953,7 @@ struct RL : public ContractBase return; } - if (!getAllowedToken(state, input.tokenName, locals.tokenData)) - { - output.returnCode = toReturnCode(EReturnCode::TOKEN_NOT_ALLOWED); - return; - } - - locals.price = locals.tokenData.pricePerTicket; - - if (locals.price == 0 || input.tokenAmount < locals.price) + if (state.lottoTokenPrice == 0 || input.tokenAmount < state.lottoTokenPrice) { output.returnCode = toReturnCode(EReturnCode::TICKET_INVALID_PRICE); return; @@ -1092,14 +967,14 @@ struct RL : public ContractBase } // Move tokens from buyer to contract - if (qpi.transferShareOwnershipAndPossession(input.tokenName, SELF, qpi.invocator(), qpi.invocator(), input.tokenAmount, NULL_ID) < 0) + if (qpi.transferShareOwnershipAndPossession(RL_LOTTO_TOKEN_NAME, SELF, qpi.invocator(), qpi.invocator(), input.tokenAmount, SELF) < 0) { output.returnCode = toReturnCode(EReturnCode::TOKEN_TRANSFER_FAILED); return; } - locals.desired = div(input.tokenAmount, locals.price); - locals.remainder = mod(input.tokenAmount, locals.price); + locals.desired = div(input.tokenAmount, state.lottoTokenPrice); + locals.remainder = mod(input.tokenAmount, state.lottoTokenPrice); locals.toBuy = min(locals.desired, locals.slotsLeft); for (locals.i = 0; locals.i < locals.toBuy; ++locals.i) @@ -1112,10 +987,16 @@ struct RL : public ContractBase } locals.unfilled = locals.desired - locals.toBuy; - locals.refundAmount = locals.remainder + (smul(locals.unfilled, locals.price)); + locals.refundAmount = locals.remainder + (smul(locals.unfilled, state.lottoTokenPrice)); if (locals.refundAmount > 0) { - qpi.transferShareOwnershipAndPossession(input.tokenName, SELF, SELF, SELF, locals.refundAmount, qpi.invocator()); + qpi.transferShareOwnershipAndPossession(RL_LOTTO_TOKEN_NAME, SELF, SELF, SELF, locals.refundAmount, qpi.invocator()); + } + + locals.burnAmount = smul(locals.toBuy, state.lottoTokenPrice); + if (locals.burnAmount > 0) + { + qpi.transferShareOwnershipAndPossession(RL_LOTTO_TOKEN_NAME, SELF, SELF, SELF, locals.burnAmount, NULL_ID); } output.returnCode = toReturnCode(EReturnCode::SUCCESS); @@ -1252,7 +1133,7 @@ struct RL : public ContractBase */ EState currentState; - HashMap allowedTokens; + uint64 lottoTokenPrice; uint16 tokenRewardDivisor; @@ -1308,6 +1189,4 @@ struct RL : public ContractBase template static constexpr const T& min(const T& a, const T& b) { return (a < b) ? a : b; } template static constexpr const T& max(const T& a, const T& b) { return (a > b) ? a : b; } - - static bool getAllowedToken(const RL& state, uint64 tokenName, TokenData& outToken) { return state.allowedTokens.get(tokenName, outToken); } }; diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index 6e6b66558..089ba6fa5 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -7,8 +7,6 @@ constexpr uint16 PROCEDURE_INDEX_BUY_TICKET = 1; constexpr uint16 PROCEDURE_INDEX_SET_PRICE = 2; constexpr uint16 PROCEDURE_INDEX_SET_SCHEDULE = 3; constexpr uint16 PROCEDURE_INDEX_BUY_TICKET_WITH_TOKEN = 4; -constexpr uint16 PROCEDURE_INDEX_ADD_ALLOWED_TOKEN = 5; -constexpr uint16 PROCEDURE_INDEX_REMOVE_ALLOWED_TOKEN = 6; constexpr uint16 PROCEDURE_INDEX_SET_TOKEN_REWARD_DIVISOR = 7; constexpr uint16 PROCEDURE_INDEX_TRANSFER_TOKEN = 8; constexpr uint16 PROCEDURE_INDEX_TRANSFER_SHARE_MANAGEMENT_RIGHTS = 9; @@ -38,10 +36,22 @@ static uint32 makeDateStamp(uint16 year, uint8 month, uint8 day) return static_cast(shortYear << 9 | month << 5 | day); } -inline bool operator==(uint8 left, RL::EReturnCode right) { return left == RL::toReturnCode(right); } -inline bool operator==(RL::EReturnCode left, uint8 right) { return right == left; } -inline bool operator!=(uint8 left, RL::EReturnCode right) { return !(left == right); } -inline bool operator!=(RL::EReturnCode left, uint8 right) { return !(right == left); } +inline bool operator==(uint8 left, RL::EReturnCode right) +{ + return left == RL::toReturnCode(right); +} +inline bool operator==(RL::EReturnCode left, uint8 right) +{ + return right == left; +} +inline bool operator!=(uint8 left, RL::EReturnCode right) +{ + return !(left == right); +} +inline bool operator!=(RL::EReturnCode left, uint8 right) +{ + return !(right == left); +} // Equality operator for comparing WinnerInfo objects // Compares all fields (address, revenue, epoch, tick, dayOfWeek) @@ -126,37 +136,6 @@ class RLChecker : public RL void setScheduleMask(uint8 newMask) { schedule = newMask; } - void expectAllowedToken(uint64 tokenName, uint64 expectedPrice) const - { - RL::TokenData tokenData{}; - EXPECT_TRUE(getAllowedToken(*this, tokenName, tokenData)); - EXPECT_EQ(tokenData.pricePerTicket, expectedPrice); - } - - bool hasAllowedToken(uint64 tokenName) const - { - RL::TokenData tokenData{}; - return getAllowedToken(*this, tokenName, tokenData); - } - - bool tokenDataOutputContains(const RL::GetTokenData_output& out, uint64 tokenName, uint64 expectedPrice) const - { - for (uint64 i = 0; i < out.tokenData.capacity(); ++i) - { - const auto entry = out.tokenData.get(i); - if (entry.tokenName == 0) - { - continue; - } - - if (entry.tokenName == tokenName) - { - return entry.tokenData.pricePerTicket == expectedPrice; - } - } - return false; - } - uint64 getPlayerCounter() const { return playerCounter; } uint64 getTicketPrice() const { return ticketPrice; } @@ -168,6 +147,10 @@ class RLChecker : public RL uint32 getLastDrawDateStamp() const { return lastDrawDateStamp; } uint64 getTokenRewardDivisor() const { return tokenRewardDivisor; } + + uint64 getLottoTokenPrice() const { return lottoTokenPrice; } + + void setLottoTokenPrice(uint64 price) { lottoTokenPrice = price; } }; class ContractTestingRL : protected ContractTesting @@ -298,9 +281,9 @@ class ContractTestingRL : protected ContractTesting return output; } - RL::BuyTicketWithToken_output buyTicketWithToken(const id& user, uint64 tokenName, uint64 tokenAmount) + RL::BuyTicketWithToken_output buyTicketWithToken(const id& user, uint64 tokenAmount) { - RL::BuyTicketWithToken_input input{tokenName, tokenAmount}; + RL::BuyTicketWithToken_input input{tokenAmount}; RL::BuyTicketWithToken_output output; if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_BUY_TICKET_WITH_TOKEN, input, output, user, 0)) { @@ -335,28 +318,6 @@ class ContractTestingRL : protected ContractTesting return output; } - RL::AddAllowedToken_output addAllowedToken(const id& invocator, uint64 tokenName, uint64 tokenPrice) - { - RL::AddAllowedToken_input input{tokenName, tokenPrice}; - RL::AddAllowedToken_output output; - if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_ADD_ALLOWED_TOKEN, input, output, invocator, 0)) - { - output.returnCode = RL::toReturnCode(RL::EReturnCode::UNKNOWN_ERROR); - } - return output; - } - - RL::RemoveAllowedToken_output removeAllowedToken(const id& invocator, uint64 tokenName, uint64 reward = 0) - { - RL::RemoveAllowedToken_input input{tokenName}; - RL::RemoveAllowedToken_output output{}; - if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_REMOVE_ALLOWED_TOKEN, input, output, invocator, reward)) - { - output.returnCode = RL::toReturnCode(RL::EReturnCode::UNKNOWN_ERROR); - } - return output; - } - RL::SetTokenRewardDivisor_output setTokenRewardDivisor(const id& invocator, uint64 divisor) { RL::SetTokenRewardDivisor_input input{divisor}; @@ -379,18 +340,6 @@ class ContractTestingRL : protected ContractTesting return output; } - RL::TransferShareManagementRights_output transferShareManagementRights( - const id& invocator, uint32 newManagingContractIndex, sint64 numberOfShares, uint64 invocationReward) - { - RL::TransferShareManagementRights_input input{newManagingContractIndex, numberOfShares}; - RL::TransferShareManagementRights_output output{}; - if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_TRANSFER_SHARE_MANAGEMENT_RIGHTS, input, output, invocator, invocationReward)) - { - output.returnCode = RL::toReturnCode(RL::EReturnCode::UNKNOWN_ERROR); - } - return output; - } - uint32 qxTransferFee() { QX::Fees_input input{}; @@ -428,25 +377,6 @@ class ContractTestingRL : protected ContractTesting EXPECT_EQ(out.returnCode, RL::EReturnCode::SUCCESS); } - uint64 issueManagedTokenToUser(uint64 tokenName, const id& user, uint64 amount) - { - int issuanceIdx = -1; - int ownershipIdx = -1; - int possessionIdx = -1; - - char tokenNameBuf[8]; - encodeTokenName(tokenName, tokenNameBuf); - EXPECT_EQ(issueAsset(rlSelf(), tokenNameBuf, 0, CONTRACT_ASSET_UNIT_OF_MEASUREMENT, amount, RL_CONTRACT_INDEX, &issuanceIdx, &ownershipIdx, - &possessionIdx), - amount); - - int dstOwnershipIdx = -1; - int dstPossessionIdx = -1; - EXPECT_TRUE(transferShareOwnershipAndPossession(ownershipIdx, possessionIdx, user, amount, &dstOwnershipIdx, &dstPossessionIdx, true)); - - return tokenName; - } - sint64 tokenBalance(uint64 assetName, const id& owner, const id& issuer) const { return numberOfPossessedShares(assetName, issuer, owner, owner, RL_CONTRACT_INDEX, RL_CONTRACT_INDEX); @@ -1083,10 +1013,10 @@ TEST(ContractRandomLottery, ParticipantsReceiveLOTTOPerTicketOnDraw) const uint64 ticketPrice = ctl.state()->getTicketPrice(); constexpr uint64 tokenPrice = 15; constexpr uint64 rewardDivisor = 5; + ctl.state()->setLottoTokenPrice(tokenPrice); ctl.beginEpochWithValidTime(); increaseEnergy(RL_DEV_ADDRESS, 1); - EXPECT_EQ(ctl.addAllowedToken(RL_DEV_ADDRESS, RL_TOKEN_NAME, tokenPrice).returnCode, RL::EReturnCode::SUCCESS); EXPECT_EQ(ctl.setTokenRewardDivisor(RL_DEV_ADDRESS, rewardDivisor).returnCode, RL::EReturnCode::SUCCESS); EXPECT_EQ(ctl.state()->getTokenRewardDivisor(), rewardDivisor); @@ -1097,22 +1027,22 @@ TEST(ContractRandomLottery, ParticipantsReceiveLOTTOPerTicketOnDraw) increaseEnergy(playerA, ticketPrice * 3); increaseEnergy(playerB, ticketPrice * 2); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, playerA, ctl.rlSelf()), 0); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, playerB, ctl.rlSelf()), 0); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, playerA, ctl.rlSelf()), 0); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, playerB, ctl.rlSelf()), 0); EXPECT_EQ(ctl.buyTicket(playerA, ticketPrice).returnCode, RL::EReturnCode::SUCCESS); EXPECT_EQ(ctl.buyTicket(playerA, ticketPrice).returnCode, RL::EReturnCode::SUCCESS); EXPECT_EQ(ctl.buyTicket(playerB, ticketPrice).returnCode, RL::EReturnCode::SUCCESS); EXPECT_EQ(ctl.state()->getPlayerCounter(), 3u); - const sint64 contractTokensBefore = ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); + const sint64 contractTokensBefore = ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); ctl.advanceOneDayAndDraw(); EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, playerA, ctl.rlSelf()), rewardPerTicket * 2); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, playerB, ctl.rlSelf()), rewardPerTicket); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore - (rewardPerTicket * 3)); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, playerA, ctl.rlSelf()), rewardPerTicket * 2); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, playerB, ctl.rlSelf()), rewardPerTicket); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore - (rewardPerTicket * 3)); } TEST(ContractRandomLottery, GetBalance) @@ -1484,67 +1414,17 @@ TEST(ContractRandomLottery, GetDrawHour_DefaultAfterBeginEpoch) EXPECT_EQ(ctl.getDrawHour().drawHour, RL_DEFAULT_DRAW_HOUR); } -TEST(ContractRandomLottery, DefaultTokenAddedOnBeginEpochAndListed) +TEST(ContractRandomLottery, LottoTokenPrice_DefaultExposed) { ContractTestingRL ctl; - EXPECT_FALSE(ctl.state()->hasAllowedToken(RL_TOKEN_NAME)); + EXPECT_EQ(ctl.state()->getLottoTokenPrice(), 0u); ctl.beginEpochWithValidTime(); - EXPECT_TRUE(ctl.state()->hasAllowedToken(RL_TOKEN_NAME)); - ctl.state()->expectAllowedToken(RL_TOKEN_NAME, RL_DEFAULT_TOKEN_PRICE); + EXPECT_EQ(ctl.state()->getLottoTokenPrice(), RL_DEFAULT_TOKEN_PRICE); const RL::GetTokenData_output out = ctl.getTokenData(); - EXPECT_TRUE(ctl.state()->tokenDataOutputContains(out, RL_TOKEN_NAME, RL_DEFAULT_TOKEN_PRICE)); -} - -TEST(ContractRandomLottery, AllowedTokens_AddUpdateAndRemove) -{ - ContractTestingRL ctl; - - const uint64 tokenName = assetNameFromString("TOKADD"); - constexpr uint64 initialPrice = 111; - constexpr uint64 updatedPrice = 222; - const id randomUser = id::randomValue(); - - increaseEnergy(randomUser, 1); - const RL::AddAllowedToken_output denied = ctl.addAllowedToken(randomUser, tokenName, initialPrice); - EXPECT_EQ(denied.returnCode, RL::EReturnCode::ACCESS_DENIED); - EXPECT_FALSE(ctl.state()->hasAllowedToken(tokenName)); - - increaseEnergy(RL_DEV_ADDRESS, 1); - const RL::AddAllowedToken_output invalid = ctl.addAllowedToken(RL_DEV_ADDRESS, tokenName, 0); - EXPECT_EQ(invalid.returnCode, RL::EReturnCode::TICKET_INVALID_PRICE); - EXPECT_FALSE(ctl.state()->hasAllowedToken(tokenName)); - - increaseEnergy(RL_DEV_ADDRESS, 1); - const RL::AddAllowedToken_output ok = ctl.addAllowedToken(RL_DEV_ADDRESS, tokenName, initialPrice); - EXPECT_EQ(ok.returnCode, RL::EReturnCode::SUCCESS); - ctl.state()->expectAllowedToken(tokenName, initialPrice); - - increaseEnergy(RL_DEV_ADDRESS, 1); - const RL::AddAllowedToken_output updated = ctl.addAllowedToken(RL_DEV_ADDRESS, tokenName, updatedPrice); - EXPECT_EQ(updated.returnCode, RL::EReturnCode::SUCCESS); - ctl.state()->expectAllowedToken(tokenName, updatedPrice); - const RL::GetTokenData_output outAfterUpdate = ctl.getTokenData(); - EXPECT_TRUE(ctl.state()->tokenDataOutputContains(outAfterUpdate, tokenName, updatedPrice)); - - constexpr uint64 invocationReward = 12345; - increaseEnergy(randomUser, invocationReward); - const uint64 randomBalanceBefore = getBalance(randomUser); - const RL::RemoveAllowedToken_output removeDenied = ctl.removeAllowedToken(randomUser, tokenName, invocationReward); - EXPECT_EQ(removeDenied.returnCode, RL::EReturnCode::ACCESS_DENIED); - EXPECT_EQ(getBalance(randomUser), randomBalanceBefore); - ctl.state()->expectAllowedToken(tokenName, updatedPrice); - - increaseEnergy(RL_DEV_ADDRESS, invocationReward); - const uint64 devBalanceBefore = getBalance(RL_DEV_ADDRESS); - const RL::RemoveAllowedToken_output removeOk = ctl.removeAllowedToken(RL_DEV_ADDRESS, tokenName, invocationReward); - EXPECT_EQ(removeOk.returnCode, RL::EReturnCode::SUCCESS); - EXPECT_EQ(getBalance(RL_DEV_ADDRESS), devBalanceBefore); - EXPECT_FALSE(ctl.state()->hasAllowedToken(tokenName)); - const RL::GetTokenData_output outAfterRemove = ctl.getTokenData(); - EXPECT_FALSE(ctl.state()->tokenDataOutputContains(outAfterRemove, tokenName, updatedPrice)); + EXPECT_EQ(out.tokenData.pricePerTicket, RL_DEFAULT_TOKEN_PRICE); } TEST(ContractRandomLottery, TokenRewardDivisor_SetAccessAndValidation) @@ -1577,7 +1457,7 @@ TEST(ContractRandomLottery, TransferToken_OwnerCanDistributeContractTokens) ContractTestingRL ctl; ctl.beginEpochWithValidTime(); - const sint64 contractTokensBefore = ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); + const sint64 contractTokensBefore = ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); ASSERT_GT(contractTokensBefore, 0); const id recipient = id::randomValue(); @@ -1586,8 +1466,8 @@ TEST(ContractRandomLottery, TransferToken_OwnerCanDistributeContractTokens) increaseEnergy(RL_DEV_ADDRESS, 1); ctl.transferToken(RL_DEV_ADDRESS, recipient, transferAmount); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, recipient, ctl.rlSelf()), transferAmount); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore - transferAmount); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, recipient, ctl.rlSelf()), transferAmount); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore - transferAmount); } TEST(ContractRandomLottery, TransferToken_UserCanTransferOwnedTokensAndFailsWhenInsufficient) @@ -1601,18 +1481,18 @@ TEST(ContractRandomLottery, TransferToken_UserCanTransferOwnedTokensAndFailsWhen increaseEnergy(RL_DEV_ADDRESS, 1); constexpr uint64 seedAmount = 3; ctl.transferToken(RL_DEV_ADDRESS, user, seedAmount); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, user, ctl.rlSelf()), seedAmount); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, user, ctl.rlSelf()), seedAmount); increaseEnergy(user, 1); ctl.transferToken(user, target, seedAmount + 1); // insufficient balance - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, user, ctl.rlSelf()), seedAmount); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, target, ctl.rlSelf()), 0); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, user, ctl.rlSelf()), seedAmount); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, target, ctl.rlSelf()), 0); increaseEnergy(user, 1); constexpr uint64 sendAmount = seedAmount - 1; ctl.transferToken(user, target, sendAmount); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, user, ctl.rlSelf()), seedAmount - sendAmount); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, target, ctl.rlSelf()), sendAmount); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, user, ctl.rlSelf()), seedAmount - sendAmount); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, target, ctl.rlSelf()), sendAmount); } TEST(ContractRandomLottery, TransferToken_NoOpForZeroAmountOrRecipient) @@ -1620,161 +1500,81 @@ TEST(ContractRandomLottery, TransferToken_NoOpForZeroAmountOrRecipient) ContractTestingRL ctl; ctl.beginEpochWithValidTime(); - const sint64 contractTokensBefore = ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); + const sint64 contractTokensBefore = ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); ASSERT_GT(contractTokensBefore, 0); const id recipient = id::randomValue(); increaseEnergy(RL_DEV_ADDRESS, 1); ctl.transferToken(RL_DEV_ADDRESS, recipient, 0); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, recipient, ctl.rlSelf()), 0); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, recipient, ctl.rlSelf()), 0); increaseEnergy(RL_DEV_ADDRESS, 1); ctl.transferToken(RL_DEV_ADDRESS, id::zero(), 1); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore); } -TEST(ContractRandomLottery, TransferShareManagementRights_SucceedsAndRefundsRemainder) -{ - ContractTestingRL ctl; - ctl.beginEpochWithValidTime(); - - const uint32 transferFee = ctl.qxTransferFee(); - const sint64 available = ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); - ASSERT_GT(available, 10); - - constexpr sint64 transferAmount = 7; - constexpr uint64 extraReward = 25; - const uint64 invocationReward = transferFee + extraReward; - - increaseEnergy(RL_DEV_ADDRESS, invocationReward); - const uint64 ownerBalanceBefore = getBalance(RL_DEV_ADDRESS); - const sint64 contractBalanceBefore = ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); - - const RL::TransferShareManagementRights_output out = - ctl.transferShareManagementRights(RL_DEV_ADDRESS, QX_CONTRACT_INDEX, transferAmount, invocationReward); - EXPECT_EQ(out.returnCode, RL::EReturnCode::SUCCESS); - EXPECT_EQ(out.transferredNumberOfShares, transferAmount); - - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractBalanceBefore - transferAmount); - EXPECT_EQ(ctl.tokenBalanceWithManagers(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf(), QX_CONTRACT_INDEX), transferAmount); - EXPECT_EQ(getBalance(RL_DEV_ADDRESS), ownerBalanceBefore - transferFee); -} - -TEST(ContractRandomLottery, TransferShareManagementRights_FailsWhenRewardTooLow) +TEST(ContractRandomLottery, BuyTicketWithToken_ClosedAndNotAllowed) { ContractTestingRL ctl; - ctl.beginEpochWithValidTime(); - - const uint32 transferFee = ctl.qxTransferFee(); - const uint64 invocationReward = transferFee > 0 ? transferFee - 1 : 0; - increaseEnergy(RL_DEV_ADDRESS, invocationReward); - const uint64 ownerBalanceBefore = getBalance(RL_DEV_ADDRESS); - const sint64 contractBalanceBefore = ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); - - const RL::TransferShareManagementRights_output out = - ctl.transferShareManagementRights(RL_DEV_ADDRESS, QX_CONTRACT_INDEX, 1, invocationReward); - EXPECT_EQ(out.transferredNumberOfShares, 0); - EXPECT_EQ(out.returnCode, RL::EReturnCode::INVALID_VALUE); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractBalanceBefore); - EXPECT_EQ(getBalance(RL_DEV_ADDRESS), ownerBalanceBefore); -} + const id buyer = id::randomValue(); + const uint64 tokenPrice = RL_DEFAULT_TOKEN_PRICE; + const uint64 mintedAmount = tokenPrice * 2; + const uint64 requestedAmount = mintedAmount + tokenPrice; // More than balance => transfer fails + increaseEnergy(buyer, 1); + increaseEnergy(RL_DEV_ADDRESS, 1); -TEST(ContractRandomLottery, TransferShareManagementRights_FailsWhenInsufficientShares) -{ - ContractTestingRL ctl; ctl.beginEpochWithValidTime(); + const RL::TransferToken_output fundOut = ctl.transferToken(RL_DEV_ADDRESS, buyer, mintedAmount); + EXPECT_EQ(fundOut.returnCode, RL::EReturnCode::SUCCESS); - const uint32 transferFee = ctl.qxTransferFee(); - const sint64 available = ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); - ASSERT_GT(available, 0); + ctl.EndEpoch(); // Close selling after funding - const sint64 transferAmount = available + 1; - increaseEnergy(RL_DEV_ADDRESS, transferFee); - const uint64 ownerBalanceBefore = getBalance(RL_DEV_ADDRESS); + const sint64 lottoBefore = ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, buyer, ctl.rlSelf()); + const sint64 contractBefore = ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); - const RL::TransferShareManagementRights_output out = - ctl.transferShareManagementRights(RL_DEV_ADDRESS, QX_CONTRACT_INDEX, transferAmount, transferFee); - EXPECT_EQ(out.transferredNumberOfShares, 0); - EXPECT_EQ(out.returnCode, RL::EReturnCode::TOKEN_TRANSFER_FAILED); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), available); - EXPECT_EQ(getBalance(RL_DEV_ADDRESS), ownerBalanceBefore); -} + const RL::BuyTicketWithToken_output closedOut = ctl.buyTicketWithToken(buyer, tokenPrice); + EXPECT_EQ(closedOut.returnCode, RL::EReturnCode::TICKET_SELLING_CLOSED); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, buyer, ctl.rlSelf()), lottoBefore); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractBefore); -TEST(ContractRandomLottery, TransferShareManagementRights_OnlyOwnerCanInvoke) -{ - ContractTestingRL ctl; ctl.beginEpochWithValidTime(); - - const uint32 transferFee = ctl.qxTransferFee(); - const id user = id::randomValue(); - increaseEnergy(user, transferFee + 10); - const uint64 userBalanceBefore = getBalance(user); - const sint64 contractBalanceBefore = ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()); - - const RL::TransferShareManagementRights_output out = - ctl.transferShareManagementRights(user, QX_CONTRACT_INDEX, 1, transferFee + 10); - EXPECT_EQ(out.transferredNumberOfShares, 0); - EXPECT_EQ(out.returnCode, RL::EReturnCode::ACCESS_DENIED); - EXPECT_EQ(ctl.tokenBalance(RL_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractBalanceBefore); - EXPECT_EQ(getBalance(user), userBalanceBefore); + const RL::BuyTicketWithToken_output insufficientOut = ctl.buyTicketWithToken(buyer, requestedAmount); + EXPECT_EQ(insufficientOut.returnCode, RL::EReturnCode::TOKEN_TRANSFER_FAILED); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, buyer, ctl.rlSelf()), lottoBefore); + EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractBefore); } -TEST(ContractRandomLottery, BuyTicketWithToken_ClosedAndNotAllowed) +TEST(ContractRandomLottery, BuyTicketWithToken_SuccessAndRefundsRemainder) { ContractTestingRL ctl; + const uint64 tokenName = RL_LOTTO_TOKEN_NAME; + const uint64 tokenPrice = RL_DEFAULT_TOKEN_PRICE; - const uint64 allowedToken = assetNameFromString("TOKCLO"); - const uint64 notAllowedToken = assetNameFromString("TOKBAD"); const id buyer = id::randomValue(); - constexpr uint64 tokenPrice = 10; - constexpr uint64 allowedAmount = tokenPrice * 2; - constexpr uint64 disallowedAmount = tokenPrice * 3; - + const uint64 tokenAmount = tokenPrice * 3 + 2; // buy 3 tickets, refund 2 tokens increaseEnergy(buyer, 1); increaseEnergy(RL_DEV_ADDRESS, 1); - const uint64 allowedAssetName = ctl.issueManagedTokenToUser(allowedToken, buyer, allowedAmount); - const uint64 disallowedAssetName = ctl.issueManagedTokenToUser(notAllowedToken, buyer, disallowedAmount); - const sint64 allowedBefore = ctl.tokenBalance(allowedAssetName, buyer, ctl.rlSelf()); - const sint64 disallowedBefore = ctl.tokenBalance(disallowedAssetName, buyer, ctl.rlSelf()); - - EXPECT_EQ(ctl.addAllowedToken(RL_DEV_ADDRESS, allowedToken, 10).returnCode, RL::EReturnCode::SUCCESS); - - const RL::BuyTicketWithToken_output closedOut = ctl.buyTicketWithToken(buyer, allowedToken, tokenPrice); - EXPECT_EQ(closedOut.returnCode, RL::EReturnCode::TICKET_SELLING_CLOSED); - EXPECT_EQ(ctl.tokenBalance(allowedAssetName, buyer, ctl.rlSelf()), allowedBefore); ctl.beginEpochWithValidTime(); - const RL::BuyTicketWithToken_output notAllowedOut = ctl.buyTicketWithToken(buyer, notAllowedToken, disallowedAmount); - EXPECT_EQ(notAllowedOut.returnCode, RL::EReturnCode::TOKEN_NOT_ALLOWED); - EXPECT_EQ(ctl.tokenBalance(disallowedAssetName, buyer, ctl.rlSelf()), disallowedBefore); -} -TEST(ContractRandomLottery, BuyTicketWithToken_SuccessAndRefundsRemainder) -{ - ContractTestingRL ctl; - ctl.beginEpochWithValidTime(); + const RL::TransferToken_output fundOut = ctl.transferToken(RL_DEV_ADDRESS, buyer, tokenAmount); + EXPECT_EQ(fundOut.returnCode, RL::EReturnCode::SUCCESS); - const uint64 tokenName = assetNameFromString("TOKSUC"); - constexpr uint64 tokenPrice = 5; - increaseEnergy(RL_DEV_ADDRESS, 1); - EXPECT_EQ(ctl.addAllowedToken(RL_DEV_ADDRESS, tokenName, tokenPrice).returnCode, RL::EReturnCode::SUCCESS); + const uint64 assetName = tokenName; - const id buyer = id::randomValue(); - increaseEnergy(buyer, 1); - constexpr uint64 tokenAmount = tokenPrice * 3 + 2; // buy 3 tickets, refund 2 tokens - const uint64 assetName = ctl.issueManagedTokenToUser(tokenName, buyer, tokenAmount); + const uint64 resolvedTokenPrice = ctl.state()->getLottoTokenPrice(); const uint64 playersBefore = ctl.state()->getPlayerCounter(); const sint64 buyerTokensBefore = ctl.tokenBalance(assetName, buyer, ctl.rlSelf()); const sint64 contractTokensBefore = ctl.tokenBalance(assetName, ctl.rlSelf(), ctl.rlSelf()); - const RL::BuyTicketWithToken_output out = ctl.buyTicketWithToken(buyer, tokenName, tokenAmount); + const RL::BuyTicketWithToken_output out = ctl.buyTicketWithToken(buyer, tokenAmount); EXPECT_EQ(out.returnCode, RL::EReturnCode::SUCCESS); constexpr uint64 expectedBought = 3; EXPECT_EQ(ctl.state()->getPlayerCounter(), playersBefore + expectedBought); - EXPECT_EQ(ctl.tokenBalance(assetName, buyer, ctl.rlSelf()), buyerTokensBefore - expectedBought * tokenPrice); - EXPECT_EQ(ctl.tokenBalance(assetName, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore + expectedBought * tokenPrice); + EXPECT_EQ(ctl.tokenBalance(assetName, buyer, ctl.rlSelf()), buyerTokensBefore - expectedBought * resolvedTokenPrice); + EXPECT_EQ(ctl.tokenBalance(assetName, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore); } From 5c14e2355fb1e0b697f786fdc9dabc933b896e61 Mon Sep 17 00:00:00 2001 From: N-010 Date: Fri, 5 Dec 2025 23:57:41 +0300 Subject: [PATCH 39/39] Disables LOTTO issuance for ticket purchases --- src/contracts/RandomLottery.h | 32 ++++++++++++++++---------------- test/contract_rl.cpp | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 82adb2056..9d35721d3 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -679,22 +679,22 @@ struct RL : public ContractBase } } - // Reward Participants With LOTTO - { - if (state.playerCounter != 0 && state.tokenRewardDivisor > 0) - { - if (state.lottoTokenPrice > 0) - { - locals.rewardPerTicket = max(1, div(state.lottoTokenPrice, state.tokenRewardDivisor)); - - for (locals.index = 0; locals.index < state.playerCounter; ++locals.index) - { - qpi.transferShareOwnershipAndPossession(RL_LOTTO_TOKEN_NAME, SELF, SELF, SELF, locals.rewardPerTicket, - state.players.get(locals.index)); - } - } - } - } + // Reward Participants With LOTTO (disabled) + // { + // if (state.playerCounter != 0 && state.tokenRewardDivisor > 0) + // { + // if (state.lottoTokenPrice > 0) + // { + // locals.rewardPerTicket = max(1, div(state.lottoTokenPrice, state.tokenRewardDivisor)); + // + // for (locals.index = 0; locals.index < state.playerCounter; ++locals.index) + // { + // qpi.transferShareOwnershipAndPossession(RL_LOTTO_TOKEN_NAME, SELF, SELF, SELF, locals.rewardPerTicket, + // state.players.get(locals.index)); + // } + // } + // } + // } clearStateOnEndDraw(state); diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp index 089ba6fa5..a7f45816e 100644 --- a/test/contract_rl.cpp +++ b/test/contract_rl.cpp @@ -1005,7 +1005,7 @@ TEST(ContractRandomLottery, DrawAndPayout_BeginTick) } } -TEST(ContractRandomLottery, ParticipantsReceiveLOTTOPerTicketOnDraw) +/*TEST(ContractRandomLottery, ParticipantsReceiveLOTTOPerTicketOnDraw) { ContractTestingRL ctl; ctl.forceSchedule(RL_ANY_DAY_DRAW_SCHEDULE); @@ -1043,7 +1043,7 @@ TEST(ContractRandomLottery, ParticipantsReceiveLOTTOPerTicketOnDraw) EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, playerA, ctl.rlSelf()), rewardPerTicket * 2); EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, playerB, ctl.rlSelf()), rewardPerTicket); EXPECT_EQ(ctl.tokenBalance(RL_LOTTO_TOKEN_NAME, ctl.rlSelf(), ctl.rlSelf()), contractTokensBefore - (rewardPerTicket * 3)); -} +}*/ TEST(ContractRandomLottery, GetBalance) {