diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index c7a4f2f45..237efde61 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -46,6 +46,7 @@ + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index 24b11e9e1..3b6f5e968 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -132,6 +132,9 @@ contracts + + contracts + contract_core diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index cf0c0e241..c2ed098ff 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -265,6 +265,16 @@ #include "contracts/VottunBridge.h" +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QUSINO_CONTRACT_INDEX 26 +#define CONTRACT_INDEX QUSINO_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QUSINO +#define CONTRACT_STATE2_TYPE QUSINO2 +#include "contracts/Qusino.h" + // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES @@ -376,6 +386,7 @@ constexpr struct ContractDescription {"QDUEL", 199, 10000, sizeof(QDUEL::StateData)}, // proposal in epoch 197, IPO in 198, construction and first use in 199 {"PULSE", 204, 10000, sizeof(PULSE::StateData)}, // proposal in epoch 202, IPO in 203, construction and first use in 204 {"VOTTUN", 206, 10000, sizeof(VOTTUNBRIDGE::StateData)}, // proposal in epoch 204, IPO in 205, construction and first use in 206 + {"QUSINO", 207, 10000, sizeof(QUSINO::StateData)}, // proposal in epoch 205, IPO in 206, construction and first use in 207 // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA::StateData)}, @@ -497,6 +508,7 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QDUEL); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(PULSE); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(VOTTUNBRIDGE); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QUSINO); // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); diff --git a/src/contracts/Qusino.h b/src/contracts/Qusino.h new file mode 100644 index 000000000..95df19978 --- /dev/null +++ b/src/contracts/Qusino.h @@ -0,0 +1,966 @@ +using namespace QPI; + +constexpr uint64 QUSINO_MAX_USERS = 131072; +constexpr uint64 QUSINO_MAX_NUMBER_OF_GAMES = 131072; +constexpr uint64 QUSINO_GAME_SUBMIT_FEE = 100000000; +constexpr uint32 QUSINO_MAX_NUMBER_OF_GAMES_FOR_VOTING_PER_USER = 64; +constexpr uint32 QUSINO_REVOTE_DURATION = 78; // in number of weeks(18 Months) +constexpr uint32 QUSINO_STAR_BONUS_FOR_QSC = 1000; +constexpr uint32 QUSINO_STAR_PRICE = 1; +constexpr uint32 QUSINO_VOTE_FEE = 1000; +constexpr uint32 QUSINO_LP_DIVIDENDS_PERCENT = 20; +constexpr uint32 QUSINO_CCF_DIVIDENDS_PERCENT = 5; +constexpr uint32 QUSINO_TREASURY_DIVIDENDS_PERCENT = 25; +constexpr uint32 QUSINO_SHAREHOLDERS_DIVIDENDS_PERCENT = 20; +constexpr uint32 QUSINO_QST_HOLDERS_DIVIDENDS_PERCENT = 30; +constexpr uint64 QUSINO_INFINITY_PRICE = 1000000000000000000ULL; +constexpr uint64 QUSINO_QSC_PRICE = 100; // 1QSC = 100Qubic +constexpr uint64 QUSINO_DEVELOPER_FEE = 333; // 33.3% +constexpr uint64 QUSINO_SUPPLY_OF_QST = 1200000000ULL; // 1.2 billion +constexpr uint64 QUSINO_DAILY_CLAIM_BONUS_DURATION = 24 * 60 * 60; // in number of seconds +constexpr uint64 QUSINO_BONUS_CLAIM_DURATION = 60; // 60s +constexpr uint64 QUSINO_BONUS_CLAIM_AMOUNT = 100; // 100STAR + 1QSC = 100Qubic, STAR isnt redeemable for qubic. +constexpr uint64 QUSINO_BONUS_CLAIM_AMOUNT_STAR = 100; +constexpr uint64 QUSINO_BONUS_CLAIM_AMOUNT_QSC = 1; + +constexpr sint32 QUSINO_SUCCESS = 0; +constexpr sint32 QUSINO_INSUFFICIENT_FUNDS = 1; +constexpr sint32 QUSINO_INSUFFICIENT_STAR = 2; +constexpr sint32 QUSINO_INSUFFICIENT_QSC = 3; +constexpr sint32 QUSINO_INSUFFICIENT_VOTE_FEE = 6; +constexpr sint32 QUSINO_WRONG_GAME_URI_FOR_VOTE = 7; +constexpr sint32 QUSINO_ALREADY_VOTED = 8; +constexpr sint32 QUSINO_NOT_VOTE_TIME = 9; +constexpr sint32 QUSINO_NO_EMPTY_SLOT = 10; +constexpr sint32 QUSINO_INSUFFICIENT_QST_AMOUNT_FOR_SALE = 11; +constexpr sint32 QUSINO_INVALID_TRANSFER = 12; +constexpr sint32 QUSINO_INSUFFICIENT_QST = 13; +constexpr sint32 QUSINO_WRONG_ASSET_TYPE = 14; +constexpr sint32 QUSINO_ALREADY_VOTED_WITH_SAME_VOTE = 15; +constexpr sint32 QUSINO_ALREADY_CLAIMED_TODAY = 16; +constexpr sint32 QUSINO_BONUS_CLAIM_TIME_NOT_COME = 17; +constexpr sint32 QUSINO_INSUFFICIENT_BONUS_AMOUNT = 18; +constexpr sint32 QUSINO_INVALID_GAME_PROPOSER = 19; + +constexpr uint8 QUSINO_ASSET_TYPE_QUBIC = 0; +constexpr uint8 QUSINO_ASSET_TYPE_QSC = 1; +constexpr uint8 QUSINO_ASSET_TYPE_STAR = 2; +constexpr uint8 QUSINO_ASSET_TYPE_QST = 3; + +constexpr uint32 QUSINO_LOG_SUCCESS = 0; +constexpr uint32 QUSINO_LOG_INSUFFICIENT_FUNDS = 1; +constexpr uint32 QUSINO_LOG_INSUFFICIENT_STAR = 2; +constexpr uint32 QUSINO_LOG_INSUFFICIENT_QSC = 3; +constexpr uint32 QUSINO_LOG_INSUFFICIENT_VOTE_FEE = 4; +constexpr uint32 QUSINO_LOG_WRONG_GAME_URI = 5; +constexpr uint32 QUSINO_LOG_NOT_VOTE_TIME = 6; +constexpr uint32 QUSINO_LOG_ALREADY_VOTED_WITH_SAME_VOTE = 7; +constexpr uint32 QUSINO_LOG_WRONG_ASSET_TYPE = 8; +constexpr uint32 QUSINO_LOG_ALREADY_CLAIMED_TODAY = 9; +constexpr uint32 QUSINO_LOG_BONUS_CLAIM_TIME_NOT_COME = 10; +constexpr uint32 QUSINO_LOG_INSUFFICIENT_BONUS_AMOUNT = 11; +constexpr uint32 QUSINO_LOG_INVALID_GAME_PROPOSER = 12; +struct QUSINOLogger +{ + uint32 _contractIndex; + uint32 _type; + sint8 _terminator; +}; + +struct QUSINO2 +{ +}; + +struct QUSINO : public ContractBase +{ +public: + struct earnSTAR_input + { + uint64 amount; // amount of STAR / 100 to earn + }; + struct earnSTAR_output + { + sint32 returnCode; + }; + struct transferSTAROrQSC_input + { + id dest; + uint64 amount; + uint8 type; // QUSINO_ASSET_TYPE_STAR or QUSINO_ASSET_TYPE_QSC + }; + struct transferSTAROrQSC_output + { + sint32 returnCode; + }; + struct submitGame_input + { + Array URI; + }; + struct submitGame_output + { + sint32 returnCode; + }; + struct voteInGameProposal_input + { + Array URI; + uint64 gameIndex; + uint8 yesNo; // 1 - yes, 2 - no + }; + struct voteInGameProposal_output + { + sint32 returnCode; + }; + + struct depositBonus_input + { + uint64 amount; + }; + struct depositBonus_output + { + sint32 returnCode; + }; + + struct dailyClaimBonus_input + { + }; + struct dailyClaimBonus_output + { + sint32 returnCode; + }; + + struct redemptionQSCToQubic_input + { + uint64 amount; + }; + struct redemptionQSCToQubic_output + { + sint32 returnCode; + }; + + struct getUserAssetVolume_input + { + id user; + }; + struct getUserAssetVolume_output + { + uint64 STARAmount; + uint64 QSCAmount; + }; + + struct GameInfo + { + Array URI; + id proposer; + uint32 yesVotes; + uint32 noVotes; + uint32 proposedEpoch; + }; + struct getFailedGameList_input + { + uint32 offset; + }; + struct getFailedGameList_output + { + Array games; + }; + + struct getSCInfo_input + { + + }; + struct getSCInfo_output + { + uint64 QSCCirclatingSupply; + uint64 STARCirclatingSupply; + uint64 burntSTAR; + uint64 epochRevenue; + uint64 maxGameIndex; + uint64 bonusAmount; + }; + + struct getActiveGameList_input + { + uint32 offset; + }; + struct getActiveGameList_output + { + Array games; + Array gameIndexes; + }; + + struct TransferShareManagementRights_input + { + Asset asset; + sint64 numberOfShares; + uint32 newManagingContractIndex; + }; + struct TransferShareManagementRights_output + { + sint64 transferredNumberOfShares; + }; + struct getProposerEarnedQSCInfo_input + { + id proposer; + uint32 epoch; + }; + struct getProposerEarnedQSCInfo_output + { + uint64 earnedQSC; + }; + + struct STARAndQSC + { + uint64 volumeOfSTAR; + uint64 volumeOfQSC; + }; + struct EarnedQSCInfo + { + id proposer; + uint32 epoch; + bool operator==(const EarnedQSCInfo& other) const + { + return proposer == other.proposer && epoch == other.epoch; + } + }; + struct VoteInfo + { + id voter; + uint64 gameIndex; + + bool operator==(const VoteInfo& other) const + { + return voter == other.voter && gameIndex == other.gameIndex; + } + }; + //---------------------------------------------------------------------------- + // Define state + struct StateData + { + HashMap userAssetVolume; + HashMap gameList; + HashMap failedGameList; + HashMap voteList; + HashMap userDailyClaimedBonus; + HashMap userEarnedQSCInfo; + id LPDividendsAddress; + id CCFDividendsAddress; + id treasuryAddress; + id QSTIssuer; + uint64 QSCCirclatingSupply; + uint64 STARCirclatingSupply; + uint64 burntSTAR; + uint64 epochRevenue; + uint64 maxGameIndex; + uint64 QSTAssetName; + uint64 bonusAmount; + sint64 transferRightsFee; + uint32 lastClaimedTime; + }; +protected: + /**************************************/ + /************UTIL FUNCTIONS************/ + /**************************************/ + inline static uint32 divUp(uint32 a, uint32 b) + { + return div((a + b - 1), b); + } + inline static uint64 divUp(uint64 a, uint64 b) + { + return div((a + b - 1), b); + } + inline static sint32 min(sint32 a, sint32 b) + { + return (a < b) ? a : b; + } + + /** + * Compare 2 date in uint32 format + * @return -1 lesser(ealier) AB + */ + inline static sint32 dateCompare(uint32& A, uint32& B, sint32& i) + { + if (A == B) return 0; + if (A < B) return -1; + return 1; + } + /** + * @return pack qusino datetime data from year, month, day, hour, minute, second to a uint32 + * year is counted from 24 (2024) + */ + inline static void packQusinoDate(uint32 _year, uint32 _month, uint32 _day, uint32 _hour, uint32 _minute, uint32 _second, uint32& res) + { + res = ((_year - 24) << 26) | (_month << 22) | (_day << 17) | (_hour << 12) | (_minute << 6) | (_second); + } + + inline static uint32 qusinoGetYear(uint32 data) + { + return ((data >> 26) + 24); + } + inline static uint32 qusinoGetMonth(uint32 data) + { + return ((data >> 22) & 0b1111); + } + inline static uint32 qusinoGetDay(uint32 data) + { + return ((data >> 17) & 0b11111); + } + inline static uint32 qusinoGetHour(uint32 data) + { + return ((data >> 12) & 0b11111); + } + inline static uint32 qusinoGetMinute(uint32 data) + { + return ((data >> 6) & 0b111111); + } + inline static uint32 qusinoGetSecond(uint32 data) + { + return (data & 0b111111); + } + /* + * @return unpack qusino datetime from uin32 to year, month, day, hour, minute, secon + */ + inline static void unpackQusinoDate(uint8& _year, uint8& _month, uint8& _day, uint8& _hour, uint8& _minute, uint8& _second, uint32 data) + { + _year = qusinoGetYear(data); // 6 bits + _month = qusinoGetMonth(data); //4bits + _day = qusinoGetDay(data); //5bits + _hour = qusinoGetHour(data); //5bits + _minute = qusinoGetMinute(data); //6bits + _second = qusinoGetSecond(data); //6bits + } + inline static void accumulatedDay(sint32 month, uint64& res) + { + switch (month) + { + case 1: res = 0; break; + case 2: res = 31; break; + case 3: res = 59; break; + case 4: res = 90; break; + case 5: res = 120; break; + case 6: res = 151; break; + case 7: res = 181; break; + case 8: res = 212; break; + case 9: res = 243; break; + case 10:res = 273; break; + case 11:res = 304; break; + case 12:res = 334; break; + } + } + /** + * @return difference in number of second, A must be smaller than or equal B to have valid value + */ + inline static void diffQusinoDateInSecond(uint32& A, uint32& B, sint32& i, uint64& dayA, uint64& dayB, uint64& res) + { + if (dateCompare(A, B, i) >= 0) + { + res = 0; + return; + } + accumulatedDay(qusinoGetMonth(A), dayA); + dayA += qusinoGetDay(A); + accumulatedDay(qusinoGetMonth(B), dayB); + dayB += (qusinoGetYear(B) - qusinoGetYear(A)) * 365ULL + qusinoGetDay(B); + + // handling leap-year: only store last 2 digits of year here, don't care about mod 100 & mod 400 case + for (i = qusinoGetYear(A); (uint32)(i) < qusinoGetYear(B); i++) + { + if (mod(i, 4) == 0) + { + dayB++; + } + } + if (mod(sint32(qusinoGetYear(A)), 4) == 0 && (qusinoGetMonth(A) > 2)) dayA++; + if (mod(sint32(qusinoGetYear(B)), 4) == 0 && (qusinoGetMonth(B) > 2)) dayB++; + res = (dayB - dayA) * 3600ULL * 24; + res += (qusinoGetHour(B) * 3600 + qusinoGetMinute(B) * 60 + qusinoGetSecond(B)); + res -= (qusinoGetHour(A) * 3600 + qusinoGetMinute(A) * 60 + qusinoGetSecond(A)); + } + inline static bool checkValidQusinoDateTime(uint32& A) + { + if (qusinoGetMonth(A) > 12) return false; + if (qusinoGetDay(A) > 31) return false; + if ((qusinoGetDay(A) == 31) && + (qusinoGetMonth(A) != 1) && (qusinoGetMonth(A) != 3) && (qusinoGetMonth(A) != 5) && + (qusinoGetMonth(A) != 7) && (qusinoGetMonth(A) != 8) && (qusinoGetMonth(A) != 10) && (qusinoGetMonth(A) != 12)) return false; + if ((qusinoGetDay(A) == 30) && (qusinoGetMonth(A) == 2)) return false; + if ((qusinoGetDay(A) == 29) && (qusinoGetMonth(A) == 2) && (mod(qusinoGetYear(A), 4u) != 0)) return false; + if (qusinoGetHour(A) >= 24) return false; + if (qusinoGetMinute(A) >= 60) return false; + if (qusinoGetSecond(A) >= 60) return false; + return true; + } + +public: + //---------------------------------------------------------------------------- + // Define user procedures and functions (with input and output) + struct earnSTAR_locals + { + STARAndQSC user; + QUSINOLogger log; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(earnSTAR) + { + if (input.amount * QUSINO_STAR_PRICE * 100 > (uint32)qpi.invocationReward()) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.returnCode = QUSINO_INSUFFICIENT_FUNDS; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_INSUFFICIENT_FUNDS, 0 }; + LOG_INFO(locals.log); + return ; + } + if (input.amount * QUSINO_STAR_PRICE * 100 < (uint32)qpi.invocationReward()) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (input.amount * QUSINO_STAR_PRICE * 100)); + } + state.get().userAssetVolume.get(qpi.invocator(), locals.user); + locals.user.volumeOfSTAR += input.amount * 100; + locals.user.volumeOfQSC += input.amount; + state.mut().userAssetVolume.set(qpi.invocator(), locals.user); + state.mut().STARCirclatingSupply += input.amount * 100; + state.mut().QSCCirclatingSupply += input.amount; + output.returnCode = QUSINO_SUCCESS; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_SUCCESS, 0 }; + LOG_INFO(locals.log); + } + struct transferSTAROrQSC_locals + { + STARAndQSC dest, sender; + QUSINOLogger log; + sint64 idx; + GameInfo game; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(transferSTAROrQSC) + { + if (input.type != QUSINO_ASSET_TYPE_STAR && input.type != QUSINO_ASSET_TYPE_QSC) + { + output.returnCode = QUSINO_WRONG_ASSET_TYPE; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_WRONG_ASSET_TYPE, 0 }; + LOG_INFO(locals.log); + return; + } + locals.idx = state.get().gameList.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) + { + locals.game = state.get().gameList.value(locals.idx); + if (locals.game.proposer == qpi.invocator()) + { + output.returnCode = QUSINO_INVALID_GAME_PROPOSER; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_INVALID_GAME_PROPOSER, 0 }; + LOG_INFO(locals.log); + return; + } + locals.idx = state.get().gameList.nextElementIndex(locals.idx); + } + state.get().userAssetVolume.get(qpi.invocator(), locals.sender); + state.get().userAssetVolume.get(input.dest, locals.dest); + if (input.type == QUSINO_ASSET_TYPE_STAR) + { + if (locals.sender.volumeOfSTAR < input.amount) + { + output.returnCode = QUSINO_INSUFFICIENT_STAR; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_INSUFFICIENT_STAR, 0 }; + LOG_INFO(locals.log); + return; + } + locals.sender.volumeOfSTAR -= input.amount; + locals.dest.volumeOfSTAR += input.amount; + } + else if (input.type == QUSINO_ASSET_TYPE_QSC) + { + if (locals.sender.volumeOfQSC < input.amount) + { + output.returnCode = QUSINO_INSUFFICIENT_QSC; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_INSUFFICIENT_QSC, 0 }; + LOG_INFO(locals.log); + return; + } + locals.sender.volumeOfQSC -= input.amount; + locals.dest.volumeOfQSC += input.amount; + } + state.mut().userAssetVolume.set(qpi.invocator(), locals.sender); + state.mut().userAssetVolume.set(input.dest, locals.dest); + output.returnCode = QUSINO_SUCCESS; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_SUCCESS, 0 }; + LOG_INFO(locals.log); + } + + struct submitGame_locals + { + GameInfo newGame; + QUSINOLogger log; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(submitGame) + { + if (qpi.invocationReward() < QUSINO_GAME_SUBMIT_FEE) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.returnCode = QUSINO_INSUFFICIENT_FUNDS; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_INSUFFICIENT_FUNDS, 0 }; + LOG_INFO(locals.log); + return ; + } + if (qpi.invocationReward() > QUSINO_GAME_SUBMIT_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - QUSINO_GAME_SUBMIT_FEE); + } + qpi.distributeDividends(div(QUSINO_GAME_SUBMIT_FEE, 676 * 10ULL)); + state.mut().epochRevenue += QUSINO_GAME_SUBMIT_FEE - div(QUSINO_GAME_SUBMIT_FEE, 676 * 10ULL) * 676; + locals.newGame.proposedEpoch = qpi.epoch(); + locals.newGame.proposer = qpi.invocator(); + copyMemory(locals.newGame.URI, input.URI); + locals.newGame.yesVotes = 0; + locals.newGame.noVotes = 0; + state.mut().gameList.set(state.mut().maxGameIndex, locals.newGame); + state.mut().maxGameIndex++; + output.returnCode = QUSINO_SUCCESS; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_SUCCESS, 0 }; + LOG_INFO(locals.log); + } + + struct voteInGameProposal_locals + { + STARAndQSC userVolume; + VoteInfo voteInfo; + GameInfo game; + uint32 i; + uint8 voteStatus; + QUSINOLogger log; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(voteInGameProposal) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + state.get().userAssetVolume.get(qpi.invocator(), locals.userVolume); + if (locals.userVolume.volumeOfSTAR < QUSINO_VOTE_FEE) + { + output.returnCode = QUSINO_INSUFFICIENT_VOTE_FEE; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_INSUFFICIENT_VOTE_FEE, 0 }; + LOG_INFO(locals.log); + return ; + } + state.get().gameList.get(input.gameIndex, locals.game); + if (locals.game.proposedEpoch != qpi.epoch() && locals.game.proposedEpoch + QUSINO_REVOTE_DURATION != qpi.epoch()) + { + output.returnCode = QUSINO_NOT_VOTE_TIME; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_NOT_VOTE_TIME, 0 }; + LOG_INFO(locals.log); + return ; + } + for (locals.i = 0; locals.i < 64; locals.i++) + { + if (locals.game.URI.get(locals.i) != input.URI.get(locals.i)) + { + output.returnCode = QUSINO_WRONG_GAME_URI_FOR_VOTE; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_WRONG_GAME_URI, 0 }; + LOG_INFO(locals.log); + return ; + } + } + locals.voteInfo.voter = qpi.invocator(); + locals.voteInfo.gameIndex = input.gameIndex; + state.get().voteList.get(locals.voteInfo, locals.voteStatus); + if (locals.voteStatus && input.yesNo == locals.voteStatus) + { + output.returnCode = QUSINO_ALREADY_VOTED_WITH_SAME_VOTE; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_ALREADY_VOTED_WITH_SAME_VOTE, 0 }; + LOG_INFO(locals.log); + return ; + } + if (locals.voteStatus) + { + if (input.yesNo == 1) + { + locals.game.yesVotes++; + locals.game.noVotes--; + } + else if (input.yesNo == 2) + { + locals.game.yesVotes--; + locals.game.noVotes++; + } + } + else + { + if (input.yesNo == 1) + { + locals.game.yesVotes++; + } + else if (input.yesNo == 2) + { + locals.game.noVotes++; + } + } + locals.userVolume.volumeOfSTAR -= QUSINO_VOTE_FEE; + state.mut().burntSTAR += QUSINO_VOTE_FEE; + state.mut().STARCirclatingSupply -= QUSINO_VOTE_FEE; + state.mut().userAssetVolume.set(qpi.invocator(), locals.userVolume); + + locals.voteStatus = input.yesNo; + state.mut().gameList.set(input.gameIndex, locals.game); + state.mut().voteList.set(locals.voteInfo, locals.voteStatus); + output.returnCode = QUSINO_SUCCESS; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_SUCCESS, 0 }; + LOG_INFO(locals.log); + } + + struct depositBonus_locals + { + QUSINOLogger log; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(depositBonus) + { + state.mut().bonusAmount += qpi.invocationReward(); + output.returnCode = QUSINO_SUCCESS; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_SUCCESS, 0 }; + LOG_INFO(locals.log); + } + + struct dailyClaimBonus_locals + { + STARAndQSC userVolume; + uint32 lastClaimedTime; + uint32 curDate; + sint32 i; + uint64 diffTime, dayA, dayB; + QUSINOLogger log; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(dailyClaimBonus) + { + packQusinoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); + state.get().userDailyClaimedBonus.get(qpi.invocator(), locals.lastClaimedTime); + diffQusinoDateInSecond(locals.lastClaimedTime, locals.curDate, locals.i, locals.dayA, locals.dayB, locals.diffTime); + if (locals.lastClaimedTime && locals.diffTime < QUSINO_DAILY_CLAIM_BONUS_DURATION) + { + output.returnCode = QUSINO_ALREADY_CLAIMED_TODAY; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_ALREADY_CLAIMED_TODAY, 0 }; + LOG_INFO(locals.log); + return ; + } + locals.lastClaimedTime = state.get().lastClaimedTime; + diffQusinoDateInSecond(locals.lastClaimedTime, locals.curDate, locals.i, locals.dayA, locals.dayB, locals.diffTime); + if (locals.lastClaimedTime && locals.diffTime < QUSINO_BONUS_CLAIM_DURATION) + { + output.returnCode = QUSINO_BONUS_CLAIM_TIME_NOT_COME; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_BONUS_CLAIM_TIME_NOT_COME, 0 }; + LOG_INFO(locals.log); + return ; + } + if (state.get().bonusAmount < QUSINO_BONUS_CLAIM_AMOUNT) + { + output.returnCode = QUSINO_INSUFFICIENT_BONUS_AMOUNT; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_INSUFFICIENT_BONUS_AMOUNT, 0 }; + LOG_INFO(locals.log); + return ; + } + state.mut().bonusAmount -= QUSINO_BONUS_CLAIM_AMOUNT; + state.get().userAssetVolume.get(qpi.invocator(), locals.userVolume); + locals.userVolume.volumeOfSTAR += QUSINO_BONUS_CLAIM_AMOUNT_STAR; + locals.userVolume.volumeOfQSC += QUSINO_BONUS_CLAIM_AMOUNT_QSC; + state.mut().userAssetVolume.set(qpi.invocator(), locals.userVolume); + state.mut().STARCirclatingSupply += QUSINO_BONUS_CLAIM_AMOUNT_STAR; + state.mut().QSCCirclatingSupply += QUSINO_BONUS_CLAIM_AMOUNT_QSC; + state.mut().lastClaimedTime = locals.curDate; + state.mut().userDailyClaimedBonus.set(qpi.invocator(), locals.curDate); + state.mut().epochRevenue += QUSINO_BONUS_CLAIM_AMOUNT_STAR; // 100STAR is 100Qubic for each bonus claim + output.returnCode = QUSINO_SUCCESS; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_SUCCESS, 0 }; + LOG_INFO(locals.log); + } + + struct redemptionQSCToQubic_locals + { + STARAndQSC userVolume; + QUSINOLogger log; + sint64 idx; + GameInfo game; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(redemptionQSCToQubic) + { + locals.idx = state.get().gameList.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) + { + locals.game = state.get().gameList.value(locals.idx); + if (locals.game.proposer == qpi.invocator()) + { + output.returnCode = QUSINO_INVALID_GAME_PROPOSER; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_INVALID_GAME_PROPOSER, 0 }; + LOG_INFO(locals.log); + return; + } + locals.idx = state.get().gameList.nextElementIndex(locals.idx); + } + state.get().userAssetVolume.get(qpi.invocator(), locals.userVolume); + if (locals.userVolume.volumeOfQSC < input.amount) + { + output.returnCode = QUSINO_INSUFFICIENT_QSC; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_INSUFFICIENT_QSC, 0 }; + LOG_INFO(locals.log); + return; + } + if (qpi.transfer(qpi.invocator(), input.amount * QUSINO_QSC_PRICE) < 0) + { + output.returnCode = QUSINO_INSUFFICIENT_FUNDS; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_INSUFFICIENT_FUNDS, 0 }; + LOG_INFO(locals.log); + return; + } + locals.userVolume.volumeOfQSC -= input.amount; + state.mut().userAssetVolume.set(qpi.invocator(), locals.userVolume); + state.mut().QSCCirclatingSupply -= input.amount; + output.returnCode = QUSINO_SUCCESS; + locals.log = QUSINOLogger{ CONTRACT_INDEX, QUSINO_LOG_SUCCESS, 0 }; + LOG_INFO(locals.log); + } + struct getUserAssetVolume_locals + { + STARAndQSC userAsset; + }; + PUBLIC_FUNCTION_WITH_LOCALS(getUserAssetVolume) + { + state.get().userAssetVolume.get(input.user, locals.userAsset); + output.QSCAmount = locals.userAsset.volumeOfQSC; + output.STARAmount = locals.userAsset.volumeOfSTAR; + } + + struct getFailedGameList_locals + { + GameInfo game; + sint64 idx; + uint32 cur; + }; + PUBLIC_FUNCTION_WITH_LOCALS(getFailedGameList) + { + if (input.offset + 32 >= 1024) + { + return ; + } + locals.cur = 0; + locals.idx = state.get().failedGameList.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) + { + if (locals.cur >= input.offset) + { + if (locals.cur >= input.offset + 32) + { + return ; + } + locals.game = state.get().failedGameList.value(locals.idx); + output.games.set(locals.cur - input.offset, locals.game); + } + locals.cur++; + locals.idx = state.get().failedGameList.nextElementIndex(locals.idx); + } + } + + PUBLIC_FUNCTION(getSCInfo) + { + output.QSCCirclatingSupply = state.get().QSCCirclatingSupply; + output.STARCirclatingSupply = state.get().STARCirclatingSupply; + output.burntSTAR = state.get().burntSTAR; + output.epochRevenue = state.get().epochRevenue; + output.maxGameIndex = state.get().maxGameIndex; + output.bonusAmount = state.get().bonusAmount; + } + + struct getActiveGameList_locals + { + GameInfo game; + sint64 idx; + sint32 cur; + }; + PUBLIC_FUNCTION_WITH_LOCALS(getActiveGameList) + { + if (input.offset + 32 >= QUSINO_MAX_NUMBER_OF_GAMES) + { + return ; + } + locals.cur = 0; + locals.idx = state.get().gameList.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) + { + if (locals.cur >= (sint32)input.offset) + { + if (locals.cur >= (sint32)(input.offset + 32)) + { + return ; + } + locals.game = state.get().gameList.value(locals.idx); + output.games.set(locals.cur - input.offset, locals.game); + output.gameIndexes.set(locals.cur - input.offset, state.get().gameList.key(locals.idx)); + } + locals.cur++; + locals.idx = state.get().gameList.nextElementIndex(locals.idx); + } + } + + PUBLIC_PROCEDURE(TransferShareManagementRights) + { + if (qpi.invocationReward() < state.get().transferRightsFee) + { + return ; + } + + if (qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer,qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.numberOfShares) + { + // not enough shares available + output.transferredNumberOfShares = 0; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + else + { + if (qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, + input.newManagingContractIndex, input.newManagingContractIndex, state.get().transferRightsFee) < 0) + { + // error + output.transferredNumberOfShares = 0; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + else + { + // success + output.transferredNumberOfShares = input.numberOfShares; + if (qpi.invocationReward() > state.get().transferRightsFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.get().transferRightsFee); + } + } + } + } + + struct getProposerEarnedQSCInfo_locals + { + EarnedQSCInfo earnedQSCInfo; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getProposerEarnedQSCInfo) + { + locals.earnedQSCInfo.proposer = input.proposer; + locals.earnedQSCInfo.epoch = input.epoch; + state.get().userEarnedQSCInfo.get(locals.earnedQSCInfo, output.earnedQSC); + } + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_FUNCTION(getUserAssetVolume, 1); + REGISTER_USER_FUNCTION(getFailedGameList, 2); + REGISTER_USER_FUNCTION(getSCInfo, 3); + REGISTER_USER_FUNCTION(getActiveGameList, 4); + REGISTER_USER_FUNCTION(getProposerEarnedQSCInfo, 5); + + REGISTER_USER_PROCEDURE(earnSTAR, 1); + REGISTER_USER_PROCEDURE(transferSTAROrQSC, 2); + REGISTER_USER_PROCEDURE(submitGame, 3); + REGISTER_USER_PROCEDURE(voteInGameProposal, 4); + REGISTER_USER_PROCEDURE(TransferShareManagementRights, 5); + REGISTER_USER_PROCEDURE(depositBonus, 6); + REGISTER_USER_PROCEDURE(dailyClaimBonus, 7); + REGISTER_USER_PROCEDURE(redemptionQSCToQubic, 8); + } + + INITIALIZE() + { + state.mut().maxGameIndex = 1; + state.mut().transferRightsFee = 100; + state.mut().LPDividendsAddress = ID(_V, _D, _I, _H, _Y, _F, _G, _B, _J, _Z, _P, _V, _V, _F, _O, _R, _Y, _Q, _V, _O, _I, _D, _U, _P, _S, _I, _H, _C, _B, _D, _K, _B, _K, _Y, _J, _V, _X, _L, _P, _Q, _W, _D, _A, _K, _L, _D, _M, _K, _A, _G, _G, _P, _O, _C, _Y, _G); + state.mut().CCFDividendsAddress = id(CCF_CONTRACT_INDEX, 0, 0, 0); + state.mut().treasuryAddress = ID(_B, _Z, _X, _I, _A, _E, _X, _W, _R, _S, _X, _M, _C, _A, _W, _A, _N, _G, _V, _Y, _T, _W, _D, _A, _U, _E, _I, _A, _D, _F, _N, _O, _F, _C, _K, _G, _X, _V, _Q, _M, _P, _C, _K, _U, _H, _S, _M, _L, _F, _E, _E, _B, _E, _P, _C, _C); + state.mut().QSTAssetName = 5526353; + state.mut().QSTIssuer = ID(_Q, _M, _H, _J, _N, _L, _M, _Q, _R, _I, _B, _I, _R, _E, _F, _I, _W, _V, _K, _Y, _Q, _E, _L, _B, _F, _A, _R, _B, _T, _D, _N, _Y, _K, _I, _O, _B, _O, _F, _F, _Y, _F, _G, _J, _Y, _Z, _S, _X, _J, _B, _V, _G, _B, _S, _U, _Q, _G); + } + + struct END_EPOCH_locals + { + STARAndQSC userVolume; + GameInfo game; + uint64 QSTDividends; + sint64 idx; + AssetPossessionIterator iter; + Asset QSTAsset; + EarnedQSCInfo earnedQSCInfo; + }; + END_EPOCH_WITH_LOCALS() + { + state.mut().failedGameList.reset(); + locals.idx = state.get().gameList.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) + { + locals.game = state.get().gameList.value(locals.idx); + if (locals.game.noVotes >= locals.game.yesVotes) + { + if (locals.game.proposedEpoch == qpi.epoch() || locals.game.proposedEpoch + QUSINO_REVOTE_DURATION == qpi.epoch()) + { + state.mut().failedGameList.set(state.get().gameList.key(locals.idx), locals.game); + state.mut().gameList.removeByIndex(locals.idx); + locals.idx = state.get().gameList.nextElementIndex(locals.idx); + continue; + } + } + // distribute QSC to the proposer + state.get().userAssetVolume.get(locals.game.proposer, locals.userVolume); + state.mut().epochRevenue += div(locals.userVolume.volumeOfQSC * QUSINO_QSC_PRICE * (1000 - QUSINO_DEVELOPER_FEE) * 1ULL, 1000ULL); + qpi.transfer(locals.game.proposer, locals.userVolume.volumeOfQSC * QUSINO_QSC_PRICE - div(locals.userVolume.volumeOfQSC * QUSINO_QSC_PRICE * (1000 - QUSINO_DEVELOPER_FEE) * 1ULL, 1000ULL)); + state.mut().QSCCirclatingSupply -= locals.userVolume.volumeOfQSC; + + // add earned QSC to userEarnedQSCInfo + locals.earnedQSCInfo.proposer = locals.game.proposer; + locals.earnedQSCInfo.epoch = qpi.epoch(); + state.mut().userEarnedQSCInfo.set(locals.earnedQSCInfo, locals.userVolume.volumeOfQSC); + + // set userVolume to 0 + locals.userVolume.volumeOfQSC = 0; + state.mut().userAssetVolume.set(locals.game.proposer, locals.userVolume); + + // remove game from gameList + state.mut().gameList.removeByIndex(locals.idx); + locals.idx = state.get().gameList.nextElementIndex(locals.idx); + } + state.mut().gameList.cleanupIfNeeded(); + state.mut().voteList.reset(); + + locals.idx = state.get().userAssetVolume.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) + { + locals.userVolume = state.get().userAssetVolume.value(locals.idx); + if (locals.userVolume.volumeOfSTAR == 0 && locals.userVolume.volumeOfQSC == 0) + { + state.mut().userAssetVolume.removeByIndex(locals.idx); + } + locals.idx = state.get().userAssetVolume.nextElementIndex(locals.idx); + } + state.mut().userAssetVolume.cleanupIfNeeded(); + + qpi.transfer(state.get().LPDividendsAddress, div(state.get().epochRevenue * QUSINO_LP_DIVIDENDS_PERCENT * 1ULL, 100ULL)); + qpi.transfer(state.get().CCFDividendsAddress, div(state.get().epochRevenue * QUSINO_CCF_DIVIDENDS_PERCENT * 1ULL, 100ULL)); + qpi.transfer(state.get().treasuryAddress, div(state.get().epochRevenue * QUSINO_TREASURY_DIVIDENDS_PERCENT * 1ULL, 100ULL)); + qpi.distributeDividends(div(state.get().epochRevenue * QUSINO_SHAREHOLDERS_DIVIDENDS_PERCENT * 1ULL, 67600ULL)); + locals.QSTAsset.assetName = state.get().QSTAssetName; + locals.QSTAsset.issuer = state.get().QSTIssuer; + locals.iter.begin(locals.QSTAsset); + while (!locals.iter.reachedEnd()) + { + qpi.transfer(locals.iter.possessor(), div(state.get().epochRevenue * QUSINO_QST_HOLDERS_DIVIDENDS_PERCENT * 1ULL, QUSINO_SUPPLY_OF_QST * 1000ULL) * locals.iter.numberOfPossessedShares()); + locals.QSTDividends += div(state.get().epochRevenue * QUSINO_QST_HOLDERS_DIVIDENDS_PERCENT * 1ULL, QUSINO_SUPPLY_OF_QST * 1000ULL) * locals.iter.numberOfPossessedShares(); + locals.iter.next(); + } + state.mut().epochRevenue -= div(state.get().epochRevenue * QUSINO_LP_DIVIDENDS_PERCENT * 1ULL, 100ULL) + div(state.get().epochRevenue * QUSINO_CCF_DIVIDENDS_PERCENT * 1ULL, 100ULL) + div(state.get().epochRevenue * QUSINO_TREASURY_DIVIDENDS_PERCENT * 1ULL, 100ULL) + (div(state.get().epochRevenue * QUSINO_SHAREHOLDERS_DIVIDENDS_PERCENT * 1ULL, 67600ULL) * 676) + locals.QSTDividends; + } + + PRE_ACQUIRE_SHARES() + { + output.allowTransfer = true; + } +}; diff --git a/test/contract_qusino.cpp b/test/contract_qusino.cpp new file mode 100644 index 000000000..c2af3b0cd --- /dev/null +++ b/test/contract_qusino.cpp @@ -0,0 +1,797 @@ +#define NO_UEFI + +#include "contract_testing.h" + +static constexpr uint64 QUSINO_ISSUE_ASSET_FEE = 1000000000ull; +static constexpr uint64 QUSINO_TRANSFER_ASSET_FEE = 100ull; +static constexpr uint64 QUSINO_TRANSFER_RIGHTS_FEE = 100ull; + +static const id QUSINO_CONTRACT_ID(QUSINO_CONTRACT_INDEX, 0, 0, 0); + +const id QUSINO_testUser1 = ID(_U, _S, _E, _R, _A, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y); +const id QUSINO_testUser2 = ID(_U, _S, _E, _R, _B, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y); +const id QUSINO_testUser3 = ID(_U, _S, _E, _R, _C, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y); +const id QUSINO_QSTIssuer = ID(_Q, _M, _H, _J, _N, _L, _M, _Q, _R, _I, _B, _I, _R, _E, _F, _I, _W, _V, _K, _Y, _Q, _E, _L, _B, _F, _A, _R, _B, _T, _D, _N, _Y, _K, _I, _O, _B, _O, _F, _F, _Y, _F, _G, _J, _Y, _Z, _S, _X, _J, _B, _V, _G, _B, _S, _U, _Q, _G); + +class QUSINOChecker : public QUSINO +{ +public: + void checkSCInfo(const QUSINO::getSCInfo_output& output, uint64 expectedQSC, uint64 expectedSTAR, uint64 expectedBurntSTAR, uint64 expectedEpochRevenue, uint64 expectedMaxGameIndex, uint64 expectedBonusAmount) + { + EXPECT_EQ(output.QSCCirclatingSupply, expectedQSC); + EXPECT_EQ(output.STARCirclatingSupply, expectedSTAR); + EXPECT_EQ(output.burntSTAR, expectedBurntSTAR); + EXPECT_EQ(output.epochRevenue, expectedEpochRevenue); + EXPECT_EQ(output.maxGameIndex, expectedMaxGameIndex); + EXPECT_EQ(output.bonusAmount, expectedBonusAmount); + } +}; + +class ContractTestingQUSINO : protected ContractTesting +{ +public: + ContractTestingQUSINO() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(QUSINO); + callSystemProcedure(QUSINO_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QX); + callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); + } + + QUSINOChecker* getState() + { + return (QUSINOChecker*)contractStates[QUSINO_CONTRACT_INDEX]; + } + + void endEpoch(bool expectSuccess = true) + { + callSystemProcedure(QUSINO_CONTRACT_INDEX, END_EPOCH, expectSuccess); + } + + sint64 issueAsset(const id& issuer, uint64 assetName, uint64 numberOfShares) + { + QX::IssueAsset_input input; + input.assetName = assetName; + input.numberOfShares = numberOfShares; + input.unitOfMeasurement = 0; + input.numberOfDecimalPlaces = 0; + QX::IssueAsset_output output; + invokeUserProcedure(QX_CONTRACT_INDEX, 1, input, output, issuer, QUSINO_ISSUE_ASSET_FEE); + return output.issuedNumberOfShares; + } + + sint64 transferAsset(const id& from, const id& to, uint64 assetName, const id& issuer, uint64 numberOfShares) + { + QX::TransferShareOwnershipAndPossession_input input; + input.assetName = assetName; + input.issuer = issuer; + input.newOwnerAndPossessor = to; + input.numberOfShares = numberOfShares; + QX::TransferShareOwnershipAndPossession_output output; + invokeUserProcedure(QX_CONTRACT_INDEX, 2, input, output, from, QUSINO_TRANSFER_ASSET_FEE); + return output.transferredNumberOfShares; + } + + sint64 transferShareManagementRightsQX(const id& invocator, const Asset& asset, sint64 numberOfShares, uint32 newManagingContractIndex, sint64 fee) + { + QX::TransferShareManagementRights_input input; + input.asset.assetName = asset.assetName; + input.asset.issuer = asset.issuer; + input.numberOfShares = numberOfShares; + input.newManagingContractIndex = newManagingContractIndex; + QX::TransferShareManagementRights_output output; + invokeUserProcedure(QX_CONTRACT_INDEX, 9, input, output, invocator, fee); + return output.transferredNumberOfShares; + } + + QUSINO::depositBonus_output depositBonus(const id& user, uint64 amount) + { + QUSINO::depositBonus_input input; + input.amount = amount; + QUSINO::depositBonus_output output; + invokeUserProcedure(QUSINO_CONTRACT_INDEX, 6, input, output, user, amount); + return output; + } + + QUSINO::dailyClaimBonus_output dailyClaimBonus(const id& user, sint64 invocationReward) + { + QUSINO::dailyClaimBonus_input input; + QUSINO::dailyClaimBonus_output output; + invokeUserProcedure(QUSINO_CONTRACT_INDEX, 7, input, output, user, invocationReward); + return output; + } + + QUSINO::earnSTAR_output earnSTAR(const id& user, uint64 amount, sint64 invocationReward) + { + QUSINO::earnSTAR_input input; + input.amount = amount; + QUSINO::earnSTAR_output output; + invokeUserProcedure(QUSINO_CONTRACT_INDEX, 1, input, output, user, invocationReward); + return output; + } + + QUSINO::transferSTAROrQSC_output transferSTAROrQSC(const id& user, const id& dest, uint64 amount, uint8 type, sint64 invocationReward) + { + QUSINO::transferSTAROrQSC_input input; + input.dest = dest; + input.amount = amount; + input.type = type; + QUSINO::transferSTAROrQSC_output output; + invokeUserProcedure(QUSINO_CONTRACT_INDEX, 2, input, output, user, invocationReward); + return output; + } + + QUSINO::submitGame_output submitGame(const id& user, const Array& URI, sint64 invocationReward) + { + QUSINO::submitGame_input input; + copyMemory(input.URI, URI); + QUSINO::submitGame_output output; + invokeUserProcedure(QUSINO_CONTRACT_INDEX, 3, input, output, user, invocationReward); + return output; + } + + QUSINO::voteInGameProposal_output voteInGameProposal(const id& user, const Array& URI, uint64 gameIndex, uint8 yesNo, sint64 invocationReward) + { + QUSINO::voteInGameProposal_input input; + copyMemory(input.URI, URI); + input.gameIndex = gameIndex; + input.yesNo = yesNo; + QUSINO::voteInGameProposal_output output; + invokeUserProcedure(QUSINO_CONTRACT_INDEX, 4, input, output, user, invocationReward); + return output; + } + + QUSINO::TransferShareManagementRights_output TransferShareManagementRights(const id& user, const Asset& asset, uint64 numberOfShares, uint32 newManagingContractIndex, sint64 invocationReward) + { + QUSINO::TransferShareManagementRights_input input; + input.asset = asset; + input.numberOfShares = numberOfShares; + input.newManagingContractIndex = newManagingContractIndex; + QUSINO::TransferShareManagementRights_output output; + invokeUserProcedure(QUSINO_CONTRACT_INDEX, 5, input, output, user, invocationReward); + return output; + } + + QUSINO::redemptionQSCToQubic_output redemptionQSCToQubic(const id& user, uint64 amount, sint64 invocationReward) + { + QUSINO::redemptionQSCToQubic_input input; + input.amount = amount; + QUSINO::redemptionQSCToQubic_output output; + invokeUserProcedure(QUSINO_CONTRACT_INDEX, 8, input, output, user, invocationReward); + return output; + } + + QUSINO::getUserAssetVolume_output getUserAssetVolume(const id& user) + { + QUSINO::getUserAssetVolume_input input; + input.user = user; + QUSINO::getUserAssetVolume_output output; + callFunction(QUSINO_CONTRACT_INDEX, 1, input, output); + return output; + } + + QUSINO::getFailedGameList_output getFailedGameList(uint32 offset) + { + QUSINO::getFailedGameList_input input; + input.offset = offset; + QUSINO::getFailedGameList_output output; + callFunction(QUSINO_CONTRACT_INDEX, 2, input, output); + return output; + } + + QUSINO::getSCInfo_output getSCInfo() + { + QUSINO::getSCInfo_input input; + QUSINO::getSCInfo_output output; + callFunction(QUSINO_CONTRACT_INDEX, 3, input, output); + return output; + } + + QUSINO::getActiveGameList_output getActiveGameList(uint32 offset) + { + QUSINO::getActiveGameList_input input; + input.offset = offset; + QUSINO::getActiveGameList_output output; + callFunction(QUSINO_CONTRACT_INDEX, 4, input, output); + return output; + } + + QUSINO::getProposerEarnedQSCInfo_output getProposerEarnedQSCInfo(const id& proposer, uint32 epoch) + { + QUSINO::getProposerEarnedQSCInfo_input input; + input.proposer = proposer; + input.epoch = epoch; + QUSINO::getProposerEarnedQSCInfo_output output; + callFunction(QUSINO_CONTRACT_INDEX, 5, input, output); + return output; + } +}; + +// Helper function to create a URI +Array createURI(const char* str) +{ + Array URI; + uint32 len = 0; + while (str[len] != '\0' && len < 64) len++; + for (uint32 i = 0; i < 64; i++) + { + if (i < len) + URI.set(i, (uint8)str[i]); + else + URI.set(i, 0); + } + return URI; +} + +TEST(ContractQUSINO, earnSTAR_Success) +{ + ContractTestingQUSINO QUSINO; + + id user = QUSINO_testUser1; + uint64 amount = 1000; + sint64 requiredReward = amount * QUSINO_STAR_PRICE * 100; + + increaseEnergy(user, requiredReward); + + QUSINO::earnSTAR_output output = QUSINO.earnSTAR(user, amount, requiredReward); + EXPECT_EQ(output.returnCode, QUSINO_SUCCESS); + + // Check user's STAR amount + QUSINO::getUserAssetVolume_output userVolume = QUSINO.getUserAssetVolume(user); + EXPECT_EQ(userVolume.STARAmount, amount * 100); + + // earnSTAR also grants amount QSC (1:1 with STAR amount in logical units) + EXPECT_EQ(userVolume.QSCAmount, amount); + + // Check SC info + QUSINO::getSCInfo_output scInfo = QUSINO.getSCInfo(); + EXPECT_EQ(scInfo.STARCirclatingSupply, amount * 100); + EXPECT_EQ(scInfo.QSCCirclatingSupply, amount); +} + +TEST(ContractQUSINO, earnSTAR_InsufficientFunds) +{ + ContractTestingQUSINO QUSINO; + + id user = QUSINO_testUser1; + uint64 amount = 1000; + sint64 insufficientReward = amount * QUSINO_STAR_PRICE * 100 - 1; + + increaseEnergy(user, insufficientReward); + + QUSINO::earnSTAR_output output = QUSINO.earnSTAR(user, amount, insufficientReward); + EXPECT_EQ(output.returnCode, QUSINO_INSUFFICIENT_FUNDS); +} + +TEST(ContractQUSINO, transferSTAROrQSC_STAR_Success) +{ + ContractTestingQUSINO QUSINO; + + id sender = QUSINO_testUser1; + id receiver = QUSINO_testUser2; + uint64 amount = 1000; + // amount is in logical STAR units; earnSTAR uses amount*100 internally + sint64 requiredReward = amount * QUSINO_STAR_PRICE * 100; + + // First earn STAR + increaseEnergy(sender, requiredReward); + QUSINO::earnSTAR_output earnOutput = QUSINO.earnSTAR(sender, amount, requiredReward); + EXPECT_EQ(earnOutput.returnCode, QUSINO_SUCCESS); + + // Transfer all earned STAR (amount * 100 units) + increaseEnergy(sender, 1); + QUSINO::transferSTAROrQSC_output output = QUSINO.transferSTAROrQSC(sender, receiver, amount * 100, QUSINO_ASSET_TYPE_STAR, 1); + EXPECT_EQ(output.returnCode, QUSINO_SUCCESS); + + // Check balances + QUSINO::getUserAssetVolume_output senderVolume = QUSINO.getUserAssetVolume(sender); + QUSINO::getUserAssetVolume_output receiverVolume = QUSINO.getUserAssetVolume(receiver); + EXPECT_EQ(senderVolume.STARAmount, 0); + EXPECT_EQ(receiverVolume.STARAmount, amount * 100); +} + +TEST(ContractQUSINO, transferSTAROrQSC_QSC_Success) +{ + ContractTestingQUSINO QUSINO; + + id sender = QUSINO_testUser2; + id receiver = QUSINO_testUser3; + uint64 amount = 5000; + + // Earn STAR (and get equal amount of QSC) for sender + sint64 requiredReward = amount * QUSINO_STAR_PRICE * 100; + increaseEnergy(sender, requiredReward); + QUSINO::earnSTAR_output earnOutput = QUSINO.earnSTAR(sender, amount, requiredReward); + EXPECT_EQ(earnOutput.returnCode, QUSINO_SUCCESS); + + // Transfer QSC from sender to receiver + increaseEnergy(sender, 1); + QUSINO::transferSTAROrQSC_output output = QUSINO.transferSTAROrQSC(sender, receiver, amount, QUSINO_ASSET_TYPE_QSC, 1); + EXPECT_EQ(output.returnCode, QUSINO_SUCCESS); + + // Check balances + QUSINO::getUserAssetVolume_output senderVolume = QUSINO.getUserAssetVolume(sender); + QUSINO::getUserAssetVolume_output receiverVolume = QUSINO.getUserAssetVolume(receiver); + EXPECT_EQ(senderVolume.QSCAmount, 0); + EXPECT_EQ(receiverVolume.QSCAmount, amount); +} + +TEST(ContractQUSINO, transferSTAROrQSC_InvalidGameProposer) +{ + ContractTestingQUSINO QUSINO; + + id proposer = QUSINO_testUser1; + id receiver = QUSINO_testUser2; + Array URI = createURI("https://example.com/game1"); + + // Proposer submits a game (has active game) + increaseEnergy(proposer, QUSINO_GAME_SUBMIT_FEE); + QUSINO::submitGame_output subOut = QUSINO.submitGame(proposer, URI, QUSINO_GAME_SUBMIT_FEE); + EXPECT_EQ(subOut.returnCode, QUSINO_SUCCESS); + + // Proposer earns STAR and QSC + uint64 amount = 1000; + sint64 requiredReward = amount * QUSINO_STAR_PRICE * 100; + increaseEnergy(proposer, requiredReward); + QUSINO::earnSTAR_output earnOut = QUSINO.earnSTAR(proposer, amount, requiredReward); + EXPECT_EQ(earnOut.returnCode, QUSINO_SUCCESS); + + // Proposer cannot transfer while they have an active game proposal + increaseEnergy(proposer, 1); + QUSINO::transferSTAROrQSC_output output = QUSINO.transferSTAROrQSC(proposer, receiver, amount, QUSINO_ASSET_TYPE_QSC, 1); + EXPECT_EQ(output.returnCode, QUSINO_INVALID_GAME_PROPOSER); +} + +TEST(ContractQUSINO, transferSTAROrQSC_InsufficientSTAR) +{ + ContractTestingQUSINO QUSINO; + + id sender = QUSINO_testUser1; + id receiver = QUSINO_testUser2; + uint64 amount = 1000; + + increaseEnergy(sender, 1); + QUSINO::transferSTAROrQSC_output output = QUSINO.transferSTAROrQSC(sender, receiver, amount, QUSINO_ASSET_TYPE_STAR, 1); + EXPECT_EQ(output.returnCode, QUSINO_INSUFFICIENT_STAR); +} + +TEST(ContractQUSINO, transferSTAROrQSC_InsufficientQSC) +{ + ContractTestingQUSINO QUSINO; + + id sender = QUSINO_testUser1; + id receiver = QUSINO_testUser2; + uint64 amount = 1000; + + increaseEnergy(sender, 1); + QUSINO::transferSTAROrQSC_output output = QUSINO.transferSTAROrQSC(sender, receiver, amount, QUSINO_ASSET_TYPE_QSC, 1); + EXPECT_EQ(output.returnCode, QUSINO_INSUFFICIENT_QSC); +} + +TEST(ContractQUSINO, submitGame_Success) +{ + ContractTestingQUSINO QUSINO; + + id user = QUSINO_testUser1; + Array URI = createURI("https://example.com/game1"); + sint64 requiredReward = QUSINO_GAME_SUBMIT_FEE; + + increaseEnergy(user, requiredReward); + QUSINO::submitGame_output output = QUSINO.submitGame(user, URI, requiredReward); + EXPECT_EQ(output.returnCode, QUSINO_SUCCESS); + + // Check game was added + QUSINO::getActiveGameList_output gameList = QUSINO.getActiveGameList(0); + EXPECT_EQ(gameList.gameIndexes.get(0), 1); + + // Check SC info + QUSINO::getSCInfo_output scInfo = QUSINO.getSCInfo(); + EXPECT_EQ(scInfo.maxGameIndex, 2); // Starts at 1, so first game is index 1 + uint64 expectedEpochRevenue = QUSINO_GAME_SUBMIT_FEE - div(QUSINO_GAME_SUBMIT_FEE, 676ULL * 10) * 676ULL; + EXPECT_EQ(scInfo.epochRevenue, expectedEpochRevenue); +} + +TEST(ContractQUSINO, submitGame_InsufficientFunds) +{ + ContractTestingQUSINO QUSINO; + + id user = QUSINO_testUser1; + Array URI = createURI("https://example.com/game1"); + sint64 insufficientReward = QUSINO_GAME_SUBMIT_FEE - 1; + + increaseEnergy(user, insufficientReward); + QUSINO::submitGame_output output = QUSINO.submitGame(user, URI, insufficientReward); + EXPECT_EQ(output.returnCode, QUSINO_INSUFFICIENT_FUNDS); +} + +TEST(ContractQUSINO, voteInGameProposal_Success) +{ + ContractTestingQUSINO QUSINO; + + id proposer = QUSINO_testUser1; + id voter = QUSINO_testUser2; + Array URI = createURI("https://example.com/game1"); + + // First submit a game + sint64 requiredReward = QUSINO_GAME_SUBMIT_FEE; + increaseEnergy(proposer, requiredReward); + QUSINO::submitGame_output submitOutput = QUSINO.submitGame(proposer, URI, requiredReward); + EXPECT_EQ(submitOutput.returnCode, QUSINO_SUCCESS); + + // Earn STAR for voting + uint64 starAmount = QUSINO_VOTE_FEE; + sint64 starReward = starAmount * QUSINO_STAR_PRICE * 100; + increaseEnergy(voter, starReward); + QUSINO::earnSTAR_output earnOutput = QUSINO.earnSTAR(voter, starAmount, starReward); + EXPECT_EQ(earnOutput.returnCode, QUSINO_SUCCESS); + + // Vote on the game + increaseEnergy(voter, 1); + QUSINO::getActiveGameList_output gameList = QUSINO.getActiveGameList(0); + uint64 gameIndex = gameList.gameIndexes.get(0); + QUSINO::voteInGameProposal_output voteOutput = QUSINO.voteInGameProposal(voter, URI, gameIndex, 1, 1); + EXPECT_EQ(voteOutput.returnCode, QUSINO_SUCCESS); + + // Check vote was recorded + QUSINO::getActiveGameList_output updatedGameList = QUSINO.getActiveGameList(0); + // Note: We can't directly check votes, but we can verify the game still exists + EXPECT_GT(updatedGameList.gameIndexes.get(0), 0); +} + +TEST(ContractQUSINO, voteInGameProposal_InsufficientVoteFee) +{ + ContractTestingQUSINO QUSINO; + + id proposer = QUSINO_testUser1; + id voter = QUSINO_testUser2; + Array URI = createURI("https://example.com/game1"); + + // Submit a game + sint64 requiredReward = QUSINO_GAME_SUBMIT_FEE; + increaseEnergy(proposer, requiredReward); + QUSINO::submitGame_output submitOutput = QUSINO.submitGame(proposer, URI, requiredReward); + EXPECT_EQ(submitOutput.returnCode, QUSINO_SUCCESS); + + // Try to vote without enough STAR + increaseEnergy(voter, 1); + QUSINO::getActiveGameList_output gameList = QUSINO.getActiveGameList(0); + uint64 gameIndex = gameList.gameIndexes.get(0); + QUSINO::voteInGameProposal_output voteOutput = QUSINO.voteInGameProposal(voter, URI, gameIndex, 1, 1); + EXPECT_EQ(voteOutput.returnCode, QUSINO_INSUFFICIENT_VOTE_FEE); +} + +TEST(ContractQUSINO, voteInGameProposal_WrongGameURI) +{ + ContractTestingQUSINO QUSINO; + + id proposer = QUSINO_testUser1; + id voter = QUSINO_testUser2; + Array URI1 = createURI("https://example.com/game1"); + Array URI2 = createURI("https://example.com/game2"); + + // Submit a game + sint64 requiredReward = QUSINO_GAME_SUBMIT_FEE; + increaseEnergy(proposer, requiredReward); + QUSINO::submitGame_output submitOutput = QUSINO.submitGame(proposer, URI1, requiredReward); + EXPECT_EQ(submitOutput.returnCode, QUSINO_SUCCESS); + + // Earn STAR for voting + uint64 starAmount = QUSINO_VOTE_FEE; + sint64 starReward = starAmount * QUSINO_STAR_PRICE * 100; + increaseEnergy(voter, starReward); + QUSINO::earnSTAR_output earnOutput = QUSINO.earnSTAR(voter, starAmount, starReward); + EXPECT_EQ(earnOutput.returnCode, QUSINO_SUCCESS); + + // Try to vote with wrong URI + increaseEnergy(voter, 1); + QUSINO::getActiveGameList_output gameList = QUSINO.getActiveGameList(0); + uint64 gameIndex = gameList.gameIndexes.get(0); + QUSINO::voteInGameProposal_output voteOutput = QUSINO.voteInGameProposal(voter, URI2, gameIndex, 1, 1); + EXPECT_EQ(voteOutput.returnCode, QUSINO_WRONG_GAME_URI_FOR_VOTE); +} + +TEST(ContractQUSINO, getUserAssetVolume_Empty) +{ + ContractTestingQUSINO QUSINO; + + id user = QUSINO_testUser1; + QUSINO::getUserAssetVolume_output output = QUSINO.getUserAssetVolume(user); + EXPECT_EQ(output.STARAmount, 0); + EXPECT_EQ(output.QSCAmount, 0); +} + +TEST(ContractQUSINO, redemptionQSCToQubic_Success) +{ + ContractTestingQUSINO QUSINO; + + id user = QUSINO_testUser1; + uint64 amount = 1000; + sint64 requiredReward = amount * QUSINO_STAR_PRICE * 100; + increaseEnergy(user, requiredReward); + QUSINO::earnSTAR_output earnOut = QUSINO.earnSTAR(user, amount, requiredReward); + EXPECT_EQ(earnOut.returnCode, QUSINO_SUCCESS); + + uint64 redeemAmount = 500; + QUSINO::redemptionQSCToQubic_output output = QUSINO.redemptionQSCToQubic(user, redeemAmount, 0); + EXPECT_EQ(output.returnCode, QUSINO_SUCCESS); + + QUSINO::getUserAssetVolume_output vol = QUSINO.getUserAssetVolume(user); + EXPECT_EQ(vol.QSCAmount, amount - redeemAmount); + QUSINO::getSCInfo_output scInfo = QUSINO.getSCInfo(); + EXPECT_EQ(scInfo.QSCCirclatingSupply, amount - redeemAmount); +} + +TEST(ContractQUSINO, redemptionQSCToQubic_InsufficientQSC) +{ + ContractTestingQUSINO QUSINO; + + id user = QUSINO_testUser1; + increaseEnergy(user, 1); + QUSINO::redemptionQSCToQubic_output output = QUSINO.redemptionQSCToQubic(user, 100, 0); + EXPECT_EQ(output.returnCode, QUSINO_INSUFFICIENT_QSC); +} + +TEST(ContractQUSINO, redemptionQSCToQubic_InvalidGameProposer) +{ + ContractTestingQUSINO QUSINO; + + id proposer = QUSINO_testUser1; + Array URI = createURI("https://example.com/game1"); + increaseEnergy(proposer, QUSINO_GAME_SUBMIT_FEE); + QUSINO::submitGame_output subOut = QUSINO.submitGame(proposer, URI, QUSINO_GAME_SUBMIT_FEE); + EXPECT_EQ(subOut.returnCode, QUSINO_SUCCESS); + + uint64 amount = 1000; + sint64 requiredReward = amount * QUSINO_STAR_PRICE * 100; + increaseEnergy(proposer, requiredReward); + QUSINO::earnSTAR_output earnOut = QUSINO.earnSTAR(proposer, amount, requiredReward); + EXPECT_EQ(earnOut.returnCode, QUSINO_SUCCESS); + + QUSINO::redemptionQSCToQubic_output output = QUSINO.redemptionQSCToQubic(proposer, 100, 0); + EXPECT_EQ(output.returnCode, QUSINO_INVALID_GAME_PROPOSER); +} + +TEST(ContractQUSINO, END_EPOCH_FailedGameRemoval) +{ + ContractTestingQUSINO QUSINO; + + // issue QST + id qstIssuer = QUSINO_QSTIssuer; + uint64 qstAssetName = 5526353; + uint64 totalShares = QUSINO_SUPPLY_OF_QST; + increaseEnergy(qstIssuer, QUSINO_ISSUE_ASSET_FEE); + EXPECT_EQ(QUSINO.issueAsset(qstIssuer, qstAssetName, totalShares), totalShares); + + id proposer = QUSINO_testUser1; + Array URI = createURI("https://example.com/game1"); + + // Submit a game + sint64 requiredReward = QUSINO_GAME_SUBMIT_FEE; + increaseEnergy(proposer, requiredReward); + QUSINO::submitGame_output submitOutput = QUSINO.submitGame(proposer, URI, requiredReward); + EXPECT_EQ(submitOutput.returnCode, QUSINO_SUCCESS); + + QUSINO::getActiveGameList_output gameList = QUSINO.getActiveGameList(0); + uint64 gameIndex = gameList.gameIndexes.get(0); + + // Vote no to make it fail + id voter1 = QUSINO_testUser2; + uint64 starAmount = QUSINO_VOTE_FEE; + sint64 starReward = starAmount * QUSINO_STAR_PRICE * 100; + increaseEnergy(voter1, starReward); + QUSINO::earnSTAR_output earnOutput1 = QUSINO.earnSTAR(voter1, starAmount, starReward); + EXPECT_EQ(earnOutput1.returnCode, QUSINO_SUCCESS); + + increaseEnergy(voter1, 1); + QUSINO::voteInGameProposal_output voteOutput1 = QUSINO.voteInGameProposal(voter1, URI, gameIndex, 2, 0); + EXPECT_EQ(voteOutput1.returnCode, QUSINO_SUCCESS); + + // End epoch - game should be moved to failed list if no votes >= yes votes + QUSINO.endEpoch(); + ++system.epoch; + + // Check failed game list + QUSINO::getFailedGameList_output failedList = QUSINO.getFailedGameList(0); + // Game should be in failed list +} + +TEST(ContractQUSINO, END_EPOCH_ProposerEarnedQSCInfo) +{ + ContractTestingQUSINO QUSINO; + + // issue QST + id qstIssuer = QUSINO_QSTIssuer; + uint64 qstAssetName = 5526353; + uint64 totalShares = QUSINO_SUPPLY_OF_QST; + increaseEnergy(qstIssuer, QUSINO_ISSUE_ASSET_FEE); + EXPECT_EQ(QUSINO.issueAsset(qstIssuer, qstAssetName, totalShares), totalShares); + + id proposer = QUSINO_testUser1; + id voter = QUSINO_testUser2; + Array URI = createURI("https://example.com/game2"); + + increaseEnergy(proposer, QUSINO_GAME_SUBMIT_FEE); + QUSINO::submitGame_output subOut = QUSINO.submitGame(proposer, URI, QUSINO_GAME_SUBMIT_FEE); + EXPECT_EQ(subOut.returnCode, QUSINO_SUCCESS); + + uint64 qscAmount = 500; + sint64 starReward = qscAmount * QUSINO_STAR_PRICE * 100; + increaseEnergy(proposer, starReward); + QUSINO::earnSTAR_output earnOut = QUSINO.earnSTAR(proposer, qscAmount, starReward); + EXPECT_EQ(earnOut.returnCode, QUSINO_SUCCESS); + + uint32 epochBeforeEnd = system.epoch; + increaseEnergy(voter, QUSINO_VOTE_FEE * QUSINO_STAR_PRICE * 100); + QUSINO::earnSTAR_output voterEarn = QUSINO.earnSTAR(voter, QUSINO_VOTE_FEE, QUSINO_VOTE_FEE * QUSINO_STAR_PRICE * 100); + EXPECT_EQ(voterEarn.returnCode, QUSINO_SUCCESS); + QUSINO::getActiveGameList_output gameList = QUSINO.getActiveGameList(0); + uint64 gameIndex = gameList.gameIndexes.get(0); + QUSINO::voteInGameProposal_output voteOut = QUSINO.voteInGameProposal(voter, URI, gameIndex, 1, 0); + EXPECT_EQ(voteOut.returnCode, QUSINO_SUCCESS); + + QUSINO.endEpoch(); + ++system.epoch; + + QUSINO::getProposerEarnedQSCInfo_output info = QUSINO.getProposerEarnedQSCInfo(proposer, epochBeforeEnd); + EXPECT_EQ(info.earnedQSC, qscAmount); +} + +TEST(ContractQUSINO, depositBonus_Success) +{ + ContractTestingQUSINO QUSINO; + + id user = QUSINO_testUser1; + uint64 amount1 = 1000; + uint64 amount2 = 500; + + // Initial bonusAmount + QUSINO::getSCInfo_output scInfo0 = QUSINO.getSCInfo(); + uint64 initialBonus = scInfo0.bonusAmount; + + // First deposit + increaseEnergy(user, amount1); + QUSINO::depositBonus_output output1 = QUSINO.depositBonus(user, amount1); + EXPECT_EQ(output1.returnCode, QUSINO_SUCCESS); + + QUSINO::getSCInfo_output scInfo1 = QUSINO.getSCInfo(); + EXPECT_EQ(scInfo1.bonusAmount, initialBonus + amount1); + + // Second deposit + increaseEnergy(user, amount2); + QUSINO::depositBonus_output output2 = QUSINO.depositBonus(user, amount2); + EXPECT_EQ(output2.returnCode, QUSINO_SUCCESS); + + QUSINO::getSCInfo_output scInfo2 = QUSINO.getSCInfo(); + EXPECT_EQ(scInfo2.bonusAmount, initialBonus + amount1 + amount2); +} + +TEST(ContractQUSINO, dailyClaimBonus_Success) +{ + ContractTestingQUSINO QUSINO; + + id user = QUSINO_testUser1; + + // Fund bonus pool + uint64 bonusFund = QUSINO_BONUS_CLAIM_AMOUNT * 10; + increaseEnergy(user, bonusFund); + QUSINO::depositBonus_output depOutput = QUSINO.depositBonus(user, bonusFund); + EXPECT_EQ(depOutput.returnCode, QUSINO_SUCCESS); + + // Set current time + setMemory(utcTime, 0); + utcTime.Year = 2024; + utcTime.Month = 1; + utcTime.Day = 1; + utcTime.Hour = 0; + utcTime.Minute = 0; + utcTime.Second = 0; + updateQpiTime(); + + // Snapshot before claim + QUSINO::getSCInfo_output scInfoBefore = QUSINO.getSCInfo(); + QUSINO::getUserAssetVolume_output volBefore = QUSINO.getUserAssetVolume(user); + + // First claim + QUSINO::dailyClaimBonus_output output = QUSINO.dailyClaimBonus(user, 0); + EXPECT_EQ(output.returnCode, QUSINO_SUCCESS); + + // Check user balances + QUSINO::getUserAssetVolume_output volAfter = QUSINO.getUserAssetVolume(user); + EXPECT_EQ(volAfter.STARAmount, volBefore.STARAmount + QUSINO_BONUS_CLAIM_AMOUNT_STAR); + EXPECT_EQ(volAfter.QSCAmount, volBefore.QSCAmount + QUSINO_BONUS_CLAIM_AMOUNT_QSC); + + // Check SC info + QUSINO::getSCInfo_output scInfoAfter = QUSINO.getSCInfo(); + EXPECT_EQ(scInfoAfter.bonusAmount, scInfoBefore.bonusAmount - QUSINO_BONUS_CLAIM_AMOUNT); + EXPECT_EQ(scInfoAfter.STARCirclatingSupply, scInfoBefore.STARCirclatingSupply + QUSINO_BONUS_CLAIM_AMOUNT_STAR); + EXPECT_EQ(scInfoAfter.QSCCirclatingSupply, scInfoBefore.QSCCirclatingSupply + QUSINO_BONUS_CLAIM_AMOUNT_QSC); + EXPECT_EQ(scInfoAfter.epochRevenue, scInfoBefore.epochRevenue + QUSINO_BONUS_CLAIM_AMOUNT_STAR); +} + +TEST(ContractQUSINO, dailyClaimBonus_AlreadyClaimedToday) +{ + ContractTestingQUSINO QUSINO; + + id user = QUSINO_testUser1; + + // Fund bonus pool + uint64 bonusFund = QUSINO_BONUS_CLAIM_AMOUNT * 10; + increaseEnergy(user, bonusFund); + QUSINO::depositBonus_output depOutput = QUSINO.depositBonus(user, bonusFund); + EXPECT_EQ(depOutput.returnCode, QUSINO_SUCCESS); + + // Set current time + setMemory(utcTime, 0); + utcTime.Year = 2024; + utcTime.Month = 1; + utcTime.Day = 1; + utcTime.Hour = 0; + utcTime.Minute = 0; + utcTime.Second = 0; + updateQpiTime(); + + // First claim + QUSINO::dailyClaimBonus_output output1 = QUSINO.dailyClaimBonus(user, 0); + EXPECT_EQ(output1.returnCode, QUSINO_SUCCESS); + + // Second claim on same day should fail + QUSINO::dailyClaimBonus_output output2 = QUSINO.dailyClaimBonus(user, 0); + EXPECT_EQ(output2.returnCode, QUSINO_ALREADY_CLAIMED_TODAY); +} + +TEST(ContractQUSINO, dailyClaimBonus_BonusClaimTimeNotCome) +{ + ContractTestingQUSINO QUSINO; + + id user1 = QUSINO_testUser1; + id user2 = QUSINO_testUser2; + + // Fund bonus pool + uint64 bonusFund = QUSINO_BONUS_CLAIM_AMOUNT * 10; + increaseEnergy(user1, bonusFund); + QUSINO::depositBonus_output depOutput = QUSINO.depositBonus(user1, bonusFund); + EXPECT_EQ(depOutput.returnCode, QUSINO_SUCCESS); + + // Set current time + setMemory(utcTime, 0); + utcTime.Year = 2026; + utcTime.Month = 1; + utcTime.Day = 1; + utcTime.Hour = 0; + utcTime.Minute = 0; + utcTime.Second = 0; + updateQpiTime(); + + // First claim by user1 + QUSINO::dailyClaimBonus_output output1 = QUSINO.dailyClaimBonus(user1, 0); + EXPECT_EQ(output1.returnCode, QUSINO_SUCCESS); + + // Immediate claim by user2 should fail due to global cooldown + increaseEnergy(user2, 1); + QUSINO::dailyClaimBonus_output output2 = QUSINO.dailyClaimBonus(user2, 0); + EXPECT_EQ(output2.returnCode, QUSINO_BONUS_CLAIM_TIME_NOT_COME); +} + +TEST(ContractQUSINO, dailyClaimBonus_InsufficientBonusAmount) +{ + ContractTestingQUSINO QUSINO; + + id user = QUSINO_testUser1; + + // Set current time + setMemory(utcTime, 0); + utcTime.Year = 2026; + utcTime.Month = 1; + utcTime.Day = 1; + utcTime.Hour = 0; + utcTime.Minute = 0; + utcTime.Second = 0; + updateQpiTime(); + + // No bonus deposited -> insufficient bonus amount + increaseEnergy(user, 1); + QUSINO::dailyClaimBonus_output output = QUSINO.dailyClaimBonus(user, 0); + EXPECT_EQ(output.returnCode, QUSINO_INSUFFICIENT_BONUS_AMOUNT); +} diff --git a/test/test.vcxproj b/test/test.vcxproj index 456481944..1607ddd1f 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -153,6 +153,7 @@ + diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index c105bdb5d..866c1e77d 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -32,6 +32,7 @@ +