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