The vectors below are specific to CCA's architecture. Some are documented by Uniswap itself, some emerge from the mechanism, and one was found by us during an audit of a protocol built on top of CCA. The contract has been audited by Spearbit, OpenZeppelin, and ABDK Consulting, with a live bug bounty on Cantina. All source references are collected at the end of this section.
tokensFilledclaimTokens() has no ownership check. Anyone can call it and pass any bidId , even if they are not the owner. When it executes, _internalClaimTokens resets $bid.tokensFilled = 0 permanently — there is no on-chain record of the original fill amount after this point.
Any protocol that reads bid.tokensFilled after the claim block to determine user allocation will compute zero if a third party called claimTokens first. A try/catch around your own claimTokens call does not help — the storage is already zeroed before your read. All downstream accounting — token transfers, bonus calculations, vesting state — computes against zero. Users' funds are permanently stuck with no recovery path.
The attack is also weaponizable: a griefing bot can iterate over all bidIds at claimBlock and zero out tokensFilled for every user before anyone interacts with your protocol.
1function _internalClaimTokens(uint256 _bidId) internal returns (address owner, uint256 tokensFilled) {
2 Bid storage $bid = _getBid(_bidId);
3 if ($bid.exitedBlock == 0) revert BidNotExited();
4 owner = $bid.owner;
5 tokensFilled = $bid.tokensFilled;
6 // Set the tokens filled to 0 — permanently, no recovery
7 $bid.tokensFilled = 0;
8}The fix: fork CCA and add access control so all claims must route through your contract, or cache tokensFilled in your own storage before claimBlock is reached — never rely on a live read from CCA state post-claim. Status: Fixed in the protocol we audited.
The _iterateOverTicksAndFindClearingPrice walks a singly-linked list of initialized ticks, subtracting each tick's currencyDemandQ96 from $sumCurrencyDemandAboveClearingQ96 until demand falls below the next tick's threshold. Every call to _checkpointAtBlock runs this loop to MAX_TICK_PTR. An attacker who initializes thousands of ticks at minimum TICK_SPACING intervals forces every subsequent checkpoint — and therefore every submitBid — to iterate through all of them. forceIterateOverTicks was added in v1.1.0 as a manual gas-bounded recovery mechanism, but it only helps if someone calls it before the next checkpoint. Uniswap's own documentation flags this: integrators should choose a tick spacing of at least 1 basis point of the floor price, with 1% or 10% also reasonable. Setting too small a tick spacing can result in a DoS where the auction cannot finish. [1][2]
1// ContinuousClearingAuction.sol#L338-L355
2while (
3 (nextActiveTickPrice_ != _untilTickPrice
4 && sumCurrencyDemandAboveClearingQ96_ >= TOTAL_SUPPLY * nextActiveTickPrice_)
5 || clearingPrice_ == nextActiveTickPrice_
6) {
7 Tick storage $nextActiveTick = _getTick(nextActiveTickPrice_);
8 sumCurrencyDemandAboveClearingQ96_ -= $nextActiveTick.currencyDemandQ96;
9 minimumClearingPrice = nextActiveTickPrice_;
10 nextActiveTickPrice_ = $nextActiveTick.next;
11 clearingPrice_ = sumCurrencyDemandAboveClearingQ96_.divUp(TOTAL_SUPPLY);
12 updateStateVariables = true;
13}
14
15// Every submitBid triggers this — always iterates to MAX_TICK_PTR (L390)
16uint256 newClearingPrice = _iterateOverTicksAndFindClearingPrice(MAX_TICK_PTR);Bids at exactly the clearing price fill pro-rata, computed from currencyRaisedAtClearingPriceQ96_X7 accumulated across checkpoints and each tick's currencyDemandQ96 — all of it readable on-chain before settlement. A searcher can submit a bid at a tick they expect to become the clearing price, diluting existing bidders' share with minimal capital. In the other direction, exitPartiallyFilledBid allows immediate exit once the clearing price rises above a bid's maxPrice. A bot monitoring ClearingPriceUpdated events can back-run every price increase to exit newly out-of-range bids in the same block. Both vectors are automatable and composable with flashloans for the front-running case. [3]
1// Pro-rata accumulation at clearing tick — _sellTokensAtClearingPrice L230-L266
2_checkpoint.currencyRaisedAtClearingPriceQ96_X7 = ValueX7.wrap(
3 ValueX7.unwrap(_checkpoint.currencyRaisedAtClearingPriceQ96_X7) + currencyRaisedAtClearingQ96X7
4);
5
6// Immediate exit once outbid — exitPartiallyFilledBid L640-L644
7upperCheckpoint = _getCheckpoint(outbidCheckpoint.prev);The auction's supply issuance is governed by auctionStepsData, where each step defines how many milli-bips of TOTAL_SUPPLY are sold per block. The final $clearingPrice at END_BLOCK is what seeds the Uniswap v4 pool via lbpInitializationParams. If the last step sells a negligible fraction of tokens, the clearing price in that final interval is cheap to move — a single large bid or withdrawal near END_BLOCK can shift the price the pool opens at. Uniswap's documentation states explicitly that the last block of the auction must sell a significant amount of tokens for this reason. [4]
1// Final clearing price seeds the pool — lbpInitializationParams L154-L161
2return LBPInitializationParams({
3 initialPriceX96: $clearingPrice, tokensSold: totalCleared(), currencyRaised: currencyRaised()
4});
5
6// Tiny mps = almost no supply anchors final price — _checkpointAtBlock L408-L411
7deltaMps += uint24(blockDelta * step.mps);The auction only tracks currency received through submitBid and tokens received through onTokensReceived up to exactly TOTAL_SUPPLY. Any currency sent directly to the contract is permanently stranded — sweepCurrency only transfers currencyRaised(), not the actual contract balance. Tokens sent beyond TOTAL_SUPPLY are equally unrecoverable: sweepUnsoldTokens calculates from TOTAL_SUPPLY - totalCleared, ignoring any excess. Uniswap documents this directly and considers it a known, permanent limitation. [5]
1// sweepCurrency transfers accounted value only, not balanceOf — L735-L741
2_sweepCurrency(_getBlockNumberish(), _currencyRaised());
3
4// sweepUnsoldTokens computed from TOTAL_SUPPLY, not balanceOf — L744-L755
5unsoldTokens = totalSupplyQ96.scaleUpToX7().saturatingSub($totalClearedQ96_X7)
6 .divUint256(FixedPoint96.Q96).scaleDownToUint256();
7
8// onTokensReceived accepts any balance >= TOTAL_SUPPLY, excess silently ignored — L140-L149
9if (TOKEN.balanceOf(address(this)) < TOTAL_SUPPLY) {
10 revert InvalidTokenAmountReceived();
11}MAX_BID_PRICE is derived from TOTAL_SUPPLY via MaxBidPriceLib.maxBidPrice(). For 1 trillion 18-decimal tokens (1e30), max bid price is 2^110. For 1 billion 6-decimal tokens (1e15), it reaches 2^160. At extreme values the Q96 fixed-point arithmetic in the loop condition — TOTAL_SUPPLY * nextActiveTickPrice_ — can approach uint256 boundaries. The contract guards against this in _submitBid with InvalidBidUnableToClear(), but verify that the specific supply and price combination in your deployment cannot push products past this guard before it fires. Uniswap strongly recommends choosing a currency worth more per unit than the token and keeping total supply well below the maximum. [6]
Uniswap explicitly warns against using the auction with tokens below 6 decimals — bidders lose significant value to rounding errors and the auction will not revert, it will just quietly misallocate. Fee-on-transfer tokens are similarly unsupported: the auction accounts for the nominal transfer amount, not the received amount, so accounting will be wrong from the first bid. If you're auditing a protocol that deploys CCA for arbitrary token launches, check that decimal validation is enforced at the token creation layer rather than relying on CCA to catch it — it won't. [7]
v1.0.0-candidate contained a bug in rare edge cases that caused bids to be permanently locked in the contract with no exit path. This was fixed in v1.1.0 alongside the addition of forceIterateOverTicks. If you're auditing a protocol that deployed against v1.0.0-candidate factory addresses (0x0000ccaDF55C911a2FbC0BB9d2942Aa77c6FAa1D), the locked bid risk is live. v1.1.0 factory addresses are 0xCCccCcCAE7503Cac057829BF2811De42E16e0bD5 across Mainnet, Unichain, and Base. [8]
CCA is permissionless to deploy, which means anyone can launch an auction with malicious parameters. Uniswap documents several known traps: an excessively high floorPrice that causes bidders to overpay; extreme startBlock or endBlock values that prevent refunds from being claimed within a reasonable timeframe; an unrealistic requiredCurrencyRaised threshold that prevents the auction from ever graduating, permanently locking all bidder funds; and a positionRecipient set to an address that immediately withdraws the liquidity position after pool creation, leaving token holders with no liquidity to trade against. None of these are caught by the contract — it is the bidder's responsibility to validate all parameters before participating. If you're building a frontend or integration on top of CCA, consider what parameter validation you surface to users. [9]
[1] TechnicalDocumentation.md L349–355 — Tick spacing / DoS risk: https://github.com/Uniswap/continuous-clearing-auction/blob/main/docs/TechnicalDocumentation.md
[2] CHANGELOG.md v1.1.0 — forceIterateOverTicks added, bid locking bug fixed: https://github.com/Uniswap/continuous-clearing-auction/blob/main/CHANGELOG.md
[3] _sellTokensAtClearingPrice L230–267, exitPartiallyFilledBid outbid path L632–644, ClearingPriceUpdated L399: ContinuousClearingAuction.sol
[4] TechnicalDocumentation.md L357–363 — Auction steps / step schedule: https://github.com/Uniswap/continuous-clearing-auction/blob/main/docs/TechnicalDocumentation.md
[5] TechnicalDocumentation.md L330–333 — Extra funds not recoverable: https://github.com/Uniswap/continuous-clearing-auction/blob/main/docs/TechnicalDocumentation.md
[6] TechnicalDocumentation.md L335–347 — Bounds on maximum bid prices: https://github.com/Uniswap/continuous-clearing-auction/blob/main/docs/TechnicalDocumentation.md
[7] TechnicalDocumentation.md L377–381 — Low-decimal / FoT token limitations: https://github.com/Uniswap/continuous-clearing-auction/blob/main/docs/TechnicalDocumentation.md
[8] CHANGELOG.md v1.1.0 — Bid locking bug fix and deployment addresses: https://github.com/Uniswap/continuous-clearing-auction/blob/main/CHANGELOG.md
[9] TechnicalDocumentation.md L365–375 — Bidder responsibilities / honeypot parameters: https://github.com/Uniswap/continuous-clearing-auction/blob/main/docs/TechnicalDocumentation.md
[10] Audit list: https://github.com/Uniswap/continuous-clearing-auction/blob/main/docs/audits/README.md
[11] Bug bounty: https://cantina.xyz/code/f9df94db-c7b1-434b-bb06-d1360abdd1be/overview