diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 942c974b..efca1ae5 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -52,6 +52,7 @@ + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index 160c5365..b15a8410 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -138,6 +138,9 @@ contracts + + contracts + contract_core diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 2aa6e074..0b499233 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -301,6 +301,20 @@ #define CONTRACT_STATE2_TYPE WOLFPACK2 #include "contracts/GGWP.h" +#ifndef NO_QLOAN + +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QLOAN_CONTRACT_INDEX 29 +#define CONTRACT_INDEX QLOAN_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QLOAN +#define CONTRACT_STATE2_TYPE QLOAN2 +#include "contracts/Qloan.h" + +#endif + // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES @@ -416,6 +430,9 @@ constexpr struct ContractDescription {"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 {"GGWP", 218, 10000, sizeof(WOLFPACK::StateData)}, // proposal in epoch 216, IPO in 217, construction and first use in 218 +#ifndef NO_QLOAN + {"QLOAN", 228, 10000, sizeof(QLOAN::StateData)}, // proposal in epoch 226, IPO in 227, construction and first use in 228 +#endif // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA::StateData)}, @@ -548,6 +565,9 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QUSINO); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(ESCROW); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(WOLFPACK); +#ifndef NO_QLOAN + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QLOAN); +#endif // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); diff --git a/src/contracts/QLoan.h b/src/contracts/QLoan.h new file mode 100644 index 00000000..7bc41593 --- /dev/null +++ b/src/contracts/QLoan.h @@ -0,0 +1,1022 @@ +using namespace QPI; + +constexpr uint64 QLOAN_PLACE_LOAN_REQ_FEE = 100000; + +constexpr uint64 QLOAN_ACCEPTANCE_FEE_PERCENT = 15; +constexpr uint64 QLOAN_BURN_PERCENT = 300; // 3% +constexpr uint64 QLOAN_QVAULT_PERCENT = 9700; // 97% + +constexpr uint64 QLOAN_MIN_LOAN_PERIOD_IN_EPOCHS = 1; +constexpr uint64 QLOAN_MAX_LOAN_PERIOD_IN_EPOCHS = 52; +constexpr uint64 QLOAN_MAX_INTEREST_RATE = 100; +constexpr uint64 QLOAN_MAX_ASSETS_NUM = 2; +constexpr uint64 QLOAN_MAX_OUTPUT_NUM = 128; + +constexpr uint64 QLOAN_MAX_LOAN_REQS_NUM = 1024; + +struct QLOAN2 +{ +}; + +struct QLOAN : public ContractBase +{ + enum LoanReqState : uint8 + { + IDLE = 1, + ACTIVE = 2, + PAYED = 3, + EXPIRED = 4, + }; + + struct LoanOutputInfo + { + id borrower; + id creditor; + id acceptedBy; + id privateId; + + uint64 reqId; + + Array assets; + Array assetAmount; + uint8 assetsNum; + + uint64 priceAmount; + uint64 interestRate; + uint64 fullInterest; + uint64 debtAmount; + + uint64 returnPeriodInEpochs; + uint64 epochsLeft; + uint64 creationEpoch; + + uint8 state; + + bit assetsToCreditor; + }; + + struct LoanReqInfo + { + id borrower; + id creditor; + // Using these field to know which request user is accepted and which is created by him + id acceptedBy; + id privateId; + + Array assets; + Array assetAmount; + uint8 assetsNum; + + uint64 priceAmount; + uint64 interestRate; + uint64 fullInterest; + uint64 debtAmount; + + uint64 returnPeriodInEpochs; + uint64 epochsLeft; + uint64 creationEpoch; + + uint8 state; + + bit assetsToCreditor; + }; + + struct StateData + { + HashMap _loanReqs; + uint64 _totalReqs; + uint64 _currentLoanIndex; + + uint64 _earnedAmount; + uint64 _distributedAmount; + + // These two mostly for debug purposes + uint64 _burnedAmount; + uint64 _toQvaultAmount; + }; + + struct _TransferAssetsFromTo_input + { + LoanReqInfo loanReq; + id from; + id to; + }; + + struct _TransferAssetsFromTo_output + { + bit allGood; + }; + + struct _TransferAssetsFromTo_locals + { + Asset userReqAsset; + uint8 userReqAssetIdx; + sint64 result; + }; + + PRIVATE_PROCEDURE_WITH_LOCALS(_TransferAssetsFromTo) + { + locals.userReqAssetIdx = 0; + output.allGood = true; + + while (locals.userReqAssetIdx < input.loanReq.assetsNum) + { + locals.userReqAsset = input.loanReq.assets.get(locals.userReqAssetIdx); + locals.result = qpi.transferShareOwnershipAndPossession(locals.userReqAsset.assetName, locals.userReqAsset.issuer, + input.from, input.from, + input.loanReq.assetAmount.get(locals.userReqAssetIdx), + input.to); + if (locals.result < 0 || locals.result == INVALID_AMOUNT) + { + output.allGood = false; + if (locals.userReqAssetIdx != 0) + { + locals.userReqAssetIdx--; + while (true) + { + locals.userReqAsset = input.loanReq.assets.get(locals.userReqAssetIdx); + qpi.transferShareOwnershipAndPossession(locals.userReqAsset.assetName, locals.userReqAsset.issuer, + input.to, input.to, + input.loanReq.assetAmount.get(locals.userReqAssetIdx), + input.from); + if (locals.userReqAssetIdx == 0) + { + break; + } + locals.userReqAssetIdx--; + } + } + return; + } + locals.userReqAssetIdx++; + } + } + + struct _CheckAssetsPresence_input + { + id owner; + Array assets; + Array assetAmount; + uint8 assetsNum; + uint64 excludeReqId; + }; + + struct _CheckAssetsPresence_output + { + bit allGood; + }; + + struct _CheckAssetsPresence_locals + { + uint64 loanReqsIdx; + LoanReqInfo tmpLoanReq; + Array userAssetsAmountInvolved; + + uint64 inputReqAssetIdx; + uint64 tmpReqAssetIdx; + }; + + PRIVATE_FUNCTION_WITH_LOCALS(_CheckAssetsPresence) + { + output.allGood = true; + + // Collect all user's tokens involved in others deals + locals.loanReqsIdx = state.get()._loanReqs.nextElementIndex(NULL_INDEX); + while (locals.loanReqsIdx != NULL_INDEX) + { + locals.tmpLoanReq = state.get()._loanReqs.value(locals.loanReqsIdx); + // If user is a borrower - we need to check if assets transfered to the creditor in case of Active request, + // otherwise request might be in the IDLE state and we still should count these tokens + // If user is a creditor and assets should be transfered to creditor in active loan request then we should count these tokens too + if (state.get()._loanReqs.key(locals.loanReqsIdx) != input.excludeReqId + && ((locals.tmpLoanReq.borrower == input.owner && ((locals.tmpLoanReq.state == LoanReqState::ACTIVE && locals.tmpLoanReq.assetsToCreditor == false) || locals.tmpLoanReq.state == LoanReqState::IDLE)) + || (locals.tmpLoanReq.creditor == input.owner && (locals.tmpLoanReq.state == LoanReqState::ACTIVE && locals.tmpLoanReq.assetsToCreditor == true)))) + { + // Iterate over assets in the user loan request + locals.inputReqAssetIdx = 0; + while (locals.inputReqAssetIdx < input.assetsNum) + { + locals.tmpReqAssetIdx = 0; + while (locals.tmpReqAssetIdx < locals.tmpLoanReq.assetsNum) + { + if (input.assets.get(locals.inputReqAssetIdx).assetName == locals.tmpLoanReq.assets.get(locals.tmpReqAssetIdx).assetName + && input.assets.get(locals.inputReqAssetIdx).issuer == locals.tmpLoanReq.assets.get(locals.tmpReqAssetIdx).issuer) + { + locals.userAssetsAmountInvolved.set(locals.inputReqAssetIdx, + locals.userAssetsAmountInvolved.get(locals.inputReqAssetIdx) + locals.tmpLoanReq.assetAmount.get(locals.tmpReqAssetIdx)); + } + locals.tmpReqAssetIdx++; + } + locals.inputReqAssetIdx++; + } + } + locals.loanReqsIdx = state.get()._loanReqs.nextElementIndex(locals.loanReqsIdx); + } + // Check that user have enough of tokens considering his tokens from others deals + locals.inputReqAssetIdx = 0; + while (locals.inputReqAssetIdx < input.assetsNum) + { + if (qpi.numberOfPossessedShares(input.assets.get(locals.inputReqAssetIdx).assetName, + input.assets.get(locals.inputReqAssetIdx).issuer, + input.owner, input.owner, + SELF_INDEX, SELF_INDEX) - locals.userAssetsAmountInvolved.get(locals.inputReqAssetIdx) < input.assetAmount.get(locals.inputReqAssetIdx)) + { + output.allGood = false; + return; + } + locals.inputReqAssetIdx++; + } + } + + struct _FillLoanReqForOutput_input + { + uint64 loanReqId; + LoanReqInfo loanReqInfo; + }; + + struct _FillLoanReqForOutput_output + { + LoanOutputInfo loanOutputInfo; + }; + + PRIVATE_FUNCTION(_FillLoanReqForOutput) + { + output.loanOutputInfo.borrower = input.loanReqInfo.borrower; + output.loanOutputInfo.creditor = input.loanReqInfo.creditor; + output.loanOutputInfo.acceptedBy = input.loanReqInfo.acceptedBy; + output.loanOutputInfo.privateId = input.loanReqInfo.privateId; + output.loanOutputInfo.reqId = input.loanReqId; + output.loanOutputInfo.assets = input.loanReqInfo.assets; + output.loanOutputInfo.assetAmount = input.loanReqInfo.assetAmount; + output.loanOutputInfo.assetsNum = input.loanReqInfo.assetsNum; + output.loanOutputInfo.priceAmount = input.loanReqInfo.priceAmount; + output.loanOutputInfo.interestRate = input.loanReqInfo.interestRate; + output.loanOutputInfo.fullInterest = input.loanReqInfo.fullInterest; + output.loanOutputInfo.debtAmount = input.loanReqInfo.debtAmount; + output.loanOutputInfo.returnPeriodInEpochs = input.loanReqInfo.returnPeriodInEpochs; + output.loanOutputInfo.epochsLeft = input.loanReqInfo.epochsLeft; + output.loanOutputInfo.creationEpoch= input.loanReqInfo.creationEpoch; + output.loanOutputInfo.assetsToCreditor = input.loanReqInfo.assetsToCreditor; + output.loanOutputInfo.state = input.loanReqInfo.state; + } + +public: + struct PlaceLoanReq_input + { + // It's for whom these deal + id privateId; + + Array assets; + Array assetAmount; + uint8 assetsNum; + + uint64 price; + uint64 interestRate; + uint64 returnPeriodInEpochs; + + bit isLoanReq; + bit assetsToCreditor; + }; + + struct PlaceLoanReq_output + { + }; + + struct PlaceLoanReq_locals + { + LoanReqInfo loanReqInfo; + uint8 userReqAssetIdx; + uint8 userReqAssetIdx2; + + _CheckAssetsPresence_input checkAssetsPresenceInput; + _CheckAssetsPresence_output checkAssetsPresenceOutput; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(PlaceLoanReq) + { + // Check if we have space for new request + if (state.get()._totalReqs >= QLOAN_MAX_LOAN_REQS_NUM) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + // Check all inputs are valid + if (input.returnPeriodInEpochs > QLOAN_MAX_LOAN_PERIOD_IN_EPOCHS + || input.returnPeriodInEpochs < QLOAN_MIN_LOAN_PERIOD_IN_EPOCHS + || input.price == 0 + || input.price >= MAX_AMOUNT - QLOAN_PLACE_LOAN_REQ_FEE + || input.interestRate == 0 + || input.interestRate > QLOAN_MAX_INTEREST_RATE + || input.assetsNum == 0 + || input.assetsNum > QLOAN_MAX_ASSETS_NUM) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + for (locals.userReqAssetIdx = 0; locals.userReqAssetIdx < input.assetsNum; locals.userReqAssetIdx++) + { + if (input.assetAmount.get(locals.userReqAssetIdx) <= 0 || input.assetAmount.get(locals.userReqAssetIdx) >= MAX_AMOUNT) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + for (locals.userReqAssetIdx2 = locals.userReqAssetIdx + 1; locals.userReqAssetIdx2 < input.assetsNum; locals.userReqAssetIdx2++) + { + if (input.assets.get(locals.userReqAssetIdx).assetName == input.assets.get(locals.userReqAssetIdx2).assetName + && input.assets.get(locals.userReqAssetIdx).issuer == input.assets.get(locals.userReqAssetIdx2).issuer) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + } + } + + if (input.isLoanReq) + { + // Check he have enough qus for FEE + if (qpi.invocationReward() < QLOAN_PLACE_LOAN_REQ_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + else if (qpi.invocationReward() > QLOAN_PLACE_LOAN_REQ_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - QLOAN_PLACE_LOAN_REQ_FEE); + } + + locals.checkAssetsPresenceInput.owner = qpi.invocator(); + locals.checkAssetsPresenceInput.assets = input.assets; + locals.checkAssetsPresenceInput.assetAmount = input.assetAmount; + locals.checkAssetsPresenceInput.assetsNum = input.assetsNum; + locals.checkAssetsPresenceInput.excludeReqId = state.get()._currentLoanIndex; + CALL(_CheckAssetsPresence, locals.checkAssetsPresenceInput, locals.checkAssetsPresenceOutput); + + if (locals.checkAssetsPresenceOutput.allGood == false) + { + qpi.transfer(qpi.invocator(), QLOAN_PLACE_LOAN_REQ_FEE); + return; + } + } + else // User want to create creditor request, he wants assets, we need to check he have enough QUs + { + if (qpi.invocationReward() < sint64(input.price + QLOAN_PLACE_LOAN_REQ_FEE)) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + else if (qpi.invocationReward() > sint64(input.price + QLOAN_PLACE_LOAN_REQ_FEE)) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - input.price - QLOAN_PLACE_LOAN_REQ_FEE); + } + } + state.mut()._earnedAmount += QLOAN_PLACE_LOAN_REQ_FEE; + + locals.loanReqInfo.borrower = input.isLoanReq ? qpi.invocator() : NULL_ID; + locals.loanReqInfo.creditor = input.isLoanReq ? NULL_ID : qpi.invocator(); + locals.loanReqInfo.acceptedBy = NULL_ID; + locals.loanReqInfo.assets = input.assets; + locals.loanReqInfo.assetAmount = input.assetAmount; + locals.loanReqInfo.assetsNum = input.assetsNum; + locals.loanReqInfo.priceAmount = input.price; + locals.loanReqInfo.interestRate = input.interestRate; + locals.loanReqInfo.fullInterest = div(smul(input.price, input.interestRate), 100ULL); + // until 1/3 of the loan period has passed, the debt will be input.price + 1/3 of the total interest + locals.loanReqInfo.debtAmount = input.price + div(smul(input.price, input.interestRate), 300ULL); + locals.loanReqInfo.returnPeriodInEpochs = input.returnPeriodInEpochs; + locals.loanReqInfo.epochsLeft = input.returnPeriodInEpochs; + locals.loanReqInfo.creationEpoch = qpi.epoch(); + locals.loanReqInfo.privateId = input.privateId; + locals.loanReqInfo.assetsToCreditor = input.assetsToCreditor; + locals.loanReqInfo.state = LoanReqState::IDLE; + + state.mut()._loanReqs.set(state.get()._currentLoanIndex, locals.loanReqInfo); + state.mut()._totalReqs++; + state.mut()._currentLoanIndex++; + } + + struct AcceptLoanReq_input + { + uint64 reqId; + }; + + struct AcceptLoanReq_output + { + }; + + struct AcceptLoanReq_locals + { + LoanReqInfo tmpLoanReq; + sint64 requestedFee; + + _CheckAssetsPresence_input checkAssetsPresenceInput; + _CheckAssetsPresence_output checkAssetsPresenceOutput; + + _TransferAssetsFromTo_input transferAssetsFromToInput; + _TransferAssetsFromTo_output transferAssetsFromToOutput; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(AcceptLoanReq) + { + if (state.get()._loanReqs.get(input.reqId, locals.tmpLoanReq) == false) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + // We need to figure out which request we want to accept(loan or credit) + // We could do that by checking ID's of the borrower and creditor + // If borrower is not empty -> loan(need to check that acceptor have enought QU's money) + // Else creditor is not empty -> credit(need to check that acceptor have enough assets listed in request) + + // Check that request was not created by the qpi.invocator() + if (locals.tmpLoanReq.borrower == qpi.invocator() || locals.tmpLoanReq.creditor == qpi.invocator()) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + // Check that request in IDLE state + if (locals.tmpLoanReq.state != LoanReqState::IDLE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + // Check that request is not private or IF private then check that user can accept it + if (locals.tmpLoanReq.privateId != NULL_ID && locals.tmpLoanReq.privateId != qpi.invocator()) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + locals.requestedFee = div(smul(div(smul(locals.tmpLoanReq.priceAmount, locals.tmpLoanReq.interestRate), 100ULL), QLOAN_ACCEPTANCE_FEE_PERCENT), 100ULL); + + // If request was created by the borrower(he wants money for the assets) + if (locals.tmpLoanReq.borrower != NULL_ID) + { + locals.checkAssetsPresenceInput.owner = locals.tmpLoanReq.borrower; + locals.checkAssetsPresenceInput.assets = locals.tmpLoanReq.assets; + locals.checkAssetsPresenceInput.assetAmount = locals.tmpLoanReq.assetAmount; + locals.checkAssetsPresenceInput.assetsNum = locals.tmpLoanReq.assetsNum; + locals.checkAssetsPresenceInput.excludeReqId = input.reqId; + CALL(_CheckAssetsPresence, locals.checkAssetsPresenceInput, locals.checkAssetsPresenceOutput); + + // Check that borrower has enough assets and creditor send us right amount of money for contract + if (locals.checkAssetsPresenceOutput.allGood == false || qpi.invocationReward() < sint64(locals.tmpLoanReq.priceAmount)) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + // Transfer all assets from request to creditor if request was created like that + if (locals.tmpLoanReq.assetsToCreditor) + { + locals.transferAssetsFromToInput.loanReq = locals.tmpLoanReq; + locals.transferAssetsFromToInput.from = locals.tmpLoanReq.borrower; + locals.transferAssetsFromToInput.to = qpi.invocator(); + CALL(_TransferAssetsFromTo, locals.transferAssetsFromToInput, locals.transferAssetsFromToOutput); + + if (!locals.transferAssetsFromToOutput.allGood) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + } + + locals.tmpLoanReq.creditor = qpi.invocator(); + locals.tmpLoanReq.acceptedBy = qpi.invocator(); + locals.tmpLoanReq.state = LoanReqState::ACTIVE; + + state.mut()._loanReqs.replace(input.reqId, locals.tmpLoanReq); + + state.mut()._earnedAmount += locals.requestedFee; + // Send coins to the borrower + qpi.transfer(locals.tmpLoanReq.borrower, + locals.tmpLoanReq.priceAmount - locals.requestedFee); + // Send left coins to the creditor + if (qpi.invocationReward() > sint64(locals.tmpLoanReq.priceAmount)) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.tmpLoanReq.priceAmount); + } + } + else // If request was created by the creditor(he wants asssets for the money) + { + locals.checkAssetsPresenceInput.owner = qpi.invocator(); + locals.checkAssetsPresenceInput.assets = locals.tmpLoanReq.assets; + locals.checkAssetsPresenceInput.assetAmount = locals.tmpLoanReq.assetAmount; + locals.checkAssetsPresenceInput.assetsNum = locals.tmpLoanReq.assetsNum; + locals.checkAssetsPresenceInput.excludeReqId = input.reqId; + CALL(_CheckAssetsPresence, locals.checkAssetsPresenceInput, locals.checkAssetsPresenceOutput); + if (locals.checkAssetsPresenceOutput.allGood == false) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + // Transfer all assets from request to creditor if request was created like that + if (locals.tmpLoanReq.assetsToCreditor) + { + locals.transferAssetsFromToInput.loanReq = locals.tmpLoanReq; + locals.transferAssetsFromToInput.from = qpi.invocator(); + locals.transferAssetsFromToInput.to = locals.tmpLoanReq.creditor; + CALL(_TransferAssetsFromTo, locals.transferAssetsFromToInput, locals.transferAssetsFromToOutput); + + if (!locals.transferAssetsFromToOutput.allGood) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + } + + locals.tmpLoanReq.borrower = qpi.invocator(); + locals.tmpLoanReq.acceptedBy = qpi.invocator(); + locals.tmpLoanReq.state = LoanReqState::ACTIVE; + state.mut()._loanReqs.replace(input.reqId, locals.tmpLoanReq); + + state.mut()._earnedAmount += locals.requestedFee; + qpi.transfer(qpi.invocator(), qpi.invocationReward() + locals.tmpLoanReq.priceAmount - locals.requestedFee); + } + } + + struct RemoveLoanReq_input + { + uint64 reqId; + }; + + struct RemoveLoanReq_output + { + }; + + struct RemoveLoanReq_locals + { + LoanReqInfo tmpLoanReq; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(RemoveLoanReq) + { + if (state.get()._loanReqs.get(input.reqId, locals.tmpLoanReq) == false) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + if ((locals.tmpLoanReq.borrower != qpi.invocator() && locals.tmpLoanReq.creditor != qpi.invocator()) + || locals.tmpLoanReq.state != LoanReqState::IDLE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + // Need to figure out who is user in this request. + // He might be a borrower and creditor. If he is a borrower, we need to send all his tokens back. + // If he is a creditor, we need to send him back his QUs. + if (locals.tmpLoanReq.creditor == qpi.invocator()) + { + // Send him back all his QUs + qpi.transfer(qpi.invocator(), qpi.invocationReward() + locals.tmpLoanReq.priceAmount); + } + + // Remove loan req from the state + state.mut()._loanReqs.removeByKey(input.reqId); + state.mut()._totalReqs--; + } + + struct TransferShareManagementRights_input + { + Asset asset; + sint64 numberOfShares; + uint32 newManagingContractIndex; + }; + + struct TransferShareManagementRights_output + { + }; + + struct TransferShareManagementRights_locals + { + sint64 loanReqsIdx; + LoanReqInfo tmpLoanReq; + + sint64 releaseResult; + uint64 userAssetsAmountInHold; + + Asset userReqAsset; + uint8 userReqAssetIdx; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(TransferShareManagementRights) + { + if (qpi.invocationReward() == 0) + { + return; + } + + locals.userAssetsAmountInHold = 0; + locals.loanReqsIdx = state.get()._loanReqs.nextElementIndex(NULL_INDEX); + while (locals.loanReqsIdx != NULL_INDEX) + { + locals.tmpLoanReq = state.get()._loanReqs.value(locals.loanReqsIdx); + + if ((locals.tmpLoanReq.borrower == qpi.invocator() && ((locals.tmpLoanReq.state == LoanReqState::ACTIVE && locals.tmpLoanReq.assetsToCreditor == false) || locals.tmpLoanReq.state == LoanReqState::IDLE)) + || (locals.tmpLoanReq.creditor == qpi.invocator() && (locals.tmpLoanReq.state == LoanReqState::ACTIVE && locals.tmpLoanReq.assetsToCreditor == true))) + { + // Need to scan all assets in loan request to make sure it's have an assets user want to release + locals.userReqAssetIdx = 0; + while (locals.userReqAssetIdx < locals.tmpLoanReq.assetsNum) + { + locals.userReqAsset = locals.tmpLoanReq.assets.get(locals.userReqAssetIdx); + if (locals.userReqAsset.assetName == input.asset.assetName + && locals.userReqAsset.issuer == input.asset.issuer) + { + locals.userAssetsAmountInHold += locals.tmpLoanReq.assetAmount.get(locals.userReqAssetIdx); + } + locals.userReqAssetIdx++; + } + } + locals.loanReqsIdx = state.get()._loanReqs.nextElementIndex(locals.loanReqsIdx); + } + + if (input.numberOfShares > 0 && qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer, + qpi.invocator(), qpi.invocator(), + SELF_INDEX, SELF_INDEX) - sint64(locals.userAssetsAmountInHold) >= input.numberOfShares) + { + locals.releaseResult = qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), + input.numberOfShares, input.newManagingContractIndex, + input.newManagingContractIndex, qpi.invocationReward()); + if (locals.releaseResult < 0 || locals.releaseResult == INVALID_AMOUNT) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + if (qpi.invocationReward() > locals.releaseResult) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.releaseResult); + } + } + else + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + + struct PayLoanDebt_input + { + uint64 reqId; + }; + + struct PayLoanDebt_output + { + }; + + struct PayLoanDebt_locals + { + LoanReqInfo tmpLoanReq; + + _TransferAssetsFromTo_input transferAssetsFromToInput; + _TransferAssetsFromTo_output transferAssetsFromToOutput; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(PayLoanDebt) + { + if (state.get()._loanReqs.get(input.reqId, locals.tmpLoanReq) == false) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + // Make sure we check all conditions before moving forward + if (locals.tmpLoanReq.borrower != qpi.invocator() + || locals.tmpLoanReq.state != LoanReqState::ACTIVE + || sint64(locals.tmpLoanReq.debtAmount) > qpi.invocationReward()) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + // Transfer all assets back to the borrower if it was transfered to the creditor + if (locals.tmpLoanReq.assetsToCreditor) + { + locals.transferAssetsFromToInput.loanReq = locals.tmpLoanReq; + locals.transferAssetsFromToInput.from = locals.tmpLoanReq.creditor; + locals.transferAssetsFromToInput.to = locals.tmpLoanReq.borrower; + CALL(_TransferAssetsFromTo, locals.transferAssetsFromToInput, locals.transferAssetsFromToOutput); + + if (!locals.transferAssetsFromToOutput.allGood) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + } + + // Send all money with percentage to the creditor from borrower + qpi.transfer(locals.tmpLoanReq.creditor, locals.tmpLoanReq.debtAmount); + + if (qpi.invocationReward() > sint64(locals.tmpLoanReq.debtAmount)) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.tmpLoanReq.debtAmount); + } + + state.mut()._loanReqs.removeByKey(input.reqId); + state.mut()._totalReqs--; + } + + struct GetAllLoanReqs_input + { + }; + + struct GetAllLoanReqs_output + { + Array reqs; + uint64 reqsAmount; + }; + + struct GetAllLoanReqs_locals + { + LoanReqInfo tmpLoanReqInfo; + + sint64 outputLoanReqsIdx; + sint64 activeLoanReqsIdx; + + _FillLoanReqForOutput_input fillLoanReqForOutputInput; + _FillLoanReqForOutput_output fillLoanReqForOutputOutput; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetAllLoanReqs) + { + output.reqsAmount = 0; + locals.outputLoanReqsIdx = 0; + + locals.activeLoanReqsIdx = state.get()._loanReqs.nextElementIndex(NULL_INDEX); + + while (locals.activeLoanReqsIdx != NULL_INDEX && locals.outputLoanReqsIdx < QLOAN_MAX_OUTPUT_NUM) + { + locals.tmpLoanReqInfo = state.get()._loanReqs.value(locals.activeLoanReqsIdx); + + locals.fillLoanReqForOutputInput.loanReqId = state.get()._loanReqs.key(locals.activeLoanReqsIdx); + locals.fillLoanReqForOutputInput.loanReqInfo = locals.tmpLoanReqInfo; + CALL(_FillLoanReqForOutput, locals.fillLoanReqForOutputInput, locals.fillLoanReqForOutputOutput); + + output.reqs.set(locals.outputLoanReqsIdx, locals.fillLoanReqForOutputOutput.loanOutputInfo); + locals.outputLoanReqsIdx++; + output.reqsAmount++; + + locals.activeLoanReqsIdx = state.get()._loanReqs.nextElementIndex(locals.activeLoanReqsIdx); + } + } + + struct GetUserActiveLoanReqs_input + { + id userId; + }; + + struct GetUserActiveLoanReqs_output + { + Array reqs; + uint64 reqsAmount; + }; + + struct GetUserActiveLoanReqs_locals + { + LoanReqInfo tmpLoanReqInfo; + + sint64 outputLoanReqsIdx; + sint64 activeLoanReqsIdx; + + _FillLoanReqForOutput_input fillLoanReqForOutputInput; + _FillLoanReqForOutput_output fillLoanReqForOutputOutput; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetUserActiveLoanReqs) + { + output.reqsAmount = 0; + locals.outputLoanReqsIdx = 0; + + locals.activeLoanReqsIdx = state.get()._loanReqs.nextElementIndex(NULL_INDEX); + + while (locals.activeLoanReqsIdx != NULL_INDEX && locals.outputLoanReqsIdx < QLOAN_MAX_OUTPUT_NUM) + { + locals.tmpLoanReqInfo = state.get()._loanReqs.value(locals.activeLoanReqsIdx); + if ((locals.tmpLoanReqInfo.borrower == input.userId || locals.tmpLoanReqInfo.creditor == input.userId) + && locals.tmpLoanReqInfo.state == LoanReqState::ACTIVE) + { + locals.fillLoanReqForOutputInput.loanReqId = state.get()._loanReqs.key(locals.activeLoanReqsIdx); + locals.fillLoanReqForOutputInput.loanReqInfo = locals.tmpLoanReqInfo; + CALL(_FillLoanReqForOutput, locals.fillLoanReqForOutputInput, locals.fillLoanReqForOutputOutput); + + output.reqs.set(locals.outputLoanReqsIdx, locals.fillLoanReqForOutputOutput.loanOutputInfo); + locals.outputLoanReqsIdx++; + output.reqsAmount++; + } + + locals.activeLoanReqsIdx = state.get()._loanReqs.nextElementIndex(locals.activeLoanReqsIdx); + } + } + + struct GetFeesInfo_input + { + }; + + struct GetFeesInfo_output + { + uint64 acceptanceFeePercent; + uint64 burnFeePercent; + + uint64 earnedAmount; + uint64 distributedAmount; + uint64 burnedAmount; + uint64 toQvaultAmount; + }; + + + PUBLIC_FUNCTION(GetFeesInfo) + { + output.acceptanceFeePercent = QLOAN_ACCEPTANCE_FEE_PERCENT; + output.burnFeePercent = QLOAN_BURN_PERCENT; + + output.earnedAmount = state.get()._earnedAmount; + output.distributedAmount = state.get()._distributedAmount; + output.burnedAmount = state.get()._burnedAmount; + output.toQvaultAmount = state.get()._toQvaultAmount; + } + + struct GetUserDebt_input + { + id userId; + }; + + struct GetUserDebt_output + { + uint64 totalUserDebt; + }; + + struct GetUserDebt_locals + { + LoanReqInfo tmpLoanReqInfo; + sint64 activeLoanReqsIdx; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetUserDebt) + { + locals.activeLoanReqsIdx = 0; + locals.activeLoanReqsIdx = state.get()._loanReqs.nextElementIndex(NULL_INDEX); + + while (locals.activeLoanReqsIdx != NULL_INDEX) + { + locals.tmpLoanReqInfo = state.get()._loanReqs.value(locals.activeLoanReqsIdx); + if (locals.tmpLoanReqInfo.borrower == input.userId && locals.tmpLoanReqInfo.state == LoanReqState::ACTIVE) + { + output.totalUserDebt += locals.tmpLoanReqInfo.debtAmount; + } + locals.activeLoanReqsIdx = state.get()._loanReqs.nextElementIndex(locals.activeLoanReqsIdx); + } + } + + struct GetLoanReqById_input + { + uint64 reqId; + }; + + struct GetLoanReqById_output + { + LoanOutputInfo loanOutputInfo; + }; + + struct GetLoanReqById_locals + { + LoanReqInfo loanReqInfo; + + _FillLoanReqForOutput_input fillLoanReqForOutputInput; + _FillLoanReqForOutput_output fillLoanReqForOutputOutput; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetLoanReqById) + { + if (state.get()._loanReqs.get(input.reqId, locals.loanReqInfo)) + { + locals.fillLoanReqForOutputInput.loanReqId = input.reqId; + locals.fillLoanReqForOutputInput.loanReqInfo = locals.loanReqInfo; + CALL(_FillLoanReqForOutput, locals.fillLoanReqForOutputInput, locals.fillLoanReqForOutputOutput); + output.loanOutputInfo = locals.fillLoanReqForOutputOutput.loanOutputInfo; + } + } + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_PROCEDURE(PlaceLoanReq, 1); + REGISTER_USER_PROCEDURE(RemoveLoanReq, 2); + REGISTER_USER_PROCEDURE(AcceptLoanReq, 3); + REGISTER_USER_PROCEDURE(TransferShareManagementRights, 4); + REGISTER_USER_PROCEDURE(PayLoanDebt, 5); + + REGISTER_USER_FUNCTION(GetAllLoanReqs, 1); + REGISTER_USER_FUNCTION(GetUserActiveLoanReqs, 2); + REGISTER_USER_FUNCTION(GetUserDebt, 3); + REGISTER_USER_FUNCTION(GetFeesInfo, 4); + REGISTER_USER_FUNCTION(GetLoanReqById, 5); + } + + struct BEGIN_EPOCH_locals + { + sint64 activeLoanReqsIdx; + LoanReqInfo tmpLoanReqInfo; + uint64 accruedInterest; + + _TransferAssetsFromTo_input transferAssetsFromToInput; + _TransferAssetsFromTo_output transferAssetsFromToOutput; + }; + + + BEGIN_EPOCH_WITH_LOCALS() + { + locals.activeLoanReqsIdx = state.get()._loanReqs.nextElementIndex(NULL_INDEX); + while (locals.activeLoanReqsIdx != NULL_INDEX) + { + locals.tmpLoanReqInfo = state.get()._loanReqs.value(locals.activeLoanReqsIdx); + + // Check if contract already ends + if (locals.tmpLoanReqInfo.epochsLeft == 0 && locals.tmpLoanReqInfo.state == LoanReqState::ACTIVE) + { + locals.transferAssetsFromToInput.loanReq = locals.tmpLoanReqInfo; + + // Check if assets were NOT transfered to the creditor, if not - transfer assets from borrower to the creditor + if (locals.tmpLoanReqInfo.assetsToCreditor == false) + { + locals.transferAssetsFromToInput.from = locals.tmpLoanReqInfo.borrower; + locals.transferAssetsFromToInput.to = locals.tmpLoanReqInfo.creditor; + CALL(_TransferAssetsFromTo, locals.transferAssetsFromToInput, locals.transferAssetsFromToOutput); + if (!locals.transferAssetsFromToOutput.allGood) + { + locals.activeLoanReqsIdx = state.get()._loanReqs.nextElementIndex(locals.activeLoanReqsIdx); + continue; + } + } + + state.mut()._loanReqs.removeByKey(state.get()._loanReqs.key(locals.activeLoanReqsIdx)); + state.mut()._totalReqs--; + } + else if (locals.tmpLoanReqInfo.state == LoanReqState::ACTIVE) + { + locals.tmpLoanReqInfo.epochsLeft--; + + if (div((locals.tmpLoanReqInfo.returnPeriodInEpochs - locals.tmpLoanReqInfo.epochsLeft) * 100, locals.tmpLoanReqInfo.returnPeriodInEpochs) >= 34) + { + locals.tmpLoanReqInfo.debtAmount = sadd(locals.tmpLoanReqInfo.priceAmount, div(smul(locals.tmpLoanReqInfo.fullInterest, locals.tmpLoanReqInfo.returnPeriodInEpochs - locals.tmpLoanReqInfo.epochsLeft), locals.tmpLoanReqInfo.returnPeriodInEpochs)); + } + + state.mut()._loanReqs.replace(state.get()._loanReqs.key(locals.activeLoanReqsIdx), locals.tmpLoanReqInfo); + } + + locals.activeLoanReqsIdx = state.get()._loanReqs.nextElementIndex(locals.activeLoanReqsIdx); + } + } + + struct END_EPOCH_locals + { + uint64 amountToBurn; + uint64 amountToQvault; + }; + + END_EPOCH_WITH_LOCALS() + { + locals.amountToBurn = div(smul((state.get()._earnedAmount - state.get()._distributedAmount), QLOAN_BURN_PERCENT), 10000ULL); + locals.amountToQvault = div(smul((state.get()._earnedAmount - state.get()._distributedAmount), QLOAN_QVAULT_PERCENT), 10000ULL); + + if (state.get()._earnedAmount > state.get()._distributedAmount) + { + qpi.burn(locals.amountToBurn); + qpi.transfer(id(QVAULT_CONTRACT_INDEX, 0, 0, 0), locals.amountToQvault); + state.mut()._distributedAmount += locals.amountToBurn; + state.mut()._distributedAmount += locals.amountToQvault; + + state.mut()._burnedAmount += locals.amountToBurn; + state.mut()._toQvaultAmount += locals.amountToQvault; + } + } + + INITIALIZE() + { + state.mut()._currentLoanIndex = 1; + state.mut()._totalReqs = 0; + + state.mut()._earnedAmount = 0; + state.mut()._distributedAmount = 0; + state.mut()._burnedAmount = 0; + state.mut()._toQvaultAmount = 0; + } + + PRE_ACQUIRE_SHARES() + { + output.allowTransfer = true; + } + + POST_INCOMING_TRANSFER() + { + if (input.type == TransferType::standardTransaction) + { + qpi.transfer(input.sourceId, input.amount); + } + } +}; diff --git a/test/contract_qloan.cpp b/test/contract_qloan.cpp new file mode 100644 index 00000000..038f37c6 --- /dev/null +++ b/test/contract_qloan.cpp @@ -0,0 +1,514 @@ +#define NO_UEFI + +#include "contract_testing.h" + +const id testAddress1 = ID(_H, _O, _G, _T, _K, _D, _N, _D, _V, _U, _U, _Z, _U, _F, _L, _A, _M, _L, _V, _B, _L, _Z, _D, _S, _G, _D, _D, _A, _E, _B, _E, _K, _K, _L, _N, _Z, _J, _B, _W, _S, _C, _A, _M, _D, _S, _X, _T, _C, _X, _A, _M, _A, _X, _U, _D, _F); +const id testAddress2 = ID(_E, _Q, _M, _B, _B, _V, _Y, _G, _Z, _O, _F, _U, _I, _H, _E, _X, _F, _O, _X, _K, _T, _F, _T, _A, _N, _E, _K, _B, _X, _L, _B, _X, _H, _A, _Y, _D, _F, _F, _M, _R, _E, _E, _M, _R, _Q, _E, _V, _A, _D, _Y, _M, _M, _E, _W, _A, _C); +const id testAddress3 = ID(_E, _C, _M, _C, _B, _V, _Y, _G, _Z, _O, _F, _L, _I, _H, _E, _X, _F, _O, _X, _K, _T, _F, _T, _A, _N, _E, _K, _B, _X, _L, _B, _X, _H, _A, _Y, _D, _F, _F, _M, _R, _E, _E, _M, _R, _Q, _E, _V, _A, _D, _Y, _M, _M, _E, _W, _A, _C); + +class QLoanChecker : public QLOAN, public QLOAN::StateData +{ +public: + uint64 getLoanReqsPopulation() + { + return _loanReqs.population(); + } + + uint64 getTotalReqs() + { + return _totalReqs; + } +}; + +class ContractTestingQLoan : protected ContractTesting +{ +public: + ContractTestingQLoan() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(QLOAN); + callSystemProcedure(QLOAN_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QX); + callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); + } + + QLoanChecker* getState() + { + return (QLoanChecker*)contractStates[QLOAN_CONTRACT_INDEX]; + } + + void beginEpoch(bool expectSuccess = true) + { + callSystemProcedure(QX_CONTRACT_INDEX, BEGIN_EPOCH, expectSuccess); + callSystemProcedure(QLOAN_CONTRACT_INDEX, BEGIN_EPOCH, expectSuccess); + } + + void endEpoch(bool expectSuccess = true) + { + callSystemProcedure(QLOAN_CONTRACT_INDEX, END_EPOCH, expectSuccess); + } + + void issueAsset(const id& user, const std::string& assetName, const uint64& numberOfShares) + { + QX::IssueAsset_input input; + input.assetName = assetNameFromString(assetName.c_str()); + input.numberOfShares = numberOfShares; + input.unitOfMeasurement = 67; + input.numberOfDecimalPlaces = 2; + QX::IssueAsset_output output; + invokeUserProcedure(QX_CONTRACT_INDEX, 1, input, output, user, 1000000000); + } + + void transferRightsToQloan(const id& user, const std::string& assetName, const id& issuer, const uint64 numberOfShares) + { + QX::TransferShareManagementRights_input input; + input.asset.assetName = assetNameFromString(assetName.c_str()); + input.asset.issuer = issuer; + input.numberOfShares = numberOfShares; + input.newManagingContractIndex = QLOAN_CONTRACT_INDEX; + QX::TransferShareManagementRights_output output; + invokeUserProcedure(QX_CONTRACT_INDEX, 9, input, output, user, 2000); + } + + void transferRightsToQx(const id& user, const std::string& assetName, const id& issuer, const uint64 numberOfShares) + { + QLOAN::TransferShareManagementRights_input input; + input.asset.assetName = assetNameFromString(assetName.c_str()); + input.asset.issuer = issuer; + input.numberOfShares = numberOfShares; + input.newManagingContractIndex = QX_CONTRACT_INDEX; + QLOAN::TransferShareManagementRights_output output; + invokeUserProcedure(QLOAN_CONTRACT_INDEX, 4, input, output, user, 2000); + } + + void placeLoanReq(const id& user, const std::string& assetName, const id& assetIssuer, const uint64& numberOfShares, + const uint64& price, const uint64& interestRate, const uint64& returnPeriod, bool isLoanReq, bool assetsToCreditor, + const int64_t& quAmount, const id& privateId = NULL_ID) + { + Asset testAsset; + testAsset.assetName = assetNameFromString(assetName.c_str()); + testAsset.issuer = assetIssuer; + + QLOAN::PlaceLoanReq_input input; + input.privateId = privateId; + input.assets.set(0, testAsset); + input.assetAmount.set(0, numberOfShares); + input.assetsNum = 1; + input.price = price; + input.interestRate = interestRate; + input.returnPeriodInEpochs = returnPeriod; + input.isLoanReq = isLoanReq; + input.assetsToCreditor = assetsToCreditor; + + QLOAN::PlaceLoanReq_output output; + invokeUserProcedure(QLOAN_CONTRACT_INDEX, 1, input, output, user, quAmount); + } + + void acceptLoanReq(const id& user, const uint64& reqId, const int64_t& quAmount) + { + QLOAN::AcceptLoanReq_input input{ reqId }; + QLOAN::AcceptLoanReq_output output; + invokeUserProcedure(QLOAN_CONTRACT_INDEX, 3, input, output, user, quAmount); + } + + void removeLoanReq(const id& user, const uint64& reqId, const int64_t& quAmount) + { + QLOAN::RemoveLoanReq_input input{ reqId }; + QLOAN::RemoveLoanReq_output output; + invokeUserProcedure(QLOAN_CONTRACT_INDEX, 2, input, output, user, quAmount); + } + + void payLoanDebt(const id& user, const uint64& reqId, const int64_t& quAmount) + { + QLOAN::PayLoanDebt_input input{ reqId }; + QLOAN::PayLoanDebt_output output; + invokeUserProcedure(QLOAN_CONTRACT_INDEX, 5, input, output, user, quAmount); + } + + QLOAN::GetAllLoanReqs_output getAllLoanReqs() + { + QLOAN::GetAllLoanReqs_input input; + QLOAN::GetAllLoanReqs_output output; + callFunction(QLOAN_CONTRACT_INDEX, 1, input, output); + return output; + } + + QLOAN::GetFeesInfo_output getFeesInfo() + { + QLOAN::GetFeesInfo_input input; + QLOAN::GetFeesInfo_output output; + callFunction(QLOAN_CONTRACT_INDEX, 4, input, output); + return output; + } + + QLOAN::GetUserDebt_output getUserDebt(const id& user) + { + QLOAN::GetUserDebt_input input{ user }; + QLOAN::GetUserDebt_output output; + callFunction(QLOAN_CONTRACT_INDEX, 3, input, output); + return output; + } +}; + +TEST(ContractQLoan, PlaceLoanReqValidation) +{ + system.epoch = 250; + ContractTestingQLoan qloan; + qloan.beginEpoch(); + + increaseEnergy(testAddress1, 2000000000); + + std::string assetName = "ABCD"; + qloan.placeLoanReq(testAddress1, assetName, testAddress1, 1000, 10, 10, 10, true, false, QLOAN_PLACE_LOAN_REQ_FEE); + EXPECT_EQ(qloan.getState()->getLoanReqsPopulation(), 0); + + qloan.issueAsset(testAddress1, assetName, 4000); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(assetName.c_str()), testAddress1, testAddress1, testAddress1, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), 4000); + qloan.transferRightsToQloan(testAddress1, assetName, testAddress1, 600); + + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(assetName.c_str()), testAddress1, testAddress1, testAddress1, QLOAN_CONTRACT_INDEX, QLOAN_CONTRACT_INDEX), 600); + qloan.placeLoanReq(testAddress1, assetName, testAddress1, 300, 10, 10, 10, true, false, QLOAN_PLACE_LOAN_REQ_FEE); + EXPECT_EQ(qloan.getState()->getLoanReqsPopulation(), 1); + + qloan.placeLoanReq(testAddress1, assetName, testAddress1, 300, 10, 10, 10, true, false, QLOAN_PLACE_LOAN_REQ_FEE); + EXPECT_EQ(qloan.getState()->getLoanReqsPopulation(), 2); +} + +TEST(ContractQLoan, PlaceLoanPrivateRequest) +{ + ContractTestingQLoan qloan; + + increaseEnergy(testAddress1, 2000000000); + increaseEnergy(testAddress2, 2000000000); + + std::string assetName = "ABCE"; + qloan.issueAsset(testAddress1, assetName, 4000); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(assetName.c_str()), testAddress1, testAddress1, testAddress1, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), 4000); + qloan.transferRightsToQloan(testAddress1, assetName, testAddress1, 300); + + qloan.placeLoanReq(testAddress1, assetName, testAddress1, 300, 2000, 10, 10, true, true, QLOAN_PLACE_LOAN_REQ_FEE, testAddress3); + + auto allReqsBefore = qloan.getAllLoanReqs(); + EXPECT_EQ(allReqsBefore.reqsAmount, 1); + EXPECT_EQ(allReqsBefore.reqs.get(0).borrower, testAddress1); + EXPECT_EQ(allReqsBefore.reqs.get(0).creditor, NULL_ID); + EXPECT_EQ(allReqsBefore.reqs.get(0).privateId, testAddress3); + EXPECT_EQ(allReqsBefore.reqs.get(0).state, QLOAN::LoanReqState::IDLE); + + uint64 reqId = allReqsBefore.reqs.get(0).reqId; + qloan.acceptLoanReq(testAddress2, reqId, allReqsBefore.reqs.get(0).priceAmount); + + auto allReqsAfter = qloan.getAllLoanReqs(); + EXPECT_EQ(allReqsAfter.reqsAmount, 1); + EXPECT_EQ(allReqsAfter.reqs.get(0).creditor, NULL_ID); + EXPECT_EQ(allReqsAfter.reqs.get(0).state, QLOAN::LoanReqState::IDLE); +} + +TEST(ContractQLoan, AcceptLoanReq) +{ + ContractTestingQLoan qloan; + + increaseEnergy(testAddress1, 2000000000); + increaseEnergy(testAddress2, 2000000000); + + std::string assetName = "ABCE"; + qloan.issueAsset(testAddress1, assetName, 4000); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(assetName.c_str()), testAddress1, testAddress1, testAddress1, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), 4000); + qloan.transferRightsToQloan(testAddress1, assetName, testAddress1, 600); + + qloan.placeLoanReq(testAddress1, assetName, testAddress1, 300, 2000, 10, 10, true, false, QLOAN_PLACE_LOAN_REQ_FEE); + + auto allReqsBefore = qloan.getAllLoanReqs(); + EXPECT_EQ(allReqsBefore.reqsAmount, 1); + EXPECT_EQ(allReqsBefore.reqs.get(0).borrower, testAddress1); + EXPECT_EQ(allReqsBefore.reqs.get(0).creditor, NULL_ID); + EXPECT_EQ(allReqsBefore.reqs.get(0).state, QLOAN::LoanReqState::IDLE); + + uint64 reqId = allReqsBefore.reqs.get(0).reqId; + qloan.acceptLoanReq(testAddress2, reqId, allReqsBefore.reqs.get(0).priceAmount); + + auto allReqsAfter = qloan.getAllLoanReqs(); + EXPECT_EQ(allReqsAfter.reqsAmount, 1); + EXPECT_EQ(allReqsAfter.reqs.get(0).creditor, testAddress2); + EXPECT_EQ(allReqsAfter.reqs.get(0).acceptedBy, testAddress2); + EXPECT_EQ(allReqsAfter.reqs.get(0).state, QLOAN::LoanReqState::ACTIVE); + + auto fees = qloan.getFeesInfo(); + EXPECT_EQ(fees.earnedAmount, QLOAN_PLACE_LOAN_REQ_FEE + 30); +} + +TEST(ContractQLoan, AcceptLoanReqCreditRequest) +{ + ContractTestingQLoan qloan; + + increaseEnergy(testAddress1, 2000000000); + increaseEnergy(testAddress2, 2000000000); + + std::string assetName = "ABCF"; + qloan.placeLoanReq(testAddress1, assetName, testAddress2, 300, 2000, 10, 10, false, false, QLOAN_PLACE_LOAN_REQ_FEE + 2000); + + qloan.issueAsset(testAddress2, assetName, 4000); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(assetName.c_str()), testAddress2, testAddress2, testAddress2, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), 4000); + qloan.transferRightsToQloan(testAddress2, assetName, testAddress2, 300); + + auto allReqsBefore = qloan.getAllLoanReqs(); + uint64 reqId = allReqsBefore.reqs.get(0).reqId; + qloan.acceptLoanReq(testAddress2, reqId, 1000); + + auto allReqsAfter = qloan.getAllLoanReqs(); + EXPECT_EQ(allReqsAfter.reqs.get(0).creditor, testAddress1); + EXPECT_EQ(allReqsAfter.reqs.get(0).borrower, testAddress2); + + EXPECT_EQ(allReqsAfter.reqs.get(0).state, QLOAN::LoanReqState::ACTIVE); +} + +TEST(ContractQLoan, RemoveLoanReq) +{ + ContractTestingQLoan qloan; + + increaseEnergy(testAddress1, 2000000000); + + std::string assetName = "ABCG"; + qloan.issueAsset(testAddress1, assetName, 4000); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(assetName.c_str()), testAddress1, testAddress1, testAddress1, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), 4000); + qloan.transferRightsToQloan(testAddress1, assetName, testAddress1, 300); + + qloan.placeLoanReq(testAddress1, assetName, testAddress1, 300, 2000, 10, 10, true, false, QLOAN_PLACE_LOAN_REQ_FEE); + + EXPECT_EQ(qloan.getState()->getLoanReqsPopulation(), 1); + EXPECT_EQ(qloan.getState()->getTotalReqs(), 1); + + auto allReqsAfter = qloan.getAllLoanReqs(); + uint64 reqId = allReqsAfter.reqs.get(0).reqId; + qloan.removeLoanReq(testAddress1, reqId, 0); + + EXPECT_EQ(qloan.getState()->getLoanReqsPopulation(), 0); + EXPECT_EQ(qloan.getState()->getTotalReqs(), 0); +} + +TEST(ContractQLoan, EpochDebtCalculation) +{ + ContractTestingQLoan qloan; + + increaseEnergy(testAddress1, 2000000000); + increaseEnergy(testAddress2, 2000000000); + + std::string assetName = "ABCF"; + qloan.placeLoanReq(testAddress1, assetName, testAddress2, 300, 2000, 10, 2, false, false, QLOAN_PLACE_LOAN_REQ_FEE + 2000); + + qloan.issueAsset(testAddress2, assetName, 4000); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(assetName.c_str()), testAddress2, testAddress2, testAddress2, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), 4000); + qloan.transferRightsToQloan(testAddress2, assetName, testAddress2, 300); + + auto reqs = qloan.getAllLoanReqs(); + uint64 reqId = reqs.reqs.get(0).reqId; + qloan.acceptLoanReq(testAddress2, reqId, 1000); + + reqs = qloan.getAllLoanReqs(); + EXPECT_EQ(reqs.reqs.get(0).creditor, testAddress1); + EXPECT_EQ(reqs.reqs.get(0).borrower, testAddress2); + + qloan.beginEpoch(); + reqs = qloan.getAllLoanReqs(); + EXPECT_EQ(reqs.reqs.get(0).debtAmount, 2100); + EXPECT_EQ(reqs.reqs.get(0).epochsLeft, 1); + + qloan.beginEpoch(); + reqs = qloan.getAllLoanReqs(); + EXPECT_EQ(reqs.reqs.get(0).debtAmount, 2200); + EXPECT_EQ(reqs.reqs.get(0).epochsLeft, 0); +} + +TEST(ContractQLoan, LoanExpiration) +{ + ContractTestingQLoan qloan; + + increaseEnergy(testAddress1, 2000000000); + increaseEnergy(testAddress2, 2000000000); + + std::string assetName = "ABCG"; + qloan.placeLoanReq(testAddress1, assetName, testAddress2, 300, 2000, 10, 2, false, false, QLOAN_PLACE_LOAN_REQ_FEE + 2000); + + qloan.issueAsset(testAddress2, assetName, 4000); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(assetName.c_str()), testAddress2, testAddress2, testAddress2, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), 4000); + qloan.transferRightsToQloan(testAddress2, assetName, testAddress2, 300); + + auto reqs = qloan.getAllLoanReqs(); + uint64 reqId = reqs.reqs.get(0).reqId; + qloan.acceptLoanReq(testAddress2, reqId, 1000); + EXPECT_EQ(reqs.reqs.get(0).debtAmount, 2066); + EXPECT_EQ(1, qloan.getState()->_loanReqs.population()); + EXPECT_EQ(1, qloan.getState()->_totalReqs); + + for (int i = 0; i < 3; i++) qloan.beginEpoch(); + + EXPECT_EQ(qloan.getState()->_loanReqs.population(), 0); + EXPECT_EQ(qloan.getState()->_totalReqs, 0); +} + +TEST(ContractQLoan, PayLoanDebt) +{ + ContractTestingQLoan qloan; + + increaseEnergy(testAddress1, 2000000000); + increaseEnergy(testAddress2, 2000000000); + + std::string assetName = "ABCH"; + qloan.placeLoanReq(testAddress1, assetName, testAddress2, 300, 2000, 10, 10, false, true, QLOAN_PLACE_LOAN_REQ_FEE + 2000); + + qloan.issueAsset(testAddress2, assetName, 4000); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(assetName.c_str()), testAddress2, testAddress2, testAddress2, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), 4000); + qloan.transferRightsToQloan(testAddress2, assetName, testAddress2, 300); + + auto reqs = qloan.getAllLoanReqs(); + uint64 reqId = reqs.reqs.get(0).reqId; + qloan.acceptLoanReq(testAddress2, reqId, 1000); + + for (int i = 0; i < 3; i++) qloan.beginEpoch(); + + reqs = qloan.getAllLoanReqs(); + EXPECT_EQ(reqs.reqs.get(0).debtAmount, 2066); + EXPECT_EQ(reqs.reqs.get(0).epochsLeft, 7); + EXPECT_EQ(reqs.reqs.get(0).state, QLOAN::LoanReqState::ACTIVE); + + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(assetName.c_str()), testAddress2, testAddress1, testAddress1, QLOAN_CONTRACT_INDEX, QLOAN_CONTRACT_INDEX), 300); + qloan.transferRightsToQx(testAddress1, assetName, testAddress2, 200); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(assetName.c_str()), testAddress2, testAddress1, testAddress1, QLOAN_CONTRACT_INDEX, QLOAN_CONTRACT_INDEX), 300); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(assetName.c_str()), testAddress2, testAddress1, testAddress1, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), 0); + + qloan.payLoanDebt(testAddress2, reqs.reqs.get(0).reqId, reqs.reqs.get(0).debtAmount); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(assetName.c_str()), testAddress2, testAddress1, testAddress1, QLOAN_CONTRACT_INDEX, QLOAN_CONTRACT_INDEX), 0); + reqs = qloan.getAllLoanReqs(); + EXPECT_EQ(reqs.reqs.get(0).debtAmount, 0); + EXPECT_EQ(qloan.getState()->_loanReqs.population(), 0); + EXPECT_EQ(qloan.getState()->_totalReqs, 0); +} + +TEST(ContractQLoan, FeesDistribution) +{ + ContractTestingQLoan qloan; + + increaseEnergy(testAddress1, 2000000000); + + std::string assetName = "ABCH"; + qloan.placeLoanReq(testAddress1, assetName, testAddress2, 300, 2000, 10, 10, false, false, QLOAN_PLACE_LOAN_REQ_FEE + 2000); + qloan.placeLoanReq(testAddress1, assetName, testAddress2, 300, 2000, 10, 10, false, false, QLOAN_PLACE_LOAN_REQ_FEE + 2000); + + auto fees1 = qloan.getFeesInfo(); + EXPECT_EQ(fees1.earnedAmount, QLOAN_PLACE_LOAN_REQ_FEE * 2); + EXPECT_EQ(fees1.distributedAmount, 0); + EXPECT_EQ(fees1.toQvaultAmount, 0); + + qloan.endEpoch(); + + auto fees2 = qloan.getFeesInfo(); + EXPECT_EQ(fees2.distributedAmount, 200000); + EXPECT_EQ(fees2.burnedAmount, 6000); + EXPECT_EQ(fees2.toQvaultAmount, 194000); +} + +TEST(ContractQLoan, TotalUserDebt) +{ + ContractTestingQLoan qloan; + qloan.beginEpoch(); + + increaseEnergy(testAddress1, 2000000000); + increaseEnergy(testAddress2, 2000000000); + + std::string assetName = "ABCK"; + qloan.placeLoanReq(testAddress1, assetName, testAddress2, 300, 2000, 100, 10, false, false, QLOAN_PLACE_LOAN_REQ_FEE + 2000); + + qloan.issueAsset(testAddress2, assetName, 4000); + qloan.transferRightsToQloan(testAddress2, assetName, testAddress2, 300); + + auto reqs = qloan.getAllLoanReqs(); + + qloan.acceptLoanReq(testAddress2, reqs.reqs.get(0).reqId, 0); + + auto fees1 = qloan.getFeesInfo(); + + // 0 + auto userDebt = qloan.getUserDebt(testAddress2); + EXPECT_EQ(userDebt.totalUserDebt, 2666); + qloan.endEpoch(); + + // 1 + system.epoch++; + qloan.beginEpoch(); + userDebt = qloan.getUserDebt(testAddress2); + EXPECT_EQ(userDebt.totalUserDebt, 2666); + qloan.endEpoch(); + + // 2 + system.epoch++; + qloan.beginEpoch(); + userDebt = qloan.getUserDebt(testAddress2); + EXPECT_EQ(userDebt.totalUserDebt, 2666); + qloan.endEpoch(); + + // 3 + system.epoch++; + qloan.beginEpoch(); + userDebt = qloan.getUserDebt(testAddress2); + EXPECT_EQ(userDebt.totalUserDebt, 2666); + qloan.endEpoch(); + + // 4 + system.epoch++; + qloan.beginEpoch(); + userDebt = qloan.getUserDebt(testAddress2); + EXPECT_EQ(userDebt.totalUserDebt, 2800); + qloan.endEpoch(); + + // 5 + system.epoch++; + qloan.beginEpoch(); + userDebt = qloan.getUserDebt(testAddress2); + EXPECT_EQ(userDebt.totalUserDebt, 3000); + qloan.endEpoch(); + + // 6 + system.epoch++; + qloan.beginEpoch(); + userDebt = qloan.getUserDebt(testAddress2); + EXPECT_EQ(userDebt.totalUserDebt, 3200); + qloan.endEpoch(); + + // 7 + system.epoch++; + qloan.beginEpoch(); + userDebt = qloan.getUserDebt(testAddress2); + EXPECT_EQ(userDebt.totalUserDebt, 3400); + qloan.endEpoch(); + + // 8 + system.epoch++; + qloan.beginEpoch(); + userDebt = qloan.getUserDebt(testAddress2); + EXPECT_EQ(userDebt.totalUserDebt, 3600); + qloan.endEpoch(); + + // 9 + system.epoch++; + qloan.beginEpoch(); + userDebt = qloan.getUserDebt(testAddress2); + EXPECT_EQ(userDebt.totalUserDebt, 3800); + qloan.endEpoch(); + + // 10 + system.epoch++; + qloan.beginEpoch(); + userDebt = qloan.getUserDebt(testAddress2); + EXPECT_EQ(userDebt.totalUserDebt, 4000); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(assetName.c_str()), testAddress2, testAddress1, testAddress1, QLOAN_CONTRACT_INDEX, QLOAN_CONTRACT_INDEX), 0); + qloan.endEpoch(); + + // 11 + system.epoch++; + qloan.beginEpoch(); + userDebt = qloan.getUserDebt(testAddress2); + EXPECT_EQ(userDebt.totalUserDebt, 0); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(assetName.c_str()), testAddress2, testAddress1, testAddress1, QLOAN_CONTRACT_INDEX, QLOAN_CONTRACT_INDEX), 300); + qloan.endEpoch(); +} diff --git a/test/test.vcxproj b/test/test.vcxproj index fe0736f6..39489bc9 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -123,6 +123,7 @@ + diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index 6f776a78..98b80259 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -58,6 +58,7 @@ +