Skip to content

feat: add 48-hour timelock delay for dispute resolution payouts - #575

Open
collinsezedike wants to merge 3 commits into
Fundable-Protocol:mainfrom
collinsezedike:feat/dispute-resolution-timelock
Open

feat: add 48-hour timelock delay for dispute resolution payouts#575
collinsezedike wants to merge 3 commits into
Fundable-Protocol:mainfrom
collinsezedike:feat/dispute-resolution-timelock

Conversation

@collinsezedike

@collinsezedike collinsezedike commented Jul 29, 2026

Copy link
Copy Markdown

Summary

  • Adds a mandatory 48-hour timelock delay queue between a decided dispute resolution and its payout execution on the payment-stream contract.
  • resolve_dispute(stream_id, recipient_amount, sender_amount) (admin-gated) records the decided outcome, moves the stream into a new Disputed status, and queues the payout with execute_after = now + 48h. It does not move funds immediately.
  • execute_resolution(dispute_id) is permissionless (the outcome and amounts were already authorized when queued; only the passage of time gates it) and pays out recipient_amount to the recipient and sender_amount back to the sender once the timelock has elapsed, then marks the stream Completed.
  • cancel_queued_resolution(dispute_id) (admin-gated) reverses a queued resolution before execution, restoring the stream to its pre-dispute status (Active or Paused), e.g. if new evidence emerges during the delay window.
  • get_queued_resolution / get_active_dispute views for inspecting dispute state.
  • While a stream is Disputed, deposits, withdrawals, pausing, resuming, and cancellation are all blocked until the dispute is executed or canceled.
  • 6 new error variants: DisputeInProgress, DisputeAlreadyQueued, DisputeNotFound, DisputeAlreadyExecuted, TimelockNotElapsed, InvalidResolutionAmounts.

Design notes

This repo doesn't yet have a standalone dispute/voting subsystem, so per the issue's scope (the timelock controller itself, not a full arbitration system) the admin acts as the resolver recording an already-decided outcome, consistent with how admin already gates protocol-level actions elsewhere in this contract (fee rate, fee collector). resolve_dispute and execute_resolution are split so the queued outcome and amounts are fixed and authorized up front, and execution only requires the delay to have elapsed, matching a standard timelock-controller pattern (anyone can trigger execution once ready). This also referenced in the issue's application thread as connecting to the emergency pause switch (#510); since that hasn't landed on main yet, execute_resolution isn't pause-gated here — happy to wire that in once #510 merges.

Test plan

  • cargo test -p payment-stream — 55/55 tests pass, including 19 new tests covering: queuing and stream-status transition, execution before/after the timelock, double-execution, executing a nonexistent dispute, cancellation restoring Active/Paused state, cancelling an already-executed dispute, admin-auth enforcement, invalid/negative/over-balance resolution amounts, already-disputed rejection, and deposit/withdraw/cancel/pause being blocked mid-dispute.
  • cargo build -p payment-stream --target wasm32v1-none --release — compiles cleanly with zero warnings.

Closes #517

Summary by CodeRabbit

  • New Features

    • Added dispute handling for payment streams.
    • Disputed streams temporarily block deposits, withdrawals, swaps, pausing, and cancellation.
    • Administrators can queue validated payout resolutions with a 48-hour timelock.
    • Resolutions can be executed after the timelock, while queued resolutions may be canceled to restore the stream’s previous status.
    • Added dispute status tracking and lifecycle events.
  • Tests

    • Added comprehensive coverage for authorization, invalid resolutions, timelocks, cancellation, duplicate disputes, and blocked operations.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@collinsezedike, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0565f20c-a325-438d-a9d3-2d88ee4fa2e1

📥 Commits

Reviewing files that changed from the base of the PR and between b54e415 and 9d76db9.

📒 Files selected for processing (2)
  • contracts/payment-stream/src/lib.rs
  • contracts/payment-stream/src/test.rs
📝 Walkthrough

Walkthrough

The payment-stream contract adds disputed stream state and a 48-hour resolution timelock. Admins can queue or cancel validated payouts. Anyone can execute queued payouts after the timelock. Tests and ledger snapshots cover the lifecycle and blocked operations.

Changes

Payment stream dispute resolution

