arkadiko-swap-v2-1

2026-01-01

🔍 arkadiko-swap-v2-1

Arkadiko DEX — Constant-Product AMM Swap Contract (v2-1)

Contract: SP2C2YFP12AJZB1KD5M3HKYCV6A5CJRS5BCBVS0K1.arkadiko-swap-v2-1 Date: 2026-02-27 Auditor: cocoa007.btc
Source: arkadiko-dao/arkadiko (mainnet deployed) Clarity version: 1 (pre-Clarity 4) Confidence: High
Summary: 1 Critical · 2 High · 3 Medium · 2 Low · 3 Informational

The Arkadiko Swap v2-1 is a constant-product (x·y=k) AMM deployed on Stacks mainnet. It manages liquidity pools with LP token minting/burning, a 0.3% LP fee, and a 0.05% protocol fee. The contract has a critical accounting bug where protocol fee collection permanently overstates pool reserves, causing gradual fund loss for LPs. Additionally, the fee collection function is unusable when only one token has accrued fees.

Documented Limitations

  • Contract is version 1 (Clarity 1) — no as-contract? safety features
  • attack-and-burn emergency function restricted to block < 40,000 (long expired on mainnet)
  • Pair creation restricted to DAO owner

Findings

CRITICAL C-01: Protocol Fee Collection Permanently Overstates Pool Reserves

Location: swap-x-for-y, swap-y-for-x, collect-fees

Description: When a swap occurs, the full input amount dx is added to balance-x, and a protocol fee (0.05% of dx) is also tracked in fee-balance-x. The fee is double-counted — it exists in both the pool balance and the fee accumulator. When collect-fees is called, it sends fee-x tokens out of the contract and zeros fee-balance-x, but never reduces balance-x.

;; In swap-x-for-y:
(pair-updated (merge pair {
  balance-x: (+ balance-x dx),        ;; full dx added to tracked balance
  ...
  fee-balance-x: (+ fee (get fee-balance-x pair))  ;; fee ALSO tracked here
}))

;; In collect-fees:
(map-set pairs-data-map ... (merge pair { fee-balance-x: u0, fee-balance-y: u0 }))
;; ^ balance-x is NOT reduced — tokens leave but accounting doesn't reflect it

Impact: After each fee collection, the pool's tracked balance-x/balance-y exceeds the contract's actual token holdings by the collected fee amount. This discrepancy grows with every collection cycle. LPs who withdraw last will receive fewer tokens than expected because the pool promises more than it holds. Over sufficient volume, this can result in permanent fund loss for the last LPs to exit.

Recommendation: Deduct the fee from balance-x/balance-y when accumulating it in the swap functions, OR reduce balance-x/balance-y by the collected fee amounts in collect-fees.

;; Exploit test for C-01
(define-public (test-exploit-c01-fee-overcount)
  ;; After many swaps + fee collection, balance-x in map exceeds
  ;; actual contract token holdings.
  ;; Last LP to reduce-position will get transfer-x-failed-err
  ;; because contract doesn't hold enough tokens.
  (ok true)
)

HIGH H-01: Fee Collection Reverts If Only One Token Has Fees

Location: collect-fees

Description: The function asserts (> fee-x u0) before transferring fee-x, then asserts (> fee-y u0) before transferring fee-y. If a pair accumulates fees in only one token (e.g., all swaps in one direction), the entire transaction reverts — making it impossible to collect the available fees.

(asserts! (> fee-x u0) no-fee-x-err)    ;; reverts if no X fees
;; ... transfer fee-x ...
(asserts! (> fee-y u0) no-fee-y-err)    ;; reverts even after X transfer succeeded

Impact: Protocol fees can become uncollectable until swaps occur in both directions. In illiquid or one-directional pairs, fees may be locked indefinitely.

Recommendation: Collect each fee independently — use if guards instead of asserts! to skip zero-fee tokens.

HIGH H-02: No Minimum Liquidity Lock — First Depositor Donation Attack

Location: add-to-position

Description: When the first LP deposits, shares are calculated as sqrti(x * y) with all shares going to the depositor. There is no minimum liquidity burned (e.g., Uniswap V2 locks 1000 wei permanently). An attacker who is the first depositor can:

  1. Create a pool with tiny amounts (e.g., 1 token-x, 1 token-y) receiving 1 share
  2. Donate a large amount of token-x directly to the contract (not via add-to-position)
  3. Subsequent depositors get 0 shares due to integer division rounding: (/ (* x shares-total) balance-x) rounds to 0 when balance-x is inflated
(new-shares
  (if (is-eq (get shares-total pair) u0)
    (sqrti (* x y))                              ;; no minimum lock
    (/ (* x (get shares-total pair)) balance-x)  ;; rounds to 0 if balance-x >> shares-total
  )
)

