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