SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.alex-vault
Audited: 2026-02-25 · Clarity version: pre-Clarity 4 · Confidence: Medium
The ALEX Vault is the central custodial contract for the ALEX DEX — the largest decentralized exchange on Stacks. It holds all pool liquidity and provides token transfer and flash loan capabilities to approved contracts.
Source: On-chain (Hiro Explorer) · Hiro API
Architecture: The vault implements an allowlist-based custody pattern. Three maps control access: approved-contracts (can transfer funds), approved-tokens (tokens the vault interacts with), and approved-flash-loan-users (contracts that can execute flash loans). The contract owner (set to .executor-dao at deployment) manages all three lists.
| Metric | Score | Weight | Weighted |
|---|---|---|---|
| Financial Risk | 3 — DeFi vault holding all DEX liquidity | 3 | 9 |
| Deployment Likelihood | 3 — Deployed on mainnet, actively used | 2 | 6 |
| Code Complexity | 2 — ~180 lines, trait interactions, flash loans | 2 | 4 |
| User Exposure | 3 — ALEX is largest Stacks DEX | 1.5 | 4.5 |
| Novelty | 2 — DEX vault with flash loans, new category | 1.5 | 3 |
Score: 2.65 / 3.0 — well above 1.8 threshold. No Clarity version penalty (deployed pre-Clarity 4 era).
Description: The contract provides add-approved-contract, add-approved-flash-loan-user, and add-approved-token functions that set map entries to true, but there are no corresponding removal functions. Once a principal is approved, it cannot be revoked.
(define-public (add-approved-contract (new-approved-contract principal))
(begin
(try! (check-is-owner))
(ok (map-set approved-contracts new-approved-contract true))
)
)
;; No remove-approved-contract exists
Impact: If an approved contract is compromised or found to be malicious, the owner cannot revoke its access to vault funds. The only remediation would be to migrate all liquidity to a new vault contract — a complex, high-risk operation for a live DEX. Given that the vault holds all ALEX pool liquidity, inability to quickly revoke a compromised contract's access is a significant risk.
Recommendation: Add removal functions for all three maps:
(define-public (revoke-approved-contract (contract principal))
(begin
(try! (check-is-owner))
(ok (map-set approved-contracts contract false))
)
)
as-contract Provides Blanket Asset AccessDescription: The contract uses the legacy as-contract primitive (pre-Clarity 4) which grants blanket authority over all vault-held assets within the enclosed expression. Every call to an external token contract via as-contract gives that token contract full access to move any asset the vault holds.
;; In transfer-ft — as-contract gives the token contract full vault authority
(as-contract (contract-call? token transfer-fixed amount tx-sender recipient none))
Impact: If a malicious or compromised token contract is added to approved-tokens, calling transfer-ft or flash-loan with that token could allow it to drain other vault-held assets during execution. The approved-tokens allowlist mitigates this somewhat, but combined with H-01 (no revocation), a bad token approval is permanent.
Recommendation: Migrate to Clarity 4's as-contract? with explicit asset allowances:
;; Clarity 4 — only allow transfer of the specific token
(as-contract? (contract-call? token transfer-fixed amount tx-sender recipient none)
(with-ft token amount)
)
Description: The flash loan function computes pre-bal before the loan, sends tokens out, calls the user's execute callback, then transfers amount-with-fee back from tx-sender. However, it never verifies the vault's post-execution balance is at least pre-bal + fee.
;; Pre-balance captured
(pre-bal (unwrap! (get-balance token) ERR-INVALID-BALANCE))
;; ... loan sent, callback executed, repayment transferred ...
;; No post-balance check!
(ok amount-with-fee)
Impact: The repayment relies solely on the transfer-fixed call succeeding. If the token contract has non-standard behavior (e.g., deflationary tokens with transfer fees, rebasing tokens), the vault could receive less than expected. Best practice for flash loans is to verify the invariant: post-bal >= pre-bal.
Recommendation: Add a post-balance assertion:
(let ((post-bal (unwrap! (get-balance token) ERR-INVALID-BALANCE)))
(asserts! (>= post-bal (+ pre-bal fee)) ERR-INVALID-BALANCE)
(ok amount-with-fee)
)
Description: The flash loan sends tokens to the borrower and then calls flash-loan-user.execute, which runs arbitrary code from an external contract. During this callback, the vault's token balance is depleted by the loan amount, but vault state otherwise appears normal.
;; Tokens sent out — vault balance reduced
(as-contract (try! (contract-call? token transfer-fixed amount tx-sender recipient none)))
;; Arbitrary code executes with vault in depleted state
(try! (contract-call? flash-loan-user execute token amount memo))
Impact: If the flash-loan-user contract is also an approved contract (or can trigger calls from one), it could call back into transfer-ft or flash-loan during execution, potentially extracting additional funds while the balance check uses stale data. The approved-flash-loan-users allowlist limits this to approved contracts, but the interaction between flash loan users and approved contracts should be carefully managed.
Recommendation: Add a re-entrancy guard (lock variable) around flash loan execution, or verify the post-balance invariant (see M-01) which would catch any re-entrancy-based extraction.
Description: The flash loan fee rate defaults to u0 (zero) and can be set to any uint value with no bounds checking.
(define-data-var flash-loan-fee-rate uint u0)
(define-public (set-flash-loan-fee-rate (fee uint))
(begin
(try! (check-is-owner))
(ok (var-set flash-loan-fee-rate fee))
)
)
Impact: With a zero fee, flash loans are free — enabling costless arbitrage, oracle manipulation, and governance attacks. While the owner (DAO) controls this, the default of zero means the vault is vulnerable until explicitly configured. An excessively high fee could also cause overflow in mul-up calculations.
Recommendation: Set a reasonable default fee (e.g., u50000 = 0.05%) and add bounds validation.
get-balance is Public Instead of Read-OnlyDescription: The get-balance function is defined as define-public but only reads data. It should be define-read-only if the underlying get-balance-fixed on the token contract is also read-only (which is standard for SIP-010).
Impact: Being public means callers must pay gas for what should be a free read operation. It also means it cannot be called from other define-read-only functions.
Recommendation: Change to define-read-only if the token's get-balance-fixed is read-only. Note: if any approved token has a public (not read-only) get-balance-fixed, this must remain public.
Description: The flash loan requires (> pre-bal amount) (strict greater than), meaning users cannot borrow the vault's full balance of a token. Borrowing pre-bal exactly will fail.
(asserts! (> pre-bal amount) ERR-INVALID-BALANCE)
Impact: Minor — prevents borrowing the last unit of a token. This may be intentional as a safety margin, but is inconsistent with standard flash loan implementations that use >=.
Recommendation: If intentional, document the 1-unit reserve requirement. If not, change to (>= pre-bal amount).
transfer-fixed / get-balance-fixed)Description: The contract uses transfer-fixed and get-balance-fixed instead of the standard SIP-010 transfer and get-balance. These are ALEX-specific fixed-point wrappers that normalize all tokens to 8 decimal places (ONE_8 = 10^8).
Impact: The vault can only work with tokens that implement the ALEX trait-sip-010.sip-010-trait interface (which extends standard SIP-010 with fixed-point variants). This is by design but limits interoperability.
Description: Good practice — the contract owner is immediately transferred to .executor-dao rather than remaining as the deployer's wallet. Initial approved contracts (.alex-reserve-pool, .fixed-weight-pool) and token (.age000-governance-token) are also set at deployment.
(set-contract-owner .executor-dao)
(map-set approved-contracts .alex-reserve-pool true)
(map-set approved-contracts .fixed-weight-pool true)
(map-set approved-tokens .age000-governance-token true)
Impact: Positive — DAO governance controls the vault rather than an individual. This significantly reduces single-point-of-failure risk.
The vault uses tx-sender (not contract-caller) to check authorization. This means approved contracts must use as-contract when calling vault functions, causing tx-sender to reflect the calling contract's principal. This is a deliberate design choice — it prevents unauthorized contracts from piggybacking on an approved contract's transaction flow. However, it creates a tight coupling: any refactoring of approved contracts must preserve the as-contract wrapping or vault calls will fail.
The flash loan implementation follows a standard borrow→callback→repay pattern. The borrower receives tokens, executes arbitrary logic, and must repay amount × (1 + fee_rate). The fee calculation uses mul-up (rounding up), which correctly favors the vault. However, the lack of post-balance verification (M-01) and re-entrancy protection (M-02) are deviations from best practice.
The contract uses three traits: ft-trait (fungible tokens), sft-trait (semi-fungible tokens), and flash-loan-user-trait. Trait-based dispatch is clean and extensible, but each trait interaction via as-contract grants blanket authority (H-02).
Medium — Source verified on-chain via Hiro API. All findings verified against the canonical deployed source. The contract is part of a larger multi-contract system (ALEX DEX), and interactions with executor-dao, alex-reserve-pool, and fixed-weight-pool were not fully traced. Cross-contract re-entrancy paths (M-02) could not be fully enumerated without auditing the full ALEX contract suite.