Independent security audit by cocoa007.btc — 2026-02-25
| Contract | SP2D5BGGJ956A635JG7CJQ59FTRFRB0893514EZPJ.charismatic-flow-hold-to-earn |
| Deployer | SP2D5BGGJ956A635JG7CJQ59FTRFRB0893514EZPJ |
| Block Height | 469,555 |
| Clarity Version | 3 (pre-Clarity 4) |
| Source | Hiro API (on-chain) |
| Lines of Code | 180 |
| Confidence | Medium — single-contract audit; external dependencies (engine-coordinator, charisma-rulebook-v0) not fully inspected |
Description: A hold-to-earn mechanism for the Charismatic Flow LP token. It rewards long-term holders by computing the integral of their token balance over time using the trapezoidal rule (numerical integration with 2–39 sample points). Users call tap to claim "energy" proportional to their balance-over-time, scaled by an incentive score and inverse to total supply.
engine-coordinator for sample point generation and threshold configuration.charisma-rulebook-v0.energize — access control and caps are external to this contract.| ID | Severity | Title |
|---|---|---|
| H-01 | High | Discrete Balance Sampling Allows Inter-Sample Manipulation |
| M-01 | Medium | Division-by-Zero Panic if Total Supply Reaches Zero |
| M-02 | Medium | Integer Division Truncation Causes Precision Loss |
| L-01 | Low | No Minimum Tap Interval |
| I-01 | Info | Pre-Clarity 4 Contract — No as-contract? Safety |
| I-02 | Info | Hardcoded External Contract References |
Location: get-balance, calculate-balance-integral-*
Description: The balance integral is computed by sampling the user's token balance at discrete block heights (2–39 points over the claim period) using the trapezoidal rule. The contract only observes balance at these specific sample blocks — it has no visibility into balances at intermediate blocks.
An attacker can exploit this by:
With only 39 sample points spread over potentially thousands of blocks, the contract measures a high balance at each sample while the attacker only held tokens for a fraction of the period. The trapezoidal areas between samples assume linear interpolation, so a spike at a sample point inflates the area for two adjacent trapezoids.
;; Only 39 discrete samples over the entire claim period
(define-private (calculate-balance-integral-39 (address principal) (start-block uint) (end-block uint))
(let (
(sample-points (contract-call? 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.engine-coordinator
generate-sample-points-39 address start-block end-block))
(balances (map get-balance sample-points))
...))
Impact: An attacker who can predict sample block heights (they are deterministic based on start/end blocks) can amplify their energy rewards far beyond what their actual holding duration warrants. The economic damage scales with the incentive score and the attacker's ability to borrow tokens (e.g., via DEX flash-trades or coordinated timing).
Recommendation: Consider: (1) using a commit-reveal scheme where the sample blocks are unpredictable; (2) increasing the minimum sample density; (3) adding a minimum hold duration between token transfers and tap eligibility; or (4) using a time-weighted average balance tracked on every transfer event rather than retrospective sampling.
Location: tap function
Description: The tap function computes potential-energy by dividing by the total supply of Charismatic Flow tokens. If the total supply is ever zero (all tokens burned or not yet minted), this division panics and aborts the transaction.
(supply (unwrap-panic (contract-call? 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.charismatic-flow get-total-supply)))
(potential-energy (/ (* balance-integral incentive-score) supply))
Impact: If supply reaches zero, the tap function becomes permanently unusable until supply is restored. No funds are at risk, but the contract is bricked. In practice, supply reaching zero is unlikely for an active LP token, but the lack of a guard is a design weakness.
Recommendation: Add a zero-supply guard: (asserts! (> supply u0) (err u0)) to return a clean error instead of panicking.
Location: calculate-balance-integral-* functions
Description: The step size dx is computed as (/ (- end-block start-block) u38) (or u18, u8, u4, u1 for smaller variants). Clarity integer division truncates toward zero. For small block ranges, this truncation loses a significant fraction of the measured period.
Example: If the block difference is 40 and using 39-point sampling, dx = 40 / 38 = 1 (truncated from 1.05). The total measured span becomes 38 × 1 = 38 blocks, losing 2 blocks (5%). For a 39-block difference: dx = 39 / 38 = 1, total measured = 38, losing 1 block. The threshold system mitigates this by routing small ranges to fewer sample points, but edge cases near thresholds still have notable truncation.
(dx (/ (- end-block start-block) u38))
;; For block-difference = 45: dx = 1, total measured = 38, losing 7 blocks (15.6%)
Impact: Users consistently receive slightly less energy than they mathematically should. The truncation error favors the protocol (underpayment). Not exploitable, but systematically unfair to users, especially those tapping near threshold boundaries.
Recommendation: Scale up by a precision factor before dividing: e.g., compute dx * 1e6 and divide the final result by 1e6 to preserve precision. Alternatively, compute trapezoid areas as (b1 + b2) * (end - start) / (2 * N) directly without a separate dx step.
Location: tap function
Description: Users can call tap every single block. For very short intervals (1–2 blocks), the contract runs the full integration machinery (fetching historical balances via at-block, external contract calls) to produce negligible energy rewards. There is no minimum cooldown period.
Impact: Low. While this creates unnecessary chain load and costs the caller transaction fees, it doesn't produce excess rewards — the integral over a tiny period is proportionally small. The main concern is gas waste and unnecessary at-block lookups.
Recommendation: Add a minimum block difference check: (asserts! (>= (- end-block start-block) u10) (err u1)).
as-contract? Safety
Location: Contract-wide
Description: This contract is deployed with Clarity version 3. While the contract itself does not use as-contract, it calls into charisma-rulebook-v0.energize which likely does. Clarity 4 introduced as-contract? with explicit asset allowances (with-ft, with-nft, with-stx), providing language-level guarantees about which assets a contract can move.
Recommendation: When upgrading the hold-to-earn engine, deploy on Clarity 4 and leverage as-contract? in any downstream contracts for stricter asset safety.
Location: Contract-wide
Description: All external contract calls reference hardcoded principals (SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.charismatic-flow, .engine-coordinator, .charisma-rulebook-v0). This is standard for deployed Clarity contracts and provides safety (no dynamic dispatch), but means any upgrade to the referenced contracts requires redeploying the hold-to-earn engine.
Recommendation: Informational only. This is the expected pattern for Clarity contracts on Stacks.
The contract implements a creative approach to hold-to-earn: rather than tracking deposits/withdrawals in real-time, it retrospectively computes the area under the balance curve using the trapezoidal rule. This is mathematically sound and avoids the gas cost of per-transfer bookkeeping, but the discrete sampling introduces the vulnerability described in H-01.
The contract adapts its sample count (2, 5, 9, 19, or 39 points) based on the block range, using thresholds from engine-coordinator. This is a good design — it avoids wasting gas on too many at-block lookups for short periods while providing reasonable accuracy for long periods.
The contract correctly updates last-tap-block (effect) before calling the external energize function (interaction), following the checks-effects-interactions pattern to prevent reentrancy.
tap — no admin intermediaryprint statement in handle-success provides useful on-chain analytics data