Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# =============================================================
# Decentralized Time-Lock Vault — Environment Variables
# =============================================================
#
# Copy this file to `.env` and fill in your values.
# cp .env.example .env
#
# WARNING: Never commit the actual `.env` file to version control.
# It is already listed in `.gitignore`.
# =============================================================

# REQUIRED: Stellar testnet secret key used by the deploy script.
# Generate one with: soroban keys generate --network testnet deployer
SOROBAN_SECRET_KEY=S...

# OPTIONAL: Override the default RPC URL for testnet deployment.
# Default: https://soroban-testnet.stellar.org
# SOROBAN_RPC_URL=https://soroban-testnet.stellar.org

# OPTIONAL: Override the default network passphrase.
# Default: Test SDF Network ; September 2015
# SOROBAN_NETWORK_PASSPHRASE=Test SDF Network ; September 2015

# OPTIONAL: WASM size threshold in bytes for the `check-wasm-size` Make target.
# Default: 65536 (64 KB)
# MAX_WASM_BYTES=65536
13 changes: 13 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ updates:
labels:
- dependencies

- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
labels:
- dependencies
- rust
groups:
soroban:
patterns:
- "soroban-sdk"
- "soroban-sdk-macros"

# Note: Dependabot does not support arbitrary binary downloads.
# The stellar-cli version (STELLAR_CLI_VERSION in .github/workflows/ci.yml)
# must be updated manually when a new release is published at:
Expand Down
42 changes: 41 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,46 @@ jobs:
- name: Run doc tests
run: cargo test --doc --features testutils

# ----------------------------------------------------------------
# Test (release mode — catches optimisation-related regressions)
# ----------------------------------------------------------------
test-release:
name: Unit Tests (release)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown

- name: Cache cargo registry
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock', 'rust-toolchain.toml') }}
restore-keys: ${{ runner.os }}-cargo-

- name: Run tests (release)
run: cargo test --release --features testutils

# ----------------------------------------------------------------
# Shell syntax validation
# ----------------------------------------------------------------
shellcheck:
name: Shell Syntax Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install shellcheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Check deploy_testnet.sh
run: shellcheck scripts/deploy_testnet.sh

# ----------------------------------------------------------------
# Unsafe Code Scanner (cargo-geiger)
# ----------------------------------------------------------------
Expand Down Expand Up @@ -161,7 +201,7 @@ jobs:
build:
name: Build WASM (${{ matrix.toolchain }})
runs-on: ubuntu-latest
needs: [lint, test, deny]
needs: [lint, test, test-release, deny]
strategy:
matrix:
toolchain: [stable, '1.81'] # MSRV
Expand Down
27 changes: 25 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `initialize(admin, fee_recipient, max_deposit, max_lock_secs)` sets the admin and fee recipient addresses; optionally overrides compile-time limits for `MAX_DEPOSIT_AMOUNT` and `MAX_LOCK_DURATION_SECS`; can only be called once

**Core**
- `deposit(depositor, token, amount, unlock_time, penalty_bps)` locks tokens until `unlock_time`; returns a per-depositor `deposit_id`
- `deposit_for(payer, depositor, token, amount, unlock_time, penalty_bps)` same as `deposit` but a third-party `payer` funds the vault on behalf of `depositor`
- `deposit(depositor, token, amount, unlock_time, penalty_bps)` locks tokens until `unlock_time` (Unix seconds derived from the ledger clock via `env.ledger().timestamp()`); returns a per-depositor `deposit_id`
- `deposit_for(payer, depositor, token, amount, unlock_time, penalty_bps)` same as `deposit` but a third-party `payer` funds the vault on behalf of `depositor`; the `payer` must sign the transaction and the depositor's address is stored as the beneficiary
- `withdraw(depositor, deposit_id)` returns the full locked amount to the depositor once `unlock_time` has passed
- `cancel_deposit(depositor, deposit_id)` early exit before unlock; applies `penalty_bps` penalty sent to `fee_recipient`, remainder returned to depositor

