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 @@
+