Token migration wrapper for ALEX staking — converts token-alex ↔ age000-governance-token and delegates to reserve pool
| Contract | SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.alex-staking-v2 |
| Protocol | ALEX — leading DEX on Stacks |
| Source | Verified on-chain via Hiro API (/v2/contracts/source) |
| Clarity Version | 2 (pre-Clarity 4 — uses as-contract without asset allowances) |
| Lines of Code | 104 |
| Confidence | 🟡 MEDIUM — thin wrapper; security depends heavily on external token contracts and reserve pool not reviewed here |
| Date | February 24, 2026 |
This contract is a thin migration wrapper in the ALEX staking system. It serves two purposes:
token-alex from the user, mints age000-governance-token to the user, then stakes via the reserve pool.age000-governance-token, and mints token-alex back to the user.The contract also implements the ALEX DAO extension and proposal traits, allowing it to be enabled/disabled via governance. Its execute function disables alex-staking (v1) and enables itself (v2).
.token-alex and 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-tokenalex-reserve-poolalex-reserve-pool) contains the actual staking logic, reward calculations, and state. This audit covers only the wrapper logic.| Metric | Score | Weight | Weighted |
|---|---|---|---|
| Financial risk | 3 (DeFi staking) | 3 | 9 |
| Deployment likelihood | 3 (deployed mainnet) | 2 | 6 |
| Code complexity | 1 (104 lines, thin wrapper) | 2 | 2 |
| User exposure | 3 (ALEX is largest DEX on Stacks) | 1.5 | 4.5 |
| Novelty | 1 (similar staking category) | 1.5 | 1.5 |
| Final Score | 2.3 / 3.0 (−0.5 Clarity version penalty → 1.8 ✅) | ||
Location: stake-tokens, claim-staking-reward
Description: The blocklist only applies to users who interact through this contract. Since the underlying alex-reserve-pool is a separate deployed contract, blocklisted users can bypass the restriction by calling the reserve pool directly (or through another authorized extension), completely defeating the blocklist mechanism.
(define-public (stake-tokens (amount-token uint) (lock-period uint))
(let ((sender tx-sender))
(asserts! (not (is-blocklisted-or-default sender)) ERR-NOT-AUTHORIZED)
;; ... user can skip this contract entirely
Impact: Blocklisted addresses (e.g., sanctioned entities) can still stake and claim rewards by interacting with the reserve pool directly, unless that contract also enforces its own blocklist.
Recommendation: The blocklist should be enforced at the reserve pool level, not just at this wrapper. Alternatively, the reserve pool should restrict which contracts can call its staking functions to ensure all interactions flow through blocklist-enforcing wrappers.
Location: claim-staking-reward, lines 67-75
Description: In claim-staking-reward, the reserve pool's claim-staking-reward is called inside the let binding — before the blocklist assertion. While the entire transaction reverts on assertion failure (so no funds are lost), this ordering wastes gas for blocklisted users and is inconsistent with stake-tokens which checks the blocklist first.
(define-public (claim-staking-reward (target-cycle uint))
(let (
(sender tx-sender)
;; Reserve pool claim executes FIRST
(claimed (try! (contract-call? ... claim-staking-reward ...)))
(total-claimed (+ (get to-return claimed) (get entitled-token claimed))))
;; Blocklist check happens AFTER
(asserts! (not (is-blocklisted-or-default sender)) ERR-NOT-AUTHORIZED)
...
Impact: No direct security impact (transaction reverts fully), but violates fail-fast principle and wastes gas for blocklisted users.
Recommendation: Move the blocklist check before the let block or make it the first binding that can short-circuit.
Location: claim-staking-reward-many, line 77
Description: The batch claim function uses map over claim-staking-reward and wraps the result in (ok ...). Since each individual claim returns a response type, failures for specific cycles are captured as (err ...) values within the list but the outer call still succeeds. Users may not realize some claims failed.
(define-public (claim-staking-reward-many (target-cycles (list 1000 uint)))
(ok (map claim-staking-reward target-cycles)))
Impact: Users calling this function may believe all claims succeeded when some silently failed. No funds are lost, but claimed amounts may be less than expected without clear indication.
Recommendation: Consider using fold to accumulate results and revert on any failure, or return a count of successful vs failed claims.
Location: stake-tokens, line 59
Description: The lock-period parameter is passed directly to the reserve pool without any local validation. While the reserve pool presumably validates this, defense-in-depth would catch invalid values earlier with a more descriptive error.
Impact: Minimal — depends on reserve pool validation. Users may get confusing error codes from the downstream contract.
Recommendation: Add a local assertion for valid lock period range (e.g., (> lock-period u0) and (<= lock-period u32)) before delegating.
Location: stake-tokens, claim-staking-reward
Description: The contract uses as-contract to call burn-fixed and mint-fixed on the token contracts. These calls succeed only if this contract is an authorized extension/minter for both .token-alex and age000-governance-token. If the contract loses extension status on either token (e.g., via a DAO governance action), all staking and claiming operations silently break.
Impact: If extension status is revoked for either token, users cannot stake or claim. Staked funds would need to be recovered through the reserve pool directly.
Recommendation: Document the extension dependency clearly. Consider adding a read-only function that checks extension status on both tokens so users/UIs can verify operability.
Description: The contract uses as-contract (Clarity 2) which gives blanket authority over all contract-held assets. Clarity 4 introduced as-contract? with explicit asset allowances (with-ft, with-nft, with-stx) that would restrict what the contract can do when acting as itself.
Recommendation: If redeploying, migrate to Clarity 4 and use as-contract? with explicit with-ft allowances for token-alex and age000-governance-token.
Description: The execute function disables v1 staking and enables v2. This is a standard DAO proposal pattern but it means the contract doubles as both a proposal and an extension. Once executed, the function has no further purpose but remains callable (though re-execution is harmless — it re-sets the same state).
(define-public (execute (sender principal))
(begin
(try! (contract-call? .executor-dao set-extensions
(list { extension: .alex-staking, enabled: false })))
(try! (contract-call? .executor-dao set-extensions
(list { extension: .alex-staking-v2, enabled: true })))
(ok true)))
Recommendation: No action needed — this is standard ALEX DAO convention.
Description: Functions like set-reward-cycle-length, set-token-halving-cycle, and set-coinbase-amount are DAO-gated and use as-contract to call the reserve pool. This means the DAO can modify fundamental staking parameters (cycle length, emission schedule) through this contract. These are powerful operations that affect all stakers.
Recommendation: These are standard DAO governance controls. Users should be aware that staking parameters can change via governance votes. No code change needed.
is-dao-or-extension before executing.stake-tokens checks (> amount-token u0) before proceeding.stake-tokens and claim-staking-reward is atomic within a single transaction — if any step fails, the entire transaction reverts.get-staked-many, get-staking-rewards-many) support up to 1000 cycles per call.This is a well-structured thin wrapper contract with minimal attack surface. The most significant finding is the blocklist bypass (H-01) — the blocklist is only enforced at this wrapper layer and can be circumvented by interacting with the reserve pool directly. The remaining findings are ordering issues and defense-in-depth improvements. The contract's security ultimately depends on the underlying alex-reserve-pool and token contracts, which were not in scope for this audit.