diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 72ba285..7f3afef 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -160,6 +160,19 @@ cat test_snapshots/test/test_vault_creation_emits_event.1.json ## Security Testing +### Release Authorization + +```bash +cargo test test_release_funds_after_deadline_without_auth +cargo test test_release_funds_before_validation_without_auth_rejected +cargo test test_release_funds_after_validation_without_release_auth +``` + +These tests cover the `release_funds` authorization matrix documented in +[docs/RELEASE_AUTH.md](./docs/RELEASE_AUTH.md): release is permissionless after a +success condition is true, but an unvalidated pre-deadline vault cannot be +released. + ### Double-Spending Prevention ```bash diff --git a/docs/RELEASE_AUTH.md b/docs/RELEASE_AUTH.md new file mode 100644 index 0000000..3670ee5 --- /dev/null +++ b/docs/RELEASE_AUTH.md @@ -0,0 +1,26 @@ +# Release Funds Authorization + +`release_funds` is a permissionless trigger once the vault has met a success +condition. The caller cannot choose where funds go: every successful release +transfers the escrowed amount to the vault's pre-committed +`success_destination`. + +## Authorization Matrix + +| Vault condition | Who may call `release_funds` | Result | +| --- | --- | --- | +| Active, milestone not validated, before `end_timestamp` | No one | Rejects with `Error::NotAuthorized` | +| Active, milestone validated, before `end_timestamp` | Anyone | Transfers to `success_destination` | +| Active, at or after `end_timestamp` | Anyone | Transfers to `success_destination` | +| Completed, Failed, or Cancelled | No one | Rejects with `Error::VaultNotActive` | + +Validation authorization remains separate: + +- If `verifier` is `Some(address)`, only that verifier can call + `validate_milestone`. +- If `verifier` is `None`, only the creator can call `validate_milestone`. + +This avoids a liveness failure where a creator disappears after funding the +vault. Once the milestone has been validated or the deadline has arrived, any +account can finalize the successful path, but funds still only reach the +configured `success_destination`. diff --git a/src/lib.rs b/src/lib.rs index 9893393..79dd69d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,8 +68,9 @@ pub struct ProductivityVault { pub milestone_hash: BytesN<32>, /// Optional designated verifier. When `Some(addr)`, only that address may call `validate_milestone`. /// When `None`, only the creator may call `validate_milestone` (no third-party validation). - /// `release_funds` is consistent: after deadline, anyone can release; before deadline, only - /// after the designated validator (or creator when verifier is None) has validated. + /// `release_funds` is permissionless once a success condition is true: after the deadline, + /// anyone can trigger release; before the deadline, release is allowed only after the + /// designated validator (or creator when verifier is None) has validated. pub verifier: Option
, /// Funds go here on success. pub success_destination: Address, @@ -254,6 +255,14 @@ impl DisciplrVault { // ----------------------------------------------------------------------- /// Release vault funds to `success_destination`. + /// + /// Authorization matrix: + /// - Before `end_timestamp`: requires prior milestone validation by the configured verifier + /// or creator, but the release call itself is permissionless. + /// - At or after `end_timestamp`: anyone can trigger release. + /// + /// This is safe because the caller cannot choose the recipient; funds always move to the + /// pre-committed `success_destination`. pub fn release_funds(env: Env, vault_id: u32, usdc_token: Address) -> Result