Skip to content
Open
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
1 change: 1 addition & 0 deletions backend/src/payments/soroban/escrow-contract.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export enum ContractError {
InvalidAmount = 3,
AlreadyExists = 4,
ArithmeticOverflow = 5,
InvalidParties = 22,
}

// ---------------------------------------------------------------------------
Expand Down
13 changes: 13 additions & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ backend maps that error to `NOT_FOUND`.
| `5` | `ArithmeticOverflow` | Checked arithmetic failed |
| `20` | `AmountExceedsEscrow` | A partial release would exceed the locked amount |
| `21` | `NothingToRefund` | Nothing remains to refund |
| `22` | `InvalidParties` | Payer and beneficiary cannot be the same address |

### Amount Units and Token Decimals

Expand Down Expand Up @@ -91,6 +92,18 @@ The backend derives escrow IDs as the **SHA-256 hash** of the `Payment` entity U
passed to the contract as `BytesN<32>`. See `escrow-submission.processor.ts` for the
exact derivation.

### Parties Validation and ID Uniqueness Rules

1. **Parties Validation**:
- `create` strictly forbids identical `payer` and `beneficiary` addresses.
- Calling `create` where `payer == beneficiary` is rejected with `Error::InvalidParties` (`#22`).

2. **Single-Use Escrow IDs & Storage Retention**:
- Escrow IDs (`BytesN<32>`) are single-use for the entire lifetime of the contract.
- Once an escrow is created, its entry in `persistent()` storage is permanently retained through all subsequent lifecycle states (`Locked` → `Released` / `Refunded` / `Completed`).
- Re-attempting `create` with an existing ID—even after full release or refund—is rejected with `Error::AlreadyExists` (`#4`).
- This prevents state corruption, replay attacks, and unauthorized re-creation.

## Arithmetic Safety (CT-73)

Amount-handling paths must use checked arithmetic (`checked_add` / `checked_sub`) rather than raw `+` / `-`. `revenue_distribution` already does this for its basis-point sum. `payment_escrow` stores a single amount per escrow and performs no arithmetic on it today; when partial releases or batch totals are added they must use the same checked pattern inline.
Expand Down
6 changes: 6 additions & 0 deletions contracts/payment_escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ pub enum Error {
AmountExceedsEscrow = 20,
/// Nothing remains to refund.
NothingToRefund = 21,
/// Payer and beneficiary cannot be the same address.
InvalidParties = 22,
}

#[contract]
Expand Down Expand Up @@ -184,6 +186,10 @@ impl PaymentEscrowContract {
return Err(Error::InvalidAmount);
}

if payer == beneficiary {
return Err(Error::InvalidParties);
}

if env
.storage()
.persistent()
Expand Down
25 changes: 25 additions & 0 deletions contracts/payment_escrow/tests/edge_cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,28 @@ fn unauthorized_refund_is_rejected() {
}]);
assert!(c.try_refund(&id).is_err());
}

#[test]
fn identical_payer_and_beneficiary_is_rejected() {
let f = fixture();
let c = client(&f);
let id = escrow_id(&f, 7);
assert_eq!(
c.try_create(&id, &f.payer, &f.payer, &100, &f.token),
Err(Ok(Error::InvalidParties))
);
}

#[test]
fn id_reuse_after_release_is_rejected() {
let f = fixture();
let c = client(&f);
let id = escrow_id(&f, 8);
c.create(&id, &f.payer, &f.beneficiary, &100, &f.token);
c.release(&id);
// Attempting to recreate an escrow with the same id must fail with AlreadyExists
assert_eq!(
c.try_create(&id, &f.payer, &f.beneficiary, &100, &f.token),
Err(Ok(Error::AlreadyExists))
);
}
2 changes: 2 additions & 0 deletions contracts/shared/error-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum ContractError {
InvalidAmount = 3,
Unauthorized = 4,
InvalidState = 5,
InvalidParties = 22,
}

export const CONTRACT_ERROR_NAMES: Readonly<Record<number, string>> =
Expand All @@ -20,6 +21,7 @@ export const CONTRACT_ERROR_NAMES: Readonly<Record<number, string>> =
[ContractError.InvalidAmount]: "InvalidAmount",
[ContractError.Unauthorized]: "Unauthorized",
[ContractError.InvalidState]: "InvalidState",
[ContractError.InvalidParties]: "InvalidParties",
});

/** Human-readable name for a contract error code, if known. */
Expand Down