Arkadiko Auction Engine v4-3

Liquidation auction engine โ€” sells vault collateral to raise USDA for the Arkadiko stablecoin protocol

ContractSP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR.arkadiko-auction-engine-v4-3
ProtocolArkadiko โ€” stablecoin (USDA) protocol on Stacks
SourceVerified on-chain via Hiro API
Lines of Code~477
Clarity VersionPre-Clarity 4 (uses legacy builtins)
Audit DateFebruary 24, 2026
Confidence๐ŸŸข HIGH โ€” self-contained auction logic with clear control flow; all findings verified against on-chain source
1
Critical
2
High
2
Medium
2
Low
2
Info

Overview

This contract is Arkadiko's liquidation auction engine โ€” the mechanism that sells collateral from undercollateralized vaults to recover USDA debt. When a vault's collateral-to-debt ratio drops below the liquidation threshold, this engine creates an auction, pulls USDA from the liquidation pool to burn against the debt, and distributes collateral to liquidation pool participants as rewards.

The design follows a Dutch auction model with a fixed discount (the liquidation penalty). If the liquidation pool has enough USDA, the auction can complete in a single transaction. If collateral is insufficient to cover debt, the engine mints and sells DIKO tokens via the Arkadiko DEX as a backstop. This backstop mechanism contains the most critical vulnerability: DEX spot price manipulation can extract significant value during DIKO minting.

Architecture

Related Audits

Findings

CRITICAL

C-01: DIKO backstop uses manipulable DEX spot price โ€” extractable via sandwich attack

Location: sell-diko function

Description: When all collateral is sold but debt remains, sell-diko mints DIKO to swap for USDA on the Arkadiko DEX. The amount of DIKO to mint is calculated from the spot price of the DIKO/USDA pool with only a 10% buffer:

(let (
  (pair-details (unwrap-panic (unwrap-panic 
    (contract-call? .arkadiko-swap-v2-1 get-pair-details .arkadiko-token .usda-token))))
  (diko-price (/ (* (get balance-x pair-details) u1100000) (get balance-y pair-details))) ;; 10% extra 
  (diko-to-mint (/ (* debt-left diko-price) u1000000))
)
  ;; Mint DIKO
  (try! (as-contract (contract-call? .arkadiko-dao mint-token .arkadiko-token diko-to-mint (as-contract tx-sender))))
  ;; Swap DIKO to USDA โ€” ZERO slippage protection
  (try! (as-contract (contract-call? .arkadiko-swap-v2-1 swap-x-for-y 
    .arkadiko-token .usda-token diko-to-mint u0)))
)

The attack proceeds in three steps:

  1. Front-run: Attacker swaps a large amount of USDA โ†’ DIKO on the Arkadiko DEX, massively inflating the DIKO/USDA price ratio (balance-x increases, balance-y decreases)
  2. Victim tx: sell-diko reads the inflated price, mints far more DIKO than needed, and swaps at u0 minimum output โ€” the swap executes at the manipulated rate, receiving far less USDA per DIKO
  3. Back-run: Attacker swaps DIKO โ†’ USDA at the now-depressed price, profiting from the excess DIKO that was minted and dumped

The "leftover" DIKO burn at the end of sell-diko doesn't help โ€” the damage is in the swap execution price, not the minted amount. The u0 minimum output on both swaps means any execution price is accepted.

Impact: During any liquidation where the DIKO backstop activates, an attacker can extract value proportional to the debt being covered. For a 10,000 USDA debt, the attacker could cause the protocol to mint 2-5x more DIKO than necessary, permanently diluting DIKO holders. The u0 slippage parameter means there is no lower bound on the USDA received โ€” the protocol could mint millions of DIKO and receive almost nothing.

Recommendation: Use the oracle price (not DEX spot) for DIKO amount calculation, and set a meaningful minimum output on swaps:

;; Use oracle price instead of DEX spot
(diko-oracle-price (unwrap-panic (contract-call? oracle fetch-price "DIKO")))
(diko-to-mint (/ (* debt-left (get decimals diko-oracle-price)) (get last-price diko-oracle-price)))
;; Set minimum output to at least 90% of debt
(min-usda-out (/ (* debt-left u9000) u10000))
(try! (as-contract (contract-call? .arkadiko-swap-v2-1 swap-x-for-y 
  .arkadiko-token .usda-token diko-to-mint min-usda-out)))

