Skip to content

Feat/reentrancy protection - #564

Open
opratem wants to merge 7 commits into
Fundable-Protocol:mainfrom
opratem:feat/reentrancy-protection
Open

Feat/reentrancy protection#564
opratem wants to merge 7 commits into
Fundable-Protocol:mainfrom
opratem:feat/reentrancy-protection

Conversation

@opratem

@opratem opratem commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Closes #505


PR: Add reentrancy protection to payment-stream contract (#505)

Summary

Adds per-stream and global reentrancy guards to every state-mutating function in the payment-stream Soroban contract, closes issue #505.


Motivation

The payment-stream contract performs token transfers (via transfer / transfer_from) inside several functions. Without reentrancy protection a malicious token contract could re-enter any of those functions mid-execution and manipulate stream state (balances, status) before the initial call has written its effects back. This class of bug has caused significant losses in EVM-based streaming protocols; Soroban's synchronous host makes the attack surface different but not absent, especially when cross-contract calls are involved.


Design

Guard mechanism

Reentrancy is detected using Soroban temporary storage boolean flags. Temporary storage is:

  • Per-transaction — entries are automatically cleared at ledger close, so a lock can never be left permanently set by a buggy call path.
  • Cheap — one temporary-storage read on entry, one removal on exit, both inside the same transaction.
  • Reorg-safe — failed transactions (panics) roll back the entire storage diff, so a panicking guarded call cannot leave the lock set.

Lock granularity

Lock key Functions guarded
(stream_id, Symbol("lock")) create_stream, deposit, withdraw, pause_stream, resume_stream, cancel_stream, set_delegate, revoke_delegate
Symbol("g_lock") set_protocol_fee_rate, set_fee_collector

Per-stream locks are preferred over a single global lock so that operations on independent streams do not block each other. withdraw_max is not given its own lock; it delegates entirely to withdraw, which holds the lock for the duration.

Error code

Error::ReentrancyGuard = 17 is added to the existing error enum.

Four private helpers

fn acquire_stream_lock(env: &Env, stream_id: u64)  // panics with #17 if lock held
fn release_stream_lock(env: &Env, stream_id: u64)  // removes the temp entry
fn acquire_global_lock(env: &Env)                  // panics with #17 if g_lock held
fn release_global_lock(env: &Env)                  // removes g_lock

All guarded functions follow check-effects-interactions order: auth checks → state validation → state mutation → lock release → token transfer. The lock is acquired at the very top of each function and released immediately before the token transfer call.


Changes

contracts/payment-stream/src/lib.rs

  • Error::ReentrancyGuard = 17 added to the Error enum.
  • Four private lock-helper functions added: acquire_stream_lock, release_stream_lock, acquire_global_lock, release_global_lock.
  • Per-stream lock applied to: create_stream, deposit, withdraw, pause_stream, resume_stream, cancel_stream, set_delegate, revoke_delegate.
  • Global lock applied to: set_protocol_fee_rate, set_fee_collector.
  • Full Rust doc comments added to every public function, including # Errors sections listing all possible error codes.

contracts/payment-stream/src/test.rs

13 new tests added (appended to the existing suite):

Test What it asserts
test_reentrancy_guard_blocks_reentrant_withdraw Pre-setting the per-stream lock → withdraw panics #17
test_reentrancy_guard_blocks_reentrant_deposit Pre-setting the per-stream lock → deposit panics #17
test_reentrancy_guard_blocks_reentrant_cancel Pre-setting the per-stream lock → cancel_stream panics #17
test_reentrancy_guard_blocks_reentrant_pause Pre-setting the per-stream lock → pause_stream panics #17
test_reentrancy_guard_blocks_reentrant_resume Pre-setting the per-stream lock → resume_stream panics #17
test_reentrancy_guard_blocks_reentrant_set_delegate Pre-setting the per-stream lock → set_delegate panics #17
test_reentrancy_guard_blocks_reentrant_revoke_delegate Pre-setting the per-stream lock → revoke_delegate panics #17
test_reentrancy_guard_blocks_global_set_fee_rate Pre-setting g_lockset_protocol_fee_rate panics #17
test_reentrancy_guard_blocks_global_set_fee_collector Pre-setting g_lockset_fee_collector panics #17
test_reentrancy_lock_released_after_successful_withdraw Two sequential withdraws on the same stream both succeed (lock released between calls)
test_reentrancy_lock_released_after_successful_cancel Two independent streams can be canceled sequentially without interference
test_independent_streams_use_separate_locks Locking stream A does not block a withdraw on stream B

Testing

# Library compiles without warnings
cargo build --lib -p payment-stream

# Run the full test suite (requires soroban-env-host fix tracked in #506)
cargo test -p payment-stream

cargo build --lib -p payment-stream is clean with zero warnings. cargo test is blocked by the upstream soroban-env-host v22.1.3 / rand_core version conflict tracked separately in #506; this is not introduced by this PR.


Checklist

  • Error::ReentrancyGuard = 17 added to error enum
  • Per-stream lock on all token-touching stream functions
  • Global lock on all admin fee-configuration functions
  • withdraw_max correctly inherits the lock via withdraw delegation
  • All guarded functions follow check-effects-interactions order
  • 13 reentrancy tests cover every guarded function (both per-stream and global)
  • Lock-released-after-success tests confirm no lock leakage
  • Independent-stream isolation test confirms per-stream (not global) locking
  • cargo build --lib -p payment-stream passes clean
  • Rust doc comments with # Errors on all public functions

Summary by CodeRabbit

  • New Features

    • Added protection against reentrant payment-stream operations, including stream actions and fee administration.
    • Ensured separate streams remain independently operable.
  • Bug Fixes

    • Corrected payment-stream creation calculations for duration, start time, and token amounts.
    • Improved wallet session restoration across connection status, wallet selection, and network preferences.
    • Refined wallet modal accessibility, sizing, spacing, and presentation.
  • Tests

    • Updated contract snapshots to the latest ledger format.
    • Expanded dispute-resolution and reentrancy-protection coverage.
    • Clarified campaign error handling in tests.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • contracts/payment-stream/src/test.rs
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1207c512-84d4-4edd-8170-b553e7140c0f

📥 Commits

Reviewing files that changed from the base of the PR and between 0c1d05f and 7989bad.

📒 Files selected for processing (1)
  • contracts/payment-stream/src/test.rs

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds payment-stream reentrancy locks and coverage, updates protocol 25 contract snapshots, modernizes contract test expectations, and adjusts wallet restoration, stream timing, modal markup, and related client code.

Changes

Payment-stream reentrancy protection

Layer / File(s) Summary
Per-stream and global lock handling
contracts/payment-stream/src/lib.rs, contracts/payment-stream/src/test.rs
Adds DataKey::Lock, error 32, lock helpers, and lock acquisition and release across stream mutations, dispute resolution, and fee administration. Tests cover blocked calls, lock release, and independent stream locks.
Reentrancy snapshots
contracts/payment-stream/test_snapshots/test/test/*reentrancy*, contracts/payment-stream/test_snapshots/test/test/test_independent_streams_use_separate_locks.1.json
Adds snapshots for blocked operations, released locks, global locks, and separate stream locks.

Protocol snapshot updates

Layer / File(s) Summary
Campaign-funding snapshots
contracts/campaign-funding/test_snapshots/tests/*
Migrates snapshots from protocol 22 to 25. Adds mux_id, string-encodes numeric values, and converts keyed ledger records to direct entries with entry and live_until.
Dispute-arbiter snapshots
contracts/dispute-arbiter/test_snapshots/test/*
Adds execution snapshots for initialization, dispute creation, voting outcomes, evidence requests, timeout resolution, and rejected actions.
Payment-stream snapshots
contracts/payment-stream/test_snapshots/test/test/*
Migrates stream and dispute snapshots to protocol 25 serialization and records current stream, dispute, storage, balance, and TTL state.

Client and contract test updates

Layer / File(s) Summary
Client state and UI adjustments
apps/web/src/providers/StellarWalletProvider.tsx, apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx, apps/web/src/components/organisms/wallet-modal.tsx, apps/web/src/components/map/FundableMapView.tsx
Restores validated wallet session state, orders stream timing and amount calculations, adds modal accessibility markup and an inner wrapper, and suppresses the mount-effect lint warning.
Contract test expectations and calls
contracts/campaign-funding/src/lib.rs, contracts/dispute-arbiter/src/lib.rs
Uses numeric campaign error codes, documents the nonexistent-campaign panic path, and removes unnecessary success-result unwrapping.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes unrelated campaign-funding, dispute-arbiter, and web application changes beyond payment-stream reentrancy protection. Remove unrelated contract snapshots, dispute-arbiter snapshots, and web changes, or move them to separate pull requests.
Linked Issues check ❓ Inconclusive The PR adds stream and global locks, error handling, and focused tests for issue #505, but the full test suite is blocked by an upstream dependency conflict. Resolve or document the soroban-env-host/rand_core conflict, then run cargo test and confirm all issue #505 acceptance criteria.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: adding reentrancy protection to the payment-stream contract.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 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.

@opratem

opratem commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

This PR is for issue #505 [Contract] Add Reentrancy Protection Guards Across All Payment Stream Functions

@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

2 similar comments
@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

@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

@opratem

opratem commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

fixed the conflicts, please merge the PR

@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: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
contracts/payment-stream/src/lib.rs (2)

1049-1060: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

withdraw releases a lock it never acquires.

This function calls release_stream_lock at lines 1058 and 1114, but it never calls acquire_stream_lock. Withdrawal has no reentrancy guard, and a preset lock is cleared instead of rejected. test_reentrancy_guard_blocks_reentrant_withdraw cannot pass with this body.

Acquire the lock as the first statement, after assert_not_paused.

🔒️ Proposed fix to acquire the lock in `withdraw`
     pub fn withdraw(env: Env, stream_id: u64, amount: i128) {
         Self::assert_not_paused(&env);
+        Self::acquire_stream_lock(&env, stream_id);
         let mut stream: Stream = Self::get_stream(env.clone(), stream_id);
🤖 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 1049 - 1060, Update
withdraw to call acquire_stream_lock as the first statement after
assert_not_paused, before retrieving the stream or performing authorization
checks. Keep the existing release_stream_lock calls for cleanup on failure and
completion, so preset locks are rejected and reentrant withdrawals are blocked.

471-473: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Define require_not_paused before using it.

create_stream, deposit, and withdraw call Self::require_not_paused(&env), but the contract only defines assert_not_paused, is_paused, and pause/resume logic. The crate will not compile until the helper exists or these call sites use an existing check.

🤖 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 471 - 473, Define the
missing Self::require_not_paused helper used by create_stream, deposit, and
withdraw, or replace those calls with the existing assert_not_paused check.
Preserve the contract’s current paused-state validation behavior and remove the
redundant or undefined helper usage so the crate compiles.
🧹 Nitpick comments (8)
contracts/campaign-funding/src/lib.rs (3)

1318-1352: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename the test or refund every contributor.

The name states that all contributors are refunded. The body refunds only contrib4 on id2. Campaign id with contrib1, contrib2, and contrib3 stays Active and is never refunded, so the multi-contributor refund path is untested. Create one failed campaign with three contributors below min_target, then refund each one and assert every balance.

🤖 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/campaign-funding/src/lib.rs` around lines 1318 - 1352, Update
test_refund_multiple_contributors_all_refunded to use one failed campaign with
contrib1, contrib2, and contrib3 contributing below min_target; trigger expiry,
refund each contributor, and assert all three restored balances, removing the
separate id2/contrib4 setup.

597-611: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider recording the fee rate per campaign.

calculate_fee reads the current global FeeRate at claim time. If the admin calls set_fee_rate after a campaign becomes Successful, the creator receives a different net amount than the rate in force during the campaign. Storing the rate in the Campaign record at creation makes the fee deterministic for contributors and creators.

🤖 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/campaign-funding/src/lib.rs` around lines 597 - 611, Record the
active fee rate in each Campaign when it is created, and update calculate_fee to
use that campaign-specific value instead of the mutable global FeeRate. Ensure
campaign creation reads the current configured rate and persists it, while
preserving existing fee validation and claim behavior.

1161-1177: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fix the contradictory comment.

Line 1166 calls env.mock_all_auths(), but the comment on Line 1174 states that no auth mocking is used. trigger_expiry requires no auth, so the test can drop mock_all_auths after setup_contract and create_campaign, or the comment must be corrected.

🤖 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/campaign-funding/src/lib.rs` around lines 1161 - 1177, Resolve the
contradiction in test_trigger_expiry_permissionless by removing the unnecessary
env.mock_all_auths() call while preserving setup_contract and create_campaign
behavior, or update the nearby comment to accurately describe the authentication
mocking. Ensure the test still verifies trigger_expiry can be called
permissionlessly.
contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_permissionless.1.json (1)

155-195: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Rename the zero-contribution expiry test to describe its actual outcome.

test_trigger_expiry_with_zero_contributions_fails sets total_raised to 0 on an active campaign past the deadline and asserts status = Failed. The function does not panic for this input, so the test name should not imply panicking.

🤖 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/campaign-funding/test_snapshots/tests/test_trigger_expiry_permissionless.1.json`
around lines 155 - 195, Rename test_trigger_expiry_with_zero_contributions_fails
to describe the campaign becoming Failed after expiry with zero contributions,
without implying that the function panics. Update any references to the test
name while preserving its existing assertions and behavior.
contracts/campaign-funding/test_snapshots/tests/test_refund_multiple_contributors_all_refunded.1.json (1)

435-457: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Record refunds for every contributor in this snapshot.

test_refund_multiple_contributors_all_refunded only calls refund(&contrib4, &id2) after expiry, so the snapshot correctly shows one refund. Rename the test to describe the single-contributor refund, or update the test to resolve the three-contributor campaign to Failed and verify each contributor’s refund.

🤖 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/campaign-funding/test_snapshots/tests/test_refund_multiple_contributors_all_refunded.1.json`
around lines 435 - 457, Update test_refund_multiple_contributors_all_refunded to
resolve the three-contributor campaign to Failed after expiry, invoke refund for
each contributor, and assert the snapshot records all three refunds;
alternatively, rename the test and snapshot to describe the current
single-contributor refund behavior.
contracts/payment-stream/src/lib.rs (3)

1512-1517: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the redundant status test.

stream.status != StreamStatus::Active already covers the Paused case.

-        if stream.status == StreamStatus::Paused || stream.status != StreamStatus::Active {
+        if stream.status != StreamStatus::Active {
             return 0;
         }
🤖 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 1512 - 1517, In
withdrawable_amount, simplify the stream status guard by removing the redundant
explicit StreamStatus::Paused comparison and retain only the
StreamStatus::Active check, preserving the existing zero return for every
non-active stream.

1603-1608: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Return NotInitialized instead of panicking on a missing admin.

Both setters call .unwrap() on the admin lookup. On an uninitialised contract this produces an opaque host panic, and the global lock stays set in temporary storage because no release runs. emergency_pause at line 377 already uses unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized)). Use the same form here.

Also applies to: 1636-1641

🤖 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 1603 - 1608, Update the
admin lookup in both setter authorization blocks to replace unwrap-based failure
with panic_with_error! returning Error::NotInitialized, matching
emergency_pause. Preserve the existing admin.require_auth() flow when an admin
is present.

606-609: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

deposit transfers before it updates state.

The token transfer at line 609 runs before the balance write at line 611. Every other mutating function in this PR commits state first. Move the transfer after the state write to keep one consistent ordering.

🤖 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 606 - 609, In the deposit
flow, update the state and balance before performing the token transfer. Move
the token::Client transfer call after the existing balance write, preserving the
current amount and recipient arguments and the reentrancy guard.
🤖 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/campaign-funding/src/lib.rs`:
- Around line 1021-1032: Update test_contribute_to_nonexistent_campaign to
expect the CampaignNotFound panic produced by load_campaign for missing campaign
99, and revise the adjacent comment to describe that behavior without claiming
it is wrapped as CampaignNotActive.
- Around line 449-456: Update the `claim_funds` documentation’s `# Errors` list
to remove `Error::Unauthorized`, since `campaign.creator.require_auth()`
produces the authorization failure directly rather than that contract error.
Keep the remaining documented error variants unchanged.
- Around line 357-397: Reorder contribute so all state effects occur before the
external token_client.transfer interaction: persist the updated contributor
balance and campaign status/total through Self::save_campaign, then perform the
transfer. Preserve the existing hard-cap check and events, ensuring reentrant
calls observe the updated campaign and contribution state.

In
`@contracts/campaign-funding/test_snapshots/tests/test_create_campaign_success.1.json`:
- Around line 73-81: Regenerate the campaign-funding snapshots using the
workspace-pinned soroban-sdk version 25.3.2, then commit all updated files under
the campaign-funding test_snapshots/tests directory, including the ledger
metadata shown in test_create_campaign_success.1.json.

In `@contracts/payment-stream/src/lib.rs`:
- Around line 650-654: Remove the truncated duplicate withdraw stub and all
stale duplicate definitions of get_stream, set_delegate, revoke_delegate,
get_delegate, withdrawable_amount, calculate_protocol_fee,
default_stream_metrics, and assert_is_recipient_or_delegate in
contracts/payment-stream/src/lib.rs:650-654, retaining one valid definition of
each. Also remove the spliced set_protocol_fee_rate declaration from
set_delegate at contracts/payment-stream/src/lib.rs:1365-1378 and restore a
single coherent set_delegate body.
- Around line 326-337: Unify initialize and all related reads/writes on the
documented Symbol and tuple-key scheme instead of mixing DataKey variants. In
contracts/payment-stream/src/lib.rs lines 326-337, define/document the chosen
keys; update the AlreadyInitialized guard at lines 317-320 to read the Symbol
admin key, stream counter logic at lines 485-497 to use the Symbol stream_count
key, and protocol metrics writes at lines 530-546 and 1257-1264 to use the
Symbol protocol_metrics key. Update stream creation writes at lines 611-624 and
1083-1097 to store streams under raw stream_id and per-stream metrics under
(stream_id, "metrics").
- Around line 1257-1264: Update the protocol metrics read in resume_stream to
use the same DataKey::ProtocolMetrics key used by the subsequent write,
replacing the ad hoc Symbol::new lookup. Preserve the existing increment of
total_active_streams and TTL update so resumed streams restore the metric
correctly.

In `@contracts/payment-stream/src/test.rs`:
- Around line 2437-2444: Add an explicit assert_ne!(stream_id1, stream_id2)
after the two stream creations in both affected tests, using the existing
stream_id1 and stream_id2 symbols. Keep the cancellation and status assertions
unchanged so the tests fail immediately if stream IDs are not unique.
- Around line 2204-2227: Update withdraw to call acquire_stream_lock for the
stream before processing, and ensure the lock is released on every successful
exit. Strengthen test_reentrancy_guard_blocks_reentrant_withdraw to verify the
specific reentrancy error rather than ambiguous code `#17`, and add a
successful-withdraw assertion that the stream lock key is removed afterward.
- Around line 2223-2224: Import Symbol from soroban_sdk in the test module of
contracts/payment-stream/src/test.rs. Ensure the import covers the Symbol::new
usages for both "lock" and "g_lock" keys, while leaving the existing test logic
unchanged.

---

Outside diff comments:
In `@contracts/payment-stream/src/lib.rs`:
- Around line 1049-1060: Update withdraw to call acquire_stream_lock as the
first statement after assert_not_paused, before retrieving the stream or
performing authorization checks. Keep the existing release_stream_lock calls for
cleanup on failure and completion, so preset locks are rejected and reentrant
withdrawals are blocked.
- Around line 471-473: Define the missing Self::require_not_paused helper used
by create_stream, deposit, and withdraw, or replace those calls with the
existing assert_not_paused check. Preserve the contract’s current paused-state
validation behavior and remove the redundant or undefined helper usage so the
crate compiles.

---

Nitpick comments:
In `@contracts/campaign-funding/src/lib.rs`:
- Around line 1318-1352: Update test_refund_multiple_contributors_all_refunded
to use one failed campaign with contrib1, contrib2, and contrib3 contributing
below min_target; trigger expiry, refund each contributor, and assert all three
restored balances, removing the separate id2/contrib4 setup.
- Around line 597-611: Record the active fee rate in each Campaign when it is
created, and update calculate_fee to use that campaign-specific value instead of
the mutable global FeeRate. Ensure campaign creation reads the current
configured rate and persists it, while preserving existing fee validation and
claim behavior.
- Around line 1161-1177: Resolve the contradiction in
test_trigger_expiry_permissionless by removing the unnecessary
env.mock_all_auths() call while preserving setup_contract and create_campaign
behavior, or update the nearby comment to accurately describe the authentication
mocking. Ensure the test still verifies trigger_expiry can be called
permissionlessly.

In
`@contracts/campaign-funding/test_snapshots/tests/test_refund_multiple_contributors_all_refunded.1.json`:
- Around line 435-457: Update test_refund_multiple_contributors_all_refunded to
resolve the three-contributor campaign to Failed after expiry, invoke refund for
each contributor, and assert the snapshot records all three refunds;
alternatively, rename the test and snapshot to describe the current
single-contributor refund behavior.

In
`@contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_permissionless.1.json`:
- Around line 155-195: Rename test_trigger_expiry_with_zero_contributions_fails
to describe the campaign becoming Failed after expiry with zero contributions,
without implying that the function panics. Update any references to the test
name while preserving its existing assertions and behavior.

In `@contracts/payment-stream/src/lib.rs`:
- Around line 1512-1517: In withdrawable_amount, simplify the stream status
guard by removing the redundant explicit StreamStatus::Paused comparison and
retain only the StreamStatus::Active check, preserving the existing zero return
for every non-active stream.
- Around line 1603-1608: Update the admin lookup in both setter authorization
blocks to replace unwrap-based failure with panic_with_error! returning
Error::NotInitialized, matching emergency_pause. Preserve the existing
admin.require_auth() flow when an admin is present.
- Around line 606-609: In the deposit flow, update the state and balance before
performing the token transfer. Move the token::Client transfer call after the
existing balance write, preserving the current amount and recipient arguments
and the reentrancy guard.
🪄 Autofix (Beta)

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: dcab643e-33a3-458b-b918-7cf66925cba8

📥 Commits

Reviewing files that changed from the base of the PR and between 9bfc775 and d6b5c42.

📒 Files selected for processing (44)
  • contracts/Cargo.toml
  • contracts/campaign-funding/Cargo.toml
  • contracts/campaign-funding/src/lib.rs
  • contracts/campaign-funding/test_snapshots/tests/test_claim_funds_double_claim.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_claim_funds_on_active_campaign.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_claim_funds_on_failed_campaign.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_claim_funds_success.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_claim_funds_zero_fee.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_accumulates.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_after_deadline.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_auto_succeed_on_hard_cap.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_exceeds_hard_cap.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_multiple_contributors.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_success.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_to_nonexistent_campaign.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_zero_amount.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_deadline_in_past.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_ids_increment.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_min_target_exceeds_target.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_not_initialized.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_success.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_zero_min_target.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_zero_target.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_fee_calculation_precision.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_initialize_fee_too_high.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_initialize_success.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_initialize_twice_fails.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_double_refund_prevented.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_multiple_contributors_all_refunded.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_no_contribution.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_on_active_campaign.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_on_successful_campaign.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_success.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_set_fee_collector.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_set_fee_rate.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_set_fee_rate_too_high.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_already_resolved.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_before_deadline.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_permissionless.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_sets_failed_when_target_not_met.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_sets_successful_when_target_met.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_with_zero_contributions_fails.1.json
  • contracts/payment-stream/src/lib.rs
  • contracts/payment-stream/src/test.rs

Comment thread contracts/campaign-funding/src/lib.rs
Comment on lines +449 to +456
/// # Errors
/// * [`Error::CampaignNotSuccessful`] — campaign is not `Successful`.
/// * [`Error::AlreadyClaimed`] — funds were already claimed.
/// * [`Error::Unauthorized`] — caller is not the campaign creator.
pub fn claim_funds(env: Env, campaign_id: u64) {
let mut campaign = Self::load_campaign(&env, campaign_id);

campaign.creator.require_auth();

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the # Errors list for claim_funds.

The doc lists Error::Unauthorized, but the code never returns it. A non-creator caller fails inside creator.require_auth() with an authorization error, not contract error #3. Error::Unauthorized is unused in the whole file.

📝 Proposed doc fix
     /// # Errors
     /// * [`Error::CampaignNotSuccessful`] — campaign is not `Successful`.
     /// * [`Error::AlreadyClaimed`]        — funds were already claimed.
-    /// * [`Error::Unauthorized`]          — caller is not the campaign creator.
+    ///
+    /// A caller other than `creator` fails the `require_auth` check with an
+    /// authorization error, not a contract error.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// # Errors
/// * [`Error::CampaignNotSuccessful`] — campaign is not `Successful`.
/// * [`Error::AlreadyClaimed`] — funds were already claimed.
/// * [`Error::Unauthorized`] — caller is not the campaign creator.
pub fn claim_funds(env: Env, campaign_id: u64) {
let mut campaign = Self::load_campaign(&env, campaign_id);
campaign.creator.require_auth();
/// # Errors
/// * [`Error::CampaignNotSuccessful`] — campaign is not `Successful`.
/// * [`Error::AlreadyClaimed`] — funds were already claimed.
///
/// A caller other than `creator` fails the `require_auth` check with an
/// authorization error, not a contract error.
pub fn claim_funds(env: Env, campaign_id: u64) {
let mut campaign = Self::load_campaign(&env, campaign_id);
campaign.creator.require_auth();
🤖 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/campaign-funding/src/lib.rs` around lines 449 - 456, Update the
`claim_funds` documentation’s `# Errors` list to remove `Error::Unauthorized`,
since `campaign.creator.require_auth()` produces the authorization failure
directly rather than that contract error. Keep the remaining documented error
variants unchanged.

Comment thread contracts/campaign-funding/src/lib.rs
Comment thread contracts/payment-stream/src/lib.rs Outdated
Comment thread contracts/payment-stream/src/lib.rs Outdated
Comment thread contracts/payment-stream/src/lib.rs Outdated
Comment thread contracts/payment-stream/src/test.rs
Comment thread contracts/payment-stream/src/test.rs Outdated
Comment on lines +2223 to +2224
let lock_key = (stream_id, Symbol::new(&env, "lock"));
env.storage().temporary().set(&lock_key, &true);

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: check how Symbol reaches the payment-stream test module.
set -euo pipefail
rg -n 'use soroban_sdk' contracts/payment-stream/src

Repository: Fundable-Protocol/stellar_client_os

Length of output: 543


🏁 Script executed:

#!/bin/bash
# Inspect the payment-stream crate root and test module imports to see what Symbol can come from.
set -euo pipefail

printf '--- lib.rs ---\n'
cat -n contracts/payment-stream/src/lib.rs | sed -n '1,120p'

printf '\n--- test.rs imports and surrounding test section ---\n'
cat -n contracts/payment-stream/src/test.rs | sed -n '1,25p;2200,2235p'

printf '\n--- all Symbol definitions/usages in payment-stream/src ---\n'
rg -n '\b(Symbol|use\s+[^;]*Symbol[^;]*|\bSymbol\b)' contracts/payment-stream/src -M 2

Repository: Fundable-Protocol/stellar_client_os

Length of output: 264


🏁 Script executed:

#!/bin/bash
# Inspect the payment-stream crate root and test module imports to see how Symbol is available.
set -euo pipefail

printf '%s\n' '--- lib.rs ---'
cat -n contracts/payment-stream/src/lib.rs | sed -n '1,120p'

printf '%s\n' ''
printf '%s\n' '--- test.rs imports and surrounding test section ---'
cat -n contracts/payment-stream/src/test.rs | sed -n '1,25p;2200,2235p'

printf '%s\n' ''
printf '%s\n' '--- Symbol symbols in payment-stream/src ---'
rg -n '\b(Symbol|use\s+[^;]*Symbol[^;]*|\bSymbol\b)' contracts/payment-stream/src -M 2 || true

Repository: Fundable-Protocol/stellar_client_os

Length of output: 13457


🏁 Script executed:

#!/bin/bash
# Check whether lib.rs re-exports Symbol or imports it from soroban_sdk.
set -euo pipefail

printf '%s\n' '--- lib.rs use block ---'
sed -n '1,3p' contracts/payment-stream/src/lib.rs

printf '%s\n' ''
printf '%s\n' '--- lib.rs pub re-exports mentioning Symbol ---'
rg -n 'pub\s+use\b|Symbol' contracts/payment-stream/src/lib.rs || true

printf '%s\n' ''
printf '%s\n' '--- complete test module imports ---'
sed -n '1,8p' contracts/payment-stream/src/test.rs

printf '%s\n' ''
printf '%s\n' '--- all Symbol::new calls in tests under a module import ---'
python3 - <<'PY'
from pathlib import Path
p = Path("contracts/payment-stream/src/test.rs")
text = p.read_text()
lines = text.splitlines()
for i,l in enumerate(lines,1):
    if "Symbol::new" in l:
        before = "\n".join(f"{j}:{lines[j-1]}" for j in range(max(1,i-15), i))
        print(f"\n-- lines {max(1,i-15)}-{i} --")
        print(before)
PY

Repository: Fundable-Protocol/stellar_client_os

Length of output: 14500


🏁 Script executed:

#!/bin/bash
# Check Soroban SDK symbol availability in manifests/config without modifying or running repository code.
set -euo pipefail

printf '%s\n' '--- cargo manifests mentioning soroban-sdk ---'
rg -n 'soroban-sdk|soroban_env|symbol|Symbol' -S --glob 'Cargo.toml' --glob 'Cargo.lock' . || true

printf '%s\n' ''
printf '%s\n' '--- git diff summary and relevant symbol changes ---'
git diff --stat || true
git diff -- contracts/payment-stream/src/lib.rs contracts/payment-stream/src/test.rs | sed -n '1,220p' || true

Repository: Fundable-Protocol/stellar_client_os

Length of output: 1471


🌐 Web query:

Soroban SDK 25 Symbol import crate::Symbol from soroban_sdk

💡 Result:

To use the Symbol type in the Soroban SDK (including version 25), you import it directly from the soroban_sdk crate [1][2]: use soroban_sdk::Symbol; Usage Overview: 1. Symbol Basics: Symbol is a short string type (maximum 32 characters) with a limited character set (a-zA-Z0-9_) [1][3]. It is commonly used for symbolic identifiers like function names or field names [1]. 2. Creating Symbols: - For short strings (9 characters or less), you should use the symbol_short! macro [3][4]. This performs the conversion at compile time, which is more efficient and results in smaller Wasm binaries [3][5]. use soroban_sdk::symbol_short; let my_symbol = symbol_short!("transfer"); - For longer strings (up to 32 characters) or dynamic values, use Symbol::new [3][5]: use soroban_sdk::{Env, Symbol}; let my_symbol = Symbol::new(&env, "longsymbolname123"); 3. Recommendations: Avoid using deprecated methods like Symbol::short (except via the macro) [5]. Always prefer symbol_short! for constants to ensure compile-time evaluation [3][5].

Citations:


Import Symbol in the test module.

use super::* does not bring Symbol into scope because contracts/payment-stream/src/lib.rs does not import or re-export it. This makes Symbol::new(&env, "lock"), Symbol::new(&env, "g_lock"), and the related test uses unresolved; add Symbol to the soroban_sdk import path.

🤖 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 2223 - 2224, Import Symbol
from soroban_sdk in the test module of contracts/payment-stream/src/test.rs.
Ensure the import covers the Symbol::new usages for both "lock" and "g_lock"
keys, while leaving the existing test logic unchanged.

Comment thread contracts/payment-stream/src/test.rs Outdated
Comment on lines +2437 to +2444
token_admin.mint(&sender, &2000);
let stream_id1 = client.create_stream(&sender, &recipient, &token, &1000, &1000, &0, &100);
let stream_id2 = client.create_stream(&sender, &recipient, &token, &1000, &1000, &0, &100);
client.cancel_stream(&stream_id1);
client.cancel_stream(&stream_id2);
assert_eq!(client.get_stream(&stream_id1).status, StreamStatus::Canceled);
assert_eq!(client.get_stream(&stream_id2).status, StreamStatus::Canceled);
}

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

These tests assume two distinct stream ids.

Both tests create two streams and treat stream_id1 and stream_id2 as different. The counter defect in contracts/payment-stream/src/lib.rs at line 493 keeps the counter at 0, so both calls return 1, the second stream overwrites the first, and the isolation assertion no longer proves anything. Add an explicit assert_ne!(stream_id1, stream_id2); so the tests fail loudly if the counter regresses.

Also applies to: 2461-2471

🤖 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 2437 - 2444, Add an
explicit assert_ne!(stream_id1, stream_id2) after the two stream creations in
both affected tests, using the existing stream_id1 and stream_id2 symbols. Keep
the cancellation and status assertions unchanged so the tests fail immediately
if stream IDs are not unique.

@drips-wave

drips-wave Bot commented Aug 7, 2026

Copy link
Copy Markdown

@opratem 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

@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: 2

🧹 Nitpick comments (1)
contracts/campaign-funding/test_snapshots/tests/test_contribute_success.1.json (1)

157-165: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Remove obsolete protocol-22 snapshots.

The 13 protocol-22 files do not correspond to current test functions. The active snapshots use protocol 25 with soroban-sdk = 25.3.2. Remove the orphaned files instead of regenerating them.

🤖 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/campaign-funding/test_snapshots/tests/test_contribute_success.1.json`
around lines 157 - 165, Remove the obsolete orphaned protocol-22 snapshot files
from the campaign-funding test snapshots; retain the active protocol-25
snapshots, including test_contribute_success.1.json, unchanged.
🤖 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 `@apps/web/src/components/organisms/wallet-modal.tsx`:
- Around line 136-139: Move aria-modal="true" from the nested wrapper div to the
Radix DialogContent element, leaving the wrapper’s other attributes unchanged.

In
`@contracts/payment-stream/test_snapshots/test/test/test_deposit_blocked_during_dispute.1.json`:
- Around line 4-5: Restore ReentrancyGuard and its lock helpers in
contracts/payment-stream/src/lib.rs, applying them to public methods that
perform token transfers, and restore the removed reentrancy tests. Resolve the
dependency conflict using soroban-sdk = "=25.3.2", then run cargo test --all to
regenerate snapshots while preserving protocol_version 25 and mux_id 0.

---

Nitpick comments:
In
`@contracts/campaign-funding/test_snapshots/tests/test_contribute_success.1.json`:
- Around line 157-165: Remove the obsolete orphaned protocol-22 snapshot files
from the campaign-funding test snapshots; retain the active protocol-25
snapshots, including test_contribute_success.1.json, unchanged.
🪄 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: f749f423-85e0-456b-b4cd-63d0f6ad1283

📥 Commits

Reviewing files that changed from the base of the PR and between fdaf581 and 21e683d.

📒 Files selected for processing (85)
  • apps/web/src/components/map/FundableMapView.tsx
  • apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx
  • apps/web/src/components/organisms/wallet-modal.tsx
  • apps/web/src/providers/StellarWalletProvider.tsx
  • contracts/campaign-funding/src/lib.rs
  • contracts/campaign-funding/test_snapshots/tests/test_claim_funds_double_claim.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_claim_funds_on_active_campaign.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_claim_funds_on_failed_campaign.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_claim_funds_success.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_claim_funds_zero_fee.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_accumulates.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_after_deadline.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_auto_succeed_on_hard_cap.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_exceeds_hard_cap.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_multiple_contributors.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_success.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_to_nonexistent_campaign.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_zero_amount.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_deadline_in_past.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_ids_increment.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_min_target_exceeds_target.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_not_initialized.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_success.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_zero_min_target.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_zero_target.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_fee_calculation_precision.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_initialize_fee_too_high.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_initialize_success.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_initialize_twice_fails.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_double_refund_prevented.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_multiple_contributors_all_refunded.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_no_contribution.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_on_active_campaign.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_on_successful_campaign.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_success.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_set_fee_collector.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_set_fee_rate.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_set_fee_rate_too_high.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_already_resolved.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_before_deadline.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_permissionless.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_sets_failed_when_target_not_met.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_sets_successful_when_target_met.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_with_zero_contributions_fails.1.json
  • contracts/dispute-arbiter/src/lib.rs
  • contracts/dispute-arbiter/test_snapshots/test/test_cast_vote_approve_consensus.1.json
  • contracts/dispute-arbiter/test_snapshots/test/test_cast_vote_reject_consensus.1.json
  • contracts/dispute-arbiter/test_snapshots/test/test_create_dispute.1.json
  • contracts/dispute-arbiter/test_snapshots/test/test_double_initialize_fails.1.json
  • contracts/dispute-arbiter/test_snapshots/test/test_double_vote_rejected.1.json
  • contracts/dispute-arbiter/test_snapshots/test/test_evidence_request_consensus.1.json
  • contracts/dispute-arbiter/test_snapshots/test/test_force_resolve_timeout.1.json
  • contracts/dispute-arbiter/test_snapshots/test/test_initialize.1.json
  • contracts/dispute-arbiter/test_snapshots/test/test_non_assigned_arbiter_rejected.1.json
  • contracts/dispute-arbiter/test_snapshots/test/test_not_enough_arbiters_rejected.1.json
  • 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_delegation_updates_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_blocked_during_dispute.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_with_swap_exceeds_total.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_with_swap_inactive_stream_rejected.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_with_swap_provider_not_set.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_with_swap_respects_actual_amount_received.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_with_swap_same_asset_rejected.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_with_swap_slippage_exceeded.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_with_swap_slippage_exceeded_by_contract_check.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_with_swap_success.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_partial_refunds_residual_to_sender.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_execute_resolution_twice_fails.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_get_swap_provider_roundtrip.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_set_swap_provider_unauthorized.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_unauthorized_non_recipient_set_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_blocked_during_dispute.1.json
💤 Files with no reviewable changes (2)
  • contracts/campaign-funding/src/lib.rs
  • apps/web/src/providers/StellarWalletProvider.tsx

Comment thread apps/web/src/components/organisms/wallet-modal.tsx

@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: 1

🧹 Nitpick comments (3)
contracts/payment-stream/src/test.rs (1)

3666-3703: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Global-lock coverage misses emergency_pause and emergency_unpause.

These two tests cover set_protocol_fee_rate and set_fee_collector. emergency_pause at contracts/payment-stream/src/lib.rs line 402 and emergency_unpause at line 439 also acquire the global lock, and neither has a blocking test.

Add one test per function, following the same shape:

💚 Proposed test
#[test]
#[should_panic(expected = "Error(Contract, `#32`)")]
fn test_reentrancy_guard_blocks_global_emergency_pause() {
    let env = Env::default();
    env.mock_all_auths();
    let admin = Address::generate(&env);
    let fee_collector = Address::generate(&env);
    let contract_id = env.register(PaymentStreamContract, ());
    let client = PaymentStreamContractClient::new(&env, &contract_id);
    client.initialize(&admin, &fee_collector, &0);
    env.as_contract(&contract_id, || {
        let key = Symbol::new(&env, "g_lock");
        env.storage().temporary().set(&key, &true);
    });
    client.emergency_pause();
}
🤖 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 3666 - 3703, Add blocking
tests for both PaymentStreamContractClient::emergency_pause and
emergency_unpause in the test module, matching the existing global-lock tests:
initialize the contract, set temporary g_lock to true inside env.as_contract,
invoke the target method, and assert the expected Error(Contract, `#32`) panic.
contracts/payment-stream/src/lib.rs (2)

693-694: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

The per-stream lock in create_stream_internal cannot block anything.

Line 689 allocates stream_id from a counter that only increases. No other frame can already hold the lock for that id, so the check at line 693 always passes. The guard protects a resource that does not yet exist.

The reentrancy vector during the escrow transfer at line 751 is a malicious token calling back into create_stream (allocating further ids and re-reading StreamCount) or into deposit on a different stream. A lock keyed by the new id stops neither.

The cost is two extra temporary-storage writes per created stream. create_batch_streams multiplies that by up to 50; test_create_batch_streams_50 already needs disable_resource_limits().

Either drop the guard here, or guard the shared resource that is actually at risk — the StreamCount counter — with the global lock.

♻️ Option A: remove the ineffective per-id lock
         env.storage().instance().set(&DataKey::StreamCount, &stream_count);
 
-        Self::acquire_stream_lock(&env, stream_id);
-
         let current_time = env.ledger().timestamp();
-        Self::release_stream_lock(&env, stream_id);
         stream_id
     }
♻️ Option B: guard the counter with the global lock
-        Self::acquire_stream_lock(&env, stream_id);
+        Self::acquire_global_lock(&env);
-        Self::release_stream_lock(&env, stream_id);
+        Self::release_global_lock(&env);
         stream_id
     }

Option B serialises stream creation against itself and against fee administration. Confirm that create_batch_streams still works, because it calls create_stream_internal in a loop and each iteration must release the global lock before the next acquires it.

Also applies to: 754-756

🤖 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 693 - 694, Remove the
ineffective per-stream lock acquisition and release from create_stream_internal,
since the newly allocated stream_id cannot already be locked. Do not add a
replacement lock unless guarding the shared StreamCount counter globally; if
choosing that approach, ensure the lock is released before each
create_batch_streams iteration so batch creation remains functional.

25-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

DataKey::Lock is declared but never used.

The lock helpers at lines 319-331 key temporary storage with the tuple (stream_id, Symbol::new(env, "lock")). Nothing constructs DataKey::Lock(stream_id). This leaves two documented key schemes for one concept and adds an unused enum variant to the contract type surface.

Pick one scheme. DataKey::Lock(stream_id) is consistent with every other key in this contract.

♻️ Proposed change (helpers use the enum variant)
     fn acquire_stream_lock(env: &Env, stream_id: u64) {
-        let key = (stream_id, Symbol::new(env, "lock"));
+        let key = DataKey::Lock(stream_id);
         if env.storage().temporary().get::<_, bool>(&key).unwrap_or(false) {
             panic_with_error!(env, Error::ReentrancyGuard);
         }
         env.storage().temporary().set(&key, &true);
     }
 
     fn release_stream_lock(env: &Env, stream_id: u64) {
-        let key = (stream_id, Symbol::new(env, "lock"));
+        let key = DataKey::Lock(stream_id);
         env.storage().temporary().remove(&key);
     }

If you change the key, update the tests in contracts/payment-stream/src/test.rs and regenerate the affected snapshots, because the snapshots record the literal key shape.

🤖 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 25 - 26, Update the lock
helpers to use DataKey::Lock(stream_id) for temporary-storage access instead of
the tuple key, then remove the now-redundant Lock enum inconsistency. Adjust the
affected tests and regenerate snapshots so they reflect the enum-based key
shape.
🤖 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/test.rs`:
- Around line 3723-3726: Update both release assertions around the lock_key in
the affected tests to verify that temporary storage no longer contains the
entry, rather than interpreting a stored false value as absence. Replace the
boolean-value check with the storage API’s direct absence check while preserving
the existing lock_key construction and contract context.

---

Nitpick comments:
In `@contracts/payment-stream/src/lib.rs`:
- Around line 693-694: Remove the ineffective per-stream lock acquisition and
release from create_stream_internal, since the newly allocated stream_id cannot
already be locked. Do not add a replacement lock unless guarding the shared
StreamCount counter globally; if choosing that approach, ensure the lock is
released before each create_batch_streams iteration so batch creation remains
functional.
- Around line 25-26: Update the lock helpers to use DataKey::Lock(stream_id) for
temporary-storage access instead of the tuple key, then remove the now-redundant
Lock enum inconsistency. Adjust the affected tests and regenerate snapshots so
they reflect the enum-based key shape.

In `@contracts/payment-stream/src/test.rs`:
- Around line 3666-3703: Add blocking tests for both
PaymentStreamContractClient::emergency_pause and emergency_unpause in the test
module, matching the existing global-lock tests: initialize the contract, set
temporary g_lock to true inside env.as_contract, invoke the target method, and
assert the expected Error(Contract, `#32`) panic.
🪄 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: 47379e57-71af-4378-a6e8-a970b4132305

📥 Commits

Reviewing files that changed from the base of the PR and between 21e683d and 0c1d05f.

📒 Files selected for processing (16)
  • apps/web/src/components/organisms/wallet-modal.tsx
  • contracts/campaign-funding/src/lib.rs
  • contracts/payment-stream/src/lib.rs
  • contracts/payment-stream/src/test.rs
  • contracts/payment-stream/test_snapshots/test/test/test_independent_streams_use_separate_locks.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_reentrancy_guard_blocks_global_set_fee_collector.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_reentrancy_guard_blocks_global_set_fee_rate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_reentrancy_guard_blocks_reentrant_cancel.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_reentrancy_guard_blocks_reentrant_deposit.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_reentrancy_guard_blocks_reentrant_pause.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_reentrancy_guard_blocks_reentrant_resume.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_reentrancy_guard_blocks_reentrant_revoke_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_reentrancy_guard_blocks_reentrant_set_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_reentrancy_guard_blocks_reentrant_withdraw.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_reentrancy_lock_released_after_successful_cancel.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_reentrancy_lock_released_after_successful_withdraw.1.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/components/organisms/wallet-modal.tsx

Comment thread contracts/payment-stream/src/test.rs
@opratem

opratem commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

fixed now, can you review and merge the PR

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 Reentrancy Protection Guards Across All Payment Stream Functions

2 participants