From c655210283c3cb5d859b2539138e03e53edfdbf0 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Wed, 20 May 2026 21:58:13 +0100 Subject: [PATCH 01/18] feat: regsiterIn, getRegister for QPortal --- src/contract_core/contract_def.h | 11 ++ src/contracts/QPortal.h | 174 +++++++++++++++++++++++++++++++ test/contract_qportal.cpp | 0 3 files changed, 185 insertions(+) create mode 100644 src/contracts/QPortal.h create mode 100644 test/contract_qportal.cpp diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index ee7c56e6c..0dc5d1cbf 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -292,6 +292,15 @@ #define CONTRACT_STATE2_TYPE ESCROW2 #include "contracts/Escrow.h" +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QPORTAL_CONTRACT_INDEX 28 +#define CONTRACT_INDEX QPORTAL_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QPORTAL +#define CONTRACT_STATE2_TYPE QPORTAL2 +#include "contracts/QPortal.h" // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES @@ -406,6 +415,7 @@ constexpr struct ContractDescription {"VOTTUN", 206, 10000, sizeof(VOTTUNBRIDGE::StateData)}, // proposal in epoch 204, IPO in 205, construction and first use in 206 {"QUSINO", 208, 10000, sizeof(QUSINO::StateData)}, // proposal in epoch 206, IPO in 207, construction and first use in 208 {"ESCROW", 210, 10000, sizeof(ESCROW::StateData)}, // proposal in epoch 208, IPO in 209, construction and first use in 210 + {"QPORTAL", 213, 10000, sizeof(QPORTAL::StateData)}, // proposal in epoch 210, IPO in 211, construction and first use in 212 // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA::StateData)}, @@ -529,6 +539,7 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(VOTTUNBRIDGE); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QUSINO); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(ESCROW); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QPORTAL); // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); diff --git a/src/contracts/QPortal.h b/src/contracts/QPortal.h new file mode 100644 index 000000000..a72b61a46 --- /dev/null +++ b/src/contracts/QPortal.h @@ -0,0 +1,174 @@ +using namespace QPI; +#include "qpi.h" + +constexpr uint64 QPORTAL_PORTAL_ASSET_NAME = 83843471265616; //Portal toekn asset name +constexpr uint32 QPORTAL_REGISTER_AMOUNT = 5; //Amount of portal token to register +constexpr uint32 QPORTAL_MAX_MEMBER = 4096; //Maximum number of members in the portal DAO + +constexpr uint32 QPORTAL_SUCCESS = 0; +constexpr uint32 QPORTAL_INSUFFICIENT_PORTAL = 1; +constexpr uint32 QPORTAL_INVALID_OFFSET_OR_LIMIT = 2; + +struct QPortal2 +{ +}; + +struct QPortal : public ContractBase +{ +public: + struct Logger + { + uint32 _contractIndex; + uint32 _type; + sint8 _terminator; + }; + + struct StateData + { + id PORTAL_Issuer; + HashMap registers; + }; + + struct getRegisters_input + { + uint32 offset; + uint32 limit; + }; + + struct getRegisters_output + { + id register1, register2, register3, register4, register5, register6, register7, register8, register9, register10; + sint32 returnCode; + } + + struct getRegisters_locals + { + id user; + sint32 index; + uint32 i; + Logger log; + }; + + struct registerInPortalDAO_input + { + } + + struct registerInPortalDAO_output + { + sint32 returnCode; + } + + struct registerInPortalDAO_locals + { + Logger log; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getRegisters) + { + if (input.limit > 10) + { + output.returnCode = QPORTAL_INVALID_OFFSET_OR_LIMIT; + return ; + } + if (input.offset + input.limit > state.get().registers.size()) + { + output.returnCode = QPORTAL_INVALID_OFFSET_OR_LIMIT; + return ; + } + locals.index = state.get().registers.nextElementIndex(NULL_INDEX); + while (locals.index != NULL_INDEX) + { + locals.user = state.get().registers.key(locals.index); + if (locals.i >= input.offset && locals.i < input.offset + input.limit) + { + if (locals.i - input.offset == 0) + { + output.register1 = locals.user; + } + else if (locals.i - input.offset == 1) + { + output.register2 = locals.user; + } + else if (locals.i - input.offset == 2) + { + output.register3 = locals.user; + } + else if (locals.i - input.offset == 3) + { + output.register4 = locals.user; + } + else if (locals.i - input.offset == 4) + { + output.register5 = locals.user; + } + else if (locals.i - input.offset == 5) + { + output.register6 = locals.user; + } + else if (locals.i - input.offset == 6) + { + output.register7 = locals.user; + } + else if (locals.i - input.offset == 7) + { + output.register8 = locals.user; + } + else if (locals.i - input.offset == 8) + { + output.register9 = locals.user; + } + else if (locals.i - input.offset == 9) + { + output.register10 = locals.user; + } + if (locals.i >= input.offset + input.limit) + { + break; + } + locals.i++; + locals.index = state.get().registers.nextElementIndex(locals.index); + } + } + output.returnCode = QPORTAL_SUCCESS; + + } + + PUBLIC_PROCEDURE_WITH_LOCALS(registerInPortalDAO) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.numberOfPossessedShares(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < QPORTAL_REGISTER_AMOUNT + 1) + { + output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; + LOG_INFO(locals.log); + return ; + } + + if(qpi.transferShareOwnershipAndPossession(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), QPORTAL_REGISTER_AMOUNT, SELF_INDEX) < 0) + { + output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; + LOG_INFO(locals.log); + return ; + } + state.mut().registers.set(qpi.invocator(), 1); + + output.returnCode = QPORTAL_SUCCESS; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_SUCCESS, 0 }; + LOG_INFO(locals.log); + } + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_PROCEDURE(registerInPortalDAO, 1); + } + + INITIALIZE() + { + state.mut().PORTAL_Issuer = ID(_I, _Q, _U, _G, _N, _V, _F, _D, _Q, _S, _L, _T, _X, _F, _J, _S, _I, _O, _P, _P, _N, _P, _Z, _I, _N, _S, _C, _D, _Q, _T, _J, _V, _J, _W, _G, _R, _P, _W, _R, _T, _F, _F, _X, _M, _X, _S, _J, _I, _A, _A, _S, _X, _O, _B, _F, _F); + } +}; \ No newline at end of file diff --git a/test/contract_qportal.cpp b/test/contract_qportal.cpp new file mode 100644 index 000000000..e69de29bb From 2a3135b0fb4e14b28c1b80f7148993bb95201b04 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Thu, 21 May 2026 09:52:10 +0900 Subject: [PATCH 02/18] fix: registerIn and getRegisters function --- src/contract_core/contract_def.h | 2 +- src/contracts/QPortal.h | 71 +++++++++++++++++++++----------- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 0dc5d1cbf..5bba78a71 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -415,7 +415,7 @@ constexpr struct ContractDescription {"VOTTUN", 206, 10000, sizeof(VOTTUNBRIDGE::StateData)}, // proposal in epoch 204, IPO in 205, construction and first use in 206 {"QUSINO", 208, 10000, sizeof(QUSINO::StateData)}, // proposal in epoch 206, IPO in 207, construction and first use in 208 {"ESCROW", 210, 10000, sizeof(ESCROW::StateData)}, // proposal in epoch 208, IPO in 209, construction and first use in 210 - {"QPORTAL", 213, 10000, sizeof(QPORTAL::StateData)}, // proposal in epoch 210, IPO in 211, construction and first use in 212 + {"QPORTAL", 213, 10000, sizeof(QPORTAL::StateData)}, // proposal in epoch 211, IPO in 212, construction and first use in 213 // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA::StateData)}, diff --git a/src/contracts/QPortal.h b/src/contracts/QPortal.h index a72b61a46..911e7f104 100644 --- a/src/contracts/QPortal.h +++ b/src/contracts/QPortal.h @@ -3,19 +3,23 @@ using namespace QPI; constexpr uint64 QPORTAL_PORTAL_ASSET_NAME = 83843471265616; //Portal toekn asset name constexpr uint32 QPORTAL_REGISTER_AMOUNT = 5; //Amount of portal token to register +constexpr uint32 QPORTAL_MIN_RETAINED = 1; //Members must keep >=1 portal token constexpr uint32 QPORTAL_MAX_MEMBER = 4096; //Maximum number of members in the portal DAO constexpr uint32 QPORTAL_SUCCESS = 0; constexpr uint32 QPORTAL_INSUFFICIENT_PORTAL = 1; constexpr uint32 QPORTAL_INVALID_OFFSET_OR_LIMIT = 2; +constexpr uint32 QPORTAL_ALREADY_REGISTERED = 3; +constexpr uint32 QPORTAL_REACHED_FULL = 4; -struct QPortal2 +struct QPORTAL2 { }; -struct QPortal : public ContractBase +struct QPORTAL : public ContractBase { public: + struct Logger { uint32 _contractIndex; @@ -27,6 +31,7 @@ struct QPortal : public ContractBase { id PORTAL_Issuer; HashMap registers; + uint32 numberOfRegisters; }; struct getRegisters_input @@ -39,28 +44,24 @@ struct QPortal : public ContractBase { id register1, register2, register3, register4, register5, register6, register7, register8, register9, register10; sint32 returnCode; - } - - struct getRegisters_locals - { - id user; - sint32 index; - uint32 i; - Logger log; }; struct registerInPortalDAO_input { - } + }; struct registerInPortalDAO_output { sint32 returnCode; - } + }; - struct registerInPortalDAO_locals +protected: + + struct getRegisters_locals { - Logger log; + id user; + sint32 index; + uint32 i; }; PUBLIC_FUNCTION_WITH_LOCALS(getRegisters) @@ -70,7 +71,7 @@ struct QPortal : public ContractBase output.returnCode = QPORTAL_INVALID_OFFSET_OR_LIMIT; return ; } - if (input.offset + input.limit > state.get().registers.size()) + if (input.offset + input.limit > state.get().numberOfRegisters) { output.returnCode = QPORTAL_INVALID_OFFSET_OR_LIMIT; return ; @@ -78,9 +79,10 @@ struct QPortal : public ContractBase locals.index = state.get().registers.nextElementIndex(NULL_INDEX); while (locals.index != NULL_INDEX) { - locals.user = state.get().registers.key(locals.index); + if (locals.i >= input.offset + input.limit) break; if (locals.i >= input.offset && locals.i < input.offset + input.limit) { + locals.user = state.get().registers.key(locals.index); if (locals.i - input.offset == 0) { output.register1 = locals.user; @@ -121,18 +123,19 @@ struct QPortal : public ContractBase { output.register10 = locals.user; } - if (locals.i >= input.offset + input.limit) - { - break; - } - locals.i++; - locals.index = state.get().registers.nextElementIndex(locals.index); } + locals.i++; + locals.index = state.get().registers.nextElementIndex(locals.index); } output.returnCode = QPORTAL_SUCCESS; } + struct registerInPortalDAO_locals + { + Logger log; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(registerInPortalDAO) { if (qpi.invocationReward() > 0) @@ -140,7 +143,23 @@ struct QPortal : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - if (qpi.numberOfPossessedShares(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < QPORTAL_REGISTER_AMOUNT + 1) + if (state.get().registers.contains(qpi.invocator())) + { + output.returnCode = QPORTAL_ALREADY_REGISTERED; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_ALREADY_REGISTERED, 0 }; + LOG_INFO(locals.log); + return ; + } + + if (state.get().numberOfRegisters >= QPORTAL_MAX_MEMBER) + { + output.returnCode = QPORTAL_REACHED_FULL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_REACHED_FULL, 0 }; + LOG_INFO(locals.log); + return ; + } + + if (qpi.numberOfPossessedShares(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < QPORTAL_REGISTER_AMOUNT + QPORTAL_MIN_RETAINED) // +1 to check if the user has enough shares to pay the fee { output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; @@ -148,7 +167,7 @@ struct QPortal : public ContractBase return ; } - if(qpi.transferShareOwnershipAndPossession(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), QPORTAL_REGISTER_AMOUNT, SELF_INDEX) < 0) + if(qpi.transferShareOwnershipAndPossession(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), QPORTAL_REGISTER_AMOUNT, SELF) < 0) { output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; @@ -156,6 +175,7 @@ struct QPortal : public ContractBase return ; } state.mut().registers.set(qpi.invocator(), 1); + state.mut().numberOfRegisters++; output.returnCode = QPORTAL_SUCCESS; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_SUCCESS, 0 }; @@ -164,11 +184,14 @@ struct QPortal : public ContractBase REGISTER_USER_FUNCTIONS_AND_PROCEDURES() { + REGISTER_USER_FUNCTION(getRegisters, 1); + REGISTER_USER_PROCEDURE(registerInPortalDAO, 1); } INITIALIZE() { state.mut().PORTAL_Issuer = ID(_I, _Q, _U, _G, _N, _V, _F, _D, _Q, _S, _L, _T, _X, _F, _J, _S, _I, _O, _P, _P, _N, _P, _Z, _I, _N, _S, _C, _D, _Q, _T, _J, _V, _J, _W, _G, _R, _P, _W, _R, _T, _F, _F, _X, _M, _X, _S, _J, _I, _A, _A, _S, _X, _O, _B, _F, _F); + state.mut().numberOfRegisters = 0; } }; \ No newline at end of file From ae30db24c56922ea35aa9d3fad3fd0b2e51b338e Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Thu, 21 May 2026 23:14:22 +0900 Subject: [PATCH 03/18] feat: submitProposal function --- src/contracts/QPortal.h | 110 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 4 deletions(-) diff --git a/src/contracts/QPortal.h b/src/contracts/QPortal.h index 911e7f104..1c5aa9aa3 100644 --- a/src/contracts/QPortal.h +++ b/src/contracts/QPortal.h @@ -4,13 +4,19 @@ using namespace QPI; constexpr uint64 QPORTAL_PORTAL_ASSET_NAME = 83843471265616; //Portal toekn asset name constexpr uint32 QPORTAL_REGISTER_AMOUNT = 5; //Amount of portal token to register constexpr uint32 QPORTAL_MIN_RETAINED = 1; //Members must keep >=1 portal token -constexpr uint32 QPORTAL_MAX_MEMBER = 4096; //Maximum number of members in the portal DAO +constexpr uint32 QPORTAL_MAX_MEMBER = 4096; //Maximum number of members in the portal DAO. +constexpr uint32 QPORTAL_MAX_PROPOSAL = 4096; //Maximum number of proposals in the portal DAO. +constexpr uint32 QPORTAL_MAX_PROPOSAL_USER = 2; //The maximum number of proposals a user can submit. +constexpr uint32 QPORTAL_MAX_PROPOSAL_EPOCH = 5; //The maximum number constexpr uint32 QPORTAL_SUCCESS = 0; constexpr uint32 QPORTAL_INSUFFICIENT_PORTAL = 1; constexpr uint32 QPORTAL_INVALID_OFFSET_OR_LIMIT = 2; constexpr uint32 QPORTAL_ALREADY_REGISTERED = 3; constexpr uint32 QPORTAL_REACHED_FULL = 4; +constexpr uint32 QPORTAL_NOT_REGISTERED = 5; +constexpr uint32 QPORTAL_REACHED_PROPOSAL = 6; +constexpr uint32 QPORTAL_ALREADY_EXISTED_PROPOSAL = 7; struct QPORTAL2 { @@ -27,11 +33,16 @@ struct QPORTAL : public ContractBase sint8 _terminator; }; + // struct + struct StateData { id PORTAL_Issuer; - HashMap registers; - uint32 numberOfRegisters; + HashMap registers; //registered members in the portal DAO + uint32 numberOfRegisters, numberOfProposalEpochs, numberOfProposals; + HashMap userProposalStatus; // 0 = no proposal, 1 = submitted 1 proposal, 2 = submitted 2 proposals (max) + HashMap submittedProposals; // 0 = voting, 1 = accepted, 2 = rejected + }; struct getRegisters_input @@ -55,6 +66,16 @@ struct QPORTAL : public ContractBase sint32 returnCode; }; + struct submitProposal_input + { + id proposalId; + }; + + struct submitProposal_output + { + sint32 returnCode; + }; + protected: struct getRegisters_locals @@ -175,7 +196,76 @@ struct QPORTAL : public ContractBase return ; } state.mut().registers.set(qpi.invocator(), 1); - state.mut().numberOfRegisters++; + state.mut().numberOfRegisters ++; + + output.returnCode = QPORTAL_SUCCESS; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_SUCCESS, 0 }; + LOG_INFO(locals.log); + } + + struct submitProposal_locals + { + sint32 user_index, proposal_index; + uint32 user_status, proposal_status; + Logger log; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(submitProposal) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (!state.get().registers.contains(qpi.invocator())) + { + output.returnCode = QPORTAL_NOT_REGISTERED; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_NOT_REGISTERED, 0 }; + LOG_INFO(locals.log); + return ; + } + + if ( state.get().numberOfProposals >= QPORTAL_MAX_PROPOSAL) + { + output.returnCode = QPORTAL_REACHED_PROPOSAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_REACHED_PROPOSAL, 0 }; + LOG_INFO(locals.log); + return ; + } + + if ( state.get().numberOfProposalEpochs >= QPORTAL_MAX_PROPOSAL_EPOCH) + { + output.returnCode = QPORTAL_REACHED_PROPOSAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_REACHED_PROPOSAL, 0 }; + LOG_INFO(locals.log); + return ; + } + + locals.user_index = state.get().userProposalStatus.getElementIndex(qpi.invocator()); + + if (locals.user_index != NULL_INDEX && (state.get().userProposalStatus.value(locals.user_index) >= QPORTAL_MAX_PROPOSAL_USER)) + { + output.returnCode = QPORTAL_REACHED_PROPOSAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_REACHED_PROPOSAL, 0 }; + LOG_INFO(locals.log); + return ; + } + + if (state.get().submittedProposals.contains(input.proposalId)) + { + output.returnCode = QPORTAL_ALREADY_EXISTED_PROPOSAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_ALREADY_EXISTED_PROPOSAL, 0 }; + LOG_INFO(locals.log); + return ; + } + + state.mut().submittedProposals.set(input.proposalId, 0); + + locals.user_status = locals.user_index == NULL_INDEX ? 0 : state.get().userProposalStatus.value(locals.user_index); + + state.mut().userProposalStatus.set(qpi.invocator(), ++ locals.user_status); + state.mut().numberOfProposals ++; + state.mut().numberOfProposalEpochs ++; output.returnCode = QPORTAL_SUCCESS; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_SUCCESS, 0 }; @@ -187,11 +277,23 @@ struct QPORTAL : public ContractBase REGISTER_USER_FUNCTION(getRegisters, 1); REGISTER_USER_PROCEDURE(registerInPortalDAO, 1); + REGISTER_USER_PROCEDURE(submitProposal, 2); + } + + END_EPOCH() + { + state.mut().numberOfProposalEpochs = 0; + state.mut().userProposalStatus.reset(); } INITIALIZE() { state.mut().PORTAL_Issuer = ID(_I, _Q, _U, _G, _N, _V, _F, _D, _Q, _S, _L, _T, _X, _F, _J, _S, _I, _O, _P, _P, _N, _P, _Z, _I, _N, _S, _C, _D, _Q, _T, _J, _V, _J, _W, _G, _R, _P, _W, _R, _T, _F, _F, _X, _M, _X, _S, _J, _I, _A, _A, _S, _X, _O, _B, _F, _F); state.mut().numberOfRegisters = 0; + state.mut().numberOfProposalEpochs = 0; + state.mut().numberOfProposals = 0; + state.mut().registers.reset(); + state.mut().userProposalStatus.reset(); + state.mut().submittedProposals.reset(); } }; \ No newline at end of file From bb5fcb1e535739183321211590ec3d56b335d9bf Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Fri, 22 May 2026 12:30:15 +0900 Subject: [PATCH 04/18] feat: submitInVote and END_EPOCH --- src/contracts/QPortal.h | 203 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 195 insertions(+), 8 deletions(-) diff --git a/src/contracts/QPortal.h b/src/contracts/QPortal.h index 1c5aa9aa3..8da2e3401 100644 --- a/src/contracts/QPortal.h +++ b/src/contracts/QPortal.h @@ -1,13 +1,15 @@ using namespace QPI; #include "qpi.h" -constexpr uint64 QPORTAL_PORTAL_ASSET_NAME = 83843471265616; //Portal toekn asset name -constexpr uint32 QPORTAL_REGISTER_AMOUNT = 5; //Amount of portal token to register +constexpr uint64 QPORTAL_PORTAL_ASSET_NAME = 83843471265616; //PORTAL asset name +constexpr uint64 QPORTAL_MIN_HOLDING_PORTAL = 100000ull; //proposal must have at least 100K PORTAL to approve a proposal. +constexpr uint32 QPORTAL_REGISTER_AMOUNT = 5; //Amount of PORTAL to register constexpr uint32 QPORTAL_MIN_RETAINED = 1; //Members must keep >=1 portal token constexpr uint32 QPORTAL_MAX_MEMBER = 4096; //Maximum number of members in the portal DAO. constexpr uint32 QPORTAL_MAX_PROPOSAL = 4096; //Maximum number of proposals in the portal DAO. constexpr uint32 QPORTAL_MAX_PROPOSAL_USER = 2; //The maximum number of proposals a user can submit. constexpr uint32 QPORTAL_MAX_PROPOSAL_EPOCH = 5; //The maximum number +constexpr uint32 QPORTAL_MAX_VOTE = 32768; //The maximum number of votes a user can make (voting in all proposals in all proposal epochs). constexpr uint32 QPORTAL_SUCCESS = 0; constexpr uint32 QPORTAL_INSUFFICIENT_PORTAL = 1; @@ -17,6 +19,9 @@ constexpr uint32 QPORTAL_REACHED_FULL = 4; constexpr uint32 QPORTAL_NOT_REGISTERED = 5; constexpr uint32 QPORTAL_REACHED_PROPOSAL = 6; constexpr uint32 QPORTAL_ALREADY_EXISTED_PROPOSAL = 7; +constexpr uint32 QPORTAL_NOT_EXISTED_PROPOSAL = 8; +constexpr uint32 QPORTAL_ALREADY_VOTED_PROPOSAL = 9; +constexpr uint32 QPORTAL_CLOSED_PROPOSAL = 10; struct QPORTAL2 { @@ -33,7 +38,25 @@ struct QPORTAL : public ContractBase sint8 _terminator; }; - // struct + struct VoteKey + { + id userId; + id proposalId; + }; + + struct voteRecord + { + id proposalId; + bit vote; // 0 = no, 1 = yes + }; + + struct voteResult + { + uint32 yesVotes; + uint32 noVotes; + uint64 yesPortal; + uint64 noPortal; + }; struct StateData { @@ -41,8 +64,13 @@ struct QPORTAL : public ContractBase HashMap registers; //registered members in the portal DAO uint32 numberOfRegisters, numberOfProposalEpochs, numberOfProposals; HashMap userProposalStatus; // 0 = no proposal, 1 = submitted 1 proposal, 2 = submitted 2 proposals (max) + Array currentEpochProposals; // record of proposal IDs in the current proposal epoch HashMap submittedProposals; // 0 = voting, 1 = accepted, 2 = rejected - + HashMap lockedAmount; + HashMap proposalVotes; // Voting records for each user per proposal epoch, used to check whether the user voted in the current proposal epoch. + HashMap proposalResults; // record of vote results for all of proposals + HashSet lockedPortals; // record of users who have locked their PORTAL + }; struct getRegisters_input @@ -76,6 +104,18 @@ struct QPORTAL : public ContractBase sint32 returnCode; }; + struct submitInVote_input + { + id proposalId; + bit vote; // 0 = no, 1 = yes + uint64 votingPortalAmount; + }; + + struct submitInVote_output + { + sint32 returnCode; + }; + protected: struct getRegisters_locals @@ -148,8 +188,8 @@ struct QPORTAL : public ContractBase locals.i++; locals.index = state.get().registers.nextElementIndex(locals.index); } - output.returnCode = QPORTAL_SUCCESS; + output.returnCode = QPORTAL_SUCCESS; } struct registerInPortalDAO_locals @@ -195,6 +235,7 @@ struct QPORTAL : public ContractBase LOG_INFO(locals.log); return ; } + state.mut().registers.set(qpi.invocator(), 1); state.mut().numberOfRegisters ++; @@ -225,7 +266,7 @@ struct QPORTAL : public ContractBase return ; } - if ( state.get().numberOfProposals >= QPORTAL_MAX_PROPOSAL) + if (state.get().numberOfProposals >= QPORTAL_MAX_PROPOSAL) { output.returnCode = QPORTAL_REACHED_PROPOSAL; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_REACHED_PROPOSAL, 0 }; @@ -233,7 +274,7 @@ struct QPORTAL : public ContractBase return ; } - if ( state.get().numberOfProposalEpochs >= QPORTAL_MAX_PROPOSAL_EPOCH) + if (state.get().numberOfProposalEpochs >= QPORTAL_MAX_PROPOSAL_EPOCH) { output.returnCode = QPORTAL_REACHED_PROPOSAL; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_REACHED_PROPOSAL, 0 }; @@ -260,6 +301,7 @@ struct QPORTAL : public ContractBase } state.mut().submittedProposals.set(input.proposalId, 0); + state.mut().currentEpochProposals.set(state.get().numberOfProposalEpochs, input.proposalId); // add proposal to the current proposal epoch locals.user_status = locals.user_index == NULL_INDEX ? 0 : state.get().userProposalStatus.value(locals.user_index); @@ -272,18 +314,162 @@ struct QPORTAL : public ContractBase LOG_INFO(locals.log); } + struct submitInVote_locals + { + id key; + sint32 index; + uint64 amount; + VoteKey vk; + voteRecord voteRec; + voteResult voteRes; + Logger log; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(submitInVote) + { + + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (!state.get().registers.contains(qpi.invocator())) + { + output.returnCode = QPORTAL_NOT_REGISTERED; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_NOT_REGISTERED, 0 }; + LOG_INFO(locals.log); + return ; + } + + if (input.votingPortalAmount == 0 || qpi.numberOfPossessedShares(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.votingPortalAmount) + { + output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; + LOG_INFO(locals.log); + return ; + } + + if (!state.get().submittedProposals.contains(input.proposalId)) + { + output.returnCode = QPORTAL_NOT_EXISTED_PROPOSAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_NOT_EXISTED_PROPOSAL, 0 }; + LOG_INFO(locals.log); + return; + } + + locals.index = state.get().submittedProposals.getElementIndex(input.proposalId); + if (state.get().submittedProposals.value(locals.index) != 0) + { + output.returnCode = QPORTAL_CLOSED_PROPOSAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_CLOSED_PROPOSAL, 0 }; + LOG_INFO(locals.log); + return; + } + + locals.vk.userId = qpi.invocator(); + locals.vk.proposalId = input.proposalId; + locals.key = qpi.K12(locals.vk); + + if (state.get().proposalVotes.getElementIndex(locals.key) != NULL_INDEX) + { + output.returnCode = QPORTAL_ALREADY_VOTED_PROPOSAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_ALREADY_VOTED_PROPOSAL, 0 }; + LOG_INFO(locals.log); + return ; + } + + qpi.transferShareOwnershipAndPossession(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), input.votingPortalAmount, SELF); + + if (state.get().lockedAmount.contains(qpi.invocator())) + { + locals.amount = state.get().lockedAmount.value(state.get().lockedAmount.getElementIndex(qpi.invocator())); + state.mut().lockedAmount.set(qpi.invocator(), locals.amount + input.votingPortalAmount); + } + else + { + state.mut().lockedAmount.set(qpi.invocator(), input.votingPortalAmount); + } + + state.mut().proposalVotes.set(locals.key, {input.proposalId, input.vote}); + + locals.index = state.get().proposalResults.getElementIndex(input.proposalId); + if (locals.index == NULL_INDEX) + { + if (input.vote == 1) + { + state.mut().proposalResults.set(input.proposalId, { 1, 0, input.votingPortalAmount, 0 }); + } + else + { + state.mut().proposalResults.set(input.proposalId, { 0, 1, 0, input.votingPortalAmount }); + } + } + else + { + locals.voteRes = state.get().proposalResults.value(locals.index); + state.mut().proposalResults.set(input.proposalId, { locals.voteRes.yesVotes + (input.vote == 1 ? 1 : 0), locals.voteRes.noVotes + (input.vote == 0 ? 1 : 0), locals.voteRes.yesPortal + (input.vote == 1 ? input.votingPortalAmount : 0), locals.voteRes.noPortal + (input.vote == 0 ? input.votingPortalAmount : 0) }); + } + + output.returnCode = QPORTAL_SUCCESS; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_SUCCESS, 0 }; + LOG_INFO(locals.log); + } + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() { REGISTER_USER_FUNCTION(getRegisters, 1); REGISTER_USER_PROCEDURE(registerInPortalDAO, 1); REGISTER_USER_PROCEDURE(submitProposal, 2); + REGISTER_USER_PROCEDURE(submitInVote, 3); } - END_EPOCH() + struct END_EPOCH_locals + { + uint32 i, index; + id proposalId; + uint32 yesVotes, noVotes; + uint64 yesPortal, noPortal; + }; + + END_EPOCH_WITH_LOCALS() { + if (state.get().numberOfProposalEpochs > 0) + { + + for (; locals.i < state.get().numberOfProposalEpochs; locals.i ++) + { + locals.proposalId = state.get().currentEpochProposals.get(locals.i); + + locals.index = state.get().proposalResults.getElementIndex(locals.proposalId); + if (locals.index == NULL_INDEX) + { + state.mut().submittedProposals.set(locals.proposalId, 2); + continue; + } + locals.yesVotes = state.get().proposalResults.value(locals.index).yesVotes; + locals.noVotes = state.get().proposalResults.value(locals.index).noVotes; + locals.yesPortal = state.get().proposalResults.value(locals.index).yesPortal; + locals.noPortal = state.get().proposalResults.value(locals.index).noPortal; + + if (locals.yesPortal + locals.noPortal < QPORTAL_MIN_HOLDING_PORTAL ) + { + state.mut().submittedProposals.set(locals.proposalId, 2); + } + else if (locals.yesVotes > locals.noVotes && locals.yesPortal > locals.noPortal ) + { + state.mut().submittedProposals.set(locals.proposalId, 1); + } + else + { + state.mut().submittedProposals.set(locals.proposalId, 2); + } + } + } + state.mut().numberOfProposalEpochs = 0; state.mut().userProposalStatus.reset(); + state.mut().proposalVotes.reset(); } INITIALIZE() @@ -295,5 +481,6 @@ struct QPORTAL : public ContractBase state.mut().registers.reset(); state.mut().userProposalStatus.reset(); state.mut().submittedProposals.reset(); + state.mut().proposalResults.reset(); } }; \ No newline at end of file From c55ecc630cb3f97f5c826697a5b73116ff83a116 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Fri, 22 May 2026 20:44:39 +0900 Subject: [PATCH 05/18] fix: submitInVote, local variable --- src/contracts/QPortal.h | 73 +++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/src/contracts/QPortal.h b/src/contracts/QPortal.h index 8da2e3401..80d1cbf5a 100644 --- a/src/contracts/QPortal.h +++ b/src/contracts/QPortal.h @@ -4,7 +4,6 @@ using namespace QPI; constexpr uint64 QPORTAL_PORTAL_ASSET_NAME = 83843471265616; //PORTAL asset name constexpr uint64 QPORTAL_MIN_HOLDING_PORTAL = 100000ull; //proposal must have at least 100K PORTAL to approve a proposal. constexpr uint32 QPORTAL_REGISTER_AMOUNT = 5; //Amount of PORTAL to register -constexpr uint32 QPORTAL_MIN_RETAINED = 1; //Members must keep >=1 portal token constexpr uint32 QPORTAL_MAX_MEMBER = 4096; //Maximum number of members in the portal DAO. constexpr uint32 QPORTAL_MAX_PROPOSAL = 4096; //Maximum number of proposals in the portal DAO. constexpr uint32 QPORTAL_MAX_PROPOSAL_USER = 2; //The maximum number of proposals a user can submit. @@ -22,6 +21,7 @@ constexpr uint32 QPORTAL_ALREADY_EXISTED_PROPOSAL = 7; constexpr uint32 QPORTAL_NOT_EXISTED_PROPOSAL = 8; constexpr uint32 QPORTAL_ALREADY_VOTED_PROPOSAL = 9; constexpr uint32 QPORTAL_CLOSED_PROPOSAL = 10; +constexpr uint32 QPORTAL_INVALID_INPUT = 11; struct QPORTAL2 { @@ -43,12 +43,6 @@ struct QPORTAL : public ContractBase id userId; id proposalId; }; - - struct voteRecord - { - id proposalId; - bit vote; // 0 = no, 1 = yes - }; struct voteResult { @@ -62,15 +56,13 @@ struct QPORTAL : public ContractBase { id PORTAL_Issuer; HashMap registers; //registered members in the portal DAO - uint32 numberOfRegisters, numberOfProposalEpochs, numberOfProposals; + uint32 numberOfRegisters, numberOfCurrentEpochProposals, numberOfProposals; HashMap userProposalStatus; // 0 = no proposal, 1 = submitted 1 proposal, 2 = submitted 2 proposals (max) Array currentEpochProposals; // record of proposal IDs in the current proposal epoch HashMap submittedProposals; // 0 = voting, 1 = accepted, 2 = rejected HashMap lockedAmount; - HashMap proposalVotes; // Voting records for each user per proposal epoch, used to check whether the user voted in the current proposal epoch. + HashMap proposalVotes; // Voting records for each user per proposal epoch, used to check whether the user voted in the current proposal epoch. HashMap proposalResults; // record of vote results for all of proposals - HashSet lockedPortals; // record of users who have locked their PORTAL - }; struct getRegisters_input @@ -220,7 +212,7 @@ struct QPORTAL : public ContractBase return ; } - if (qpi.numberOfPossessedShares(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < QPORTAL_REGISTER_AMOUNT + QPORTAL_MIN_RETAINED) // +1 to check if the user has enough shares to pay the fee + if (qpi.numberOfPossessedShares(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < QPORTAL_REGISTER_AMOUNT) { output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; @@ -246,8 +238,7 @@ struct QPORTAL : public ContractBase struct submitProposal_locals { - sint32 user_index, proposal_index; - uint32 user_status, proposal_status; + sint32 index, status; Logger log; }; @@ -266,6 +257,14 @@ struct QPORTAL : public ContractBase return ; } + if (input.proposalId == NULL_ID) + { + output.returnCode = QPORTAL_INVALID_INPUT; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INVALID_INPUT, 0 }; + LOG_INFO(locals.log); + return ; + } + if (state.get().numberOfProposals >= QPORTAL_MAX_PROPOSAL) { output.returnCode = QPORTAL_REACHED_PROPOSAL; @@ -274,7 +273,7 @@ struct QPORTAL : public ContractBase return ; } - if (state.get().numberOfProposalEpochs >= QPORTAL_MAX_PROPOSAL_EPOCH) + if (state.get().numberOfCurrentEpochProposals >= QPORTAL_MAX_PROPOSAL_EPOCH) { output.returnCode = QPORTAL_REACHED_PROPOSAL; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_REACHED_PROPOSAL, 0 }; @@ -282,9 +281,9 @@ struct QPORTAL : public ContractBase return ; } - locals.user_index = state.get().userProposalStatus.getElementIndex(qpi.invocator()); + locals.index = state.get().userProposalStatus.getElementIndex(qpi.invocator()); - if (locals.user_index != NULL_INDEX && (state.get().userProposalStatus.value(locals.user_index) >= QPORTAL_MAX_PROPOSAL_USER)) + if (locals.index != NULL_INDEX && (state.get().userProposalStatus.value(locals.index) >= QPORTAL_MAX_PROPOSAL_USER)) { output.returnCode = QPORTAL_REACHED_PROPOSAL; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_REACHED_PROPOSAL, 0 }; @@ -301,13 +300,13 @@ struct QPORTAL : public ContractBase } state.mut().submittedProposals.set(input.proposalId, 0); - state.mut().currentEpochProposals.set(state.get().numberOfProposalEpochs, input.proposalId); // add proposal to the current proposal epoch + state.mut().currentEpochProposals.set(state.get().numberOfCurrentEpochProposals, input.proposalId); // add proposal to the current proposal epoch - locals.user_status = locals.user_index == NULL_INDEX ? 0 : state.get().userProposalStatus.value(locals.user_index); + locals.status = locals.index == NULL_INDEX ? 0 : state.get().userProposalStatus.value(locals.index); - state.mut().userProposalStatus.set(qpi.invocator(), ++ locals.user_status); + state.mut().userProposalStatus.set(qpi.invocator(), ++ locals.status); state.mut().numberOfProposals ++; - state.mut().numberOfProposalEpochs ++; + state.mut().numberOfCurrentEpochProposals ++; output.returnCode = QPORTAL_SUCCESS; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_SUCCESS, 0 }; @@ -320,7 +319,6 @@ struct QPORTAL : public ContractBase sint32 index; uint64 amount; VoteKey vk; - voteRecord voteRec; voteResult voteRes; Logger log; }; @@ -333,6 +331,14 @@ struct QPORTAL : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + if (input.proposalId == NULL_ID || input.votingPortalAmount == 0) + { + output.returnCode = QPORTAL_INVALID_INPUT; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INVALID_INPUT, 0 }; + LOG_INFO(locals.log); + return ; + } + if (!state.get().registers.contains(qpi.invocator())) { output.returnCode = QPORTAL_NOT_REGISTERED; @@ -341,7 +347,7 @@ struct QPORTAL : public ContractBase return ; } - if (input.votingPortalAmount == 0 || qpi.numberOfPossessedShares(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.votingPortalAmount) + if (qpi.numberOfPossessedShares(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.votingPortalAmount) { output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; @@ -378,7 +384,13 @@ struct QPORTAL : public ContractBase return ; } - qpi.transferShareOwnershipAndPossession(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), input.votingPortalAmount, SELF); + if (qpi.transferShareOwnershipAndPossession(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), input.votingPortalAmount, SELF) < 0) + { + output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; + LOG_INFO(locals.log); + return ; + } if (state.get().lockedAmount.contains(qpi.invocator())) { @@ -390,7 +402,7 @@ struct QPORTAL : public ContractBase state.mut().lockedAmount.set(qpi.invocator(), input.votingPortalAmount); } - state.mut().proposalVotes.set(locals.key, {input.proposalId, input.vote}); + state.mut().proposalVotes.set(locals.key, input.vote); locals.index = state.get().proposalResults.getElementIndex(input.proposalId); if (locals.index == NULL_INDEX) @@ -434,10 +446,10 @@ struct QPORTAL : public ContractBase END_EPOCH_WITH_LOCALS() { - if (state.get().numberOfProposalEpochs > 0) + if (state.get().numberOfCurrentEpochProposals > 0) { - for (; locals.i < state.get().numberOfProposalEpochs; locals.i ++) + for (; locals.i < state.get().numberOfCurrentEpochProposals; locals.i ++) { locals.proposalId = state.get().currentEpochProposals.get(locals.i); @@ -467,7 +479,7 @@ struct QPORTAL : public ContractBase } } - state.mut().numberOfProposalEpochs = 0; + state.mut().numberOfCurrentEpochProposals = 0; state.mut().userProposalStatus.reset(); state.mut().proposalVotes.reset(); } @@ -476,8 +488,11 @@ struct QPORTAL : public ContractBase { state.mut().PORTAL_Issuer = ID(_I, _Q, _U, _G, _N, _V, _F, _D, _Q, _S, _L, _T, _X, _F, _J, _S, _I, _O, _P, _P, _N, _P, _Z, _I, _N, _S, _C, _D, _Q, _T, _J, _V, _J, _W, _G, _R, _P, _W, _R, _T, _F, _F, _X, _M, _X, _S, _J, _I, _A, _A, _S, _X, _O, _B, _F, _F); state.mut().numberOfRegisters = 0; - state.mut().numberOfProposalEpochs = 0; + state.mut().numberOfCurrentEpochProposals = 0; state.mut().numberOfProposals = 0; + state.mut().lockedAmount.reset(); + state.mut().currentEpochProposals.setAll(0); + state.mut().proposalVotes.reset(); state.mut().registers.reset(); state.mut().userProposalStatus.reset(); state.mut().submittedProposals.reset(); From b4fb9d775af664ff90072606b7902f09ac7df5bd Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Fri, 22 May 2026 23:56:59 +0900 Subject: [PATCH 06/18] feat: get functions and refund and submitOutVote function --- src/contracts/QPortal.h | 372 +++++++++++++++++++++++++++++++++------- 1 file changed, 308 insertions(+), 64 deletions(-) diff --git a/src/contracts/QPortal.h b/src/contracts/QPortal.h index 80d1cbf5a..795ce3b48 100644 --- a/src/contracts/QPortal.h +++ b/src/contracts/QPortal.h @@ -3,7 +3,8 @@ using namespace QPI; constexpr uint64 QPORTAL_PORTAL_ASSET_NAME = 83843471265616; //PORTAL asset name constexpr uint64 QPORTAL_MIN_HOLDING_PORTAL = 100000ull; //proposal must have at least 100K PORTAL to approve a proposal. -constexpr uint32 QPORTAL_REGISTER_AMOUNT = 5; //Amount of PORTAL to register +constexpr uint32 QPORTAL_REGISTER_FEE = 5; //Fee for registering in the portal DAO +constexpr uint64 QPORTAL_REFUND_FEE = 5; //PORTAL fee charged on each refund constexpr uint32 QPORTAL_MAX_MEMBER = 4096; //Maximum number of members in the portal DAO. constexpr uint32 QPORTAL_MAX_PROPOSAL = 4096; //Maximum number of proposals in the portal DAO. constexpr uint32 QPORTAL_MAX_PROPOSAL_USER = 2; //The maximum number of proposals a user can submit. @@ -22,6 +23,8 @@ constexpr uint32 QPORTAL_NOT_EXISTED_PROPOSAL = 8; constexpr uint32 QPORTAL_ALREADY_VOTED_PROPOSAL = 9; constexpr uint32 QPORTAL_CLOSED_PROPOSAL = 10; constexpr uint32 QPORTAL_INVALID_INPUT = 11; +constexpr uint32 QPORTAL_NOT_VOTED_PROPOSAL = 12; +constexpr uint32 QPORTAL_EXISTED_PROPOSAL = 13; struct QPORTAL2 { @@ -38,11 +41,23 @@ struct QPORTAL : public ContractBase sint8 _terminator; }; - struct VoteKey + struct submitInfo + { + id userId; + uint8 proposalStatus; // 0 = voting, 1 = accepted, 2 = rejected + }; + + struct voteKey { id userId; id proposalId; }; + + struct voteRecord + { + bit vote; + uint64 votingPortalAmount; + }; struct voteResult { @@ -59,9 +74,9 @@ struct QPORTAL : public ContractBase uint32 numberOfRegisters, numberOfCurrentEpochProposals, numberOfProposals; HashMap userProposalStatus; // 0 = no proposal, 1 = submitted 1 proposal, 2 = submitted 2 proposals (max) Array currentEpochProposals; // record of proposal IDs in the current proposal epoch - HashMap submittedProposals; // 0 = voting, 1 = accepted, 2 = rejected + HashMap submittedProposals; // 0 = voting, 1 = accepted, 2 = rejected HashMap lockedAmount; - HashMap proposalVotes; // Voting records for each user per proposal epoch, used to check whether the user voted in the current proposal epoch. + HashMap proposalVotes; // Voting records for each user per proposal epoch, used to check whether the user voted in the current proposal epoch. HashMap proposalResults; // record of vote results for all of proposals }; @@ -77,6 +92,52 @@ struct QPORTAL : public ContractBase sint32 returnCode; }; + struct getCurrentEpochProposals_input + { + }; + + struct getCurrentEpochProposals_output + { + id proposal1, proposal2, proposal3, proposal4, proposal5; + sint32 returnCode; + }; + + struct getProposalStatus_input + { + id proposalId; + }; + + struct getProposalStatus_output + { + uint32 status; + sint32 returnCode; + }; + + struct getUserLockedAmount_input + { + }; + + struct getUserLockedAmount_output + { + uint64 lockedAmount; + sint32 returnCode; + }; + + struct getProposalInfo_input + { + id proposalId; + }; + + struct getProposalInfo_output + { + id proposer; + uint32 yesVotes; + uint32 noVotes; + uint64 yesPortal; + uint64 noPortal; + sint32 returnCode; + }; + struct registerInPortalDAO_input { }; @@ -108,12 +169,32 @@ struct QPORTAL : public ContractBase sint32 returnCode; }; + struct submitOutVote_input + { + id proposalId; + }; + + struct submitOutVote_output + { + sint32 returnCode; + }; + + struct requestRefund_input + { + uint64 amount; + }; + + struct requestRefund_output + { + sint32 returnCode; + }; + protected: struct getRegisters_locals { id user; - sint32 index; + sint32 index, cnt; uint32 i; }; @@ -124,68 +205,99 @@ struct QPORTAL : public ContractBase output.returnCode = QPORTAL_INVALID_OFFSET_OR_LIMIT; return ; } - if (input.offset + input.limit > state.get().numberOfRegisters) + + if (input.offset >= state.get().numberOfRegisters) { output.returnCode = QPORTAL_INVALID_OFFSET_OR_LIMIT; return ; } - locals.index = state.get().registers.nextElementIndex(NULL_INDEX); - while (locals.index != NULL_INDEX) + + locals.index = state.get().registers.nextElementIndex(input.offset == 0 ? NULL_INDEX: input.offset -1); + locals.cnt = input.offset + input.limit > state.get().numberOfRegisters ? state.get().numberOfRegisters - input.offset : input.limit; + while (locals.cnt > 0) { - if (locals.i >= input.offset + input.limit) break; - if (locals.i >= input.offset && locals.i < input.offset + input.limit) + locals.user = state.get().registers.key(locals.index); + switch (locals.index - input.offset) { - locals.user = state.get().registers.key(locals.index); - if (locals.i - input.offset == 0) - { - output.register1 = locals.user; - } - else if (locals.i - input.offset == 1) - { - output.register2 = locals.user; - } - else if (locals.i - input.offset == 2) - { - output.register3 = locals.user; - } - else if (locals.i - input.offset == 3) - { - output.register4 = locals.user; - } - else if (locals.i - input.offset == 4) - { - output.register5 = locals.user; - } - else if (locals.i - input.offset == 5) - { - output.register6 = locals.user; - } - else if (locals.i - input.offset == 6) - { - output.register7 = locals.user; - } - else if (locals.i - input.offset == 7) - { - output.register8 = locals.user; - } - else if (locals.i - input.offset == 8) - { - output.register9 = locals.user; - } - else if (locals.i - input.offset == 9) - { - output.register10 = locals.user; - } - } - locals.i++; + case 0: output.register1 = locals.user; break; + case 1: output.register2 = locals.user; break; + case 2: output.register3 = locals.user; break; + case 3: output.register4 = locals.user; break; + case 4: output.register5 = locals.user; break; + case 5: output.register6 = locals.user; break; + case 6: output.register7 = locals.user; break; + case 7: output.register8 = locals.user; break; + case 8: output.register9 = locals.user; break; + case 9: output.register10 = locals.user; break; + } locals.index = state.get().registers.nextElementIndex(locals.index); + locals.cnt --; + } + + output.returnCode = QPORTAL_SUCCESS; + } + + PUBLIC_FUNCTION(getCurrentEpochProposals) + { + output.proposal1 = state.get().currentEpochProposals.get(0); + output.proposal2 = state.get().currentEpochProposals.get(1); + output.proposal3 = state.get().currentEpochProposals.get(2); + output.proposal4 = state.get().currentEpochProposals.get(3); + output.proposal5 = state.get().currentEpochProposals.get(4); + + output.returnCode = QPORTAL_SUCCESS; + } + + PUBLIC_FUNCTION(getProposalStatus) + { + if (input.proposalId == NULL_ID) + { + output.returnCode = QPORTAL_INVALID_INPUT; + return ; + } + + if (!state.get().submittedProposals.contains(input.proposalId)) + { + output.returnCode = QPORTAL_NOT_EXISTED_PROPOSAL; + return ; + } + + output.status = state.get().submittedProposals.value(state.get().submittedProposals.getElementIndex(input.proposalId)).proposalStatus; + output.returnCode = QPORTAL_SUCCESS; + } + + PUBLIC_FUNCTION(getUserLockedAmount) + { + if (!state.get().registers.contains(qpi.invocator())) + { + output.returnCode = QPORTAL_NOT_REGISTERED; + return ; + } + + output.lockedAmount = state.get().lockedAmount.contains(qpi.invocator()) ? state.get().lockedAmount.value(state.get().lockedAmount.getElementIndex(qpi.invocator())) : 0; + output.returnCode = QPORTAL_SUCCESS; + } + + PUBLIC_FUNCTION(getProposalInfo) + { + if (!state.get().proposalResults.contains(input.proposalId)) + { + output.returnCode = QPORTAL_NOT_EXISTED_PROPOSAL; + return ; } + output.proposer = state.get().submittedProposals.value(state.get().submittedProposals.getElementIndex(input.proposalId)).userId; + output.yesVotes = state.get().proposalResults.value(state.get().proposalResults.getElementIndex(input.proposalId)).yesVotes; + output.noVotes = state.get().proposalResults.value(state.get().proposalResults.getElementIndex(input.proposalId)).noVotes; + output.yesPortal = state.get().proposalResults.value(state.get().proposalResults.getElementIndex(input.proposalId)).yesPortal; + output.noPortal = state.get().proposalResults.value(state.get().proposalResults.getElementIndex(input.proposalId)).noPortal; + output.returnCode = QPORTAL_SUCCESS; } struct registerInPortalDAO_locals { + sint64 ownedShares; Logger log; }; @@ -212,7 +324,8 @@ struct QPORTAL : public ContractBase return ; } - if (qpi.numberOfPossessedShares(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < QPORTAL_REGISTER_AMOUNT) + locals.ownedShares = qpi.numberOfPossessedShares(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX); + if (locals.ownedShares < 0 || (uint64)locals.ownedShares < QPORTAL_REGISTER_FEE) { output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; @@ -220,7 +333,7 @@ struct QPORTAL : public ContractBase return ; } - if(qpi.transferShareOwnershipAndPossession(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), QPORTAL_REGISTER_AMOUNT, SELF) < 0) + if(qpi.transferShareOwnershipAndPossession(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), QPORTAL_REGISTER_FEE, SELF) < 0) { output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; @@ -299,7 +412,7 @@ struct QPORTAL : public ContractBase return ; } - state.mut().submittedProposals.set(input.proposalId, 0); + state.mut().submittedProposals.set(input.proposalId, {qpi.invocator(), 0}); // record proposal info with voting status = 0 (voting) state.mut().currentEpochProposals.set(state.get().numberOfCurrentEpochProposals, input.proposalId); // add proposal to the current proposal epoch locals.status = locals.index == NULL_INDEX ? 0 : state.get().userProposalStatus.value(locals.index); @@ -318,7 +431,8 @@ struct QPORTAL : public ContractBase id key; sint32 index; uint64 amount; - VoteKey vk; + sint64 ownedShares; + voteKey vk; voteResult voteRes; Logger log; }; @@ -347,7 +461,8 @@ struct QPORTAL : public ContractBase return ; } - if (qpi.numberOfPossessedShares(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.votingPortalAmount) + locals.ownedShares = qpi.numberOfPossessedShares(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX); + if (locals.ownedShares < 0 || (uint64)locals.ownedShares < input.votingPortalAmount) { output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; @@ -364,7 +479,7 @@ struct QPORTAL : public ContractBase } locals.index = state.get().submittedProposals.getElementIndex(input.proposalId); - if (state.get().submittedProposals.value(locals.index) != 0) + if (state.get().submittedProposals.value(locals.index).proposalStatus != 0) { output.returnCode = QPORTAL_CLOSED_PROPOSAL; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_CLOSED_PROPOSAL, 0 }; @@ -402,7 +517,7 @@ struct QPORTAL : public ContractBase state.mut().lockedAmount.set(qpi.invocator(), input.votingPortalAmount); } - state.mut().proposalVotes.set(locals.key, input.vote); + state.mut().proposalVotes.set(locals.key, {input.vote, input.votingPortalAmount}); // record user's vote and voting portal amount for the proposal locals.index = state.get().proposalResults.getElementIndex(input.proposalId); if (locals.index == NULL_INDEX) @@ -427,6 +542,133 @@ struct QPORTAL : public ContractBase LOG_INFO(locals.log); } + struct submitOutVote_locals + { + sint32 index; + voteRecord vr; + voteResult tmp; + Logger log; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(submitOutVote) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (input.proposalId == NULL_ID) + { + output.returnCode = QPORTAL_INVALID_INPUT; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INVALID_INPUT, 0 }; + LOG_INFO(locals.log); + return ; + } + + if (!state.get().registers.contains(qpi.invocator())) + { + output.returnCode = QPORTAL_NOT_REGISTERED; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_NOT_REGISTERED, 0 }; + LOG_INFO(locals.log); + return ; + } + + if (!state.get().submittedProposals.contains(input.proposalId)) + { + output.returnCode = QPORTAL_NOT_EXISTED_PROPOSAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_NOT_EXISTED_PROPOSAL, 0 }; + LOG_INFO(locals.log); + return; + } + + if (state.get().submittedProposals.value(state.get().submittedProposals.getElementIndex(input.proposalId)).proposalStatus != 0) + { + output.returnCode = QPORTAL_CLOSED_PROPOSAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_CLOSED_PROPOSAL, 0 }; + LOG_INFO(locals.log); + return ; + } + + locals.index = state.get().proposalVotes.getElementIndex(qpi.K12(voteKey{ qpi.invocator(), input.proposalId })); + if (locals.index == NULL_INDEX) + { + output.returnCode = QPORTAL_NOT_VOTED_PROPOSAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_NOT_VOTED_PROPOSAL, 0 }; + LOG_INFO(locals.log); + return ; + } + + locals.vr = state.get().proposalVotes.value(locals.index); + locals.tmp = state.get().proposalResults.value(state.get().proposalResults.getElementIndex(input.proposalId)); + state.mut().proposalResults.set(input.proposalId, { + locals.tmp.yesVotes - (locals.vr.vote == 1 ? 1 : 0), + locals.tmp.noVotes - (locals.vr.vote == 0 ? 1 : 0), + locals.tmp.yesPortal - (locals.vr.vote == 1 ? locals.vr.votingPortalAmount : 0), + locals.tmp.noPortal - (locals.vr.vote == 0 ? locals.vr.votingPortalAmount : 0) + }); + state.mut().proposalVotes.removeByIndex(locals.index); + + + output.returnCode = QPORTAL_SUCCESS; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_SUCCESS, 0 }; + LOG_INFO(locals.log); + } + + struct requestRefund_locals + { + sint32 i; + Logger log; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(requestRefund) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (input.amount <= QPORTAL_REFUND_FEE) + { + output.returnCode = QPORTAL_INVALID_INPUT; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INVALID_INPUT, 0 }; + LOG_INFO(locals.log); + return ; + } + + for (; locals.i < state.get().numberOfCurrentEpochProposals; locals.i ++) + { + if (state.get().proposalVotes.getElementIndex(qpi.K12(voteKey{ qpi.invocator(), state.get().currentEpochProposals.get(locals.i) })) != NULL_INDEX) + { + output.returnCode = QPORTAL_EXISTED_PROPOSAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_EXISTED_PROPOSAL, 0 }; + LOG_INFO(locals.log); + return ; + } + } + + if (!state.get().lockedAmount.contains(qpi.invocator()) || state.get().lockedAmount.value(state.get().lockedAmount.getElementIndex(qpi.invocator())) < input.amount) + { + output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; + LOG_INFO(locals.log); + return ; + } + + if(qpi.transferShareOwnershipAndPossession(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, SELF, SELF, input.amount - QPORTAL_REFUND_FEE, qpi.invocator()) < 0) + { + output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; + LOG_INFO(locals.log); + return ; + } + + state.mut().lockedAmount.set(qpi.invocator(), state.get().lockedAmount.value(state.get().lockedAmount.getElementIndex(qpi.invocator())) - input.amount); + + output.returnCode = QPORTAL_SUCCESS; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_SUCCESS, 0 }; + LOG_INFO(locals.log); + } + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() { REGISTER_USER_FUNCTION(getRegisters, 1); @@ -434,6 +676,8 @@ struct QPORTAL : public ContractBase REGISTER_USER_PROCEDURE(registerInPortalDAO, 1); REGISTER_USER_PROCEDURE(submitProposal, 2); REGISTER_USER_PROCEDURE(submitInVote, 3); + REGISTER_USER_PROCEDURE(submitOutVote, 4); + REGISTER_USER_PROCEDURE(requestRefund, 5); } struct END_EPOCH_locals @@ -456,7 +700,7 @@ struct QPORTAL : public ContractBase locals.index = state.get().proposalResults.getElementIndex(locals.proposalId); if (locals.index == NULL_INDEX) { - state.mut().submittedProposals.set(locals.proposalId, 2); + state.mut().submittedProposals.set(locals.proposalId, {state.get().submittedProposals.value(state.get().submittedProposals.getElementIndex(locals.proposalId)).userId, 2}); // if there is no vote for the proposal, set the proposal status to rejected (2) continue; } locals.yesVotes = state.get().proposalResults.value(locals.index).yesVotes; @@ -466,15 +710,15 @@ struct QPORTAL : public ContractBase if (locals.yesPortal + locals.noPortal < QPORTAL_MIN_HOLDING_PORTAL ) { - state.mut().submittedProposals.set(locals.proposalId, 2); + state.mut().submittedProposals.set(locals.proposalId, {state.get().submittedProposals.value(state.get().submittedProposals.getElementIndex(locals.proposalId)).userId, 2}); } else if (locals.yesVotes > locals.noVotes && locals.yesPortal > locals.noPortal ) { - state.mut().submittedProposals.set(locals.proposalId, 1); + state.mut().submittedProposals.set(locals.proposalId, {state.get().submittedProposals.value(state.get().submittedProposals.getElementIndex(locals.proposalId)).userId, 1}); } else { - state.mut().submittedProposals.set(locals.proposalId, 2); + state.mut().submittedProposals.set(locals.proposalId, {state.get().submittedProposals.value(state.get().submittedProposals.getElementIndex(locals.proposalId)).userId, 2}); } } } From a9cd65d5744468df7aea4b856613734023ba53de Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Sat, 23 May 2026 01:06:25 +0900 Subject: [PATCH 07/18] feat: getCurrentNumberOfRegisters function --- src/contracts/QPortal.h | 71 +++++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/src/contracts/QPortal.h b/src/contracts/QPortal.h index 795ce3b48..bcdc99ca4 100644 --- a/src/contracts/QPortal.h +++ b/src/contracts/QPortal.h @@ -92,6 +92,16 @@ struct QPORTAL : public ContractBase sint32 returnCode; }; + struct getCurrentNumberOfRegisters_input + { + }; + + struct getCurrentNumberOfRegisters_output + { + uint32 numberOfRegisters; + sint32 returnCode; + }; + struct getCurrentEpochProposals_input { }; @@ -131,6 +141,7 @@ struct QPORTAL : public ContractBase struct getProposalInfo_output { id proposer; + uint8 status; uint32 yesVotes; uint32 noVotes; uint64 yesPortal; @@ -194,13 +205,13 @@ struct QPORTAL : public ContractBase struct getRegisters_locals { id user; - sint32 index, cnt; - uint32 i; + sint32 index; + uint32 i, cnt; }; PUBLIC_FUNCTION_WITH_LOCALS(getRegisters) { - if (input.limit > 10) + if (input.limit == 0 || input.limit > 10) { output.returnCode = QPORTAL_INVALID_OFFSET_OR_LIMIT; return ; @@ -212,12 +223,16 @@ struct QPORTAL : public ContractBase return ; } - locals.index = state.get().registers.nextElementIndex(input.offset == 0 ? NULL_INDEX: input.offset -1); + locals.index = state.get().registers.nextElementIndex(NULL_INDEX); locals.cnt = input.offset + input.limit > state.get().numberOfRegisters ? state.get().numberOfRegisters - input.offset : input.limit; - while (locals.cnt > 0) + for (locals.i = 0; locals.i < input.offset; locals.i ++) + { + locals.index = state.get().registers.nextElementIndex(locals.index); + } + for (locals.i = 0; locals.i < locals.cnt && locals.index != NULL_INDEX; locals.i++) { locals.user = state.get().registers.key(locals.index); - switch (locals.index - input.offset) + switch (locals.i) { case 0: output.register1 = locals.user; break; case 1: output.register2 = locals.user; break; @@ -229,14 +244,19 @@ struct QPORTAL : public ContractBase case 7: output.register8 = locals.user; break; case 8: output.register9 = locals.user; break; case 9: output.register10 = locals.user; break; - } + } locals.index = state.get().registers.nextElementIndex(locals.index); - locals.cnt --; } output.returnCode = QPORTAL_SUCCESS; } + PUBLIC_FUNCTION(getCurrentNumberOfRegisters) + { + output.numberOfRegisters = state.get().numberOfRegisters; + output.returnCode = QPORTAL_SUCCESS; + } + PUBLIC_FUNCTION(getCurrentEpochProposals) { output.proposal1 = state.get().currentEpochProposals.get(0); @@ -278,19 +298,32 @@ struct QPORTAL : public ContractBase output.returnCode = QPORTAL_SUCCESS; } - PUBLIC_FUNCTION(getProposalInfo) + struct getProposalInfo_locals + { + submitInfo si; + voteResult vr; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getProposalInfo) { - if (!state.get().proposalResults.contains(input.proposalId)) + if (!state.get().submittedProposals.contains(input.proposalId)) { output.returnCode = QPORTAL_NOT_EXISTED_PROPOSAL; return ; } - output.proposer = state.get().submittedProposals.value(state.get().submittedProposals.getElementIndex(input.proposalId)).userId; - output.yesVotes = state.get().proposalResults.value(state.get().proposalResults.getElementIndex(input.proposalId)).yesVotes; - output.noVotes = state.get().proposalResults.value(state.get().proposalResults.getElementIndex(input.proposalId)).noVotes; - output.yesPortal = state.get().proposalResults.value(state.get().proposalResults.getElementIndex(input.proposalId)).yesPortal; - output.noPortal = state.get().proposalResults.value(state.get().proposalResults.getElementIndex(input.proposalId)).noPortal; + locals.si = state.get().submittedProposals.value(state.get().submittedProposals.getElementIndex(input.proposalId)); + output.proposer = locals.si.userId; + output.status = locals.si.proposalStatus; + + if (state.get().proposalResults.contains(input.proposalId)) + { + locals.vr = state.get().proposalResults.value(state.get().proposalResults.getElementIndex(input.proposalId)); + output.yesVotes = locals.vr.yesVotes; + output.noVotes = locals.vr.noVotes; + output.yesPortal = locals.vr.yesPortal; + output.noPortal = locals.vr.noPortal; + } output.returnCode = QPORTAL_SUCCESS; } @@ -672,6 +705,11 @@ struct QPORTAL : public ContractBase REGISTER_USER_FUNCTIONS_AND_PROCEDURES() { REGISTER_USER_FUNCTION(getRegisters, 1); + REGISTER_USER_FUNCTION(getCurrentNumberOfRegisters, 2); + REGISTER_USER_FUNCTION(getCurrentEpochProposals, 3); + REGISTER_USER_FUNCTION(getProposalStatus, 4); + REGISTER_USER_FUNCTION(getUserLockedAmount, 5); + REGISTER_USER_FUNCTION(getProposalInfo, 6); REGISTER_USER_PROCEDURE(registerInPortalDAO, 1); REGISTER_USER_PROCEDURE(submitProposal, 2); @@ -725,7 +763,8 @@ struct QPORTAL : public ContractBase state.mut().numberOfCurrentEpochProposals = 0; state.mut().userProposalStatus.reset(); - state.mut().proposalVotes.reset(); + state.mut().proposalVotes.reset(); + state.mut().currentEpochProposals.setAll(0); } INITIALIZE() From 006e07f50d1ca0ff1f4aa5a97a9cbba9caac3437 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Sat, 23 May 2026 02:42:40 +0900 Subject: [PATCH 08/18] feat: test and transferShareManagementRights --- src/contracts/QPortal.h | 80 ++- test/contract_qportal.cpp | 1012 +++++++++++++++++++++++++++++++++++++ 2 files changed, 1088 insertions(+), 4 deletions(-) diff --git a/src/contracts/QPortal.h b/src/contracts/QPortal.h index bcdc99ca4..0b67f8373 100644 --- a/src/contracts/QPortal.h +++ b/src/contracts/QPortal.h @@ -8,8 +8,10 @@ constexpr uint64 QPORTAL_REFUND_FEE = 5; //PORTAL fee charged on each refund constexpr uint32 QPORTAL_MAX_MEMBER = 4096; //Maximum number of members in the portal DAO. constexpr uint32 QPORTAL_MAX_PROPOSAL = 4096; //Maximum number of proposals in the portal DAO. constexpr uint32 QPORTAL_MAX_PROPOSAL_USER = 2; //The maximum number of proposals a user can submit. -constexpr uint32 QPORTAL_MAX_PROPOSAL_EPOCH = 5; //The maximum number +constexpr uint32 QPORTAL_MAX_PROPOSAL_EPOCH = 5; //The maximum number of proposals per epoch. +constexpr uint32 QPORTAL_EPOCH_PROPOSALS_CAPACITY = 8; //Backing capacity for currentEpochProposals (Array requires 2^N, must be >= QPORTAL_MAX_PROPOSAL_EPOCH). constexpr uint32 QPORTAL_MAX_VOTE = 32768; //The maximum number of votes a user can make (voting in all proposals in all proposal epochs). +constexpr uint32 QPORTAL_TRANSFER_SHARE_FEE = 100; constexpr uint32 QPORTAL_SUCCESS = 0; constexpr uint32 QPORTAL_INSUFFICIENT_PORTAL = 1; @@ -73,11 +75,12 @@ struct QPORTAL : public ContractBase HashMap registers; //registered members in the portal DAO uint32 numberOfRegisters, numberOfCurrentEpochProposals, numberOfProposals; HashMap userProposalStatus; // 0 = no proposal, 1 = submitted 1 proposal, 2 = submitted 2 proposals (max) - Array currentEpochProposals; // record of proposal IDs in the current proposal epoch + Array currentEpochProposals; // record of proposal IDs in the current proposal epoch HashMap submittedProposals; // 0 = voting, 1 = accepted, 2 = rejected HashMap lockedAmount; HashMap proposalVotes; // Voting records for each user per proposal epoch, used to check whether the user voted in the current proposal epoch. HashMap proposalResults; // record of vote results for all of proposals + sint64 burnAmt; }; struct getRegisters_input @@ -200,6 +203,19 @@ struct QPORTAL : public ContractBase sint32 returnCode; }; + struct transferShareManagementRights_input + { + Asset asset; + sint64 numberOfShares; + uint32 newManagementContractIndex; + }; + + struct transferShareManagementRights_output + { + sint64 transferredNumberOfShares; + sint32 returnCode; + }; + protected: struct getRegisters_locals @@ -702,6 +718,54 @@ struct QPORTAL : public ContractBase LOG_INFO(locals.log); } + struct transferShareManagementRights_locals + { + sint64 result; + sint64 transferredShares; + sint64 offeredFee; + Logger log; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(transferShareManagementRights) + { + if (qpi.invocationReward() < QPORTAL_TRANSFER_SHARE_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; + LOG_INFO(locals.log); + output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; + return ; + } + + if (qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.numberOfShares) + { + output.transferredNumberOfShares = 0; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; + LOG_INFO(locals.log); + output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; + return ; + } + + locals.offeredFee = qpi.invocationReward() - QPORTAL_TRANSFER_SHARE_FEE; + locals.result = qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, input.newManagementContractIndex, input.newManagementContractIndex, locals.offeredFee); + if (locals.result == INVALID_AMOUNT || locals.result < 0) + { + output.transferredNumberOfShares = 0; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; + LOG_INFO(locals.log); + output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; + return ; + } + + qpi.transfer(qpi.invocator(), locals.offeredFee - locals.result); + state.mut().burnAmt += QPORTAL_TRANSFER_SHARE_FEE; + + output.transferredNumberOfShares = input.numberOfShares; + output.returnCode = QPORTAL_SUCCESS; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_SUCCESS, 0 }; + LOG_INFO(locals.log); + } + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() { REGISTER_USER_FUNCTION(getRegisters, 1); @@ -716,6 +780,7 @@ struct QPORTAL : public ContractBase REGISTER_USER_PROCEDURE(submitInVote, 3); REGISTER_USER_PROCEDURE(submitOutVote, 4); REGISTER_USER_PROCEDURE(requestRefund, 5); + REGISTER_USER_PROCEDURE(transferShareManagementRights, 6); } struct END_EPOCH_locals @@ -760,11 +825,18 @@ struct QPORTAL : public ContractBase } } } + qpi.burn(state.get().burnAmt); state.mut().numberOfCurrentEpochProposals = 0; state.mut().userProposalStatus.reset(); state.mut().proposalVotes.reset(); - state.mut().currentEpochProposals.setAll(0); + state.mut().currentEpochProposals.setAll(NULL_ID); + state.mut().burnAmt = 0; + } + + PRE_ACQUIRE_SHARES() + { + output.allowTransfer = true; } INITIALIZE() @@ -774,7 +846,7 @@ struct QPORTAL : public ContractBase state.mut().numberOfCurrentEpochProposals = 0; state.mut().numberOfProposals = 0; state.mut().lockedAmount.reset(); - state.mut().currentEpochProposals.setAll(0); + state.mut().currentEpochProposals.setAll(NULL_ID); state.mut().proposalVotes.reset(); state.mut().registers.reset(); state.mut().userProposalStatus.reset(); diff --git a/test/contract_qportal.cpp b/test/contract_qportal.cpp index e69de29bb..b10699530 100644 --- a/test/contract_qportal.cpp +++ b/test/contract_qportal.cpp @@ -0,0 +1,1012 @@ +#define NO_UEFI + +#include "contract_testing.h" + +#include +#include + +// Generous per-user energy so contract calls never run dry. +static constexpr sint64 QPORTAL_USER_ENERGY = 5000000000ll; + +static const id QPORTAL_CONTRACT_ID(QPORTAL_CONTRACT_INDEX, 0, 0, 0); + +// Ordering for std::set (m256i has no operator<). +struct IdLess +{ + bool operator()(const id& a, const id& b) const + { + for (int i = 0; i < 4; i++) + if (a.m256i_u64[i] != b.m256i_u64[i]) + return a.m256i_u64[i] < b.m256i_u64[i]; + return false; + } +}; + +// Exposes QPORTAL internal state for assertions. +class QPORTALChecker : public QPORTAL, public QPORTAL::StateData +{ +public: + id getPortalIssuer() const { return PORTAL_Issuer; } + uint32 getNumberOfRegisters() const { return numberOfRegisters; } + uint32 getNumberOfProposals() const { return numberOfProposals; } + uint32 getNumberOfCurrentEpochProposals() const { return numberOfCurrentEpochProposals; } + bool isRegistered(const id& u) const { return registers.contains(u); } + bool hasProposal(const id& p) const { return submittedProposals.contains(p); } + + uint8 proposalStatusOf(const id& p) const + { + sint64 idx = submittedProposals.getElementIndex(p); + EXPECT_NE(idx, NULL_INDEX); + return submittedProposals.value(idx).proposalStatus; + } + + uint64 lockedOf(const id& u) const + { + return lockedAmount.contains(u) + ? lockedAmount.value(lockedAmount.getElementIndex(u)) + : 0ull; + } +}; + +class ContractTestingQPortal : protected ContractTesting +{ +public: + id portalIssuer; + int portalOwnershipIdx = -1; + int portalPossessionIdx = -1; + + ContractTestingQPortal() + { + initEmptySpectrum(); + initEmptyUniverse(); + + INIT_CONTRACT(QPORTAL); + callSystemProcedure(QPORTAL_CONTRACT_INDEX, INITIALIZE); + + // The contract is only callable from its construction epoch onwards. + system.epoch = contractDescriptions[QPORTAL_CONTRACT_INDEX].constructionEpoch; + + portalIssuer = getState()->getPortalIssuer(); + } + + QPORTALChecker* getState() + { + return (QPORTALChecker*)contractStates[QPORTAL_CONTRACT_INDEX]; + } + + // ---- Asset / spectrum setup helpers ----------------------------------- + // + // The PORTAL asset is issued directly with its management rights already on + // QPORTAL. This deliberately bypasses QX::TransferShareManagementRights: that + // QX path crashes in the current build (the pre-existing QIP test, which + // exercises the same path, crashes identically). Issuing the asset already + // managed by QPORTAL reproduces the same end state a real user reaches after + // transferring management rights, so register / vote / refund behave normally. + + // Issues the whole PORTAL supply to the hardcoded issuer, managed by QPORTAL. + void issuePortal(sint64 totalSupply) + { + char name[7] = { 'P', 'O', 'R', 'T', 'A', 'L', 0 }; + char unit[7] = { 0, 0, 0, 0, 0, 0, 0 }; + int issuanceIdx = -1; + EXPECT_EQ(issueAsset(portalIssuer, name, 0, unit, totalSupply, + (unsigned short)QPORTAL_CONTRACT_INDEX, + &issuanceIdx, &portalOwnershipIdx, &portalPossessionIdx), + totalSupply); + } + + // Gives `user` `amount` PORTAL shares (already managed by QPORTAL) plus energy. + void fundUser(const id& user, sint64 amount) + { + increaseEnergy(user, QPORTAL_USER_ENERGY); + int dstO = -1, dstP = -1; + EXPECT_TRUE(transferShareOwnershipAndPossession( + portalOwnershipIdx, portalPossessionIdx, user, amount, &dstO, &dstP, true)); + } + + sint64 portalBalanceOnQPortal(const id& owner) + { + return numberOfPossessedShares( + QPORTAL_PORTAL_ASSET_NAME, portalIssuer, owner, owner, + QPORTAL_CONTRACT_INDEX, QPORTAL_CONTRACT_INDEX); + } + + // ---- QPORTAL procedures ----------------------------------------------- + + sint32 registerInPortalDAO(const id& user) + { + QPORTAL::registerInPortalDAO_input input; + QPORTAL::registerInPortalDAO_output output; + invokeUserProcedure(QPORTAL_CONTRACT_INDEX, 1, input, output, user, 0); + return output.returnCode; + } + + sint32 submitProposal(const id& user, const id& proposalId) + { + QPORTAL::submitProposal_input input; + input.proposalId = proposalId; + QPORTAL::submitProposal_output output; + invokeUserProcedure(QPORTAL_CONTRACT_INDEX, 2, input, output, user, 0); + return output.returnCode; + } + + sint32 submitInVote(const id& user, const id& proposalId, int vote, uint64 amount) + { + QPORTAL::submitInVote_input input; + input.proposalId = proposalId; + input.vote = (vote != 0); + input.votingPortalAmount = amount; + QPORTAL::submitInVote_output output; + invokeUserProcedure(QPORTAL_CONTRACT_INDEX, 3, input, output, user, 0); + return output.returnCode; + } + + sint32 submitOutVote(const id& user, const id& proposalId) + { + QPORTAL::submitOutVote_input input; + input.proposalId = proposalId; + QPORTAL::submitOutVote_output output; + invokeUserProcedure(QPORTAL_CONTRACT_INDEX, 4, input, output, user, 0); + return output.returnCode; + } + + sint32 requestRefund(const id& user, uint64 amount) + { + QPORTAL::requestRefund_input input; + input.amount = amount; + QPORTAL::requestRefund_output output; + invokeUserProcedure(QPORTAL_CONTRACT_INDEX, 5, input, output, user, 0); + return output.returnCode; + } + + // ---- QPORTAL functions ------------------------------------------------ + + QPORTAL::getRegisters_output getRegisters(uint32 offset, uint32 limit) + { + QPORTAL::getRegisters_input input; + input.offset = offset; + input.limit = limit; + QPORTAL::getRegisters_output output; + callFunction(QPORTAL_CONTRACT_INDEX, 1, input, output); + return output; + } + + // Collects the non-null member ids returned by getRegisters into a vector. + std::vector getRegistersVec(uint32 offset, uint32 limit) + { + QPORTAL::getRegisters_output o = getRegisters(offset, limit); + EXPECT_EQ(o.returnCode, (sint32)QPORTAL_SUCCESS); + std::vector result; + const id slots[10] = { o.register1, o.register2, o.register3, o.register4, o.register5, + o.register6, o.register7, o.register8, o.register9, o.register10 }; + for (int i = 0; i < 10; i++) + if (slots[i] != NULL_ID) + result.push_back(slots[i]); + return result; + } + + uint32 getCurrentNumberOfRegisters() + { + QPORTAL::getCurrentNumberOfRegisters_input input; + QPORTAL::getCurrentNumberOfRegisters_output output; + callFunction(QPORTAL_CONTRACT_INDEX, 2, input, output); + EXPECT_EQ(output.returnCode, (sint32)QPORTAL_SUCCESS); + return output.numberOfRegisters; + } + + QPORTAL::getCurrentEpochProposals_output getCurrentEpochProposals() + { + QPORTAL::getCurrentEpochProposals_input input; + QPORTAL::getCurrentEpochProposals_output output; + callFunction(QPORTAL_CONTRACT_INDEX, 3, input, output); + return output; + } + + QPORTAL::getProposalStatus_output getProposalStatus(const id& proposalId) + { + QPORTAL::getProposalStatus_input input; + input.proposalId = proposalId; + QPORTAL::getProposalStatus_output output; + callFunction(QPORTAL_CONTRACT_INDEX, 4, input, output); + return output; + } + + QPORTAL::getProposalInfo_output getProposalInfo(const id& proposalId) + { + QPORTAL::getProposalInfo_input input; + input.proposalId = proposalId; + QPORTAL::getProposalInfo_output output; + callFunction(QPORTAL_CONTRACT_INDEX, 6, input, output); + return output; + } + + void endEpoch() + { + callSystemProcedure(QPORTAL_CONTRACT_INDEX, END_EPOCH); + } +}; + +// Deterministic, well-scattered distinct ids for test members / proposals. +static id qpMember(int n) +{ + return id(n * 0x9E3779B97F4A7C15ull + 1, n * 7ull + 3, n * 13ull + 5, n * 17ull + 9); +} +static id qpProposal(int n) +{ + return id(n * 0xC2B2AE3D27D4EB4Full + 7, n * 11ull + 1, n * 19ull + 2, n * 23ull + 4); +} + +// =========================================================================== +// Registration +// =========================================================================== + +TEST(ContractQPortal, Register_Success) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + + id user = qpMember(1); + qp.fundUser(user, 1000); + + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_SUCCESS); + EXPECT_TRUE(qp.getState()->isRegistered(user)); + EXPECT_EQ(qp.getState()->getNumberOfRegisters(), 1u); + + // Register fee (5 PORTAL) moved from the user to the contract. + EXPECT_EQ(qp.portalBalanceOnQPortal(user), 1000 - (sint64)QPORTAL_REGISTER_FEE); + EXPECT_EQ(qp.portalBalanceOnQPortal(QPORTAL_CONTRACT_ID), (sint64)QPORTAL_REGISTER_FEE); +} + +TEST(ContractQPortal, Register_AlreadyRegistered) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + + id user = qpMember(1); + qp.fundUser(user, 1000); + + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_ALREADY_REGISTERED); + EXPECT_EQ(qp.getState()->getNumberOfRegisters(), 1u); +} + +TEST(ContractQPortal, Register_InsufficientPortal) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + + id user = qpMember(1); + qp.fundUser(user, QPORTAL_REGISTER_FEE - 1); // below the register fee + + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_INSUFFICIENT_PORTAL); + EXPECT_FALSE(qp.getState()->isRegistered(user)); + EXPECT_EQ(qp.getState()->getNumberOfRegisters(), 0u); +} + +TEST(ContractQPortal, Register_NoPortalShares) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + + // User has a spectrum entry but holds no PORTAL shares. + id user = qpMember(1); + increaseEnergy(user, QPORTAL_USER_ENERGY); + + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_INSUFFICIENT_PORTAL); + EXPECT_FALSE(qp.getState()->isRegistered(user)); +} + +TEST(ContractQPortal, Register_MultipleMembers) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + + for (int i = 0; i < 6; i++) + { + id user = qpMember(i); + qp.fundUser(user, 100); + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_SUCCESS); + } + EXPECT_EQ(qp.getState()->getNumberOfRegisters(), 6u); + EXPECT_EQ(qp.getCurrentNumberOfRegisters(), 6u); +} + +// =========================================================================== +// getRegisters +// =========================================================================== + +TEST(ContractQPortal, GetRegisters_InvalidLimit) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + id user = qpMember(1); + qp.fundUser(user, 100); + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_SUCCESS); + + EXPECT_EQ(qp.getRegisters(0, 0).returnCode, (sint32)QPORTAL_INVALID_OFFSET_OR_LIMIT); + EXPECT_EQ(qp.getRegisters(0, 11).returnCode, (sint32)QPORTAL_INVALID_OFFSET_OR_LIMIT); +} + +TEST(ContractQPortal, GetRegisters_OffsetOutOfRange) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + id user = qpMember(1); + qp.fundUser(user, 100); + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_SUCCESS); + + // offset == numberOfRegisters is out of range + EXPECT_EQ(qp.getRegisters(1, 5).returnCode, (sint32)QPORTAL_INVALID_OFFSET_OR_LIMIT); + EXPECT_EQ(qp.getRegisters(5, 5).returnCode, (sint32)QPORTAL_INVALID_OFFSET_OR_LIMIT); +} + +TEST(ContractQPortal, GetRegisters_ReturnsAllMembers) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + + std::set registered; + for (int i = 0; i < 7; i++) + { + id user = qpMember(i); + qp.fundUser(user, 100); + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_SUCCESS); + registered.insert(user); + } + + std::vector page = qp.getRegistersVec(0, 10); + EXPECT_EQ(page.size(), 7u); + std::set returned(page.begin(), page.end()); + EXPECT_EQ(returned.size(), 7u); // no duplicates + EXPECT_TRUE(returned == registered); // exactly the registered set +} + +TEST(ContractQPortal, GetRegisters_Pagination) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + + std::set registered; + const int total = 23; + for (int i = 0; i < total; i++) + { + id user = qpMember(i); + qp.fundUser(user, 100); + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_SUCCESS); + registered.insert(user); + } + EXPECT_EQ(qp.getCurrentNumberOfRegisters(), (uint32)total); + + // Walk every page of 10 and verify the union is exactly the member set, + // with no overlap between pages. + std::set seen; + for (uint32 offset = 0; offset < (uint32)total; offset += 10) + { + std::vector page = qp.getRegistersVec(offset, 10); + uint32 expectedCount = ((uint32)total - offset) < 10 ? ((uint32)total - offset) : 10; + EXPECT_EQ(page.size(), expectedCount); + for (const id& m : page) + { + EXPECT_TRUE(registered.count(m) == 1); // belongs to the member set + EXPECT_TRUE(seen.insert(m).second); // not seen on a previous page + } + } + EXPECT_TRUE(seen == registered); +} + +TEST(ContractQPortal, GetRegisters_LimitCapsResult) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + for (int i = 0; i < 8; i++) + { + id user = qpMember(i); + qp.fundUser(user, 100); + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_SUCCESS); + } + + EXPECT_EQ(qp.getRegistersVec(0, 3).size(), 3u); // limited by limit + EXPECT_EQ(qp.getRegistersVec(6, 10).size(), 2u); // limited by remaining + EXPECT_EQ(qp.getRegistersVec(7, 10).size(), 1u); +} + +// =========================================================================== +// submitProposal +// =========================================================================== + +TEST(ContractQPortal, SubmitProposal_Success) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + id user = qpMember(1); + qp.fundUser(user, 100); + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_SUCCESS); + + id proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(user, proposal), (sint32)QPORTAL_SUCCESS); + EXPECT_TRUE(qp.getState()->hasProposal(proposal)); + EXPECT_EQ(qp.getState()->getNumberOfProposals(), 1u); + EXPECT_EQ(qp.getState()->getNumberOfCurrentEpochProposals(), 1u); + EXPECT_EQ(qp.getProposalStatus(proposal).status, 0u); // voting +} + +TEST(ContractQPortal, SubmitProposal_NotRegistered) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + id user = qpMember(1); + increaseEnergy(user, QPORTAL_USER_ENERGY); + + EXPECT_EQ(qp.submitProposal(user, qpProposal(1)), (sint32)QPORTAL_NOT_REGISTERED); +} + +TEST(ContractQPortal, SubmitProposal_InvalidInput) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + id user = qpMember(1); + qp.fundUser(user, 100); + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_SUCCESS); + + EXPECT_EQ(qp.submitProposal(user, NULL_ID), (sint32)QPORTAL_INVALID_INPUT); +} + +TEST(ContractQPortal, SubmitProposal_AlreadyExists) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + id userA = qpMember(1), userB = qpMember(2); + qp.fundUser(userA, 100); + qp.fundUser(userB, 100); + EXPECT_EQ(qp.registerInPortalDAO(userA), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.registerInPortalDAO(userB), (sint32)QPORTAL_SUCCESS); + + id proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(userA, proposal), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitProposal(userB, proposal), (sint32)QPORTAL_ALREADY_EXISTED_PROPOSAL); +} + +TEST(ContractQPortal, SubmitProposal_MaxPerUser) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + id user = qpMember(1); + qp.fundUser(user, 100); + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_SUCCESS); + + EXPECT_EQ(qp.submitProposal(user, qpProposal(1)), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitProposal(user, qpProposal(2)), (sint32)QPORTAL_SUCCESS); + // QPORTAL_MAX_PROPOSAL_USER == 2 + EXPECT_EQ(qp.submitProposal(user, qpProposal(3)), (sint32)QPORTAL_REACHED_PROPOSAL); + EXPECT_EQ(qp.getState()->getNumberOfProposals(), 2u); +} + +TEST(ContractQPortal, SubmitProposal_MaxPerEpoch) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + + // QPORTAL_MAX_PROPOSAL_EPOCH == 5; with 2 proposals/user we need 3 members. + for (int i = 0; i < 3; i++) + { + qp.fundUser(qpMember(i), 100); + EXPECT_EQ(qp.registerInPortalDAO(qpMember(i)), (sint32)QPORTAL_SUCCESS); + } + EXPECT_EQ(qp.submitProposal(qpMember(0), qpProposal(1)), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitProposal(qpMember(0), qpProposal(2)), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitProposal(qpMember(1), qpProposal(3)), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitProposal(qpMember(1), qpProposal(4)), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitProposal(qpMember(2), qpProposal(5)), (sint32)QPORTAL_SUCCESS); + // 6th proposal of the epoch is rejected + EXPECT_EQ(qp.submitProposal(qpMember(2), qpProposal(6)), (sint32)QPORTAL_REACHED_PROPOSAL); + EXPECT_EQ(qp.getState()->getNumberOfCurrentEpochProposals(), 5u); +} + +TEST(ContractQPortal, GetCurrentEpochProposals_ReflectsSubmissions) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + id user = qpMember(1); + qp.fundUser(user, 100); + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_SUCCESS); + + id p1 = qpProposal(1), p2 = qpProposal(2); + EXPECT_EQ(qp.submitProposal(user, p1), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitProposal(user, p2), (sint32)QPORTAL_SUCCESS); + + QPORTAL::getCurrentEpochProposals_output o = qp.getCurrentEpochProposals(); + EXPECT_EQ(o.returnCode, (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(o.proposal1, p1); + EXPECT_EQ(o.proposal2, p2); + EXPECT_EQ(o.proposal3, NULL_ID); +} + +// =========================================================================== +// submitInVote +// =========================================================================== + +TEST(ContractQPortal, SubmitInVote_SuccessYes) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(1), voter = qpMember(2); + qp.fundUser(proposer, 100); + qp.fundUser(voter, 50000); + EXPECT_EQ(qp.registerInPortalDAO(proposer), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + + id proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(proposer, proposal), (sint32)QPORTAL_SUCCESS); + + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 30000), (sint32)QPORTAL_SUCCESS); + + // Voting PORTAL is locked and transferred to the contract. + EXPECT_EQ(qp.getState()->lockedOf(voter), 30000ull); + EXPECT_EQ(qp.portalBalanceOnQPortal(voter), 50000 - (sint64)QPORTAL_REGISTER_FEE - 30000); + + QPORTAL::getProposalInfo_output info = qp.getProposalInfo(proposal); + EXPECT_EQ(info.returnCode, (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(info.yesVotes, 1u); + EXPECT_EQ(info.noVotes, 0u); + EXPECT_EQ(info.yesPortal, 30000ull); + EXPECT_EQ(info.noPortal, 0ull); + EXPECT_EQ(info.proposer, proposer); +} + +TEST(ContractQPortal, SubmitInVote_SuccessNo) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(1), voter = qpMember(2); + qp.fundUser(proposer, 100); + qp.fundUser(voter, 50000); + EXPECT_EQ(qp.registerInPortalDAO(proposer), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + id proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(proposer, proposal), (sint32)QPORTAL_SUCCESS); + + EXPECT_EQ(qp.submitInVote(voter, proposal, 0, 12345), (sint32)QPORTAL_SUCCESS); + + QPORTAL::getProposalInfo_output info = qp.getProposalInfo(proposal); + EXPECT_EQ(info.yesVotes, 0u); + EXPECT_EQ(info.noVotes, 1u); + EXPECT_EQ(info.yesPortal, 0ull); + EXPECT_EQ(info.noPortal, 12345ull); +} + +TEST(ContractQPortal, SubmitInVote_NotRegistered) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(1), voter = qpMember(2); + qp.fundUser(proposer, 100); + qp.fundUser(voter, 50000); + EXPECT_EQ(qp.registerInPortalDAO(proposer), (sint32)QPORTAL_SUCCESS); + id proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(proposer, proposal), (sint32)QPORTAL_SUCCESS); + + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 1000), (sint32)QPORTAL_NOT_REGISTERED); +} + +TEST(ContractQPortal, SubmitInVote_InvalidInput) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(1), voter = qpMember(2); + qp.fundUser(proposer, 100); + qp.fundUser(voter, 50000); + EXPECT_EQ(qp.registerInPortalDAO(proposer), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + id proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(proposer, proposal), (sint32)QPORTAL_SUCCESS); + + EXPECT_EQ(qp.submitInVote(voter, NULL_ID, 1, 1000), (sint32)QPORTAL_INVALID_INPUT); + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 0), (sint32)QPORTAL_INVALID_INPUT); +} + +TEST(ContractQPortal, SubmitInVote_ProposalNotExist) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id voter = qpMember(2); + qp.fundUser(voter, 50000); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + + EXPECT_EQ(qp.submitInVote(voter, qpProposal(99), 1, 1000), (sint32)QPORTAL_NOT_EXISTED_PROPOSAL); +} + +TEST(ContractQPortal, SubmitInVote_AlreadyVoted) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(1), voter = qpMember(2); + qp.fundUser(proposer, 100); + qp.fundUser(voter, 50000); + EXPECT_EQ(qp.registerInPortalDAO(proposer), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + id proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(proposer, proposal), (sint32)QPORTAL_SUCCESS); + + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 1000), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 1000), (sint32)QPORTAL_ALREADY_VOTED_PROPOSAL); + // Only the first vote locked PORTAL. + EXPECT_EQ(qp.getState()->lockedOf(voter), 1000ull); +} + +TEST(ContractQPortal, SubmitInVote_InsufficientPortal) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(1), voter = qpMember(2); + qp.fundUser(proposer, 100); + qp.fundUser(voter, 1000); + EXPECT_EQ(qp.registerInPortalDAO(proposer), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + id proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(proposer, proposal), (sint32)QPORTAL_SUCCESS); + + // voter only has 1000 - 5 = 995 PORTAL left after registration + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 5000), (sint32)QPORTAL_INSUFFICIENT_PORTAL); + EXPECT_EQ(qp.getState()->lockedOf(voter), 0ull); +} + +TEST(ContractQPortal, SubmitInVote_MultipleVotersTally) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(0); + qp.fundUser(proposer, 100); + EXPECT_EQ(qp.registerInPortalDAO(proposer), (sint32)QPORTAL_SUCCESS); + id proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(proposer, proposal), (sint32)QPORTAL_SUCCESS); + + for (int i = 1; i <= 3; i++) + { + id v = qpMember(i); + qp.fundUser(v, 100000); + EXPECT_EQ(qp.registerInPortalDAO(v), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitInVote(v, proposal, 1, 10000), (sint32)QPORTAL_SUCCESS); + } + id vno = qpMember(4); + qp.fundUser(vno, 100000); + EXPECT_EQ(qp.registerInPortalDAO(vno), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitInVote(vno, proposal, 0, 7000), (sint32)QPORTAL_SUCCESS); + + QPORTAL::getProposalInfo_output info = qp.getProposalInfo(proposal); + EXPECT_EQ(info.yesVotes, 3u); + EXPECT_EQ(info.noVotes, 1u); + EXPECT_EQ(info.yesPortal, 30000ull); + EXPECT_EQ(info.noPortal, 7000ull); +} + +// =========================================================================== +// submitOutVote +// =========================================================================== + +TEST(ContractQPortal, SubmitOutVote_Success) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(1), voter = qpMember(2); + qp.fundUser(proposer, 100); + qp.fundUser(voter, 50000); + EXPECT_EQ(qp.registerInPortalDAO(proposer), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + id proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(proposer, proposal), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 8000), (sint32)QPORTAL_SUCCESS); + + EXPECT_EQ(qp.submitOutVote(voter, proposal), (sint32)QPORTAL_SUCCESS); + + // The vote is removed from the tally. + QPORTAL::getProposalInfo_output info = qp.getProposalInfo(proposal); + EXPECT_EQ(info.yesVotes, 0u); + EXPECT_EQ(info.yesPortal, 0ull); +} + +TEST(ContractQPortal, SubmitOutVote_NotVoted) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(1), voter = qpMember(2); + qp.fundUser(proposer, 100); + qp.fundUser(voter, 50000); + EXPECT_EQ(qp.registerInPortalDAO(proposer), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + id proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(proposer, proposal), (sint32)QPORTAL_SUCCESS); + + EXPECT_EQ(qp.submitOutVote(voter, proposal), (sint32)QPORTAL_NOT_VOTED_PROPOSAL); +} + +TEST(ContractQPortal, SubmitOutVote_ProposalNotExist) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id voter = qpMember(2); + qp.fundUser(voter, 50000); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + + EXPECT_EQ(qp.submitOutVote(voter, qpProposal(99)), (sint32)QPORTAL_NOT_EXISTED_PROPOSAL); +} + +TEST(ContractQPortal, SubmitOutVote_ThenRevoteAllowed) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(1), voter = qpMember(2); + qp.fundUser(proposer, 100); + qp.fundUser(voter, 50000); + EXPECT_EQ(qp.registerInPortalDAO(proposer), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + id proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(proposer, proposal), (sint32)QPORTAL_SUCCESS); + + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 5000), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitOutVote(voter, proposal), (sint32)QPORTAL_SUCCESS); + // Re-voting after withdrawing is allowed. + EXPECT_EQ(qp.submitInVote(voter, proposal, 0, 3000), (sint32)QPORTAL_SUCCESS); + + QPORTAL::getProposalInfo_output info = qp.getProposalInfo(proposal); + EXPECT_EQ(info.noVotes, 1u); + EXPECT_EQ(info.noPortal, 3000ull); +} + +// =========================================================================== +// requestRefund +// =========================================================================== + +TEST(ContractQPortal, RequestRefund_BlockedWhileVoteActive) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(1), voter = qpMember(2); + qp.fundUser(proposer, 100); + qp.fundUser(voter, 50000); + EXPECT_EQ(qp.registerInPortalDAO(proposer), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + id proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(proposer, proposal), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 10000), (sint32)QPORTAL_SUCCESS); + + // The locked PORTAL backs an active vote, so a refund is blocked. + EXPECT_EQ(qp.requestRefund(voter, 5000), (sint32)QPORTAL_EXISTED_PROPOSAL); +} + +TEST(ContractQPortal, RequestRefund_InvalidAmount) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id voter = qpMember(2); + qp.fundUser(voter, 50000); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + + // amount must be strictly greater than the refund fee + EXPECT_EQ(qp.requestRefund(voter, QPORTAL_REFUND_FEE), (sint32)QPORTAL_INVALID_INPUT); +} + +TEST(ContractQPortal, RequestRefund_InsufficientLocked) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id voter = qpMember(2); + qp.fundUser(voter, 50000); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + + // Nothing locked yet. + EXPECT_EQ(qp.requestRefund(voter, 1000), (sint32)QPORTAL_INSUFFICIENT_PORTAL); +} + +TEST(ContractQPortal, RequestRefund_SuccessAfterOutVote) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(1), voter = qpMember(2); + qp.fundUser(proposer, 100); + qp.fundUser(voter, 50000); + EXPECT_EQ(qp.registerInPortalDAO(proposer), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + id proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(proposer, proposal), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 10000), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitOutVote(voter, proposal), (sint32)QPORTAL_SUCCESS); + + sint64 before = qp.portalBalanceOnQPortal(voter); + EXPECT_EQ(qp.requestRefund(voter, 10000), (sint32)QPORTAL_SUCCESS); + + // Refund returns amount - fee; lockedAmount is fully cleared. + EXPECT_EQ(qp.portalBalanceOnQPortal(voter), before + 10000 - (sint64)QPORTAL_REFUND_FEE); + EXPECT_EQ(qp.getState()->lockedOf(voter), 0ull); +} + +TEST(ContractQPortal, RequestRefund_SuccessAfterEpoch) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(1), voter = qpMember(2); + qp.fundUser(proposer, 100); + qp.fundUser(voter, 50000); + EXPECT_EQ(qp.registerInPortalDAO(proposer), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + id proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(proposer, proposal), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 20000), (sint32)QPORTAL_SUCCESS); + + // After the epoch ends, votes are cleared and the lock can be withdrawn. + qp.endEpoch(); + EXPECT_EQ(qp.getState()->lockedOf(voter), 20000ull); + + sint64 before = qp.portalBalanceOnQPortal(voter); + EXPECT_EQ(qp.requestRefund(voter, 20000), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.portalBalanceOnQPortal(voter), before + 20000 - (sint64)QPORTAL_REFUND_FEE); + EXPECT_EQ(qp.getState()->lockedOf(voter), 0ull); +} + +// =========================================================================== +// END_EPOCH resolution +// =========================================================================== + +// Helper: register a proposer and submit one proposal. +static void setupProposal(ContractTestingQPortal& qp, const id& proposer, const id& proposal) +{ + qp.fundUser(proposer, 100); + EXPECT_EQ(qp.registerInPortalDAO(proposer), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitProposal(proposer, proposal), (sint32)QPORTAL_SUCCESS); +} + +TEST(ContractQPortal, EndEpoch_ProposalAccepted) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(0); + id proposal = qpProposal(1); + setupProposal(qp, proposer, proposal); + + // yesVotes > noVotes, yesPortal > noPortal, total >= QPORTAL_MIN_HOLDING_PORTAL (100000) + id v1 = qpMember(1), v2 = qpMember(2); + qp.fundUser(v1, 200000); + qp.fundUser(v2, 200000); + EXPECT_EQ(qp.registerInPortalDAO(v1), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.registerInPortalDAO(v2), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitInVote(v1, proposal, 1, 70000), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitInVote(v2, proposal, 1, 40000), (sint32)QPORTAL_SUCCESS); + + qp.endEpoch(); + + EXPECT_EQ(qp.getState()->proposalStatusOf(proposal), 1); // accepted + EXPECT_EQ(qp.getProposalStatus(proposal).status, 1u); +} + +TEST(ContractQPortal, EndEpoch_ProposalRejectedLowHolding) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(0); + id proposal = qpProposal(1); + setupProposal(qp, proposer, proposal); + + // Total voting PORTAL below QPORTAL_MIN_HOLDING_PORTAL (100000). + id v1 = qpMember(1); + qp.fundUser(v1, 200000); + EXPECT_EQ(qp.registerInPortalDAO(v1), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitInVote(v1, proposal, 1, 30000), (sint32)QPORTAL_SUCCESS); + + qp.endEpoch(); + EXPECT_EQ(qp.getState()->proposalStatusOf(proposal), 2); // rejected +} + +TEST(ContractQPortal, EndEpoch_ProposalRejectedNoVotes) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(0); + id proposal = qpProposal(1); + setupProposal(qp, proposer, proposal); + + qp.endEpoch(); + EXPECT_EQ(qp.getState()->proposalStatusOf(proposal), 2); // rejected +} + +TEST(ContractQPortal, EndEpoch_ProposalRejectedNoMajority) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(0); + id proposal = qpProposal(1); + setupProposal(qp, proposer, proposal); + + // total >= 100000 but noPortal > yesPortal + id v1 = qpMember(1), v2 = qpMember(2); + qp.fundUser(v1, 200000); + qp.fundUser(v2, 200000); + EXPECT_EQ(qp.registerInPortalDAO(v1), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.registerInPortalDAO(v2), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitInVote(v1, proposal, 1, 40000), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitInVote(v2, proposal, 0, 70000), (sint32)QPORTAL_SUCCESS); + + qp.endEpoch(); + EXPECT_EQ(qp.getState()->proposalStatusOf(proposal), 2); // rejected +} + +TEST(ContractQPortal, EndEpoch_ResetsEpochState) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id user = qpMember(0); + qp.fundUser(user, 100); + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitProposal(user, qpProposal(1)), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitProposal(user, qpProposal(2)), (sint32)QPORTAL_SUCCESS); + + qp.endEpoch(); + + // Current-epoch counters reset; lifetime proposal count preserved. + EXPECT_EQ(qp.getState()->getNumberOfCurrentEpochProposals(), 0u); + EXPECT_EQ(qp.getState()->getNumberOfProposals(), 2u); + + // currentEpochProposals array is cleared. + QPORTAL::getCurrentEpochProposals_output o = qp.getCurrentEpochProposals(); + EXPECT_EQ(o.proposal1, NULL_ID); + EXPECT_EQ(o.proposal2, NULL_ID); + + // userProposalStatus reset: the user may submit the per-user max again. + EXPECT_EQ(qp.submitProposal(user, qpProposal(3)), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitProposal(user, qpProposal(4)), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitProposal(user, qpProposal(5)), (sint32)QPORTAL_REACHED_PROPOSAL); +} + +TEST(ContractQPortal, EndEpoch_ClosedProposalRejectsVotes) +{ + ContractTestingQPortal qp; + qp.issuePortal(10000000); + id proposer = qpMember(0); + id proposal = qpProposal(1); + setupProposal(qp, proposer, proposal); + qp.endEpoch(); // proposal resolved (rejected, no votes) + + id voter = qpMember(1); + qp.fundUser(voter, 50000); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + // Voting on a resolved proposal is rejected. + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 1000), (sint32)QPORTAL_CLOSED_PROPOSAL); +} + +// =========================================================================== +// getProposalInfo / getProposalStatus edge cases +// =========================================================================== + +TEST(ContractQPortal, GetProposalInfo_NotExist) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + EXPECT_EQ(qp.getProposalInfo(qpProposal(123)).returnCode, (sint32)QPORTAL_NOT_EXISTED_PROPOSAL); +} + +TEST(ContractQPortal, GetProposalInfo_VotelessProposalHasZeroCounts) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + id user = qpMember(0); + qp.fundUser(user, 100); + EXPECT_EQ(qp.registerInPortalDAO(user), (sint32)QPORTAL_SUCCESS); + id proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(user, proposal), (sint32)QPORTAL_SUCCESS); + + // A submitted-but-not-yet-voted proposal must report zeroed counts, not garbage. + QPORTAL::getProposalInfo_output info = qp.getProposalInfo(proposal); + EXPECT_EQ(info.returnCode, (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(info.proposer, user); + EXPECT_EQ(info.status, 0u); + EXPECT_EQ(info.yesVotes, 0u); + EXPECT_EQ(info.noVotes, 0u); + EXPECT_EQ(info.yesPortal, 0ull); + EXPECT_EQ(info.noPortal, 0ull); +} + +TEST(ContractQPortal, GetProposalStatus_InvalidAndMissing) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + EXPECT_EQ(qp.getProposalStatus(NULL_ID).returnCode, (sint32)QPORTAL_INVALID_INPUT); + EXPECT_EQ(qp.getProposalStatus(qpProposal(7)).returnCode, (sint32)QPORTAL_NOT_EXISTED_PROPOSAL); +} From a3e4cad3988ed2d77b4035daacca667be4a8ba13 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Sat, 23 May 2026 03:38:24 +0900 Subject: [PATCH 09/18] chore: fix build error and fix header and add new test --- src/contracts/QPortal.h | 14 +++--- test/contract_qportal.cpp | 96 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/src/contracts/QPortal.h b/src/contracts/QPortal.h index 0b67f8373..c6d46aee7 100644 --- a/src/contracts/QPortal.h +++ b/src/contracts/QPortal.h @@ -1,5 +1,4 @@ using namespace QPI; -#include "qpi.h" constexpr uint64 QPORTAL_PORTAL_ASSET_NAME = 83843471265616; //PORTAL asset name constexpr uint64 QPORTAL_MIN_HOLDING_PORTAL = 100000ull; //proposal must have at least 100K PORTAL to approve a proposal. @@ -221,7 +220,7 @@ struct QPORTAL : public ContractBase struct getRegisters_locals { id user; - sint32 index; + sint64 index; uint32 i, cnt; }; @@ -400,7 +399,8 @@ struct QPORTAL : public ContractBase struct submitProposal_locals { - sint32 index, status; + sint64 index; + uint32 status; Logger log; }; @@ -478,7 +478,7 @@ struct QPORTAL : public ContractBase struct submitInVote_locals { id key; - sint32 index; + sint64 index; uint64 amount; sint64 ownedShares; voteKey vk; @@ -593,7 +593,7 @@ struct QPORTAL : public ContractBase struct submitOutVote_locals { - sint32 index; + sint64 index; voteRecord vr; voteResult tmp; Logger log; @@ -665,7 +665,7 @@ struct QPORTAL : public ContractBase struct requestRefund_locals { - sint32 i; + uint32 i; Logger log; }; @@ -785,7 +785,7 @@ struct QPORTAL : public ContractBase struct END_EPOCH_locals { - uint32 i, index; + sint64 i, index; id proposalId; uint32 yesVotes, noVotes; uint64 yesPortal, noPortal; diff --git a/test/contract_qportal.cpp b/test/contract_qportal.cpp index b10699530..c509f64c9 100644 --- a/test/contract_qportal.cpp +++ b/test/contract_qportal.cpp @@ -46,6 +46,8 @@ class QPORTALChecker : public QPORTAL, public QPORTAL::StateData ? lockedAmount.value(lockedAmount.getElementIndex(u)) : 0ull; } + + sint64 getBurnAmt() const { return burnAmt; } }; class ContractTestingQPortal : protected ContractTesting @@ -60,6 +62,10 @@ class ContractTestingQPortal : protected ContractTesting initEmptySpectrum(); initEmptyUniverse(); + // QX is needed as a destination managing contract for transferShareManagementRights. + INIT_CONTRACT(QX); + callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QPORTAL); callSystemProcedure(QPORTAL_CONTRACT_INDEX, INITIALIZE); @@ -159,6 +165,26 @@ class ContractTestingQPortal : protected ContractTesting return output.returnCode; } + QPORTAL::transferShareManagementRights_output transferShareMgmtRights( + const id& user, uint64 numberOfShares, uint32 newMgmtIdx, sint64 invocationReward) + { + QPORTAL::transferShareManagementRights_input input; + input.asset.assetName = QPORTAL_PORTAL_ASSET_NAME; + input.asset.issuer = portalIssuer; + input.numberOfShares = numberOfShares; + input.newManagementContractIndex = newMgmtIdx; + QPORTAL::transferShareManagementRights_output output; + invokeUserProcedure(QPORTAL_CONTRACT_INDEX, 6, input, output, user, invocationReward); + return output; + } + + sint64 portalBalanceOnQX(const id& owner) + { + return numberOfPossessedShares( + QPORTAL_PORTAL_ASSET_NAME, portalIssuer, owner, owner, + QX_CONTRACT_INDEX, QX_CONTRACT_INDEX); + } + // ---- QPORTAL functions ------------------------------------------------ QPORTAL::getRegisters_output getRegisters(uint32 offset, uint32 limit) @@ -1010,3 +1036,73 @@ TEST(ContractQPortal, GetProposalStatus_InvalidAndMissing) EXPECT_EQ(qp.getProposalStatus(NULL_ID).returnCode, (sint32)QPORTAL_INVALID_INPUT); EXPECT_EQ(qp.getProposalStatus(qpProposal(7)).returnCode, (sint32)QPORTAL_NOT_EXISTED_PROPOSAL); } + +// =========================================================================== +// transferShareManagementRights +// =========================================================================== + +TEST(ContractQPortal, TransferShareMgmtRights_InsufficientReward) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + id user = qpMember(1); + qp.fundUser(user, 1000); + + // invocationReward below QPORTAL_TRANSFER_SHARE_FEE (100) is rejected. + QPORTAL::transferShareManagementRights_output o = + qp.transferShareMgmtRights(user, 500, QX_CONTRACT_INDEX, QPORTAL_TRANSFER_SHARE_FEE - 1); + EXPECT_EQ(o.returnCode, (sint32)QPORTAL_INSUFFICIENT_PORTAL); + // Management is unchanged: all shares still on QPORTAL. + EXPECT_EQ(qp.portalBalanceOnQPortal(user), 1000); +} + +TEST(ContractQPortal, TransferShareMgmtRights_InsufficientShares) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + id user = qpMember(1); + qp.fundUser(user, 50); + + // Requesting more shares than the user holds is rejected. + QPORTAL::transferShareManagementRights_output o = + qp.transferShareMgmtRights(user, 1000, QX_CONTRACT_INDEX, QPORTAL_TRANSFER_SHARE_FEE); + EXPECT_EQ(o.returnCode, (sint32)QPORTAL_INSUFFICIENT_PORTAL); + EXPECT_EQ(o.transferredNumberOfShares, 0ll); + EXPECT_EQ(qp.portalBalanceOnQPortal(user), 50); +} + +TEST(ContractQPortal, TransferShareMgmtRights_SelfDestinationRejected) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + id user = qpMember(1); + qp.fundUser(user, 1000); + + // releaseShares rejects a transfer whose destination is the current contract. + QPORTAL::transferShareManagementRights_output o = + qp.transferShareMgmtRights(user, 500, QPORTAL_CONTRACT_INDEX, 5000000); + EXPECT_EQ(o.returnCode, (sint32)QPORTAL_INSUFFICIENT_PORTAL); + EXPECT_EQ(o.transferredNumberOfShares, 0ll); + EXPECT_EQ(qp.portalBalanceOnQPortal(user), 1000); +} + +TEST(ContractQPortal, TransferShareMgmtRights_SuccessToQX) +{ + ContractTestingQPortal qp; + qp.issuePortal(1000000); + id user = qpMember(1); + qp.fundUser(user, 1000); + + // Move management of 600 shares from QPORTAL to QX (a contract that accepts). + QPORTAL::transferShareManagementRights_output o = + qp.transferShareMgmtRights(user, 600, QX_CONTRACT_INDEX, 5000000); + EXPECT_EQ(o.returnCode, (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(o.transferredNumberOfShares, 600ll); + + // 600 shares are now managed by QX, 400 remain on QPORTAL. + EXPECT_EQ(qp.portalBalanceOnQX(user), 600); + EXPECT_EQ(qp.portalBalanceOnQPortal(user), 400); + + // The transfer fee is accumulated for burning. + EXPECT_EQ(qp.getState()->getBurnAmt(), (sint64)QPORTAL_TRANSFER_SHARE_FEE); +} From 89c148fefff3d9ada3d75139928f623ddaa50629 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Sun, 24 May 2026 05:04:55 +0900 Subject: [PATCH 10/18] fix/feat: add freeze time in vote, execustion fee issue and approve proposal issue --- src/contracts/QPortal.h | 180 ++++++++++++++++++++++++++++++------- test/contract_qportal.cpp | 183 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 323 insertions(+), 40 deletions(-) diff --git a/src/contracts/QPortal.h b/src/contracts/QPortal.h index c6d46aee7..a746956bb 100644 --- a/src/contracts/QPortal.h +++ b/src/contracts/QPortal.h @@ -1,7 +1,7 @@ using namespace QPI; constexpr uint64 QPORTAL_PORTAL_ASSET_NAME = 83843471265616; //PORTAL asset name -constexpr uint64 QPORTAL_MIN_HOLDING_PORTAL = 100000ull; //proposal must have at least 100K PORTAL to approve a proposal. +constexpr uint64 QPORTAL_MIN_TOTAL_VOTED_PORTAL = 100000ull; //proposal must have at least 100K PORTAL to approve a proposal(yes + no). constexpr uint32 QPORTAL_REGISTER_FEE = 5; //Fee for registering in the portal DAO constexpr uint64 QPORTAL_REFUND_FEE = 5; //PORTAL fee charged on each refund constexpr uint32 QPORTAL_MAX_MEMBER = 4096; //Maximum number of members in the portal DAO. @@ -10,7 +10,7 @@ constexpr uint32 QPORTAL_MAX_PROPOSAL_USER = 2; //The maximum number of proposal constexpr uint32 QPORTAL_MAX_PROPOSAL_EPOCH = 5; //The maximum number of proposals per epoch. constexpr uint32 QPORTAL_EPOCH_PROPOSALS_CAPACITY = 8; //Backing capacity for currentEpochProposals (Array requires 2^N, must be >= QPORTAL_MAX_PROPOSAL_EPOCH). constexpr uint32 QPORTAL_MAX_VOTE = 32768; //The maximum number of votes a user can make (voting in all proposals in all proposal epochs). -constexpr uint32 QPORTAL_TRANSFER_SHARE_FEE = 100; +constexpr uint32 QPORTAL_EXECUTION_FEE = 100; constexpr uint32 QPORTAL_SUCCESS = 0; constexpr uint32 QPORTAL_INSUFFICIENT_PORTAL = 1; @@ -26,6 +26,8 @@ constexpr uint32 QPORTAL_CLOSED_PROPOSAL = 10; constexpr uint32 QPORTAL_INVALID_INPUT = 11; constexpr uint32 QPORTAL_NOT_VOTED_PROPOSAL = 12; constexpr uint32 QPORTAL_EXISTED_PROPOSAL = 13; +constexpr uint32 QPORTAL_EPOCH_FROZEN = 14; //Submissions and voting are frozen from Monday 00:00 UTC to Wednesday 12:00 UTC. +constexpr uint32 QPORTAL_INSUFFICIENT_EXECUTION_FEE = 15; struct QPORTAL2 { @@ -79,7 +81,6 @@ struct QPORTAL : public ContractBase HashMap lockedAmount; HashMap proposalVotes; // Voting records for each user per proposal epoch, used to check whether the user voted in the current proposal epoch. HashMap proposalResults; // record of vote results for all of proposals - sint64 burnAmt; }; struct getRegisters_input @@ -127,6 +128,7 @@ struct QPORTAL : public ContractBase struct getUserLockedAmount_input { + id userId; }; struct getUserLockedAmount_output @@ -206,7 +208,7 @@ struct QPORTAL : public ContractBase { Asset asset; sint64 numberOfShares; - uint32 newManagementContractIndex; + uint16 newManagementContractIndex; }; struct transferShareManagementRights_output @@ -303,13 +305,13 @@ struct QPORTAL : public ContractBase PUBLIC_FUNCTION(getUserLockedAmount) { - if (!state.get().registers.contains(qpi.invocator())) + if (!state.get().registers.contains(input.userId)) { output.returnCode = QPORTAL_NOT_REGISTERED; return ; } - output.lockedAmount = state.get().lockedAmount.contains(qpi.invocator()) ? state.get().lockedAmount.value(state.get().lockedAmount.getElementIndex(qpi.invocator())) : 0; + output.lockedAmount = state.get().lockedAmount.contains(input.userId) ? state.get().lockedAmount.value(state.get().lockedAmount.getElementIndex(input.userId)) : 0; output.returnCode = QPORTAL_SUCCESS; } @@ -344,18 +346,29 @@ struct QPORTAL : public ContractBase } struct registerInPortalDAO_locals - { + { sint64 ownedShares; Logger log; }; PUBLIC_PROCEDURE_WITH_LOCALS(registerInPortalDAO) { - if (qpi.invocationReward() > 0) + if (qpi.invocationReward() < QPORTAL_EXECUTION_FEE) + { + qpi.burn(qpi.invocationReward()); + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_EXECUTION_FEE, 0 }; + LOG_INFO(locals.log); + output.returnCode = QPORTAL_INSUFFICIENT_EXECUTION_FEE; + return; + } + + if (qpi.invocationReward() > QPORTAL_EXECUTION_FEE) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + qpi.transfer(qpi.invocator(), qpi.invocationReward() - QPORTAL_EXECUTION_FEE); } + qpi.burn(QPORTAL_EXECUTION_FEE); + if (state.get().registers.contains(qpi.invocator())) { output.returnCode = QPORTAL_ALREADY_REGISTERED; @@ -401,14 +414,37 @@ struct QPORTAL : public ContractBase { sint64 index; uint32 status; + uint8 dow; Logger log; }; PUBLIC_PROCEDURE_WITH_LOCALS(submitProposal) { - if (qpi.invocationReward() > 0) + if (qpi.invocationReward() < QPORTAL_EXECUTION_FEE) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + qpi.burn(qpi.invocationReward()); + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_EXECUTION_FEE, 0 }; + LOG_INFO(locals.log); + output.returnCode = QPORTAL_INSUFFICIENT_EXECUTION_FEE; + return; + } + + if (qpi.invocationReward() > QPORTAL_EXECUTION_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - QPORTAL_EXECUTION_FEE); + } + + qpi.burn(QPORTAL_EXECUTION_FEE); + + { + locals.dow = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); + if (locals.dow == 5 || locals.dow == 6 || (locals.dow == 0 && qpi.hour() < 12)) + { + output.returnCode = QPORTAL_EPOCH_FROZEN; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_EPOCH_FROZEN, 0 }; + LOG_INFO(locals.log); + return ; + } } if (!state.get().registers.contains(qpi.invocator())) @@ -478,6 +514,7 @@ struct QPORTAL : public ContractBase struct submitInVote_locals { id key; + uint8 dow; sint64 index; uint64 amount; sint64 ownedShares; @@ -489,12 +526,34 @@ struct QPORTAL : public ContractBase PUBLIC_PROCEDURE_WITH_LOCALS(submitInVote) { - if (qpi.invocationReward() > 0) + if (qpi.invocationReward() < QPORTAL_EXECUTION_FEE) + { + qpi.burn(qpi.invocationReward()); + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_EXECUTION_FEE, 0 }; + LOG_INFO(locals.log); + output.returnCode = QPORTAL_INSUFFICIENT_EXECUTION_FEE; + return; + } + + if (qpi.invocationReward() > QPORTAL_EXECUTION_FEE) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + qpi.transfer(qpi.invocator(), qpi.invocationReward() - QPORTAL_EXECUTION_FEE); } - if (input.proposalId == NULL_ID || input.votingPortalAmount == 0) + qpi.burn(QPORTAL_EXECUTION_FEE); + + { + locals.dow = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); + if (locals.dow == 5 || locals.dow == 6 || (locals.dow == 0 && qpi.hour() < 12)) + { + output.returnCode = QPORTAL_EPOCH_FROZEN; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_EPOCH_FROZEN, 0 }; + LOG_INFO(locals.log); + return ; + } + } + + if (input.proposalId == NULL_ID || input.votingPortalAmount <= 0) { output.returnCode = QPORTAL_INVALID_INPUT; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INVALID_INPUT, 0 }; @@ -593,6 +652,7 @@ struct QPORTAL : public ContractBase struct submitOutVote_locals { + uint8 dow; sint64 index; voteRecord vr; voteResult tmp; @@ -601,9 +661,31 @@ struct QPORTAL : public ContractBase PUBLIC_PROCEDURE_WITH_LOCALS(submitOutVote) { - if (qpi.invocationReward() > 0) + if (qpi.invocationReward() < QPORTAL_EXECUTION_FEE) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + qpi.burn(qpi.invocationReward()); + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_EXECUTION_FEE, 0 }; + LOG_INFO(locals.log); + output.returnCode = QPORTAL_INSUFFICIENT_EXECUTION_FEE; + return; + } + + if (qpi.invocationReward() > QPORTAL_EXECUTION_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - QPORTAL_EXECUTION_FEE); + } + + qpi.burn(QPORTAL_EXECUTION_FEE); + + { + locals.dow = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); + if (locals.dow == 5 || locals.dow == 6 || (locals.dow == 0 && qpi.hour() < 12)) + { + output.returnCode = QPORTAL_EPOCH_FROZEN; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_EPOCH_FROZEN, 0 }; + LOG_INFO(locals.log); + return ; + } } if (input.proposalId == NULL_ID) @@ -655,8 +737,9 @@ struct QPORTAL : public ContractBase locals.tmp.yesPortal - (locals.vr.vote == 1 ? locals.vr.votingPortalAmount : 0), locals.tmp.noPortal - (locals.vr.vote == 0 ? locals.vr.votingPortalAmount : 0) }); + state.mut().proposalVotes.removeByIndex(locals.index); - + state.mut().proposalVotes.cleanupIfNeeded(); output.returnCode = QPORTAL_SUCCESS; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_SUCCESS, 0 }; @@ -667,15 +750,27 @@ struct QPORTAL : public ContractBase { uint32 i; Logger log; + sint64 remaining; }; PUBLIC_PROCEDURE_WITH_LOCALS(requestRefund) { - if (qpi.invocationReward() > 0) + if (qpi.invocationReward() < QPORTAL_EXECUTION_FEE) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + qpi.burn(qpi.invocationReward()); + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_EXECUTION_FEE, 0 }; + LOG_INFO(locals.log); + output.returnCode = QPORTAL_INSUFFICIENT_EXECUTION_FEE; + return; } + if (qpi.invocationReward() > QPORTAL_EXECUTION_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - QPORTAL_EXECUTION_FEE); + } + + qpi.burn(QPORTAL_EXECUTION_FEE); + if (input.amount <= QPORTAL_REFUND_FEE) { output.returnCode = QPORTAL_INVALID_INPUT; @@ -711,7 +806,16 @@ struct QPORTAL : public ContractBase return ; } - state.mut().lockedAmount.set(qpi.invocator(), state.get().lockedAmount.value(state.get().lockedAmount.getElementIndex(qpi.invocator())) - input.amount); + locals.remaining = state.get().lockedAmount.value(state.get().lockedAmount.getElementIndex(qpi.invocator())) - input.amount; + if (locals.remaining > 0) + { + state.mut().lockedAmount.set(qpi.invocator(), locals.remaining); + } + else + { + state.mut().lockedAmount.removeByIndex(state.get().lockedAmount.getElementIndex(qpi.invocator())); + state.mut().lockedAmount.cleanupIfNeeded(); + } output.returnCode = QPORTAL_SUCCESS; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_SUCCESS, 0 }; @@ -728,17 +832,30 @@ struct QPORTAL : public ContractBase PUBLIC_PROCEDURE_WITH_LOCALS(transferShareManagementRights) { - if (qpi.invocationReward() < QPORTAL_TRANSFER_SHARE_FEE) + if (qpi.invocationReward() < QPORTAL_EXECUTION_FEE) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; + qpi.burn(qpi.invocationReward()); + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_EXECUTION_FEE, 0 }; + LOG_INFO(locals.log); + output.returnCode = QPORTAL_INSUFFICIENT_EXECUTION_FEE; + return ; + } + + locals.offeredFee = qpi.invocationReward() - QPORTAL_EXECUTION_FEE; + qpi.burn(QPORTAL_EXECUTION_FEE); + + if (input.numberOfShares <= 0 || input.newManagementContractIndex == 0 || input.newManagementContractIndex == SELF_INDEX) + { + qpi.transfer(qpi.invocator(), locals.offeredFee); + output.returnCode = QPORTAL_INVALID_INPUT; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INVALID_INPUT, 0 }; LOG_INFO(locals.log); - output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; return ; } if (qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.numberOfShares) { + qpi.transfer(qpi.invocator(), locals.offeredFee); output.transferredNumberOfShares = 0; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; LOG_INFO(locals.log); @@ -746,10 +863,10 @@ struct QPORTAL : public ContractBase return ; } - locals.offeredFee = qpi.invocationReward() - QPORTAL_TRANSFER_SHARE_FEE; locals.result = qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, input.newManagementContractIndex, input.newManagementContractIndex, locals.offeredFee); if (locals.result == INVALID_AMOUNT || locals.result < 0) { + qpi.transfer(qpi.invocator(), locals.offeredFee); output.transferredNumberOfShares = 0; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; LOG_INFO(locals.log); @@ -758,7 +875,6 @@ struct QPORTAL : public ContractBase } qpi.transfer(qpi.invocator(), locals.offeredFee - locals.result); - state.mut().burnAmt += QPORTAL_TRANSFER_SHARE_FEE; output.transferredNumberOfShares = input.numberOfShares; output.returnCode = QPORTAL_SUCCESS; @@ -811,13 +927,10 @@ struct QPORTAL : public ContractBase locals.yesPortal = state.get().proposalResults.value(locals.index).yesPortal; locals.noPortal = state.get().proposalResults.value(locals.index).noPortal; - if (locals.yesPortal + locals.noPortal < QPORTAL_MIN_HOLDING_PORTAL ) - { - state.mut().submittedProposals.set(locals.proposalId, {state.get().submittedProposals.value(state.get().submittedProposals.getElementIndex(locals.proposalId)).userId, 2}); - } - else if (locals.yesVotes > locals.noVotes && locals.yesPortal > locals.noPortal ) + if (locals.yesPortal + locals.noPortal >= QPORTAL_MIN_TOTAL_VOTED_PORTAL && locals.yesPortal > locals.noPortal && locals.yesVotes > locals.noVotes) { state.mut().submittedProposals.set(locals.proposalId, {state.get().submittedProposals.value(state.get().submittedProposals.getElementIndex(locals.proposalId)).userId, 1}); + continue; } else { @@ -825,13 +938,14 @@ struct QPORTAL : public ContractBase } } } - qpi.burn(state.get().burnAmt); state.mut().numberOfCurrentEpochProposals = 0; state.mut().userProposalStatus.reset(); state.mut().proposalVotes.reset(); state.mut().currentEpochProposals.setAll(NULL_ID); - state.mut().burnAmt = 0; + state.mut().lockedAmount.cleanupIfNeeded(); + state.mut().proposalResults.cleanupIfNeeded(); + state.mut().submittedProposals.cleanupIfNeeded(); } PRE_ACQUIRE_SHARES() diff --git a/test/contract_qportal.cpp b/test/contract_qportal.cpp index c509f64c9..3957a38e6 100644 --- a/test/contract_qportal.cpp +++ b/test/contract_qportal.cpp @@ -73,6 +73,23 @@ class ContractTestingQPortal : protected ContractTesting system.epoch = contractDescriptions[QPORTAL_CONTRACT_INDEX].constructionEpoch; portalIssuer = getState()->getPortalIssuer(); + + // Default to a Thursday (outside the Mon 00:00 -> end-of-epoch freeze window) + // so the rest of the suite is not blocked by the freeze. + setDateTime(2025, 1, 2, 12); + } + + // Sets the simulated UTC date/time visible to the contract via qpi.year/month/day. + void setDateTime(uint16 year, uint8 month, uint8 day, uint8 hour) + { + utcTime.Year = year; + utcTime.Month = month; + utcTime.Day = day; + utcTime.Hour = hour; + utcTime.Minute = 0; + utcTime.Second = 0; + utcTime.Nanosecond = 0; + updateQpiTime(); } QPORTALChecker* getState() @@ -1048,9 +1065,9 @@ TEST(ContractQPortal, TransferShareMgmtRights_InsufficientReward) id user = qpMember(1); qp.fundUser(user, 1000); - // invocationReward below QPORTAL_TRANSFER_SHARE_FEE (100) is rejected. + // invocationReward below QPORTAL_EXECUTION_FEE (100) is rejected. QPORTAL::transferShareManagementRights_output o = - qp.transferShareMgmtRights(user, 500, QX_CONTRACT_INDEX, QPORTAL_TRANSFER_SHARE_FEE - 1); + qp.transferShareMgmtRights(user, 500, QX_CONTRACT_INDEX, QPORTAL_EXECUTION_FEE - 1); EXPECT_EQ(o.returnCode, (sint32)QPORTAL_INSUFFICIENT_PORTAL); // Management is unchanged: all shares still on QPORTAL. EXPECT_EQ(qp.portalBalanceOnQPortal(user), 1000); @@ -1065,7 +1082,7 @@ TEST(ContractQPortal, TransferShareMgmtRights_InsufficientShares) // Requesting more shares than the user holds is rejected. QPORTAL::transferShareManagementRights_output o = - qp.transferShareMgmtRights(user, 1000, QX_CONTRACT_INDEX, QPORTAL_TRANSFER_SHARE_FEE); + qp.transferShareMgmtRights(user, 1000, QX_CONTRACT_INDEX, QPORTAL_EXECUTION_FEE); EXPECT_EQ(o.returnCode, (sint32)QPORTAL_INSUFFICIENT_PORTAL); EXPECT_EQ(o.transferredNumberOfShares, 0ll); EXPECT_EQ(qp.portalBalanceOnQPortal(user), 50); @@ -1078,14 +1095,166 @@ TEST(ContractQPortal, TransferShareMgmtRights_SelfDestinationRejected) id user = qpMember(1); qp.fundUser(user, 1000); - // releaseShares rejects a transfer whose destination is the current contract. + // The contract's input-validation guard rejects destination == SELF. QPORTAL::transferShareManagementRights_output o = qp.transferShareMgmtRights(user, 500, QPORTAL_CONTRACT_INDEX, 5000000); - EXPECT_EQ(o.returnCode, (sint32)QPORTAL_INSUFFICIENT_PORTAL); - EXPECT_EQ(o.transferredNumberOfShares, 0ll); + EXPECT_EQ(o.returnCode, (sint32)QPORTAL_INVALID_INPUT); EXPECT_EQ(qp.portalBalanceOnQPortal(user), 1000); } +// =========================================================================== +// Freeze window: Monday 00:00 UTC -> Wednesday 12:00 UTC +// +// One test per row of the required-tests matrix: +// Sunday 23:59 -> proposal/vote/cancel allowed +// Monday 00:00 -> QPORTAL_EPOCH_FROZEN +// Monday 12:00 -> QPORTAL_EPOCH_FROZEN +// Tuesday 23:59 -> QPORTAL_EPOCH_FROZEN +// Wednesday 11:59 -> QPORTAL_EPOCH_FROZEN +// Wednesday 12:00 -> proposal/vote/cancel allowed for new epoch +// During freeze, requestRefund() with active vote -> rejected +// After END_EPOCH, requestRefund() -> allowed +// +// Reference calendar: 2026-05-17 Sun, -18 Mon, -19 Tue, -20 Wed. +// setDateTime() has hour granularity, so 23:59 is exercised with hour=23. +// =========================================================================== + +// Helper: build a fresh fixture with a registered proposer/voter and one +// proposal, the voter holding `voterFund` PORTAL ready to vote. All setup is +// performed on the fixture's default Thursday (outside the freeze window). +static void freezeSetup(ContractTestingQPortal& qp, id& proposer, id& voter, id& proposal, + sint64 voterFund = 50000) +{ + qp.issuePortal(10000000); + proposer = qpMember(1); + voter = qpMember(2); + qp.fundUser(proposer, 100); + qp.fundUser(voter, voterFund); + EXPECT_EQ(qp.registerInPortalDAO(proposer), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.registerInPortalDAO(voter), (sint32)QPORTAL_SUCCESS); + proposal = qpProposal(1); + EXPECT_EQ(qp.submitProposal(proposer, proposal), (sint32)QPORTAL_SUCCESS); +} + +// Row 1: Sunday 23:59 -> proposal/vote/cancel allowed. +TEST(ContractQPortal, Freeze_Allowed_Sunday_23_59) +{ + ContractTestingQPortal qp; + id proposer, voter, proposal; + freezeSetup(qp, proposer, voter, proposal); + + qp.setDateTime(2026, 5, 17, 23); // Sunday 23:xx UTC + EXPECT_EQ(qp.submitProposal(proposer, qpProposal(2)), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 1000), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitOutVote(voter, proposal), (sint32)QPORTAL_SUCCESS); +} + +// Row 2: Monday 00:00 -> QPORTAL_EPOCH_FROZEN. +TEST(ContractQPortal, Freeze_Frozen_Monday_00_00) +{ + ContractTestingQPortal qp; + id proposer, voter, proposal; + freezeSetup(qp, proposer, voter, proposal); + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 1000), (sint32)QPORTAL_SUCCESS); + + qp.setDateTime(2026, 5, 18, 0); // Monday 00:00 UTC + EXPECT_EQ(qp.submitProposal(proposer, qpProposal(2)), (sint32)QPORTAL_EPOCH_FROZEN); + EXPECT_EQ(qp.submitInVote(voter, proposal, 0, 500), (sint32)QPORTAL_EPOCH_FROZEN); + EXPECT_EQ(qp.submitOutVote(voter, proposal), (sint32)QPORTAL_EPOCH_FROZEN); +} + +// Row 3: Monday 12:00 -> QPORTAL_EPOCH_FROZEN. +TEST(ContractQPortal, Freeze_Frozen_Monday_12_00) +{ + ContractTestingQPortal qp; + id proposer, voter, proposal; + freezeSetup(qp, proposer, voter, proposal); + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 1000), (sint32)QPORTAL_SUCCESS); + + qp.setDateTime(2026, 5, 18, 12); // Monday 12:00 UTC + EXPECT_EQ(qp.submitProposal(proposer, qpProposal(2)), (sint32)QPORTAL_EPOCH_FROZEN); + EXPECT_EQ(qp.submitInVote(voter, proposal, 0, 500), (sint32)QPORTAL_EPOCH_FROZEN); + EXPECT_EQ(qp.submitOutVote(voter, proposal), (sint32)QPORTAL_EPOCH_FROZEN); +} + +// Row 4: Tuesday 23:59 -> QPORTAL_EPOCH_FROZEN. +TEST(ContractQPortal, Freeze_Frozen_Tuesday_23_59) +{ + ContractTestingQPortal qp; + id proposer, voter, proposal; + freezeSetup(qp, proposer, voter, proposal); + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 1000), (sint32)QPORTAL_SUCCESS); + + qp.setDateTime(2026, 5, 19, 23); // Tuesday 23:xx UTC + EXPECT_EQ(qp.submitProposal(proposer, qpProposal(2)), (sint32)QPORTAL_EPOCH_FROZEN); + EXPECT_EQ(qp.submitInVote(voter, proposal, 0, 500), (sint32)QPORTAL_EPOCH_FROZEN); + EXPECT_EQ(qp.submitOutVote(voter, proposal), (sint32)QPORTAL_EPOCH_FROZEN); +} + +// Row 5: Wednesday 11:59 -> QPORTAL_EPOCH_FROZEN (one hour before thaw). +TEST(ContractQPortal, Freeze_Frozen_Wednesday_11_59) +{ + ContractTestingQPortal qp; + id proposer, voter, proposal; + freezeSetup(qp, proposer, voter, proposal); + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 1000), (sint32)QPORTAL_SUCCESS); + + qp.setDateTime(2026, 5, 20, 11); // Wednesday 11:xx UTC + EXPECT_EQ(qp.submitProposal(proposer, qpProposal(2)), (sint32)QPORTAL_EPOCH_FROZEN); + EXPECT_EQ(qp.submitInVote(voter, proposal, 0, 500), (sint32)QPORTAL_EPOCH_FROZEN); + EXPECT_EQ(qp.submitOutVote(voter, proposal), (sint32)QPORTAL_EPOCH_FROZEN); +} + +// Row 6: Wednesday 12:00 -> proposal/vote/cancel allowed for the new epoch. +TEST(ContractQPortal, Freeze_Allowed_Wednesday_12_00) +{ + ContractTestingQPortal qp; + id proposer, voter, proposal; + freezeSetup(qp, proposer, voter, proposal); + + qp.setDateTime(2026, 5, 20, 12); // Wednesday 12:00 UTC -- exact thaw boundary + EXPECT_EQ(qp.submitProposal(proposer, qpProposal(2)), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 1000), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.submitOutVote(voter, proposal), (sint32)QPORTAL_SUCCESS); +} + +// Row 7: during the freeze, requestRefund() for an active vote is rejected. +TEST(ContractQPortal, Freeze_RefundRejectedDuringFreezeWithActiveVote) +{ + ContractTestingQPortal qp; + id proposer, voter, proposal; + freezeSetup(qp, proposer, voter, proposal); + + // Vote on the default Thursday; the vote is active in proposalVotes. + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 10000), (sint32)QPORTAL_SUCCESS); + + // Move into the freeze window (Tuesday) and request a refund. + qp.setDateTime(2026, 5, 19, 12); + // The refund check sees the still-active vote and rejects. + EXPECT_EQ(qp.requestRefund(voter, 5000), (sint32)QPORTAL_EXISTED_PROPOSAL); + EXPECT_EQ(qp.getState()->lockedOf(voter), 10000ull); // still locked +} + +// Row 8: after END_EPOCH, requestRefund() is allowed. +TEST(ContractQPortal, Freeze_RefundAllowedAfterEndEpoch) +{ + ContractTestingQPortal qp; + id proposer, voter, proposal; + freezeSetup(qp, proposer, voter, proposal); + + EXPECT_EQ(qp.submitInVote(voter, proposal, 1, 10000), (sint32)QPORTAL_SUCCESS); + + // END_EPOCH clears proposalVotes; lockedAmount persists. + qp.endEpoch(); + EXPECT_EQ(qp.getState()->lockedOf(voter), 10000ull); + + // After the epoch transition the refund goes through. + sint64 before = qp.portalBalanceOnQPortal(voter); + EXPECT_EQ(qp.requestRefund(voter, 10000), (sint32)QPORTAL_SUCCESS); + EXPECT_EQ(qp.portalBalanceOnQPortal(voter), before + 10000 - (sint64)QPORTAL_REFUND_FEE); + EXPECT_EQ(qp.getState()->lockedOf(voter), 0ull); +} + TEST(ContractQPortal, TransferShareMgmtRights_SuccessToQX) { ContractTestingQPortal qp; @@ -1104,5 +1273,5 @@ TEST(ContractQPortal, TransferShareMgmtRights_SuccessToQX) EXPECT_EQ(qp.portalBalanceOnQPortal(user), 400); // The transfer fee is accumulated for burning. - EXPECT_EQ(qp.getState()->getBurnAmt(), (sint64)QPORTAL_TRANSFER_SHARE_FEE); + EXPECT_EQ(qp.getState()->getBurnAmt(), (sint64)QPORTAL_EXECUTION_FEE); } From 53ca4527d3224a355a9b99ec95bcd0fbeabca084 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Tue, 26 May 2026 07:27:50 +0900 Subject: [PATCH 11/18] update: execution fee for burn wallet --- src/contracts/QPortal.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/QPortal.h b/src/contracts/QPortal.h index a746956bb..5fee5beb8 100644 --- a/src/contracts/QPortal.h +++ b/src/contracts/QPortal.h @@ -10,7 +10,7 @@ constexpr uint32 QPORTAL_MAX_PROPOSAL_USER = 2; //The maximum number of proposal constexpr uint32 QPORTAL_MAX_PROPOSAL_EPOCH = 5; //The maximum number of proposals per epoch. constexpr uint32 QPORTAL_EPOCH_PROPOSALS_CAPACITY = 8; //Backing capacity for currentEpochProposals (Array requires 2^N, must be >= QPORTAL_MAX_PROPOSAL_EPOCH). constexpr uint32 QPORTAL_MAX_VOTE = 32768; //The maximum number of votes a user can make (voting in all proposals in all proposal epochs). -constexpr uint32 QPORTAL_EXECUTION_FEE = 100; +constexpr uint32 QPORTAL_EXECUTION_FEE = 20000; constexpr uint32 QPORTAL_SUCCESS = 0; constexpr uint32 QPORTAL_INSUFFICIENT_PORTAL = 1; From b8cec02b48355ac16082d6d68b2b8e24406726ab Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Wed, 27 May 2026 22:27:24 +0900 Subject: [PATCH 12/18] feat: add logout function --- src/contracts/QPortal.h | 55 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/src/contracts/QPortal.h b/src/contracts/QPortal.h index 5fee5beb8..180b4826a 100644 --- a/src/contracts/QPortal.h +++ b/src/contracts/QPortal.h @@ -162,6 +162,15 @@ struct QPORTAL : public ContractBase sint32 returnCode; }; + struct logoutInPortalDAO_input + { + }; + + struct logoutInPortalDAO_output + { + sint32 returnCode; + }; + struct submitProposal_input { id proposalId; @@ -410,6 +419,41 @@ struct QPORTAL : public ContractBase LOG_INFO(locals.log); } + struct logoutInPortalDAO_locals + { + Logger log; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(logoutInPortalDAO) + { + if(qpi.invocationReward() < QPORTAL_EXECUTION_FEE) + { + qpi.burn(qpi.invocationReward()); + output.returnCode = QPORTAL_INSUFFICIENT_EXECUTION_FEE; + return; + } + + if (qpi.invocationReward() > QPORTAL_EXECUTION_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - QPORTAL_EXECUTION_FEE); + } + + qpi.burn(QPORTAL_EXECUTION_FEE); + + if (!state.get().registers.contains(qpi.invocator())) + { + output.returnCode = QPORTAL_NOT_REGISTERED; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_NOT_REGISTERED, 0 }; + LOG_INFO(locals.log); + return ; + } + + state.mut().registers.removeByKey(qpi.invocator()); + state.mut().numberOfRegisters --; + locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_SUCCESS, 0 }; + LOG_INFO(locals.log); + } + struct submitProposal_locals { sint64 index; @@ -892,11 +936,12 @@ struct QPORTAL : public ContractBase REGISTER_USER_FUNCTION(getProposalInfo, 6); REGISTER_USER_PROCEDURE(registerInPortalDAO, 1); - REGISTER_USER_PROCEDURE(submitProposal, 2); - REGISTER_USER_PROCEDURE(submitInVote, 3); - REGISTER_USER_PROCEDURE(submitOutVote, 4); - REGISTER_USER_PROCEDURE(requestRefund, 5); - REGISTER_USER_PROCEDURE(transferShareManagementRights, 6); + REGISTER_USER_PROCEDURE(logoutInPortalDAO, 2); + REGISTER_USER_PROCEDURE(submitProposal, 3); + REGISTER_USER_PROCEDURE(submitInVote, 4); + REGISTER_USER_PROCEDURE(submitOutVote, 5); + REGISTER_USER_PROCEDURE(requestRefund, 6); + REGISTER_USER_PROCEDURE(transferShareManagementRights, 7); } struct END_EPOCH_locals From fabb2eb65a89318e9ab7515e744e57fd5a9260f5 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Thu, 28 May 2026 01:13:32 +0900 Subject: [PATCH 13/18] feat: splite the fee with burn and share --- src/contracts/QPortal.h | 73 ++++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/src/contracts/QPortal.h b/src/contracts/QPortal.h index 180b4826a..0986d7dfe 100644 --- a/src/contracts/QPortal.h +++ b/src/contracts/QPortal.h @@ -1,4 +1,5 @@ using namespace QPI; +#include "qpi.h"; constexpr uint64 QPORTAL_PORTAL_ASSET_NAME = 83843471265616; //PORTAL asset name constexpr uint64 QPORTAL_MIN_TOTAL_VOTED_PORTAL = 100000ull; //proposal must have at least 100K PORTAL to approve a proposal(yes + no). @@ -11,6 +12,9 @@ constexpr uint32 QPORTAL_MAX_PROPOSAL_EPOCH = 5; //The maximum number of proposa constexpr uint32 QPORTAL_EPOCH_PROPOSALS_CAPACITY = 8; //Backing capacity for currentEpochProposals (Array requires 2^N, must be >= QPORTAL_MAX_PROPOSAL_EPOCH). constexpr uint32 QPORTAL_MAX_VOTE = 32768; //The maximum number of votes a user can make (voting in all proposals in all proposal epochs). constexpr uint32 QPORTAL_EXECUTION_FEE = 20000; +constexpr uint32 QPORTAL_TOTAL_FEE = 4; //100% +constexpr uint32 QPORTAL_BURN_FEE = 1; // 25% +constexpr uint32 QPORTAL_SHAREHOLDER_FEE = 3; //75% constexpr uint32 QPORTAL_SUCCESS = 0; constexpr uint32 QPORTAL_INSUFFICIENT_PORTAL = 1; @@ -81,6 +85,8 @@ struct QPORTAL : public ContractBase HashMap lockedAmount; HashMap proposalVotes; // Voting records for each user per proposal epoch, used to check whether the user voted in the current proposal epoch. HashMap proposalResults; // record of vote results for all of proposals + uint64 epochRevenue; + uint64 portalEpochRevenue; }; struct getRegisters_input @@ -364,7 +370,7 @@ struct QPORTAL : public ContractBase { if (qpi.invocationReward() < QPORTAL_EXECUTION_FEE) { - qpi.burn(qpi.invocationReward()); + state.mut().epochRevenue += qpi.invocationReward(); locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_EXECUTION_FEE, 0 }; LOG_INFO(locals.log); output.returnCode = QPORTAL_INSUFFICIENT_EXECUTION_FEE; @@ -376,7 +382,7 @@ struct QPORTAL : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward() - QPORTAL_EXECUTION_FEE); } - qpi.burn(QPORTAL_EXECUTION_FEE); + state.mut().epochRevenue += QPORTAL_EXECUTION_FEE; if (state.get().registers.contains(qpi.invocator())) { @@ -428,7 +434,7 @@ struct QPORTAL : public ContractBase { if(qpi.invocationReward() < QPORTAL_EXECUTION_FEE) { - qpi.burn(qpi.invocationReward()); + state.mut().epochRevenue += qpi.invocationReward(); output.returnCode = QPORTAL_INSUFFICIENT_EXECUTION_FEE; return; } @@ -438,7 +444,7 @@ struct QPORTAL : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward() - QPORTAL_EXECUTION_FEE); } - qpi.burn(QPORTAL_EXECUTION_FEE); + state.mut().epochRevenue += QPORTAL_EXECUTION_FEE; if (!state.get().registers.contains(qpi.invocator())) { @@ -466,7 +472,7 @@ struct QPORTAL : public ContractBase { if (qpi.invocationReward() < QPORTAL_EXECUTION_FEE) { - qpi.burn(qpi.invocationReward()); + state.mut().epochRevenue += qpi.invocationReward(); locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_EXECUTION_FEE, 0 }; LOG_INFO(locals.log); output.returnCode = QPORTAL_INSUFFICIENT_EXECUTION_FEE; @@ -478,7 +484,7 @@ struct QPORTAL : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward() - QPORTAL_EXECUTION_FEE); } - qpi.burn(QPORTAL_EXECUTION_FEE); + state.mut().epochRevenue += QPORTAL_EXECUTION_FEE; { locals.dow = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); @@ -572,7 +578,7 @@ struct QPORTAL : public ContractBase if (qpi.invocationReward() < QPORTAL_EXECUTION_FEE) { - qpi.burn(qpi.invocationReward()); + state.mut().epochRevenue += qpi.invocationReward(); locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_EXECUTION_FEE, 0 }; LOG_INFO(locals.log); output.returnCode = QPORTAL_INSUFFICIENT_EXECUTION_FEE; @@ -584,7 +590,7 @@ struct QPORTAL : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward() - QPORTAL_EXECUTION_FEE); } - qpi.burn(QPORTAL_EXECUTION_FEE); + state.mut().epochRevenue += QPORTAL_EXECUTION_FEE; { locals.dow = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); @@ -707,7 +713,7 @@ struct QPORTAL : public ContractBase { if (qpi.invocationReward() < QPORTAL_EXECUTION_FEE) { - qpi.burn(qpi.invocationReward()); + state.mut().epochRevenue += qpi.invocationReward(); locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_EXECUTION_FEE, 0 }; LOG_INFO(locals.log); output.returnCode = QPORTAL_INSUFFICIENT_EXECUTION_FEE; @@ -719,7 +725,7 @@ struct QPORTAL : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward() - QPORTAL_EXECUTION_FEE); } - qpi.burn(QPORTAL_EXECUTION_FEE); + state.mut().epochRevenue += QPORTAL_EXECUTION_FEE; { locals.dow = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); @@ -801,7 +807,7 @@ struct QPORTAL : public ContractBase { if (qpi.invocationReward() < QPORTAL_EXECUTION_FEE) { - qpi.burn(qpi.invocationReward()); + state.mut().epochRevenue += qpi.invocationReward(); locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_EXECUTION_FEE, 0 }; LOG_INFO(locals.log); output.returnCode = QPORTAL_INSUFFICIENT_EXECUTION_FEE; @@ -813,7 +819,7 @@ struct QPORTAL : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward() - QPORTAL_EXECUTION_FEE); } - qpi.burn(QPORTAL_EXECUTION_FEE); + state.mut().epochRevenue += QPORTAL_EXECUTION_FEE; if (input.amount <= QPORTAL_REFUND_FEE) { @@ -850,6 +856,8 @@ struct QPORTAL : public ContractBase return ; } + state.mut().portalEpochRevenue += QPORTAL_REFUND_FEE; + locals.remaining = state.get().lockedAmount.value(state.get().lockedAmount.getElementIndex(qpi.invocator())) - input.amount; if (locals.remaining > 0) { @@ -878,7 +886,7 @@ struct QPORTAL : public ContractBase { if (qpi.invocationReward() < QPORTAL_EXECUTION_FEE) { - qpi.burn(qpi.invocationReward()); + state.mut().epochRevenue += qpi.invocationReward(); locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_EXECUTION_FEE, 0 }; LOG_INFO(locals.log); output.returnCode = QPORTAL_INSUFFICIENT_EXECUTION_FEE; @@ -886,7 +894,7 @@ struct QPORTAL : public ContractBase } locals.offeredFee = qpi.invocationReward() - QPORTAL_EXECUTION_FEE; - qpi.burn(QPORTAL_EXECUTION_FEE); + state.mut().epochRevenue += QPORTAL_EXECUTION_FEE; if (input.numberOfShares <= 0 || input.newManagementContractIndex == 0 || input.newManagementContractIndex == SELF_INDEX) { @@ -946,10 +954,14 @@ struct QPORTAL : public ContractBase struct END_EPOCH_locals { - sint64 i, index; + sint64 i, index, sharesHeld; id proposalId; uint32 yesVotes, noVotes; uint64 yesPortal, noPortal; + uint64 burnFee, shareholderFeePerShare, sharholderPortalFeePerShare; + AssetPossessionIterator iter; + Asset QPortalAsset; + }; END_EPOCH_WITH_LOCALS() @@ -982,8 +994,38 @@ struct QPORTAL : public ContractBase state.mut().submittedProposals.set(locals.proposalId, {state.get().submittedProposals.value(state.get().submittedProposals.getElementIndex(locals.proposalId)).userId, 2}); } } + + } + + locals.shareholderFeePerShare = div(div(state.get().epochRevenue, QPORTAL_TOTAL_FEE) * QPORTAL_SHAREHOLDER_FEE, NUMBER_OF_COMPUTORS); + if (locals.shareholderFeePerShare > 0) + { + qpi.distributeDividends(locals.shareholderFeePerShare); + } + + locals.burnFee = state.get().epochRevenue - locals.shareholderFeePerShare * NUMBER_OF_COMPUTORS; + if (locals.burnFee > 0) + { + qpi.burn(locals.burnFee); + } + + locals.sharholderPortalFeePerShare = div(state.get().portalEpochRevenue, NUMBER_OF_COMPUTORS); + if (locals.sharholderPortalFeePerShare > 0) + { + locals.iter.begin(locals.QPortalAsset); + while (!locals.iter.reachedEnd()) + { + locals.sharesHeld = locals.iter.numberOfPossessedShares(); + if (locals.sharesHeld > 0) + { + qpi.transferShareOwnershipAndPossession(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, SELF, SELF, (sint64)(locals.sharholderPortalFeePerShare * (uint64)locals.sharesHeld), locals.iter.possessor()); + } + locals.iter.next(); + } + state.mut().portalEpochRevenue = state.get().portalEpochRevenue - locals.sharholderPortalFeePerShare * NUMBER_OF_COMPUTORS; } + state.mut().epochRevenue = 0; state.mut().numberOfCurrentEpochProposals = 0; state.mut().userProposalStatus.reset(); state.mut().proposalVotes.reset(); @@ -1004,6 +1046,7 @@ struct QPORTAL : public ContractBase state.mut().numberOfRegisters = 0; state.mut().numberOfCurrentEpochProposals = 0; state.mut().numberOfProposals = 0; + state.mut().epochRevenue = 0; state.mut().lockedAmount.reset(); state.mut().currentEpochProposals.setAll(NULL_ID); state.mut().proposalVotes.reset(); From 3c2deaace04cdac0e620c3f5a37b0e0dbbe0ee87 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Thu, 28 May 2026 10:11:08 +0900 Subject: [PATCH 14/18] fix: sharemangement issue, returncode issue, clean hashmap issue --- src/contracts/QPortal.h | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/contracts/QPortal.h b/src/contracts/QPortal.h index 0986d7dfe..f07369bbd 100644 --- a/src/contracts/QPortal.h +++ b/src/contracts/QPortal.h @@ -455,7 +455,9 @@ struct QPORTAL : public ContractBase } state.mut().registers.removeByKey(qpi.invocator()); + state.mut().registers.cleanupIfNeeded(); state.mut().numberOfRegisters --; + output.returnCode = QPORTAL_SUCCESS; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_SUCCESS, 0 }; LOG_INFO(locals.log); } @@ -821,7 +823,7 @@ struct QPORTAL : public ContractBase state.mut().epochRevenue += QPORTAL_EXECUTION_FEE; - if (input.amount <= QPORTAL_REFUND_FEE) + if (input.amount <= 0) { output.returnCode = QPORTAL_INVALID_INPUT; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INVALID_INPUT, 0 }; @@ -840,7 +842,7 @@ struct QPORTAL : public ContractBase } } - if (!state.get().lockedAmount.contains(qpi.invocator()) || state.get().lockedAmount.value(state.get().lockedAmount.getElementIndex(qpi.invocator())) < input.amount) + if (!state.get().lockedAmount.contains(qpi.invocator()) || state.get().lockedAmount.value(state.get().lockedAmount.getElementIndex(qpi.invocator())) < (input.amount + QPORTAL_REFUND_FEE)) { output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; @@ -848,7 +850,7 @@ struct QPORTAL : public ContractBase return ; } - if(qpi.transferShareOwnershipAndPossession(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, SELF, SELF, input.amount - QPORTAL_REFUND_FEE, qpi.invocator()) < 0) + if(qpi.transferShareOwnershipAndPossession(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, SELF, SELF, input.amount, qpi.invocator()) < 0) { output.returnCode = QPORTAL_INSUFFICIENT_PORTAL; locals.log = Logger{ QPORTAL_CONTRACT_INDEX, QPORTAL_INSUFFICIENT_PORTAL, 0 }; @@ -958,7 +960,7 @@ struct QPORTAL : public ContractBase id proposalId; uint32 yesVotes, noVotes; uint64 yesPortal, noPortal; - uint64 burnFee, shareholderFeePerShare, sharholderPortalFeePerShare; + uint64 burnFee, shareholderFeePerShare, shareholderPortalFeePerShare; AssetPossessionIterator iter; Asset QPortalAsset; @@ -1009,20 +1011,22 @@ struct QPORTAL : public ContractBase qpi.burn(locals.burnFee); } - locals.sharholderPortalFeePerShare = div(state.get().portalEpochRevenue, NUMBER_OF_COMPUTORS); - if (locals.sharholderPortalFeePerShare > 0) + locals.shareholderPortalFeePerShare = div(state.get().portalEpochRevenue, NUMBER_OF_COMPUTORS); + if (locals.shareholderPortalFeePerShare > 0) { + locals.QPortalAsset.assetName = QPORTAL_PORTAL_ASSET_NAME; + locals.QPortalAsset.issuer = state.get().PORTAL_Issuer; locals.iter.begin(locals.QPortalAsset); while (!locals.iter.reachedEnd()) { locals.sharesHeld = locals.iter.numberOfPossessedShares(); if (locals.sharesHeld > 0) { - qpi.transferShareOwnershipAndPossession(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, SELF, SELF, (sint64)(locals.sharholderPortalFeePerShare * (uint64)locals.sharesHeld), locals.iter.possessor()); + qpi.transferShareOwnershipAndPossession(QPORTAL_PORTAL_ASSET_NAME, state.get().PORTAL_Issuer, SELF, SELF, (sint64)(locals.shareholderPortalFeePerShare * (uint64)locals.sharesHeld), locals.iter.possessor()); } locals.iter.next(); } - state.mut().portalEpochRevenue = state.get().portalEpochRevenue - locals.sharholderPortalFeePerShare * NUMBER_OF_COMPUTORS; + state.mut().portalEpochRevenue = state.get().portalEpochRevenue - locals.shareholderPortalFeePerShare * NUMBER_OF_COMPUTORS; } state.mut().epochRevenue = 0; @@ -1033,11 +1037,14 @@ struct QPORTAL : public ContractBase state.mut().lockedAmount.cleanupIfNeeded(); state.mut().proposalResults.cleanupIfNeeded(); state.mut().submittedProposals.cleanupIfNeeded(); + state.mut().registers.cleanupIfNeeded(); } PRE_ACQUIRE_SHARES() { - output.allowTransfer = true; + output.allowTransfer = + (input.asset.assetName == QPORTAL_PORTAL_ASSET_NAME) && + (input.asset.issuer == state.get().PORTAL_Issuer); } INITIALIZE() From 68de67f964443ab6e6dfcba6f97095b4ff42b93f Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Sat, 30 May 2026 00:09:38 +0900 Subject: [PATCH 15/18] fix: update contructiion epoch in contract_def file --- src/contract_core/contract_def.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 5bba78a71..c58e00bd1 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -415,7 +415,7 @@ constexpr struct ContractDescription {"VOTTUN", 206, 10000, sizeof(VOTTUNBRIDGE::StateData)}, // proposal in epoch 204, IPO in 205, construction and first use in 206 {"QUSINO", 208, 10000, sizeof(QUSINO::StateData)}, // proposal in epoch 206, IPO in 207, construction and first use in 208 {"ESCROW", 210, 10000, sizeof(ESCROW::StateData)}, // proposal in epoch 208, IPO in 209, construction and first use in 210 - {"QPORTAL", 213, 10000, sizeof(QPORTAL::StateData)}, // proposal in epoch 211, IPO in 212, construction and first use in 213 + {"QPORTAL", 217, 10000, sizeof(QPORTAL::StateData)}, // proposal in epoch 215, IPO in 216, construction and first use in 213 // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA::StateData)}, From b743b894ad2eb4ec1ff6ac87f941e8ef7f291331 Mon Sep 17 00:00:00 2001 From: double-k-3033 Date: Sat, 30 May 2026 00:18:15 +0900 Subject: [PATCH 16/18] fix:remove header --- src/contracts/QPortal.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/contracts/QPortal.h b/src/contracts/QPortal.h index f07369bbd..b5e0ef9e9 100644 --- a/src/contracts/QPortal.h +++ b/src/contracts/QPortal.h @@ -1,5 +1,4 @@ using namespace QPI; -#include "qpi.h"; constexpr uint64 QPORTAL_PORTAL_ASSET_NAME = 83843471265616; //PORTAL asset name constexpr uint64 QPORTAL_MIN_TOTAL_VOTED_PORTAL = 100000ull; //proposal must have at least 100K PORTAL to approve a proposal(yes + no). From 84045baa0b4ca9bdc0692ca377ae817752c332e0 Mon Sep 17 00:00:00 2001 From: DoubleK <177026+double-k-3033@users.noreply.github.com> Date: Tue, 2 Jun 2026 02:39:45 +0900 Subject: [PATCH 17/18] change the pos of qportal contract --- src/contract_core/contract_def.h | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 6458a0ad7..6f0b7f7aa 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -288,6 +288,16 @@ #define CONTRACT_STATE2_TYPE ESCROW2 #include "contracts/Escrow.h" +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QPORTAL_CONTRACT_INDEX 28 +#define CONTRACT_INDEX QPORTAL_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QPORTAL +#define CONTRACT_STATE2_TYPE QPORTAL2 +#include "contracts/QPortal.h" + #ifndef NO_GGWP #undef CONTRACT_INDEX @@ -302,16 +312,6 @@ #endif -#undef CONTRACT_INDEX -#undef CONTRACT_STATE_TYPE -#undef CONTRACT_STATE2_TYPE - -#define QPORTAL_CONTRACT_INDEX 29 -#define CONTRACT_INDEX QPORTAL_CONTRACT_INDEX -#define CONTRACT_STATE_TYPE QPORTAL -#define CONTRACT_STATE2_TYPE QPORTAL2 -#include "contracts/QPortal.h" - // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES @@ -426,10 +426,10 @@ constexpr struct ContractDescription {"VOTTUN", 206, 10000, sizeof(VOTTUNBRIDGE::StateData)}, // proposal in epoch 204, IPO in 205, construction and first use in 206 {"QUSINO", 208, 10000, sizeof(QUSINO::StateData)}, // proposal in epoch 206, IPO in 207, construction and first use in 208 {"ESCROW", 210, 10000, sizeof(ESCROW::StateData)}, // proposal in epoch 208, IPO in 209, construction and first use in 210 + {"QPORTAL", 217, 10000, sizeof(QPORTAL::StateData)}, // proposal in epoch 215, IPO in 216, construction and first use in 217 #ifndef NO_GGWP {"GGWP", 217, 10000, sizeof(WOLFPACK::StateData)}, // proposal in epoch 215, IPO in 216, construction and first use in 217 #endif - {"QPORTAL", 217, 10000, sizeof(QPORTAL::StateData)}, // proposal in epoch 215, IPO in 216, construction and first use in 217 // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA::StateData)}, @@ -549,14 +549,14 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRP); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QTF); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QDUEL); - REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(PULSE); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(PULSE); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(VOTTUNBRIDGE); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QUSINO); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(ESCROW); + EGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QPORTAL); #ifndef NO_GGWP REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(WOLFPACK); #endif - EGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QPORTAL); // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); From a76c12f47a75fa18cc1ce3262805d6d85bdf7599 Mon Sep 17 00:00:00 2001 From: DoubleK <177026+double-k-3033@users.noreply.github.com> Date: Tue, 2 Jun 2026 02:43:40 +0900 Subject: [PATCH 18/18] fix: missing character `R` --- src/contract_core/contract_def.h | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 6f0b7f7aa..690afc94b 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -288,16 +288,6 @@ #define CONTRACT_STATE2_TYPE ESCROW2 #include "contracts/Escrow.h" -#undef CONTRACT_INDEX -#undef CONTRACT_STATE_TYPE -#undef CONTRACT_STATE2_TYPE - -#define QPORTAL_CONTRACT_INDEX 28 -#define CONTRACT_INDEX QPORTAL_CONTRACT_INDEX -#define CONTRACT_STATE_TYPE QPORTAL -#define CONTRACT_STATE2_TYPE QPORTAL2 -#include "contracts/QPortal.h" - #ifndef NO_GGWP #undef CONTRACT_INDEX @@ -312,6 +302,16 @@ #endif +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QPORTAL_CONTRACT_INDEX 29 +#define CONTRACT_INDEX QPORTAL_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QPORTAL +#define CONTRACT_STATE2_TYPE QPORTAL2 +#include "contracts/QPortal.h" + // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES @@ -553,7 +553,7 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(VOTTUNBRIDGE); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QUSINO); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(ESCROW); - EGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QPORTAL); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QPORTAL); #ifndef NO_GGWP REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(WOLFPACK); #endif