Skip to content

perf(stream): single-read vesting_summary view and cancel-boundary guards - #247

Merged
techAlhaji merged 2 commits into
SorobanForge:mainfrom
Ayomikun2005:fix-issues-78-79-81-83
Oct 20, 2026
Merged

techAlhaji merged 2 commits into
SorobanForge:mainfrom
Ayomikun2005:fix-issues-78-79-81-83

Conversation

@Ayomikun2005

@Ayomikun2005 Ayomikun2005 commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

This PR resolves the following assigned issues:

Rebased onto current main (fb1b9294, "refactor(types): unify stream status and progress derivation" #280).
The branch now carries exactly the two things this issue pair is about; everything that main had already shipped
in the meantime was dropped rather than re-fought (see "What the rebase removed").

Changes Made

#78 — cancel at schedule boundaries (contracts/stream/src/test.rs, +127, appended at end of file)

The suite cancelled at 30 % of the schedule and strictly before start (ts=100 vs start=500); the
now == start_time and now == end_time cases did not exist. Four tests pin the boundary semantics:

  • test_cancel_at_exactly_start_time_refunds_full_amount — at now == start_time the refund is the whole deposit
    (vested_amount is 0 at the start instant), claimable == 0, and a follow-up withdraw fails with
    NothingToWithdraw.
  • test_cancel_at_exactly_end_time_refunds_nothing — at now == end_time nothing is refunded (vested_amount
    returns the total once now >= end_time), the recipient withdraws the full amount and the contract drains to zero.
  • test_cancel_at_midpoint_refunds_total_minus_vested_exactly and
    test_cancel_at_midpoint_truncated_division_no_drift — a mid-schedule cancel refunds exactly total - vested,
    including a truncating schedule (1000 over 3 ledgers: refund 667 / vested 333) with
    refund + payout == locked total and no residue.

No off-by-one was found, so the linear path in the contract is unchanged; the behaviour is now locked in by tests.
Existing tests were not renumbered or reflowed — the block is appended after test_version_matches_shared_constant.

#83 — claimable/vested read amplification (contracts/stream/src/lib.rs, +29, of which 4 lines are a
#[cfg(test)] extern crate std; the cost test's println! needs in this no_std crate)

Neither view re-reads the stream entry redundantly internally: each touches exactly the instance + stream entry
and writes nothing (asserted). The genuine redundancy is across the pair — a dashboard that wants both numbers paid
two full entry reads and two invocations.

  • VestingSummary { vested, claimable } and the vesting_summary view compute both from a single storage read.
    It reuses vested_amount() and (vested - withdrawn).max(0), i.e. literally the bodies of claimable
    (contracts/stream/src/lib.rs:590-599) and vested (:602-609), so the two paths cannot drift apart.
  • test_claimable_vested_view_costs asserts the resource counts through Env::cost_estimate() (write entries,
    ledger-entry reads, and that the combined view is cheaper than the two views back to back), so a future regression
    that reads the stream entry twice fails.
  • test_claimable_vested_boundary_values (before start / exactly start / exactly cliff / last ledger before end /
    exactly end / far past end, plus withdrawn bookkeeping) and test_vesting_summary_matches_views (agreement with
    both dedicated views at every schedule point, partially withdrawn and cancelled streams) pin correctness.

What the rebase removed (and why)

  • security(contracts): test behaviour with a hostile token contract #79 (hostile-token tests) — dropped, no closes line. main merged fix(contracts): write state before payouts and test token reentrancy #254, which shipped the shared
    HostileTokenContract fixture (contracts/test-support/test_support.rs), 5 escrow + 2 splits + 5 stream reentry
    tests, and the SECURITY.md Threat Model + Token trust assumptions write-up. The pre-rebase branch had its own
    parallel HostileTokenHandle + reentry() + set_shortfall scaffolding and 9 test_hostile_token_* tests, and a
    76-line Known Limitations section. Carrying them would have meant a second hostile fixture in shared test support
    and a second, partly contradictory security doc, so SECURITY.md, contracts/escrow/src/test.rs,
    contracts/splits/src/test.rs and contracts/test-support/test_support.rs are all back to main's content
    byte-for-byte and left the diff. Honest residual, left on the table and not claimed here: the merged fixture always
    transfers the full amount, so the "create overstates the stream" / "withdraw underpays the recipient"
    short-transfer scenarios still have no coverage. That needs a fixture change, not a re-add of mine.
  • feat(stream): milestone-based (non-linear) vesting #81 (milestone vesting) — split out, no closes line. Feature-sized work does not belong on a test/perf
    branch. Tranche / MilestoneSet / DataKey::Milestones / create_milestones / release_milestones /
    cancel_milestones / get_milestones, the six new StreamError variants and their 7 tests are parked for a
    dedicated PR. This PR keeps only docs/milestone-vesting.md, the design note for that follow-up, because no other
    lane owns that path yet; the doc now states explicitly at the top that the entry points are not implemented on
    this branch, and its error-code numbers were corrected to the next free values after AmountOverflow = 19.
  • refactor(types): unify stream status and progress derivation #280 overlap check. main's unified status/progress derivation lives in packages/types/src/index.ts and
    apps/web; stream/src/lib.rs gained only create_streams, top_up, pause, resume, version and the
    fee/treasury/pause events. Nothing on the contract side already returned (vested, claimable) together
    (grep -n vesting_summary on main → no hits), so vesting_summary is not a duplicate and was kept.
  • Third-party feat(stream): add transfer_recipient to hand off an active stream #253 touches contracts/stream/src/{lib,test}.rs; this branch was resolved against main only and
    never rebased onto that PR. The overlap is disclosed rather than coordinated.

Test results

Gates run locally (this is a fork PR whose ci.yml runs wait for maintainer approval, so nothing here is a CI claim).
Head 1413bd40, base fb1b9294:

cargo fmt --manifest-path contracts/Cargo.toml --all -- --check   -> exit 0
cargo test -p stream --manifest-path contracts/Cargo.toml -- --nocapture
  -> test result: ok. 52 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
     (45 pre-existing on main + the 7 new tests above, each listed and run by name)
cargo clippy -p stream --manifest-path contracts/Cargo.toml --all-targets -- -D warnings
  -> exit 0
Measured in test_claimable_vested_view_costs: budget cpu claimable=67743 vested=67743 summary=69569

pnpm-side jobs were not re-run and are not claimed: the branch differs from main in no .ts/.tsx/.json
package file (git diff --name-only fb1b9294 HEAD covers only contracts/stream/** and
docs/milestone-vesting.md), so typecheck/lint/test/test:exports consume byte-identical inputs to main.

Snapshot note: the 7 goldens under contracts/stream/test_snapshots/test/ for the 7 new tests
(test_cancel_at_exactly_*, test_cancel_at_midpoint_*, test_claimable_vested_boundary_values,
test_claimable_vested_view_costs, test_vesting_summary_matches_views, each .1.json) ARE committed — they are
the first-ever golden for those test names and a cargo test run needs them. No pre-existing snapshot file is
changed: the 17 goldens that drift additively on every local run (paused_at/total_paused fields main never
regenerated after #182) and the 4 goldens missing on main for its own pause/event tests are left untouched,
outside this diff, exactly as inherited.

@drips-wave

drips-wave Bot commented Oct 17, 2026

Copy link
Copy Markdown

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

@vercel

vercel Bot commented Oct 17, 2026

Copy link
Copy Markdown

@Ayomikun2005 is attempting to deploy a commit to the techalhaji's projects Team on Vercel.

A member of the Team first needs to authorize it.

@techAlhaji

Copy link
Copy Markdown
Collaborator

resolve conflicts please.

@Ayomikun2005 Ayomikun2005 changed the title Fix issues: 78, 79, 81, 83 perf(stream): single-read vesting_summary view and cancel-boundary guards Oct 19, 2026
Issue SorobanForge#83: callers that need both `vested` and `claimable` paid two full
reads of the same stream entry. `vesting_summary` returns the pair from a
single read; `test_claimable_vested_view_costs` pins the resource counts so
a regression that re-reads the entry fails.

Issue SorobanForge#78: the cancel suite only exercised mid-schedule and pre-start
timestamps. Four tests pin `now == start_time`, `now == end_time` and the
midpoint boundaries, including a truncating schedule where refund + payout
must equal the locked total exactly.

Scope: rebased onto main, which already carries the hostile-token work
(SorobanForge#254) and the unified status/progress derivation (SorobanForge#280), so this branch
keeps only what those do not provide. docs/milestone-vesting.md is the
design note for issue SorobanForge#81, implemented in a follow-up PR.
@Ayomikun2005
Ayomikun2005 force-pushed the fix-issues-78-79-81-83 branch from 657b6b5 to 1413bd4 Compare October 20, 2026 14:37
@techAlhaji
techAlhaji merged commit 7a7fce6 into SorobanForge:main Oct 20, 2026
8 of 10 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

2 participants