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
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ 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`. |
| `release_funds` | Yes | Sends net funds to `success_destination`, optionally routes a protocol fee, and moves the vault to `Completed`. |
| `redirect_funds` | Yes | Sends net funds to `failure_destination`, optionally routes a protocol fee, and moves the vault to `Failed`. |
| `cancel_vault` | Yes | Returns funds 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 @@ -40,6 +40,18 @@ 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` | `InvalidFee` | `create_vault`, settlement helpers | `fee_bps` exceeds `MAX_FEE_BPS` or checked fee math fails. |

## Protocol Fees

Vaults include a `ProtocolFeeConfig` at creation time. `fee_bps` is capped at
`MAX_FEE_BPS` (`1000`, or 10%) and `fee_recipient` receives the fee when
`release_funds` or `redirect_funds` settles the vault. The fee is rounded up in
the protocol's favor and deducted from the destination payout. `fee_bps == 0`
preserves the original behavior: the full vault amount goes to the success or
failure destination and no `fee_collected` event is emitted.

See [`docs/FEES.md`](docs/FEES.md) for the formula and worked examples.

## Vault Lifecycle

Expand All @@ -50,8 +62,8 @@ Vault records are never deleted by normal contract operation. A vault starts in
stateDiagram-v2
[*] --> Active: create_vault
Active --> Active: validate_milestone / milestone_validated = true
Active --> Completed: release_funds / success_destination receives funds
Active --> Failed: redirect_funds / failure_destination receives funds
Active --> Completed: release_funds / success_destination receives net funds
Active --> Failed: redirect_funds / failure_destination receives net funds
Active --> Cancelled: cancel_vault / creator receives refund
Completed --> [*]
Failed --> [*]
Expand All @@ -62,8 +74,8 @@ 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` | `Completed` | `release_funds` | Creator authorizes; vault is active; milestone is validated or the deadline has been reached. | `fee_collected` when non-zero, then `funds_released` |
| `Active` | `Failed` | `redirect_funds` | Vault is active; ledger time is strictly greater than `end_timestamp`; milestone is not validated. | `fee_collected` when non-zero, then `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 Down
20 changes: 18 additions & 2 deletions contract-interface.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,19 @@
{ "name": "verifier", "type": "Option<Address>" },
{ "name": "success_destination", "type": "Address" },
{ "name": "failure_destination", "type": "Address" },
{ "name": "fee_bps", "type": "u32" },
{ "name": "fee_recipient", "type": "Address" },
{ "name": "status", "type": "VaultStatus" },
{ "name": "milestone_validated", "type": "bool" }
]
},
"ProtocolFeeConfig": {
"type": "struct",
"fields": [
{ "name": "fee_bps", "type": "u32" },
{ "name": "fee_recipient", "type": "Address" }
]
},
"Error": {
"type": "error",
"variants": {
Expand All @@ -38,7 +47,8 @@
"InvalidStatus": 6,
"InvalidAmount": 7,
"InvalidTimestamps": 8,
"DurationTooLong": 9
"DurationTooLong": 9,
"InvalidFee": 10
}
}
},
Expand All @@ -54,7 +64,8 @@
{ "name": "milestone_hash", "type": "BytesN<32>" },
{ "name": "verifier", "type": "Option<Address>" },
{ "name": "success_destination", "type": "Address" },
{ "name": "failure_destination", "type": "Address" }
{ "name": "failure_destination", "type": "Address" },
{ "name": "fee_config", "type": "ProtocolFeeConfig" }
],
"outputs": { "type": "Result<u32, Error>" }
},
Expand Down Expand Up @@ -123,6 +134,11 @@
"topic": ["funds_redirected", "u32"],
"data": "i128"
},
{
"name": "fee_collected",
"topic": ["fee_collected", "u32", "Address"],
"data": "i128"
},
{
"name": "vault_cancelled",
"topic": ["vault_cancelled", "u32"],
Expand Down
32 changes: 32 additions & 0 deletions docs/FEES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Protocol Fees

Each vault stores an optional protocol fee configuration at creation time:

- `fee_bps`: basis-point fee charged when `release_funds` or `redirect_funds` settles the vault.
- `fee_recipient`: address that receives the protocol fee.

`fee_bps` must be between `0` and `MAX_FEE_BPS` (`1000`, or 10%). Values above the cap return `Error::InvalidFee` before the creator's funds are transferred into escrow.

## Formula

The settlement fee is rounded up in the protocol's favor:

```text
fee = ceil(amount * fee_bps / 10_000)
net = amount - fee
```

When `fee_bps == 0`, the fee is `0` and settlement matches the original behavior exactly: the full vault amount goes to the success or failure destination and no `fee_collected` event is emitted.

## Example

For a vault with `amount = 1_000_000_000` stroops and `fee_bps = 250`:

```text
fee = ceil(1_000_000_000 * 250 / 10_000) = 25_000_000
net = 975_000_000
```

On success, `release_funds` sends `25_000_000` to `fee_recipient`, sends `975_000_000` to `success_destination`, emits `fee_collected`, and then emits `funds_released` with the net amount.

On failure, `redirect_funds` applies the same fee split but sends the net amount to `failure_destination`.
76 changes: 46 additions & 30 deletions src/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,15 @@ This guide provides comprehensive documentation for backend developers integrati
"amount": "1000000000",
"start_timestamp": 1704067200,
"end_timestamp": 1706640000,
"milestone_hash": "4d696c6573746f6e655f726571756972656d656e74735f68617368",
"verifier": "GB7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"success_destination": "GC7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"failure_destination": "GD7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
"milestone_hash": "4d696c6573746f6e655f726571756972656d656e74735f68617368",
"verifier": "GB7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"success_destination": "GC7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"failure_destination": "GD7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"fee_config": {
"fee_bps": 250,
"fee_recipient": "GE7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
}
```

**Field Descriptions:**
Expand All @@ -64,15 +68,18 @@ This guide provides comprehensive documentation for backend developers integrati
| `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 |
| `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 |
| `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 |
| `fee_config.fee_bps` | integer | Yes | Protocol fee in basis points, from 0 to 1000 |
| `fee_config.fee_recipient` | string | Yes | Address receiving protocol fees when fee_bps is non-zero |

