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
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ and the backend mapping in [`src/doc.md`](src/doc.md#error-handling).
| `1` | `VaultNotFound` | `validate_milestone`, `release_funds`, `redirect_funds`, `cancel_vault` | No `ProductivityVault` exists for the supplied `vault_id`. `get_vault_state` returns `None` instead of raising this error. |
| `2` | `NotAuthorized` | `release_funds`, `redirect_funds` | `release_funds` is called before the vault is validated and before the deadline, or `redirect_funds` is called after the milestone was already validated. Address-level auth failures from `require_auth` surface as Soroban auth failures rather than this contract error. |
| `3` | `VaultNotActive` | `validate_milestone`, `release_funds`, `redirect_funds`, `cancel_vault` | The vault status is already terminal: `Completed`, `Failed`, or `Cancelled`. |
| `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`. |
| `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 + grace_period`. Checked grace-deadline overflow also 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. |
| `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). |
| `9` | `DurationTooLong` | `create_vault` | `end_timestamp - start_timestamp` exceeds `MAX_VAULT_DURATION` (365 days), or `grace_period` exceeds `MAX_VAULT_DURATION`. |

## Vault Lifecycle

Expand All @@ -60,10 +60,10 @@ stateDiagram-v2

| From | To | Entrypoint | Preconditions | Resulting event |
| --- | --- | --- | --- | --- |
| None | `Active` | `create_vault` | Creator authorizes; amount and timestamps are valid; duration is within the maximum; token transfer into the contract succeeds. | `vault_created` |
| None | `Active` | `create_vault` | Creator authorizes; amount and timestamps are valid; duration and grace period are 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` | `Failed` | `redirect_funds` | Vault is active; ledger time is strictly greater than `end_timestamp + grace_period`; milestone is not validated. | `funds_redirected` |
| `Active` | `Cancelled` | `cancel_vault` | Creator authorizes and vault is active. | `vault_cancelled` |

Any attempt to call `validate_milestone`, `release_funds`, `redirect_funds`, or
Expand All @@ -77,3 +77,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/GRACE_PERIOD.md`](docs/GRACE_PERIOD.md) documents redirect grace-period
timing and boundary behavior.
3 changes: 3 additions & 0 deletions README_CANCEL_VAULT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Behavior & rules
- Cancellation allowed only when `status == Active` (i.e., before validation/completion).
- Caller must be the `creator` (enforced via `Address::require_auth`).
- Refund is performed against a simulated per-vault balance stored in persistent storage under key `("vault_balance", vault_id)`.
- Cancellation is independent of redirect grace periods: an active vault can still be cancelled by
its creator before another terminal transition occurs. Once `redirect_funds` succeeds after
`end_timestamp + grace_period`, cancellation is rejected because the vault is `Failed`.

Security notes
- Real token transfer: the current implementation simulates token movement by updating contract-owned balance storage. For production, replace this with a real token transfer using the Soroban token client and ensure the contract has allowance/escrowed tokens before creating the vault.
Expand Down
2 changes: 2 additions & 0 deletions contract-interface.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
{ "name": "amount", "type": "i128" },
{ "name": "start_timestamp", "type": "u64" },
{ "name": "end_timestamp", "type": "u64" },
{ "name": "grace_period", "type": "u64" },
{ "name": "milestone_hash", "type": "BytesN<32>" },
{ "name": "verifier", "type": "Option<Address>" },
{ "name": "success_destination", "type": "Address" },
Expand Down Expand Up @@ -53,6 +54,7 @@
{ "name": "end_timestamp", "type": "u64" },
{ "name": "milestone_hash", "type": "BytesN<32>" },
{ "name": "verifier", "type": "Option<Address>" },
{ "name": "grace_period", "type": "u64" },
{ "name": "success_destination", "type": "Address" },
{ "name": "failure_destination", "type": "Address" }
],
Expand Down
30 changes: 30 additions & 0 deletions docs/GRACE_PERIOD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Redirect Grace Period

Vault creators can configure a `grace_period` in seconds when calling
`create_vault`. The value gives late-but-valid milestone completions a buffer
after `end_timestamp` before funds can be redirected to `failure_destination`.

## Rules

| Rule | Behavior |
| --- | --- |
| `grace_period == 0` | Preserves the original behavior: redirect succeeds only when `now > end_timestamp`. |
| `0 < grace_period <= MAX_VAULT_DURATION` | Redirect succeeds only when `now > end_timestamp + grace_period`. |
| `grace_period > MAX_VAULT_DURATION` | `create_vault` rejects with `DurationTooLong`. |
| `end_timestamp + grace_period` overflow | `redirect_funds` rejects with `InvalidTimestamp`. |
| Milestone already validated | `redirect_funds` rejects with `NotAuthorized`, even after the grace period. |

The redirect boundary is strict. At exactly `end_timestamp + grace_period`,
redirection is still too early. One second later, redirection is allowed if the
vault remains active and unvalidated.

## Example

```text
start_timestamp = 1_700_000_000
end_timestamp = 1_700_086_400
grace_period = 3_600
redirect_after = 1_700_090_000
```

`redirect_funds` rejects at `1_700_090_000` and succeeds at `1_700_090_001`.
68 changes: 39 additions & 29 deletions src/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ This guide provides comprehensive documentation for backend developers integrati
{
"usdc_token": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
"creator": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"amount": "1000000000",
"start_timestamp": 1704067200,
"end_timestamp": 1706640000,
"milestone_hash": "4d696c6573746f6e655f726571756972656d656e74735f68617368",
"amount": "1000000000",
"start_timestamp": 1704067200,
"end_timestamp": 1706640000,
"grace_period": 3600,
"milestone_hash": "4d696c6573746f6e655f726571756972656d656e74735f68617368",
"verifier": "GB7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"success_destination": "GC7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"failure_destination": "GD7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
Expand All @@ -60,19 +61,21 @@ This guide provides comprehensive documentation for backend developers integrati
|-------|------|----------|-------------|
| `usdc_token` | string | Yes | Contract address of USDC token (StrKey format) |
| `creator` | string | Yes | Creator's Stellar address (G-address) |
| `amount` | string | Yes | Amount in stroops (7 decimals: 1 USDC = 10,000,000 stroops) |
| `start_timestamp` | integer | Yes | Unix timestamp when vault becomes active |
| `end_timestamp` | integer | Yes | Unix timestamp deadline for milestone validation |
| `milestone_hash` | string | Yes | Hex-encoded SHA-256 hash of milestone document |
| `amount` | string | Yes | Amount in stroops (7 decimals: 1 USDC = 10,000,000 stroops) |
| `start_timestamp` | integer | Yes | Unix timestamp when vault becomes active |
| `end_timestamp` | integer | Yes | Unix timestamp deadline for milestone validation |
| `grace_period` | integer | Yes | Additional seconds after `end_timestamp` before redirect is allowed; use `0` for legacy behavior |
| `milestone_hash` | string | Yes | Hex-encoded SHA-256 hash of milestone document |
| `verifier` | string | Optional | Designated verifier address (null for creator-only validation) |
| `success_destination` | string | Yes | Address to receive funds on successful milestone |
| `failure_destination` | string | Yes | Address to receive funds on failure |

**Constraints:**
- `amount` must be between 1 USDC (10,000,000 stroops) and 10M USDC (10,000,000,000,000 stroops)
- `end_timestamp` must be greater than `start_timestamp`
- Vault duration cannot exceed 1 year (365 days)
- `start_timestamp` must not be in the past
- `end_timestamp` must be greater than `start_timestamp`
- Vault duration cannot exceed 1 year (365 days)
- `grace_period` cannot exceed 1 year (365 days)
- `start_timestamp` must not be in the past

**Response (201 Created):**
```json
Expand Down Expand Up @@ -215,9 +218,9 @@ This guide provides comprehensive documentation for backend developers integrati
| `caller_signature` | string | Yes | Signed transaction from caller |

**Constraints:**
- Vault must exist and be in `Active` status
- Current timestamp must be strictly greater than or equal to `end_timestamp`
- `milestone_validated` must be false
- Vault must exist and be in `Active` status
- Current timestamp must be strictly greater than `end_timestamp + grace_period`
- `milestone_validated` must be false

**Response (200 OK):**
```json
Expand All @@ -238,7 +241,7 @@ This guide provides comprehensive documentation for backend developers integrati
|-------------|------------|-------------|
| 404 | `VaultNotFound` | Vault does not exist |
| 400 | `VaultNotActive` | Vault is not in Active status |
| 400 | `InvalidTimestamp` | Current time < end_timestamp (deadline not reached) |
| 400 | `InvalidTimestamp` | Current time <= end_timestamp + grace_period, or grace deadline overflow |
| 401 | `NotAuthorized` | Milestone was validated - funds should be released, not redirected |

---
Expand Down Expand Up @@ -302,10 +305,11 @@ This guide provides comprehensive documentation for backend developers integrati
"exists": true,
"vault": {
"creator": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"amount": "1000000000",
"start_timestamp": 1704067200,
"end_timestamp": 1706640000,
"milestone_hash": "4d696c6573746f6e655f726571756972656d656e74735f68617368",
"amount": "1000000000",
"start_timestamp": 1704067200,
"end_timestamp": 1706640000,
"grace_period": 3600,
"milestone_hash": "4d696c6573746f6e655f726571756972656d656e74735f68617368",
"verifier": "GB7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"success_destination": "GC7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"failure_destination": "GD7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
Expand Down Expand Up @@ -350,10 +354,11 @@ This guide provides comprehensive documentation for backend developers integrati
interface CreateVaultRequest {
usdc_token: string;
creator: string;
amount: string;
start_timestamp: number;
end_timestamp: number;
milestone_hash: string;
amount: string;
start_timestamp: number;
end_timestamp: number;
grace_period: number;
milestone_hash: string;
verifier?: string;
success_destination: string;
failure_destination: string;
Expand Down Expand Up @@ -397,10 +402,15 @@ class VaultService {

const duration = request.end_timestamp - request.start_timestamp;
const maxDuration = 365 * 24 * 60 * 60; // 1 year
if (duration > maxDuration) {
throw new ValidationError('DurationTooLong',
'Vault duration cannot exceed 1 year');
}
if (duration > maxDuration) {
throw new ValidationError('DurationTooLong',
'Vault duration cannot exceed 1 year');
}

if (request.grace_period > maxDuration) {
throw new ValidationError('DurationTooLong',
'Grace period cannot exceed 1 year');
}

const now = Math.floor(Date.now() / 1000);
if (request.start_timestamp < now) {
Expand Down Expand Up @@ -589,7 +599,7 @@ All transactions benefit from Stellar's built-in replay protection via sequence
| `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 |
| `redirect_funds` | Anyone | Conditions: not validated AND past deadline plus grace period |
| `cancel_vault` | Creator only | Vault must be Active |

---
Expand Down Expand Up @@ -706,7 +716,7 @@ This is the single enforcement point. Every state-changing function (`release_fu
### `redirect_funds`

1. Loads the vault and calls `require_active`.
2. Checks `env.ledger().timestamp() > end_timestamp` — panics with `DeadlineNotReached` if too early.
2. Checks `env.ledger().timestamp() > end_timestamp + grace_period` with checked addition — returns `InvalidTimestamp` if too early or if the grace deadline overflows.
3. **Sets `status = Failed` and persists the vault** (Checks & Effects).
4. Performs the transfer to `failure_destination` (Interactions).

Expand Down
Loading
Loading