Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save silentgeckoaudit3801/f25c35f4f4bd7406d4118cbdb5c27d76 to your computer and use it in GitHub Desktop.

Select an option

Save silentgeckoaudit3801/f25c35f4f4bd7406d4118cbdb5c27d76 to your computer and use it in GitHub Desktop.
Velar univ2-core static analysis

Velar univ2-core Static Analysis

Target: SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.univ2-core

Source: Hiro mainnet source

Review date: 2026-06-20

Scope: all 629 source lines, with special attention to pool creation, liquidity accounting, fee authority, constant-product enforcement, and caller-provided trait contracts. No high or critical issue was identified; private disclosure was not triggered.

Executive summary

univ2-core is a compact constant-product AMM that holds assets for every pool in one contract principal while isolating accounting by pool ID. It checks token/LP trait principals against stored pool configuration, limits the effective swap fee to at most 0.5%, caps the protocol share at 50% of swap fees, and preserves a * b >= reserve0 * reserve1 after swaps.

The strongest user-facing issue is liquidity-add slippage. mint accepts fixed token amounts but has no minimum-LP parameter, deadline, or refund of the non-limiting token. If reserves move before execution, the user can receive LP shares based on the smaller proportional side while the entire larger side is added to reserves, transferring the excess to existing LPs. burn similarly lacks minimum token outputs. Pool creation also assumes, but does not verify, that the supplied LP token has zero initial supply and exclusive core-controlled mint authority.

1. State model

Constants

Lines Constant Meaning
12-25 err-* Typed authorization, pool, create, mint, burn, swap, collect, and anti-rug errors.
128 MAX-SWAP-FEE {num: u995, den: u1000} retained-input multiplier; effective swap fee cannot exceed 0.5%.
148 MAX-PROTOCOL-FEE Protocol can receive at most 50% of the swap fee.

Data variables

Lines Variable Initial value Mutation authority
29 owner deployer Current owner through set-owner; checked using contract-caller.
38 protocol-fee-to deployer Owner through set-protocol-fee-to.
47 share-fee-to .univ2-share-fee-to Owner through set-share-fee-to.
56 pool-id u0 Incremented privately during owner-only create; failed transactions roll back the increment.

Maps

Lines Map Contents Mutation
66-80 pools Token/LP principals, reserves, fee fractions, last Stacks and burn heights create, fee updates, mint, burn, swap.
82-84 index Ordered token-pair to pool ID Owner-only create. Reverse lookup is handled by lookup-pool.
87 lp-tokens Set of LP-token principals already bound to a pool Owner-only create.
89-94 revenue Accrued protocol-fee remainder by pool/token side create, swap, collect.

Accounting invariants

  • Each pool ID binds exactly two distinct token contracts and one LP token.
  • An LP token may be registered for only one pool.
  • Stored reserves exclude protocol revenue and already-transferred share fees.
  • swap enforces the constant-product invariant on fee-adjusted balances.
  • The contract assumes LP total supply is controlled consistently by the supplied LP-token contract.

2. Function inventory

Administrative functions

Function Lines Authority Mutations / notes
set-owner 33-36 contract-caller == owner Replaces owner in one step.
set-protocol-fee-to 42-45 Owner Replaces collection recipient.
set-share-fee-to 49-52 Owner Replaces share-fee receiver contract.
update-swap-fee 130-137 Owner Updates retained-input multiplier; denominator must be 1000 and numerator at least 995.
update-protocol-fee 150-157 Owner Updates protocol share; denominator 1000, numerator at most 500.
update-share-fee 169-176 Owner Updates share of protocol fee; denominator 1000, numerator at most 1000.
create 243-290 Owner Creates pool, ordered index, LP-token binding, and zero revenue row. Calls token symbols through traits.

Liquidity and trading functions

Function Lines Authority Preconditions and effects
mint 294-353 Open; acts for tx-sender Verifies trait principals and positive amounts/liquidity; transfers both tokens to core, mints LP, increases reserves. No minimum LP or refund.
burn 372-423 Open; acts for tx-sender Verifies traits and positive proportional outputs; transfers both tokens to user, burns LP, reduces reserves. No minimum outputs.
swap 438-551 Open; acts for tx-sender Verifies token pair and share receiver, transfers exact input/output, routes fees, updates reserves/revenue, enforces adjusted constant product.
collect 584-626 tx-sender == protocol-fee-to Transfers tracked protocol revenue to tx-sender, then resets the row.

