Soroban smart contracts for Disciplr: programmable time-locked USDC vaults on Stellar.
Single contract disciplr-vault with:
- Data model:
ProductivityVault(creator, amount, start/end timestamps, milestone hash, optional verifier, success/failure destinations, status). - Status:
Active,Completed,Failed,Cancelled. - Methods:
- ✅
create_vault(...)— create vault and transfer USDC from creator to contract (IMPLEMENTED) validate_milestone(vault_id)— verifier validates milestone (release logic TODO).release_funds(vault_id)— release to success destination (TODO).redirect_funds(vault_id)— redirect to failure destination (TODO).cancel_vault(vault_id)— cancel and return funds to creator; sets status toCancelled.get_vault_state(vault_id)— return vault state from storage.
- ✅
The create_vault function now includes full USDC token transfer functionality:
- Transfers specified USDC amount from creator to contract
- Validates all inputs (amount > 0, valid timestamps)
- Requires creator authorization
- Handles insufficient balance errors
- Test coverage: 100% of create_vault logic
- 8/8 tests passing
See USDC_INTEGRATION.md for detailed documentation.
For detailed contract documentation, see vesting.md.
The Disciplr Vault follows a transparent security model based on creator authorization and optional third-party verification. For a detailed analysis of the trust model, assumptions, and known limitations (including CEI pattern notes), please refer to the Security and Trust Model in the documentation.
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.
- 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
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 |
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 lifecycle state of the vault
}| 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 |
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,
) -> u32Parameters:
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 documentverifier: Optional verifier address (None = anyone can validate)success_destination: Address to receive funds on successfailure_destination: Address to receive funds on failure
Returns: u32 - Unique vault identifier
Requirements:
- Caller must authorize the transaction (
creator.require_auth()) end_timestampmust be greater thanstart_timestamp- USDC transfer must be approved by creator before calling
Emits: vault_created event
Allows the verifier (or authorized party) to validate milestone completion and release funds.
pub fn validate_milestone(env: Env, vault_id: u32) -> boolParameters:
vault_id: ID of the vault to validate
Returns: bool - True if validation successful
Requirements (TODO):
- Vault must exist and be in
Activestatus - Caller must be the designated verifier (if set)
- Current timestamp must be before
end_timestamp
Emits: milestone_validated event
Releases locked funds to the success destination (typically after validation).
pub fn release_funds(env: Env, vault_id: u32) -> boolParameters:
vault_id: ID of the vault to release funds from
Returns: bool - True if release successful
Requirements (TODO):
- Vault status must be
Active - Caller must be authorized (verifier or contract logic)
- Transfers USDC to
success_destination - Sets status to
Completed
Redirects funds to the failure destination when milestone is not completed by deadline.
pub fn redirect_funds(env: Env, vault_id: u32) -> boolParameters:
vault_id: ID of the vault to redirect funds from
Returns: bool - True if redirect successful
Requirements (TODO):
- Vault status must be
Active - Current timestamp must be past
end_timestamp - Transfers USDC to
failure_destination - Sets status to
Failed
Allows the creator to cancel the vault and retrieve locked funds.
pub fn cancel_vault(env: Env, vault_id: u32) -> boolParameters:
vault_id: ID of the vault to cancel
Returns: bool - True if cancellation successful
Requirements (TODO):
- Caller must be the vault creator
- Vault status must be
Active - Returns USDC to creator
- Sets status to
Cancelled
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> - Vault data if exists, None otherwise
Note: Current implementation returns None as placeholder. Full implementation will read from persistent storage.
Emitted when a new vault is created.
Topic: ("vault_created", vault_id)
Data: Full ProductivityVault struct
Emitted when a milestone is successfully validated.
Topic: ("milestone_validated", vault_id)
Data: () (empty tuple)
+--------------+
| CREATED |
| |
| create_vault |
+------+-------+
|
v
+--------------+
+---------| ACTIVE |---------+
| | | |
| +--------------+ |
| |
v v
+-----------------+ +---------------------+
| validate_ | | redirect_funds |
| milestone() | | (deadline passed) |
+--------+--------+ +----------+----------+
| |
v v
+-----------------+ +---------------------+
| COMPLETED | | FAILED |
| | | |
+-----------------+ +---------------------+
|
v
+-----------------+
| cancel_vault() |
+--------+--------+
|
v
+-----------------+
| CANCELLED |
| |
+-----------------+
-
Creator Authorization: The vault creator must authorize transactions via
require_auth(). This ensures only the creator can initiate vault creation or cancellation. -
Verifier Role: An optional verifier can be designated during vault creation. If set, only the verifier can validate milestones. If not set, anyone can validate (which may be useful for decentralized verification).
-
Destination Addresses: Once set, success and failure destinations cannot be modified. This prevents fund redirection attacks.
-
Start Timestamp: Vault funds are locked from
start_timestamp. Before this time, the vault exists but is not active. -
End Timestamp: After
end_timestamp:- If milestone is validated → funds release to success destination
- If not validated → funds redirect to failure destination
-
Timestamp Validation: All time-sensitive operations must check that the current block timestamp is within the valid window.
-
USDC Integration: The contract expects USDC (or similar token) to be transferred to the contract before vault creation. This requires:
- Creator approves token transfer
- Contract pulls tokens from creator (via
transfer_from)
-
Non-Custodial: The contract holds tokens in escrow but never has withdrawal authority beyond the defined destination addresses.
The following security features are not yet implemented:
- Storage Persistence: Vaults are not persisted between contract calls
- Token Transfer: Actual USDC transfer logic is not implemented
- Timestamp Validation: Methods don't validate timestamps
- Verifier Authorization: No check that caller is the designated verifier
- Reentrancy Protection: No guards against reentrancy attacks
- Access Control: Basic auth only; no complex role-based access
- Use Soroban Token Interface: Implement standard token operations for USDC
- Add Access Control: Implement
Ownablepattern for admin functions - Circuit Breaker: Add emergency pause functionality
- Upgradeability: Consider proxy pattern for contract upgrades
- Comprehensive Tests: Achieve 95%+ test coverage
- External Audits: Have security experts review before mainnet deployment
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 = 0The 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 CompletedAfter 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 FailedCreator 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 CancelledCheck 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
}
}- Rust (edition 2021)
- Soroban SDK (22.x) for Stellar smart contracts
- Build target: WASM (cdylib)
- Rust (stable)
- Stellar Soroban CLI (optional, for build/deploy)
- WASM target:
rustup target add wasm32-unknown-unknown
# From repo root
cd disciplr-contracts
cargo buildWASM build (for deployment):
cargo build --target wasm32-unknown-unknown --releaseOutput: target/wasm32-unknown-unknown/release/disciplr_vault.wasm
cargo testExpected output:
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
disciplr-contracts/
├── src/
│ └── lib.rs # DisciplrVault contract + ProductivityVault type
├── Cargo.toml
├── README.md
└── USDC_INTEGRATION.md # USDC integration documentation
We welcome contributions from the community! Please follow this workflow to ensure a smooth collaboration.
Click the "Fork" button on the GitHub repository to create your own copy.
git clone https://github.com/YOUR_USERNAME/disciplr-contracts.git
cd disciplr-contractsgit remote add upstream https://github.com/your-org/disciplr-contracts.gitgit checkout -b feature/your-feature-name-
Keep your fork in sync: Regularly pull from upstream to stay current
git fetch upstream git checkout main git merge upstream/main
-
Make your changes: Implement your feature or fix
-
Write tests: Aim for 95%+ test coverage
# Add tests to src/lib.rs cargo test
-
Build and verify:
cargo build cargo build --target wasm32-unknown-unknown --release
- Follow standard Rust conventions
- Use meaningful variable and function names
- Add comments for complex logic
- Document public functions with doc comments
Use clear, descriptive commit messages:
type: short description
Detailed description of changes.
Fixes #issue-number
Types:
feat: New featurefix: Bug fixdocs: Documentationtest: Adding testsrefactor: Code restructuringchore: Maintenance tasks
Example:
feat: add milestone validation logic
- Implement validate_milestone() with verifier authorization
- Add timestamp checks to prevent late validations
- Emit milestone_validated event on success
Fixes #42
git push origin feature/your-feature-name- Navigate to your fork on GitHub
- Click "Compare & pull request"
- Fill in the PR template:
- Title: Clear description of the change
- Description: Explain what, why, and how
- Testing: Document test results
- Screenshots: If applicable
- All PRs require review before merging
- Address feedback promptly
- Keep PRs focused and atomic
Before submitting a PR:
-
Run all tests:
cargo test -
Build for release:
cargo build --target wasm32-unknown-unknown --release
-
Verify no warnings:
cargo clippy
- Aim for 95%+ test coverage
- Test all public functions
- Include edge cases
- Test error conditions
When contributing code:
- Never commit secrets: Don't include API keys, passwords, or private keys
- Validate inputs: Always validate user inputs
- Follow best practices:
- Use
require_auth()for authorization - Check for overflow/underflow
- Avoid reentrancy vulnerabilities
- Use
- Document security implications: Add security notes in PR description
Found a bug or have a feature request?
- Search existing issues to avoid duplicates
- Create a new issue with:
- Clear title
- Detailed description
- Steps to reproduce (for bugs)
- Expected vs actual behavior
- Be respectful and inclusive
- Welcome newcomers
- Provide constructive feedback
- Focus on what is best for the community
This directory is a separate git repo. To push to your own remote:
cd disciplr-contracts
git remote add origin <your-disciplr-contracts-repo-url>
git push -u origin main- Timestamp Validation: Milestone validation is strictly prohibited once the ledger timestamp reaches or exceeds
end_timestamp. This prevents "late" validations and ensures the time-lock is honored. - Authentication:
validate_milestonerequires authorization from the verifier (if specified) or the creator. This ensures only authorized parties can progress the vault state. - State Integrity: Operations like
validate_milestone,release_funds, andcancel_vaultcheck the currentstatus(must beActive) to prevent double-spending or invalid state transitions.
The project includes unit tests for core logic, specifically:
- Verification of milestone rejection after
end_timestamp. - Verification of successful milestone validation before
end_timestamp.
To run tests:
cargo testTo reduce abuse, spam, and potential overflow risk, strict bounds are enforced during vault creation.
The following constants were introduced:
pub const MAX_VAULT_DURATION: u64 = 365 * 24 * 60 * 60; // 1 year
pub const MIN_AMOUNT: i128 = 10_000_000; // 1 USDC (7 decimals)
pub const MAX_AMOUNT: i128 = 10_000_000_000_000; // 10 million USDC (7 decimals)During create_vault, the contract enforces:
amountmust be ≥MIN_AMOUNTamountmust be ≤MAX_AMOUNTstart_timestampmust not be in the pastend_timestampmust be strictly greater thanstart_timestampend_timestamp - start_timestampmust not exceedMAX_VAULT_DURATION
All validations occur before event emission or state mutation, ensuring invalid vaults cannot be created.
Boundary and over-limit cases are fully covered in the tests, including:
- The exact minimum and maximum amount values
- The amounts below minimum and above maximum
- The exact maximum duration
- Duration exceeding maximum
- Invalid timestamp ordering
- Past start timestamps