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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ 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` | `ConflictingAddresses` | `create_vault` | A configured verifier is also the success or failure payout destination. See [`docs/ADDRESS_CONSTRAINTS.md`](docs/ADDRESS_CONSTRAINTS.md). |

## Vault Lifecycle

Expand All @@ -60,7 +61,7 @@ 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; verifier/destination addresses satisfy the address matrix; 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` |
Expand Down
3 changes: 2 additions & 1 deletion contract-interface.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"InvalidStatus": 6,
"InvalidAmount": 7,
"InvalidTimestamps": 8,
"DurationTooLong": 9
"DurationTooLong": 9,
"ConflictingAddresses": 10
}
}
},
Expand Down
25 changes: 25 additions & 0 deletions docs/ADDRESS_CONSTRAINTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Address Constraints

`create_vault` accepts four participant addresses:

- `creator`: funds the vault and may cancel it while active.
- `verifier`: optional milestone validator.
- `success_destination`: receives funds when the vault succeeds.
- `failure_destination`: receives funds when the vault fails.

The verifier must not also be a payout destination. A verifier who can validate
their own payout creates a conflict of interest, so `create_vault` rejects that
configuration with `Error::ConflictingAddresses`.

## Matrix

| Configuration | Result | Rationale |
| --- | --- | --- |
| `verifier == success_destination` | Rejected | Verifier would be able to approve their own success payout. |
| `verifier == failure_destination` | Rejected | Verifier would have a payout interest in the failure path. |
| `verifier == creator` | Allowed | This is an explicit creator-validation mode and is covered by tests. |
| `verifier == None` and `success_destination == failure_destination` | Allowed | No third-party verifier conflict exists; both terminal paths pay the configured destination. |
| Distinct verifier, success, and failure addresses | Allowed | Standard third-party validation setup. |

These checks run before the USDC escrow transfer, so rejected configurations do
not move funds into the contract.
97 changes: 97 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ pub enum Error {
InvalidTimestamps = 8,
/// Vault duration (end − start) exceeds MAX_VAULT_DURATION.
DurationTooLong = 9,
/// Verifier cannot also be a success or failure payout destination.
ConflictingAddresses = 10,
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -129,6 +131,8 @@ 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`.
/// - `verifier`, when present, must differ from `success_destination` and `failure_destination`;
/// otherwise returns `Error::ConflictingAddresses`.
///
/// # Prerequisites
/// Creator must have sufficient USDC balance and authorize the transaction.
Expand All @@ -154,6 +158,12 @@ impl DisciplrVault {
return Err(Error::InvalidAmount);
}

if let Some(ref verifier_addr) = verifier {
if *verifier_addr == success_destination || *verifier_addr == failure_destination {
return Err(Error::ConflictingAddresses);
}
}

// Validate timestamps
let current_time = env.ledger().timestamp();
if start_timestamp < current_time {
Expand Down Expand Up @@ -646,6 +656,93 @@ mod tests {
);
}

#[test]
#[should_panic(expected = "Error(Contract, #10)")]
fn test_create_vault_verifier_equals_success_destination_rejected() {
let setup = TestSetup::new();
let client = setup.client();

setup.env.ledger().set_timestamp(setup.start_timestamp);
client.create_vault(
&setup.usdc_token,
&setup.creator,
&setup.amount,
&setup.start_timestamp,
&setup.end_timestamp,
&setup.milestone_hash(),
&Some(setup.success_dest.clone()),
&setup.success_dest,
&setup.failure_dest,
);
}

#[test]
#[should_panic(expected = "Error(Contract, #10)")]
fn test_create_vault_verifier_equals_failure_destination_rejected() {
let setup = TestSetup::new();
let client = setup.client();

setup.env.ledger().set_timestamp(setup.start_timestamp);
client.create_vault(
&setup.usdc_token,
&setup.creator,
&setup.amount,
&setup.start_timestamp,
&setup.end_timestamp,
&setup.milestone_hash(),
&Some(setup.failure_dest.clone()),
&setup.success_dest,
&setup.failure_dest,
);
}

#[test]
fn test_create_vault_verifier_same_as_creator_allowed() {
let setup = TestSetup::new();
let client = setup.client();

setup.env.ledger().set_timestamp(setup.start_timestamp);
let vault_id = client.create_vault(
&setup.usdc_token,
&setup.creator,
&setup.amount,
&setup.start_timestamp,
&setup.end_timestamp,
&setup.milestone_hash(),
&Some(setup.creator.clone()),
&setup.success_dest,
&setup.failure_dest,
);

let vault = client.get_vault_state(&vault_id).unwrap();
assert_eq!(vault.verifier, Some(setup.creator));
assert_eq!(vault.status, VaultStatus::Active);
}

#[test]
fn test_create_vault_no_verifier_destination_overlap_allowed() {
let setup = TestSetup::new();
let client = setup.client();

setup.env.ledger().set_timestamp(setup.start_timestamp);
let vault_id = client.create_vault(
&setup.usdc_token,
&setup.creator,
&setup.amount,
&setup.start_timestamp,
&setup.end_timestamp,
&setup.milestone_hash(),
&None,
&setup.success_dest,
&setup.success_dest,
);

let vault = client.get_vault_state(&vault_id).unwrap();
assert_eq!(vault.verifier, None);
assert_eq!(vault.success_destination, setup.success_dest);
assert_eq!(vault.failure_destination, setup.success_dest);
}

#[test]
fn test_validate_milestone_rejects_after_end() {
let setup = TestSetup::new();
Expand Down
1 change: 0 additions & 1 deletion tests/proptest_timestamps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,6 @@ fn edge_start_eq_now_succeeds() {
assert_eq!(vault.end_timestamp, end);
}


#[test]
fn edge_start_eq_end_rejected() {
let (env, client, usdc, usdc_asset) = setup();
Expand Down
Loading