From 8fa006a2a3bfc4050432101c2ce31452d05a1a3f Mon Sep 17 00:00:00 2001 From: icyblob Date: Fri, 6 Dec 2024 13:16:03 +0700 Subject: [PATCH] Add Multi-sig Vault SC --- src/Qubic.vcxproj | 1 + src/Qubic.vcxproj.filters | 3 + src/contract_core/contract_def.h | 12 + src/contracts/MsVault.h | 708 +++++++++++++++++++++++++++++++ test/contract_msvault.cpp | 418 ++++++++++++++++++ test/test.vcxproj | 1 + test/test.vcxproj.filters | 1 + 7 files changed, 1144 insertions(+) create mode 100644 src/contracts/MsVault.h create mode 100644 test/contract_msvault.cpp diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 27c41c20..2dc7217a 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -27,6 +27,7 @@ + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index 94918128..46ef6bf2 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -102,6 +102,9 @@ contracts + + + contracts contracts diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 9826c336..2be36ba9 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -161,6 +161,16 @@ struct __FunctionOrProcedureBeginEndGuard #define CONTRACT_STATE2_TYPE QVAULT2 #include "contracts/QVAULT.h" +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define MSVAULT_CONTRACT_INDEX 11 +#define CONTRACT_INDEX MSVAULT_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE MSVAULT +#define CONTRACT_STATE2_TYPE MSVAULT2 +#include "contracts/MsVault.h" + #define MAX_CONTRACT_ITERATION_DURATION 0 // In milliseconds, must be above 0; for now set to 0 to disable timeout, because a rollback mechanism needs to be implemented to properly handle timeout #undef INITIALIZE @@ -214,6 +224,7 @@ constexpr struct ContractDescription {"CCF", 127, 10000, sizeof(CCF)}, // proposal in epoch 125, IPO in 126, construction and first use in 127 {"QEARN", 137, 10000, sizeof(QEARN)}, // proposal in epoch 135, IPO in 136, construction in 137 / first donation after END_EPOCH, first round in epoch 138 {"QVAULT", 138, 10000, sizeof(IPO)}, // proposal in epoch 136, IPO in 137, construction and first use in 138 + {"MSVAULT", 999, 10000, sizeof(MSVAULT)}, }; constexpr unsigned int contractCount = sizeof(contractDescriptions) / sizeof(contractDescriptions[0]); @@ -300,4 +311,5 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(CCF); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QEARN); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QVAULT); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(MSVAULT); } diff --git a/src/contracts/MsVault.h b/src/contracts/MsVault.h new file mode 100644 index 00000000..ce58e04d --- /dev/null +++ b/src/contracts/MsVault.h @@ -0,0 +1,708 @@ +using namespace QPI; + +constexpr uint16 MAX_OWNERS = 32; +constexpr uint16 INITIAL_MAX_VAULTS = 1024; +constexpr uint16 MAX_VAULTS = INITIAL_MAX_VAULTS * X_MULTIPLIER; + +constexpr uint64 REGISTERING_FEE = 10000ULL; +constexpr uint64 RELEASE_FEE = 1000ULL; +constexpr uint64 RELEASE_RESET_FEE = 500ULL; +constexpr uint64 HOLDING_FEE = 100ULL; + +constexpr uint16 VAULT_TYPE_QUORUM = 1; +constexpr uint16 VAULT_TYPE_TWO_OUT_OF_X = 2; + +struct MSVAULT2 +{ +}; + +struct MSVAULT : public ContractBase +{ + struct Vault + { + uint16 vaultType; + id vaultName; + array owners; + uint16 numberOfOwners; + sint64 balance; + bit isActive; + array releaseAmounts; + array releaseDestinations; + }; + + struct MSVaultLogger + { + unsigned int _contractIndex; + // 1: Invalid vault ID or vault inactive + // 2: Caller not an owner + // 3: Invalid parameters (e.g., amount=0, destination=NULL_ID) + // 4: Release successful + // 5: Insufficient balance + // 6: Release not fully approved + // 7: Reset release requests successful + unsigned int _type; + uint64 vaultID; + id ownerID; + uint64 amount; + id destination; + char _terminator; + }; + + array vaults; + + uint32 numberOfActiveVaults; + uint64 totalRevenue; + uint64 totalDistributedToShareholders; + + struct findOwnerIndexInVault_input + { + Vault vault; + id ownerID; + }; + struct findOwnerIndexInVault_output + { + sint32 index; + }; + struct findOwnerIndexInVault_locals + { + sint32 i; + }; + + struct isOwnerOfVault_input + { + Vault vault; + id ownerID; + }; + struct isOwnerOfVault_output + { + bit result; + }; + struct isOwnerOfVault_locals + { + findOwnerIndexInVault_input fi_in; + findOwnerIndexInVault_output fi_out; + findOwnerIndexInVault_locals fi_locals; + }; + + struct resetReleaseRequests_input + { + Vault vault; + }; + struct resetReleaseRequests_output + { + Vault vault; + }; + struct resetReleaseRequests_locals + { + sint32 i; + }; + + struct registerVault_input + { + uint16 vaultType; + id vaultName; + array owners; + }; + struct registerVault_output + { + }; + struct registerVault_locals + { + uint16 ownerCount; + uint16 i, j; + sint32 slotIndex; + Vault newVault; + + resetReleaseRequests_input rr_in; + resetReleaseRequests_output rr_out; + resetReleaseRequests_locals rr_locals; + }; + + struct deposit_input + { + uint64 vaultID; + }; + struct deposit_output + { + }; + struct deposit_locals + { + Vault vault; + }; + + struct releaseTo_input + { + uint64 vaultID; + uint64 amount; + id destination; + }; + struct releaseTo_output + { + }; + struct releaseTo_locals + { + Vault vault; + MSVaultLogger logger; + + sint32 ownerIndex; + uint16 approvals; + uint16 totalOwners; + bit releaseApproved; + uint16 requiredApprovals; + uint16 i; + + isOwnerOfVault_input io_in; + isOwnerOfVault_output io_out; + isOwnerOfVault_locals io_locals; + + findOwnerIndexInVault_input fi_in; + findOwnerIndexInVault_output fi_out; + findOwnerIndexInVault_locals fi_locals; + + resetReleaseRequests_input rr_in; + resetReleaseRequests_output rr_out; + resetReleaseRequests_locals rr_locals; + }; + + struct resetRelease_input + { + uint64 vaultID; + }; + struct resetRelease_output + { + }; + struct resetRelease_locals + { + Vault vault; + MSVaultLogger logger; + sint32 ownerIndex; + + isOwnerOfVault_input io_in; + isOwnerOfVault_output io_out; + isOwnerOfVault_locals io_locals; + + findOwnerIndexInVault_input fi_in; + findOwnerIndexInVault_output fi_out; + findOwnerIndexInVault_locals fi_locals; + + bit found; + }; + + struct getVaults_input + { + id publicKey; + }; + struct getVaults_output + { + uint16 numberOfVaults; + array vaultIDs; + array vaultNames; + }; + struct getVaults_locals + { + uint16 count; + uint16 i, j; + Vault v; + }; + + struct getReleaseStatus_input + { + uint64 vaultID; + }; + struct getReleaseStatus_output + { + array amounts; + array destinations; + }; + struct getReleaseStatus_locals + { + Vault vault; + uint16 i; + }; + + struct getBalanceOf_input + { + uint64 vaultID; + }; + struct getBalanceOf_output + { + sint64 balance; + }; + struct getBalanceOf_locals + { + Vault vault; + }; + + struct getVaultName_input + { + uint64 vaultID; + }; + struct getVaultName_output + { + id vaultName; + }; + struct getVaultName_locals + { + Vault vault; + }; + + struct getRevenueInfo_input {}; + struct getRevenueInfo_output + { + uint32 numberOfActiveVaults; + uint64 totalRevenue; + uint64 totalBurned; + uint64 totalDistributedToShareholders; + }; + + // Helper Functions + PRIVATE_FUNCTION_WITH_LOCALS(findOwnerIndexInVault) + output.index = -1; + for (locals.i = 0; locals.i < input.vault.numberOfOwners; locals.i++) + { + if (input.vault.owners.get(locals.i) == input.ownerID) + { + output.index = locals.i; + break; + } + } + _ + + PRIVATE_FUNCTION_WITH_LOCALS(isOwnerOfVault) + locals.fi_in.vault = input.vault; + locals.fi_in.ownerID = input.ownerID; + findOwnerIndexInVault(qpi, state, locals.fi_in, locals.fi_out, locals.fi_locals); + output.result = (locals.fi_out.index != -1); + _ + + PRIVATE_FUNCTION_WITH_LOCALS(resetReleaseRequests) + for (locals.i = 0; locals.i < MAX_OWNERS; locals.i++) + { + input.vault.releaseAmounts.set(locals.i, 0); + input.vault.releaseDestinations.set(locals.i, NULL_ID); + } + output.vault = input.vault; + _ + + // Procedures and functions + PUBLIC_PROCEDURE_WITH_LOCALS(registerVault) + if (qpi.invocationReward() < REGISTERING_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + locals.ownerCount = 0; + for (locals.i = 0; locals.i < MAX_OWNERS; locals.i++) + { + if (input.owners.get(locals.i) != NULL_ID) + locals.ownerCount++; + } + + if (locals.ownerCount <= 1) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + // Find empty slot + locals.slotIndex = -1; + for (locals.i = 0; locals.i < MAX_VAULTS; locals.i++) + { + Vault tempVault = state.vaults.get(locals.i); + if (!tempVault.isActive && tempVault.numberOfOwners == 0 && tempVault.balance == 0) + { + locals.slotIndex = (sint32)locals.i; + break; + } + } + + if (locals.slotIndex == -1) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + locals.newVault.vaultType = input.vaultType; + locals.newVault.vaultName = input.vaultName; + locals.newVault.numberOfOwners = locals.ownerCount; + locals.newVault.balance = 0; + locals.newVault.isActive = true; + + locals.rr_in.vault = locals.newVault; + resetReleaseRequests(qpi, state, locals.rr_in, locals.rr_out, locals.rr_locals); + locals.newVault = locals.rr_out.vault; + + for (locals.i = 0; locals.i < locals.ownerCount; locals.i++) + { + locals.newVault.owners.set(locals.i, input.owners.get(locals.i)); + } + + state.vaults.set((uint64)locals.slotIndex, locals.newVault); + + //qpi.burn(REGISTERING_FEE); + if (qpi.invocationReward() > REGISTERING_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - REGISTERING_FEE); + } + + state.numberOfActiveVaults++; + state.totalRevenue += REGISTERING_FEE; + _ + + PUBLIC_PROCEDURE_WITH_LOCALS(deposit) + if (input.vaultID >= MAX_VAULTS) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + locals.vault = state.vaults.get(input.vaultID); + if (!locals.vault.isActive) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + locals.vault.balance += qpi.invocationReward(); + state.vaults.set(input.vaultID, locals.vault); + _ + + PUBLIC_PROCEDURE_WITH_LOCALS(releaseTo) + if (qpi.invocationReward() > RELEASE_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - RELEASE_FEE); + } + state.totalRevenue += RELEASE_FEE; + + MSVaultLogger loggerMsg; + loggerMsg._contractIndex = CONTRACT_INDEX; + loggerMsg._type = 0; + loggerMsg.vaultID = input.vaultID; + loggerMsg.ownerID = qpi.invocator(); + loggerMsg.amount = input.amount; + loggerMsg.destination = input.destination; + + if (input.vaultID >= MAX_VAULTS) + { + loggerMsg._type = 1; + LOG_INFO(loggerMsg); + return; + } + + locals.vault = state.vaults.get(input.vaultID); + + if (!locals.vault.isActive) + { + loggerMsg._type = 1; + LOG_INFO(loggerMsg); + return; + } + + locals.io_in.vault = locals.vault; + locals.io_in.ownerID = qpi.invocator(); + isOwnerOfVault(qpi, state, locals.io_in, locals.io_out, locals.io_locals); + if (!locals.io_out.result) + { + loggerMsg._type = 2; + LOG_INFO(loggerMsg); + return; + } + + if (input.amount == 0 || input.destination == NULL_ID) + { + loggerMsg._type = 3; + LOG_INFO(loggerMsg); + return; + } + + locals.fi_in.vault = locals.vault; + locals.fi_in.ownerID = qpi.invocator(); + findOwnerIndexInVault(qpi, state, locals.fi_in, locals.fi_out, locals.fi_locals); + locals.ownerIndex = locals.fi_out.index; + + locals.vault.releaseAmounts.set(locals.ownerIndex, input.amount); + locals.vault.releaseDestinations.set(locals.ownerIndex, input.destination); + + locals.approvals = 0; + locals.totalOwners = locals.vault.numberOfOwners; + for (locals.i = 0; locals.i < locals.totalOwners; locals.i++) + { + if (locals.vault.releaseAmounts.get(locals.i) == input.amount && + locals.vault.releaseDestinations.get(locals.i) == input.destination) + { + locals.approvals++; + } + } + + locals.releaseApproved = false; + if (locals.vault.vaultType == VAULT_TYPE_QUORUM) + { + uint64 calc = ((uint64)locals.totalOwners * 2ULL) + 2ULL; + uint64 divResult = QPI::div(calc, 3ULL); + locals.requiredApprovals = (uint16)divResult; + + if (locals.approvals >= locals.requiredApprovals) + locals.releaseApproved = true; + } + else if (locals.vault.vaultType == VAULT_TYPE_TWO_OUT_OF_X) + { + if (locals.approvals >= 2) + locals.releaseApproved = true; + } + + if (locals.releaseApproved) + { + if (locals.vault.balance >= (sint64)input.amount) + { + qpi.transfer(input.destination, input.amount); + locals.vault.balance -= (sint64)input.amount; + + locals.rr_in.vault = locals.vault; + resetReleaseRequests(qpi, state, locals.rr_in, locals.rr_out, locals.rr_locals); + locals.vault = locals.rr_out.vault; + + state.vaults.set(input.vaultID, locals.vault); + + loggerMsg._type = 4; + LOG_INFO(loggerMsg); + } + else + { + loggerMsg._type = 5; + LOG_INFO(loggerMsg); + } + } + else + { + state.vaults.set(input.vaultID, locals.vault); + loggerMsg._type = 6; + LOG_INFO(loggerMsg); + } + _ + + PUBLIC_PROCEDURE_WITH_LOCALS(resetRelease) + if (qpi.invocationReward() > RELEASE_RESET_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - RELEASE_RESET_FEE); + } + state.totalRevenue += RELEASE_RESET_FEE; + + MSVaultLogger loggerMsg; + loggerMsg._contractIndex = CONTRACT_INDEX; + loggerMsg._type = 0; + loggerMsg.vaultID = input.vaultID; + loggerMsg.ownerID = qpi.invocator(); + loggerMsg.amount = 0; + loggerMsg.destination = NULL_ID; + + if (input.vaultID >= MAX_VAULTS) + { + loggerMsg._type = 1; + LOG_INFO(loggerMsg); + return; + } + + locals.vault = state.vaults.get(input.vaultID); + + if (!locals.vault.isActive) + { + loggerMsg._type = 1; + LOG_INFO(loggerMsg); + return; + } + + locals.io_in.vault = locals.vault; + locals.io_in.ownerID = qpi.invocator(); + isOwnerOfVault(qpi, state, locals.io_in, locals.io_out, locals.io_locals); + if (!locals.io_out.result) + { + loggerMsg._type = 2; + LOG_INFO(loggerMsg); + return; + } + + locals.fi_in.vault = locals.vault; + locals.fi_in.ownerID = qpi.invocator(); + findOwnerIndexInVault(qpi, state, locals.fi_in, locals.fi_out, locals.fi_locals); + locals.ownerIndex = locals.fi_out.index; + + locals.vault.releaseAmounts.set(locals.ownerIndex, 0); + locals.vault.releaseDestinations.set(locals.ownerIndex, NULL_ID); + + state.vaults.set(input.vaultID, locals.vault); + + loggerMsg._type = 7; + LOG_INFO(loggerMsg); + _ + + PUBLIC_FUNCTION_WITH_LOCALS(getVaults) + output.numberOfVaults = 0; + locals.count = 0; + for (locals.i = 0; locals.i < 1024; locals.i++) + { + Vault v = state.vaults.get(locals.i); + if (v.isActive) + { + for (locals.j = 0; locals.j < v.numberOfOwners; locals.j++) + { + if (v.owners.get(locals.j) == input.publicKey) + { + output.vaultIDs.set(locals.count, (uint64)locals.i); + output.vaultNames.set(locals.count, v.vaultName); + locals.count++; + break; + } + } + } + } + output.numberOfVaults = locals.count; + _ + + PUBLIC_FUNCTION_WITH_LOCALS(getReleaseStatus) + if (input.vaultID >= MAX_VAULTS) + { + for (locals.i = 0; locals.i < MAX_OWNERS; locals.i++) + { + output.amounts.set(locals.i, 0); + output.destinations.set(locals.i, NULL_ID); + } + return; + } + + Vault v = state.vaults.get(input.vaultID); + if (!v.isActive) + { + for (locals.i = 0; locals.i < MAX_OWNERS; locals.i++) + { + output.amounts.set(locals.i, 0); + output.destinations.set(locals.i, NULL_ID); + } + return; + } + + for (locals.i = 0; locals.i < v.numberOfOwners; locals.i++) + { + output.amounts.set(locals.i, v.releaseAmounts.get(locals.i)); + output.destinations.set(locals.i, v.releaseDestinations.get(locals.i)); + } + _ + + PUBLIC_FUNCTION_WITH_LOCALS(getBalanceOf) + if (input.vaultID >= MAX_VAULTS) + { + output.balance = 0; + return; + } + + locals.vault = state.vaults.get(input.vaultID); + if (!locals.vault.isActive) + { + output.balance = 0; + return; + } + output.balance = locals.vault.balance; + _ + + PUBLIC_FUNCTION_WITH_LOCALS(getVaultName) + if (input.vaultID >= MAX_VAULTS) + { + output.vaultName = NULL_ID; + return; + } + + locals.vault = state.vaults.get(input.vaultID); + if (!locals.vault.isActive) + { + output.vaultName = NULL_ID; + return; + } + output.vaultName = locals.vault.vaultName; + _ + + PUBLIC_FUNCTION(getRevenueInfo) + output.numberOfActiveVaults = state.numberOfActiveVaults; + output.totalRevenue = state.totalRevenue; + output.totalDistributedToShareholders = state.totalDistributedToShareholders; + _ + + INITIALIZE + state.numberOfActiveVaults = 0; + state.totalRevenue = 0; + state.totalDistributedToShareholders = 0; + _ + + struct BEGIN_EPOCH_locals + { + uint64 i; + resetReleaseRequests_input rr_in; + resetReleaseRequests_output rr_out; + resetReleaseRequests_locals rr_locals; + }; + + BEGIN_EPOCH_WITH_LOCALS + for (locals.i = 0; locals.i < MAX_VAULTS; locals.i++) + { + Vault v = state.vaults.get(locals.i); + if (v.isActive) + { + locals.rr_in.vault = v; + resetReleaseRequests(qpi, state, locals.rr_in, locals.rr_out, locals.rr_locals); + v = locals.rr_out.vault; + state.vaults.set(locals.i, v); + } + } + _ + + struct END_EPOCH_locals + { + uint64 i; + }; + + END_EPOCH_WITH_LOCALS + for (locals.i = 0; locals.i < MAX_VAULTS; locals.i++) + { + Vault v = state.vaults.get(locals.i); + if (v.isActive) + { + if (v.balance >= (sint64)HOLDING_FEE) + { + // pay the holding fee + v.balance -= (sint64)HOLDING_FEE; + state.totalRevenue += HOLDING_FEE; + state.vaults.set(locals.i, v); + } + else + { + // ,ot enough funds to pay holding fee + v.isActive = false; + state.numberOfActiveVaults--; + state.vaults.set(locals.i, v); + } + } + } + + { + sint64 amountToDistribute = QPI::div(state.totalRevenue, (uint64)NUMBER_OF_COMPUTORS); + if (amountToDistribute > 0) + { + if (qpi.distributeDividends(amountToDistribute)) + { + state.totalDistributedToShareholders += amountToDistribute * NUMBER_OF_COMPUTORS; + } + } + } + _ + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES + REGISTER_USER_PROCEDURE(registerVault, 1); + REGISTER_USER_PROCEDURE(deposit, 2); + REGISTER_USER_PROCEDURE(releaseTo, 3); + REGISTER_USER_PROCEDURE(resetRelease, 4); + REGISTER_USER_FUNCTION(getVaults, 5); + REGISTER_USER_FUNCTION(getReleaseStatus, 6); + REGISTER_USER_FUNCTION(getBalanceOf, 7); + REGISTER_USER_FUNCTION(getVaultName, 8); + REGISTER_USER_FUNCTION(getRevenueInfo, 9); + _ +}; diff --git a/test/contract_msvault.cpp b/test/contract_msvault.cpp new file mode 100644 index 00000000..e34ab955 --- /dev/null +++ b/test/contract_msvault.cpp @@ -0,0 +1,418 @@ +#define NO_UEFI + +#include "contract_testing.h" +#include "test_util.h" + +static constexpr uint64 MSVAULT_REGISTERING_FEE = 10000ULL; +static constexpr uint64 MSVAULT_DEPOSIT_FEE = 0ULL; +static constexpr uint64 MSVAULT_RELEASE_FEE = 1000ULL; +static constexpr uint64 MSVAULT_RESET_FEE = 500ULL; + +static const id OWNER1 = ID(_T, _K, _U, _W, _W, _S, _N, _B, _A, _E, _G, _W, _J, _H, _Q, _J, _D, _F, _L, _G, _Q, _H, _J, _J, _C, _J, _B, _A, _X, _B, _S, _Q, _M, _Q, _A, _Z, _J, _J, _D, _Y, _X, _E, _P, _B, _V, _B, _B, _L, _I, _Q, _A, _N, _J, _T, _I, _D); +static const id OWNER2 = ID(_F, _X, _J, _F, _B, _T, _J, _M, _Y, _F, _J, _H, _P, _B, _X, _C, _D, _Q, _T, _L, _Y, _U, _K, _G, _M, _H, _B, _B, _Z, _A, _A, _F, _T, _I, _C, _W, _U, _K, _R, _B, _M, _E, _K, _Y, _N, _U, _P, _M, _R, _M, _B, _D, _N, _D, _R, _G); +static const id OWNER3 = ID(_K, _E, _F, _D, _Z, _T, _Y, _L, _F, _E, _R, _A, _H, _D, _V, _L, _N, _Q, _O, _R, _D, _H, _F, _Q, _I, _B, _S, _B, _Z, _C, _W, _S, _Z, _X, _Z, _F, _F, _A, _N, _O, _T, _F, _A, _H, _W, _M, _O, _V, _G, _T, _R, _Q, _J, _P, _X, _D); +static const id TEST_VAULT_NAME = ID(_M, _Y, _M, _S, _V, _A, _U, _L, _U, _S, _E, _D, _F, _O, _R, _U, _N, _I, _T, _T, _T, _E, _S, _T, _I, _N, _G, _P, _U, _R, _P, _O, _S, _E, _S, _O, _N, _L, _Y, _U, _N, _I, _T, _T, _E, _S, _C, _O, _R, _E, _S, _M, _A, _R, _T, _T); + + +class ContractTestingMsVault : protected ContractTesting +{ +public: + ContractTestingMsVault() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(MSVAULT); + callSystemProcedure(MSVAULT_CONTRACT_INDEX, INITIALIZE); + } + + void registerVault(uint16 vaultType, id vaultName, const std::vector& owners, uint64 fee) + { + MSVAULT::registerVault_input input; + for (uint16 i = 0; i < MAX_OWNERS; i++) + { + input.owners.set(i, (i < owners.size()) ? owners[i] : NULL_ID); + } + input.vaultType = vaultType; + input.vaultName = vaultName; + MSVAULT::registerVault_output regOut; + invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 1, input, regOut, owners[0], fee); + } + + void deposit(uint64 vaultID, uint64 amount, const id& from) + { + MSVAULT::deposit_input input; + input.vaultID = vaultID; + increaseEnergy(from, amount); + MSVAULT::deposit_output depOut; + invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 2, input, depOut, from, amount); + } + + void releaseTo(uint64 vaultID, uint64 amount, const id& destination, const id& owner, uint64 fee = MSVAULT_RELEASE_FEE) + { + MSVAULT::releaseTo_input input; + input.vaultID = vaultID; + input.amount = amount; + input.destination = destination; + + increaseEnergy(owner, fee); + MSVAULT::releaseTo_output relOut; + invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 3, input, relOut, owner, fee); + } + + void resetRelease(uint64 vaultID, const id& owner, uint64 fee = MSVAULT_RESET_FEE) + { + MSVAULT::resetRelease_input input; + input.vaultID = vaultID; + + increaseEnergy(owner, fee); + MSVAULT::resetRelease_output rstOut; + invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 4, input, rstOut, owner, fee); + } + + MSVAULT::getVaultName_output getVaultName(uint64 vaultID) const + { + MSVAULT::getVaultName_input input; + MSVAULT::getVaultName_output output; + input.vaultID = vaultID; + callFunction(MSVAULT_CONTRACT_INDEX, 8, input, output); + return output; + } + + MSVAULT::getVaults_output getVaults(const id& pubKey) const + { + MSVAULT::getVaults_input input; + MSVAULT::getVaults_output output; + input.publicKey = pubKey; + callFunction(MSVAULT_CONTRACT_INDEX, 5, input, output); + return output; + } + + MSVAULT::getBalanceOf_output getBalanceOf(uint64 vaultID) const + { + MSVAULT::getBalanceOf_input input; + MSVAULT::getBalanceOf_output output; + input.vaultID = vaultID; + callFunction(MSVAULT_CONTRACT_INDEX, 7, input, output); + return output; + } + + MSVAULT::getReleaseStatus_output getReleaseStatus(uint64 vaultID) const + { + MSVAULT::getReleaseStatus_input input; + MSVAULT::getReleaseStatus_output output; + input.vaultID = vaultID; + callFunction(MSVAULT_CONTRACT_INDEX, 6, input, output); + return output; + } + + MSVAULT::getRevenueInfo_output getRevenueInfo() const + { + MSVAULT::getRevenueInfo_input input; + MSVAULT::getRevenueInfo_output output; + callFunction(MSVAULT_CONTRACT_INDEX, 9, input, output); + return output; + } + + // Helper: find newly created vault by difference + sint64 findNewVaultIdAfterRegister(const id& owner, uint16 prevCount) + { + auto vaultsAfter = getVaults(owner); + if (vaultsAfter.numberOfVaults == prevCount + 1) + { + return (sint64)vaultsAfter.vaultIDs.get(prevCount); + } + return -1; + } +}; + +TEST(ContractMsVault, RegisterVault_InsufficientFee) +{ + ContractTestingMsVault msVault; + + // Check how many vaults OWNER1 has initially + auto vaultsO1Before = msVault.getVaults(OWNER1); + + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); + // Attempt with insufficient fee + msVault.registerVault(VAULT_TYPE_QUORUM, TEST_VAULT_NAME, { OWNER1, OWNER2 }, 5000ULL); + + // No new vault should be created + auto vaultsO1After = msVault.getVaults(OWNER1); + EXPECT_EQ(static_cast(vaultsO1After.numberOfVaults), + static_cast(vaultsO1Before.numberOfVaults)); +} + +TEST(ContractMsVault, RegisterVault_OneOwner) +{ + ContractTestingMsVault msVault; + auto vaultsO1Before = msVault.getVaults(OWNER1); + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); + // Only one owner + msVault.registerVault(VAULT_TYPE_QUORUM, TEST_VAULT_NAME, { OWNER1 }, MSVAULT_REGISTERING_FEE); + + // Should fail, no new vault + auto vaultsO1After = msVault.getVaults(OWNER1); + EXPECT_EQ(static_cast(vaultsO1After.numberOfVaults), + static_cast(vaultsO1Before.numberOfVaults)); +} + +TEST(ContractMsVault, RegisterVault_Success) +{ + ContractTestingMsVault msVault; + auto vaultsO1Before = msVault.getVaults(OWNER1); + + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); + msVault.registerVault(VAULT_TYPE_QUORUM, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + + auto vaultsO1After = msVault.getVaults(OWNER1); + EXPECT_EQ(static_cast((unsigned)vaultsO1After.numberOfVaults), + static_cast((unsigned)(vaultsO1Before.numberOfVaults + 1))); + + // Extract the new vaultID + uint64 vaultID = vaultsO1After.vaultIDs.get(vaultsO1Before.numberOfVaults); + // Check vault name + auto nameOut = msVault.getVaultName(vaultID); + EXPECT_EQ(nameOut.vaultName, TEST_VAULT_NAME); + + // Check revenue info + auto revenue = msVault.getRevenueInfo(); + // At least one vault active, revenue should have increased + EXPECT_GE(revenue.numberOfActiveVaults, 1U); + EXPECT_GE(revenue.totalRevenue, MSVAULT_REGISTERING_FEE); +} + +TEST(ContractMsVault, GetVaultName) +{ + ContractTestingMsVault msVault; + + auto vaultsO1Before = msVault.getVaults(OWNER1); + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); + msVault.registerVault(VAULT_TYPE_QUORUM, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + + auto vaultsO1After = msVault.getVaults(OWNER1); + EXPECT_EQ(static_cast(vaultsO1After.numberOfVaults), + static_cast(vaultsO1Before.numberOfVaults + 1)); + uint64 vaultID = vaultsO1After.vaultIDs.get(vaultsO1Before.numberOfVaults); + + auto nameOut = msVault.getVaultName(vaultID); + EXPECT_EQ(nameOut.vaultName, TEST_VAULT_NAME); + + auto invalidNameOut = msVault.getVaultName(9999ULL); + EXPECT_EQ(invalidNameOut.vaultName, NULL_ID); +} + +TEST(ContractMsVault, Deposit_InvalidVault) +{ + ContractTestingMsVault msVault; + // deposit to a non-existent vault + auto beforeBalance = msVault.getBalanceOf(999ULL); + msVault.deposit(999ULL, 5000ULL, OWNER1); + // no change in balance + auto afterBalance = msVault.getBalanceOf(999ULL); + EXPECT_EQ(afterBalance.balance, beforeBalance.balance); +} + +TEST(ContractMsVault, Deposit_Success) +{ + ContractTestingMsVault msVault; + + auto vaultsO1Before = msVault.getVaults(OWNER1); + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); + msVault.registerVault(VAULT_TYPE_QUORUM, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + + auto vaultsO1After = msVault.getVaults(OWNER1); + uint64 vaultID = vaultsO1After.vaultIDs.get(vaultsO1Before.numberOfVaults); + + auto balBefore = msVault.getBalanceOf(vaultID); + msVault.deposit(vaultID, 10000ULL, OWNER1); + auto balAfter = msVault.getBalanceOf(vaultID); + EXPECT_EQ(balAfter.balance, balBefore.balance + 10000ULL); +} + +TEST(ContractMsVault, ReleaseTo_NonOwner) +{ + ContractTestingMsVault msVault; + + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); + msVault.registerVault(VAULT_TYPE_TWO_OUT_OF_X, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + auto vaultsO1 = msVault.getVaults(OWNER1); + uint64 vaultID = vaultsO1.vaultIDs.get(vaultsO1.numberOfVaults - 1); + + msVault.deposit(vaultID, 10000ULL, OWNER1); + auto releaseStatusBefore = msVault.getReleaseStatus(vaultID); + + // Non-owner attempt release + msVault.releaseTo(vaultID, 5000ULL, OWNER3, OWNER3); + auto releaseStatusAfter = msVault.getReleaseStatus(vaultID); + + // No approvals should be set + EXPECT_EQ(releaseStatusAfter.amounts.get(0), releaseStatusBefore.amounts.get(0)); + EXPECT_EQ(releaseStatusAfter.destinations.get(0), releaseStatusBefore.destinations.get(0)); +} + +TEST(ContractMsVault, ReleaseTo_InvalidParams) +{ + ContractTestingMsVault msVault; + + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); + msVault.registerVault(VAULT_TYPE_TWO_OUT_OF_X, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + + auto vaultsO1 = msVault.getVaults(OWNER1); + uint64 vaultID = vaultsO1.vaultIDs.get(vaultsO1.numberOfVaults - 1); + + msVault.deposit(vaultID, 10000ULL, OWNER1); + + auto releaseStatusBefore = msVault.getReleaseStatus(vaultID); + // amount=0 + msVault.releaseTo(vaultID, 0ULL, OWNER2, OWNER1); + auto releaseStatusAfter1 = msVault.getReleaseStatus(vaultID); + EXPECT_EQ(releaseStatusAfter1.amounts.get(0), releaseStatusBefore.amounts.get(0)); + + // destination NULL_ID + msVault.releaseTo(vaultID, 5000ULL, NULL_ID, OWNER1); + auto releaseStatusAfter2 = msVault.getReleaseStatus(vaultID); + EXPECT_EQ(releaseStatusAfter2.amounts.get(0), releaseStatusBefore.amounts.get(0)); +} + +TEST(ContractMsVault, ReleaseTo_PartialApproval) +{ + ContractTestingMsVault msVault; + + increaseEnergy(OWNER1, 100000ULL); + increaseEnergy(OWNER3, 100000ULL); + msVault.registerVault(VAULT_TYPE_QUORUM, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + + auto vaultsO1 = msVault.getVaults(OWNER1); + uint64 vaultID = vaultsO1.vaultIDs.get(vaultsO1.numberOfVaults - 1); + + msVault.deposit(vaultID, 15000ULL, OWNER1); + msVault.releaseTo(vaultID, 5000ULL, OWNER3, OWNER1); + + auto status = msVault.getReleaseStatus(vaultID); + // Partial approval means just first owner sets the request + EXPECT_EQ(status.amounts.get(0), 5000ULL); + EXPECT_EQ(status.destinations.get(0), OWNER3); +} + +TEST(ContractMsVault, ReleaseTo_FullApproval) +{ + ContractTestingMsVault msVault; + + increaseEnergy(OWNER1, 100000ULL); + increaseEnergy(OWNER2, 100000ULL); + increaseEnergy(OWNER3, 100000ULL); + + msVault.registerVault(VAULT_TYPE_TWO_OUT_OF_X, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + + auto vaultsO1 = msVault.getVaults(OWNER1); + uint64 vaultID = vaultsO1.vaultIDs.get(vaultsO1.numberOfVaults - 1); + + msVault.deposit(vaultID, 10000ULL, OWNER1); + + // OWNER1 requests 5000 Qubics to OWNER3 + msVault.releaseTo(vaultID, 5000ULL, OWNER3, OWNER1); + // Not approved yet + + msVault.releaseTo(vaultID, 5000ULL, OWNER3, OWNER2); // second approval + + // After full approval, amount should be released + auto bal = msVault.getBalanceOf(vaultID); + EXPECT_EQ(bal.balance, 10000ULL - 5000ULL); +} + +TEST(ContractMsVault, ReleaseTo_InsufficientBalance) +{ + ContractTestingMsVault msVault; + + increaseEnergy(OWNER1, 100000ULL); + increaseEnergy(OWNER2, 100000ULL); + increaseEnergy(OWNER3, 100000ULL); + + msVault.registerVault(VAULT_TYPE_TWO_OUT_OF_X, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + + auto vaultsO1 = msVault.getVaults(OWNER1); + uint64 vaultID = vaultsO1.vaultIDs.get(vaultsO1.numberOfVaults - 1); + + msVault.deposit(vaultID, 10000ULL, OWNER1); + + auto balBefore = msVault.getBalanceOf(vaultID); + // Attempt to release more than balance + msVault.releaseTo(vaultID, 20000ULL, OWNER3, OWNER1); + + // Should fail, balance no change + auto balAfter = msVault.getBalanceOf(vaultID); + EXPECT_EQ(balAfter.balance, balBefore.balance); +} + +TEST(ContractMsVault, ResetRelease_NonOwner) +{ + ContractTestingMsVault msVault; + + increaseEnergy(OWNER1, 100000ULL); + increaseEnergy(OWNER2, 100000ULL); + increaseEnergy(OWNER3, 100000ULL); + + msVault.registerVault(VAULT_TYPE_QUORUM, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + + auto vaultsO1 = msVault.getVaults(OWNER1); + uint64 vaultID = vaultsO1.vaultIDs.get(vaultsO1.numberOfVaults - 1); + + msVault.deposit(vaultID, 5000ULL, OWNER1); + + msVault.releaseTo(vaultID, 2000ULL, OWNER2, OWNER1); + + auto statusBefore = msVault.getReleaseStatus(vaultID); + msVault.resetRelease(vaultID, OWNER3); // Non owner tries to reset + auto statusAfter = msVault.getReleaseStatus(vaultID); + + // No change in release requests + EXPECT_EQ(statusAfter.amounts.get(0), statusBefore.amounts.get(0)); + EXPECT_EQ(statusAfter.destinations.get(0), statusBefore.destinations.get(0)); +} + +TEST(ContractMsVault, ResetRelease_Success) +{ + ContractTestingMsVault msVault; + + increaseEnergy(OWNER1, 100000ULL); + increaseEnergy(OWNER2, 100000ULL); + increaseEnergy(OWNER3, 100000ULL); + + msVault.registerVault(VAULT_TYPE_QUORUM, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + + auto vaultsO1 = msVault.getVaults(OWNER1); + uint64 vaultID = vaultsO1.vaultIDs.get(vaultsO1.numberOfVaults - 1); + + msVault.deposit(vaultID, 5000ULL, OWNER1); + + // OWNER2 requests a releaseTo + msVault.releaseTo(vaultID, 2000ULL, OWNER1, OWNER2); + // Now reset by OWNER2 + msVault.resetRelease(vaultID, OWNER2); + + auto status = msVault.getReleaseStatus(vaultID); + // All cleared + for (uint16 i = 0; i < 3; i++) + { + EXPECT_EQ(status.amounts.get(i), 0ULL); + EXPECT_EQ(status.destinations.get(i), NULL_ID); + } +} + +TEST(ContractMsVault, GetVaults_Multiple) +{ + ContractTestingMsVault msVault; + + increaseEnergy(OWNER1, 100000ULL); + increaseEnergy(OWNER2, 100000ULL); + increaseEnergy(OWNER3, 100000ULL); + + auto vaultsForOwner2Before = msVault.getVaults(OWNER2); + + msVault.registerVault(VAULT_TYPE_QUORUM, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + msVault.registerVault(VAULT_TYPE_TWO_OUT_OF_X, TEST_VAULT_NAME, { OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + + auto vaultsForOwner2After = msVault.getVaults(OWNER2); + EXPECT_GE(static_cast((unsigned)vaultsForOwner2After.numberOfVaults), + static_cast((unsigned)(vaultsForOwner2Before.numberOfVaults + 2U))); +} diff --git a/test/test.vcxproj b/test/test.vcxproj index 04cfbced..2d2cf601 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -112,6 +112,7 @@ + diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index ab78a526..5574ba65 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -22,6 +22,7 @@ +