Rewards distribution contract for stSTX and stSTXbtc liquid staking on Stacks
| Contract | SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.rewards-v8 |
| Protocol | Zest Protocol โ liquid staking (stSTX, stSTXbtc) on Stacks |
| Source | Verified on-chain via Hiro API |
| Lines of Code | ~373 |
| Clarity Version | Pre-Clarity 4 (uses legacy as-contract) |
| Audit Date | February 24, 2026 |
| Confidence | ๐ข HIGH โ self-contained rewards distributor; all findings verified against on-chain source |
This contract manages the collection and linear distribution of staking rewards for Zest Protocol's liquid staking tokens (stSTX for STX stacking rewards, stSTXbtc for sBTC stacking rewards). Rewards earned during PoX cycle X are distributed gradually throughout cycle X+1, released in equal portions across configurable intervals.
The contract is well-structured with proper DAO access control on administrative functions. The process-rewards function validates all trait-based contract parameters against both stored addresses and DAO protocol membership. However, there are concerns around sBTC reward loss when token supplies reach zero, and the pre-Clarity 4 as-contract usage grants blanket authority to commission trait contracts.
add-rewards / add-rewards-sbtc accept rewards from callers, split them into commission and protocol portions, and pay pool-owner commissions immediatelyprocess-rewards releases accumulated rewards proportionally based on elapsed intervals within the distribution cycle.dao check-is-protocolLocation: process-rewards โ sBTC distribution block
Description: When rewards-protocol-sbtc > 0, the contract calculates distribution between v1 and v2 token holders based on supply:
(let (
(ststxbtc-supply (unwrap-panic (contract-call? .ststxbtc-token get-total-supply)))
(ststxbtc-supply-v2 (unwrap-panic (contract-call? .ststxbtc-token-v2 get-total-supply)))
(total-supply (+ ststxbtc-supply ststxbtc-supply-v2))
(rewards-v1 (if (is-eq total-supply u0)
u0
(/ (* rewards-protocol-sbtc ststxbtc-supply) total-supply)
))
(rewards-v2 (if (is-eq total-supply u0)
u0
(- rewards-protocol-sbtc rewards-v1)
))
)
When total-supply = 0, both rewards-v1 and rewards-v2 are set to u0. Neither tracking contract receives any sBTC. However, the function still increments processed-protocol-sbtc by the full rewards-protocol-sbtc amount. The sBTC remains in the contract with no mechanism to distribute it to future holders.
Impact: If all stSTXbtc holders exit (both v1 and v2 supply reach zero) before rewards for a cycle are processed, those sBTC rewards are effectively locked. The get-sbtc admin function provides an escape hatch (DAO can recover the funds), but automatic distribution is permanently lost for those rewards. This is a realistic scenario during token migration periods.
Recommendation: Either revert when total-supply = 0 to defer processing until holders exist, or route orphaned rewards to the reserve:
(if (is-eq total-supply u0)
;; No holders โ send to reserve or revert
(try! (as-contract (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
transfer rewards-protocol-sbtc tx-sender reserve-address none)))
(begin
;; Normal v1/v2 split...
)
)
as-contract grants blanket authority to commission trait contractsLocation: process-rewards โ commission calls
Description: The contract calls commission contracts using as-contract:
(try! (as-contract (contract-call? commission-ststx-contract add-commission
staking-contract rewards-commission-stx)))
In pre-Clarity 4, as-contract gives the callee full authority to act as the rewards contract โ including transferring any STX or tokens held by the contract, not just the intended commission amount. While the commission contract must be both DAO-approved and match the stored address, a compromised or malicious commission contract could drain all funds.
Impact: If a malicious commission contract is approved by the DAO (via governance attack or compromised multisig), it could drain the entire STX and sBTC balance of the rewards contract during a process-rewards call. The dual validation (stored address + DAO check) provides good defense-in-depth, but the pre-Clarity 4 as-contract makes the blast radius larger than necessary.
Recommendation: Migrate to Clarity 4 and use as-contract? with explicit asset allowances:
;; Only allow transferring the exact commission amount
(try! (as-contract?
(with-stx rewards-commission-stx)
(contract-call? commission-ststx-contract add-commission
staking-contract rewards-commission-stx)))
Location: process-rewards โ sBTC v1/v2 split
Description: The v1/v2 reward split uses integer division for v1, then gives the remainder to v2:
(rewards-v1 (/ (* rewards-protocol-sbtc ststxbtc-supply) total-supply))
(rewards-v2 (- rewards-protocol-sbtc rewards-v1))
Integer division truncates, so v1 always rounds down and v2 always gets the rounding dust. Over many cycles, v2 holders systematically receive slightly more than their proportional share.
Impact: The per-cycle bias is at most 1 sat (the truncation remainder). Over hundreds of cycles, v2 holders accumulate at most a few hundred sats more than strictly proportional. This is economically negligible but represents a systematic unfairness.
Recommendation: This is a known pattern in integer-math DeFi. Alternating which side gets the remainder (e.g., based on cycle parity) would eliminate the systematic bias, though the economic impact doesn't warrant the added complexity.
add-rewards functionsLocation: add-rewards and add-rewards-sbtc
Description: Both reward addition functions accept any stx-amount / sbtc-amount, including zero. With small amounts, the commission calculation (/ (* amount commission) DENOMINATOR_BPS) can round to zero, meaning the full amount bypasses commission. A zero-amount call succeeds, emitting events and updating maps with +0 values.
Impact: No direct financial loss (caller pays their own funds), but zero/dust-amount calls pollute event logs and waste block space. With very small amounts, commission rounding to zero means pool owners receive nothing while the protocol gets the full amount.
Recommendation: Add a minimum amount check: (asserts! (> stx-amount u0) (err ERR_INVALID_AMOUNT)). Consider a higher minimum to ensure commission calculations are meaningful.
as-contract?Location: Entire contract
Description: The contract uses as-contract in multiple locations for STX transfers and sBTC transfers, as well as trait-based calls to commission and tracking contracts. Clarity 4 introduces as-contract? with explicit asset allowances (with-stx, with-ft) that limit the scope of authority granted to callees.
Recommendation: When deploying rewards-v9, target Clarity 4 and replace all as-contract calls with scoped as-contract?. This would mitigate M-02 at the language level.
Location: add-rewards-sbtc, get-sbtc
Description: The sBTC token address is hardcoded as 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token throughout the contract. If sBTC migrates to a new contract (e.g., a v2 token), a new rewards contract version would need to be deployed.
Recommendation: This is acceptable for the current version since sBTC is canonical. A future version could use a configurable token address (DAO-settable), but the additional complexity may not be warranted given that a rewards contract upgrade would likely be needed anyway for other reasons.
.dao check-is-protocol using contract-caller (not tx-sender), preventing direct-call bypassesprocess-rewards validates trait contracts against both stored addresses AND DAO protocol membership โ compromise of one is insufficientset-rewards-interval-length validates that the interval divides the cycle length evenly, preventing remainder-based edge casesget-stx and get-sbtc allow DAO to recover funds in case of bugs (mitigates M-01)add-rewards and process-rewards emit detailed print events for off-chain monitoring| Function | Access | Notes |
|---|---|---|
add-rewards | Permissionless | Caller pays own funds โ no restriction needed |
add-rewards-sbtc | Permissionless | Caller pays own funds โ no restriction needed |
process-rewards | Permissionless (DAO-gated traits) | Anyone can trigger, but all contracts must be DAO-approved + match stored addresses |
get-stx | DAO protocol only | Emergency fund recovery |
get-sbtc | DAO protocol only | Emergency fund recovery |
set-rewards-interval-length | DAO protocol only | Validates interval divides cycle length |
set-ststx-commission-contract | DAO protocol only | Correctly guarded |
set-ststxbtc-commission-contract | DAO protocol only | Correctly guarded |
set-staking-contract-address | DAO protocol only | Correctly guarded |
| Metric | Score | Weight | Weighted |
|---|---|---|---|
| Financial Risk | 3 โ DeFi staking rewards (holds STX + sBTC) | 3 | 9 |
| Deployment Likelihood | 3 โ deployed on mainnet | 2 | 6 |
| Code Complexity | 2 โ ~373 lines, multi-token distribution logic | 2 | 4 |
| User Exposure | 3 โ Zest is a prominent Stacks protocol | 1.5 | 4.5 |
| Novelty | 2 โ first rewards/vesting contract in collection | 1.5 | 3 |
| Total Score | 2.65 / 3.0 | ||
Clarity version penalty: -0.5 (pre-Clarity 4) โ Adjusted: 2.15 / 3.0 โ well above 1.8 threshold.