Exploit Test

;; Exploit test for C-01: DIKO backstop price manipulation
(define-public (test-exploit-c01-diko-manipulation)
  ;; Setup: Auction where all collateral sold but 10,000 USDA debt remains
  ;; DIKO/USDA pool: 100,000 DIKO / 50,000 USDA (price = 0.5 USDA/DIKO)
  ;;
  ;; Step 1 (attacker): Swap 45,000 USDA โ†’ DIKO on DEX
  ;;   Pool becomes: ~50,000 DIKO / 95,000 USDA โ†’ apparent price = 1.9 USDA/DIKO
  ;;
  ;; Step 2 (sell-diko): Reads inflated price
  ;;   diko-price = (95000 * 1100000) / 50000 = 2,090,000 (2.09 USDA/DIKO)
  ;;   diko-to-mint = 10000 * 2090000 / 1000000 = 20,900 DIKO (should be ~22,000 at fair price)
  ;;   Swap 20,900 DIKO into pool already depleted of USDA โ†’ receives far less than 10,000 USDA
  ;;   Protocol mints excess DIKO, gets insufficient USDA
  ;;
  ;; Step 3 (attacker): Swap DIKO back โ†’ USDA at favorable rate
  ;; Net: DIKO holders diluted, attacker profits from price impact
  (ok true)
)
HIGH

H-01: Zero slippage protection on all DEX swaps in sell-diko

Location: sell-diko โ€” both swap-x-for-y and swap-y-for-x calls

Description: Both DEX swap calls in sell-diko pass u0 as the minimum output amount:

;; DIKO โ†’ USDA: zero minimum
(try! (as-contract (contract-call? .arkadiko-swap-v2-1 swap-x-for-y 
  .arkadiko-token .usda-token diko-to-mint u0)))

;; Leftover USDA โ†’ DIKO: zero minimum
(try! (as-contract (contract-call? .arkadiko-swap-v2-1 swap-y-for-x 
  .arkadiko-token .usda-token leftover-usda u0)))

With u0 minimum output, the swap will succeed regardless of how unfavorable the price is. Even without active manipulation (C-01), low-liquidity conditions or large debt amounts will cause significant slippage losses.

Impact: Every DIKO backstop activation suffers value loss from slippage. For a pool with 50,000 USDA liquidity and a 20,000 USDA debt to cover, the price impact alone would cause ~25% loss โ€” the protocol would need to mint far more DIKO than necessary, diluting all DIKO holders. Combined with C-01, the loss can be near-total.

Recommendation: Calculate an expected output based on oracle prices and enforce a minimum (e.g., 95% of expected). At minimum, use (/ (* debt-left u9500) u10000) as the floor for the DIKOโ†’USDA swap.

HIGH

H-02: No auction timeout โ€” underfunded auctions stay open indefinitely, blocking vault owner's collateral

Location: get-auction-open and finalize-liquidation

Description: An auction is considered open as long as total-debt-burned < debt-to-raise:

(define-read-only (get-auction-open (auction-id uint))
  (let ((auction (get-auction-by-id auction-id)))
    (if (>= (get total-debt-burned auction) (get debt-to-raise auction))
      false
      true)))

There is no time-based expiration. If the liquidation pool lacks sufficient USDA and the DIKO backstop doesn't fully cover the debt (e.g., due to slippage), the auction remains open forever. finalize-liquidation asserts (not (get-auction-open auction-id)), so the vault's remaining collateral can never be returned to the owner.

Impact: Vault owners permanently lose access to any remaining collateral if an auction cannot fully clear. In a market crash scenario where many vaults are liquidated simultaneously, the liquidation pool may be exhausted โ€” leaving multiple auctions stuck open and locking collateral indefinitely. There is no admin function to force-close an auction.

Recommendation: Add a timeout mechanism (e.g., 2,016 burn blocks โ‰ˆ ~2 weeks). After timeout, allow finalization regardless of debt coverage:

(define-read-only (get-auction-open (auction-id uint))
  (let ((auction (get-auction-by-id auction-id)))
    (if (or 
          (>= (get total-debt-burned auction) (get debt-to-raise auction))
          (> (- burn-block-height (get id auction)) u2016))  ;; timeout
      false
      true)))
