From 4e907cca9e3c03fa7e0a22c32f395a8724f7d928 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Thu, 26 Mar 2026 01:23:20 +0100 Subject: [PATCH 01/14] update qraffle --- src/contracts/QRaffle.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/contracts/QRaffle.h b/src/contracts/QRaffle.h index ed5dc7bd0..8c964b1c4 100644 --- a/src/contracts/QRaffle.h +++ b/src/contracts/QRaffle.h @@ -1,18 +1,18 @@ using namespace QPI; constexpr uint64 QRAFFLE_REGISTER_AMOUNT = 1000000000ull; -constexpr uint64 QRAFFLE_QXMR_REGISTER_AMOUNT = 100000000ull; +constexpr uint64 QRAFFLE_QXMR_REGISTER_AMOUNT = 250000000ull; constexpr uint64 QRAFFLE_MAX_QRE_AMOUNT = 1000000000ull; constexpr uint64 QRAFFLE_ASSET_NAME = 19505638103142993; constexpr uint64 QRAFFLE_QXMR_ASSET_NAME = 1380800593; // QXMR token asset name constexpr uint32 QRAFFLE_LOGOUT_FEE = 50000000; constexpr uint32 QRAFFLE_QXMR_LOGOUT_FEE = 5000000; // QXMR logout fee constexpr uint32 QRAFFLE_TRANSFER_SHARE_FEE = 100; -constexpr uint32 QRAFFLE_BURN_FEE = 10; // percent +constexpr uint32 QRAFFLE_BURN_FEE = 5; // percent constexpr uint32 QRAFFLE_REGISTER_FEE = 5; // percent constexpr uint32 QRAFFLE_FEE = 1; // percent constexpr uint32 QRAFFLE_CHARITY_FEE = 1; // percent -constexpr uint32 QRAFFLE_SHRAEHOLDER_FEE = 3; // percent +constexpr uint32 QRAFFLE_SHRAEHOLDER_FEE = 8; // percent constexpr uint32 QRAFFLE_MAX_EPOCH = 65536; constexpr uint32 QRAFFLE_MAX_PROPOSAL_EPOCH = 128; constexpr uint32 QRAFFLE_MAX_MEMBER = 65536; From b1fbfd032778e21462e18a3aec89571f4df702a5 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Tue, 7 Apr 2026 21:23:16 +0900 Subject: [PATCH 02/14] update: qxmr logout fee 12.5m --- src/contracts/QRaffle.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/QRaffle.h b/src/contracts/QRaffle.h index 8c964b1c4..9082dae47 100644 --- a/src/contracts/QRaffle.h +++ b/src/contracts/QRaffle.h @@ -6,7 +6,7 @@ constexpr uint64 QRAFFLE_MAX_QRE_AMOUNT = 1000000000ull; constexpr uint64 QRAFFLE_ASSET_NAME = 19505638103142993; constexpr uint64 QRAFFLE_QXMR_ASSET_NAME = 1380800593; // QXMR token asset name constexpr uint32 QRAFFLE_LOGOUT_FEE = 50000000; -constexpr uint32 QRAFFLE_QXMR_LOGOUT_FEE = 5000000; // QXMR logout fee +constexpr uint32 QRAFFLE_QXMR_LOGOUT_FEE = 12500000; // QXMR logout fee constexpr uint32 QRAFFLE_TRANSFER_SHARE_FEE = 100; constexpr uint32 QRAFFLE_BURN_FEE = 5; // percent constexpr uint32 QRAFFLE_REGISTER_FEE = 5; // percent From 2f320509774a6c1b39eb1b0a65ee11750a6c8c79 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Sat, 18 Apr 2026 00:40:54 +0900 Subject: [PATCH 03/14] fix: calculation of fee by div --- src/contracts/QRaffle.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/QRaffle.h b/src/contracts/QRaffle.h index 9082dae47..8550f23be 100644 --- a/src/contracts/QRaffle.h +++ b/src/contracts/QRaffle.h @@ -5,8 +5,8 @@ constexpr uint64 QRAFFLE_QXMR_REGISTER_AMOUNT = 250000000ull; constexpr uint64 QRAFFLE_MAX_QRE_AMOUNT = 1000000000ull; constexpr uint64 QRAFFLE_ASSET_NAME = 19505638103142993; constexpr uint64 QRAFFLE_QXMR_ASSET_NAME = 1380800593; // QXMR token asset name -constexpr uint32 QRAFFLE_LOGOUT_FEE = 50000000; -constexpr uint32 QRAFFLE_QXMR_LOGOUT_FEE = 12500000; // QXMR logout fee +constexpr uint32 QRAFFLE_LOGOUT_FEE = div(QRAFFLE_REGISTER_AMOUNT, 20); +constexpr uint32 QRAFFLE_QXMR_LOGOUT_FEE = div(QRAFFLE_QXMR_REGISTER_AMOUNT, 20); // QXMR logout fee constexpr uint32 QRAFFLE_TRANSFER_SHARE_FEE = 100; constexpr uint32 QRAFFLE_BURN_FEE = 5; // percent constexpr uint32 QRAFFLE_REGISTER_FEE = 5; // percent From 4fa58aba7afde617cedbd7371d340712dd87c51e Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Sat, 18 Apr 2026 22:25:55 +0900 Subject: [PATCH 04/14] fix: variable name and constant --- src/contracts/QRaffle.h | 12 +++++++----- test/contract_qraffle.cpp | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/contracts/QRaffle.h b/src/contracts/QRaffle.h index 8550f23be..6255e9a1c 100644 --- a/src/contracts/QRaffle.h +++ b/src/contracts/QRaffle.h @@ -12,13 +12,15 @@ constexpr uint32 QRAFFLE_BURN_FEE = 5; // percent constexpr uint32 QRAFFLE_REGISTER_FEE = 5; // percent constexpr uint32 QRAFFLE_FEE = 1; // percent constexpr uint32 QRAFFLE_CHARITY_FEE = 1; // percent -constexpr uint32 QRAFFLE_SHRAEHOLDER_FEE = 8; // percent +constexpr uint32 QRAFFLE_SHAREHOLDER_FEE = 8; // percent constexpr uint32 QRAFFLE_MAX_EPOCH = 65536; constexpr uint32 QRAFFLE_MAX_PROPOSAL_EPOCH = 128; constexpr uint32 QRAFFLE_MAX_MEMBER = 65536; constexpr uint32 QRAFFLE_DEFAULT_QRAFFLE_AMOUNT = 10000000ull; constexpr uint32 QRAFFLE_MIN_QRAFFLE_AMOUNT = 1000000ull; constexpr uint32 QRAFFLE_MAX_QRAFFLE_AMOUNT = 1000000000ull; +constexpr uint32 QRAFFLE_MAX_TOKEN_RAFFLES = 1048576; +constexpr uint32 QRAFFLE_MAX_SHAREHOLDERS = 1024; constexpr sint32 QRAFFLE_SUCCESS = 0; constexpr sint32 QRAFFLE_INSUFFICIENT_FUND = 1; @@ -207,9 +209,9 @@ struct QRAFFLE : public ContractBase Array tmpTokenRaffleMembers; Array QuRaffles; - Array tokenRaffle; + Array tokenRaffle; HashMap quRaffleEntryAmount; - HashSet shareholdersList; + HashSet shareholdersList; id initialRegister1, initialRegister2, initialRegister3, initialRegister4, initialRegister5; id charityAddress, feeAddress, QXMRIssuer; @@ -1271,7 +1273,7 @@ struct QRAFFLE : public ContractBase // Calculate fee distributions locals.burnAmount = div(state.get().qREAmount * state.get().numberOfQuRaffleMembers * QRAFFLE_BURN_FEE, 100); locals.charityRevenue = div(state.get().qREAmount * state.get().numberOfQuRaffleMembers * QRAFFLE_CHARITY_FEE, 100); - locals.shareholderRevenue = div(state.get().qREAmount * state.get().numberOfQuRaffleMembers * QRAFFLE_SHRAEHOLDER_FEE, 100); + locals.shareholderRevenue = div(state.get().qREAmount * state.get().numberOfQuRaffleMembers * QRAFFLE_SHAREHOLDER_FEE, 100); locals.registerRevenue = div(state.get().qREAmount * state.get().numberOfQuRaffleMembers * QRAFFLE_REGISTER_FEE, 100); locals.fee = div(state.get().qREAmount * state.get().numberOfQuRaffleMembers * QRAFFLE_FEE, 100); locals.winnerRevenue = state.get().qREAmount * state.get().numberOfQuRaffleMembers - locals.burnAmount - locals.charityRevenue - div(locals.shareholderRevenue, 676) * 676 - div(locals.registerRevenue, state.get().numberOfRegisters) * state.get().numberOfRegisters - locals.fee; @@ -1370,7 +1372,7 @@ struct QRAFFLE : public ContractBase // Calculate token raffle fee distributions locals.burnAmount = div(locals.acTokenRaffle.entryAmount * state.get().numberOfTokenRaffleMembers.get(locals.i) * QRAFFLE_BURN_FEE, 100); locals.charityRevenue = div(locals.acTokenRaffle.entryAmount * state.get().numberOfTokenRaffleMembers.get(locals.i) * QRAFFLE_CHARITY_FEE, 100); - locals.shareholderRevenue = div(locals.acTokenRaffle.entryAmount * state.get().numberOfTokenRaffleMembers.get(locals.i) * QRAFFLE_SHRAEHOLDER_FEE, 100); + locals.shareholderRevenue = div(locals.acTokenRaffle.entryAmount * state.get().numberOfTokenRaffleMembers.get(locals.i) * QRAFFLE_SHAREHOLDER_FEE, 100); locals.registerRevenue = div(locals.acTokenRaffle.entryAmount * state.get().numberOfTokenRaffleMembers.get(locals.i) * QRAFFLE_REGISTER_FEE, 100); locals.fee = div(locals.acTokenRaffle.entryAmount * state.get().numberOfTokenRaffleMembers.get(locals.i) * QRAFFLE_FEE, 100); locals.winnerRevenue = locals.acTokenRaffle.entryAmount * state.get().numberOfTokenRaffleMembers.get(locals.i) - locals.burnAmount - locals.charityRevenue - div(locals.shareholderRevenue, 676) * 676 - div(locals.registerRevenue, state.get().numberOfRegisters) * state.get().numberOfRegisters - locals.fee; diff --git a/test/contract_qraffle.cpp b/test/contract_qraffle.cpp index ed1644c0b..43750e182 100644 --- a/test/contract_qraffle.cpp +++ b/test/contract_qraffle.cpp @@ -974,7 +974,7 @@ TEST(ContractQraffle, GetFunctions) expectedTotalBurnAmount += (totalQuRaffleAmount * QRAFFLE_BURN_FEE) / 100; expectedTotalCharityAmount += (totalQuRaffleAmount * QRAFFLE_CHARITY_FEE) / 100; - expectedTotalShareholderAmount += ((totalQuRaffleAmount * QRAFFLE_SHRAEHOLDER_FEE) / 100) / 676 * 676; + expectedTotalShareholderAmount += ((totalQuRaffleAmount * QRAFFLE_SHAREHOLDER_FEE) / 100) / 676 * 676; expectedTotalRegisterAmount += ((totalQuRaffleAmount * QRAFFLE_REGISTER_FEE) / 100) / registerCount * registerCount; expectedTotalFeeAmount += (totalQuRaffleAmount * QRAFFLE_FEE) / 100; From 15c9df6a2c465153d997c8a4f7fb79deb172f216 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Tue, 21 Apr 2026 05:27:12 +0900 Subject: [PATCH 05/14] QRaffle: fix END_EPOCH winner randomness --- src/contracts/QRaffle.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/contracts/QRaffle.h b/src/contracts/QRaffle.h index 6255e9a1c..7357dd52f 100644 --- a/src/contracts/QRaffle.h +++ b/src/contracts/QRaffle.h @@ -1230,7 +1230,7 @@ struct QRAFFLE : public ContractBase ActiveTokenRaffleInfo acTokenRaffle; AssetPossessionIterator iter; Asset QraffleAsset; - id digest, winner, shareholder; + id digest, winner, shareholder, baseSeed, raffleSeed; sint64 idx; uint64 sumOfEntryAmountSubmitted, r, winnerRevenue, burnAmount, charityRevenue, shareholderRevenue, registerRevenue, fee, oneShareholderRev; uint32 i, j, winnerIndex; @@ -1249,7 +1249,9 @@ struct QRAFFLE : public ContractBase state.mut().epochRevenue -= locals.oneShareholderRev * 676; locals.digest = qpi.getPrevSpectrumDigest(); - locals.r = (qpi.numberOfTickTransactions() + 1) * locals.digest.u64._0 + (qpi.second()) * locals.digest.u64._1 + locals.digest.u64._2; + locals.baseSeed = qpi.K12(m256i(locals.digest.u64._0 ^ (uint64)qpi.epoch(), locals.digest.u64._1 ^ (uint64)qpi.tick(), locals.digest.u64._2 ^ (uint64)(qpi.numberOfTickTransactions() + 1), locals.digest.u64._3 ^ (uint64)qpi.second())); + locals.raffleSeed = qpi.K12(m256i(locals.baseSeed.u64._0, locals.baseSeed.u64._1, locals.baseSeed.u64._2, locals.baseSeed.u64._3 ^ 0ULL)); + locals.r = locals.raffleSeed.u64._0; locals.winnerIndex = (uint32)mod(locals.r, state.get().numberOfQuRaffleMembers * 1ull); locals.winner = state.get().quRaffleMembers.get(locals.winnerIndex); @@ -1363,6 +1365,8 @@ struct QRAFFLE : public ContractBase { if (state.get().numberOfTokenRaffleMembers.get(locals.i) > 0) { + locals.raffleSeed = qpi.K12(m256i(locals.baseSeed.u64._0, locals.baseSeed.u64._1, locals.baseSeed.u64._2, locals.baseSeed.u64._3 ^ ((uint64)locals.i + 1ULL))); + locals.r = locals.raffleSeed.u64._0; locals.winnerIndex = (uint32)mod(locals.r, state.get().numberOfTokenRaffleMembers.get(locals.i) * 1ull); state.get().tokenRaffleMembers.get(locals.i, state.mut().tmpTokenRaffleMembers); locals.winner = state.get().tmpTokenRaffleMembers.get(locals.winnerIndex); From 7f3562e56642ff84152ca927cba957e6df958a18 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Tue, 21 Apr 2026 08:21:43 +0900 Subject: [PATCH 06/14] QRaffle: cap proposals per user and validate submitProposal inputs --- src/contracts/QRaffle.h | 36 ++++++++++++++++++++++++++++++++++-- test/contract_qraffle.cpp | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/contracts/QRaffle.h b/src/contracts/QRaffle.h index 7357dd52f..bf2433629 100644 --- a/src/contracts/QRaffle.h +++ b/src/contracts/QRaffle.h @@ -21,6 +21,7 @@ constexpr uint32 QRAFFLE_MIN_QRAFFLE_AMOUNT = 1000000ull; constexpr uint32 QRAFFLE_MAX_QRAFFLE_AMOUNT = 1000000000ull; constexpr uint32 QRAFFLE_MAX_TOKEN_RAFFLES = 1048576; constexpr uint32 QRAFFLE_MAX_SHAREHOLDERS = 1024; +constexpr uint8 QRAFFLE_MAX_PROPOSALS_PER_PROPOSER = 3; // per epoch; mitigates global-slot DoS without a proposal fee constexpr sint32 QRAFFLE_SUCCESS = 0; constexpr sint32 QRAFFLE_INSUFFICIENT_FUND = 1; @@ -41,6 +42,7 @@ constexpr sint32 QRAFFLE_USER_NOT_FOUND = 15; constexpr sint32 QRAFFLE_INVALID_ENTRY_AMOUNT = 16; constexpr sint32 QRAFFLE_EMPTY_QU_RAFFLE = 17; constexpr sint32 QRAFFLE_EMPTY_TOKEN_RAFFLE = 18; +constexpr sint32 QRAFFLE_MAX_PROPOSAL_PER_USER_REACHED = 19; struct QRAFFLE2 { @@ -82,7 +84,8 @@ struct QRAFFLE : public ContractBase QRAFFLE_tokenRaffleDeposited = 29, QRAFFLE_shareManagementRightsTransferred = 30, QRAFFLE_emptyQuRaffle = 31, - QRAFFLE_emptyTokenRaffle = 32 + QRAFFLE_emptyTokenRaffle = 32, + QRAFFLE_maxProposalPerUserReached = 33 }; struct Logger @@ -195,7 +198,6 @@ struct QRAFFLE : public ContractBase struct StateData { HashMap registers; - Array proposals; HashMap , QRAFFLE_MAX_PROPOSAL_EPOCH> voteStatus; @@ -218,6 +220,7 @@ struct QRAFFLE : public ContractBase uint64 epochRevenue, epochQXMRRevenue, qREAmount, totalBurnAmount, totalCharityAmount, totalShareholderAmount, totalRegisterAmount, totalFeeAmount, totalWinnerAmount, largestWinnerAmount; uint32 numberOfRegisters, numberOfQuRaffleMembers, numberOfEntryAmountSubmitted, numberOfProposals, numberOfActiveTokenRaffle, numberOfEndedTokenRaffle; Array daoMemberCount; // Number of DAO members (registers) at each epoch + HashMap proposalsPerProposer; }; struct registerInSystem_input @@ -627,6 +630,7 @@ struct QRAFFLE : public ContractBase struct submitProposal_locals { ProposalInfo proposal; + uint8 countThisEpoch; Logger log; }; @@ -650,12 +654,39 @@ struct QRAFFLE : public ContractBase LOG_INFO(locals.log); return ; } + locals.countThisEpoch = 0; + if (state.get().proposalsPerProposer.contains(qpi.invocator())) + { + state.get().proposalsPerProposer.get(qpi.invocator(), locals.countThisEpoch); + } + if (locals.countThisEpoch >= QRAFFLE_MAX_PROPOSALS_PER_PROPOSER) + { + output.returnCode = QRAFFLE_MAX_PROPOSAL_PER_USER_REACHED; + locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_maxProposalPerUserReached, 0 }; + LOG_INFO(locals.log); + return ; + } + if (input.entryAmount < QRAFFLE_MIN_QRAFFLE_AMOUNT || input.entryAmount > QRAFFLE_MAX_QRAFFLE_AMOUNT) + { + output.returnCode = QRAFFLE_INVALID_ENTRY_AMOUNT; + locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidEntryAmount, 0 }; + LOG_INFO(locals.log); + return ; + } + if (!qpi.isAssetIssued(input.tokenIssuer, input.tokenName)) + { + output.returnCode = QRAFFLE_INVALID_TOKEN_TYPE; + locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidTokenType, 0 }; + LOG_INFO(locals.log); + return ; + } locals.proposal.token.issuer = input.tokenIssuer; locals.proposal.token.assetName = input.tokenName; locals.proposal.entryAmount = input.entryAmount; locals.proposal.proposer = qpi.invocator(); state.mut().proposals.set(state.get().numberOfProposals, locals.proposal); state.mut().numberOfProposals++; + state.mut().proposalsPerProposer.set(qpi.invocator(), locals.countThisEpoch + 1); output.returnCode = QRAFFLE_SUCCESS; locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_proposalSubmitted, 0 }; LOG_INFO(locals.log); @@ -1487,6 +1518,7 @@ struct QRAFFLE : public ContractBase state.mut().numberOfVotedInProposal.setAll(0); state.mut().tokenRaffleMembers.reset(); + state.mut().proposalsPerProposer.reset(); state.mut().quRaffleEntryAmount.reset(); state.mut().shareholdersList.reset(); state.mut().voteStatus.reset(); diff --git a/test/contract_qraffle.cpp b/test/contract_qraffle.cpp index 43750e182..54a123292 100644 --- a/test/contract_qraffle.cpp +++ b/test/contract_qraffle.cpp @@ -587,7 +587,42 @@ TEST(ContractQraffle, SubmitProposal) increaseEnergy(unregisteredUser, QRAFFLE_REGISTER_AMOUNT); auto result = qraffle.submitProposal(unregisteredUser, token1, 1000000); EXPECT_EQ(result.returnCode, QRAFFLE_UNREGISTERED); -} +} + +TEST(ContractQraffle, SubmitProposalPerUserLimitAndValidation) +{ + ContractTestingQraffle qraffle; + + auto users = getRandomUsers(100, 100); + for (const auto& user : users) + { + increaseEnergy(user, QRAFFLE_REGISTER_AMOUNT); + qraffle.registerInSystem(user, QRAFFLE_REGISTER_AMOUNT, 0); + } + + id issuer = getUser(2000); + increaseEnergy(issuer, 1000000000ULL); + uint64 assetName = assetNameFromString("LIMVAL"); + qraffle.issueAsset(issuer, assetName, 10000000, 0, 0); + + Asset token; + token.assetName = assetName; + token.issuer = issuer; + + increaseEnergy(users[0], 1000); + EXPECT_EQ(qraffle.submitProposal(users[0], token, 1000000ULL).returnCode, QRAFFLE_SUCCESS); + EXPECT_EQ(qraffle.submitProposal(users[0], token, 2000000ULL).returnCode, QRAFFLE_SUCCESS); + EXPECT_EQ(qraffle.submitProposal(users[0], token, 3000000ULL).returnCode, QRAFFLE_SUCCESS); + EXPECT_EQ(qraffle.submitProposal(users[0], token, 4000000ULL).returnCode, QRAFFLE_MAX_PROPOSAL_PER_USER_REACHED); + + EXPECT_EQ(qraffle.submitProposal(users[0], token, 500000ULL).returnCode, QRAFFLE_INVALID_ENTRY_AMOUNT); + EXPECT_EQ(qraffle.submitProposal(users[0], token, 2000000000ULL).returnCode, QRAFFLE_INVALID_ENTRY_AMOUNT); + + Asset fakeToken; + fakeToken.assetName = assetNameFromString("NOTISS"); + fakeToken.issuer = issuer; + EXPECT_EQ(qraffle.submitProposal(users[1], fakeToken, 1000000ULL).returnCode, QRAFFLE_INVALID_TOKEN_TYPE); +} TEST(ContractQraffle, VoteInProposal) { From 6279533c5cb8a73030c8d2e09df0edae905d4440 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Wed, 22 Apr 2026 19:19:46 +0900 Subject: [PATCH 07/14] QRaffle: security fixes, fee accounting, and correctness improvements --- src/contracts/QRaffle.h | 383 +++++++++++++++++++++++++++++--------- test/contract_qraffle.cpp | 45 ++++- 2 files changed, 329 insertions(+), 99 deletions(-) diff --git a/src/contracts/QRaffle.h b/src/contracts/QRaffle.h index bf2433629..4b393d9d2 100644 --- a/src/contracts/QRaffle.h +++ b/src/contracts/QRaffle.h @@ -21,7 +21,7 @@ constexpr uint32 QRAFFLE_MIN_QRAFFLE_AMOUNT = 1000000ull; constexpr uint32 QRAFFLE_MAX_QRAFFLE_AMOUNT = 1000000000ull; constexpr uint32 QRAFFLE_MAX_TOKEN_RAFFLES = 1048576; constexpr uint32 QRAFFLE_MAX_SHAREHOLDERS = 1024; -constexpr uint8 QRAFFLE_MAX_PROPOSALS_PER_PROPOSER = 3; // per epoch; mitigates global-slot DoS without a proposal fee +constexpr uint8 QRAFFLE_MAX_PROPOSALS_PER_PROPOSER = 3; // max proposals per user per epoch constexpr sint32 QRAFFLE_SUCCESS = 0; constexpr sint32 QRAFFLE_INSUFFICIENT_FUND = 1; @@ -680,6 +680,15 @@ struct QRAFFLE : public ContractBase LOG_INFO(locals.log); return ; } + // Reject internal tokens: QRAFFLE shares and QXMR are reserved for dividends/registration. + if ((input.tokenName == QRAFFLE_ASSET_NAME && input.tokenIssuer == NULL_ID) + || (input.tokenName == QRAFFLE_QXMR_ASSET_NAME && input.tokenIssuer == state.get().QXMRIssuer)) + { + output.returnCode = QRAFFLE_INVALID_TOKEN_TYPE; + locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidTokenType, 0 }; + LOG_INFO(locals.log); + return ; + } locals.proposal.token.issuer = input.tokenIssuer; locals.proposal.token.assetName = input.tokenName; locals.proposal.entryAmount = input.entryAmount; @@ -696,7 +705,9 @@ struct QRAFFLE : public ContractBase { ProposalInfo proposal; VotedId votedId; + VotedId emptyVote; uint32 i; + uint32 votedCount; Logger log; }; @@ -721,8 +732,20 @@ struct QRAFFLE : public ContractBase return ; } locals.proposal = state.get().proposals.get(input.indexOfProposal); - state.get().voteStatus.get(input.indexOfProposal, state.mut().tmpVoteStatus); - for (locals.i = 0; locals.i < state.get().numberOfVotedInProposal.get(input.indexOfProposal); locals.i++) + // Load vote buffer for this proposal. Clear it if no votes exist yet to avoid + // matching stale entries left over from a previous proposal in the same slot. + if (state.get().voteStatus.contains(input.indexOfProposal)) + { + state.get().voteStatus.get(input.indexOfProposal, state.mut().tmpVoteStatus); + } + else + { + locals.emptyVote.user = NULL_ID; + locals.emptyVote.status = 0; + state.mut().tmpVoteStatus.setAll(locals.emptyVote); + } + locals.votedCount = state.get().numberOfVotedInProposal.get(input.indexOfProposal); + for (locals.i = 0; locals.i < locals.votedCount; locals.i++) { if (state.get().tmpVoteStatus.get(locals.i).user == qpi.invocator()) { @@ -735,15 +758,22 @@ struct QRAFFLE : public ContractBase } else { + // Guard against unsigned underflow when flipping the vote. if (input.yes) { locals.proposal.nYes++; - locals.proposal.nNo--; + if (locals.proposal.nNo > 0) + { + locals.proposal.nNo--; + } } else { locals.proposal.nNo++; - locals.proposal.nYes--; + if (locals.proposal.nYes > 0) + { + locals.proposal.nYes--; + } } state.mut().proposals.set(input.indexOfProposal, locals.proposal); } @@ -758,6 +788,14 @@ struct QRAFFLE : public ContractBase return ; } } + // Reject new votes once the per-proposal buffer is full. + if (locals.votedCount >= QRAFFLE_MAX_MEMBER) + { + output.returnCode = QRAFFLE_MAX_MEMBER_REACHED; + locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_maxMemberReached, 0 }; + LOG_INFO(locals.log); + return ; + } if (input.yes) { locals.proposal.nYes++; @@ -770,9 +808,9 @@ struct QRAFFLE : public ContractBase locals.votedId.user = qpi.invocator(); locals.votedId.status = input.yes; - state.mut().tmpVoteStatus.set(state.get().numberOfVotedInProposal.get(input.indexOfProposal), locals.votedId); + state.mut().tmpVoteStatus.set(locals.votedCount, locals.votedId); state.mut().voteStatus.set(input.indexOfProposal, state.get().tmpVoteStatus); - state.mut().numberOfVotedInProposal.set(input.indexOfProposal, state.get().numberOfVotedInProposal.get(input.indexOfProposal) + 1); + state.mut().numberOfVotedInProposal.set(input.indexOfProposal, locals.votedCount + 1); output.returnCode = QRAFFLE_SUCCESS; locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_proposalVoted, 0 }; LOG_INFO(locals.log); @@ -832,7 +870,9 @@ struct QRAFFLE : public ContractBase struct depositInTokenRaffle_locals { + ActiveTokenRaffleInfo raffleInfo; uint32 i; + uint32 currentMembers; Logger log; }; @@ -849,6 +889,18 @@ struct QRAFFLE : public ContractBase LOG_INFO(locals.log); return ; } + // Only registered members may deposit. + if (state.get().registers.contains(qpi.invocator()) == 0) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.returnCode = QRAFFLE_UNREGISTERED; + locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_unregistered, 0 }; + LOG_INFO(locals.log); + return ; + } if (input.indexOfTokenRaffle >= state.get().numberOfActiveTokenRaffle) { if (qpi.invocationReward() > 0) @@ -860,7 +912,36 @@ struct QRAFFLE : public ContractBase LOG_INFO(locals.log); return ; } - if (qpi.transferShareOwnershipAndPossession(state.get().activeTokenRaffle.get(input.indexOfTokenRaffle).token.assetName, state.get().activeTokenRaffle.get(input.indexOfTokenRaffle).token.issuer, qpi.invocator(), qpi.invocator(), state.get().activeTokenRaffle.get(input.indexOfTokenRaffle).entryAmount, SELF) < 0) + locals.currentMembers = state.get().numberOfTokenRaffleMembers.get(input.indexOfTokenRaffle); + if (locals.currentMembers >= QRAFFLE_MAX_MEMBER) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.returnCode = QRAFFLE_MAX_MEMBER_REACHED; + locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_maxMemberReached, 0 }; + LOG_INFO(locals.log); + return ; + } + // Reject duplicate deposit from the same user. + state.get().tokenRaffleMembers.get(input.indexOfTokenRaffle, state.mut().tmpTokenRaffleMembers); + for (locals.i = 0; locals.i < locals.currentMembers; locals.i++) + { + if (state.get().tmpTokenRaffleMembers.get(locals.i) == qpi.invocator()) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.returnCode = QRAFFLE_ALREADY_REGISTERED; + locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_alreadyRegistered, 0 }; + LOG_INFO(locals.log); + return ; + } + } + locals.raffleInfo = state.get().activeTokenRaffle.get(input.indexOfTokenRaffle); + if (qpi.transferShareOwnershipAndPossession(locals.raffleInfo.token.assetName, locals.raffleInfo.token.issuer, qpi.invocator(), qpi.invocator(), locals.raffleInfo.entryAmount, SELF) < 0) { if (qpi.invocationReward() > 0) { @@ -871,10 +952,14 @@ struct QRAFFLE : public ContractBase LOG_INFO(locals.log); return ; } - qpi.transfer(qpi.invocator(), qpi.invocationReward() - QRAFFLE_TRANSFER_SHARE_FEE); - state.get().tokenRaffleMembers.get(input.indexOfTokenRaffle, state.mut().tmpTokenRaffleMembers); - state.mut().tmpTokenRaffleMembers.set(state.get().numberOfTokenRaffleMembers.get(input.indexOfTokenRaffle), qpi.invocator()); - state.mut().numberOfTokenRaffleMembers.set(input.indexOfTokenRaffle, state.get().numberOfTokenRaffleMembers.get(input.indexOfTokenRaffle) + 1); + // Keep QRAFFLE_TRANSFER_SHARE_FEE as service revenue; refund any excess. + if (qpi.invocationReward() > QRAFFLE_TRANSFER_SHARE_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - QRAFFLE_TRANSFER_SHARE_FEE); + } + state.mut().epochRevenue += QRAFFLE_TRANSFER_SHARE_FEE; + state.mut().tmpTokenRaffleMembers.set(locals.currentMembers, qpi.invocator()); + state.mut().numberOfTokenRaffleMembers.set(input.indexOfTokenRaffle, locals.currentMembers + 1); state.mut().tokenRaffleMembers.set(input.indexOfTokenRaffle, state.get().tmpTokenRaffleMembers); output.returnCode = QRAFFLE_SUCCESS; locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_tokenRaffleDeposited, 0 }; @@ -884,11 +969,15 @@ struct QRAFFLE : public ContractBase struct TransferShareManagementRights_locals { Asset asset; + sint64 offeredFee; + sint64 paidFee; Logger log; }; PUBLIC_PROCEDURE_WITH_LOCALS(TransferShareManagementRights) { + // Requires QRAFFLE_TRANSFER_SHARE_FEE minimum. The rest is offered to the destination + // contract as transfer fee; the unused portion is refunded after releaseShares. if (qpi.invocationReward() < QRAFFLE_TRANSFER_SHARE_FEE) { if (qpi.invocationReward() > 0) @@ -902,7 +991,7 @@ struct QRAFFLE : public ContractBase if (qpi.numberOfPossessedShares(input.tokenName, input.tokenIssuer,qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.numberOfShares) { - // not enough shares available + // Not enough shares — refund in full. output.transferredNumberOfShares = 0; locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_notEnoughShares, 0 }; LOG_INFO(locals.log); @@ -915,10 +1004,12 @@ struct QRAFFLE : public ContractBase { locals.asset.assetName = input.tokenName; locals.asset.issuer = input.tokenIssuer; - if (qpi.releaseShares(locals.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, - input.newManagingContractIndex, input.newManagingContractIndex, QRAFFLE_TRANSFER_SHARE_FEE) < 0) + locals.offeredFee = qpi.invocationReward() - QRAFFLE_TRANSFER_SHARE_FEE; + locals.paidFee = qpi.releaseShares(locals.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, + input.newManagingContractIndex, input.newManagingContractIndex, locals.offeredFee); + if (locals.paidFee < 0) { - // error + // Transfer rejected by the destination — refund everything. output.transferredNumberOfShares = 0; locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_transferFailed, 0 }; LOG_INFO(locals.log); @@ -929,11 +1020,12 @@ struct QRAFFLE : public ContractBase } else { - // success + // Success — keep service fee as revenue, refund unused transfer fee. output.transferredNumberOfShares = input.numberOfShares; - if (qpi.invocationReward() > QRAFFLE_TRANSFER_SHARE_FEE) + state.mut().epochRevenue += QRAFFLE_TRANSFER_SHARE_FEE; + if (locals.offeredFee > locals.paidFee) { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - QRAFFLE_TRANSFER_SHARE_FEE); + qpi.transfer(qpi.invocator(), locals.offeredFee - locals.paidFee); } locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_shareManagementRightsTransferred, 0 }; LOG_INFO(locals.log); @@ -1116,6 +1208,9 @@ struct QRAFFLE : public ContractBase PUBLIC_FUNCTION_WITH_LOCALS(getEpochRaffleIndexes) { + // Initialise outputs so callers see deterministic values when no raffles match. + output.StartIndex = 0; + output.EndIndex = 0; if (input.epoch > qpi.epoch()) { output.returnCode = QRAFFLE_INVALID_EPOCH; @@ -1125,6 +1220,7 @@ struct QRAFFLE : public ContractBase { output.StartIndex = 0; output.EndIndex = state.get().numberOfActiveTokenRaffle; + output.returnCode = QRAFFLE_SUCCESS; return ; } for (locals.i = 0; locals.i < (sint32)state.get().numberOfEndedTokenRaffle; locals.i++) @@ -1148,6 +1244,10 @@ struct QRAFFLE : public ContractBase PUBLIC_FUNCTION(getEndedQuRaffle) { + // Note: indices are masked by the underlying Array (capacity is power-of-two), so + // any uint32 epoch value is safely bounded inside the QuRaffles ring. Slots that + // were never written return a zero-initialised QuRaffleInfo, which the caller can + // detect via numberOfMembers == 0 / epochWinner == NULL_ID. output.epochWinner = state.get().QuRaffles.get(input.epoch).epochWinner; output.receivedAmount = state.get().QuRaffles.get(input.epoch).receivedAmount; output.entryAmount = state.get().QuRaffles.get(input.epoch).entryAmount; @@ -1194,10 +1294,13 @@ struct QRAFFLE : public ContractBase PUBLIC_FUNCTION_WITH_LOCALS(getQuRaffleEntryAverageAmount) { + locals.entryAmount = 0; + locals.totalEntryAmount = 0; locals.idx = state.get().quRaffleEntryAmount.nextElementIndex(NULL_INDEX); while (locals.idx != NULL_INDEX) { - locals.totalEntryAmount += state.get().quRaffleEntryAmount.value(locals.idx); + locals.entryAmount = state.get().quRaffleEntryAmount.value(locals.idx); + locals.totalEntryAmount += locals.entryAmount; locals.idx = state.get().quRaffleEntryAmount.nextElementIndex(locals.idx); } if (state.get().numberOfEntryAmountSubmitted > 0) @@ -1261,9 +1364,20 @@ struct QRAFFLE : public ContractBase ActiveTokenRaffleInfo acTokenRaffle; AssetPossessionIterator iter; Asset QraffleAsset; - id digest, winner, shareholder, baseSeed, raffleSeed; + id digest, computerDigest, winner, shareholder, baseSeed, raffleSeed; sint64 idx; + sint64 sharesHeld; + sint64 perShare; + sint64 transferResult; uint64 sumOfEntryAmountSubmitted, r, winnerRevenue, burnAmount, charityRevenue, shareholderRevenue, registerRevenue, fee, oneShareholderRev; + uint64 tokenPool; + uint64 shareholderPerShareUnit; + uint64 registerPerShareUnit; + uint64 actualShareholderTotal; + uint64 actualRegisterTotal; + uint64 qxmrPerShare; + uint64 qxmrDistributedTotal; + uint64 qxmrContractBalance; uint32 i, j, winnerIndex; Logger log; EmptyTokenRaffleLogger emptyTokenRafflelog; @@ -1275,68 +1389,89 @@ struct QRAFFLE : public ContractBase END_EPOCH_WITH_LOCALS() { - locals.oneShareholderRev = div(state.get().epochRevenue, 676); - qpi.distributeDividends(locals.oneShareholderRev); - state.mut().epochRevenue -= locals.oneShareholderRev * 676; + // Distribute logout-fee revenue to shareholders. + locals.oneShareholderRev = div(state.get().epochRevenue, NUMBER_OF_COMPUTORS); + if (locals.oneShareholderRev > 0) + { + qpi.distributeDividends(locals.oneShareholderRev); + state.mut().epochRevenue -= locals.oneShareholderRev * NUMBER_OF_COMPUTORS; + } + // RNG seed: XOR of prevSpectrumDigest and prevComputerDigest, hashed with K12. + // Tick-level inputs are excluded so a computor cannot grind the seed. locals.digest = qpi.getPrevSpectrumDigest(); - locals.baseSeed = qpi.K12(m256i(locals.digest.u64._0 ^ (uint64)qpi.epoch(), locals.digest.u64._1 ^ (uint64)qpi.tick(), locals.digest.u64._2 ^ (uint64)(qpi.numberOfTickTransactions() + 1), locals.digest.u64._3 ^ (uint64)qpi.second())); - locals.raffleSeed = qpi.K12(m256i(locals.baseSeed.u64._0, locals.baseSeed.u64._1, locals.baseSeed.u64._2, locals.baseSeed.u64._3 ^ 0ULL)); - locals.r = locals.raffleSeed.u64._0; - locals.winnerIndex = (uint32)mod(locals.r, state.get().numberOfQuRaffleMembers * 1ull); - locals.winner = state.get().quRaffleMembers.get(locals.winnerIndex); - - // Get QRAFFLE asset shareholders + locals.computerDigest = qpi.getPrevComputerDigest(); + locals.baseSeed = qpi.K12(m256i( + locals.digest.u64._0 ^ locals.computerDigest.u64._0, + locals.digest.u64._1 ^ locals.computerDigest.u64._1, + locals.digest.u64._2 ^ locals.computerDigest.u64._2, + locals.digest.u64._3 ^ locals.computerDigest.u64._3)); + + // Build the shareholder set for token-raffle and QXMR distributions. locals.QraffleAsset.assetName = QRAFFLE_ASSET_NAME; locals.QraffleAsset.issuer = NULL_ID; locals.iter.begin(locals.QraffleAsset); while (!locals.iter.reachedEnd()) { - locals.shareholder = locals.iter.possessor(); - if (state.get().shareholdersList.contains(locals.shareholder) == 0) + if (locals.iter.numberOfPossessedShares() > 0) { - state.mut().shareholdersList.add(locals.shareholder); + locals.shareholder = locals.iter.possessor(); + if (state.get().shareholdersList.contains(locals.shareholder) == 0) + { + state.mut().shareholdersList.add(locals.shareholder); + } } - locals.iter.next(); } if (state.get().numberOfQuRaffleMembers > 0) { - // Calculate fee distributions - locals.burnAmount = div(state.get().qREAmount * state.get().numberOfQuRaffleMembers * QRAFFLE_BURN_FEE, 100); - locals.charityRevenue = div(state.get().qREAmount * state.get().numberOfQuRaffleMembers * QRAFFLE_CHARITY_FEE, 100); - locals.shareholderRevenue = div(state.get().qREAmount * state.get().numberOfQuRaffleMembers * QRAFFLE_SHAREHOLDER_FEE, 100); - locals.registerRevenue = div(state.get().qREAmount * state.get().numberOfQuRaffleMembers * QRAFFLE_REGISTER_FEE, 100); - locals.fee = div(state.get().qREAmount * state.get().numberOfQuRaffleMembers * QRAFFLE_FEE, 100); - locals.winnerRevenue = state.get().qREAmount * state.get().numberOfQuRaffleMembers - locals.burnAmount - locals.charityRevenue - div(locals.shareholderRevenue, 676) * 676 - div(locals.registerRevenue, state.get().numberOfRegisters) * state.get().numberOfRegisters - locals.fee; + // Pick winner. + locals.raffleSeed = qpi.K12(m256i(locals.baseSeed.u64._0, locals.baseSeed.u64._1, locals.baseSeed.u64._2, locals.baseSeed.u64._3)); + locals.r = locals.raffleSeed.u64._0; + locals.winnerIndex = (uint32)mod(locals.r, state.get().numberOfQuRaffleMembers * 1ull); + locals.winner = state.get().quRaffleMembers.get(locals.winnerIndex); + + // Calculate fee distributions. + locals.tokenPool = state.get().qREAmount * state.get().numberOfQuRaffleMembers; + locals.burnAmount = div(locals.tokenPool * QRAFFLE_BURN_FEE, 100); + locals.charityRevenue = div(locals.tokenPool * QRAFFLE_CHARITY_FEE, 100); + locals.shareholderRevenue = div(locals.tokenPool * QRAFFLE_SHAREHOLDER_FEE, 100); + locals.registerRevenue = div(locals.tokenPool * QRAFFLE_REGISTER_FEE, 100); + locals.fee = div(locals.tokenPool * QRAFFLE_FEE, 100); + // Round down per-share amounts; winner gets the remainder. + locals.shareholderPerShareUnit = div(locals.shareholderRevenue, NUMBER_OF_COMPUTORS); + locals.actualShareholderTotal = locals.shareholderPerShareUnit * NUMBER_OF_COMPUTORS; + locals.registerPerShareUnit = div(locals.registerRevenue, state.get().numberOfRegisters); + locals.actualRegisterTotal = locals.registerPerShareUnit * state.get().numberOfRegisters; + locals.winnerRevenue = locals.tokenPool - locals.burnAmount - locals.charityRevenue - locals.actualShareholderTotal - locals.actualRegisterTotal - locals.fee; - // Log detailed revenue distribution information locals.revenueLog = RevenueLogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_revenueDistributed, locals.burnAmount, locals.charityRevenue, - div(locals.shareholderRevenue, 676) * 676, - div(locals.registerRevenue, state.get().numberOfRegisters) * state.get().numberOfRegisters, + locals.actualShareholderTotal, + locals.actualRegisterTotal, locals.fee, locals.winnerRevenue, 0 }; LOG_INFO(locals.revenueLog); - // Execute transfers and log each distribution qpi.transfer(locals.winner, locals.winnerRevenue); qpi.burn(locals.burnAmount); qpi.transfer(state.get().charityAddress, locals.charityRevenue); - qpi.distributeDividends(div(locals.shareholderRevenue, 676)); + if (locals.shareholderPerShareUnit > 0) + { + qpi.distributeDividends(locals.shareholderPerShareUnit); + } qpi.transfer(state.get().feeAddress, locals.fee); - // Update total amounts and log largest winner update state.mut().totalBurnAmount += locals.burnAmount; state.mut().totalCharityAmount += locals.charityRevenue; - state.mut().totalShareholderAmount += div(locals.shareholderRevenue, 676) * 676; - state.mut().totalRegisterAmount += div(locals.registerRevenue, state.get().numberOfRegisters) * state.get().numberOfRegisters; + state.mut().totalShareholderAmount += locals.actualShareholderTotal; + state.mut().totalRegisterAmount += locals.actualRegisterTotal; state.mut().totalFeeAmount += locals.fee; state.mut().totalWinnerAmount += locals.winnerRevenue; if (locals.winnerRevenue > state.get().largestWinnerAmount) @@ -1344,46 +1479,62 @@ struct QRAFFLE : public ContractBase state.mut().largestWinnerAmount = locals.winnerRevenue; } - locals.idx = state.get().registers.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) + if (locals.registerPerShareUnit > 0) { - qpi.transfer(state.get().registers.key(locals.idx), div(locals.registerRevenue, state.get().numberOfRegisters)); - locals.idx = state.get().registers.nextElementIndex(locals.idx); + locals.idx = state.get().registers.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) + { + qpi.transfer(state.get().registers.key(locals.idx), locals.registerPerShareUnit); + locals.idx = state.get().registers.nextElementIndex(locals.idx); + } } - // Store QuRaffle results and log completion with detailed information locals.qraffle.epochWinner = locals.winner; locals.qraffle.receivedAmount = locals.winnerRevenue; locals.qraffle.entryAmount = state.get().qREAmount; locals.qraffle.numberOfMembers = state.get().numberOfQuRaffleMembers; locals.qraffle.winnerIndex = locals.winnerIndex; state.mut().QuRaffles.set(qpi.epoch(), locals.qraffle); - state.mut().daoMemberCount.set(qpi.epoch(), state.get().numberOfRegisters); // Store DAO member count for this epoch - // Log QuRaffle completion with detailed information locals.endEpochLog = EndEpochLogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_revenueDistributed, qpi.epoch(), state.get().numberOfQuRaffleMembers, - state.get().qREAmount * state.get().numberOfQuRaffleMembers, + locals.tokenPool, locals.winnerRevenue, locals.winnerIndex, 0 }; LOG_INFO(locals.endEpochLog); - if (state.get().epochQXMRRevenue >= 676) + // Distribute QXMR to shareholders. Only deduct what was actually transferred. + locals.qxmrPerShare = div(state.get().epochQXMRRevenue, NUMBER_OF_COMPUTORS); + locals.qxmrDistributedTotal = 0; + if (locals.qxmrPerShare > 0) { - locals.idx = state.get().shareholdersList.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) + locals.qxmrContractBalance = (uint64)qpi.numberOfPossessedShares(QRAFFLE_QXMR_ASSET_NAME, state.get().QXMRIssuer, SELF, SELF, SELF_INDEX, SELF_INDEX); + locals.iter.begin(locals.QraffleAsset); + while (!locals.iter.reachedEnd()) { - locals.shareholder = state.get().shareholdersList.key(locals.idx); - qpi.transferShareOwnershipAndPossession(QRAFFLE_QXMR_ASSET_NAME, state.get().QXMRIssuer, SELF, SELF, div(state.get().epochQXMRRevenue, 676) * qpi.numberOfShares(locals.QraffleAsset, AssetOwnershipSelect::byOwner(locals.shareholder), AssetPossessionSelect::byPossessor(locals.shareholder)), locals.shareholder); - locals.idx = state.get().shareholdersList.nextElementIndex(locals.idx); + locals.sharesHeld = locals.iter.numberOfPossessedShares(); + if (locals.sharesHeld > 0) + { + locals.shareholder = locals.iter.possessor(); + locals.perShare = (sint64)(locals.qxmrPerShare * (uint64)locals.sharesHeld); + if ((uint64)locals.perShare <= locals.qxmrContractBalance - locals.qxmrDistributedTotal) + { + locals.transferResult = qpi.transferShareOwnershipAndPossession(QRAFFLE_QXMR_ASSET_NAME, state.get().QXMRIssuer, SELF, SELF, locals.perShare, locals.shareholder); + if (locals.transferResult >= 0) + { + locals.qxmrDistributedTotal += (uint64)locals.perShare; + } + } + } + locals.iter.next(); } - state.mut().epochQXMRRevenue -= div(state.get().epochQXMRRevenue, 676) * 676; } + state.mut().epochQXMRRevenue -= locals.qxmrDistributedTotal; } else { @@ -1391,7 +1542,7 @@ struct QRAFFLE : public ContractBase LOG_INFO(locals.log); } - // Process each active token raffle and log + // Process each active token raffle. for (locals.i = 0 ; locals.i < state.get().numberOfActiveTokenRaffle; locals.i++) { if (state.get().numberOfTokenRaffleMembers.get(locals.i) > 0) @@ -1404,33 +1555,71 @@ struct QRAFFLE : public ContractBase locals.acTokenRaffle = state.get().activeTokenRaffle.get(locals.i); - // Calculate token raffle fee distributions - locals.burnAmount = div(locals.acTokenRaffle.entryAmount * state.get().numberOfTokenRaffleMembers.get(locals.i) * QRAFFLE_BURN_FEE, 100); - locals.charityRevenue = div(locals.acTokenRaffle.entryAmount * state.get().numberOfTokenRaffleMembers.get(locals.i) * QRAFFLE_CHARITY_FEE, 100); - locals.shareholderRevenue = div(locals.acTokenRaffle.entryAmount * state.get().numberOfTokenRaffleMembers.get(locals.i) * QRAFFLE_SHAREHOLDER_FEE, 100); - locals.registerRevenue = div(locals.acTokenRaffle.entryAmount * state.get().numberOfTokenRaffleMembers.get(locals.i) * QRAFFLE_REGISTER_FEE, 100); - locals.fee = div(locals.acTokenRaffle.entryAmount * state.get().numberOfTokenRaffleMembers.get(locals.i) * QRAFFLE_FEE, 100); - locals.winnerRevenue = locals.acTokenRaffle.entryAmount * state.get().numberOfTokenRaffleMembers.get(locals.i) - locals.burnAmount - locals.charityRevenue - div(locals.shareholderRevenue, 676) * 676 - div(locals.registerRevenue, state.get().numberOfRegisters) * state.get().numberOfRegisters - locals.fee; - - // Execute token transfers and log each - qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.winnerRevenue, locals.winner); - qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.burnAmount, NULL_ID); - qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.charityRevenue, state.get().charityAddress); - qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.fee, state.get().feeAddress); - - locals.idx = state.get().shareholdersList.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) + locals.tokenPool = locals.acTokenRaffle.entryAmount * state.get().numberOfTokenRaffleMembers.get(locals.i); + locals.burnAmount = div(locals.tokenPool * QRAFFLE_BURN_FEE, 100); + locals.charityRevenue = div(locals.tokenPool * QRAFFLE_CHARITY_FEE, 100); + locals.shareholderRevenue = div(locals.tokenPool * QRAFFLE_SHAREHOLDER_FEE, 100); + locals.registerRevenue = div(locals.tokenPool * QRAFFLE_REGISTER_FEE, 100); + locals.fee = div(locals.tokenPool * QRAFFLE_FEE, 100); + // Round down per-share amounts; winner gets the remainder. + locals.shareholderPerShareUnit = div(locals.shareholderRevenue, NUMBER_OF_COMPUTORS); + locals.actualShareholderTotal = locals.shareholderPerShareUnit * NUMBER_OF_COMPUTORS; + locals.registerPerShareUnit = div(locals.registerRevenue, state.get().numberOfRegisters); + locals.actualRegisterTotal = locals.registerPerShareUnit * state.get().numberOfRegisters; + locals.winnerRevenue = locals.tokenPool - locals.burnAmount - locals.charityRevenue - locals.actualShareholderTotal - locals.actualRegisterTotal - locals.fee; + + // Send winner share. Skip the whole raffle if this fails to prevent partial distribution. + locals.transferResult = qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.winnerRevenue, locals.winner); + if (locals.transferResult < 0) { - locals.shareholder = state.get().shareholdersList.key(locals.idx); - qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, div(locals.shareholderRevenue, 676) * qpi.numberOfShares(locals.QraffleAsset, AssetOwnershipSelect::byOwner(locals.shareholder), AssetPossessionSelect::byPossessor(locals.shareholder)), locals.shareholder); - locals.idx = state.get().shareholdersList.nextElementIndex(locals.idx); + // Winner transfer failed — skip raffle, leave funds for next epoch. + locals.emptyTokenRafflelog = EmptyTokenRaffleLogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_transferFailed, locals.i, 0 }; + LOG_INFO(locals.emptyTokenRafflelog); + state.mut().numberOfTokenRaffleMembers.set(locals.i, 0); + continue; } - locals.idx = state.get().registers.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) + // Burn shares (NULL_ID); fall back to charity if burn is rejected. + if (locals.burnAmount > 0) { - qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, div(locals.registerRevenue, state.get().numberOfRegisters), state.get().registers.key(locals.idx)); - locals.idx = state.get().registers.nextElementIndex(locals.idx); + locals.transferResult = qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.burnAmount, NULL_ID); + if (locals.transferResult < 0) + { + qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.burnAmount, state.get().charityAddress); + } + } + if (locals.charityRevenue > 0) + { + qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.charityRevenue, state.get().charityAddress); + } + if (locals.fee > 0) + { + qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.fee, state.get().feeAddress); + } + + // Pay shareholders proportional to possessed shares. + if (locals.shareholderPerShareUnit > 0) + { + locals.iter.begin(locals.QraffleAsset); + while (!locals.iter.reachedEnd()) + { + locals.sharesHeld = locals.iter.numberOfPossessedShares(); + if (locals.sharesHeld > 0) + { + qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, (sint64)(locals.shareholderPerShareUnit * (uint64)locals.sharesHeld), locals.iter.possessor()); + } + locals.iter.next(); + } + } + + if (locals.registerPerShareUnit > 0) + { + locals.idx = state.get().registers.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) + { + qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, (sint64)locals.registerPerShareUnit, state.get().registers.key(locals.idx)); + locals.idx = state.get().registers.nextElementIndex(locals.idx); + } } locals.tRaffle.epochWinner = locals.winner; @@ -1442,7 +1631,6 @@ struct QRAFFLE : public ContractBase locals.tRaffle.epoch = qpi.epoch(); state.mut().tokenRaffle.set(state.get().numberOfEndedTokenRaffle, locals.tRaffle); - // Log token raffle ended with detailed information locals.tokenRaffleLog = TokenRaffleLogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_tokenRaffleEnded, @@ -1516,6 +1704,9 @@ struct QRAFFLE : public ContractBase } } + // Record DAO member count for this epoch before resetting per-epoch state. + state.mut().daoMemberCount.set(qpi.epoch(), state.get().numberOfRegisters); + state.mut().numberOfVotedInProposal.setAll(0); state.mut().tokenRaffleMembers.reset(); state.mut().proposalsPerProposer.reset(); @@ -1530,6 +1721,18 @@ struct QRAFFLE : public ContractBase PRE_ACQUIRE_SHARES() { + // Accept all incoming share management transfers for free. + // Service fees are collected in user procedures (depositInTokenRaffle, TransferShareManagementRights). + output.requestedFee = 0; output.allowTransfer = true; } + + POST_ACQUIRE_SHARES() + { + // Credit any received fee to epochRevenue. + if (input.receivedFee > 0) + { + state.mut().epochRevenue += (uint64)input.receivedFee; + } + } }; diff --git a/test/contract_qraffle.cpp b/test/contract_qraffle.cpp index 54a123292..3990d80b6 100644 --- a/test/contract_qraffle.cpp +++ b/test/contract_qraffle.cpp @@ -423,6 +423,11 @@ class ContractTestingQraffle : protected ContractTesting } sint64 TransferShareManagementRightsQraffle(const id& issuer, uint64 assetName, uint32 newManagingContractIndex, sint64 numberOfShares, const id& currentOwner) + { + return TransferShareManagementRightsQraffleWithFee(issuer, assetName, newManagingContractIndex, numberOfShares, currentOwner, QRAFFLE_TRANSFER_SHARE_FEE); + } + + sint64 TransferShareManagementRightsQraffleWithFee(const id& issuer, uint64 assetName, uint32 newManagingContractIndex, sint64 numberOfShares, const id& currentOwner, sint64 invocationReward) { QRAFFLE::TransferShareManagementRights_input input; QRAFFLE::TransferShareManagementRights_output output; @@ -432,7 +437,7 @@ class ContractTestingQraffle : protected ContractTesting input.newManagingContractIndex = newManagingContractIndex; input.numberOfShares = numberOfShares; - invokeUserProcedure(QRAFFLE_CONTRACT_INDEX, 8, input, output, currentOwner, QRAFFLE_TRANSFER_SHARE_FEE); + invokeUserProcedure(QRAFFLE_CONTRACT_INDEX, 8, input, output, currentOwner, invocationReward); return output.transferredNumberOfShares; } }; @@ -553,7 +558,8 @@ TEST(ContractQraffle, SubmitProposal) // Issue some test assets id issuer = getUser(2000); - increaseEnergy(issuer, 1000000000ULL); + // Asset issuance fee on QX is 1,000,000,000 per asset; need at least 2 fees here. + increaseEnergy(issuer, 2 * 1000000000ULL); uint64 assetName1 = assetNameFromString("TEST1"); uint64 assetName2 = assetNameFromString("TEST2"); qraffle.issueAsset(issuer, assetName1, 1000000, 0, 0); @@ -615,13 +621,14 @@ TEST(ContractQraffle, SubmitProposalPerUserLimitAndValidation) EXPECT_EQ(qraffle.submitProposal(users[0], token, 3000000ULL).returnCode, QRAFFLE_SUCCESS); EXPECT_EQ(qraffle.submitProposal(users[0], token, 4000000ULL).returnCode, QRAFFLE_MAX_PROPOSAL_PER_USER_REACHED); - EXPECT_EQ(qraffle.submitProposal(users[0], token, 500000ULL).returnCode, QRAFFLE_INVALID_ENTRY_AMOUNT); - EXPECT_EQ(qraffle.submitProposal(users[0], token, 2000000000ULL).returnCode, QRAFFLE_INVALID_ENTRY_AMOUNT); + // Use a different proposer for entry-amount validation; per-user cap would otherwise mask it. + EXPECT_EQ(qraffle.submitProposal(users[1], token, 500000ULL).returnCode, QRAFFLE_INVALID_ENTRY_AMOUNT); + EXPECT_EQ(qraffle.submitProposal(users[1], token, 2000000000ULL).returnCode, QRAFFLE_INVALID_ENTRY_AMOUNT); Asset fakeToken; fakeToken.assetName = assetNameFromString("NOTISS"); fakeToken.issuer = issuer; - EXPECT_EQ(qraffle.submitProposal(users[1], fakeToken, 1000000ULL).returnCode, QRAFFLE_INVALID_TOKEN_TYPE); + EXPECT_EQ(qraffle.submitProposal(users[2], fakeToken, 1000000ULL).returnCode, QRAFFLE_INVALID_TOKEN_TYPE); } TEST(ContractQraffle, VoteInProposal) @@ -802,13 +809,25 @@ TEST(ContractQraffle, DepositInTokenRaffle) auto result = qraffle.depositInTokenRaffle(poorUser, 0, QRAFFLE_TRANSFER_SHARE_FEE - 1); EXPECT_EQ(result.returnCode, QRAFFLE_INSUFFICIENT_FUND); - // Test insufficient Token + // Test unregistered user (must be a DAO member to deposit in token raffle) + increaseEnergy(poorUser, QRAFFLE_TRANSFER_SHARE_FEE); + result = qraffle.depositInTokenRaffle(poorUser, 0, QRAFFLE_TRANSFER_SHARE_FEE); + EXPECT_EQ(result.returnCode, QRAFFLE_UNREGISTERED); + + // Test insufficient Token (registered DAO member with too few shares) id poorUser2 = getUser(8888); + increaseEnergy(poorUser2, QRAFFLE_REGISTER_AMOUNT); + qraffle.registerInSystem(poorUser2, QRAFFLE_REGISTER_AMOUNT, 0); increaseEnergy(poorUser2, QRAFFLE_TRANSFER_SHARE_FEE); qraffle.transferShareOwnershipAndPossession(issuer, assetName, issuer, 999999, poorUser2); result = qraffle.depositInTokenRaffle(poorUser2, 0, QRAFFLE_TRANSFER_SHARE_FEE); EXPECT_EQ(result.returnCode, QRAFFLE_FAILED_TO_DEPOSIT); + // Test duplicate deposit by same user (already deposited above) + increaseEnergy(users[0], QRAFFLE_TRANSFER_SHARE_FEE); + result = qraffle.depositInTokenRaffle(users[0], 0, QRAFFLE_TRANSFER_SHARE_FEE); + EXPECT_EQ(result.returnCode, QRAFFLE_ALREADY_REGISTERED); + // Test invalid token raffle index increaseEnergy(users[0], QRAFFLE_TRANSFER_SHARE_FEE); result = qraffle.depositInTokenRaffle(users[0], 999, QRAFFLE_TRANSFER_SHARE_FEE); @@ -831,7 +850,11 @@ TEST(ContractQraffle, TransferShareManagementRights) EXPECT_EQ(numberOfPossessedShares(assetName, issuer, user1, user1, QRAFFLE_CONTRACT_INDEX, QRAFFLE_CONTRACT_INDEX), 1000000); increaseEnergy(user1, 1000000000ULL); - qraffle.TransferShareManagementRightsQraffle(issuer, assetName, QX_CONTRACT_INDEX, 1000000, user1); + // QRAFFLE's procedure now forwards (invocationReward - QRAFFLE_TRANSFER_SHARE_FEE) as the + // offeredTransferFee to the destination contract (QX requires 100 QU). Previously the test + // only paid QRAFFLE's own fee, leaving 0 to offer QX which silently failed. We now pass + // a sufficient amount and rely on the procedure to refund any excess. + qraffle.TransferShareManagementRightsQraffleWithFee(issuer, assetName, QX_CONTRACT_INDEX, 1000000, user1, 2 * QRAFFLE_TRANSFER_SHARE_FEE); EXPECT_EQ(numberOfPossessedShares(assetName, issuer, user1, user1, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), 1000000); } @@ -866,7 +889,8 @@ TEST(ContractQraffle, GetFunctions) // Create some proposals id issuer = getUser(2000); - increaseEnergy(issuer, 1000000000ULL); + // Asset issuance fee on QX is 1,000,000,000 per asset; need at least 2 fees here. + increaseEnergy(issuer, 2 * 1000000000ULL); uint64 assetName1 = assetNameFromString("TEST1"); uint64 assetName2 = assetNameFromString("TEST2"); qraffle.issueAsset(issuer, assetName1, 1000000000, 0, 0); @@ -1454,7 +1478,10 @@ TEST(ContractQraffle, QXMRRevenueDistribution) qraffle.depositInQuRaffle(users[0], QRAFFLE_DEFAULT_QRAFFLE_AMOUNT); qraffle.endEpoch(); - EXPECT_EQ(qraffle.getState()->getEpochQXMRRevenue(), expectedQXMRRevenue - div(expectedQXMRRevenue, 676ull) * 676); + // No QRAFFLE shareholders exist in this test universe, so no QXMR is actually transferred. + // The contract must therefore retain the full epochQXMRRevenue rather than booking it as + // distributed (previous behavior silently leaked QXMR accounting). + EXPECT_EQ(qraffle.getState()->getEpochQXMRRevenue(), expectedQXMRRevenue); } TEST(ContractQraffle, GetQuRaffleEntryAmountPerUser) From 48a4ba75dcef6176c71962da5f4b5b062e2f29cb Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Fri, 24 Apr 2026 10:41:31 +0900 Subject: [PATCH 08/14] fix: voteStatus's data structure - O(N) -> O(1) --- src/contracts/QRaffle.h | 111 ++++++++++++++++------------------------ 1 file changed, 45 insertions(+), 66 deletions(-) diff --git a/src/contracts/QRaffle.h b/src/contracts/QRaffle.h index 4b393d9d2..60501ceb8 100644 --- a/src/contracts/QRaffle.h +++ b/src/contracts/QRaffle.h @@ -166,11 +166,6 @@ struct QRAFFLE : public ContractBase uint32 nNo; }; - struct VotedId { - id user; - bit status; - }; - struct QuRaffleInfo { id epochWinner; @@ -200,8 +195,10 @@ struct QRAFFLE : public ContractBase HashMap registers; Array proposals; - HashMap , QRAFFLE_MAX_PROPOSAL_EPOCH> voteStatus; - Array tmpVoteStatus; + // Per-user vote tracking with dual BitArray (qRWA pattern). + // O(1) lookup via id hash, 1 bit per proposal. ~4 MB each, ~8 MB total. + HashMap , QRAFFLE_MAX_MEMBER> voteParticipation; // bit=1 if user has voted + HashMap , QRAFFLE_MAX_MEMBER> voteValues; // bit=1 for yes, bit=0 for no Array numberOfVotedInProposal; Array quRaffleMembers; @@ -704,9 +701,8 @@ struct QRAFFLE : public ContractBase struct voteInProposal_locals { ProposalInfo proposal; - VotedId votedId; - VotedId emptyVote; - uint32 i; + BitArray participation; + BitArray values; uint32 votedCount; Logger log; }; @@ -732,63 +728,42 @@ struct QRAFFLE : public ContractBase return ; } locals.proposal = state.get().proposals.get(input.indexOfProposal); - // Load vote buffer for this proposal. Clear it if no votes exist yet to avoid - // matching stale entries left over from a previous proposal in the same slot. - if (state.get().voteStatus.contains(input.indexOfProposal)) - { - state.get().voteStatus.get(input.indexOfProposal, state.mut().tmpVoteStatus); - } - else - { - locals.emptyVote.user = NULL_ID; - locals.emptyVote.status = 0; - state.mut().tmpVoteStatus.setAll(locals.emptyVote); - } - locals.votedCount = state.get().numberOfVotedInProposal.get(input.indexOfProposal); - for (locals.i = 0; locals.i < locals.votedCount; locals.i++) + + // O(1) vote lookup via per-user bitfield (id hash, no K12 overhead). + state.get().voteParticipation.get(qpi.invocator(), locals.participation); + if (locals.participation.get(input.indexOfProposal)) { - if (state.get().tmpVoteStatus.get(locals.i).user == qpi.invocator()) + // Already voted — check if same direction. + state.get().voteValues.get(qpi.invocator(), locals.values); + if (locals.values.get(input.indexOfProposal) == input.yes) { - if (state.get().tmpVoteStatus.get(locals.i).status == input.yes) - { - output.returnCode = QRAFFLE_ALREADY_VOTED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_alreadyVoted, 0 }; - LOG_INFO(locals.log); - return ; - } - else - { - // Guard against unsigned underflow when flipping the vote. - if (input.yes) - { - locals.proposal.nYes++; - if (locals.proposal.nNo > 0) - { - locals.proposal.nNo--; - } - } - else - { - locals.proposal.nNo++; - if (locals.proposal.nYes > 0) - { - locals.proposal.nYes--; - } - } - state.mut().proposals.set(input.indexOfProposal, locals.proposal); - } - - locals.votedId.user = qpi.invocator(); - locals.votedId.status = input.yes; - state.mut().tmpVoteStatus.set(locals.i, locals.votedId); - state.mut().voteStatus.set(input.indexOfProposal, state.get().tmpVoteStatus); - output.returnCode = QRAFFLE_SUCCESS; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_proposalVoted, 0 }; + output.returnCode = QRAFFLE_ALREADY_VOTED; + locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_alreadyVoted, 0 }; LOG_INFO(locals.log); return ; } + // Flip the vote: update counters with underflow guard. + if (input.yes) + { + locals.proposal.nYes++; + if (locals.proposal.nNo > 0) { locals.proposal.nNo--; } + } + else + { + locals.proposal.nNo++; + if (locals.proposal.nYes > 0) { locals.proposal.nYes--; } + } + state.mut().proposals.set(input.indexOfProposal, locals.proposal); + locals.values.set(input.indexOfProposal, input.yes); + state.mut().voteValues.replace(qpi.invocator(), locals.values); + output.returnCode = QRAFFLE_SUCCESS; + locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_proposalVoted, 0 }; + LOG_INFO(locals.log); + return ; } - // Reject new votes once the per-proposal buffer is full. + + // New vote: check capacity before inserting. + locals.votedCount = state.get().numberOfVotedInProposal.get(input.indexOfProposal); if (locals.votedCount >= QRAFFLE_MAX_MEMBER) { output.returnCode = QRAFFLE_MAX_MEMBER_REACHED; @@ -806,10 +781,13 @@ struct QRAFFLE : public ContractBase } state.mut().proposals.set(input.indexOfProposal, locals.proposal); - locals.votedId.user = qpi.invocator(); - locals.votedId.status = input.yes; - state.mut().tmpVoteStatus.set(locals.votedCount, locals.votedId); - state.mut().voteStatus.set(input.indexOfProposal, state.get().tmpVoteStatus); + // Mark participation and record vote direction. + locals.participation.set(input.indexOfProposal, 1); + state.mut().voteParticipation.set(qpi.invocator(), locals.participation); + state.get().voteValues.get(qpi.invocator(), locals.values); + locals.values.set(input.indexOfProposal, input.yes); + state.mut().voteValues.set(qpi.invocator(), locals.values); + state.mut().numberOfVotedInProposal.set(input.indexOfProposal, locals.votedCount + 1); output.returnCode = QRAFFLE_SUCCESS; locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_proposalVoted, 0 }; @@ -1712,7 +1690,8 @@ struct QRAFFLE : public ContractBase state.mut().proposalsPerProposer.reset(); state.mut().quRaffleEntryAmount.reset(); state.mut().shareholdersList.reset(); - state.mut().voteStatus.reset(); + state.mut().voteParticipation.reset(); + state.mut().voteValues.reset(); state.mut().numberOfEntryAmountSubmitted = 0; state.mut().numberOfProposals = 0; state.mut().numberOfQuRaffleMembers = 0; From b7421a99c6a0ae0b6194b3de449be1636cbae381 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Fri, 24 Apr 2026 20:59:44 +0900 Subject: [PATCH 09/14] QRaffle: cut vote and token-raffle state size; O(1) vote and deposit paths --- src/contracts/QRaffle.h | 44 ++++++++++++++++++++------------------- test/contract_qraffle.cpp | 3 +-- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/contracts/QRaffle.h b/src/contracts/QRaffle.h index 60501ceb8..7d665197f 100644 --- a/src/contracts/QRaffle.h +++ b/src/contracts/QRaffle.h @@ -21,6 +21,7 @@ constexpr uint32 QRAFFLE_MIN_QRAFFLE_AMOUNT = 1000000ull; constexpr uint32 QRAFFLE_MAX_QRAFFLE_AMOUNT = 1000000000ull; constexpr uint32 QRAFFLE_MAX_TOKEN_RAFFLES = 1048576; constexpr uint32 QRAFFLE_MAX_SHAREHOLDERS = 1024; +constexpr uint32 QRAFFLE_TOKEN_RAFFLE_SLOT_SIZE = 512; // 2^9, max members per token raffle constexpr uint8 QRAFFLE_MAX_PROPOSALS_PER_PROPOSER = 3; // max proposals per user per epoch constexpr sint32 QRAFFLE_SUCCESS = 0; @@ -203,9 +204,11 @@ struct QRAFFLE : public ContractBase Array quRaffleMembers; Array activeTokenRaffle; - HashMap , QRAFFLE_MAX_PROPOSAL_EPOCH> tokenRaffleMembers; + // Per-user O(1) duplicate check for token raffle deposits. ~4 MB. + HashMap , QRAFFLE_MAX_MEMBER> tokenRaffleParticipation; + // Flat indexed member storage: raffle i occupies slots [i*SLOT_SIZE .. i*SLOT_SIZE+count). ~2 MB. + Array tokenRaffleMemberSlots; Array numberOfTokenRaffleMembers; - Array tmpTokenRaffleMembers; Array QuRaffles; Array tokenRaffle; @@ -849,7 +852,7 @@ struct QRAFFLE : public ContractBase struct depositInTokenRaffle_locals { ActiveTokenRaffleInfo raffleInfo; - uint32 i; + BitArray participation; uint32 currentMembers; Logger log; }; @@ -891,7 +894,7 @@ struct QRAFFLE : public ContractBase return ; } locals.currentMembers = state.get().numberOfTokenRaffleMembers.get(input.indexOfTokenRaffle); - if (locals.currentMembers >= QRAFFLE_MAX_MEMBER) + if (locals.currentMembers >= QRAFFLE_TOKEN_RAFFLE_SLOT_SIZE) { if (qpi.invocationReward() > 0) { @@ -902,21 +905,18 @@ struct QRAFFLE : public ContractBase LOG_INFO(locals.log); return ; } - // Reject duplicate deposit from the same user. - state.get().tokenRaffleMembers.get(input.indexOfTokenRaffle, state.mut().tmpTokenRaffleMembers); - for (locals.i = 0; locals.i < locals.currentMembers; locals.i++) + // O(1) duplicate check via per-user bitfield. + state.get().tokenRaffleParticipation.get(qpi.invocator(), locals.participation); + if (locals.participation.get(input.indexOfTokenRaffle)) { - if (state.get().tmpTokenRaffleMembers.get(locals.i) == qpi.invocator()) + if (qpi.invocationReward() > 0) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.returnCode = QRAFFLE_ALREADY_REGISTERED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_alreadyRegistered, 0 }; - LOG_INFO(locals.log); - return ; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + output.returnCode = QRAFFLE_ALREADY_REGISTERED; + locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_alreadyRegistered, 0 }; + LOG_INFO(locals.log); + return ; } locals.raffleInfo = state.get().activeTokenRaffle.get(input.indexOfTokenRaffle); if (qpi.transferShareOwnershipAndPossession(locals.raffleInfo.token.assetName, locals.raffleInfo.token.issuer, qpi.invocator(), qpi.invocator(), locals.raffleInfo.entryAmount, SELF) < 0) @@ -936,9 +936,12 @@ struct QRAFFLE : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward() - QRAFFLE_TRANSFER_SHARE_FEE); } state.mut().epochRevenue += QRAFFLE_TRANSFER_SHARE_FEE; - state.mut().tmpTokenRaffleMembers.set(locals.currentMembers, qpi.invocator()); + + // Store member in flat slot array and mark participation. + state.mut().tokenRaffleMemberSlots.set(input.indexOfTokenRaffle * QRAFFLE_TOKEN_RAFFLE_SLOT_SIZE + locals.currentMembers, qpi.invocator()); state.mut().numberOfTokenRaffleMembers.set(input.indexOfTokenRaffle, locals.currentMembers + 1); - state.mut().tokenRaffleMembers.set(input.indexOfTokenRaffle, state.get().tmpTokenRaffleMembers); + locals.participation.set(input.indexOfTokenRaffle, 1); + state.mut().tokenRaffleParticipation.set(qpi.invocator(), locals.participation); output.returnCode = QRAFFLE_SUCCESS; locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_tokenRaffleDeposited, 0 }; LOG_INFO(locals.log); @@ -1528,8 +1531,7 @@ struct QRAFFLE : public ContractBase locals.raffleSeed = qpi.K12(m256i(locals.baseSeed.u64._0, locals.baseSeed.u64._1, locals.baseSeed.u64._2, locals.baseSeed.u64._3 ^ ((uint64)locals.i + 1ULL))); locals.r = locals.raffleSeed.u64._0; locals.winnerIndex = (uint32)mod(locals.r, state.get().numberOfTokenRaffleMembers.get(locals.i) * 1ull); - state.get().tokenRaffleMembers.get(locals.i, state.mut().tmpTokenRaffleMembers); - locals.winner = state.get().tmpTokenRaffleMembers.get(locals.winnerIndex); + locals.winner = state.get().tokenRaffleMemberSlots.get(locals.i * QRAFFLE_TOKEN_RAFFLE_SLOT_SIZE + locals.winnerIndex); locals.acTokenRaffle = state.get().activeTokenRaffle.get(locals.i); @@ -1686,7 +1688,7 @@ struct QRAFFLE : public ContractBase state.mut().daoMemberCount.set(qpi.epoch(), state.get().numberOfRegisters); state.mut().numberOfVotedInProposal.setAll(0); - state.mut().tokenRaffleMembers.reset(); + state.mut().tokenRaffleParticipation.reset(); state.mut().proposalsPerProposer.reset(); state.mut().quRaffleEntryAmount.reset(); state.mut().shareholdersList.reset(); diff --git a/test/contract_qraffle.cpp b/test/contract_qraffle.cpp index d87f62c48..4d6f9b6be 100644 --- a/test/contract_qraffle.cpp +++ b/test/contract_qraffle.cpp @@ -105,11 +105,10 @@ class QRaffleChecker : public QRAFFLE, public QRAFFLE::StateData void tokenRaffleMemberChecker(uint32 raffleIndex, const id& user, uint32 expectedMembers) { - tokenRaffleMembers.get(raffleIndex, tmpTokenRaffleMembers); bool found = false; for (uint32 i = 0; i < numberOfTokenRaffleMembers.get(raffleIndex); i++) { - if (tmpTokenRaffleMembers.get(i) == user) + if (tokenRaffleMemberSlots.get(raffleIndex * QRAFFLE_TOKEN_RAFFLE_SLOT_SIZE + i) == user) { found = true; break; From 1603dc4a406294a55d436e00a926bbf96d4a5703 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Fri, 24 Apr 2026 22:31:04 +0900 Subject: [PATCH 10/14] QRaffle: O(1) qu-raffle dedup, shrink token-raffle history ring, fix ring queries --- src/contracts/QRaffle.h | 48 +++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/src/contracts/QRaffle.h b/src/contracts/QRaffle.h index 7d665197f..af46a1cd5 100644 --- a/src/contracts/QRaffle.h +++ b/src/contracts/QRaffle.h @@ -19,7 +19,9 @@ constexpr uint32 QRAFFLE_MAX_MEMBER = 65536; constexpr uint32 QRAFFLE_DEFAULT_QRAFFLE_AMOUNT = 10000000ull; constexpr uint32 QRAFFLE_MIN_QRAFFLE_AMOUNT = 1000000ull; constexpr uint32 QRAFFLE_MAX_QRAFFLE_AMOUNT = 1000000000ull; -constexpr uint32 QRAFFLE_MAX_TOKEN_RAFFLES = 1048576; +// Ended token-raffle ring: 16 384 slots × ~96 B ≈ 1.5 MB. +// At most QRAFFLE_MAX_PROPOSAL_EPOCH (128) raffles/epoch → covers ~128 epochs of history. +constexpr uint32 QRAFFLE_MAX_TOKEN_RAFFLES = 16384; constexpr uint32 QRAFFLE_MAX_SHAREHOLDERS = 1024; constexpr uint32 QRAFFLE_TOKEN_RAFFLE_SLOT_SIZE = 512; // 2^9, max members per token raffle constexpr uint8 QRAFFLE_MAX_PROPOSALS_PER_PROPOSER = 3; // max proposals per user per epoch @@ -202,6 +204,8 @@ struct QRAFFLE : public ContractBase HashMap , QRAFFLE_MAX_MEMBER> voteValues; // bit=1 for yes, bit=0 for no Array numberOfVotedInProposal; Array quRaffleMembers; + // O(1) duplicate guard for quRaffle entries; mirrors quRaffleMembers for membership tests. + HashSet quRaffleMemberSet; Array activeTokenRaffle; // Per-user O(1) duplicate check for token raffle deposits. ~4 MB. @@ -799,7 +803,6 @@ struct QRAFFLE : public ContractBase struct depositInQuRaffle_locals { - uint32 i; Logger log; }; @@ -827,22 +830,21 @@ struct QRAFFLE : public ContractBase LOG_INFO(locals.log); return ; } - for (locals.i = 0 ; locals.i < state.get().numberOfQuRaffleMembers; locals.i++) + // O(1) duplicate check via HashSet (replaces former O(N) linear scan over quRaffleMembers). + if (state.get().quRaffleMemberSet.contains(qpi.invocator())) { - if (state.get().quRaffleMembers.get(locals.i) == qpi.invocator()) + if (qpi.invocationReward() > 0) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.returnCode = QRAFFLE_ALREADY_REGISTERED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_alreadyRegistered, 0 }; - LOG_INFO(locals.log); - return ; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + output.returnCode = QRAFFLE_ALREADY_REGISTERED; + locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_alreadyRegistered, 0 }; + LOG_INFO(locals.log); + return ; } qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.get().qREAmount); state.mut().quRaffleMembers.set(state.get().numberOfQuRaffleMembers, qpi.invocator()); + state.mut().quRaffleMemberSet.add(qpi.invocator()); state.mut().numberOfQuRaffleMembers++; output.returnCode = QRAFFLE_SUCCESS; locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_quRaffleDeposited, 0 }; @@ -1172,6 +1174,13 @@ struct QRAFFLE : public ContractBase output.returnCode = QRAFFLE_INVALID_TOKEN_RAFFLE; return ; } + // Reject indices that have been overwritten by the ring buffer. + if (state.get().numberOfEndedTokenRaffle > QRAFFLE_MAX_TOKEN_RAFFLES + && input.indexOfRaffle < state.get().numberOfEndedTokenRaffle - QRAFFLE_MAX_TOKEN_RAFFLES) + { + output.returnCode = QRAFFLE_INVALID_TOKEN_RAFFLE; + return ; + } output.epochWinner = state.get().tokenRaffle.get(input.indexOfRaffle).epochWinner; output.tokenName = state.get().tokenRaffle.get(input.indexOfRaffle).token.assetName; output.tokenIssuer = state.get().tokenRaffle.get(input.indexOfRaffle).token.issuer; @@ -1184,12 +1193,13 @@ struct QRAFFLE : public ContractBase struct getEpochRaffleIndexes_locals { - sint32 i; + uint32 ringStart; + uint32 ringEnd; + uint32 i; }; PUBLIC_FUNCTION_WITH_LOCALS(getEpochRaffleIndexes) { - // Initialise outputs so callers see deterministic values when no raffles match. output.StartIndex = 0; output.EndIndex = 0; if (input.epoch > qpi.epoch()) @@ -1204,7 +1214,10 @@ struct QRAFFLE : public ContractBase output.returnCode = QRAFFLE_SUCCESS; return ; } - for (locals.i = 0; locals.i < (sint32)state.get().numberOfEndedTokenRaffle; locals.i++) + // Only scan the valid ring window to avoid re-reading overwritten slots. + locals.ringEnd = state.get().numberOfEndedTokenRaffle; + locals.ringStart = (locals.ringEnd > QRAFFLE_MAX_TOKEN_RAFFLES) ? (locals.ringEnd - QRAFFLE_MAX_TOKEN_RAFFLES) : 0; + for (locals.i = locals.ringStart; locals.i < locals.ringEnd; locals.i++) { if (state.get().tokenRaffle.get(locals.i).epoch == input.epoch) { @@ -1212,8 +1225,10 @@ struct QRAFFLE : public ContractBase break; } } - for (locals.i = (sint32)state.get().numberOfEndedTokenRaffle - 1; locals.i >= 0; locals.i--) + locals.i = locals.ringEnd; + while (locals.i > locals.ringStart) { + locals.i--; if (state.get().tokenRaffle.get(locals.i).epoch == input.epoch) { output.EndIndex = locals.i; @@ -1697,6 +1712,7 @@ struct QRAFFLE : public ContractBase state.mut().numberOfEntryAmountSubmitted = 0; state.mut().numberOfProposals = 0; state.mut().numberOfQuRaffleMembers = 0; + state.mut().quRaffleMemberSet.reset(); if (state.get().registers.needsCleanup()) { state.mut().registers.cleanup(); } } From f76123154f2905dae5dbaf093c22a80857663b70 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Fri, 8 May 2026 09:18:45 +0900 Subject: [PATCH 11/14] fix: qraffle test file --- test/CMakeLists.txt | 1 + test/contract_qraffle.cpp | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d429b8716..2424ea757 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -33,6 +33,7 @@ add_executable( # contract_qearn.cpp # contract_qvault.cpp # contract_qx.cpp + contract_qraffle.cpp contract_vottunbridge.cpp # kangaroo_twelve.cpp m256.cpp diff --git a/test/contract_qraffle.cpp b/test/contract_qraffle.cpp index 4d6f9b6be..b7bf4e580 100644 --- a/test/contract_qraffle.cpp +++ b/test/contract_qraffle.cpp @@ -661,12 +661,14 @@ TEST(ContractQraffle, VoteInProposal) proposalCount++; uint32 yesVotes = 0, noVotes = 0; + bit users0OriginalVote = 0; // Test voting - for (const auto& user : users) + for (size_t vi = 0; vi < users.size(); vi++) { bit vote = (bit)random(0, 2); - auto result = qraffle.voteInProposal(user, 0, vote); + if (vi == 0) users0OriginalVote = vote; + auto result = qraffle.voteInProposal(users[vi], 0, vote); EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); if (vote) @@ -677,8 +679,8 @@ TEST(ContractQraffle, VoteInProposal) qraffle.getState()->voteChecker(0, yesVotes, noVotes); } - // Test duplicate vote (should change vote) - bit newVote = (bit)random(0, 2); + // Test duplicate vote: explicitly use the opposite direction to guarantee the vote changes + bit newVote = users0OriginalVote ? (bit)0 : (bit)1; auto result = qraffle.voteInProposal(users[0], 0, newVote); EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); @@ -974,8 +976,8 @@ TEST(ContractQraffle, GetFunctions) auto registers2 = qraffle.getRegisters(registerCount + 10, 5); EXPECT_EQ(registers2.returnCode, QRAFFLE_INVALID_OFFSET_OR_LIMIT); - // Test with limit exceeding maximum (1024) - auto registers3 = qraffle.getRegisters(0, 1025); + // Test with limit exceeding maximum (20) + auto registers3 = qraffle.getRegisters(0, 21); EXPECT_EQ(registers3.returnCode, QRAFFLE_INVALID_OFFSET_OR_LIMIT); // Test with offset + limit exceeding total registers @@ -1110,12 +1112,16 @@ TEST(ContractQraffle, GetFunctions) // Test with current epoch (0) auto endedQuRaffle = qraffle.getEndedQuRaffle(0); EXPECT_EQ(endedQuRaffle.returnCode, QRAFFLE_SUCCESS); - EXPECT_NE(endedQuRaffle.epochWinner, id(0, 0, 0, 0)); // Winner should be set - EXPECT_GT(endedQuRaffle.receivedAmount, 0); - EXPECT_EQ(endedQuRaffle.entryAmount, 10000000); EXPECT_EQ(endedQuRaffle.numberOfMembers, memberCount); EXPECT_GT(endedQuRaffle.numberOfDaoMembers, 0u); EXPECT_EQ(endedQuRaffle.numberOfDaoMembers, qraffle.getState()->getNumberOfRegisters()); + // Winner and prize are only set when at least one member participated + if (memberCount > 0) + { + EXPECT_NE(endedQuRaffle.epochWinner, id(0, 0, 0, 0)); + EXPECT_GT(endedQuRaffle.receivedAmount, 0); + EXPECT_EQ(endedQuRaffle.entryAmount, 10000000); + } // Test with future epoch auto futureQuRaffle = qraffle.getEndedQuRaffle(1); From ce3dd0bac4df86a6aae498a08b4e32c4177611b1 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Tue, 26 May 2026 21:51:38 +0900 Subject: [PATCH 12/14] fix: remove statement that check qraffle DAO member in depositInTokenRaffle. so Everyone can participate in one --- src/contracts/QRaffle.h | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/contracts/QRaffle.h b/src/contracts/QRaffle.h index af46a1cd5..687bf8443 100644 --- a/src/contracts/QRaffle.h +++ b/src/contracts/QRaffle.h @@ -872,18 +872,7 @@ struct QRAFFLE : public ContractBase LOG_INFO(locals.log); return ; } - // Only registered members may deposit. - if (state.get().registers.contains(qpi.invocator()) == 0) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.returnCode = QRAFFLE_UNREGISTERED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_unregistered, 0 }; - LOG_INFO(locals.log); - return ; - } + if (input.indexOfTokenRaffle >= state.get().numberOfActiveTokenRaffle) { if (qpi.invocationReward() > 0) From ca8f5162d3308b5d7b56e07110a7a6294c52df03 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Wed, 27 May 2026 06:53:29 +0900 Subject: [PATCH 13/14] fix: remove statement that check internal token and Share - qraffle and qxmr --- src/contracts/QRaffle.h | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/contracts/QRaffle.h b/src/contracts/QRaffle.h index 687bf8443..221207aee 100644 --- a/src/contracts/QRaffle.h +++ b/src/contracts/QRaffle.h @@ -684,15 +684,7 @@ struct QRAFFLE : public ContractBase LOG_INFO(locals.log); return ; } - // Reject internal tokens: QRAFFLE shares and QXMR are reserved for dividends/registration. - if ((input.tokenName == QRAFFLE_ASSET_NAME && input.tokenIssuer == NULL_ID) - || (input.tokenName == QRAFFLE_QXMR_ASSET_NAME && input.tokenIssuer == state.get().QXMRIssuer)) - { - output.returnCode = QRAFFLE_INVALID_TOKEN_TYPE; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidTokenType, 0 }; - LOG_INFO(locals.log); - return ; - } + locals.proposal.token.issuer = input.tokenIssuer; locals.proposal.token.assetName = input.tokenName; locals.proposal.entryAmount = input.entryAmount; From 20aee8fe27b978ebc552d2a58fe0b6fcdbd1d23a Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Wed, 27 May 2026 20:48:48 +0900 Subject: [PATCH 14/14] fix: remove statement that check amount of asset --- src/contracts/QRaffle.h | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/contracts/QRaffle.h b/src/contracts/QRaffle.h index 221207aee..f43a19c14 100644 --- a/src/contracts/QRaffle.h +++ b/src/contracts/QRaffle.h @@ -670,13 +670,6 @@ struct QRAFFLE : public ContractBase LOG_INFO(locals.log); return ; } - if (input.entryAmount < QRAFFLE_MIN_QRAFFLE_AMOUNT || input.entryAmount > QRAFFLE_MAX_QRAFFLE_AMOUNT) - { - output.returnCode = QRAFFLE_INVALID_ENTRY_AMOUNT; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidEntryAmount, 0 }; - LOG_INFO(locals.log); - return ; - } if (!qpi.isAssetIssued(input.tokenIssuer, input.tokenName)) { output.returnCode = QRAFFLE_INVALID_TOKEN_TYPE;