Skip to content

fix(ci): restore green content after conflict-merge corruption (all m… - #1

Open
Kingsley4867 wants to merge 441 commits into
Kingsley4867:feature/authorization-22-index-eventfrom
Talenttrust:main
Open

fix(ci): restore green content after conflict-merge corruption (all m…#1
Kingsley4867 wants to merge 441 commits into
Kingsley4867:feature/authorization-22-index-eventfrom
Talenttrust:main

Conversation

@Kingsley4867

Copy link
Copy Markdown
Owner

…erges preserved)

Close Talenttrust#981

syed-ghufran-hassan and others added 30 commits August 29, 2026 11:54
…ates (#1366)

Co-authored-by: kingsrichie79-cmyk <kingsrichie79-cmyk@users.noreply.github.com>
…arded matrix (#1367)

This commit implements centralized milestone status-transition validation to fix
Issue #1340, where different entrypoints enforced inconsistent transition rules,
risking state divergence.

CORE CHANGES:
- New module milestone_transitions.rs with validate_milestone_transition() as
  single source of truth for all milestone state changes
- MilestoneState enum (Pending, Released, Refunded) makes implicit states explicit
- Centralized transition matrix: Pending can go to Released/Refunded (both terminal)
- Atomic version + actor recording on every transition (optimistic concurrency control)
- Backward-compatible storage (version/actor default to 0/zero-address for existing milestones)

ENTRYPOINTS UPDATED:
- release_milestone: routes through validator before setting released=true
- refund_unreleased_milestones: two-pass validation+application with per-milestone versioning

UNCHANGED (PRESERVED):
- Fund transfer amounts and timing (escrow conservation verified)
- Authorization boundaries per entrypoint (each still enforces its own require_auth)
- Event semantics (same events emitted as before)
- Error discriminants (no renumbering; uses existing InvalidStatusTransition)
- Contract-level operations (raise_dispute, resolve_dispute, cancel, finalize
  operate orthogonally, no milestone state changes)

TEST COVERAGE:
- 30+ unit tests covering all transition matrix pairs, state conversions, versioning
- 20+ integration tests covering all 5 named edge cases:
  * Valid transitions
  * Idempotent same-status transitions
  * Backward transitions (correctly rejected)
  * Concurrent modifications (versioning prevents state corruption)
  * Invalid states (both flags set)
- Authorization boundary regression tests
- Error consistency validation

VERIFICATION:
- Syntax validated; full compilation requires Soroban toolchain
- All code follows existing Soroban/Rust conventions
- Storage compatible (no migration required)
- Escrow conservation verified (fund transfers completely unchanged)
- Error stability preserved (no discriminant changes)

Co-authored-by: Alu-card19 <clintoncodes68@gmail.com>
Co-authored-by: mikewheeleer <268014356+mikewheeleer@users.noreply.github.com>
…ion (#1362)

Issue #1357 — Explicit pause scope:
- Add PauseTarget enum (Payout, Dispute, Global) and PauseScope struct
- Add pause_with_scope entrypoint for scoped pauses
- Update raise_dispute/resolve_dispute to check PauseTarget::Dispute scope
- get_pause_scope view returns the active PauseScope if any
- Legacy pause() and Paused flag preserved for backward compatibility

Issue #1353 — Admin nonce rejection:
- Add AdminNonce DataKey and monotonic u64 counter
- consume_admin_nonce helper validates and increments atomically
- Nonce parameter added to pause, pause_with_scope, set_protocol_fee_bps
- get_admin_nonce view exposes the next expected nonce
- StaleNonce error (code 68) for replay rejection

Both features include comprehensive test coverage in test_pause_scope.rs.

Closes #1357, #1353

🤖 Generated with Codebuff

Co-authored-by: scarface-dev1 <scarface-dev1@users.noreply.github.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: mikewheeleer <268014356+mikewheeleer@users.noreply.github.com>
…changes (#1326) (#1337)

* feat(escrow): implement schema-versioned migration path for escrow storage (#1328)

* feat(escrow): implement bounded admin parameter setter with auth and events (#1327)

* feat(escrow): implement atomic milestone settlement reentrancy and double-spend guard (#1323)

* feat(escrow): implement typed error taxonomy with exhaustive negative tests (#1325)

* feat(escrow): implement bounded all-or-nothing batch release for milestones (#1322)

* feat(escrow): implement event emission on all milestone escrow state changes (#1326)

---------

Co-authored-by: mikewheeleer <268014356+mikewheeleer@users.noreply.github.com>
…-path tests (#1325) (#1336)

* feat(escrow): implement schema-versioned migration path for escrow storage (#1328)

* feat(escrow): implement bounded admin parameter setter with auth and events (#1327)

* feat(escrow): implement atomic milestone settlement reentrancy and double-spend guard (#1323)

* feat(escrow): implement typed error taxonomy with exhaustive negative tests (#1325)

---------

Co-authored-by: mikewheeleer <268014356+mikewheeleer@users.noreply.github.com>
…#1324) (#1335)

* feat(escrow): implement schema-versioned migration path for escrow storage (#1328)

* feat(escrow): implement bounded admin parameter setter with auth and events (#1327)

* feat(escrow): implement atomic milestone settlement reentrancy and double-spend guard (#1323)

* feat(escrow): implement typed error taxonomy with exhaustive negative tests (#1325)

* feat(escrow): implement bounded all-or-nothing batch release for milestones (#1322)

* feat(escrow): implement event emission on all milestone escrow state changes (#1326)

* feat(escrow): implement two-step admin transfer with timelock (#1321)

* feat(escrow): implement pause emergency-stop mechanism with admin guard and events (#1320)

* feat(escrow): centralize namespaced storage keys with TTL/bump policy (#1324)

---------

Co-authored-by: mikewheeleer <268014356+mikewheeleer@users.noreply.github.com>
Co-authored-by: mikewheeleer <268014356+mikewheeleer@users.noreply.github.com>
…stones (#1322) (#1333)

Co-authored-by: mikewheeleer <268014356+mikewheeleer@users.noreply.github.com>
…#1332)

* feat(escrow): implement schema-versioned migration path for escrow storage (#1328)

* feat(escrow): implement bounded admin parameter setter with auth and events (#1327)

* feat(escrow): implement atomic milestone settlement reentrancy and double-spend guard (#1323)

* feat(escrow): implement typed error taxonomy with exhaustive negative tests (#1325)

* feat(escrow): implement bounded all-or-nothing batch release for milestones (#1322)

* feat(escrow): implement event emission on all milestone escrow state changes (#1326)

* feat(escrow): implement two-step admin transfer with timelock (#1321)

---------

Co-authored-by: mikewheeleer <268014356+mikewheeleer@users.noreply.github.com>
…rd and events (#1320) (#1331)

* feat(escrow): implement schema-versioned migration path for escrow storage (#1328)

* feat(escrow): implement bounded admin parameter setter with auth and events (#1327)

* feat(escrow): implement atomic milestone settlement reentrancy and double-spend guard (#1323)

* feat(escrow): implement typed error taxonomy with exhaustive negative tests (#1325)

* feat(escrow): implement bounded all-or-nothing batch release for milestones (#1322)

* feat(escrow): implement event emission on all milestone escrow state changes (#1326)

* feat(escrow): implement two-step admin transfer with timelock (#1321)

* feat(escrow): implement pause emergency-stop mechanism with admin guard and events (#1320)

---------

Co-authored-by: mikewheeleer <268014356+mikewheeleer@users.noreply.github.com>
Renames the existing propose/accept/cancel governance-admin rotation
to propose_admin/accept_admin/cancel_admin/get_pending_admin so the
public API matches the issue's contract, removes the get_admin/
get_governance_admin duplicate, and closes two gaps: propose_admin now
rejects self-proposals (CannotProposeSelf), and accept_admin now
enforces an upper bound (ADMIN_ROTATION_PROPOSAL_TTL_LEDGERS, ~9 days)
so a forgotten or unaddressed proposal can't be accepted long after
the fact (AdminProposalExpired) instead of staying acceptable forever.

Wires up and repairs the admin rotation, admin-auth-helper, and
protocol-fee-event test modules, which existed on disk but were never
included in the test tree, and adds unit coverage for every edge case
in the issue (propose/accept happy path, timelock/expiry boundaries,
wrong-account rejection, cancel-then-reuse, events). Verified against
a pristine-main baseline: identical set of 174 pre-existing unrelated
test failures before and after, zero regressions, 32 net new passing
tests.

Updates docs/escrow/* (README, abi-reference, authorization,
authorization-storage, governance-security, ledger-time-source,
protocol-fees, quickstart, settlement-storage) to match the renamed
API and document the expiry rule; the doc-drift guard integration test
is updated to match.

Closes #1321

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: mikewheeleer <268014356+mikewheeleer@users.noreply.github.com>
…) (#1376)

Co-authored-by: Techman-devv <Techman-devv@users.noreply.github.com>
…#1375)

Summary
This PR implements deterministic batch size limits and all-or-nothing accounting for batch milestone releases in the TalentTrust escrow contract. The changes prevent partially paid batches by enforcing strict validation before any state mutations, ensuring atomic commits, and properly handling protocol fee calculations across batch operations.

Changes
Error Variants (types.rs)
Added EmptyBatch (error code 70) - rejects empty batch requests
Added BatchLimitExceeded (error code 71) - enforces MAX_BATCH_MILESTONES limit (10)
Added DuplicateMilestoneInBatch (error code 72) - detects duplicate milestone indices
Batch Release Logic (release.rs)
Fixed balance calculation: Available balance now accounts for accumulated protocol fees, preventing releases that would exceed escrow capacity
Two-pass validation: All milestones are validated (bounds, release status, refund status, authorization) before any state changes
Atomic fee calculation: Total protocol fees calculated upfront for the entire batch
Net amount tracking: released_amount now tracks net amounts (gross - protocol fee) for accurate accounting
Atomic fee accumulation: Protocol fees accumulated atomically after all milestone updates
Accounting invariant: Final check ensures released_amount + refunded_amount + accumulated_fees ≤ funded_amount
Test Coverage (test_batch_release.rs)
Added 12 comprehensive tests covering:

Empty batch rejection
Over-limit batch rejection (11+ items)
Maximum batch success (10 items)
Duplicate index detection
All-or-nothing atomicity (rollback on any invalid item)
Invalid item scenarios (refunded milestone, out-of-bounds index)
Successful batch release with contract completion
Protocol fee accounting verification
Insufficient funds rejection
Authorization boundary enforcement
Accounting invariant preservation across multiple batches
Partial batch releases
Security Considerations
Escrow conservation: The accounting invariant check ensures no funds can be double-counted or lost
Atomic commits: All state changes happen after validation, preventing partial state corruption
Fee isolation: Protocol fees are calculated and accumulated atomically, preventing fee leakage
Overflow protection: All arithmetic uses checked operations to prevent integer overflow
Authorization preservation: Existing authorization checks remain unchanged and are applied to each milestone
Test Evidence
All new tests validate the deterministic limits and all-or-nothing semantics:

Batches with any invalid item (already released, refunded, out-of-bounds, unauthorized) fail atomically with no state changes
Maximum batch size (10) is enforced and works correctly
Protocol fees are calculated correctly and accumulated atomically
Accounting invariants hold across multiple batch operations

Co-authored-by: Swayymalcolm99 <alexakpojosevbe40@gmail.com>
* Update fix_reputation.py

* Update amount_validation.rs
* Update pause_controls.rs

* Update reputation_storage.rs
)

Add 14 proptest-based property tests that exercise `is_milestone_overdue`
with bounded timestamps and durations, covering all edge cases specified
in #1359:

- Zero duration / zero deadline (now=0 and now=1)
- Maximum duration / u64 boundary (deadline=u64::MAX)
- Past deadline (now > deadline, now == deadline strict >)
- Monotonicity (before/at/after ordering)
- Ledger boundary (timestamp=0, small deadlines)
- Released milestone never overdue
- None deadline never overdue
- Out-of-bounds and unknown contracts
- Escrow conservation under deadline operations

Closes #1359

🤖 Generated with Codebuff

Co-authored-by: DammyAji <damilolaajibade121@gmail.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: mikewheeleer <268014356+mikewheeleer@users.noreply.github.com>
…1373)

Closes #1356

## Problem

Once a client or arbiter approves a milestone for release, the freelancer
could still overwrite `work_evidence` (the on-chain deliverable reference,
e.g. IPFS CID or URL hash) via `submit_work_evidence`. This breaks the
audit trail: approvals represent a claim that the submitted deliverable
was accepted, yet the deliverable reference itself remained mutable,
allowing a freelancer to retroactively change what was "approved."

## Solution

Lock deliverable metadata (`work_evidence`) after a milestone has pending
release approvals. The approval record in temporary storage (with TTL)
serves as the acceptance signal — once present, the evidence is frozen.

### Core changes

| File | Change |
|---|---|
| `types.rs` | Add `EvidenceLocked = 70` error variant |
| `lib.rs` — `submit_work_evidence` | Check `DataKey::MilestoneApprovals` in temporary storage; panic with `EvidenceLocked` if present |
| `milestones.rs` — `submit_work_evidence_impl` | Same guard in the internal implementation |

The lock is **per-milestone**: approving milestone 0 only freezes evidence
for milestone 0. Other milestones remain editable.

The lock respects **TTL expiry**: if the approval record expires (auto-
cleaned by Soroban temporary storage TTL), evidence becomes editable again,
which is the correct behavior — an expired approval means the release
window has lapsed.

### Behavior matrix

| State | `submit_work_evidence` result |
|---|---|
| No approval, milestone pending | ✅ Allowed (overwrite OK) |
| Any approval present | ❌ `EvidenceLocked` |
| Milestone already released | ❌ `MilestoneAlreadyReleased` |
| Milestone already refunded | ❌ `AlreadyRefunded` |
| Contract not `Funded` | ❌ `InvalidState` |

### Test coverage (5 new tests in `test/access_control.rs`)

| Test | Scenario |
|---|---|
| `submit_work_evidence_succeeds_before_approval` | Evidence can be submitted/overwritten before any approval exists |
| `submit_work_evidence_rejected_after_approval` | After client approval, evidence update is rejected with `EvidenceLocked`; original evidence unchanged |
| `submit_work_evidence_overwrite_allowed_before_approval` | Regression guard: overwrite still works pre-approval |
| `submit_work_evidence_rejected_after_multisig_approval` | MultiSig: locked once both client and freelancer approve |
| `submit_work_evidence_other_milestone_not_locked` | Approving milestone 0 only locks milestone 0; milestone 1 remains editable |

### Additional error variants

The following error variants are added to `types.rs` to support other
in-flight features (batch release, pause scope, migration, nonce):

- `EmptyBatch = 71`
- `BatchLimitExceeded = 72`
- `DuplicateMilestoneInBatch = 73`
- `PauseScopeActive = 74`
- `InvalidMigrationVersion = 75`
- `StaleNonce = 76`

### Ancillary fixes

These are pre-existing compilation errors fixed as part of this PR to
restore a green build:

- **Admin nonce parameter**: `pause()` and `set_protocol_fee_bps()` now
  require an `admin_nonce: u64` argument — updated all test call sites
  (14 files) to pass the correct nonce value.
- **Governance method renames**: `propose_governance_admin` →
  `propose_admin`, `accept_governance_admin` → `accept_admin`, etc. —
  updated `governance_test.rs`.
- **Milestone struct drift**: removed stale `progress` field and
  `MilestoneProgress::Pending` from `test_pause_scope.rs`.
- **Soroban `vec!` in no_std**: replaced `soroban_sdk::vec!` with a
  fixed-size array in `milestone_transitions_integration.rs` (not all
  types implement `TryFromVal`).
- **release.rs**: fixed missing `MilestoneState` construction before
  `validate_milestone_transition` call.
- **milestone_transitions.rs**: default actor falls back to
  `env.current_contract_address()` instead of the zero address; removed
  stale re-exports.

## Verification

- `cargo check` — clean ✅
- `cargo clippy` — clean (0 warnings) ✅
- All 5 new EvidenceLocked tests pass ✅
- All 26 `submit_work_evidence` access control tests pass ✅

Generated with Codebuff 🤖

Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: mikewheeleer <268014356+mikewheeleer@users.noreply.github.com>
#1382)

Implements a request → approve/reject → apply state machine that prevents
a single unreviewed request from executing a high-impact parameter override.

## What changed

### Core state machine (contracts/escrow/src/governance_proposal.rs)
Four new entrypoints on the Escrow contract:
- request_governance_proposal(kind) → proposal_id
  Admin-gated. Validates the payload immediately (same bounds as the live
  setter). Returns a monotonic ID and stores a Pending proposal.
- approve_governance_proposal(proposal_id, approver)
  Approver must differ from the requester (GovernanceSelfApproval guard).
  Moves proposal from Pending → Approved.
- reject_governance_proposal(proposal_id, approver)
  Same self-approval guard. Moves Pending → Rejected (terminal).
- apply_governance_proposal(proposal_id)
  Admin-gated. Requires Approved state. Materialises the parameter change,
  then sets Applied (idempotency: a second call fails with
  GovernanceProposalInvalidState).

Two read-only helpers:
- get_governance_proposal(id) → Option<GovernanceProposal>
- get_next_governance_proposal_id() → u64

### Types (contracts/escrow/src/types.rs)
- GovernanceProposalKind enum (SetProtocolFeeBps, SetGovernedParams,
  SetFeeWithdrawalCap, SetFeeWithdrawalCooldown, SetMaxMilestones)
- GovernanceProposalState enum (Pending / Approved / Rejected / Applied)
- GovernanceProposal struct with requester, approver, state, kind, expiry
- DataKey variants: GovernanceProposal(u64) and NextGovernanceProposalId
- Error codes: GovernanceProposalNotFound(77), GovernanceProposalInvalidState(78),
  GovernanceProposalExpired(79), GovernanceSelfApproval(80)

### TTL (contracts/escrow/src/ttl.rs)
- GOVERNANCE_PROPOSAL_TTL_LEDGERS = LEDGERS_PER_DAY * 3 (~3 days)
- GOVERNANCE_PROPOSAL_BUMP_THRESHOLD = LEDGERS_PER_DAY
- set_governance_proposal_ttl() helper

### Tests (contracts/escrow/src/test/governance_proposal.rs) — 26 tests
All 5 required edge cases from issue #1221:
1. request by operator — admin submits proposal; ID positive; Pending state
2. self-approval — requester cannot approve own proposal → GovernanceSelfApproval
3. expired request — any action past TTL → GovernanceProposalExpired
4. rejected request — Rejected terminal; approve/apply fail → GovernanceProposalInvalidState
5. apply twice — second apply → GovernanceProposalInvalidState

Plus: happy-path round-trips for all 3 proposal kinds, not-found guards,
expiry boundary (succeeds at boundary, fails one past), events for each
transition, multiple independent proposals, approver identity recorded.

## Security notes
- Self-approval is blocked at both approve and reject paths.
- Expiry is checked on every mutable action; the 3-day window limits the
  blast radius of a forgotten or compromised pending proposal.
- Rejection is terminal — a rejected proposal cannot be re-approved or
  applied; the admin must open a fresh proposal.
- Applied proposals cannot be re-applied (idempotency guard on state).
- Payload is validated at request time so invalid values never reach storage.

Closes #1221
* feat(governance): two-step approval workflow for high-impact overrides

Implements a request → approve/reject → apply state machine that prevents
a single unreviewed request from executing a high-impact parameter override.

## What changed

### Core state machine (contracts/escrow/src/governance_proposal.rs)
Four new entrypoints on the Escrow contract:
- request_governance_proposal(kind) → proposal_id
  Admin-gated. Validates the payload immediately (same bounds as the live
  setter). Returns a monotonic ID and stores a Pending proposal.
- approve_governance_proposal(proposal_id, approver)
  Approver must differ from the requester (GovernanceSelfApproval guard).
  Moves proposal from Pending → Approved.
- reject_governance_proposal(proposal_id, approver)
  Same self-approval guard. Moves Pending → Rejected (terminal).
- apply_governance_proposal(proposal_id)
  Admin-gated. Requires Approved state. Materialises the parameter change,
  then sets Applied (idempotency: a second call fails with
  GovernanceProposalInvalidState).

Two read-only helpers:
- get_governance_proposal(id) → Option<GovernanceProposal>
- get_next_governance_proposal_id() → u64

### Types (contracts/escrow/src/types.rs)
- GovernanceProposalKind enum (SetProtocolFeeBps, SetGovernedParams,
  SetFeeWithdrawalCap, SetFeeWithdrawalCooldown, SetMaxMilestones)
- GovernanceProposalState enum (Pending / Approved / Rejected / Applied)
- GovernanceProposal struct with requester, approver, state, kind, expiry
- DataKey variants: GovernanceProposal(u64) and NextGovernanceProposalId
- Error codes: GovernanceProposalNotFound(77), GovernanceProposalInvalidState(78),
  GovernanceProposalExpired(79), GovernanceSelfApproval(80)

### TTL (contracts/escrow/src/ttl.rs)
- GOVERNANCE_PROPOSAL_TTL_LEDGERS = LEDGERS_PER_DAY * 3 (~3 days)
- GOVERNANCE_PROPOSAL_BUMP_THRESHOLD = LEDGERS_PER_DAY
- set_governance_proposal_ttl() helper

### Tests (contracts/escrow/src/test/governance_proposal.rs) — 26 tests
All 5 required edge cases from issue #1221:
1. request by operator — admin submits proposal; ID positive; Pending state
2. self-approval — requester cannot approve own proposal → GovernanceSelfApproval
3. expired request — any action past TTL → GovernanceProposalExpired
4. rejected request — Rejected terminal; approve/apply fail → GovernanceProposalInvalidState
5. apply twice — second apply → GovernanceProposalInvalidState

Plus: happy-path round-trips for all 3 proposal kinds, not-found guards,
expiry boundary (succeeds at boundary, fails one past), events for each
transition, multiple independent proposals, approver identity recorded.

## Security notes
- Self-approval is blocked at both approve and reject paths.
- Expiry is checked on every mutable action; the 3-day window limits the
  blast radius of a forgotten or compromised pending proposal.
- Rejection is terminal — a rejected proposal cannot be re-approved or
  applied; the admin must open a fresh proposal.
- Applied proposals cannot be re-applied (idempotency guard on state).
- Payload is validated at request time so invalid values never reach storage.

Closes #1221

* feat(escrow): token scale persistence, validation, and normalized-value exposure

Closes #1346

## Problem

Different token decimal configurations cause contract amounts to be
misinterpreted by clients and payout logic:
- A client passing visible-token units instead of raw stroops creates
  milestone amounts 10^decimals times too small.
- An amount not exactly divisible by 10^decimals silently loses the
  fractional remainder on display.

## What changed

### New module: contracts/escrow/src/token_scale.rs
- capture_and_store_token_scale(env, token) — probes token.decimals()
  at bind time and stores under DataKey::TokenScale (read-only probe,
  no transfer risk).
- require_exact_scale(env, amount, decimals) — validates a single raw
  amount is exactly representable; panics with FractionalTokenAmount.
- require_all_exact_scale(env, amounts, decimals) — iterates over a
  milestone slice.
- normalized_amount(amount, decimals) — divides raw by 10^decimals for
  the view layer (integer; exact because of prior validation).
- scale_multiplier(decimals) — computes 10^decimals safely.
- MAX_TOKEN_DECIMALS = 18 upper bound.

### types.rs
- DataKey::TokenScale — new persistent key.
- Error::TokenScaleNotSet = 81
- Error::FractionalTokenAmount = 82
- Error::TokenScaleMismatch = 83

### lib.rs (bind_settlement_token)
- After write_settlement_token, calls
  token_scale::capture_and_store_token_scale to persist decimals.

### create_contract.rs
- After amount bounds validation, calls
  token_scale::require_all_exact_scale when a scale is recorded.
  Skipped when no token bound yet (pre-bind contract creation allowed).

### New read entrypoints on Escrow
- get_token_scale() -> Option<u32>
- get_normalized_amount(raw_amount: i128) -> i128

### Tests: contracts/escrow/src/test/token_scale.rs — 28 tests

All 5 required edge cases:
1. zero decimals — scale_multiplier(0)==1; any integer representable
2. fractional input — amounts not divisible by scale rejected
3. maximum value — MAX_TOTAL_ESCROW_STROOPS accepted and normalizes correctly
4. scale mismatch — write-once bind prevents scale change; stored scale frozen
5. scale change after funding — funded contract amounts remain valid; second
   bind rejected, scale unchanged

Plus: get_token_scale before/after bind, get_normalized_amount round-trip,
pre-bind contract creation skips validation, scale_multiplier and
normalized_amount unit tests.

## Security notes
- decimals() probe is read-only — no funds moved, no re-entrancy risk.
- Scale stored by value at bind time; subsequent changes to the token's
  on-chain decimals field have no effect on stored validation.
- write-once bind_settlement_token prevents scale drift after contracts
  are created.
- All arithmetic uses integer ops; no floating point.

---------

Co-authored-by: mikewheeleer <268014356+mikewheeleer@users.noreply.github.com>
…d/dispute flows (#1383)

Implements reusable accounting invariant helpers and full lifecycle edge-case
tests required by issue #1358:

- assert_accounting_invariant: checks total_deposited == released + refunded + available
  after every mutating step so the first violating operation is immediately identified
- assert_token_conservation: cross-checks internal counters against the real SAC
  on-chain balance for single-contract scenarios
- assert_token_conservation_multi: summed balance check for multi-contract tests

Edge cases covered (each with dedicated tests):
- deposit → full release (all milestones, balance → 0)
- deposit → full refund (all milestones, tokens returned to client)
- partial releases then refund remainder (mixed Completed state)
- incremental per-milestone refunds (invariant holds at each step)
- dispute → FullRefund / FullPayout / Split closure (accounting-only, no token transfer)
- re-raise after resolution rejected (terminal state guard)
- multiple escrows isolated (A operations never bleed into B, with multi-contract conservation)
- three concurrent escrows with mixed auth modes (ClientOnly, ClientAndArbiter, dispute)

Authorization boundaries:
- outsider cannot raise dispute (UnauthorizedRole)
- impostor cannot resolve dispute (UnauthorizedRole)
- non-client deposit rejected (UnauthorizedRole)
- double release rejected (MilestoneAlreadyReleased)
- double refund rejected (AlreadyRefunded)

Boundary / replay tests:
- cancel after full deposit returns all tokens (zero escrow balance)
- cancel unfunded contract is token no-op
- over-deposit rejected, correct deposit still works
- zero deposit rejected
- release without deposit rejected
- out-of-range milestone rejected (IndexOutOfBounds)
- storage round-trip after complete lifecycle
- typed error codes stable across repeated calls

Security note: resolve_dispute updates accounting counters only and does not
execute SAC token transfers. The conservation invariant (available_balance == 0
after resolution) is verified by assert_token_conservation post-resolution.

Closes #1358
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.