Layer / File(s) Summary
Dispute state and public contracts
contracts/payment-stream/src/lib.rs
Adds dispute storage keys, StreamStatus::Disputed, queued-resolution types, lifecycle events, errors, and the 48-hour timelock.
Queue, execute, and cancel resolution
contracts/payment-stream/src/lib.rs
Adds resolve_dispute, execute_resolution, cancel_queued_resolution, and dispute lookup methods. Disputed streams reject deposits and swap deposits.
Dispute lifecycle and operation tests
contracts/payment-stream/src/test.rs
Tests timelock behavior, payout execution, cancellation, authorization, amount validation, duplicate disputes, and blocked stream operations.
Ledger snapshot coverage
contracts/payment-stream/test_snapshots/test/test/*
Adds snapshots for dispute queueing, execution, cancellation, validation failures, authorization failures, and blocked operations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant PaymentStream
  participant Anyone
  participant TokenContract
  Admin->>PaymentStream: Queue dispute resolution
  PaymentStream->>PaymentStream: Mark stream Disputed
  Anyone->>PaymentStream: Execute after 48-hour timelock
  PaymentStream->>TokenContract: Transfer recipient and sender payouts
  PaymentStream->>PaymentStream: Complete stream and clear dispute
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The implementation covers the dispute queue, timelock, authorization, errors, lifecycle tests, and blocked operations, but storage TTL and public documentation are not evidenced. Provide evidence of storage TTL management and documentation comments for all new public functions, or add the missing implementation and documentation.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding a 48-hour timelock for dispute-resolution payouts.
Out of Scope Changes check ✅ Passed The contract changes, tests, and snapshots directly support the dispute-resolution timelock objectives and do not show unrelated code changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@drips-wave

drips-wave Bot commented Jul 29, 2026

Copy link
Copy Markdown

@collinsezedike Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Idrhas

Idrhas commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

dont forget to offramp using https://stellar.fundable.finance/offramp its fast, free and p2p rates

1 similar comment
@Idrhas

Idrhas commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

dont forget to offramp using https://stellar.fundable.finance/offramp its fast, free and p2p rates

…on-timelock

# Conflicts:
#	contracts/payment-stream/src/lib.rs
#	contracts/payment-stream/src/test.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (8)
contracts/payment-stream/src/lib.rs (3)

1591-1605: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Bound cancellation to the timelock window.

The doc states cancellation happens "before its timelock elapses". The code allows cancellation at any time while executed == false. The admin can therefore cancel a matured resolution, re-queue it with resolve_dispute, and repeat, which defers the payout indefinitely and weakens the 48-hour guarantee.

Reject cancellation once execute_after has passed, or update the doc to state that the admin can cancel at any time before execution.

Also consider adding Self::assert_not_paused(&env) for consistency with resolve_dispute and execute_resolution. If the omission is intentional, so that the admin can unwind disputes during an incident, state that in the doc.

♻️ Proposed change to enforce the cancellation window
         if queued.executed {
             panic_with_error!(&env, Error::DisputeAlreadyExecuted);
         }
+        if env.ledger().timestamp() >= queued.execute_after {
+            panic_with_error!(&env, Error::TimelockNotElapsed);
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/payment-stream/src/lib.rs` around lines 1591 - 1605, Update
cancel_queued_resolution to reject cancellation once queued.execute_after has
elapsed, while preserving cancellation before the timelock and the existing
executed/not-found checks. Add Self::assert_not_paused(&env) for consistency
with resolve_dispute and execute_resolution, or explicitly document the
intentional paused-state behavior if cancellation must remain available during
incidents.

1634-1642: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Executed dispute records are retained indefinitely.

execute_resolution keeps the Dispute(dispute_id) record and extends its TTL after execution (Lines 1574-1576). The record then only serves historical lookup through get_queued_resolution, while DisputeExecutedEvent already carries the same data. Linked issue #517 asks for an efficient state footprint.

Consider removing the record after execution, or stop extending its TTL so it expires naturally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/payment-stream/src/lib.rs` around lines 1634 - 1642, The
execute_resolution flow should no longer retain executed Dispute(dispute_id)
records indefinitely. Update execute_resolution to remove the dispute record
after successful execution, or omit its TTL extension so it expires naturally,
while preserving DisputeExecutedEvent emission and active-dispute cleanup
behavior.

1441-1447: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Blocked operations do not report DisputeInProgress.

The doc says the Disputed status blocks deposits, withdrawals, pausing, resuming, and cancellation. Only deposit (Line 723) and deposit_with_swap (Line 807) raise DisputeInProgress. The other operations fall through to pre-existing status checks and report misleading codes:

  • withdraw reports InsufficientWithdrawable, because withdrawable_amount returns 0 for a non-Active stream.
  • pause_stream reports StreamNotActive.
  • resume_stream reports StreamNotPaused.
  • cancel_stream reports StreamCannotBeCanceled.

The behavior is safe, but clients cannot distinguish an active dispute from an ordinary state error. Add an explicit Disputed check to each of these entry points so the contract returns DisputeInProgress.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/payment-stream/src/lib.rs` around lines 1441 - 1447, Add an
explicit Disputed-status check to withdraw, pause_stream, resume_stream, and
cancel_stream so each returns DisputeInProgress before existing state or balance
validation. Preserve the current behavior for all non-disputed statuses and keep
the existing deposit checks unchanged.
contracts/payment-stream/src/test.rs (5)

1894-1921: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a test at the exact timelock boundary.

execute_resolution rejects only when env.ledger().timestamp() < queued.execute_after. Execution at exactly execute_after must succeed. The current tests cover DAY (rejected) and 2 * DAY + 1 (accepted), so they do not pin the boundary. An off-by-one change to that comparison would not fail any test.

💚 Proposed additional test
#[test]
fn test_execute_resolution_at_exact_timelock_succeeds() {
    let env = Env::default();
    env.mock_all_auths();

    let (_admin, _sender, _recipient, _token, contract_id, stream_id) = setup_dispute_test(&env);
    let client = PaymentStreamContractClient::new(&env, &contract_id);

    let dispute_id = client.resolve_dispute(&stream_id, &600, &400);

    // execute_after == 0 + 2 * DAY; execution must be allowed at that instant.
    env.ledger().set_timestamp(2 * DAY);
    client.execute_resolution(&dispute_id);

    let stream = client.get_stream(&stream_id);
    assert_eq!(stream.status, StreamStatus::Completed);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/payment-stream/src/test.rs` around lines 1894 - 1921, Add a
boundary test alongside test_execute_resolution_before_timelock_fails and
test_execute_resolution_after_timelock_succeeds that resolves a dispute, sets
the ledger timestamp to exactly 2 * DAY (the queued execute_after value), calls
execute_resolution, and asserts the stream status is StreamStatus::Completed.

2104-2127: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover the remaining resolve_dispute validation branches.

Two guards in resolve_dispute have no test:

  • total_resolution <= 0: call resolve_dispute(&stream_id, &0, &0). Both amounts are non-negative, so this reaches the zero-total guard rather than the negative-amount guard.
  • checked_add overflow: call with i128::MAX and a positive sender amount to reach Error::ArithmeticOverflow before the balance comparison.

Both tests are short and pin guards that protect payout amounts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/payment-stream/src/test.rs` around lines 2104 - 2127, Add tests
alongside test_resolve_dispute_invalid_amounts_exceeding_balance and
test_resolve_dispute_negative_amounts_rejected covering the remaining
resolve_dispute guards: assert that zero sender and recipient amounts return an
error for the total_resolution <= 0 path, and assert that i128::MAX with a
positive sender amount returns an error for the checked_add overflow path.

1870-1891: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the let _ = admin; artifact.

Bind the value as _admin in the destructuring pattern. The other dispute tests already use that form. This removes the trailing discard statement.

♻️ Proposed refactor
-    let (admin, _sender, _recipient, _token, contract_id, stream_id) = setup_dispute_test(&env);
+    let (_admin, _sender, _recipient, _token, contract_id, stream_id) = setup_dispute_test(&env);
@@
     let protocol_metrics = client.get_protocol_metrics();
     assert_eq!(protocol_metrics.total_active_streams, 0);
-
-    let _ = admin;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/payment-stream/src/test.rs` around lines 1870 - 1891, Update the
setup_dispute_test destructuring to bind the unused administrator value as
_admin instead of admin, then remove the trailing let _ = admin; statement.
Leave the remaining test logic unchanged.

1836-1863: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Return the client from setup_dispute_test.

The helper builds a PaymentStreamContractClient and then discards it. Every one of the 14 dispute tests re-creates the same client from contract_id. Return the client to remove that duplicated line.

♻️ Proposed refactor
-fn setup_dispute_test(env: &Env) -> (Address, Address, Address, Address, Address, u64) {
+fn setup_dispute_test(
+    env: &Env,
+) -> (PaymentStreamContractClient, Address, Address, Address, Address, Address, u64) {
     let admin = Address::generate(env);
@@
-    (admin, sender, recipient, token, contract_id, stream_id)
+    (client, admin, sender, recipient, token, contract_id, stream_id)
 }

Each caller then becomes:

let (client, _admin, _sender, _recipient, _token, contract_id, stream_id) =
    setup_dispute_test(&env);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/payment-stream/src/test.rs` around lines 1836 - 1863, Update
setup_dispute_test to return the existing PaymentStreamContractClient alongside
the current setup values, preserving the client’s initialization and stream
creation. Adjust all dispute-test callers to destructure the returned client and
use it directly instead of reconstructing it from contract_id.

1906-1907: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the expected error variants for dispute-resolution tests.

try_execute_resolution(&dispute_id) needs Err(Ok(Error::<variant>)) assertions instead of is_err(), so tests catch the exact failure path such as TimelockNotElapsed, DisputeAlreadyExecuted, or DisputeNotFound rather than any error.

Applies to lines 1906-1907, 1950-1951, 1962-1963, 2029-2030, 2100-2101, 2113-2114, 2125-2126, 2139-2140, 2153-2154, 2168-2169, 2182-2183, 2196-2197.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/payment-stream/src/test.rs` around lines 1906 - 1907, Update each
dispute-resolution test assertion around try_execute_resolution to match the
exact expected Err(Ok(Error::<variant>)) value instead of using is_err(). Use
the failure variant appropriate to each scenario, including TimelockNotElapsed,
DisputeAlreadyExecuted, or DisputeNotFound, while preserving the existing test
setup and execution flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contracts/payment-stream/src/lib.rs`:
- Around line 1561-1571: Update execute_resolution so any escrow remaining after
transferring the resolved recipient_amount and sender_amount is refunded to
stream.sender before marking the stream Completed. Ensure the total payouts,
including this residual refund, consume the full escrowed balance while
preserving the existing exact-split behavior; alternatively, enforce in
resolve_dispute that total_resolution equals the escrowed balance.

In `@contracts/payment-stream/src/test.rs`:
- Around line 2011-2014: Update the comment above the total_active_streams
assertion to state that pausing decrements the active count, while preserving
the assertion value of 0 and leaving the test logic unchanged.
- Around line 2143-2155: Update test_deposit_blocked_during_dispute to ensure
the attempted deposit is otherwise valid by creating the disputed stream with
initial balance below total_amount, or assert the specific dispute-blocking
error variant returned by try_deposit. Keep the test focused on verifying that
an available deposit is rejected because the stream is under dispute, not
because it exceeds the total.

In
`@contracts/payment-stream/test_snapshots/test/test/test_execute_resolution_twice_fails.1.json`:
- Around line 402-511: The execute_resolution flow must account for both
recipient and sender transfers when updating stream settlement state. Update the
relevant balance/withdrawn_amount logic to record the full
queue.recipient_amount plus queue.sender_amount, ensuring the resulting balance
and withdrawn_amount reflect zero remaining escrow after a 600/400 resolution.

---

Nitpick comments:
In `@contracts/payment-stream/src/lib.rs`:
- Around line 1591-1605: Update cancel_queued_resolution to reject cancellation
once queued.execute_after has elapsed, while preserving cancellation before the
timelock and the existing executed/not-found checks. Add
Self::assert_not_paused(&env) for consistency with resolve_dispute and
execute_resolution, or explicitly document the intentional paused-state behavior
if cancellation must remain available during incidents.
- Around line 1634-1642: The execute_resolution flow should no longer retain
executed Dispute(dispute_id) records indefinitely. Update execute_resolution to
remove the dispute record after successful execution, or omit its TTL extension
so it expires naturally, while preserving DisputeExecutedEvent emission and
active-dispute cleanup behavior.
- Around line 1441-1447: Add an explicit Disputed-status check to withdraw,
pause_stream, resume_stream, and cancel_stream so each returns DisputeInProgress
before existing state or balance validation. Preserve the current behavior for
all non-disputed statuses and keep the existing deposit checks unchanged.

In `@contracts/payment-stream/src/test.rs`:
- Around line 1894-1921: Add a boundary test alongside
test_execute_resolution_before_timelock_fails and
test_execute_resolution_after_timelock_succeeds that resolves a dispute, sets
the ledger timestamp to exactly 2 * DAY (the queued execute_after value), calls
execute_resolution, and asserts the stream status is StreamStatus::Completed.
- Around line 2104-2127: Add tests alongside
test_resolve_dispute_invalid_amounts_exceeding_balance and
test_resolve_dispute_negative_amounts_rejected covering the remaining
resolve_dispute guards: assert that zero sender and recipient amounts return an
error for the total_resolution <= 0 path, and assert that i128::MAX with a
positive sender amount returns an error for the checked_add overflow path.
- Around line 1870-1891: Update the setup_dispute_test destructuring to bind the
unused administrator value as _admin instead of admin, then remove the trailing
let _ = admin; statement. Leave the remaining test logic unchanged.
- Around line 1836-1863: Update setup_dispute_test to return the existing
PaymentStreamContractClient alongside the current setup values, preserving the
client’s initialization and stream creation. Adjust all dispute-test callers to
destructure the returned client and use it directly instead of reconstructing it
from contract_id.
- Around line 1906-1907: Update each dispute-resolution test assertion around
try_execute_resolution to match the exact expected Err(Ok(Error::<variant>))
value instead of using is_err(). Use the failure variant appropriate to each
scenario, including TimelockNotElapsed, DisputeAlreadyExecuted, or
DisputeNotFound, while preserving the existing test setup and execution flow.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 328dbbeb-ac7c-42fa-b21c-5d4019117503

📥 Commits

Reviewing files that changed from the base of the PR and between a9642f1 and b54e415.

📒 Files selected for processing (18)
  • contracts/payment-stream/src/lib.rs
  • contracts/payment-stream/src/test.rs
  • contracts/payment-stream/test_snapshots/test/test/test_cancel_already_executed_resolution_fails.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_cancel_queued_resolution_restores_paused_stream.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_cancel_queued_resolution_restores_stream.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_cancel_stream_blocked_during_dispute.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_blocked_during_dispute.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_execute_nonexistent_resolution_fails.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_execute_resolution_after_timelock_succeeds.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_execute_resolution_before_timelock_fails.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_execute_resolution_twice_fails.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_pause_blocked_during_dispute.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_resolve_dispute_already_disputed_fails.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_resolve_dispute_invalid_amounts_exceeding_balance.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_resolve_dispute_negative_amounts_rejected.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_resolve_dispute_queues_and_pauses_stream.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_resolve_dispute_requires_admin_auth.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_blocked_during_dispute.1.json

Comment thread contracts/payment-stream/src/lib.rs
Comment thread contracts/payment-stream/src/test.rs Outdated
Comment on lines +2143 to +2155
#[test]
fn test_deposit_blocked_during_dispute() {
let env = Env::default();
env.mock_all_auths();

let (_admin, _sender, _recipient, _token, contract_id, stream_id) = setup_dispute_test(&env);
let client = PaymentStreamContractClient::new(&env, &contract_id);

client.resolve_dispute(&stream_id, &600, &400);

let result = client.try_deposit(&stream_id, &1);
assert!(result.is_err());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The deposit test does not prove the dispute block.

setup_dispute_test creates the stream with total_amount = 1000 and initial_amount = 1000, so balance already equals total_amount. try_deposit(&stream_id, &1) therefore fails the "deposit exceeds total" check as well, exactly as test_deposit_exceeds_total at line 399 shows. The assertion passes for the wrong reason, and it would still pass if the dispute block were removed.

Create a stream with headroom, or assert the specific dispute error variant.

💚 Proposed fix
 fn test_deposit_blocked_during_dispute() {
     let env = Env::default();
     env.mock_all_auths();
 
-    let (_admin, _sender, _recipient, _token, contract_id, stream_id) = setup_dispute_test(&env);
-    let client = PaymentStreamContractClient::new(&env, &contract_id);
+    let (_admin, sender, _recipient, token, contract_id, _stream_id) = setup_dispute_test(&env);
+    let client = PaymentStreamContractClient::new(&env, &contract_id);
+
+    // Fund a second stream that has deposit headroom, so the only reason a
+    // deposit can fail is the active dispute.
+    let token_admin = token::StellarAssetClient::new(&env, &token);
+    token_admin.mint(&sender, &1000);
+    let recipient2 = Address::generate(&env);
+    let stream_id = client.create_stream(&sender, &recipient2, &token, &1000, &500, &0, &100);
 
     client.resolve_dispute(&stream_id, &600, &400);
 
     let result = client.try_deposit(&stream_id, &1);
     assert!(result.is_err());
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/payment-stream/src/test.rs` around lines 2143 - 2155, Update
test_deposit_blocked_during_dispute to ensure the attempted deposit is otherwise
valid by creating the disputed stream with initial balance below total_amount,
or assert the specific dispute-blocking error variant returned by try_deposit.
Keep the test focused on verifying that an available deposit is rejected because
the stream is under dispute, not because it exceeds the total.

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.

[Contract] Add Timelock Delay Controller for Stream Resolution Disputes

2 participants