A production-ready Soroban smart contract on the Stellar blockchain that locks XLM or any Stellar asset until a future timestamp is reached.
| Property | Value |
|---|---|
| Network | Stellar (Soroban) |
| Language | Rust |
| SDK | soroban-sdk v22 |
| Storage | Persistent (per-depositor) |
| Max deposit | 10^15 units (1 quadrillion) |
| Max lock duration | 5 years |
- A user calls
deposit(token, amount, unlock_time)β tokens transfer from their wallet into the contract. - The contract stores a
VaultEntryin Persistent Storage keyed by the depositor's address. - When the user calls
withdraw(), the contract checksenv.ledger().timestamp() >= unlock_time. - If the time has passed, tokens are returned. Otherwise the call fails with
FundsStillLocked. - An admin can perform emergency withdrawals (funds always return to the depositor, never to the admin).
- Admin rights can be transferred via a two-step process, or permanently renounced to make the vault fully trustless.
.
βββ Cargo.toml # Workspace manifest
βββ Makefile # Build / test / lint / deploy helpers
βββ rust-toolchain.toml # Pins stable Rust + wasm32 target
βββ .gitignore
βββ README.md
βββ .github/
β βββ workflows/
β βββ ci.yml # CI: lint β test β build WASM
βββ scripts/
β βββ deploy_testnet.sh # Automated testnet deploy + smoke test
βββ contracts/time-lock-vault/
βββ Cargo.toml
βββ src/
βββ lib.rs # Crate root & module declarations
βββ contract.rs # All public entry points
βββ types.rs # VaultKey, VaultEntry, protocol constants
βββ errors.rs # VaultError enum (8 typed codes)
βββ events.rs # Event emission helpers
βββ storage.rs # Persistent storage helpers + TTL bump logic
βββ test.rs # Full unit test suite (35+ tests)
Sets the admin address. Must be called once after deployment.
Locks amount of token until unlock_time (Unix seconds).
| Param | Type | Constraint |
|---|---|---|
depositor |
Address |
Must sign |
token |
Address |
SEP-41 token contract |
amount |
i128 |
0 < amount β€ 10^15 |
unlock_time |
u64 |
now < unlock_time β€ now + 5 years |
Withdraws funds if now >= unlock_time. Fails with FundsStillLocked otherwise.
Admin-only. Returns funds to the depositor regardless of lock time. Funds always go to the depositor β never to the admin.
Step 1 of a two-step admin transfer. Nominates new_admin as pending admin.
Step 2. The pending admin accepts and becomes the active admin.
Cancels a pending admin transfer. Only the current admin can cancel.
Permanently removes admin privileges. After this call, emergency_withdraw and all admin functions are disabled forever. Makes the vault fully trustless.
Returns the current vault entry. Does not bump storage TTL (no extra fees).
Returns seconds until unlock. Returns 0 if unlocked or no deposit exists. Does not bump TTL.
Returns the current ledger timestamp.
Returns the current admin, or None if renounced.
Returns the pending admin during a transfer, or None.
Returns (MAX_DEPOSIT_AMOUNT, MAX_LOCK_DURATION_SECS) for client-side validation.
| Code | Name | Meaning |
|---|---|---|
| 1 | InvalidAmount |
Amount β€ 0 |
| 2 | UnlockTimeNotInFuture |
unlock_time β€ current ledger time |
| 3 | NoDepositFound |
No active deposit for this address |
| 4 | FundsStillLocked |
Lock period not yet expired |
| 5 | DepositAlreadyExists |
Must withdraw before re-depositing |
| 6 | LockDurationTooLong |
Lock period exceeds 5 years |
| 7 | Unauthorized |
Caller is not the admin |
| 8 | AmountTooLarge |
Amount exceeds 10^15 |
| Property | Implementation |
|---|---|
| Checks-Effects-Interactions | Storage cleared before token transfer on every withdrawal |
| Auth-first ordering | require_auth() is always the first statement in every mutating function |
| No re-entrancy surface | State removed before any external token call |
| Bounded inputs | Amount capped at 10^15; lock duration capped at 5 years |
| No admin fund theft | Emergency withdraw always sends to depositor, never to admin |
| Trustless mode | Admin can permanently renounce via renounce_admin() |
| Safe admin transfer | Two-step transfer prevents accidental key loss |
| TTL management | Persistent entries bumped to ~1 year on every write; view functions skip TTL bump |
| No testutils in production | features = ["testutils"] only in [dev-dependencies] |
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Add WASM target
rustup target add wasm32-unknown-unknown
# Install Soroban CLI
cargo install --locked soroban-climake buildmake testmake checkmake optimizemake check-wasm-sizeFails if the optimized WASM exceeds MAX_WASM_BYTES (default 65 536 bytes / 64 KB).
Override the threshold at the command line:
make check-wasm-size MAX_WASM_BYTES=81920 # 80 KBThe same threshold is enforced in CI via the Check WASM size step in .github/workflows/ci.yml.
To update the limit, change MAX_WASM_BYTES in both places (or only in ci.yml if you don't use the Makefile target locally).
export SOROBAN_SECRET_KEY=S...
make deploy-testnet- Savings accounts β Lock funds for a fixed period to enforce saving discipline.
- Token vesting β Team or investor tokens released on a schedule.
- HODL challenges β Commit to not selling until a future date.
- Escrow β Time-gated release of payment.
MIT