Skip to main content

Overview

First-come first-served (FCFS) sales allow broad public participation while enforcing user purchase constraints through per-entity caps and total supply limits. This pattern is ideal for standard token launches.

Use Case

  • Open public sale with no allowlist requirements
  • Per-entity caps to prevent individual dominance ($5k example)
  • Multiple wallets per entity allowed for convenience

Contract Implementation

This example extends the base ExampleSale contract. See the Smart Contract Integration Guide for the complete base implementation.
contract FCFSSale is ExampleSale {
    enum SaleStage { NotOpen, Open, Closed }
    
    uint256 public constant MAX_PER_ENTITY = 5_000_000_000; // $5k per entity
    uint256 public constant TOTAL_SALE_CAP = 10_000_000_000_000; // $10M total
    uint256 public totalRaised = 0;
    SaleStage public currentStage = SaleStage.NotOpen;

    error SaleCapExceeded(uint256 attempted, uint256 remaining);
    error EntityCapExceeded(uint256 attempted, uint256 maxAllowed);
    error SaleNotOpen(SaleStage current);
    
    event StageChanged(SaleStage newStage);

    modifier onlyDuringStage(SaleStage stage) {
        if (currentStage != stage) {
            revert SaleNotOpen(currentStage);
        }
        _;
    }

    constructor(bytes16 _saleUUID, address _signer) 
        ExampleSale(_saleUUID, _signer) {}
    
    function purchase(
        uint256 amount,
        PurchasePermitWithAllocation calldata purchasePermit,
        bytes calldata purchasePermitSignature
    ) external override onlyDuringStage(SaleStage.Open) {
        _validatePurchasePermit(purchasePermit, purchasePermitSignature);
        
        // Check total sale cap
        if (totalRaised + amount > TOTAL_SALE_CAP) {
            revert SaleCapExceeded(totalRaised + amount, TOTAL_SALE_CAP - totalRaised);
        }
        
        uint256 newTotalAmount = amountByAddress[msg.sender] + amount;

        // Validate permit allocation limits
        if (newTotalAmount < purchasePermit.minAmount) {
            revert AmountBelowMinimum(newTotalAmount, purchasePermit.minAmount);
        }
        if (newTotalAmount > purchasePermit.maxAmount) {
            revert AmountExceedsMaximum(newTotalAmount, purchasePermit.maxAmount);
        }

        // Additional per-entity cap check
        if (newTotalAmount > MAX_PER_ENTITY) {
            revert EntityCapExceeded(newTotalAmount, MAX_PER_ENTITY);
        }

        _trackEntity(purchasePermit.permit.entityID, msg.sender);
        amountByAddress[msg.sender] = newTotalAmount;
        totalRaised += amount;

        // // Note: If the purchaser was transferring tokens as part of the purchase, you would do that here.

        emit Purchased(msg.sender, purchasePermit.permit.entityID, amount, newTotalAmount);
    }
    
    function setSaleStage(SaleStage newStage) external onlyRole(DEFAULT_ADMIN_ROLE) {
        currentStage = newStage;
        emit StageChanged(newStage);
    }
    
    function remainingCap() external view returns (uint256) {
        return TOTAL_SALE_CAP - totalRaised;
    }
}

See Also

I