Skip to content

fix: repair Rust contract merge corruption, fix vesting/auth bugs#215

Merged
Austinaminu2 merged 2 commits into
mainfrom
fix/contract-merge-corruption-and-vesting-bug
Jul 8, 2026
Merged

fix: repair Rust contract merge corruption, fix vesting/auth bugs#215
Austinaminu2 merged 2 commits into
mainfrom
fix/contract-merge-corruption-and-vesting-bug

Conversation

@Austinaminu2

Copy link
Copy Markdown
Contributor

Summary

The streaming contract has had the same "both sides of a merge conflict kept" corruption pattern as the frontend (fixed in a separate commit), recurring across at least 5 separate merges going back weeks — deep enough that Contract CI and the Security Scan workflow have almost certainly never actually passed on main.

  • Restored a single, correct version of initialize, pause/unpause, withdraw, transfer_stream, bump_stream, create_stream, and the delegate functions (including a DataKey::Delegate enum variant that was referenced everywhere but never actually declared), plus the equivalent duplicate/orphaned test functions and wrong assertions in the test files.
  • Two real security bugs surfaced during the repair, both fixed:
    • migrate() fetched the admin from storage but never checked their authorization — anyone could call it to silently un-pause the contract, bypassing the admin-only pause/incident-response mechanism.
    • top_up()'s vesting bookkeeping was inconsistent with what withdraw/cancel actually use (unlocked_amount): funds added via top_up sat invisible until end_time instead of streaming out progressively, and a second top-up could retroactively re-apply the new rate across the entire elapsed time, overstating vesting. Fixed by re-anchoring the schedule off unlocked_amount on every top-up, with a regression test reproducing the original bug.
  • Unchecked += replaced with checked_add on withdrawn_amount; remove_from_index now extends TTL like its push_to_index counterpart.
  • Two false positives fixed in the repo's own Soroban security-pattern script (name() flagged as an unauthenticated write).
  • .lintstagedrc's cargo fmt --check fixed to rustfmt --check — the former silently failed on every staged .rs file since cargo fmt doesn't accept file-path arguments, meaning the pre-commit hook never actually checked Rust formatting. Ran cargo fmt across the crate accordingly.
  • Dropped the create-form UI fields for stream metadata (name/category/memo) — they built a local object that was never actually sent in the create_stream call, so they silently discarded whatever a user typed.

Test plan

  • cargo test — 121/121 passing
  • cargo clippy --all-targets -- -D warnings — clean
  • cargo audit — no vulnerabilities
  • cargo build --target wasm32v1-none --release — succeeds
  • node scripts/soroban-security-check.mjs — no HIGH severity findings
  • Frontend npm test / npm run build — unaffected, still passing

🤖 Generated with Claude Code

Austinaminu2 and others added 2 commits July 8, 2026 09:24
CI has been red on main: the test build failed on a duplicate formatUsd
export, eslint-plugin-security was incompatible with ESLint 10 (breaking
lint and the pre-commit hook for every contributor), and Next 16's
Turbopack default conflicts with Sentry's webpack config.

Several files also had unresolved merge conflicts silently merged in —
duplicate declarations, dead code after early returns, and in two cases
an entire abandoned feature branch pasted on top of the real
(already-split) page implementation. Restored the intended
page.tsx/dashboard.tsx and page.tsx/create-form.tsx server/client
splits, finished the create-form.tsx split that had been left as a
placeholder stub, and removed five orphaned files that never compiled
and were never imported anywhere.

tsc --noEmit was fully masked by `ignoreBuildErrors: true`; removing it
turned up real bugs now fixed: Sentry's beforeSend was silently
crashing (targeted the pre-v8 breadcrumb shape, so wallet addresses
were never actually being redacted), fee estimation read a `.cost`
field the current SDK type doesn't have, and a stream card had a <div>
nested inside a <p> causing a hydration warning. Removing
ignoreBuildErrors also means the build now fails loudly on this class
of bug instead of merging silently.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The streaming contract has had the same "both sides of a merge conflict
kept" corruption as the frontend, recurring across at least 5 separate
merges — deep enough that Contract CI and the security scan have likely
never actually passed on main. Restored a single, correct version of
initialize/pause/unpause/withdraw/transfer_stream/bump_stream/create_stream
and the delegate functions (including a missing DataKey::Delegate variant),
plus the equivalent duplicate/orphaned test functions and wrong assertions
in the test files.