MEDIUM

M-01: sell-diko inverted price formula โ€” mints more DIKO when DIKO is expensive

Location: sell-diko

Description: The price calculation computes the ratio of balance-x (DIKO) to balance-y (USDA):

(diko-price (/ (* (get balance-x pair-details) u1100000) (get balance-y pair-details)))

This calculates how many DIKO exist per USDA in the pool. When DIKO is expensive (less DIKO relative to USDA), balance-x / balance-y is smaller, which means diko-to-mint is smaller โ€” that's correct directionally. However, the variable name diko-price is misleading: it's actually the inverse of the USDA-denominated DIKO price. The 10% buffer (u1100000 = 1.1x) is applied to this inverse ratio.

More importantly, the formula diko-to-mint = debt-left * diko-price / 1e6 where diko-price = balance-x * 1.1 / balance-y gives: diko-to-mint = debt-left * balance-x * 1.1 / balance-y / 1e6. For an AMM pool, the effective price after a large swap is not the spot price times quantity โ€” the constant-product formula means larger swaps suffer quadratic price impact. The 10% buffer is insufficient for large liquidations.

Impact: For small liquidations, the formula is approximately correct. For large ones (e.g., debt-left > 10% of pool USDA balance), the minted DIKO may be insufficient to cover the debt via swap, or far too much is minted and the excess must be burned โ€” but only after absorbing the swap's price impact. This creates unpredictable DIKO supply inflation.

Recommendation: Use the oracle price for DIKO instead of DEX spot, or calculate the required input amount using the constant-product AMM formula directly: diko-needed = (balance-x * usda-needed) / (balance-y - usda-needed).

MEDIUM

M-02: Collateral fee deducted from rewards but tracked as fully sold โ€” accounting mismatch

Location: burn-usda

Description: The auction fee is deducted from collateral before depositing rewards, but total-collateral-sold tracks the pre-fee amount:

(collateral-fee (/ (* collateral-sold (var-get auction-fee)) u10000))
(collateral-without-fee (- collateral-sold collateral-fee))

;; Deposits only collateral-without-fee to rewards
(try! (as-contract (contract-call? liquidation-rewards add-reward-locked 
  block-height token-unlock-height token-is-stx ft collateral-without-fee)))

;; But tracks full collateral-sold
(map-set auctions { id: auction-id } (merge auction {
  total-collateral-sold: (+ (get total-collateral-sold auction) collateral-sold),
  ...
}))

When finalize-liquidation returns remaining collateral to the vault owner, it calculates: collateral-amount - total-collateral-sold. Since total-collateral-sold includes the fee portion, the vault owner receives less collateral than they should. The fee tokens are held by the auction engine contract (accessible via withdraw-fees), so funds aren't lost โ€” but the vault owner is shortchanged by the fee amount.

Impact: Vault owners lose an additional auction-fee percentage of their collateral beyond what the auction mechanism requires. Currently auction-fee is set to u0, so this has no active impact โ€” but any non-zero fee would create a systematic loss for vault owners.

Recommendation: Track total-collateral-sold as collateral-without-fee instead, or explicitly document that the fee comes from the vault owner's remaining collateral.

LOW

L-01: get-auction-by-id returns default struct for non-existent auctions

Location: get-auction-by-id

Description: Querying a non-existent auction ID returns a default struct with all zeros instead of failing:

(define-read-only (get-auction-by-id (id uint))
  (default-to
    { id: u0, auction-type: "collateral", collateral-amount: u0, ... }
    (map-get? auctions { id: id })))

Internal callers (e.g., burn-usda, finalize-liquidation) operate on this default struct without checking if the auction exists. For burn-usda, a non-existent auction would have debt-to-raise = u0 and total-debt-burned = u0, causing debt-left = u0, which eventually leads to usda-to-use = u0 โ€” the no-op path. So exploitation is limited, but it masks bugs in calling code.

Impact: Low โ€” the zero-value defaults cause most operations to no-op harmlessly. However, it prevents early failure detection and could confuse off-chain monitoring systems that query auction state.

Recommendation: Add an auction-exists check or use map-get? with unwrap! in functions that modify state.

LOW

L-02: withdraw-fees allows guardian to drain any token held by the contract

