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
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ and the machine-readable interface lives in
| --- | --- | --- |
| `create_vault` | Yes | Creates and funds a new vault, assigns a sequential vault id, and starts it in `Active`. |
| `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`. |
| `cancel_vault` | Yes | Returns funds to the creator and moves the vault to `Cancelled`. |
| `release_funds` | Yes | Sends the full remaining balance to `success_destination` and moves the vault to `Completed`. |
| `release_partial` | Yes | Sends a positive tranche to `success_destination` and keeps the vault `Active` until the remaining balance is zero. |
| `redirect_funds` | Yes | Sends the remaining balance to `failure_destination` and moves the vault to `Failed`. |
| `cancel_vault` | Yes | Returns the remaining balance to the creator and moves the vault to `Cancelled`. |
| `get_vault_state` | No | Reads a vault record, returning `None` for an unknown id. |
| `vault_count` | No | Returns the number of vault ids assigned so far. |

Expand All @@ -37,20 +38,24 @@ and the backend mapping in [`src/doc.md`](src/doc.md#error-handling).
| `4` | `InvalidTimestamp` | `create_vault`, `redirect_funds` | `create_vault` receives a `start_timestamp` earlier than the current ledger timestamp, or `redirect_funds` is called at or before `end_timestamp`. The exact deadline case returns `#4`. |
| `5` | `MilestoneExpired` | `validate_milestone` | The current ledger timestamp is greater than or equal to `end_timestamp`, so validation is no longer allowed. |
| `6` | `InvalidStatus` | None in the current implementation | Reserved by the enum for invalid-status flows. Current state-changing entrypoints use `VaultNotActive` for non-`Active` vault states. |
| `7` | `InvalidAmount` | `create_vault` | `amount` is below `MIN_AMOUNT` or above `MAX_AMOUNT`. This covers zero, negative, and over-maximum amounts. |
| `7` | `InvalidAmount` | `create_vault`, `release_partial` | `create_vault` amount is below `MIN_AMOUNT` or above `MAX_AMOUNT`, or a partial release amount is zero, negative, or greater than the vault's remaining balance. |
| `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). |

## Vault Lifecycle

Vault records are never deleted by normal contract operation. A vault starts in
`Active`; `Completed`, `Failed`, and `Cancelled` are terminal states.
`Active`; `Completed`, `Failed`, and `Cancelled` are terminal states. Each vault
stores its original `amount` and a mutable `remaining_amount` so partial success
releases cannot pay out more than the escrowed balance.

```mermaid
stateDiagram-v2
[*] --> Active: create_vault
Active --> Active: validate_milestone / milestone_validated = true
Active --> Active: release_partial / remaining_amount > 0
Active --> Completed: release_funds / success_destination receives funds
Active --> Completed: release_partial / remaining_amount = 0
Active --> Failed: redirect_funds / failure_destination receives funds
Active --> Cancelled: cancel_vault / creator receives refund
Completed --> [*]
Expand All @@ -62,13 +67,17 @@ stateDiagram-v2
| --- | --- | --- | --- | --- |
| None | `Active` | `create_vault` | Creator authorizes; amount and timestamps are valid; duration is within the maximum; token transfer into the contract succeeds. | `vault_created` |
| `Active` | `Active` | `validate_milestone` | Vault exists, is active, caller is the configured verifier or creator fallback, and ledger time is before `end_timestamp`. | `milestone_validated` |
| `Active` | `Completed` | `release_funds` | Creator authorizes; vault is active; milestone is validated or the deadline has been reached. | `funds_released` |
| `Active` | `Failed` | `redirect_funds` | Vault is active; ledger time is strictly greater than `end_timestamp`; milestone is not validated. | `funds_redirected` |
| `Active` | `Cancelled` | `cancel_vault` | Creator authorizes and vault is active. | `vault_cancelled` |
| `Active` | `Active` | `release_partial` | Creator authorizes; vault is active; milestone is validated or the deadline has been reached; release amount is positive and less than `remaining_amount`. | `funds_released`, `funds_released_partial` |
| `Active` | `Completed` | `release_funds` / final `release_partial` | Creator authorizes; vault is active; milestone is validated or the deadline has been reached; remaining balance becomes zero. | `funds_released`, `funds_released_partial` for partial API |
| `Active` | `Failed` | `redirect_funds` | Vault is active; ledger time is strictly greater than `end_timestamp`; milestone is not validated. Only `remaining_amount` is sent. | `funds_redirected` |
| `Active` | `Cancelled` | `cancel_vault` | Creator authorizes and vault is active. Only `remaining_amount` is refunded. | `vault_cancelled` |

Any attempt to call `validate_milestone`, `release_funds`, `redirect_funds`, or
`cancel_vault` after a terminal transition returns `VaultNotActive` (`#3`).

See [`docs/PARTIAL_RELEASE.md`](docs/PARTIAL_RELEASE.md) for tranche examples
and the remaining-balance invariants.

## Source Of Truth

- [`src/lib.rs`](src/lib.rs) defines the enum values, transition guards, events,
Expand Down
15 changes: 15 additions & 0 deletions contract-interface.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"fields": [
{ "name": "creator", "type": "Address" },
{ "name": "amount", "type": "i128" },
{ "name": "remaining_amount", "type": "i128" },
{ "name": "start_timestamp", "type": "u64" },
{ "name": "end_timestamp", "type": "u64" },
{ "name": "milestone_hash", "type": "BytesN<32>" },
Expand Down Expand Up @@ -73,6 +74,15 @@
],
"outputs": { "type": "Result<bool, Error>" }
},
{
"name": "release_partial",
"inputs": [
{ "name": "vault_id", "type": "u32" },
{ "name": "usdc_token", "type": "Address" },
{ "name": "release_amount", "type": "i128" }
],
"outputs": { "type": "Result<bool, Error>" }
},
{
"name": "redirect_funds",
"inputs": [
Expand Down Expand Up @@ -118,6 +128,11 @@
"topic": ["funds_released", "u32"],
"data": "i128"
},
{
"name": "funds_released_partial",
"topic": ["funds_released_partial", "u32"],
"data": "(i128, i128)"
},
{
"name": "funds_redirected",
"topic": ["funds_redirected", "u32"],
Expand Down
35 changes: 35 additions & 0 deletions docs/PARTIAL_RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Partial Release

Disciplr vaults support tranche-based success payouts through `release_partial`.
A vault still records the original escrowed `amount`, and it also tracks
`remaining_amount` as the source of truth for future payouts, redirects, or
cancellation refunds.

## Invariants

- `remaining_amount` starts equal to `amount`.
- Every partial release must be positive and no greater than `remaining_amount`.
- A partial release transfers only the requested tranche to `success_destination`.
- The vault stays `Active` while `remaining_amount > 0`.
- The vault becomes `Completed` when `remaining_amount == 0`.
- `release_funds` preserves the old full-release behavior by releasing the
current `remaining_amount` in one call.
- `redirect_funds` and `cancel_vault` transfer only `remaining_amount`, so prior
success tranches cannot be paid twice.

## Worked Example

A creator funds a vault with `30_000_000` stroops.

1. `remaining_amount = 30_000_000` after creation.
2. The verifier validates the milestone.
3. `release_partial(..., 10_000_000)` sends `10_000_000` to
`success_destination` and stores `remaining_amount = 20_000_000`.
4. The vault remains `Active`.
5. A final `release_funds(...)` sends the remaining `20_000_000` and marks the
vault `Completed`.

Deadline-based releases use the same authorization gate as `release_funds`. If
a creator releases a partial tranche after the deadline without milestone
validation, the unreleased remainder can still be redirected to
`failure_destination` because the milestone remains unvalidated.
84 changes: 61 additions & 23 deletions src/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ This guide provides comprehensive documentation for backend developers integrati

| Contract Method | HTTP Method | API Endpoint | Purpose |
|----------------|-------------|--------------|---------|
| `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 |
| `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 |
| `release_partial` | POST | `/api/v1/vaults/{vault_id}/release-partial` | Release a success tranche and keep tracking remaining escrow |
| `redirect_funds` | POST | `/api/v1/vaults/{vault_id}/redirect` | Redirect funds to failure destination |
| `cancel_vault` | POST | `/api/v1/vaults/{vault_id}/cancel` | Cancel vault and return funds |
| `get_vault_state` | GET | `/api/v1/vaults/{vault_id}` | Query vault state |
| `vault_count` | GET | `/api/v1/vaults/count` | Get total vault count |
Expand Down Expand Up @@ -174,9 +175,10 @@ This guide provides comprehensive documentation for backend developers integrati
```json
{
"vault_id": 42,
"status": "Completed",
"amount_released": "1000000000",
"destination": "GC7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"status": "Completed",
"amount_released": "1000000000",
"remaining_amount": "0",
"destination": "GC7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"transaction_hash": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3",
"ledger_sequence": 12345682,
"released_at": "2024-01-20T00:00:00Z"
Expand All @@ -189,11 +191,44 @@ This guide provides comprehensive documentation for backend developers integrati
|-------------|------------|-------------|
| 404 | `VaultNotFound` | Vault does not exist |
| 400 | `VaultNotActive` | Vault is not in Active status |
| 401 | `NotAuthorized` | Release conditions not met (not validated and before deadline) |

---

### 4. Redirect Funds
| 401 | `NotAuthorized` | Release conditions not met (not validated and before deadline) |

---

### 3a. Release Partial Funds

**Endpoint:** `POST /api/v1/vaults/{vault_id}/release-partial`

**Request Payload:**
```json
{
"vault_id": 42,
"usdc_token": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
"release_amount": "250000000",
"caller_signature": "signature_data_here"
}
```

Partial release uses the same validation-or-deadline gate as `release_funds`.
`release_amount` must be positive and no greater than `remaining_amount`.

**Response (200 OK):**
```json
{
"vault_id": 42,
"status": "Active",
"amount_released": "250000000",
"remaining_amount": "750000000",
"destination": "GC7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"transaction_hash": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3",
"ledger_sequence": 12345682,
"released_at": "2024-01-20T00:00:00Z"
}
```

---

### 4. Redirect Funds

**Endpoint:** `POST /api/v1/vaults/{vault_id}/redirect`

Expand Down Expand Up @@ -301,9 +336,10 @@ This guide provides comprehensive documentation for backend developers integrati
"vault_id": 42,
"exists": true,
"vault": {
"creator": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"amount": "1000000000",
"start_timestamp": 1704067200,
"creator": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"amount": "1000000000",
"remaining_amount": "1000000000",
"start_timestamp": 1704067200,
"end_timestamp": 1706640000,
"milestone_hash": "4d696c6573746f6e655f726571756972656d656e74735f68617368",
"verifier": "GB7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
Expand Down Expand Up @@ -493,10 +529,11 @@ class ReleaseService {
const result = await this.submitTransaction(tx);

return {
vault_id: request.vault_id,
status: 'Completed',
amount_released: vault.amount,
destination: vault.success_destination,
vault_id: request.vault_id,
status: 'Completed',
amount_released: vault.remaining_amount,
remaining_amount: '0',
destination: vault.success_destination,
transaction_hash: result.txHash,
ledger_sequence: result.ledger,
released_at: new Date().toISOString()
Expand Down Expand Up @@ -613,10 +650,11 @@ class EventMonitor {
async subscribeToVaultEvents(vaultId: number): Promise<void> {
const filter = {
topics: [
['vault_created', vaultId.toString()],
['milestone_validated', vaultId.toString()],
['funds_released', vaultId.toString()],
['funds_redirected', vaultId.toString()],
['vault_created', vaultId.toString()],
['milestone_validated', vaultId.toString()],
['funds_released', vaultId.toString()],
['funds_released_partial', vaultId.toString()],
['funds_redirected', vaultId.toString()],
['vault_cancelled', vaultId.toString()]
]
};
Expand Down
Loading
Loading