apeir online / reports / Clarity checks

Six Clarity checks from paid audits

Short, concrete things to look for in your own Stacks contracts. The first two come from bounty reports that were paid and are public. The rest are patterns that come up in almost every DeFi review on Stacks.

1. A clock that anyone can restart for free

In the CityCoins ccd016-swap-vault-mia-v2 review, start-clock was public and only checked (not (is-idle)) and (not (window-open)). Once a sell window had passed with even 1 sat left in the vault, anyone could call it again and push batch-start forward by another 288 blocks. Every exit path asserted window-elapsed, so one caller with no capital could keep them blocked forever. The comment said the clock was "moved only by funding". The code didn't enforce that. Full write-up.

Check: for every permissionless function that moves a deadline or a window start, does it require the state change that justifies it (new funds, a new batch id)? Or only that the window is closed right now?

2. One price used in two comparisons with opposite risk

In the Jing v6-2 order book, the maker margin gate widened the oracle mid by 40 bp against the entrant. That is right for the entrant's own limit test. But the same widened price went into the scan of the opposite book, and there the widening helped the entrant. Resting orders inside that 40 bp band were invisible to the gate but still filled at settlement, which runs at the raw mid, so the rebate the gate was meant to force could be skipped. Full write-up.

Check: when a helper takes one price and uses it in two <=/>= tests, ask which way the safety margin should point in each. Often it has to flip.

3. Share price read from a balance

Vaults that compute assets-per-share from ft-get-balance or stx-get-balance of the contract can be moved by a plain transfer to the contract. With a small or empty vault, the first depositor can donate to inflate the share price and round the next depositor down to zero shares.

Check: track assets in a data-var that only deposits, withdrawals and accounted yield change. Or add a virtual-share offset and dead shares sized for the smallest TVL the vault will ever have, including a fresh redeploy.

4. tx-sender where contract-caller was meant

An authorization check on tx-sender passes for any contract the user calls, including a malicious one that then calls yours in the same transaction. Post-conditions limit what the user loses, but only for the assets they list.

Check: for every (asserts! (is-eq tx-sender ...)) in a function that moves funds or changes roles, decide whether an intermediate contract should be allowed. If it shouldn't, use contract-caller.

5. Rewards that can be caught just in time

If rewards are credited in a step that anyone can see coming (a daily log, a once-per-cycle distribution) and deposits count at once, someone can deposit just before the credit and withdraw right after. They take a share of rewards they never earned.

Check: is there a minimum holding time, a lock until the next cycle, or a stream over blocks rather than a single step? If the design accepts the risk, write down the cap that bounds it.

6. Post-conditions that are allowed to be empty

Any function you expect users to call through your own front end should still be safe in allow mode with no post-conditions. A different front end, a script or another protocol will call it that way sooner or later.

Check: walk each public function assuming no post-conditions at all. Does any path move more than the caller expects (fees taken from a different balance, a refund sent somewhere else, a swap with no minimum out)?

Want these checked on your contract? Quick review: 20,000 sats for one contract up to ~400 lines. Full review: 60,000 sats for a contract set up to ~1,500 lines. You pay after you have read the report. Details and contact: apeir online.