Skip to main content
VaultCovenant is the treasury enforcement contract. It holds funds, enforces signer-gated spending, and applies optional period caps and recipient allowlists. Contract path: contracts/core/treasury/VaultCovenant.cash CashScript version: ^0.13.0

Constructor Parameters (Bytecode-Embedded)

All parameters are compiled into the P2SH32 bytecode at deployment. They are immutable.
ParameterTypeDescription
vaultIdbytes32Unique vault identifier. Non-zero required.
requiredApprovalsintGovernance threshold metadata. The current spend path itself is fixed to two distinct signer approvals.
signer1Hashbytes20hash160(pubkey) of signer 1
signer2Hashbytes20hash160(pubkey) of signer 2
signer3Hashbytes20hash160(pubkey) of signer 3
periodDurationintSeconds per spending period. 0 = no cap.
periodCapintMax satoshis per period. 0 = unlimited.
recipientCapintMax satoshis per single spend. 0 = unlimited.
allowlistEnabledint1 = enforce recipient allowlist
allowedAddr1bytes20Allowlisted recipient 1
allowedAddr2bytes20Allowlisted recipient 2
allowedAddr3bytes20Allowlisted recipient 3

Transaction Structure

All entrypoints spend vault state from tx.inputs[0].
FunctionRequired outputsNotes
unlockPeriodtx.outputs[0] updated vault UTXOAllows small fee delta (<= 2000 sats) from vault UTXO.
spendtx.outputs[0] recipient payout, tx.outputs[1] updated vault UTXOEnforces caps/allowlist on payout path.
pausetx.outputs[0] updated vault UTXOAny single registered signer.
resumetx.outputs[0] updated vault UTXOExactly 2 distinct registered signers in this version.
emergencyLocktx.outputs[0] updated vault UTXOExactly 3 signatures in fixed signer order.

NFT State (32 bytes)

[0]:    version           (uint8)
[1]:    status            (uint8) — 0=ACTIVE, 1=PAUSED, 2=EMERGENCY_LOCK, 3=MIGRATING
[2-4]:  rolesMask         (3 bytes)
[5-8]:  current_period_id (uint32)
[9-16]: spent_this_period (uint64, satoshis)
[17-24]: last_update_timestamp (uint64, unix seconds)
[25-31]: reserved

Functions

spend(sig sig1, pubkey pubkey1, sig sig2, pubkey pubkey2, bytes32 proposalId, bytes20 recipientHash, int payoutAmount, int newPeriodId, int newSpent)

Execute a spending proposal. Requires two distinct registered signers. Validates:
  • Both signers are in {signer1Hash, signer2Hash, signer3Hash} and are distinct
  • Both signatures are valid
  • status == ACTIVE
  • Period cap: newSpent <= periodCap (if periodCap > 0)
  • Recipient cap: payoutAmount <= recipientCap (if recipientCap > 0)
  • Allowlist: recipientHash is in {allowedAddr1, allowedAddr2, allowedAddr3} (if allowlistEnabled == 1)
  • Period rollover: if newPeriodId > currentPeriodId, checks that periodDuration has elapsed
  • proposalId != 0x00...00
Outputs:
  • outputs[0]: P2PKH to recipientHash, value = payoutAmount
  • outputs[1]: Updated vault UTXO with new NFT state
requiredApprovals is not directly used inside spend() in this contract version. FlowGuard currently ships a fixed three-signer vault with a two-signer spend path, single-signer pause/unlockPeriod path, and all-three emergency lock path.
The BCH 2026 VM upgrade expands what future covenant designs may be able to express, but it does not automatically widen this contract. Larger signer sets require a new VaultCovenant implementation and redeployment inside FlowGuard.

unlockPeriod(sig authSig, pubkey authPubkey, int newPeriodId, int newSpent)

Roll the accounting period forward without spending. Any single registered signer. Validates:
  • status == ACTIVE
  • newPeriodId > currentPeriodId
  • newSpent == 0
  • If periodDuration > 0, sufficient time has elapsed

pause(sig authSig, pubkey authPubkey)

Any single registered signer freezes the vault immediately. State change: statusPAUSED

resume(sig sig1, pubkey pubkey1, sig sig2, pubkey pubkey2)

Resume a paused vault. Requires exactly two distinct registered signers. State change: statusACTIVE

emergencyLock(sig sig1, pubkey pubkey1, sig sig2, pubkey pubkey2, sig sig3, pubkey pubkey3)

Full lockdown requiring all three signers in exact order: pubkey1 == signer1Hash, pubkey2 == signer2Hash, pubkey3 == signer3Hash. State change: statusEMERGENCY_LOCK
Emergency lock is irreversible in the current contract version. Funds are permanently frozen. Deploy a new vault for recovery.

Fee Allowance

The unlockPeriod() function allows the vault UTXO to pay BCH miner fees from its own value:
feeDelta = inputs[0].value - outputs[0].value
require(feeDelta >= 0 && feeDelta <= 2000)  // max 2000 satoshis fee
Other functions do not define an explicit on-chain fee-delta cap in this contract. Operationally, keep miner fees and value transitions constrained in transaction builder policy.