Location: withdraw-fees

Description: The withdraw-fees function transfers the entire balance of any SIP-010 token to the guardian:

(define-public (withdraw-fees (ft <ft-trait>))
  (let (
    (sender tx-sender)
    (contract-balance (unwrap-panic (contract-call? ft get-balance (as-contract tx-sender))))
  )
    (asserts! (is-eq sender (contract-call? .arkadiko-dao get-guardian-address)) (err ERR-NOT-AUTHORIZED))
    (as-contract (contract-call? ft transfer contract-balance (as-contract tx-sender) sender none))
  )
)

There's no restriction on which token can be withdrawn. If the contract temporarily holds collateral tokens during auction processing (between redeem-auction-collateral and add-reward-locked), and the guardian calls withdraw-fees with the collateral token in the same block, they could drain auction collateral.

Impact: Low โ€” requires guardian key compromise AND precise timing within the same block as an active auction. The guardian is already a trusted role. However, the function should be scoped to only withdraw fee tokens, not arbitrary balances.

Recommendation: Track accumulated fees per token in a map and only allow withdrawing the fee amount, not the full contract balance.

INFO

I-01: Pre-Clarity 4 โ€” extensive as-contract usage without asset restrictions

Location: burn-usda, sell-diko, withdraw-fees, start-auction

Description: The contract uses as-contract extensively to operate on behalf of the contract principal โ€” withdrawing from liquidation pools, burning USDA, minting DIKO, transferring collateral, and swapping on the DEX. In pre-Clarity 4, as-contract grants blanket authority over all assets the contract holds.

While all trait parameters are validated against the DAO registry (preventing arbitrary contract injection), the as-contract scope is still broader than necessary. Clarity 4's as-contract? with with-ft/with-stx allowances would restrict each operation to only the specific tokens it should touch.

Recommendation: Upgrade to Clarity 4 and use as-contract? with explicit asset allowances for each operation block.

INFO

I-02: Hardcoded stacker contract references limit extensibility

Location: get-unlock-height

Description: The unlock height lookup chains through 4 hardcoded stacker contracts:

(if (is-eq stacker "stacker")
  (unwrap-panic (contract-call? .arkadiko-stacker-v1-1 get-stacking-unlock-burn-height))
  (if (is-eq stacker "stacker-2")
    (unwrap-panic (contract-call? .arkadiko-stacker-2-v1-1 get-stacking-unlock-burn-height))
    (if (is-eq stacker "stacker-3")
      ...)))

Adding a 5th stacker would require deploying a new version of the auction engine. If a vault references a stacker name not in this list, the unlock height defaults to u0, which means token-unlock-height = u0 + unlock-height-extra โ€” potentially allowing immediate claiming of rewards that should be locked.

Recommendation: Use a map-based stacker registry or pass the stacker contract as a trait parameter (validated against the DAO registry).

Positive Observations

Access Control Summary

FunctionAccessNotes
start-auctionPermissionless (vault-manager gated)Requires valid vault-manager from DAO registry + undercollateralized vault
burn-usdaPermissionless (multi-trait gated)All 6 trait params validated against DAO registry
finalize-liquidationPermissionless (vault-manager gated)Only succeeds when auction is closed
toggle-auction-engine-shutdownGuardian onlyCorrectly guarded via get-guardian-address
withdraw-feesGuardian onlyDrains full token balance (see L-02)
update-feeGuardian onlyNo upper bound validation
update-unlock-height-extraGuardian onlyCorrectly guarded
sell-dikoPrivate (internal only)Called from finalize-liquidation
burn-usda-amountPublic (read-like)Computes amounts; no state change directly

Priority Score

MetricScoreWeightWeighted
Financial Risk3 โ€” DeFi liquidation engine (handles vault collateral + DIKO minting)39
Deployment Likelihood3 โ€” deployed on mainnet26
Code Complexity2 โ€” ~477 lines, multi-contract interactions, DEX integration24
User Exposure3 โ€” well-known Stacks DeFi protocol1.54.5
Novelty2 โ€” liquidation auction pattern, complements existing Arkadiko audits1.53
Total Score2.65 / 3.0

Clarity version penalty: -0.5 (pre-Clarity 4) โ†’ Adjusted: 2.15 / 3.0 โ€” well above 1.8 threshold.