Expand All @@ -42,6 +42,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `get_depositors(offset, limit) -> Vec<Address>` paginated list of active depositor addresses
- `is_initialized() -> bool` whether `initialize` has been called

#### Ledger-Based Deposit Timing

Unlock times are validated and enforced using the **Soroban ledger clock** (`env.ledger().timestamp()`), not wall-clock time. Key implications:

- `unlock_time` must be supplied as a Unix timestamp (seconds since the Unix epoch).
- The contract reads `env.ledger().timestamp()` once per invocation and caches the value locally to avoid repeated host-function calls.
- A deposit is accepted only when `unlock_time > now` (strictly future).
- A `withdraw` succeeds only when `env.ledger().timestamp() >= unlock_time`.
- Ledger close times on Stellar advance roughly every 5–6 seconds. For short lock durations, callers should account for this granularity when choosing `unlock_time`.

Example — depositing with a 1-hour lock:
```
let now: u64 = env.ledger().timestamp(); // e.g. 1_700_000_000
let one_hour = 3_600_u64;
contract.deposit(&depositor, &token, &amount, &(now + one_hour), &0_u32);
```

The `deposit_for` function follows the same ledger-time semantics:
```
// payer funds the vault; depositor is the beneficiary
contract.deposit_for(&payer, &depositor, &token, &amount, &(now + one_hour), &0_u32);
```

#### Protocol Constants

- `MAX_DEPOSIT_AMOUNT` `1_000_000_000_000_000` (10^15 token base units)
Expand Down
60 changes: 60 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,63 @@ This convention applies to any repeated host accessor (`env.ledger().sequence()`
## Test Snapshots

Running `cargo test` may generate a `contracts/time-lock-vault/test_snapshots/` directory containing XDR snapshots of contract state produced by the Soroban test environment. These are transient build artefacts, not committed regression fixtures, and are listed in `.gitignore`. Do not commit them.

## Soroban-Specific Guidance

### How the Test Environment Works

Soroban unit tests use the `soroban-sdk` testutils feature to spin up an in-process simulated ledger. There is **no external node or network** required. The simulated environment provides a `MockHost` that emulates ledger storage, events, and auth.

Key points:
- Tests must run natively (without `--target wasm32-unknown-unknown`) so that testutils can compile.
- The `testutils` feature is in `[dev-dependencies]` only and is never compiled into the production WASM.
- Never run `cargo test --target wasm32-unknown-unknown` — it will fail because testutils are not available in the WASM target.

### Running Tests

```bash
# Run the full test suite
cargo test --features testutils

# Run a single test with stdout
cargo test test_deposit_success --features testutils -- --nocapture

# Run tests in release mode (catches optimisation-related edge cases)
cargo test --release --features testutils

# Run all tests with output
cargo test --features testutils -- --nocapture
```

### Writing New Tests

All tests live in `contracts/time-lock-vault/src/test.rs`. Follow these conventions:

1. Use `soroban_sdk::testutils::Ledger` to set the ledger timestamp before calling time-sensitive functions:

```rust
env.ledger().with_mut(|l| {
l.timestamp = 1_700_000_000;
});
```

2. Advance ledger time to simulate the passage of time:

```rust
env.ledger().with_mut(|l| {
l.timestamp += 3_600; // advance 1 hour
});
```

3. Assert on typed errors, not on panic messages:

```rust
let err = contract.withdraw(&depositor, &0).unwrap_err();
assert_eq!(err, VaultError::FundsStillLocked.into());
```

4. Every new contract function or behaviour change must be accompanied by at least one positive-path and one negative-path test.

### Soroban SDK Version

The workspace is pinned to `soroban-sdk = "22"` in `Cargo.toml`. Do not change the SDK version in a feature PR — version bumps require a dedicated chore PR with a full audit of breaking-change behaviour.
Loading