-
Notifications
You must be signed in to change notification settings - Fork 49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Multi-sig Vault SC #237
base: develop
Are you sure you want to change the base?
Conversation
2e8198f
to
8fa006a
Compare
@philippwerner This PR is ready to be reviewed. Thank you. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your contribution. I had a quick lock through some of your code and added a view comments. Please apply these to all similar cases in the contract (all temporary vars should be in the locals struct, none on the function call stack.
In case you don't know it yet, please read: https://github.com/qubic/core/blob/main/doc/contracts.md
I will continue reviewing later.
src/contracts/MsVault.h
Outdated
|
||
struct MSVaultLogger | ||
{ | ||
unsigned int _contractIndex; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it make sense to not mix between built in types an qpi types? (i mean use consequently qpi tpyes)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
src/contracts/MsVault.h
Outdated
|
||
struct getReleaseStatus_input | ||
{ | ||
uint64 vaultID; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the term vaultID is used at several places.
internally it is the the index, one could think about name it also index to make it also clear for anyone which is using the SC.
if we keep id, i personally would also write vaultId
instead of vaultID
to keep it all in camal case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have renamed to vaultId
to keep it as camel case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice contract, thanks!
LOG_INFO(loggerMsg); | ||
return; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i would not let users to create a release request when the current balance is to low. therefore i would check this before and reject the request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now the function will check for balance and reject before setting the release request. It will still check the balance again before transfering the funds.
src/contracts/MsVault.h
Outdated
loggerMsg.amount = 0; | ||
loggerMsg.destination = NULL_ID; | ||
|
||
if (input.vaultID >= MAX_VAULTS) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the following check sections are repeated at least twice. maybe a helper method could unify them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have created a helper function isValidVaultId
and replace about 6 places that check for vaultId validity
src/contracts/MsVault.h
Outdated
PUBLIC_FUNCTION_WITH_LOCALS(getReleaseStatus) | ||
if (input.vaultID >= MAX_VAULTS) | ||
{ | ||
for (locals.i = 0; locals.i < MAX_OWNERS; locals.i++) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of setting all just to 0 you could also add a status to the output that let the user identify that the requested vault is invalid otherwise it could be a user error because he requested wrong vault index
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the same can apply to the other conditions where the user will not receive a valid result. e.g. inactive vault
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same could also apply to getBalanceOf, getVaultName
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done for all these functions getReleaseStatus
, getBalanceOf
, and getVaultName
src/contracts/MsVault.h
Outdated
BEGIN_EPOCH_WITH_LOCALS | ||
for (locals.i = 0; locals.i < MAX_VAULTS; locals.i++) | ||
{ | ||
Vault v = state.vaults.get(locals.i); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i guess also here: assign this to locals.vault @philippwerner ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
src/contracts/MsVault.h
Outdated
if (v.isActive) | ||
{ | ||
locals.rr_in.vault = v; | ||
resetReleaseRequests(qpi, state, locals.rr_in, locals.rr_out, locals.rr_locals); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why should the release request not being kept over epochs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@krypdkat what do you think?
after rethinking it makes more sense to keen the requests. from users perspective it is best practice that they can send release requests at any time. may be adding a timeout, but for simplicity i would just keep it active until manual reset or acceptance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@J0ET0M the main purpose was only trying to make a clean state at every epoch begin (easier debugging and calculation), nothing more.
So if the code and other logic are concrete, then reseting storage is not needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
understand. then let's make the sure the code is clean and we can have release requests stay over epochs. from a user perspective this is def. something which is needed. @icyblob
src/contracts/MsVault.h
Outdated
constexpr uint64 REGISTERING_FEE = 10000ULL; | ||
constexpr uint64 RELEASE_FEE = 1000ULL; | ||
constexpr uint64 RELEASE_RESET_FEE = 500ULL; | ||
constexpr uint64 HOLDING_FEE = 100ULL; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Minor] Should we allow getting those fees from client side (cli, front-end) ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added a function to get all the fees.
core side: 3e4a994
cli side: qubic/qubic-cli@a1424b2
@philippwerner @J0ET0M @cyber-pc Thank you for your reviewing. I have addressed all the comments. |
bcd0ca6
to
b8e2f6a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
have a few comments, overall lgtm
src/contracts/MsVault.h
Outdated
using namespace QPI; | ||
|
||
constexpr uint16 MSVAULT_MAX_OWNERS = 32; | ||
constexpr uint16 MSVAULT_INITIAL_MAX_VAULTS = 1024; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this too small, can we pump this number up?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have pump this number to 2048. Please let me know if you think this is still small.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the state size at 2048? Just need to make it close to 1GiB
src/contracts/MsVault.h
Outdated
PUBLIC_FUNCTION_WITH_LOCALS(getVaults) | ||
output.numberOfVaults = 0; | ||
locals.count = 0; | ||
for (locals.i = 0; locals.i < 1024; locals.i++) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1024
=> MSVAULT_INITIAL_MAX_VAULTS
and other places as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
else | ||
{ | ||
// Not enough funds to pay holding fee | ||
locals.v.isActive = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the SC takes everything left. eg: fee is 10 coins, vault has 7 coins => SC takes 7 coins and then destroy the vault. By destroy I mean: reset everything about that vault to zero and empty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
a398be5
to
7ef45e1
Compare
d087b1c
to
3949967
Compare
- Bump up max vaults to 2048 - Change max vault data type to uint64 and other related variables' types - Adjsut max owners to 16 - Add max co-owners to 8 and reject any vault registration if having any owners exceed that limit - If the vault can't afford holding fee, take all that left, and reset EVERYTHING to 0 - Adjust the dividend payment logic - Change 1 input type for registerVault to fix misalignment issue
Purpose
The MSVAULT (Multi-Signature Vault) smart contract is designed as a secure vault for Qubic that requires multiple parties to manage and approve fund releases. By enabling multiple owners per vault and enforcing specific consensus rules (such as 2/3 quorum or two-out-of-x approvals), MSVAULT will make sure that no single party can unilaterally withdraw funds. This empowers groups to hold and manage assets collectively with strong on-chain security guarantees.
Core Design Features
Multiple Ownership and management:
MSVAULT supports up to 32 owners per vault. Each owner can submit requests to release funds, and the contract enforces that only after the required number of owner approvals are given will the funds actually move. This prevents unauthorized access, ensuring a safer environment for pooled capital.
Two Vault Types for Flexibility:
MSVAULT offers two distinct types of multi-sig management:
Auditable and Transparent:
All actions—registration, deposits, and release requests—are recorded on-chain. Users can query states via public functions to see vault balances, vault ownership, and pending release requests. This transparency fosters trust and easy verification of vault activities.
Fees and Incentives:
The contract charges fees to create new vaults, release funds, and reset release requests. These fees discourage spam and generate revenue for the network, some of which may be distributed to shareholders or burned, contributing to network sustainability.
Automatic Maintenance:
At the end of each epoch, the contract charges a small holding fee. If a vault cannot afford this fee, it gets destroyed and removed from active storage. This ensures that inactive or underfunded vaults do not consume resources indefinitely. Freed slot can be reused for a future vault.
Core Functionalities
Procedures (Invoked by Transactions)
register(vaultType, vaultName, owners...):
REGISTERING_FEE
paid by the caller.uint64 vaultID
identifying the newly registered vault.deposit(vaultID):
releaseTo(vaultID, amount, destination):
amount
QUs to a givendestination
.vaultID
.destination
.RELEASE_FEE
to prevent spam.resetRelease(vaultID):
RELEASE_RESET_FEE
is charged.Automatic Epoch-End Maintenance
HOLDING_FEE
is taken. If a vault’s balance is insufficient:View Functions (No funds Required)
getVaults(publicKey):
publicKey
owns.getReleaseStatus(vaultID):
getBalanceOf(vaultID):
getVaultName(vaultID):
getRevenueInfo():
Security Considerations
Multi-Owner Approval:
The core security model relies on multiple owners approving the same release request. This reduces the risk of a single compromised owner stealing funds.
Fees to Prevent Spam:
Introducing
RELEASE_FEE
andRELEASE_RESET_FEE
ensures owners don’t spam release requests, maintaining stable contract performance and reducing denial-of-service risks.Epoch-Based Maintenance:
Regular
END_EPOCH
checks ensure dormant or depleted vaults cannot remain indefinitely. This prevents “storage bloating” and improves state sanity.Proper Input Validation:
The contract ignores invalid requests (e.g., zero amount or NULL destination) and ensures callers must be owners. This reduces exploitable edge cases.
How the Contract Works Step-by-Step
Vault Creation:
A user calls
register(...)
with multiple owners and aREGISTERING_FEE
. The contract assigns avaultID
and records the vault’s info. The vault now securely holds QUs.Deposits:
Owners or any external party can deposit funds by calling
deposit(vaultID)
and including QUs. The vault balance increases on-chain, controlled by the MSVAULT contract’s logic.Releasing Funds:
Suppose 5 owners (A, B, C, D, E) manage the vault. A tries
releaseTo(...)
withX
QUs todest1
. If the vault is type 1 (2/3 quorum), at least 4 owners must request the exact sameX
anddest1
. Upon reaching that threshold, funds are released immediately. For a type 2 vault, only 2 owners out of total owners are needed.Resetting Requests:
If an owner changes their mind, they call
resetRelease(vaultID)
, removing their part of a pending request. This action costs a small fee, ensuring it’s not abused.Epoch-End Maintenance:
Periodically, the contract deducts a
HOLDING_FEE
from each vault. If a vault cannot pay, it is closed and freed, ensuring the contract’s state remains manageable.CLI interface
qubic/qubic-cli#50
Frontend Integration Requirements
Dashboard:
getVaults(publicKey)
).Transaction Builder:
register(...)
that allow selecting vault type, specifying vault name, and adding multiple owners.amount
anddestination
.Real-Time Updates:
getReleaseStatus
,getBalanceOf
, andgetVaultName
to keep UI updated.Error Handling and Confirmation:
Security and Education:
Constants
MAX_OWNERS = 32
:Limits the maximum number of owners per vault, simplifying the state and ensuring efficient handling of approvals.
INITIAL_MAX_VAULTS = 1024
andMAX_VAULTS = INITIAL_MAX_VAULTS * X_MULTIPLIER
:Sets initial and scalable capacity for vault creation, allowing the contract to handle a growing number of vaults while maintaining performance.
REGISTERING_FEE = 10000ULL
:Required to create a vault, discouraging spam and ensuring only serious users allocate resources to occupy on-chain space.
RELEASE_FEE = 1000ULL
:Imposed when initiating a fund release request, preventing spam by making repetitive or frivolous attempts costly.
RELEASE_RESET_FEE = 500ULL
:Charged when an owner resets their release request, balancing flexibility with responsibility and preventing constant toggling of requests.
HOLDING_FEE = 100ULL
:Collected at the end of each epoch from each active vault. Insufficient funds result in vault deactivation, ensuring no stale vaults linger indefinitely.
Project link: https://github.com/orgs/qubic/projects/1/views/1?filterQuery=subproject%3ACore&pane=issue&itemId=88813217