**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
- Vault duration cannot exceed 1 year (365 days)
- `start_timestamp` must not be in the past
- `fee_config.fee_bps` must be between 0 and 1000 (inclusive)

**Response (201 Created):**
```json
Expand All @@ -91,9 +98,10 @@ This guide provides comprehensive documentation for backend developers integrati
|-------------|------------|-------------|
| 400 | `InvalidAmount` | Amount outside valid range |
| 400 | `InvalidTimestamps` | Invalid timestamp ordering |
| 400 | `InvalidTimestamp` | Start timestamp in the past |
| 400 | `DurationTooLong` | Vault duration exceeds 1 year |
| 401 | `NotAuthorized` | Creator signature invalid or missing |
| 400 | `InvalidTimestamp` | Start timestamp in the past |
| 400 | `DurationTooLong` | Vault duration exceeds 1 year |
| 400 | `InvalidFee` | Protocol fee exceeds MAX_FEE_BPS |
| 401 | `NotAuthorized` | Creator signature invalid or missing |
| 409 | `InsufficientBalance` | Creator has insufficient USDC balance |

---
Expand Down Expand Up @@ -306,12 +314,14 @@ This guide provides comprehensive documentation for backend developers integrati
"start_timestamp": 1704067200,
"end_timestamp": 1706640000,
"milestone_hash": "4d696c6573746f6e655f726571756972656d656e74735f68617368",
"verifier": "GB7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"success_destination": "GC7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"failure_destination": "GD7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"status": "Active",
"milestone_validated": false
}
"verifier": "GB7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"success_destination": "GC7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"failure_destination": "GD7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"fee_bps": 250,
"fee_recipient": "GE7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"status": "Active",
"milestone_validated": false
}
}
```

Expand Down Expand Up @@ -354,10 +364,14 @@ interface CreateVaultRequest {
start_timestamp: number;
end_timestamp: number;
milestone_hash: string;
verifier?: string;
success_destination: string;
failure_destination: string;
}
verifier?: string;
success_destination: string;
failure_destination: string;
fee_config: {
fee_bps: number;
fee_recipient: string;
};
}

class VaultService {
async createVault(request: CreateVaultRequest): Promise<VaultResponse> {
Expand Down Expand Up @@ -493,10 +507,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.amount_minus_fee,
protocol_fee: vault.protocol_fee,
destination: vault.success_destination,
transaction_hash: result.txHash,
ledger_sequence: result.ledger,
released_at: new Date().toISOString()
Expand Down Expand Up @@ -524,9 +539,10 @@ Those sections are kept aligned with `src/lib.rs` and `contract-interface.json`.
| `InvalidTimestamp` | 400 | INVALID_TIMESTAMP | No |
| `MilestoneExpired` | 400 | MILESTONE_EXPIRED | No |
| `InvalidStatus` | 400 | INVALID_STATUS | No |
| `InvalidAmount` | 400 | INVALID_AMOUNT | No |
| `InvalidTimestamps` | 400 | INVALID_TIMESTAMPS | No |
| `DurationTooLong` | 400 | DURATION_TOO_LONG | No |
| `InvalidAmount` | 400 | INVALID_AMOUNT | No |
| `InvalidTimestamps` | 400 | INVALID_TIMESTAMPS | No |
| `DurationTooLong` | 400 | DURATION_TOO_LONG | No |
| `InvalidFee` | 400 | INVALID_FEE | No |

### Standard Error Response Format

Expand Down
Loading
Loading