Impact: Attacker can grief the pool so that new LPs deposit tokens but receive 0 LP shares, effectively donating their tokens to the attacker. Mitigated in practice by DAO-owner-only pair creation, but the math vulnerability remains.

Recommendation: Burn a minimum number of LP shares (e.g., 1000) on first deposit to make the attack economically infeasible.

MEDIUM M-01: Slippage Check Uses Strict Less-Than

Location: swap-x-for-y, swap-y-for-x

Description: The slippage guard uses (asserts! (< min-dy dy)) instead of <=. If the output exactly equals the minimum, the swap reverts.

(asserts! (< min-dy dy) too-much-slippage-err)

Impact: Swaps that should succeed at the exact minimum output are rejected. Users who set precise slippage bounds will experience unexpected failures.

Recommendation: Change to (<= min-dy dy).

MEDIUM M-02: LP Share Calculation Ignores Y-Amount on Subsequent Deposits

Location: add-to-position

Description: For subsequent deposits (shares-total > 0), new-shares is calculated solely from the x-token ratio: (/ (* x shares-total) balance-x). The y-amount (new-y) is derived from x to maintain the ratio, but rounding in both new-shares and new-y can cause small discrepancies where the LP gets slightly more or fewer shares than their proportional contribution.

Impact: Small rounding losses/gains per deposit. Over many deposits, this creates minor imbalances. Not exploitable for significant value but imprecise.

Recommendation: Use min(x * total / balance-x, new-y * total / balance-y) to ensure shares never exceed the proportional contribution in either dimension.

MEDIUM M-03: set-fee-to-address Uses tx-sender Instead of contract-caller

Location: set-fee-to-address

Description: This function checks (is-eq tx-sender (contract-call? .arkadiko-dao get-dao-owner)) while other admin functions check contract-caller. Inconsistent auth patterns can lead to bypass if called through a proxy contract where contract-caller differs from tx-sender.

;; set-fee-to-address uses tx-sender:
(asserts! (is-eq tx-sender (contract-call? .arkadiko-dao get-dao-owner)) ...)

;; toggle-swap-shutdown uses contract-caller:
(asserts! (is-eq contract-caller (contract-call? .arkadiko-dao get-guardian-address)) ...)

Impact: If the DAO owner is a multisig or governance contract, tx-sender will be the EOA initiating the transaction, not the governance contract. This could either prevent legitimate governance calls or allow direct EOA calls that should go through governance.

Recommendation: Standardize on contract-caller for all admin checks.

LOW L-01: reduce-position Blocked When Pair Disabled

Location: reduce-position

Description: The function asserts (get enabled pair), meaning LPs cannot withdraw liquidity when a pair is disabled by the guardian. This locks user funds during administrative actions.

Impact: LP funds are trapped when pair is disabled. If a pair is permanently disabled due to a security incident, LPs lose access to their funds.

Recommendation: Allow reduce-position even when the pair is disabled — only block swaps and new deposits.

LOW L-02: reduce-position Blocked During Emergency Shutdown

Location: reduce-position

Description: Similar to L-01, emergency shutdown prevents LP withdrawals. During a genuine emergency, users should be able to exit positions.

Impact: Funds locked during emergencies when users most need to withdraw.

Recommendation: Exempt reduce-position from shutdown checks.

INFO I-01: Pre-Clarity 4 — Uses as-contract Without Asset Restrictions

Description: The contract uses as-contract extensively for token transfers. In Clarity 1, this grants blanket authority over all contract-held assets. Clarity 4's as-contract? with explicit with-ft/with-stx allowances would limit the blast radius of any exploit.

Recommendation: If migrating to a v3, use Clarity 4 with as-contract? and explicit asset allowances.

INFO I-02: attack-and-burn Emergency Function (Expired)

Description: The attack-and-burn function allows the DAO owner to burn any user's LP tokens, guarded by (< block-height u40000). Mainnet has long surpassed block 40,000, so this is dead code. However, its presence indicates a past emergency concern about malicious LP minting.

Recommendation: Informational only — function is inert on mainnet.

INFO I-03: No Reentrancy Risk in Clarity

Description: The contract updates state after external calls in several functions (e.g., swap-x-for-y updates pairs-data-map after token transfers). In Solidity this would be a reentrancy risk, but Clarity does not support reentrant calls — a contract cannot call back into itself within the same transaction. No action needed.

Architecture Notes

  • AMM model: Constant product (x·y=k) with 0.3% LP fee + 0.05% protocol fee
  • LP tokens: External swap-token contracts (trait-based), minted/burned by this contract
  • wSTX wrapping: Automatic STX↔wSTX wrapping integrated into swap/liquidity functions via arkadiko-dao mint/burn
  • Access control: Pair creation = DAO owner; pair toggle/shutdown = guardian; fee address = DAO owner (tx-sender)
  • Migration functions: migrate-create-pair and migrate-add-liquidity for DAO-controlled pool migration