Read-only and private helpers

get-owner, fee-recipient getters, get-nr-pools, get-pool, get-pool-id, lookup-pool, fee validators, calc-mint, calc-burn, calc-swap, and min are open reads. do-get-pool and do-get-revenue panic on missing rows. Private helpers increment IDs and update reserves/revenue.

3. Post-condition coverage matrix

Public function Asset movement Caller safeguards
create, fee setters, owner setters No user token movement Verify printed/new state and expected principal/fraction. Use a two-step governance process off chain because the contract has none.
mint Sends exact amt0 and amt1 from caller; receives calculated LP Attach sent-eq post-conditions for both inputs. The contract exposes no minimum-LP argument, so pre-simulate current reserves and avoid stale transactions; post-conditions cannot fully substitute for a receive minimum.
burn Burns exact LP from caller; receives calculated token0/token1 Constrain LP spend. Because no min-amt0/min-amt1 exists, pre-simulate and submit with a short validity window at the coordinator layer.
swap Sends exact amt-in; receives exact caller-selected amt-out Attach exact/max input and minimum/exact output post-conditions. The supplied amt-out already functions as an on-chain quote bound because invariant failure aborts.
collect Core sends tracked token0/token1 revenue to fee recipient Recipient should reconcile printed revenue and token receipts.

4. Authority and access-control matrix

Capability Principal On-chain check Risk notes
Ownership transfer Current owner contract-caller Supports DAO contracts, but transfer is immediate and has no acceptance step.
Pool creation Owner contract-caller Owner selects arbitrary trait contracts and is responsible for LP-token integrity.
Fee configuration Owner contract-caller plus fraction guards No delay; current fee can change before a pending user transaction executes.
Protocol revenue collection protocol-fee-to tx-sender A contract-principal recipient cannot satisfy this equality directly.
Share-fee callback Stored share-fee-to contract Exact trait-principal equality in swap Transfer and callback are atomic.
Mint/burn/swap Open Trait principal equality with stored pool Uses tx-sender as end user, including through intermediary calls.
Pause/kill switch None Not present Owner cannot stop an individual compromised pool without a migration or external-token intervention.

5. Clarity best-practice review

  • Caller semantics: owner checks correctly use contract-caller, enabling governance contracts. collect instead uses tx-sender, which excludes contract-principal fee recipients. User operations use tx-sender, granting intermediaries transitive authority over the originator's approved token movements.
  • Panics: do-get-pool and do-get-revenue use unwrap-panic. Invalid pool IDs can panic before authorization or typed precondition checks.
  • Arithmetic: amt * reserve, amt * total-supply, and reserve0 * reserve1 execute before postcondition checks. Clarity uint overflow aborts safely but can make high-decimal/large-reserve pools unusable.
  • as-contract: required to move core-held tokens and call LP mint/burn. Trait-principal checks limit substitution after pool creation.
  • Trait trust: owner-selected FT and LP contracts remain a protocol trust boundary. The core cannot prove exclusive LP mint authority.
  • Invariant: swap checks adjusted a * b >= k; fee portions are consistently split among reserves, share receiver, and tracked revenue.
  • Rounding: calc-mint and calc-burn round down. Positive-output guards prevent zero-value LP issuance/redemption, but excess liquidity input is not refunded.
  • First deposit: no minimum liquidity is permanently burned. Direct token donations do not update stored reserves because no sync/skim exists, but LP-token supply assumptions remain critical.

6. Findings table

