Contract: SPQC38PW542EQJ5M11CR25P7BS1CA6QT4TBXGB3M.stableswap-stx-ststx-v-1-2
Audit date: 2026-06-08
Scope: Deployed source returned by the Hiro mainnet contract-source API.
The contract custodies STX and stSTX, issues LP shares through a caller-supplied LP trait, calculates a two-asset stable invariant, and distributes swap fees to LP rewards, StackingDAO, and Bitflow.
The most consequential defect is an inverted fee-selection condition in both swap directions: ordinary users receive the zero-valued admin fee schedule, while admins receive the normal fee schedule. The review also identified a governance authorization pattern that exposes every admin capability through intermediary-contract phishing, and inconsistent amplification handling between swap math and LP accounting.
No critical or high-severity issue was identified. The fee bypass affects protocol and LP revenue rather than user principal, and the governance issue requires an admin to initiate a transaction through an untrusted contract.
| Severity | Count |
|---|---|
| Critical | 0 |
| High | 0 |
| Medium | 4 |
| Low | 2 |
| Informational | 2 |
| Constant | Line | Purpose |
|---|---|---|
this-contract |
20 | Contract principal captured through as-contract |
deployment-height |
23 | Burn height used as cycle epoch |
cycle-length |
26 | 144-block reward-accounting cycle |
index-list |
29 | 384 Newton-Raphson iterations |
number-of-tokens |
32 | Two-token invariant parameter |
contract-deployer |
35 | Immutable original deployer and non-removable admin |
| Store | Line | Initial value | Writers / authority |
|---|---|---|---|
staking-and-rewards-contract |
47 | deployer | set-staking-contract; any admin, once |
staking-and-rewards-contract-is-set |
50 | false |
set-staking-contract; any admin |
stacking-dao-contract |
53 | fixed principal | set-stacking-dao-contract; any admin |
bitflow-contract |
56 | fixed principal | set-bitflow-contract; any admin |
admins |
59 | deployer | add-admin, remove-admin; any admin |
buy-fees |
62 | 3/0/2 bps | change-buy-fee; any admin |
sell-fees |
65 | 3/195/2 bps | change-sell-fee; any admin |
admin-swap-fees |
68 | 0/0/0 bps | change-admin-swap-fee; any admin |
liquidity-fees |
71 | 3 bps | change-liquidity-fee; any admin |
helper-principal |
74 | deployer | Temporary writer in remove-admin |
convergence-threshold |
77 | u2 |
change-convergence-threshold; any admin |
| Map | Lines | Fields | Writers |
|---|---|---|---|
PairsDataMap |
84-93 | approval, shares, decimals, balances, D, amplification | create, swaps, LP operations, approval and A setters |
CycleDataMap |
95-97 | per-pair/cycle LP fee balance in STX | both swap directions |
PairsDataMap is also the contract's internal reserve ledger. Actual STX and
stSTX custody remains at this contract principal; LP supply and balances live
in the supplied LP-token contract.
| Group | Functions | Behavior |
|---|---|---|
| State/cycle reads | get-pair-data, get-cycle-data, get-current-cycle, get-cycle-from-height, get-starting-height-from-cycle, get-deployment-height |
Open state and cycle queries |
| Quotes | get-dx, get-dy |
Quotes Y-to-STX and STX-to-Y using normal fee schedules |
| Invariant math | get-x, get-y, get-D |
Newton-Raphson stable-invariant calculations |
| Fold helpers | x-for-loop, y-for-loop, D-for-loop |
Up to 384 iterations; return u0 convergence sentinel on failure |
| Decimal helpers | get-scaled-up-token-amounts, get-scaled-down-token-amounts |
Normalize token precision |
| Governance helper | is-not-removeable |
Filters the selected admin from the list |
| Function(s) | Lines | Authority / preconditions | Mutations / transfers |
|---|---|---|---|
swap-x-for-y |
321-439 | Open; pair approved; input less than 10x reserve; output strictly above minimum | STX from caller; stSTX to caller; three fee transfers; pair/cycle maps |
swap-y-for-x |
443-557 | Open; pair approved; input less than 10x reserve; output strictly above minimum | stSTX from caller; STX to caller and fee recipients; pair/cycle maps |
add-liquidity |
568-680 | Open; approved pair; positive input; D and LP minimum checks | STX/stSTX in, fees to Bitflow, LP mint, pair map |
withdraw-liquidity |
685-738 | Open; strict token minimums; notably does not enforce pair approval | LP burn, STX/stSTX out, pair map |
create-pair |
865-909 | Any admin; unique pair; scaled balances equal and not both zero | Initial custody, LP mint, pair map |
set-pair-approval |
915-932 | Any admin | Pair approval |
add-admin, remove-admin |
937-981 | Any admin; deployer cannot be removed | Admin list and helper var |
| Fee setters | 984-1033 | Any admin; no numeric bounds | Fee variables |
change-amplification-coefficient |
1037-1055 | Any admin; no bound or ramp | Pair A parameter |
change-convergence-threshold |
1058-1068 | Any admin; no bound | Global math threshold |
| Destination setters | 1072-1124 | Any admin; staking destination only once | Fee/reward destinations |
| Public function | Token movement | Recommended caller post-conditions |
|---|---|---|
swap-x-for-y |
STX leaves caller; stSTX enters caller | Cap STX sent; require minimum stSTX received; bind exact token and pool |
swap-y-for-x |
stSTX leaves caller; STX enters caller | Cap stSTX sent; require minimum STX received |
add-liquidity |
STX/stSTX leave caller; LP enters caller | Cap both inputs and require minimum LP received |
withdraw-liquidity |
LP burns; STX/stSTX enter caller | Cap LP burn and require minimum receipts for both assets |
create-pair |
Initial STX/stSTX leave admin; LP enters admin | Cap both deposits and require expected LP receipt |
| Governance functions | No direct caller token movement | Bind exact contract/function/arguments; admins must avoid intermediary contracts |
Minimum arguments use strict > checks rather than >=. A transaction whose
output exactly equals the caller's stated minimum will revert.
| Capability | Intended authority | Actual check |
|---|---|---|
| Swap / add / withdraw | Any caller meeting pair/math checks | Open |
| Create or approve pair | Admin | Membership of original tx-sender in admins |
| Add/remove admin | Admin | Membership of original tx-sender in admins |
| Change fees, A, threshold, destinations | Admin | Membership of original tx-sender in admins |
| Remove deployer | Nobody | Explicitly prohibited |
| Withdraw from disapproved pair | Any LP holder | Allowed; approval value is read but not asserted |
All admin checks rely only on tx-sender; no admin function checks
contract-caller. Therefore an untrusted contract called by an admin can invoke
the complete governance surface with that admin's authority.
| Check | Result |
|---|---|
tx-sender vs contract-caller |
Every governance function trusts tx-sender; see SS-02 |
| Panic unwraps | No unwrap-panic or unwrap-err-panic found |
| Arithmetic overflow / underflow | Stable-invariant multiplication is large and all fee/A/threshold setters are unbounded; malformed privileged settings can abort paths |
as-contract / principal escalation |
Necessary for custody transfers and LP minting, but it magnifies the impact of caller-supplied trait behavior and governance compromise |
| Trait conformance | SIP-010 and LP traits are used. Pair creation is admin-gated, containing arbitrary-trait risk |
| Precision / rounding | Decimal scaling floors on scale-down. Quote/execution fee behavior diverges for ordinary users because of SS-01 |
| Convergence | Math returns a zero sentinel after 384 unsuccessful iterations and callers do not reject it; see SS-04 |
| Amplification governance | Immediate, unbounded changes with no ramp or timelock; swap and LP math pass A differently; see SS-03 |
| ID | Severity | Function | Line | Finding | Recommended fix |
|---|---|---|---|---|---|
| SS-01 | Medium | Both swaps | 332-343, 454-465 | The admin fee condition is inverted. When the caller is an admin the contract selects normal buy-fees/sell-fees; ordinary users receive admin-swap-fees, initialized to zero. Regular swaps therefore bypass LP, StackingDAO, and Bitflow fees. |
Swap the branches so admins receive admin-swap-fees and all other callers receive the directional normal schedule. Add tests for admin and non-admin callers in both directions. |
| SS-02 | Medium | All governance functions | 865-1122 | Admin authority is exposed through tx-sender. An intermediary contract called by any admin can add itself as admin, change fee destinations, alter fees/A/threshold, and approve pairs because the original admin remains tx-sender. |
Require contract-caller to be an admin. For direct-wallet-only governance, require both tx-sender and contract-caller to be the same admin. Prefer a timelocked governance contract. |
| SS-03 | Medium | LP operations vs swaps | 357, 415, 475, 533, 596-612, 706 | Swap and LP accounting use different effective amplification values. Swaps and quotes call invariant helpers with A * number-of-tokens, while add/withdraw liquidity call get-D with raw A. LP share minting and stored D can therefore be calculated on a different curve than swap pricing. |
Define one canonical Ann helper and use it in every call to get-D, get-x, and get-y. Recompute expected LP mint/burn values in regression tests. |
| SS-04 | Medium | get-x, get-y, get-D callers |
177-307, 749-797 | Non-convergence silently becomes u0. Fold helpers leave converged at zero after 384 iterations; callers consume that value as a valid reserve/invariant. In unfavorable inputs this can produce nonsensical quotes or abort later arithmetic instead of a structured failure. |
Return a response from invariant helpers and reject converged == u0 with a dedicated error before using the value. |
| SS-05 | Low | Fee/A/threshold setters | 984-1066 | Critical numeric governance settings have no local bounds. Fee sums can exceed 100%, A can be zero or changed abruptly, and threshold can accept coarse convergence. Accidental or compromised settings can brick or distort pricing. | Enforce component and total fee caps, a positive bounded A with gradual ramping, and a conservative threshold range. |
| SS-06 | Low | Quote functions | 141-176, 225-260 | Quotes always apply normal fees while ordinary execution currently applies the zero admin schedule. Integrators receive a materially different quote from current execution, particularly on the 195 bps sell side. | Fix SS-01, then expose caller-aware quote variants or clearly document that quotes assume the normal schedule. |
| SS-07 | Informational | User minimum checks | 376, 494, 636, 710, 713 | Minimums use strict > instead of >=, causing transactions that exactly meet their stated bound to revert. |
Use >= unless the stricter semantic is deliberate and documented. |
| SS-08 | Informational | withdraw-liquidity |
689-713 | The function reads current-approval but never asserts it. This permits exits after a pair is disabled, which is usually desirable but differs from swaps/additions and should be explicit. |
Document exit-only behavior for disapproved pairs or add an intentional named policy check. |
The comments state "Admins pay no fees on swaps," but the conditional branches
do the opposite. With the deployed initial state, a non-admin STX-to-stSTX swap
selects {lps: u0, stacking-dao: u0, bitflow: u0} rather than the 5 bps buy
schedule. A non-admin stSTX-to-STX swap similarly bypasses the configured 200
bps sell schedule, including the 195 bps StackingDAO component.
This is rated medium because it causes systematic loss of expected fee revenue and LP incentives without directly taking user principal. No private responsible-disclosure action was required for this severity.
In Clarity, tx-sender remains the original transaction initiator across
contract calls, while contract-caller identifies the immediate caller. If an
admin wallet calls a malicious contract, that contract can call add-admin
while the stableswap still sees the wallet admin as tx-sender. Once added, the
attacker principal can invoke governance directly.
The issue requires an admin to initiate a call through attacker-controlled code and is therefore rated medium rather than high.
- Pair creation and arbitrary trait selection are admin-gated.
- Pair uniqueness is enforced.
- Swaps and additions require pair approval.
- Swap input has a reserve-relative cap and user-provided minimum output.
- Multi-contract transfer and accounting workflows remain atomic on error.
- The deployer cannot be removed from the admin set.
- All asset transfers use structured
unwrap!errors. - No high or critical issue was found, so mandatory private disclosure was not triggered before publication.
- Execute both swap directions as an admin and a normal user; assert the exact recipient fee amounts after correcting SS-01.
- Have an admin call a mock intermediary that invokes
add-adminand each setter; verify current success and proposed rejection. - Property-test LP mint/burn and swap invariants using one canonical
Ann. - Force non-convergent or zero-convergence math and verify a structured error.
- Fuzz fee sums, A, threshold, decimals, reserves, and minimum-output edges.
- Verify documented exit behavior after
set-pair-approval false.
This is nice! We built immunity-agent to ensure developers and agents write code securely, anyone can use it to have better package decisions, secret redaction and prompt injection! Would love community feedback and contribution