Two real security bugs surfaced during the repair, both fixed:

- migrate() fetched the admin from storage but never checked their
  authorization, so anyone could call it to silently un-pause the
  contract, bypassing the admin-only pause/incident-response mechanism.

- top_up()'s vesting bookkeeping was inconsistent with what withdraw/
  cancel actually use (unlocked_amount): funds added via top_up sat
  invisible until end_time instead of streaming out progressively, and
  a second top-up could retroactively re-apply the new rate across the
  entire elapsed time, overstating vesting. Fixed by re-anchoring the
  schedule off unlocked_amount on every top-up, with a regression test
  reproducing the original bug.

Also: unchecked += replaced with checked_add on withdrawn_amount,
remove_from_index now extends TTL like its push_to_index counterpart,
two false positives fixed in the repo's own Soroban security-pattern
script (name() flagged as an unauthenticated write), and .lintstagedrc's
"cargo fmt --check" fixed to "rustfmt --check" — the former silently
failed on every staged .rs file since cargo fmt doesn't accept file-path
arguments, so the pre-commit hook never actually checked Rust formatting.
Ran cargo fmt across the crate accordingly.

Dropped the create-form UI fields for stream metadata (name/category/
memo) — they built a local object that was never actually sent in the
create_stream call, so they silently discarded whatever a user typed.

Verified: cargo test (121/121), cargo clippy --all-targets -D warnings,
cargo audit, cargo build --target wasm32v1-none --release, and the
frontend build/test suite all pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

🔐 Contract Security Scan

cargo audit
�[0m�[0m�[1m�[32m    Fetching�[0m advisory database from `https://github.com/RustSec/advisory-db.git`
�[0m�[0m�[1m�[32m      Loaded�[0m 1159 security advisories (from /home/runner/.cargo/advisory-db)
�[0m�[0m�[1m�[32m    Updating�[0m crates.io index
�[0m�[0m�[1m�[32m    Scanning�[0m Cargo.lock for vulnerabilities (193 crate dependencies)
�[0m�[0m�[1m�[33mCrate:    �[0m paste
�[0m�[0m�[1m�[33mVersion:  �[0m 1.0.15
�[0m�[0m�[1m�[33mWarning:  �[0m unmaintained
�[0m�[0m�[1m�[33mTitle:    �[0m paste - no longer maintained
�[0m�[0m�[1m�[33mDate:     �[0m 2024-10-07
�[0m�[0m�[1m�[33mID:       �[0m RUSTSEC-2024-0436
�[0m�[0m�[1m�[33mURL:      �[0m https://rustsec.org/advisories/RUSTSEC-2024-0436

�[0m�[0m�[1m�[33mwarning:�[0m 1 allowed warning found
Soroban pattern check
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:288 — panic! with string: panic!("already initialized");
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:306 — panic! with string: .unwrap_or_else(|| panic!("not initialized"));
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:326 — panic! with string: .unwrap_or_else(|| panic!("not initialized"));
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:347 — panic! with string: panic!("unauthorized");
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:359 — panic! with string: .unwrap_or_else(|| panic!("not initialized"));
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:375 — panic! with string: panic!("contract is paused");
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:404 — panic! with string: panic!("stream duration exceeds maximum");
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:416 — panic! with string: panic!("recipient cannot be the contract itself");
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:429 — panic! with string: panic!("stream amount too small for duration — rate would be 0");
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:539 — panic! with string: panic!("stream duration exceeds maximum");
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:551 — panic! with string: panic!("recipient cannot be the contract itself");
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:562 — panic! with string: panic!("stream amount too small for duration — rate would be 0");
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:750 — panic! with string: panic!("stream amount too small for remaining duration");
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:772 — panic! with string: panic!("stream amount too small for remaining duration");
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:1072 — panic! with string: Self::load_stream(&env, stream_id).unwrap_or_else(|_| panic!("stream not found"));
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:1076 — panic! with string: panic!("only sender or recipient may clean up a stream");
⚠️  LOW  [SOROBAN-004] contracts/streaming/src/lib.rs:1083 — panic! with string: panic!("stream must be cancelled or fully completed before cleanup");

17 LOW severity issue(s) found (warnings only).

@Austinaminu2 Austinaminu2 merged commit 628efc3 into main Jul 8, 2026
4 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant