Skip to content

Latest commit

 

History

History
498 lines (372 loc) · 19.6 KB

File metadata and controls

498 lines (372 loc) · 19.6 KB

Disciplr Vault Contract Documentation

Overview

The Disciplr Vault is a Soroban smart contract deployed on the Stellar blockchain that enables programmable time-locked USDC vaults for productivity-based milestone funding. It allows creators to lock USDC tokens with specific milestones and conditions, ensuring funds are only released upon verified completion or redirected to a failure destination if milestones are not met.

Use Cases

  • Vesting schedules: Lock tokens that vest over time based on milestone completion
  • Grant funding: Enable grant providers to fund projects with accountability
  • Team incentives: Align team compensation with deliverable completion
  • Bug bounties: Create time-bound bounty programs with predefined payout conditions

Data Model

VaultStatus Enum

Represents the current state of a vault:

#[contracttype]
pub enum VaultStatus {
    Active = 0,      // Vault created and funds locked
    Completed = 1,  // Milestone validated, funds released to success destination
    Failed = 2,     // Milestone not completed by deadline, funds redirected
    Cancelled = 3,  // Vault cancelled by creator, funds returned
}
Status Description
Active Vault is live, waiting for milestone validation or deadline
Completed Milestone verified, funds released to success destination
Failed Deadline passed without validation, funds redirected
Cancelled Creator cancelled vault, funds returned

ProductivityVault Struct

The main data structure representing a vault:

#[contracttype]
pub struct ProductivityVault {
    pub creator: Address,           // Address that created the vault
    pub amount: i128,                // Amount of USDC locked (in stroops)
    pub start_timestamp: u64,       // Unix timestamp when vault becomes active
    pub end_timestamp: u64,          // Unix deadline for milestone validation
    pub milestone_hash: BytesN<32>, // SHA-256 hash of milestone requirements
    pub verifier: Option<Address>,  // Optional trusted verifier address
    pub success_destination: Address, // Address for fund release on success
    pub failure_destination: Address, // Address for fund redirect on failure
    pub status: VaultStatus,        // Current vault status
}
Field Type Description
creator Address Wallet address that created the vault
amount i128 Total USDC amount locked (in stroops, 1 USDC = 10^7 stroops)
start_timestamp u64 Unix timestamp (seconds) when vault becomes active
end_timestamp u64 Unix timestamp (seconds) deadline for milestone validation
milestone_hash BytesN<32> SHA-256 hash documenting milestone requirements
verifier Option<Address> Optional trusted party who can validate milestones
success_destination Address Recipient address on successful milestone completion
failure_destination Address Recipient address when milestone is not completed
status VaultStatus Current lifecycle state of the vault

Contract Methods

create_vault

Creates a new productivity vault and locks USDC funds.

pub fn create_vault(
    env: Env,
    creator: Address,
    amount: i128,
    start_timestamp: u64,
    end_timestamp: u64,
    milestone_hash: BytesN<32>,
    verifier: Option<Address>,
    success_destination: Address,
    failure_destination: Address,
) -> u32

Parameters:

  • creator: Address of the vault creator (must authorize transaction)
  • amount: USDC amount to lock (in stroops)
  • start_timestamp: When vault becomes active (unix seconds)
  • end_timestamp: Deadline for milestone validation (unix seconds)
  • milestone_hash: SHA-256 hash of milestone document
  • verifier: Optional verifier address (None = creator validates)
  • success_destination: Address to receive funds on success
  • failure_destination: Address to receive funds on failure

Returns: u32 - Unique vault identifier

Requirements:

  • Caller must authorize the transaction (creator.require_auth())
  • amount must be within [MIN_AMOUNT, MAX_AMOUNT]; otherwise returns Error::InvalidAmount
  • start_timestamp must be strictly less than end_timestamp; otherwise returns Error::InvalidTimestamps
  • end_timestamp - start_timestamp must not exceed MAX_VAULT_DURATION; otherwise returns Error::DurationTooLong
  • success_destination must differ from failure_destination; otherwise returns Error::SameDestination (error code #10). Equal destinations make the success/failure outcome financially indistinguishable, removing the accountability incentive of the vault.
  • creator must differ from success_destination and failure_destination; otherwise returns Error::InvalidAddress (error code #11). A creator that is also a destination could trivially recover funds regardless of milestone outcome, defeating the vault's accountability mechanism.
  • verifier (when Some) must differ from creator; otherwise returns Error::InvalidAddress (error code #11). A verifier equal to the creator provides no independent validation.
  • USDC transfer must be approved by creator before calling

Emits: vault_created event


validate_milestone

Allows the verifier (or authorized party) to validate milestone completion and release funds.

pub fn validate_milestone(env: Env, vault_id: u32) -> bool

Parameters:

  • vault_id: ID of the vault to validate

Returns: bool - True if validation successful

Requirements:

  • Vault must exist and be in Active status
  • Caller must be the designated verifier (if set), or creator (if verifier is None)
  • Current timestamp must be before end_timestamp

Emits: milestone_validated event


release_funds

Releases locked funds to the success destination (after validation or deadline).

pub fn release_funds(env: Env, vault_id: u32, usdc_token: Address) -> bool

Parameters:

  • vault_id: ID of the vault to release funds from
  • usdc_token: Address of the USDC token contract

Returns: bool - True if release successful

Requirements:

  • Vault status must be Active
  • Milestone must be validated OR current time must be past end_timestamp
  • Transfers USDC to success_destination
  • Sets status to Completed

redirect_funds

Redirects funds to the failure destination when milestone is not completed by deadline.

pub fn redirect_funds(env: Env, vault_id: u32, usdc_token: Address) -> bool

Parameters:

  • vault_id: ID of the vault to redirect funds from
  • usdc_token: Address of the USDC token contract

Returns: bool - True if redirect successful

Requirements:

  • Vault status must be Active
  • Current timestamp must be past end_timestamp
  • Milestone must NOT have been validated
  • Transfers USDC to failure_destination
  • Sets status to Failed

cancel_vault

Allows the creator to cancel the vault and retrieve locked funds.

pub fn cancel_vault(env: Env, vault_id: u32, usdc_token: Address) -> bool

Parameters:

  • vault_id: ID of the vault to cancel
  • usdc_token: Address of the USDC token contract

Returns: bool - True if cancellation successful

Requirements:

  • Caller must be the vault creator
  • Vault status must be Active
  • Returns USDC to creator
  • Sets status to Cancelled

get_vault_state

Retrieves the current state of a vault.

pub fn get_vault_state(env: Env, vault_id: u32) -> Option<ProductivityVault>

Parameters:

  • vault_id: ID of the vault to query

Returns: Option<ProductivityVault> - Stored vault data when a record exists for that ID.

Behavior: Created vault records are not deleted during normal contract execution. Completed, failed, and cancelled vaults still return Some(ProductivityVault) with their terminal status. None therefore means the ID was never assigned (vault_id >= vault_count()) or storage was cleared outside the contract's normal lifecycle.


Events

vault_created

Emitted when a new vault is created.

Topic:

("vault_created", vault_id)

Data:

ProductivityVault {
    creator: Address,
    amount: i128,
    start_timestamp: u64,
    end_timestamp: u64,
    milestone_hash: BytesN<32>,
    verifier: Option<Address>,
    success_destination: Address,
    failure_destination: Address,
    status: VaultStatus::Active,
}

milestone_validated

Emitted when a milestone is successfully validated.

Topic:

("milestone_validated", vault_id)

Data: () (empty tuple)


Lifecycle

                    ┌──────────────┐
                    │   CREATED    │
                    │              │
                    │ create_vault │
                    └──────┬───────┘
                           │
                           ▼
                    ┌──────────────┐
         ┌─────────│    ACTIVE    │─────────┐
         │         │              │         │
         │         └──────────────┘         │
         │                                    │
         ▼                                    ▼
┌─────────────────┐              ┌─────────────────────┐
│ validate_       │              │  redirect_funds     │
│ milestone()     │              │  (deadline passed)  │
└────────┬────────┘              └──────────┬──────────┘
         │                                   │
         ▼                                   ▼
┌─────────────────┐              ┌─────────────────────┐
│   COMPLETED    │              │      FAILED        │
│                │              │                     │
└─────────────────┘              └─────────────────────┘

         │
         ▼
┌─────────────────┐
│ cancel_vault()  │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   CANCELLED    │
│                │
└─────────────────┘

Security and Trust Model

This section outlines the security properties, trust assumptions, and known limitations of the Disciplr Vault contract to assist auditors and users.

Trust Model

  1. Verifier Trust (Critical): When a verifier is designated (via Some(Address)), that address has absolute power to validate the milestone and cause funds to be released to the success_destination before the deadline. If the verifier is compromised or malicious, they can release funds prematurely or to a non-compliant recipient.
  2. Creator Authority: The creator is the only address authorized to create_vault or cancel_vault. They must authorize the initial USDC funding. If no verifier is set (None), only the creator can validate the milestone.
  3. No Administrative Overrides: There is no "admin" or "owner" role with the power to sweep funds or override the vault logic. Funds can only flow to the predefined success_destination, failure_destination, or back to the creator on cancellation.
  4. Immutable Destinations: Once a vault is created, the success_destination and failure_destination are immutable. This prevents redirection of funds after the vault is funded.

Security Assumptions

  1. Stellar Ledger Integrity: We assume the underlying Stellar blockchain and Soroban runtime correctly enforce authorization (require_auth) and maintain state integrity.
  2. Ledger Timestamp: The contract relies on env.ledger().timestamp() for all time-based logic (start/end windows). We assume ledger timestamps are reasonably accurate and monotonic as per Stellar network consensus.
  3. Token Contract Behavior: The contract interacts with a USDC token contract (standard Soroban token interface). We assume the token contract is honest and follows the expected transfer behavior.

Known Limitations & Security Notes

  1. Per-Call Token Address: The usdc_token address is passed as an argument to release/redirect functions rather than being pinned to the vault data at creation. This introduces a risk where a malicious caller could potentially pass a different token address (though they would still need the contract to hold a balance of that token).
  2. Checks-Effects-Interactions (CEI): In release_funds, redirect_funds, and cancel_vault, the USDC transfer is initiated before the internal status is updated to Completed, Failed, or Cancelled. While Soroban's atomicity safeguards against most reentrancy/partial-success risks, this is a deviation from the strict CEI pattern.
  3. Lack of Emergency Stops: There is currently no circuit breaker or emergency pause mechanism.
  4. Precision: All amounts are handled as i128 in stroops (7 decimals for USDC); users must ensure they provide correct decimal-adjusted amounts.
  5. Equal Destinations Rejected (Issue #124): success_destination and failure_destination must be different addresses. If they were equal, the outcome of the vault (success vs. failure) would be financially indistinguishable for the creator: funds would arrive at the same address regardless of whether the milestone was completed. This eliminates the accountability incentive that is the core purpose of the vault, and could be used to disguise a vault with no real consequence for non-completion. The contract enforces this at creation time with Error::SameDestination (code #10).
  6. Invalid Address Roles Rejected (Issue #125): The contract rejects configurations where address roles overlap in ways that defeat the vault's accountability mechanism. Specifically: (a) creator == success_destination — the creator would trivially recover funds on success regardless of milestone completion; (b) creator == failure_destination — the creator would recover funds on failure with no consequence for non-completion; (c) verifier == creator — the creator would be validating their own milestone, providing no independent oversight. All three cases return Error::InvalidAddress (code #11). Note: the Soroban SDK does not expose a way to detect the Stellar zero-address (GAAA...WHF) at the contract level in no_std environments; these role-overlap checks are the detectable "obviously invalid placeholder" validations available at contract level.

Recommendations for Production

  1. Use Soroban Token Interface: Implement standard token operations for USDC
  2. Add Access Control: Implement Ownable pattern for admin functions
  3. Circuit Breaker: Add emergency pause functionality
  4. Upgradeability: Consider proxy pattern for contract upgrades
  5. Comprehensive Tests: Achieve 95%+ test coverage
  6. External Audits: Have security experts review before mainnet deployment
  7. Multisig Verifiers: For high-value vaults, use a multisig address as the verifier

Usage Examples

Example 1: Create a Milestone-Based Funding Vault

A project owner wants to lock 1000 USDC for a bug bounty program with a 30-day deadline.

// Parameters
let creator: Address = Address::from_string("GA7..."); // Creator wallet
let amount: i128 = 1000 * 10_000_000; // 1000 USDC in stroops
let start_timestamp: u64 = 1704067200; // Jan 1, 2024 00:00:00 UTC
let end_timestamp: u64 = 1706640000;    // Jan 30, 2024 00:00:00 UTC (30 days)

// Hash of milestone requirements (off-chain document)
let milestone_hash: BytesN<32> = BytesN::from_array(&env, &[
    0x4d, 0x69, 0x6c, 0x65, 0x73, 0x74, 0x6f, 0x6e,
    0x65, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72,
    0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x5f, 0x68,
    0x61, 0x73, 0x68, 0x5f, 0x65, 0x78, 0x61, 0x6d
]);

let verifier: Option<Address> = Some(Address::from_string("GB7..."));
let success_destination: Address = Address::from_string("GC7..."); // Project wallet
let failure_destination: Address = Address::from_string("GD7..."); // Funder wallet

// Create vault
let vault_id = DisciplrVaultClient::new(&env, &contract_address)
    .create_vault(
        &creator,
        &amount,
        &start_timestamp,
        &end_timestamp,
        &milestone_hash,
        &verifier,
        &success_destination,
        &failure_destination,
    );
// vault_id = 0

Example 2: Validate Milestone and Release Funds

The verifier validates that milestone requirements were met and releases funds.

let verifier: Address = Address::from_string("GB7..."); // Designated verifier

let result = DisciplrVaultClient::new(&env, &contract_address)
    .with_source_account(&verifier)
    .validate_milestone(&vault_id);
// result = true

// Funds now transferred to success_destination
// Vault status changed to Completed

Example 3: Handle Deadline Without Validation

After the deadline passes without milestone validation, funds are redirected.

// Assume end_timestamp has passed and no validation occurred

let result = DisciplrVaultClient::new(&env, &contract_address)
    .redirect_funds(&vault_id);
// result = true

// Funds transferred to failure_destination
// Vault status changed to Failed

Example 4: Cancel Vault Before Deadline

Creator decides to cancel the vault before the deadline.

let creator: Address = Address::from_string("GA7..."); // Original creator

let result = DisciplrVaultClient::new(&env, &contract_address)
    .with_source_account(&creator)
    .cancel_vault(&vault_id);
// result = true

// Funds returned to creator
// Vault status changed to Cancelled

Example 5: Query Vault State

Check the current state of a vault.

let vault_state = DisciplrVaultClient::new(&env, &contract_address)
    .get_vault_state(&vault_id);

// Returns Some(ProductivityVault) or None
match vault_state {
    Some(vault) => {
        // Access vault fields
        let current_status = vault.status;
        let amount_locked = vault.amount;
    }
    None => {
        // Vault not found or not initialized
    }
}

Testing

Run the test suite to verify contract functionality:

cargo test

Expected output should include tests for:

  • Vault creation with valid parameters
  • Vault creation authorization
  • Event emission on vault creation
  • Milestone validation logic
  • Fund release and redirect logic
  • Vault cancellation
  • State retrieval
  • Equal destination rejection (Issue #124)
  • Invalid address role rejection (Issue #125)

File Structure

The repository layout changes as tests, documentation, workflows, and tooling evolve. See the live repository tree for the current, complete file structure.


Related Documentation


Changelog

Version Changes
0.1.0 Initial release with basic vault structure, stubbed implementations
0.2.0 Issue #124: reject equal success/failure destinations (SameDestination error)
0.3.0 Issue #125: reject invalid address role overlaps (InvalidAddress error)