diff --git a/README.md b/README.md index f980c63..acc2dc3 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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. diff --git a/README_CANCEL_VAULT.md b/README_CANCEL_VAULT.md index 9bbf181..eaf443c 100644 --- a/README_CANCEL_VAULT.md +++ b/README_CANCEL_VAULT.md @@ -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. diff --git a/contract-interface.json b/contract-interface.json index 6450206..7bcdf59 100644 --- a/contract-interface.json +++ b/contract-interface.json @@ -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
" }, { "name": "success_destination", "type": "Address" }, @@ -53,6 +54,7 @@ { "name": "end_timestamp", "type": "u64" }, { "name": "milestone_hash", "type": "BytesN<32>" }, { "name": "verifier", "type": "Option" }, + { "name": "grace_period", "type": "u64" }, { "name": "success_destination", "type": "Address" }, { "name": "failure_destination", "type": "Address" } ], diff --git a/docs/GRACE_PERIOD.md b/docs/GRACE_PERIOD.md new file mode 100644 index 0000000..a7721eb --- /dev/null +++ b/docs/GRACE_PERIOD.md @@ -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`. diff --git a/src/doc.md b/src/doc.md index 4fb8ad2..995644d 100644 --- a/src/doc.md +++ b/src/doc.md @@ -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" @@ -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 @@ -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 @@ -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 | --- @@ -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", @@ -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; @@ -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) { @@ -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 | --- @@ -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). diff --git a/src/lib.rs b/src/lib.rs index 9893393..e4fc736 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,6 +64,8 @@ pub struct ProductivityVault { pub start_timestamp: u64, /// Ledger timestamp after which deadline-based release is allowed. pub end_timestamp: u64, + /// Additional seconds after `end_timestamp` before redirection is allowed. + pub grace_period: u64, /// Hash representing the milestone the creator commits to. pub milestone_hash: BytesN<32>, /// Optional designated verifier. When `Some(addr)`, only that address may call `validate_milestone`. @@ -129,6 +131,7 @@ impl DisciplrVault { /// # Validation Rules /// - `amount` must be positive; otherwise returns `Error::InvalidAmount`. /// - `start_timestamp` must be strictly less than `end_timestamp`; otherwise returns `Error::InvalidTimestamps`. + /// - `grace_period` must be no greater than `MAX_VAULT_DURATION`; otherwise returns `Error::DurationTooLong`. /// /// # Prerequisites /// Creator must have sufficient USDC balance and authorize the transaction. @@ -141,6 +144,7 @@ impl DisciplrVault { end_timestamp: u64, milestone_hash: BytesN<32>, verifier: Option, + grace_period: u64, success_destination: Address, failure_destination: Address, ) -> Result