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.
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.
| 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. |
| 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. |
| 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. |
- 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.
swapenforces the constant-product invariant on fee-adjusted balances.- The contract assumes LP total supply is controlled consistently by the supplied LP-token contract.
| 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. |
| 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. |
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.
| 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. |
| 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. |
- Caller semantics: owner checks correctly use
contract-caller, enabling governance contracts.collectinstead usestx-sender, which excludes contract-principal fee recipients. User operations usetx-sender, granting intermediaries transitive authority over the originator's approved token movements. - Panics:
do-get-poolanddo-get-revenueuseunwrap-panic. Invalid pool IDs can panic before authorization or typed precondition checks. - Arithmetic:
amt * reserve,amt * total-supply, andreserve0 * reserve1execute 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-mintandcalc-burnround 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/skimexists, but LP-token supply assumptions remain critical.
| 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. |
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.
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.
- Start with reserves
reserve0 = 100,reserve1 = 100, and LP supply100. - A user prepares
mint(10, 10)expectingmin(10 * 100 / 100, 10 * 100 / 100) = 10LP. - Before execution, trading changes the stored ratio to
reserve0 = 100,reserve1 = 200. - At execution,
calc-mintreturnsmin(10 * 100 / 100, 10 * 100 / 200) = 5LP. mintstill transfers all10token0 and all10token1 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.
- The owner creates a pool whose selected LP-token contract already reports
total-supply > 0while the new pool reserves are both zero. - The first liquidity provider calls
mintwith positive token amounts. - The nonzero-supply branch of
calc-mintevaluates proportional terms containing division byreserve0andreserve1. - 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.
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.