You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Security Review] StackStream v1.0.0-rc1 — Pre-Mainnet Community Review #1
StackStream is a Bitcoin-native payment streaming protocol on Stacks — the first of its kind on the chain. I'm preparing for mainnet deployment on April 17, 2026 as part of a Stacks Endowment grant ( 3 milestones).
I've completed a self-audit of all 8 public functions in stream-manager.clar and published the full review in the repository:
Independent review for StackStream Clarity contracts
Scope: stream-manager.clar, stream-factory.clar — authorization, arithmetic, state transitions, token handling, aligned with SECURITY_REVIEW.md v1.0.0-rc1.
Method: Manual trace of all public entry points; ran local simnet tests via npm install && npx vitest run (Vitest + Clarinet SDK).
Summary: I agree with the bulk of the author review: use of contract-caller for access control, token principal equality checks, ordering of SIP-010 transfers vs map updates on create/top-up, cancel fund split (recipient-amount + sender-refund vs escrow), pause/resume accounting, and factory track-stream requiring the caller to be the stream’s sender. I did not identify a critical vulnerability (e.g. arbitrary drain or principal spoofing) in this codebase under standard SIP-010 assumptions.
Finding non-zero rate-per-block invariant
The document states that rate-per-block cannot be zero given deposit > 0 and duration > 0. With Clarity integer division, rate-per-block = floor((deposit * PRECISION) / duration) is zero whenever deposit * PRECISION < duration. Effects: no accrual for the recipient; top-up-stream divides by rate and fails for those streams. Funds are recoverable by the sender via cancel-stream, so this is not a direct theft bug, but it is a real edge case and breaks an implied invariant.
Recommendation: Reject create-stream when (deposit-amount * PRECISION) < duration-blocks (equivalently require >= so the computed rate is at least 1). I implemented this guard and a regression test in my branch/PR: [link if applicable].
Residual notes: Operator must protect CONTRACT-OWNER; emergency pause scope matches code. Third-party token behavior remains a deployment/integration risk.
I performed a line-by-line review of both contracts against current Stacks/Clarity best practices (Clarity v3, Epoch 3.0) and the published self-audit. The overall design is solid — authorization, token conservation, and arithmetic safety are handled correctly. I found 0 Critical, 0 High, 0 Medium, 2 Low, and 3 Informational items. The self-audit's findings are confirmed and two additional items are reported below.
Positive Observations (Best Practices Confirmed)
Before the findings, credit where it's due — the following patterns align with current Stacks security best practices:
Pattern
Status
Notes
contract-caller for all authorization
✅ Correct
Prevents the well-documented tx-sender phishing attack where a malicious intermediary contract hijacks the persistence of tx-sender to bypass asserts! checks. Every mutating function in both contracts uses contract-caller, not tx-sender.
stacks-block-height (not block-height)
✅ Correct
The contracts correctly use stacks-block-height, which is the Clarity 3 replacement. block-height is deprecated in Clarity 3 (Epoch 3.0) and returns tenure-height semantics instead of true Stacks block height. Using the wrong keyword would break all time-based calculations post-Nakamoto.
Token substitution prevention
✅ Correct
Every function accepting a <sip-010-trait> parameter validates (is-eq token-principal (contract-of token)) against the stored principal. This prevents an attacker from passing a different token contract to drain the escrow.
try! on all token transfers
✅ Correct
All contract-call? token transfer invocations are wrapped in try!, ensuring any transfer failure atomically reverts the entire transaction. No partial state can be written on transfer failure.
State-after-transfer ordering
✅ Correct
In create-stream, the token escrow transfer executes before the stream data is written to maps. If the token transfer fails, no state is persisted. This is the right pattern even though Clarity has no reentrancy — it prevents state corruption from failed transfers.
Arithmetic overflow safety
✅ Correct
The most sensitive product deposit-amount * PRECISION (where PRECISION = 1e12) stays well within Clarity's uint ceiling of 2^128 − 1 ≈ 3.4 × 10^38. For Bitcoin-scale tokens (21M × 10^8 decimals), the max product is ≈ 2.1 × 10^27. No practical overflow risk.
Streamed amount clamp to deposit
✅ Correct
calculate-streamed-amount-internal ensures streamed ≤ deposit-amount, guarding against rounding-up edge cases.
Emergency pause scoping
✅ Correct
set-emergency-pause blocks only create-stream. Existing streams continue to accrue, recipients can claim, and senders can cancel. This is the correct narrow scope — a circuit breaker should stop new exposure without freezing user funds.
No reentrancy risk
✅ N/A
Clarity prohibits reentrancy at the language level. No mitigations required.
Response handling
✅ Correct
All public functions return proper (response) types. Clarity enforces this at the consensus level — contracts with unchecked responses cannot be deployed.
Findings
Finding 1 — Rounding Dust Permanently Locked in Contract
Dust locked: 1 unit (0.00000001 tokens / 1 satoshi)
For most real-world streams the dust is sub-satoshi and economically negligible. However, across thousands of streams, the cumulative locked dust grows.
Suggestion:
Option A (minimal): Document as a known limitation. Encourage senders to call cancel-stream after the stream fully elapses to recover residual dust.
Option B (code change): In claim, when effective-elapsed == duration (stream fully elapsed), set streamed = deposit-amount instead of using the rate calculation. This guarantees the last claimable unit reaches the recipient.
Option C: Add a sweep-dust admin function that can transfer residual amounts from fully-elapsed, non-cancelled streams back to the sender.
Finding 2 — Stream Count Limit is Lifetime, Not Concurrent
The MAX-STREAMS-PER-USER (100) limit is enforced by sender-stream-count and recipient-stream-count, which are incremented on stream creation but never decremented when a stream is cancelled or depleted:
;; Incremented in add-sender-stream:
(map-setsender-stream-count sender (+ current-count u1))
;; Never decremented anywhere in the contract
This means the 100-stream cap is a lifetime limit per principal, not a concurrent limit. A DAO that creates 100 streams (even if all are subsequently cancelled or depleted) can never create stream #101.
For individual users this is likely sufficient for v1. For high-frequency DAOs running recurring payroll streams (e.g., monthly contributor payments), this limit could be reached within ~8 years at one stream/month, or much sooner with weekly streams.
Suggestion:
Option A (minimal): Document the lifetime cap prominently. DAOs that anticipate high volume can rotate sender addresses.
Option B (code change): Decrement the count in cancel-stream and in claim when the stream transitions to STATUS-DEPLETED. This turns the cap into a concurrent-streams limit.
Finding 3 — Redundant Status Checks in pause-stream
Lines 386–387 check status != CANCELLED and status != DEPLETED after line 385 already asserts status == ACTIVE. If status is ACTIVE, it cannot simultaneously be CANCELLED or DEPLETED. The checks are unreachable but cost gas.
(asserts! (is-eq status STATUS-ACTIVE) ERR-STREAM-PAUSED)
(asserts! (not (is-eq status STATUS-CANCELLED)) ERR-STREAM-CANCELLED) ;; unreachable
(asserts! (not (is-eq status STATUS-DEPLETED)) ERR-STREAM-DEPLETED) ;; unreachable
I confirm the self-audit's assessment: no security impact. Removing them saves execution cost and improves code clarity (no pun intended), but is not urgent for mainnet.
If the underlying stream is later topped up via top-up-stream (which increases deposit-amount and end-block), the factory's analytics are never refreshed. Additionally, track-stream can only be called once per stream (due to the ERR-ALREADY-TRACKED guard), so there is no way to re-read the updated deposit.
Impact: Analytics only. No funds at risk. The stream-manager correct handles the top-up regardless of the factory's state.
Suggestion: Accept as a known limitation for v1 analytics, or add a refresh-stream-tracking read-and-update function to stream-factory.
Finding 5 — CONTRACT-OWNER Is Non-Transferable Single Key
CONTRACT-OWNER is a constant set at deploy time. It cannot be updated to a multisig, a DAO, or a successor key. If the deployer key is lost, emergency pause capability is permanently lost. If compromised, the attacker can toggle the pause (but cannot access escrowed funds).
I confirm the self-audit's assessment: acceptable for v1 given the narrow scope of the emergency function. The correct use of contract-caller (not tx-sender) in the check means the owner must call directly — a phishing contract cannot invoke set-emergency-pause on the owner's behalf by hijacking tx-sender.
Suggestion for v2: Consider a two-step ownership transfer pattern (propose-new-owner / accept-ownership) or multisig governance.
ACTIVE → DEPLETED:claim when new-withdrawn == deposit. Automatic status transition.
CANCELLED/DEPLETED: Terminal states. All mutating functions correctly reject these.
Claiming while paused: Correctly allowed. calculate-effective-elapsed freezes time at paused-at-block, so the recipient claims only what accrued before the pause. This prevents a sender from holding earned tokens hostage.
The 1e12 precision multiplier provides 12 additional decimal places of precision beyond the token's native decimals. For 8-decimal tokens (sBTC), this gives 20 effective decimal places in intermediate calculations — more than sufficient.
Pause Duration Accumulation
Across N pause/resume cycles:
total_paused = Σ (resume_block[i] - pause_block[i]) for i in 1..N
adjusted_elapsed = raw_elapsed - total_paused
Since stacks-block-height is monotonically increasing, each pause_duration = current_block - paused_at_block ≥ 0. Underflow is impossible. The accumulation is correct across arbitrary pause cycles.
Since rate = deposit × 1e12 / duration, substituting gives additional_blocks = top_up_amount × duration / deposit. The streaming rate (tokens per block) remains constant. The same rounding dust issue from Finding 1 applies to the extended portion.
Summary Table
#
Function
Severity
Description
Self-Audit
1
create-stream / rate calc
Low
Rounding dust permanently locked when deposit % duration ≠ 0
Not covered
2
create-stream / stream count
Low
100-stream limit is lifetime, not concurrent
Not covered
3
pause-stream
Low
Redundant status checks (no security impact)
✅ Confirmed
4
track-stream (factory)
Informational
DAO analytics stale after top-up
Not covered
5
set-emergency-pause
Informational
Single non-transferable owner key
✅ Confirmed
No critical, high, or medium severity issues found.
Conclusion
StackStream's contracts demonstrate strong security engineering for a v1 Clarity protocol. The authorization model correctly uses contract-caller throughout (the #1 Clarity security pattern), token conservation is maintained on every exit path, and the arithmetic handles precision responsibly. The use of Clarity 3 (stacks-block-height, Epoch 3.0) is correct for post-Nakamoto deployment.
The two new findings (rounding dust lock and lifetime stream cap) are both Low severity and neither blocks mainnet launch. They are worth documenting and addressing in a future version.
Good work preparing this for community review — the self-audit in SECURITY_REVIEW.md is thorough and accurate. I look forward to the mainnet launch.
Completed an independent review of stream-manager.clar and stream-factory.clar. No critical, high, or medium findings.
Function: claim
Severity: Low
Description: Authorization check (asserts! caller == recipient) was placed after the let block computed effective-elapsed, streamed, and claim-amount. A non-recipient caller triggers
unnecessary computation before being rejected.
Suggestion: Reorder asserts — authorization first, then state/token checks.
Function: top-up-stream
Severity: Low
Description: Same pattern — auth check placed after let computed additional-blocks, new-deposit, new-end-block.
Suggestion: Assert authorization before arithmetic bindings where possible.
Function: resume-stream
Severity: Low
Description: No end-block guard. A sender can resume a paused stream after its natural end time, leaving it in STATUS-ACTIVE indefinitely. No extra tokens accrue (elapsed is clamped to
duration), but the on-chain state is misleading until the recipient calls claim-all.
Suggestion: Add (asserts! (< current-block end-block) ERR-STREAM-ENDED).
Function: track-stream (stream-factory)
Severity: Informational
Description: total-deposited is recorded at tracking time and not updated when a stream is topped up. Analytics will undercount for topped-up streams.
Suggestion: Add a sync-stream-deposit function in v2.
The emergency pause only blocks new stream creation but still allows claims and cancels. Was this intentional design choice? Any scenario where you might want to also pause claims in a true emergency?
Independent review — novel finding in top-up-stream
Function: top-up-stream
Severity: Low
Description: When the top-up amount is smaller than the stream's per-block rate in token units, integer division truncates additional-blocks to zero. The sender's tokens are transferred to the contract escrow, but end-block is unchanged. Because rate-per-block is never modified, the maximum tokens the recipient can ever claim is bounded by the original stream's duration × rate — the topped-up tokens silently exceed that ceiling and are permanently unreachable by the recipient until the sender calls cancel-stream.
The condition is: amount × PRECISION < rate-per-block, equivalently amount < deposit / duration.
max streamed over lifetime = 100 × rate / PRECISION ≈ 1_000_000_000
5_000_000 tokens stuck in escrow, claimable only by sender via cancel-stream
This is the direct top-up-stream analogue of the zero-rate guard already present in create-stream (line 225):
;; top-up-stream — analogous guard MISSING
(additional-blocks (/ (* amount PRECISION) rate)) ;; silently truncates to 0
Tokens are not permanently lost — the sender can recover them via cancel-stream — but the function silently accepts a no-op deposit instead of rejecting it, which is a real footgun for any DAO building payroll tooling on top of this protocol.
Suggestion: Add the following assert in top-up-stream before the token transfer, mirroring the existing create-stream guard:
;; Prevent zero-extension top-ups: amount must extend the stream by at least 1 block
(asserts! (>= (* amount PRECISION) rate) ERR-INVALID-AMOUNT)
This rejects top-up amounts that are too small to extend the stream, forcing the caller to either top up with a meaningful amount or accept that very small additions aren't supported at the current rate.
Description: After the L-4 fix (which correctly blocks resume-stream past end-block), a new stuck-funds path exists. If a sender pauses a stream and never resumes or cancels it — and end-block passes — the unearned portion of the deposit is permanently locked in the contract with no recipient-side recovery path.
Recipient can call claim and receives the 500 tokens earned before block 150
The remaining 500 tokens are stuck forever: resume-stream fails with ERR-STREAM-ENDED, cancel-stream requires the sender, and set-emergency-pause only blocks new stream creation — it has no authority over existing stream escrows
The only recovery path (sender calling cancel-stream) belongs exclusively to the sender. No admin escape hatch exists for this scenario.
Suggestion: Several options exist at different trade-off points:
(Minimal change) Add an expire-stream public function callable by anyone once stacks-block-height > end-block and status == STATUS-PAUSED. It would settle identically to cancel-stream — earned tokens to recipient, remainder back to sender — without requiring the sender to act.
(Recipient-friendly) Allow the recipient to call a claim-all-on-expiry that releases the full remaining escrowed balance to themselves once stacks-block-height > end-block + grace-period and the stream is still paused.
(Design change) Extend end-block in resume-stream by the pause duration (new-end-block = end-block + pause-duration). This eliminates the scenario entirely: streams that were paused always get their full intended streaming window, so expiry-while-paused never leaves a shortfall.
Description:pause-stream does not validate that current-block >= start-block (i.e. that the stream has actually started). A sender can pause a stream before streaming begins. When they later call resume-stream, the full wall-clock pause duration — including time before start-block — is added to total-paused-duration. This pre-start time is meaningless (no tokens were accruing), but it still reduces the effective streaming window.
Completed an independent review of stream-manager.clar and stream-factory.clar. No critical or high findings. One medium and two low findings.
Function:cancel-stream
Severity: Low
Description:cancel-stream is callable by the sender even while the stream is STATUS-PAUSED. When paused, calculate-effective-elapsed correctly freezes elapsed time at paused-at-block.
This means a sender can: (1) create a stream, (2) immediately pause it before the recipient claims anything, (3) cancel it and recover nearly the entire deposit minus only the blocks elapsed before the pause. This is technically correct behaviour, but it makes streams a reversible commitment. The sender retains unilateral ability to claw back funds at any time by pausing then cancelling, giving recipients very weak guarantees.
Suggestion: This is a design decision, not a bug. But it should be prominently documented as a trust assumption: recipients must trust the sender not to pause-cancel. For v2, consider a non-cancellable flag or a minimum lock-in period. At minimum, document that recipient funds are only guaranteed once the stream elapses (or they claim).
Function:set-emergency-pause
Severity: Medium
Description:CONTRACT-OWNER is set to tx-sender at deploy time (the top-level define-constant). In Clarity, tx-sender at the top level of a contract is the deployer's address. If this contract is ever redeployed (e.g. upgraded), and deployed by a different address or a deployment script, CONTRACT-OWNER silently changes to whatever address ran the deploy transaction. There is no on-chain visibility into who owns the contract outside of reading the constant. Additionally, the current self-audit notes this is a single non-transferable key but what's missing is that there is no transfer-ownership function, making key rotation impossible without redeployment (which changes all contract addresses and breaks all existing integrations).
Suggestion: For mainnet, at minimum store the owner as a define-data-var with a transfer-ownership function guarded by the current owner. This enables key rotation without redeployment and is a standard pattern in Clarity admin contracts.
Function:top-up-stream
Severity: Low
Description:top-up-stream does not check status == ACTIVE explicitly — it only blocks CANCELLED and DEPLETED. This means a paused stream can be topped up, which extends its end-block. This is not necessarily wrong, but it creates an edge case: after the top-up, the extended end-block may now be far in the future, but the resume-stream guard (< current-block end-block) will pass — so a stream that was paused past its original end-block and would have been unresumable can now be resumed by topping up first. This is a sneaky state bypass.
Suggestion: Either explicitly allow pause-state top-ups (and document it) or add (asserts! (not (is-eq status STATUS-PAUSED)) ERR-STREAM-PAUSED). The former is more flexible but needs documentation. If allowed, consider whether the end-block extension should account for the ongoing pause duration.
Security Review - StackStream Contracts
Function: create-stream
Severity: Medium
Description: You validate the recipient address, but there's nothing stopping someone from sending tokens to a malicious or non-compliant contract.
Suggestion: Consider maintaining a list of approved SIP-010 compliant tokens, or at minimum document the risk so other developers are aware.
Function: top-up-stream
Severity: High
Description: The calculation (/ (* amount PRECISION) rate) will cause a division by zero if rate is ever zero. While the current create-stream logic makes a zero rate unlikely, there is no explicit guard in top-up-stream itself.
Suggestion: Add (asserts! (> rate u0) ERR-INVALID-DURATION) before the division.
Function: pause-stream
Severity: Informational
Description:
Error reuse: (asserts! (is-eq status STATUS_ACTIVE) ERR-STREAM-PAUSED)
This returns ERR-STREAM-PAUSED even if stream is cancelled or depleted, which can be misleading.
Suggestion:
Use more precise error codes for better debugging and UX.
Function: claim-all
Severity: Informational
Description: The function hardcodes the maximum uint value directly when calling claim. This large magic number reduces code readability and could become a maintenance issue if Clarity's uint behavior or the internal min logic changes in the future.
Suggestion: Define a clear constant such as (define-constant MAX-CLAIM-AMOUNT u340282366920938463463374607431768211455) at the top of the file for better maintainability.
Function: transfer-ownership
Severity: Low
Description: Ownership transfer (via updating the contract-owner data-var) is immediate and one-step. There is no confirmation step, so a typo or wrong address results in permanent loss of owner privileges with no recovery mechanism.
Suggestion: Implement a standard two-step ownership transfer: add propose-new-owner and accept-ownership functions.
General Finding – Event Logging
Severity: Informational
Description: Events are emitted consistently using (print {...}), but none of the events include a version field or schema identifier. Future contract upgrades could silently break off-chain indexers, dashboards, and analytics tools.
Suggestion: Add an event-version: "v1.0.0" (or similar) field to all printed events and document the current schema version in the README
Severity: Informational Description: The guard on line 227 (asserts! (>= (* deposit-amount PRECISION) duration-blocks) ERR-INVALID-DURATION) is well-designed to prevent zero rate-per-block scenarios. This correctly enforces the invariant that rate-per-block >= 1, preventing silent math failures. Suggestion: No changes needed. Document this constraint in README for clarity.
2. Function:create-stream
Severity: Informational Description: The contract uses contract-caller for authorization (line 231), which correctly captures the actual transaction sender for role-based checks. This is appropriate for access control decisions. Suggestion: Consider documenting the authorization model (contract-caller vs tx-sender rationale) in code comments.
3. Function:claim
Severity: Low Description: On line 323, the claim amount is silently clamped to available balance without warning: (claim-amount (if (> amount claimable) claimable amount)). While not a security issue, users requesting more than available may not realize they're getting less. Suggestion: Consider emitting an event with both requested and actual claim amounts for transparency.
4. Function:claim
Severity: Informational Description: Token conservation is correctly enforced via:
Claimable amount never exceeds streamed amount (line 320)
Streamed amount clamped to deposit amount in calculate-streamed-amount-internal (line 158-160)
All three exit paths (partial claim, full claim, cancellation) maintain the invariant: withdrawn-amount + unearned-refund = deposit-amount.
5. Function:pause-stream
Severity: Low Description: Redundant state checks on lines 391-393:
(asserts! (is-eq status STATUS-ACTIVE) ERR-STREAM-PAUSED)
(asserts! (not (is-eq status STATUS-CANCELLED)) ERR-STREAM-CANCELLED)
(asserts! (not (is-eq status STATUS-DEPLETED)) ERR-STREAM-DEPLETED)
The first assertion already requires status == STATUS-ACTIVE, so the second and third are unreachable. This has no security impact. Suggestion: Remove lines 392-393 for code clarity.
6. Function:resume-stream
Severity: Informational Description: The check on line 444 (asserts! (< current-block (get end-block stream-data)) ERR-STREAM-ENDED) correctly prevents resumes after the stream window closes, eliminating the "zombie ACTIVE state" scenario. Suggestion: No changes needed. This is correct-by-design.
State update correctly sets withdrawn-amount to streamed to prevent double-claiming (line 526)
8. Function:expire-stream
Severity: Informational Description: Permissionless expiry is correctly gated by three preconditions:
Stream must be paused (line 589)
Current block >= end-block (line 592)
Token must match (line 595)
This prevents abuse while enabling recovery from stuck-paused-and-expired scenarios.
9. Function:top-up-stream
Severity: Informational Description: Zero-extension prevention guard on line 675 (asserts! (>= (* amount PRECISION) rate) ERR-INVALID-AMOUNT) correctly prevents "silent no-op" scenarios where tokens transfer to escrow but end-block doesn't extend. This is well-designed defensive programming.
Numerator: effective-elapsed * rate-per-block can safely fit in uint (max ~2^128)
Division by PRECISION downscales to actual tokens ✓
Result clamped to deposit (line 158-160) prevents rounding artifacts
12. Function:set-emergency-pause
Severity: Informational Description: Emergency pause is correctly scoped: stops new stream creation (line 214) but doesn't freeze existing streams. Recipients can still claim, senders can still cancel. This is a reasonable circuit breaker design.
Stream-Factory Contract
13. Function:register-dao
Severity: Informational Description: Authorization is implicit via contract-caller (line 64). DAO admin = caller. This is correct for self-registration.
14. Function:track-stream
Severity: Informational Description: Cross-contract call to stream-manager (line 157) is correctly followed by re-verification that caller == stream.sender (line 160). This prevents a malicious actor from tracking someone else's stream.
15. Function:track-stream
Severity: Low Description: No idempotency guard for repeated tracking. If the same stream is tracked multiple times, total-deposited increments each time (line 171). However, the guard on line 163 (asserts! (is-none ...)) prevents this issue—attempting to re-track throws ERR-ALREADY-TRACKED.
16. Function:update-dao-name
Severity: Informational Description: Name registry collision detection on line 106 (asserts! (is-none (map-get? dao-names new-name))) correctly prevents name reuse. Old name properly deleted on line 109 before reassignment.
SUMMARY
Severity
Count
Critical
0
High
0
Medium
0
Low
2 (redundant checks in pause-stream; minor UX on claim silencing)
Informational
14
All findings are either no-ops or minor code-clarity improvements. The authorization model (contract-caller for all mutating calls), token conservation, arithmetic safety, and state transitions are solid. The contract demonstrates excellent defensive programming (zero-rate guards, zero-extension prevention, pause boundary checks).
Activity
Link community review issue #1 in SECURITY_REVIEW.md
Sobilo34 commentedon Apr 12, 2026
Independent review for StackStream Clarity contracts
Scope: stream-manager.clar, stream-factory.clar — authorization, arithmetic, state transitions, token handling, aligned with SECURITY_REVIEW.md v1.0.0-rc1.
Method: Manual trace of all public entry points; ran local simnet tests via npm install && npx vitest run (Vitest + Clarinet SDK).
Summary: I agree with the bulk of the author review: use of contract-caller for access control, token principal equality checks, ordering of SIP-010 transfers vs map updates on create/top-up, cancel fund split (recipient-amount + sender-refund vs escrow), pause/resume accounting, and factory track-stream requiring the caller to be the stream’s sender. I did not identify a critical vulnerability (e.g. arbitrary drain or principal spoofing) in this codebase under standard SIP-010 assumptions.
Finding non-zero rate-per-block invariant
The document states that rate-per-block cannot be zero given deposit > 0 and duration > 0. With Clarity integer division, rate-per-block = floor((deposit * PRECISION) / duration) is zero whenever deposit * PRECISION < duration. Effects: no accrual for the recipient; top-up-stream divides by rate and fails for those streams. Funds are recoverable by the sender via cancel-stream, so this is not a direct theft bug, but it is a real edge case and breaks an implied invariant.
Recommendation: Reject create-stream when (deposit-amount * PRECISION) < duration-blocks (equivalently require >= so the computed rate is at least 1). I implemented this guard and a regression test in my branch/PR: [link if applicable].
Residual notes: Operator must protect CONTRACT-OWNER; emergency pause scope matches code. Third-party token behavior remains a deployment/integration risk.
Akanimoh12 commentedon Apr 13, 2026
Reviewer: Independent Review by Akanmoh Johnson
Date: April 13, 2026
Scope:
stream-manager.clar(736 lines, 8 public functions),stream-factory.clar(218 lines, 4 public functions)References: SECURITY_REVIEW.md, Stacks Clarity Docs, Clarity tx-sender Security, Stacks Post-Conditions
Executive Summary
I performed a line-by-line review of both contracts against current Stacks/Clarity best practices (Clarity v3, Epoch 3.0) and the published self-audit. The overall design is solid — authorization, token conservation, and arithmetic safety are handled correctly. I found 0 Critical, 0 High, 0 Medium, 2 Low, and 3 Informational items. The self-audit's findings are confirmed and two additional items are reported below.
Positive Observations (Best Practices Confirmed)
Before the findings, credit where it's due — the following patterns align with current Stacks security best practices:
contract-callerfor all authorizationtx-senderto bypassasserts!checks. Every mutating function in both contracts usescontract-caller, nottx-sender.stacks-block-height(notblock-height)stacks-block-height, which is the Clarity 3 replacement.block-heightis deprecated in Clarity 3 (Epoch 3.0) and returnstenure-heightsemantics instead of true Stacks block height. Using the wrong keyword would break all time-based calculations post-Nakamoto.<sip-010-trait>parameter validates(is-eq token-principal (contract-of token))against the stored principal. This prevents an attacker from passing a different token contract to drain the escrow.try!on all token transferscontract-call? token transferinvocations are wrapped intry!, ensuring any transfer failure atomically reverts the entire transaction. No partial state can be written on transfer failure.create-stream, the token escrow transfer executes before the stream data is written to maps. If the token transfer fails, no state is persisted. This is the right pattern even though Clarity has no reentrancy — it prevents state corruption from failed transfers.deposit-amount * PRECISION(wherePRECISION = 1e12) stays well within Clarity'suintceiling of2^128 − 1 ≈ 3.4 × 10^38. For Bitcoin-scale tokens (21M × 10^8 decimals), the max product is ≈2.1 × 10^27. No practical overflow risk.calculate-streamed-amount-internalensuresstreamed ≤ deposit-amount, guarding against rounding-up edge cases.set-emergency-pauseblocks onlycreate-stream. Existing streams continue to accrue, recipients can claim, and senders can cancel. This is the correct narrow scope — a circuit breaker should stop new exposure without freezing user funds.(response)types. Clarity enforces this at the consensus level — contracts with unchecked responses cannot be deployed.Findings
Finding 1 — Rounding Dust Permanently Locked in Contract
Function:
create-stream/calculate-streamed-amount-internalSeverity: Low
Description:
When
deposit-amountis not perfectly divisible byduration-blocks, the rate calculation truncates:After the stream fully elapses, the maximum
streamedamount is:Due to integer division, this can be strictly less than
deposit-amount. The difference (dust) is permanently locked in the contract because:streamed(which is <deposit)STATUS-DEPLETED(sincewithdrawncan never equaldeposit)cancel-streamcan recover the dust (viasender-refund = deposit - streamed)Worked example:
deposit-amount = 10,000,000,000(100 tokens, 8 decimals)duration-blocks = 3rate = 10,000,000,000 × 1e12 / 3 = 3,333,333,333,333,333,333streamed = 3 × 3,333,333,333,333,333,333 / 1e12 = 9,999,999,999For most real-world streams the dust is sub-satoshi and economically negligible. However, across thousands of streams, the cumulative locked dust grows.
Suggestion:
Option A (minimal): Document as a known limitation. Encourage senders to call
cancel-streamafter the stream fully elapses to recover residual dust.Option B (code change): In
claim, wheneffective-elapsed == duration(stream fully elapsed), setstreamed = deposit-amountinstead of using the rate calculation. This guarantees the last claimable unit reaches the recipient.Option C: Add a
sweep-dustadmin function that can transfer residual amounts from fully-elapsed, non-cancelled streams back to the sender.Finding 2 — Stream Count Limit is Lifetime, Not Concurrent
Function:
create-stream/add-sender-stream/add-recipient-streamSeverity: Low
Description:
The
MAX-STREAMS-PER-USER(100) limit is enforced bysender-stream-countandrecipient-stream-count, which are incremented on stream creation but never decremented when a stream is cancelled or depleted:This means the 100-stream cap is a lifetime limit per principal, not a concurrent limit. A DAO that creates 100 streams (even if all are subsequently cancelled or depleted) can never create stream #101.
For individual users this is likely sufficient for v1. For high-frequency DAOs running recurring payroll streams (e.g., monthly contributor payments), this limit could be reached within ~8 years at one stream/month, or much sooner with weekly streams.
Suggestion:
Option A (minimal): Document the lifetime cap prominently. DAOs that anticipate high volume can rotate sender addresses.
Option B (code change): Decrement the count in
cancel-streamand inclaimwhen the stream transitions toSTATUS-DEPLETED. This turns the cap into a concurrent-streams limit.Finding 3 — Redundant Status Checks in
pause-streamFunction:
pause-streamSeverity: Low (confirming self-audit finding)
Description:
Lines 386–387 check
status != CANCELLEDandstatus != DEPLETEDafter line 385 already assertsstatus == ACTIVE. If status is ACTIVE, it cannot simultaneously be CANCELLED or DEPLETED. The checks are unreachable but cost gas.I confirm the self-audit's assessment: no security impact. Removing them saves execution cost and improves code clarity (no pun intended), but is not urgent for mainnet.
Finding 4 — DAO Analytics Stale After Top-Up
Function:
track-stream(stream-factory.clar)Severity: Informational
Description:
When a DAO calls
track-stream, thetotal-depositedanalytics field is updated from the stream'sdeposit-amountat that moment:If the underlying stream is later topped up via
top-up-stream(which increasesdeposit-amountandend-block), the factory's analytics are never refreshed. Additionally,track-streamcan only be called once per stream (due to theERR-ALREADY-TRACKEDguard), so there is no way to re-read the updated deposit.Impact: Analytics only. No funds at risk. The stream-manager correct handles the top-up regardless of the factory's state.
Suggestion: Accept as a known limitation for v1 analytics, or add a
refresh-stream-trackingread-and-update function to stream-factory.Finding 5 —
CONTRACT-OWNERIs Non-Transferable Single KeyFunction:
set-emergency-pauseSeverity: Informational (confirming self-audit finding)
Description:
CONTRACT-OWNERis a constant set at deploy time. It cannot be updated to a multisig, a DAO, or a successor key. If the deployer key is lost, emergency pause capability is permanently lost. If compromised, the attacker can toggle the pause (but cannot access escrowed funds).I confirm the self-audit's assessment: acceptable for v1 given the narrow scope of the emergency function. The correct use of
contract-caller(nottx-sender) in the check means the owner must call directly — a phishing contract cannot invokeset-emergency-pauseon the owner's behalf by hijackingtx-sender.Suggestion for v2: Consider a two-step ownership transfer pattern (
propose-new-owner/accept-ownership) or multisig governance.State Transition Analysis
I traced all possible state machine paths:
All transitions are correct:
pause-stream(sender only). Only active streams can be paused.resume-stream(sender only). Pause duration correctly accumulated intotal-paused-duration.cancel-stream(sender only). Token conservation holds:recipient-amount + sender-refund = deposit - withdrawn(escrow balance).claimwhennew-withdrawn == deposit. Automatic status transition.calculate-effective-elapsedfreezes time atpaused-at-block, so the recipient claims only what accrued before the pause. This prevents a sender from holding earned tokens hostage.Arithmetic Verification
Rate Calculation Precision
The 1e12 precision multiplier provides 12 additional decimal places of precision beyond the token's native decimals. For 8-decimal tokens (sBTC), this gives 20 effective decimal places in intermediate calculations — more than sufficient.
Pause Duration Accumulation
Across N pause/resume cycles:
Since
stacks-block-heightis monotonically increasing, eachpause_duration = current_block - paused_at_block ≥ 0. Underflow is impossible. The accumulation is correct across arbitrary pause cycles.Cancel Token Conservation
Top-Up Rate Preservation
Since
rate = deposit × 1e12 / duration, substituting givesadditional_blocks = top_up_amount × duration / deposit. The streaming rate (tokens per block) remains constant. The same rounding dust issue from Finding 1 applies to the extended portion.Summary Table
create-stream/ rate calcdeposit % duration ≠ 0create-stream/ stream countpause-streamtrack-stream(factory)set-emergency-pauseNo critical, high, or medium severity issues found.
Conclusion
StackStream's contracts demonstrate strong security engineering for a v1 Clarity protocol. The authorization model correctly uses
contract-callerthroughout (the #1 Clarity security pattern), token conservation is maintained on every exit path, and the arithmetic handles precision responsibly. The use of Clarity 3 (stacks-block-height, Epoch 3.0) is correct for post-Nakamoto deployment.The two new findings (rounding dust lock and lifetime stream cap) are both Low severity and neither blocks mainnet launch. They are worth documenting and addressing in a future version.
Good work preparing this for community review — the self-audit in SECURITY_REVIEW.md is thorough and accurate. I look forward to the mainnet launch.
security: apply community review fixes from issue jayteemoney#1
Marvy247 commentedon Apr 13, 2026
Completed an independent review of stream-manager.clar and stream-factory.clar. No critical, high, or medium findings.
Function: claim
Severity: Low
Description: Authorization check (asserts! caller == recipient) was placed after the let block computed effective-elapsed, streamed, and claim-amount. A non-recipient caller triggers
unnecessary computation before being rejected.
Suggestion: Reorder asserts — authorization first, then state/token checks.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Function: top-up-stream
Severity: Low
Description: Same pattern — auth check placed after let computed additional-blocks, new-deposit, new-end-block.
Suggestion: Assert authorization before arithmetic bindings where possible.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Function: resume-stream
Severity: Low
Description: No end-block guard. A sender can resume a paused stream after its natural end time, leaving it in STATUS-ACTIVE indefinitely. No extra tokens accrue (elapsed is clamped to
duration), but the on-chain state is misleading until the recipient calls claim-all.
Suggestion: Add (asserts! (< current-block end-block) ERR-STREAM-ENDED).
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Function: track-stream (stream-factory)
Severity: Informational
Description: total-deposited is recorded at tracking time and not updated when a stream is topped up. Analytics will undercount for topped-up streams.
Suggestion: Add a sync-stream-deposit function in v2.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Fixes for all three Low findings submitted in PR #3.
Ali6nXI commentedon Apr 13, 2026
The emergency pause only blocks new stream creation but still allows claims and cancels. Was this intentional design choice? Any scenario where you might want to also pause claims in a true emergency?
Godbrand0 commentedon Apr 13, 2026
Independent review — novel finding in top-up-stream
Function: top-up-stream
Severity: Low
Description: When the top-up amount is smaller than the stream's per-block rate in token units, integer division truncates additional-blocks to zero. The sender's tokens are transferred to the contract escrow, but end-block is unchanged. Because rate-per-block is never modified, the maximum tokens the recipient can ever claim is bounded by the original stream's duration × rate — the topped-up tokens silently exceed that ceiling and are permanently unreachable by the recipient until the sender calls cancel-stream.
The condition is: amount × PRECISION < rate-per-block, equivalently amount < deposit / duration.
Worked example:
deposit-amount = 1_000_000_000 (1,000 tokens, 6 decimals)
duration-blocks = 100
rate-per-block = 1,000,000,000 × 1e12 / 100 = 1e22
top-up amount = 5_000_000 (5 tokens)
additional-blocks = (5_000_000 × 1e12) / 1e22 = 5e18 / 1e22 = 0
Result:
This is the direct top-up-stream analogue of the zero-rate guard already present in create-stream (line 225):
;; create-stream — guard present
(asserts! (>= (* deposit-amount PRECISION) duration-blocks) ERR-INVALID-DURATION)
;; top-up-stream — analogous guard MISSING
(additional-blocks (/ (* amount PRECISION) rate)) ;; silently truncates to 0
Tokens are not permanently lost — the sender can recover them via cancel-stream — but the function silently accepts a no-op deposit instead of rejecting it, which is a real footgun for any DAO building payroll tooling on top of this protocol.
Suggestion: Add the following assert in top-up-stream before the token transfer, mirroring the existing create-stream guard:
;; Prevent zero-extension top-ups: amount must extend the stream by at least 1 block
(asserts! (>= (* amount PRECISION) rate) ERR-INVALID-AMOUNT)
This rejects top-up amounts that are too small to extend the stream, forcing the caller to either top up with a meaningful amount or accept that very small additions aren't supported at the current rate.
dannyy2000 commentedon Apr 13, 2026
Function:
pause-stream/ general designSeverity: Medium
Description: After the L-4 fix (which correctly blocks
resume-streampastend-block), a new stuck-funds path exists. If a sender pauses a stream and never resumes or cancels it — andend-blockpasses — the unearned portion of the deposit is permanently locked in the contract with no recipient-side recovery path.Concretely:
deposit = 1000,duration = 100 blocks,start = 100,end = 200claimand receives the 500 tokens earned before block 150resume-streamfails withERR-STREAM-ENDED,cancel-streamrequires the sender, andset-emergency-pauseonly blocks new stream creation — it has no authority over existing stream escrowsThe only recovery path (sender calling
cancel-stream) belongs exclusively to the sender. No admin escape hatch exists for this scenario.Suggestion: Several options exist at different trade-off points:
(Minimal change) Add an
expire-streampublic function callable by anyone oncestacks-block-height > end-blockandstatus == STATUS-PAUSED. It would settle identically tocancel-stream— earned tokens to recipient, remainder back to sender — without requiring the sender to act.(Recipient-friendly) Allow the recipient to call a
claim-all-on-expirythat releases the full remaining escrowed balance to themselves oncestacks-block-height > end-block + grace-periodand the stream is still paused.(Design change) Extend
end-blockinresume-streamby the pause duration (new-end-block = end-block + pause-duration). This eliminates the scenario entirely: streams that were paused always get their full intended streaming window, so expiry-while-paused never leaves a shortfall.dannyy2000 commentedon Apr 13, 2026
Function:
pause-streamSeverity: Low
Description:
pause-streamdoes not validate thatcurrent-block >= start-block(i.e. that the stream has actually started). A sender can pause a stream before streaming begins. When they later callresume-stream, the full wall-clock pause duration — including time beforestart-block— is added tototal-paused-duration. This pre-start time is meaningless (no tokens were accruing), but it still reduces the effective streaming window.Example:
start = 200,end = 300,deposit = 100,duration = 100pause-duration = 260 − 190 = 70is stored intotal-paused-durationraw-elapsed = 100,adjusted-elapsed = 100 − 70 = 30cancel-stream)The 10-block discrepancy comes from the pre-start pause time being overcounted.
Suggestion: Add a start-block guard in
pause-stream:This prevents pausing a stream that has not yet started and eliminates the overcounting.
zachyo commentedon Apr 13, 2026
Completed an independent review of stream-manager.clar and stream-factory.clar. No critical or high findings. One medium and two low findings.
Function:
cancel-streamSeverity: Low
Description:
cancel-streamis callable by the sender even while the stream isSTATUS-PAUSED. When paused,calculate-effective-elapsedcorrectly freezes elapsed time atpaused-at-block.This means a sender can: (1) create a stream, (2) immediately pause it before the recipient claims anything, (3) cancel it and recover nearly the entire deposit minus only the blocks elapsed before the pause. This is technically correct behaviour, but it makes streams a reversible commitment. The sender retains unilateral ability to claw back funds at any time by pausing then cancelling, giving recipients very weak guarantees.
Suggestion: This is a design decision, not a bug. But it should be prominently documented as a trust assumption: recipients must trust the sender not to pause-cancel. For v2, consider a
non-cancellableflag or a minimum lock-in period. At minimum, document that recipient funds are only guaranteed once the stream elapses (or they claim).Function:
set-emergency-pauseSeverity: Medium
Description:
CONTRACT-OWNERis set totx-senderat deploy time (the top-leveldefine-constant). In Clarity,tx-senderat the top level of a contract is the deployer's address. If this contract is ever redeployed (e.g. upgraded), and deployed by a different address or a deployment script,CONTRACT-OWNERsilently changes to whatever address ran the deploy transaction. There is no on-chain visibility into who owns the contract outside of reading the constant. Additionally, the current self-audit notes this is a single non-transferable key but what's missing is that there is notransfer-ownershipfunction, making key rotation impossible without redeployment (which changes all contract addresses and breaks all existing integrations).Suggestion: For mainnet, at minimum store the owner as a
define-data-varwith a transfer-ownership function guarded by the current owner. This enables key rotation without redeployment and is a standard pattern in Clarity admin contracts.Function:
top-up-streamSeverity: Low
Description:
top-up-streamdoes not checkstatus == ACTIVEexplicitly — it only blocksCANCELLEDandDEPLETED. This means a paused stream can be topped up, which extends itsend-block. This is not necessarily wrong, but it creates an edge case: after the top-up, the extendedend-blockmay now be far in the future, but theresume-streamguard(< current-block end-block)will pass — so a stream that was paused past its original end-block and would have been unresumable can now be resumed by topping up first. This is a sneaky state bypass.Suggestion: Either explicitly allow pause-state top-ups (and document it) or add
(asserts! (not (is-eq status STATUS-PAUSED)) ERR-STREAM-PAUSED). The former is more flexible but needs documentation. If allowed, consider whether the end-block extension should account for the ongoing pause duration.IdokoMarcelina commentedon Apr 14, 2026
Security Review - StackStream Contracts
Function: create-stream
Severity: Medium
Description: You validate the recipient address, but there's nothing stopping someone from sending tokens to a malicious or non-compliant contract.
Suggestion: Consider maintaining a list of approved SIP-010 compliant tokens, or at minimum document the risk so other developers are aware.
Function: top-up-stream
Severity: High
Description: The calculation (/ (* amount PRECISION) rate) will cause a division by zero if rate is ever zero. While the current create-stream logic makes a zero rate unlikely, there is no explicit guard in top-up-stream itself.
Suggestion: Add (asserts! (> rate u0) ERR-INVALID-DURATION) before the division.
Function: pause-stream
Severity: Informational
Description:
Error reuse: (asserts! (is-eq status STATUS_ACTIVE) ERR-STREAM-PAUSED)
This returns ERR-STREAM-PAUSED even if stream is cancelled or depleted, which can be misleading.
Suggestion:
Use more precise error codes for better debugging and UX.
Ryjen1 commentedon Apr 15, 2026
Function: claim-all
Severity: Informational
Description: The function hardcodes the maximum uint value directly when calling claim. This large magic number reduces code readability and could become a maintenance issue if Clarity's uint behavior or the internal min logic changes in the future.
Suggestion: Define a clear constant such as (define-constant MAX-CLAIM-AMOUNT u340282366920938463463374607431768211455) at the top of the file for better maintainability.
Function: transfer-ownership
Severity: Low
Description: Ownership transfer (via updating the contract-owner data-var) is immediate and one-step. There is no confirmation step, so a typo or wrong address results in permanent loss of owner privileges with no recovery mechanism.
Suggestion: Implement a standard two-step ownership transfer: add propose-new-owner and accept-ownership functions.
General Finding – Event Logging
Severity: Informational
Description: Events are emitted consistently using (print {...}), but none of the events include a version field or schema identifier. Future contract upgrades could silently break off-chain indexers, dashboards, and analytics tools.
Suggestion: Add an event-version: "v1.0.0" (or similar) field to all printed events and document the current schema version in the README
Jayy4rl commentedon Apr 15, 2026
Stream-Manager Contract
1. Function:
create-streamSeverity: Informational
Description: The guard on line 227
(asserts! (>= (* deposit-amount PRECISION) duration-blocks) ERR-INVALID-DURATION)is well-designed to prevent zero rate-per-block scenarios. This correctly enforces the invariant that rate-per-block >= 1, preventing silent math failures.Suggestion: No changes needed. Document this constraint in README for clarity.
2. Function:
create-streamSeverity: Informational
Description: The contract uses
contract-callerfor authorization (line 231), which correctly captures the actual transaction sender for role-based checks. This is appropriate for access control decisions.Suggestion: Consider documenting the authorization model (
contract-callervstx-senderrationale) in code comments.3. Function:
claimSeverity: Low
Description: On line 323, the claim amount is silently clamped to available balance without warning:
(claim-amount (if (> amount claimable) claimable amount)). While not a security issue, users requesting more than available may not realize they're getting less.Suggestion: Consider emitting an event with both requested and actual claim amounts for transparency.
4. Function:
claimSeverity: Informational
Description: Token conservation is correctly enforced via:
calculate-streamed-amount-internal(line 158-160)All three exit paths (partial claim, full claim, cancellation) maintain the invariant:
withdrawn-amount + unearned-refund = deposit-amount.5. Function:
pause-streamSeverity: Low
Description: Redundant state checks on lines 391-393:
The first assertion already requires status == STATUS-ACTIVE, so the second and third are unreachable. This has no security impact.
Suggestion: Remove lines 392-393 for code clarity.
6. Function:
resume-streamSeverity: Informational
Description: The check on line 444
(asserts! (< current-block (get end-block stream-data)) ERR-STREAM-ENDED)correctly prevents resumes after the stream window closes, eliminating the "zombie ACTIVE state" scenario.Suggestion: No changes needed. This is correct-by-design.
7. Function:
cancel-streamSeverity: Informational
Description: Token conservation verified:
8. Function:
expire-streamSeverity: Informational
Description: Permissionless expiry is correctly gated by three preconditions:
This prevents abuse while enabling recovery from stuck-paused-and-expired scenarios.
9. Function:
top-up-streamSeverity: Informational
Description: Zero-extension prevention guard on line 675
(asserts! (>= (* amount PRECISION) rate) ERR-INVALID-AMOUNT)correctly prevents "silent no-op" scenarios where tokens transfer to escrow but end-block doesn't extend. This is well-designed defensive programming.10. Function:
calculate-effective-elapsedSeverity: Informational
Description: Pause duration accumulation correctly handles:
No arithmetic overflow risks identified. All comparisons use
<=/>=appropriately.11. Function:
calculate-streamed-amount-internalSeverity: Informational
Description: High-precision math implemented correctly:
(effective-elapsed * rate-per-block) / PRECISION(line 155)effective-elapsed * rate-per-blockcan safely fit in uint (max ~2^128)12. Function:
set-emergency-pauseSeverity: Informational
Description: Emergency pause is correctly scoped: stops new stream creation (line 214) but doesn't freeze existing streams. Recipients can still claim, senders can still cancel. This is a reasonable circuit breaker design.
Stream-Factory Contract
13. Function:
register-daoSeverity: Informational
Description: Authorization is implicit via
contract-caller(line 64). DAO admin = caller. This is correct for self-registration.14. Function:
track-streamSeverity: Informational
Description: Cross-contract call to
stream-manager(line 157) is correctly followed by re-verification thatcaller == stream.sender(line 160). This prevents a malicious actor from tracking someone else's stream.15. Function:
track-streamSeverity: Low
Description: No idempotency guard for repeated tracking. If the same stream is tracked multiple times,
total-depositedincrements each time (line 171). However, the guard on line 163(asserts! (is-none ...))prevents this issue—attempting to re-track throwsERR-ALREADY-TRACKED.16. Function:
update-dao-nameSeverity: Informational
Description: Name registry collision detection on line 106
(asserts! (is-none (map-get? dao-names new-name)))correctly prevents name reuse. Old name properly deleted on line 109 before reassignment.SUMMARY
pause-stream; minor UX on claim silencing)All findings are either no-ops or minor code-clarity improvements. The authorization model (
contract-callerfor all mutating calls), token conservation, arithmetic safety, and state transitions are solid. The contract demonstrates excellent defensive programming (zero-rate guards, zero-extension prevention, pause boundary checks).