ID Severity Function / lines Finding Recommended fix
VL-01 Medium mint, 294-328; calc-mint, 355-366 No min-liquidity, deadline, or proportional refund exists. If reserves move before execution, LP output is the minimum proportional side while both full token amounts enter reserves. The non-limiting excess becomes a donation to existing LPs. Add min-liquidity and deadline parameters; calculate optimal counterpart amount and refund/reject excess; emit the actual accepted amounts.
VL-02 Medium create, 243-279; mint/burn, 307/384 Pool creation does not verify zero initial LP supply or exclusive core mint/burn authority. A pre-minted LP token causes first calc-mint to divide by zero because reserves are zero while supply is nonzero; an LP contract with external mint authority can undermine redemption accounting. Require a dedicated LP token with zero supply at creation and verifiable core-only mint/burn authority; deploy LP tokens through a canonical factory.
VL-03 Medium Arithmetic at 334-335, 364-366, 432-433, 467, 527, 560-564 Large supplies/reserves or high-decimal tokens can overflow uint products before the intended postcondition checks, aborting mint, burn, or swap and potentially bricking a pool at scale. Use divide-before-multiply helpers with remainder handling or checked wide/fixed-point math; enforce token/reserve bounds at creation and update.
VL-04 Low burn, 372-407 Liquidity removal has no minimum token0/token1 outputs or deadline. Reserve changes before execution can produce materially different proceeds than the user quoted. Add min-amt0, min-amt1, and deadline parameters and assert computed outputs before transfers.
VL-05 Low set-owner, 33-36; fee-recipient setters 42-52 Ownership and fee destinations change in one transaction with no propose/accept handshake or delay. A typo can permanently transfer control or make fees inaccessible. Use pending-principal plus explicit acceptance; consider a timelock for fee-routing changes.
VL-06 Low collect, 590-600 Fee collection authenticates tx-sender, unlike owner functions that use contract-caller. Setting protocol-fee-to to a contract principal makes collection unreachable because a transaction origin cannot equal that contract principal. Authenticate contract-caller, or support both direct standard-principal and contract-principal recipients explicitly.
VL-07 Informational do-get-pool, 101-102; do-get-revenue, 115-116 Invalid IDs panic instead of returning err-no-such-pool; several admin/user paths evaluate these helpers before typed authorization/precondition checks. Replace with unwrap! ... err-no-such-pool in response-returning paths and perform auth before lookups where possible.
VL-08 Informational mint, burn, swap No on-chain deadline exists. Old signed transactions can execute later if their other conditions still pass; this matters most for liquidity functions without output minima. Add a burn-height or block-height deadline to state-changing market operations.

Responsible-disclosure assessment

No high or critical issue was identified. The medium findings require either reserve movement, pool misconfiguration, or arithmetic scale and do not provide an immediate untrusted-caller drain against correctly configured deployed pools. Public reporting is appropriate under the bounty rules.

Mainnet state verification

On 2026-06-22, the deployed read-only get-nr-pools function returned u87. I then queried get-pool for every pool ID from 1 through 87 through Hiro's read-only contract endpoint. Twenty pools had both stored reserves equal to zero: IDs 2, 4, 5, 17, 20, 31, 37, 47, 48, 51, 52, 53, 59, 66, 67, 73, 75, 76, 77, and 85. The other 67 had two nonzero stored reserves; none had only one zero reserve.

This snapshot does not establish that any deployed LP token is externally mintable or already has nonzero supply. It does show that the zero-reserve initialization state implicated by VL-02 is not merely hypothetical: 20 registered pools remain in that state. Before activating one, governance should verify the LP token's live total supply is zero and that mint/burn authority is restricted to the core contract. The core currently performs neither check itself.

Reproduction traces

VL-01: stale-ratio liquidity donation

  1. Start with reserves reserve0 = 100, reserve1 = 100, and LP supply 100.
  2. A user prepares mint(10, 10) expecting min(10 * 100 / 100, 10 * 100 / 100) = 10 LP.
  3. Before execution, trading changes the stored ratio to reserve0 = 100, reserve1 = 200.
  4. At execution, calc-mint returns min(10 * 100 / 100, 10 * 100 / 200) = 5 LP.
  5. mint still transfers all 10 token0 and all 10 token1 into the pool. Only half of token0 was needed for the limiting ratio, so the unmatched value increases reserves for existing LP holders.

A min-liquidity parameter would at least abort this stale quote; optimal counterpart calculation plus refund/rejection would prevent the excess donation itself.

VL-02: nonzero external LP supply blocks first mint

  1. The owner creates a pool whose selected LP-token contract already reports total-supply > 0 while the new pool reserves are both zero.
  2. The first liquidity provider calls mint with positive token amounts.
  3. The nonzero-supply branch of calc-mint evaluates proportional terms containing division by reserve0 and reserve1.
  4. Both reserves are zero, so the call aborts before usable pool liquidity can be initialized.

The core also cannot prove that the selected LP contract lacks an external minter. Canonical factory deployment with zero-supply and core-only mint/burn authority removes both assumptions.

Conclusion

The swap path is a faithful fixed-output constant-product design with meaningful anti-rug fee bounds. The priority remediation is liquidity-operation slippage protection: minimum outputs, deadlines, and proportional acceptance/refunds. Canonical LP-token deployment and overflow-aware arithmetic would materially strengthen pool creation and long-term scalability.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment