Skip to main content
Status: Draft Type: Standards Track Category: Core Created: 2026-01-18 Requires: CIP-20, CIP-21

Abstract

CIP-22 defines Continuous Clearing Auctions (CCAs) for Cowboy, a fair price discovery mechanism for token launches that eliminates sniping, timing games, and information asymmetry. Tokens are released gradually over time, with each block settling at a uniform clearing price. Upon auction completion, proceeds automatically seed a CIP-21 liquidity pool. This design is inspired by Uniswap’s CCA mechanism, adapted for Cowboy’s actor model with native timer integration for gas-efficient per-block clearing. Key features:
  • Gradual price discovery: Tokens released per-block according to a schedule
  • Uniform pricing: All bidders in a block pay the same clearing price
  • Anti-sniping: Early bidders achieve better average prices
  • Automatic liquidity: Auction proceeds seed a V2 or V3 pool
  • Composable: Validation hooks for KYC, geographic restrictions, etc.

Motivation

The Token Launch Problem

Traditional token launches suffer from well-known issues:
MethodProblem
Fixed-price saleUnderpricing leaves value on table; overpricing fails
Dutch auctionSniping at the last moment; poor price discovery
Batch auctionAll-or-nothing; no gradual discovery
AMM launchBots front-run; massive volatility; MEV extraction
These mechanisms create timing games where sophisticated actors extract value from regular participants.

How CCAs Solve This

CCAs distribute tokens over time through continuous clearing:
  1. No sniping advantage: Supply is released every block; last-minute bids get no special treatment
  2. Early bidder advantage: Bids placed earlier fill across more blocks at (likely) lower average prices
  3. Uniform block pricing: All bidders in a block pay the same price—no information asymmetry
  4. Gradual convergence: Market has time to discover fair value

Why CCAs on Cowboy?

Continuous Clearing Auctions require per-block processing—releasing tokens, sorting bids, calculating clearing prices. On Ethereum, this creates a keeper dependency: someone must submit a transaction each block to trigger clearing, paying gas and creating MEV opportunities.

The Keeper Problem on Ethereum

ApproachProblem
External keeper/botSomeone must pay gas; dependency on altruistic/MEV actors
Lazy clearing on user actionClearing delayed until next bid; stale prices
Bundled into user txsUsers pay extra gas for clearing; unfair cost distribution
Flashbots/MEV searchersWorks but adds MEV middlemen; centralization risk
Over a 7-day auction (~100,800 blocks), someone must pay for ~100,800 clearing transactions.

Cowboy’s Timer Advantage

Cowboy’s native timers (CIP-1/CIP-5) eliminate the keeper problem entirely:
AspectEthereum CCACowboy CCA
Clearing triggerExternal tx requiredNative timer, automatic
Cost~50k gas per block (paid by someone)Timer lane, no extra user cost
ReliabilityDepends on keepers showing upGuaranteed by protocol
Missed blocksPossible if gas too highImpossible (timer always fires)
MEV surfaceKeeper selection is MEV opportunityDeterministic execution
FinalizationSeparate tx to seed LPAtomic via timer callback

Specific Benefits

  1. Zero Keeper Infrastructure: No bots, no keeper networks, no gas subsidies. The auction actor schedules a timer once at creation; the protocol handles the rest.
  2. Guaranteed Execution: Cowboy reserves 20% of block capacity for timers. Even during extreme congestion, clearing executes. No “clearing got delayed because gas was too high.”
  3. True Per-Block Clearing: On Ethereum, if no one calls the clear function, clearing is deferred and bids see stale prices. Cowboy timers guarantee actual per-block clearing for smoother price discovery.
  4. Lower Total Cost: Users only pay for their bid transactions. Clearing is “free” (uses reserved timer capacity, not user-paid gas).
  5. Atomic Graduation: When the auction ends, a single timer callback checks the minimum raise, seeds the LP pool, and enables claims—all atomically, no separate finalization transaction.
  6. Reduced MEV Surface: Timer execution is deterministic and protocol-ordered. No competition over “who gets to call clear,” no sandwich opportunities around the clearing transaction.
This makes CCAs a natural fit for Cowboy—the mechanism works as designed without the infrastructure overhead required on keeper-dependent chains.

Specification

Overview

CCA Lifecycle

Core Data Structures

Auction Configuration

@dataclass
class AuctionConfig:
    # Token being sold
    token_id: bytes32               # CIP-20 platform token

    # Currency accepted (e.g., USDC, CBY)
    currency_id: bytes32            # CIP-20 platform token

    # Timing
    start_block: u64                # Auction begins
    end_block: u64                  # Auction ends
    claim_block: u64                # When claims open (>= end_block)

    # Pricing
    floor_price: u256               # Minimum price (Q96 format)
    tick_spacing: u24               # Price granularity (basis points)

    # Supply
    total_supply: u256              # Total tokens to sell
    release_schedule: list[ReleaseStep]  # Per-block release rates

    # Graduation
    min_currency_raised: u256       # Minimum to graduate
    target_currency_raised: u256    # Target (for UI)

    # Liquidity seeding
    pool_type: PoolType             # V2 or V3
    pool_fee_bps: u16               # Pool fee tier
    lp_recipient: address           # Who receives LP tokens/position

    # Optional
    validation_hook: address | None # For KYC, geographic restrictions
    metadata_uri: str | None        # Auction details

@dataclass
class ReleaseStep:
    rate_mps: u24                   # Tokens per block in milli-basis points (1e-7)
    duration_blocks: u40            # How many blocks at this rate

@enum
class PoolType:
    V2_FULL_RANGE = 1
    V3_CONCENTRATED = 2

Bid Structure

@dataclass
class Bid:
    bid_id: u256
    bidder: address

    # Bid parameters
    currency_amount: u256           # Total spend budget
    max_price: u256                 # Maximum acceptable price (Q96)

    # Tracking
    currency_spent: u256            # How much has been used
    tokens_received: u256           # How much has been filled

    # Status
    created_block: u64
    is_withdrawn: bool

Auction State

@dataclass
class AuctionState:
    # Current state
    current_block: u64
    current_clearing_price: u256    # Q96 format
    tokens_released: u256
    tokens_sold: u256
    currency_raised: u256

    # Active demand (sorted by max_price descending)
    active_bids: SortedMap[u256, list[Bid]]  # max_price -> bids
    total_active_demand: u256       # Sum of remaining bid budgets

    # Checkpoints for pro-rata calculation
    checkpoints: list[Checkpoint]

    # Status
    status: AuctionStatus

@dataclass
class Checkpoint:
    block: u64
    clearing_price: u256
    currency_raised_cumulative: u256
    tokens_sold_cumulative: u256

@enum
class AuctionStatus:
    PENDING = 0
    ACTIVE = 1
    ENDED = 2
    GRADUATED = 3
    FAILED = 4

Auction Interface

class ICCA:
    """Continuous Clearing Auction interface"""

    # ─────────────────────────────────────────────────────────
    # Bidding
    # ─────────────────────────────────────────────────────────

    def place_bid(
        self,
        currency_amount: u256,
        max_price: u256
    ) -> u256:
        """
        Place a bid in the auction.

        Args:
            currency_amount: Total spend budget
            max_price: Maximum price willing to pay (Q96 format)

        Returns:
            bid_id

        Requirements:
            - Auction is ACTIVE
            - max_price >= floor_price
            - max_price >= current_clearing_price (or bid is out-of-range)
            - currency_amount > 0

        Behavior:
            - Transfers currency_amount from bidder to auction
            - Bid is automatically spread across remaining blocks
            - If max_price < current_clearing_price, bid is "out of range"
              and can be withdrawn
        """

    def withdraw_bid(self, bid_id: u256) -> u256:
        """
        Withdraw an out-of-range bid.

        Requirements:
            - Caller is bid owner
            - Bid's max_price < current_clearing_price

        Returns:
            currency_amount refunded
        """

    def increase_bid(self, bid_id: u256, additional_amount: u256):
        """
        Add more currency to an existing bid.

        Requirements:
            - Caller is bid owner
            - Auction is ACTIVE
        """

    def update_max_price(self, bid_id: u256, new_max_price: u256):
        """
        Update bid's maximum price.

        Requirements:
            - Caller is bid owner
            - new_max_price >= floor_price
        """

    # ─────────────────────────────────────────────────────────
    # Claiming
    # ─────────────────────────────────────────────────────────

    def claim(self, bid_id: u256) -> (u256, u256):
        """
        Claim tokens from a bid after auction ends.

        Requirements:
            - block.height >= claim_block
            - Auction status is GRADUATED or FAILED

        Returns:
            (tokens_received, currency_refunded)

        Behavior:
            - If GRADUATED: receive tokens, refund unspent currency
            - If FAILED: refund all currency
        """

    def claim_batch(self, bid_ids: list[u256]) -> (u256, u256):
        """Claim multiple bids at once."""

    # ─────────────────────────────────────────────────────────
    # Queries
    # ─────────────────────────────────────────────────────────

    def get_config(self) -> AuctionConfig: ...
    def get_state(self) -> AuctionState: ...
    def get_bid(self, bid_id: u256) -> Bid: ...
    def get_bids_for_user(self, user: address) -> list[Bid]: ...
    def get_clearing_price(self) -> u256: ...
    def get_tokens_available(self) -> u256: ...
    def estimate_fill(self, currency_amount: u256, max_price: u256) -> u256: ...

Clearing Mechanism

The core innovation of CCAs is per-block clearing. Each block:
  1. Release tokens according to schedule
  2. Sort active bids by max_price (descending)
  3. Fill bids from highest to lowest until supply exhausted
  4. Set clearing price to the marginal bid’s max_price
  5. Pro-rata fill any bids at exactly the clearing price

Clearing Algorithm

def clear_block(self):
    """Called by timer each block during auction."""

    if block.height < self.config.start_block:
        return
    if block.height > self.config.end_block:
        self._finalize_auction()
        return

    # 1. Calculate tokens to release this block
    tokens_to_release = self._get_release_amount(block.height)
    self.state.tokens_released += tokens_to_release

    # 2. Get active demand at or above floor
    active_bids = self._get_active_bids_sorted()  # Sorted by max_price DESC

    if len(active_bids) == 0:
        # No demand: price stays at floor, tokens accumulate
        self.state.current_clearing_price = self.config.floor_price
        return

    # 3. Calculate clearing price and fills
    tokens_remaining = tokens_to_release
    clearing_price = self.config.floor_price
    fills = []

    for price_tier, bids_at_tier in active_bids.items():
        if tokens_remaining == 0:
            break

        # Calculate demand at this price tier
        demand_at_tier = sum(
            self._remaining_demand(bid, price_tier)
            for bid in bids_at_tier
        )

        # How many tokens can be sold at this tier?
        tokens_at_tier = min(tokens_remaining, demand_at_tier)

        if tokens_at_tier == demand_at_tier:
            # Fully fill all bids at this tier
            for bid in bids_at_tier:
                fill_amount = self._remaining_demand(bid, price_tier)
                fills.append((bid, fill_amount, price_tier))
            tokens_remaining -= tokens_at_tier
        else:
            # Pro-rata fill at this tier (marginal price)
            clearing_price = price_tier
            for bid in bids_at_tier:
                bid_demand = self._remaining_demand(bid, price_tier)
                pro_rata_share = bid_demand * tokens_at_tier // demand_at_tier
                fills.append((bid, pro_rata_share, price_tier))
            tokens_remaining = 0
            break

        clearing_price = price_tier

    # 4. Execute fills
    for (bid, token_amount, price) in fills:
        currency_cost = token_amount * price // Q96
        bid.currency_spent += currency_cost
        bid.tokens_received += token_amount
        self.state.tokens_sold += token_amount
        self.state.currency_raised += currency_cost

    # 5. Update state
    self.state.current_clearing_price = clearing_price
    self.state.current_block = block.height

    # 6. Checkpoint for pro-rata claims
    self.state.checkpoints.append(Checkpoint(
        block=block.height,
        clearing_price=clearing_price,
        currency_raised_cumulative=self.state.currency_raised,
        tokens_sold_cumulative=self.state.tokens_sold
    ))

    emit_event("BlockCleared", {
        "block": block.height,
        "tokens_released": tokens_to_release,
        "tokens_sold": sum(f[1] for f in fills),
        "clearing_price": clearing_price,
        "currency_raised": self.state.currency_raised
    })

Remaining Demand Calculation

def _remaining_demand(self, bid: Bid, price: u256) -> u256:
    """
    Calculate how many tokens a bid can still purchase at given price.

    The bid's budget is spread across remaining blocks.
    """
    if bid.max_price < price:
        return 0  # Bid is out of range

    remaining_currency = bid.currency_amount - bid.currency_spent
    remaining_blocks = self.config.end_block - block.height

    # Currency available this block (spread evenly)
    currency_this_block = remaining_currency // remaining_blocks

    # Convert to token demand at this price
    token_demand = currency_this_block * Q96 // price

    return token_demand

Release Schedules

The release schedule controls how tokens are distributed over time. Common patterns:

Constant Release

# Release 1M tokens over 1000 blocks = 1000 tokens/block
release_schedule = [
    ReleaseStep(rate_mps=1_000_000, duration_blocks=1000)
]

Front-loaded (Faster Start)

# Higher release early, tapering off
release_schedule = [
    ReleaseStep(rate_mps=2_000_000, duration_blocks=250),   # 2x first quarter
    ReleaseStep(rate_mps=1_000_000, duration_blocks=500),   # Normal middle
    ReleaseStep(rate_mps=500_000, duration_blocks=250),     # 0.5x final quarter
]

Back-loaded (Slower Start)

# Lower release early, accelerating
release_schedule = [
    ReleaseStep(rate_mps=500_000, duration_blocks=250),
    ReleaseStep(rate_mps=1_000_000, duration_blocks=500),
    ReleaseStep(rate_mps=2_000_000, duration_blocks=250),
]

Graduation and Liquidity Seeding

When the auction ends successfully:
def _finalize_auction(self):
    """Called after end_block."""

    if self.state.currency_raised < self.config.min_currency_raised:
        # Failed: refund everyone
        self.state.status = AuctionStatus.FAILED
        emit_event("AuctionFailed", {
            "currency_raised": self.state.currency_raised,
            "min_required": self.config.min_currency_raised
        })
        return

    # Success: seed liquidity pool
    self.state.status = AuctionStatus.GRADUATED

    final_price = self.state.current_clearing_price
    unsold_tokens = self.state.tokens_released - self.state.tokens_sold
    currency_for_lp = self.state.currency_raised
    tokens_for_lp = unsold_tokens  # Or could be a configured portion

    if self.config.pool_type == PoolType.V2_FULL_RANGE:
        # Seed V2 pool
        pool = Factory.get_or_create_v2_pool(
            self.config.token_id,
            self.config.currency_id,
            self.config.pool_fee_bps
        )

        Token.approve(self.config.token_id, pool.address, tokens_for_lp)
        Token.approve(self.config.currency_id, pool.address, currency_for_lp)

        (amount_a, amount_b, lp_tokens) = pool.add_liquidity(
            tokens_for_lp,
            currency_for_lp,
            0, 0,  # No slippage check (we control the price)
            self.config.lp_recipient
        )

    elif self.config.pool_type == PoolType.V3_CONCENTRATED:
        # Seed V3 pool with concentrated position around final price
        pool = Factory.get_or_create_v3_pool(
            self.config.token_id,
            self.config.currency_id,
            self.config.pool_fee_bps,
            tick_spacing=60
        )

        # Full range position
        tick_lower = -887220  # MIN_TICK
        tick_upper = 887220   # MAX_TICK

        position_id = pool.mint_position(
            tick_lower,
            tick_upper,
            tokens_for_lp,
            self.config.lp_recipient
        )

    emit_event("AuctionGraduated", {
        "final_price": final_price,
        "currency_raised": self.state.currency_raised,
        "tokens_sold": self.state.tokens_sold,
        "pool": pool.address,
        "lp_recipient": self.config.lp_recipient
    })

Validation Hooks

CCAs support validation hooks for compliance:
class ICCAHook:
    """Validation hook interface for CCAs"""

    def can_bid(
        self,
        auction: address,
        bidder: address,
        currency_amount: u256,
        max_price: u256
    ) -> bool:
        """
        Called before accepting a bid.

        Use for: KYC verification, geographic restrictions,
                 accredited investor checks, bid limits.
        """

    def on_bid(
        self,
        auction: address,
        bidder: address,
        bid_id: u256,
        currency_amount: u256,
        max_price: u256
    ) -> None:
        """Called after bid is placed. For analytics."""

    def can_claim(
        self,
        auction: address,
        bidder: address,
        bid_id: u256
    ) -> bool:
        """Called before allowing claim. For vesting, lockups."""

Example: KYC Hook

class KYCAuctionHook(Actor):
    """Only verified users can bid"""

    def init(self, kyc_registry: address):
        self.kyc_registry = kyc_registry

    def can_bid(self, auction, bidder, currency_amount, max_price) -> bool:
        return KYCRegistry(self.kyc_registry).is_verified(bidder)

    def on_bid(self, auction, bidder, bid_id, currency_amount, max_price):
        emit_event("KYCBidPlaced", {"bidder": bidder, "bid_id": bid_id})

    def can_claim(self, auction, bidder, bid_id) -> bool:
        # Always allow claim (can't trap funds)
        return True

Example: Geographic Restriction Hook

class GeoRestrictedHook(Actor):
    """Block certain jurisdictions"""

    def init(self, blocked_countries: list[str], attestation_registry: address):
        self.blocked_countries = set(blocked_countries)
        self.attestation_registry = attestation_registry

    def can_bid(self, auction, bidder, currency_amount, max_price) -> bool:
        attestation = AttestationRegistry(self.attestation_registry).get(bidder)
        if attestation is None:
            return False
        return attestation.country not in self.blocked_countries

Timer Integration

Cowboy’s native timers enable automatic per-block clearing:
class ContinuousClearingAuction(Actor):
    """CCA implementation with timer-based clearing"""

    def init(self, config: AuctionConfig):
        self.config = config
        self.state = AuctionState(...)

        # Schedule clearing timer for each block during auction
        self.schedule_timer(
            trigger_type="height",
            start_height=config.start_block,
            interval=1,  # Every block
            end_height=config.end_block + 1,
            handler="clear_block"
        )

        # Schedule finalization
        self.schedule_timer(
            trigger_type="height",
            due_height=config.end_block + 1,
            handler="finalize"
        )

    def clear_block(self):
        """Timer callback: process clearing for current block"""
        # ... clearing logic as above ...

    def finalize(self):
        """Timer callback: finalize auction and seed liquidity"""
        self._finalize_auction()

Factory

class ICCAFactory:
    """Factory for creating CCAs"""

    def create_auction(
        self,
        token_id: bytes32,
        currency_id: bytes32,
        total_supply: u256,
        floor_price: u256,
        start_block: u64,
        end_block: u64,
        release_schedule: list[ReleaseStep],
        min_currency_raised: u256,
        pool_type: PoolType,
        pool_fee_bps: u16,
        lp_recipient: address,
        validation_hook: address | None = None,
        metadata_uri: str | None = None
    ) -> address:
        """
        Create a new CCA.

        Requirements:
            - Caller has approval to transfer total_supply of token_id
            - end_block > start_block
            - release_schedule covers full duration

        Returns:
            Auction actor address
        """

    def create_auction_with_token(
        self,
        name: str,
        symbol: str,
        total_supply: u256,
        currency_id: bytes32,
        floor_price: u256,
        start_block: u64,
        end_block: u64,
        release_schedule: list[ReleaseStep],
        min_currency_raised: u256,
        pool_type: PoolType,
        pool_fee_bps: u16,
        lp_recipient: address,
        validation_hook: address | None = None
    ) -> (bytes32, address):
        """
        Create a new token and CCA in one transaction.

        Returns:
            (token_id, auction_address)
        """

Events

# Auction lifecycle
AuctionCreated(auction: address, token: bytes32, currency: bytes32, config: AuctionConfig)
AuctionStarted(auction: address, start_block: u64)
AuctionEnded(auction: address, end_block: u64)
AuctionGraduated(auction: address, final_price: u256, currency_raised: u256, pool: address)
AuctionFailed(auction: address, currency_raised: u256, min_required: u256)

# Per-block clearing
BlockCleared(auction: address, block: u64, tokens_released: u256, tokens_sold: u256, clearing_price: u256)

# Bidding
BidPlaced(auction: address, bidder: address, bid_id: u256, currency_amount: u256, max_price: u256)
BidWithdrawn(auction: address, bidder: address, bid_id: u256, currency_refunded: u256)
BidIncreased(auction: address, bid_id: u256, additional_amount: u256)
BidPriceUpdated(auction: address, bid_id: u256, old_price: u256, new_price: u256)

# Claims
TokensClaimed(auction: address, bidder: address, bid_id: u256, tokens: u256, currency_refunded: u256)

Security Considerations

Price Manipulation

  • TWAP resistance: Clearing price is demand-weighted, not spot-manipulable
  • No flash loans: Bids are non-withdrawable while in range
  • Gradual discovery: Manipulation must be sustained across many blocks

Bid Griefing

  • Out-of-range withdrawal: Bids below clearing price can always be withdrawn
  • No trapped funds: Failed auctions refund all currency
  • Gas-efficient claims: Batch claiming supported

Hook Security

  • Hooks capped at 50,000 cycles (same as CIP-20/CIP-21)
  • can_claim must return true for funds to be claimable (hooks should not trap funds)
  • Timelock recommended for hook updates

Integer Precision

  • Q96 format for prices (matches Uniswap V3)
  • Pro-rata rounding: Rounds down (conservative for users)
  • Overflow checks: Max supply capped at 1e30 wei

Rationale

Why Not Dutch Auction?

Dutch auctions suffer from:
  • Sniping: Everyone waits for the last moment
  • Winner’s curse: Early bidders overpay
  • All-or-nothing: Single clearing moment
CCAs distribute over time, rewarding early participation.

Why Not Batch Auction?

Batch auctions are good but:
  • Single clearing: No gradual discovery
  • Timing games: Coordinate around deadline
  • Less information: Market can’t observe demand curve
CCAs reveal demand continuously.

Why Timer-Based Clearing?

Cowboy’s native timers enable:
  • No keeper costs: Clearing is automatic
  • Deterministic execution: Same result on all nodes
  • Gas efficiency: Timer execution uses reserved block capacity

Example: Token Launch

from cowboy_sdk import Token, CCAFactory

# 1. Create token
my_token = Token.create(
    name="My Protocol Token",
    symbol="MPT",
    decimals=18,
    initial_supply=100_000_000 * 10**18  # 100M tokens
)

# 2. Configure auction (sell 50% of supply)
auction_supply = 50_000_000 * 10**18

release_schedule = [
    # Constant release over 7 days (~605k blocks at 1s)
    ReleaseStep(rate_mps=500_000, duration_blocks=604_800)
]

# 3. Create auction
auction = CCAFactory.create_auction(
    token_id=my_token,
    currency_id=USDC,
    total_supply=auction_supply,
    floor_price=to_q96(0.10),  # $0.10 floor
    start_block=block.height + 100,  # Start in ~10 minutes
    end_block=block.height + 100 + 100_800,  # 7 days
    release_schedule=release_schedule,
    min_currency_raised=1_000_000 * 10**6,  # $1M minimum
    pool_type=PoolType.V2_FULL_RANGE,
    pool_fee_bps=30,  # 0.30% fee
    lp_recipient=TREASURY,
    metadata_uri="ipfs://..."
)

# 4. Transfer tokens to auction
Token.transfer(my_token, auction.address, auction_supply)

Backwards Compatibility

This is a new standard. No backwards compatibility concerns.

Reference Implementation

See examples/cca/ for reference implementation.

Acknowledgments

This design is inspired by Uniswap’s Continuous Clearing Auctions and their Liquidity Launchpad.