Skip to content
Closed
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ and the machine-readable interface lives in
| Entrypoint | Mutates state | Purpose |
| --- | --- | --- |
| `create_vault` | Yes | Creates and funds a new vault, assigns a sequential vault id, and starts it in `Active`. |
| `initialize` | Yes | Stores the emergency-pause admin exactly once. |
| `set_paused` | Yes | Lets the initialized admin pause or unpause mutating entrypoints. |
| `validate_milestone` | Yes | Marks an `Active` vault milestone as validated before the deadline. |
| `release_funds` | Yes | Sends funds to `success_destination` and moves the vault to `Completed`. |
| `redirect_funds` | Yes | Sends funds to `failure_destination` and moves the vault to `Failed`. |
Expand All @@ -40,6 +42,9 @@ and the backend mapping in [`src/doc.md`](src/doc.md#error-handling).
| `7` | `InvalidAmount` | `create_vault` | `amount` is below `MIN_AMOUNT` or above `MAX_AMOUNT`. This covers zero, negative, and over-maximum amounts. |
| `8` | `InvalidTimestamps` | `create_vault` | `end_timestamp` is less than or equal to `start_timestamp`. |
| `9` | `DurationTooLong` | `create_vault` | `end_timestamp - start_timestamp` exceeds `MAX_VAULT_DURATION` (365 days). |
| `10` | `ContractPaused` | Mutating vault entrypoints | The initialized admin has paused the contract. Read-only getters still work. |
| `11` | `AlreadyInitialized` | `initialize` | The admin has already been set. |
| `12` | `NotInitialized` | `set_paused` | Pause administration was called before `initialize`. |

## Vault Lifecycle

Expand Down Expand Up @@ -68,6 +73,8 @@ stateDiagram-v2

Any attempt to call `validate_milestone`, `release_funds`, `redirect_funds`, or
`cancel_vault` after a terminal transition returns `VaultNotActive` (`#3`).
When paused, mutating entrypoints return `ContractPaused` (`#10`) before their
normal state transitions; `get_vault_state` and `vault_count` remain available.

## Source Of Truth

Expand All @@ -77,3 +84,5 @@ Any attempt to call `validate_milestone`, `release_funds`, `redirect_funds`, or
for integrators and tooling.
- [`src/doc.md`](src/doc.md) maps these contract semantics to backend API
payloads and HTTP error responses.
- [`docs/ADMIN_PAUSE.md`](docs/ADMIN_PAUSE.md) documents the optional admin
emergency-pause model.
19 changes: 18 additions & 1 deletion contract-interface.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,28 @@
"InvalidStatus": 6,
"InvalidAmount": 7,
"InvalidTimestamps": 8,
"DurationTooLong": 9
"DurationTooLong": 9,
"ContractPaused": 10,
"AlreadyInitialized": 11,
"NotInitialized": 12
}
}
},
"functions": [
{
"name": "initialize",
"inputs": [
{ "name": "admin", "type": "Address" }
],
"outputs": { "type": "Result<bool, Error>" }
},
{
"name": "set_paused",
"inputs": [
{ "name": "paused", "type": "bool" }
],
"outputs": { "type": "Result<bool, Error>" }
},
{
"name": "create_vault",
"inputs": [
Expand Down
36 changes: 36 additions & 0 deletions docs/ADMIN_PAUSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Admin Emergency Pause

`DisciplrVault` supports an optional contract-level emergency pause for incident
response. Existing vault flows remain unpaused by default, but once an admin is
initialized that admin can halt mutating entrypoints while investigation or a fix
is prepared.

## Admin Setup

| Entrypoint | Authorization | Behavior |
| --- | --- | --- |
| `initialize(admin)` | `admin.require_auth()` | Stores the admin once and initializes `Paused = false`. |
| `set_paused(paused)` | Stored admin | Toggles the pause flag and emits `paused` or `unpaused`. |

Initialization is one-time. A second `initialize` call returns
`AlreadyInitialized`; calling `set_paused` before initialization returns
`NotInitialized`.

## Pause Scope

When paused, these mutating entrypoints return `ContractPaused` before auth,
storage mutation, or token transfer work:

- `create_vault`
- `validate_milestone`
- `release_funds`
- `redirect_funds`
- `cancel_vault`

Read-only entrypoints remain callable while paused:

- `get_vault_state`
- `vault_count`

This keeps operators able to inspect vault state during an incident while
preventing new vault creation and terminal fund movements.
30 changes: 20 additions & 10 deletions src/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ This guide provides comprehensive documentation for backend developers integrati

### Method to API Mapping

| Contract Method | HTTP Method | API Endpoint | Purpose |
|----------------|-------------|--------------|---------|
| `create_vault` | POST | `/api/v1/vaults` | Create new productivity vault |
| Contract Method | HTTP Method | API Endpoint | Purpose |
|----------------|-------------|--------------|---------|
| `initialize` | POST | `/api/v1/admin/initialize` | Set the emergency-pause admin once |
| `set_paused` | POST | `/api/v1/admin/pause` | Toggle emergency pause state |
| `create_vault` | POST | `/api/v1/vaults` | Create new productivity vault |
| `validate_milestone` | POST | `/api/v1/vaults/{vault_id}/validate` | Validate milestone completion |
| `release_funds` | POST | `/api/v1/vaults/{vault_id}/release` | Release funds to success destination |
| `redirect_funds` | POST | `/api/v1/vaults/{vault_id}/redirect` | Redirect funds to failure destination |
Expand Down Expand Up @@ -524,9 +526,12 @@ Those sections are kept aligned with `src/lib.rs` and `contract-interface.json`.
| `InvalidTimestamp` | 400 | INVALID_TIMESTAMP | No |
| `MilestoneExpired` | 400 | MILESTONE_EXPIRED | No |
| `InvalidStatus` | 400 | INVALID_STATUS | No |
| `InvalidAmount` | 400 | INVALID_AMOUNT | No |
| `InvalidTimestamps` | 400 | INVALID_TIMESTAMPS | No |
| `DurationTooLong` | 400 | DURATION_TOO_LONG | No |
| `InvalidAmount` | 400 | INVALID_AMOUNT | No |
| `InvalidTimestamps` | 400 | INVALID_TIMESTAMPS | No |
| `DurationTooLong` | 400 | DURATION_TOO_LONG | No |
| `ContractPaused` | 503 | CONTRACT_PAUSED | Yes |
| `AlreadyInitialized` | 409 | ALREADY_INITIALIZED | No |
| `NotInitialized` | 400 | NOT_INITIALIZED | No |

### Standard Error Response Format

Expand Down Expand Up @@ -584,13 +589,18 @@ All transactions benefit from Stellar's built-in replay protection via sequence

### 4. Authorization Matrix

| Operation | Authorized Caller | Notes |
|-----------|-------------------|-------|
| `create_vault` | Creator | Must sign and authorize USDC transfer |
| Operation | Authorized Caller | Notes |
|-----------|-------------------|-------|
| `initialize` | Admin address being configured | One-time setup; rejects repeat initialization |
| `set_paused` | Configured admin | Toggles mutating-entrypoint pause state |
| `create_vault` | Creator | Must sign and authorize USDC transfer |
| `validate_milestone` | Verifier (if set) or Creator | Must be before deadline |
| `release_funds` | Anyone | Conditions: validated OR past deadline |
| `redirect_funds` | Anyone | Conditions: not validated AND past deadline |
| `cancel_vault` | Creator only | Vault must be Active |
| `cancel_vault` | Creator only | Vault must be Active |

When paused, mutating vault operations return `ContractPaused`; `get_vault_state`
and `vault_count` remain readable for incident response.

---

Expand Down
59 changes: 59 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ pub enum Error {
InvalidTimestamps = 8,
/// Vault duration (end − start) exceeds MAX_VAULT_DURATION.
DurationTooLong = 9,
/// Contract is paused by the configured admin.
ContractPaused = 10,
/// Admin has already been initialized.
AlreadyInitialized = 11,
/// Admin has not been initialized.
NotInitialized = 12,
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -113,6 +119,8 @@ pub const MAX_AMOUNT: i128 = 10_000_000_000_000; // 10M USDC
pub enum DataKey {
Vault(u32),
VaultCount,
Admin,
Paused,
}

// ---------------------------------------------------------------------------
Expand All @@ -122,8 +130,54 @@ pub enum DataKey {
#[contract]
pub struct DisciplrVault;

fn require_not_paused(env: &Env) -> Result<(), Error> {
let paused = env
.storage()
.instance()
.get(&DataKey::Paused)
.unwrap_or(false);
if paused {
return Err(Error::ContractPaused);
}
Ok(())
}

#[contractimpl]
impl DisciplrVault {
/// Initialize the contract admin once.
///
/// Existing vault flows remain usable before initialization, but pause
/// administration requires this one-time setup. Re-initialization returns
/// `Error::AlreadyInitialized`.
pub fn initialize(env: Env, admin: Address) -> Result<bool, Error> {
if env.storage().instance().has(&DataKey::Admin) {
return Err(Error::AlreadyInitialized);
}
admin.require_auth();
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::Paused, &false);
Ok(true)
}

/// Set the contract emergency pause state.
///
/// Only the initialized admin may toggle this flag. While paused, mutating
/// vault entrypoints reject with `Error::ContractPaused`; read-only getters
/// remain available.
pub fn set_paused(env: Env, paused: bool) -> Result<bool, Error> {
let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.ok_or(Error::NotInitialized)?;
admin.require_auth();
env.storage().instance().set(&DataKey::Paused, &paused);
let event_name = if paused { "paused" } else { "unpaused" };
env.events()
.publish((Symbol::new(&env, event_name), admin), paused);
Ok(true)
}

/// Create a new productivity vault. Transfers USDC from creator to contract.
///
/// # Validation Rules
Expand All @@ -144,6 +198,7 @@ impl DisciplrVault {
success_destination: Address,
failure_destination: Address,
) -> Result<u32, Error> {
require_not_paused(&env)?;
creator.require_auth();

// Validate amount bounds
Expand Down Expand Up @@ -218,6 +273,7 @@ impl DisciplrVault {
/// this function. If `verifier` is `None`, only the creator may call it (no validation by
/// other parties). Rejects when current time >= end_timestamp (MilestoneExpired).
pub fn validate_milestone(env: Env, vault_id: u32) -> Result<bool, Error> {
require_not_paused(&env)?;
let vault_key = DataKey::Vault(vault_id);
let mut vault: ProductivityVault = env
.storage()
Expand Down Expand Up @@ -255,6 +311,7 @@ impl DisciplrVault {

/// Release vault funds to `success_destination`.
pub fn release_funds(env: Env, vault_id: u32, usdc_token: Address) -> Result<bool, Error> {
require_not_paused(&env)?;
let vault_key = DataKey::Vault(vault_id);
let mut vault: ProductivityVault = env
.storage()
Expand Down Expand Up @@ -300,6 +357,7 @@ impl DisciplrVault {

/// Redirect funds to `failure_destination` (e.g. after deadline without validation).
pub fn redirect_funds(env: Env, vault_id: u32, usdc_token: Address) -> Result<bool, Error> {
require_not_paused(&env)?;
let vault_key = DataKey::Vault(vault_id);
let mut vault: ProductivityVault = env
.storage()
Expand Down Expand Up @@ -343,6 +401,7 @@ impl DisciplrVault {

/// Cancel vault and return funds to creator.
pub fn cancel_vault(env: Env, vault_id: u32, usdc_token: Address) -> Result<bool, Error> {
require_not_paused(&env)?;
let vault_key = DataKey::Vault(vault_id);
let mut vault: ProductivityVault = env
.storage()
Expand Down
Loading
Loading