PRIV ProtocolPRIV Docs
Contracts

PrivPresale

Multi-stage presale contract with 10 progressive pricing stages, ETH/USDC/card payments, Chainlink oracle integration, and 20% TGE + 6-month linear vesting.

Overview

PrivPresale is a multi-stage token distribution contract that allocates 15 million PRIV across 10 pricing stages. Each stage has a fixed token supply and a progressively higher price, ranging from $0.020 to $0.150 per token. Buyers can pay with ETH (converted via Chainlink price feed), USDC, or credit card (recorded by admin via MoonPay integration). All purchases are subject to per-wallet caps and vesting: 20% unlocks at TGE, with the remaining 80% vesting linearly over 6 months.

PropertyValue
ContractPrivPresale
Address (Base Sepolia)0xcD582BeE71c52e363EBb994BB7d87D5c774A59Fe
Total Allocation15,000,000 PRIV
Stages10
Price Range$0.020 -- $0.150
TGE Unlock20%
Vesting Duration180 days (linear)
Min Purchase$10 USD
Max Purchase$50,000 USD
NetworkBase (Chain ID: 8453)

Stage Pricing

The presale is divided into 10 stages with increasing prices and decreasing per-wallet limits. When a stage sells out, the next stage activates automatically.

StagePrice (USD)Token AllocationMax Per WalletTotal Raise
1$0.0202,000,000500,000$40,000
2$0.0252,000,000500,000$50,000
3$0.0301,500,000400,000$45,000
4$0.0401,500,000400,000$60,000
5$0.0501,500,000300,000$75,000
6$0.0601,500,000300,000$90,000
7$0.0751,250,000250,000$93,750
8$0.0901,250,000250,000$112,500
9$0.1201,250,000200,000$150,000
10$0.1501,250,000200,000$187,500

Total: 15,000,000 PRIV across all stages, raising up to $903,750 if fully subscribed.


How It Works

Buyer sends ETH/USDC  -->  USD amount calculated  -->  Tokens allocated  -->  Stage auto-advances
       |                          |                         |                        |
       v                          v                         v                        v
  buyWithETH()             Chainlink oracle          Per-wallet cap           Next stage activates
  buyWithUSDC()            (1hr staleness)           enforced                 when sold out
  recordCardPurchase()

Stage Advancement

  1. Each stage has a fixed tokenAllocation and starts in order (stage 1 active at deploy).
  2. When tokensSold >= tokenAllocation for the current stage, it is marked inactive.
  3. The next stage is activated automatically and currentStageIndex increments.
  4. If a purchase would exceed the remaining allocation, the buyer receives only the remaining tokens and any excess ETH is refunded.
  5. After all 10 stages complete, currentStageIndex is set to NUM_STAGES (10) and no further purchases are accepted.

Payment Methods

ETH Payments

buyWithETH(stageIndex) converts the sent ETH to a USD amount using the Chainlink ETH/USD price feed. The feed must return a positive, non-stale price (updated within 1 hour). If the purchase is capped by the remaining stage allocation, excess ETH is refunded to the buyer.

function buyWithETH(uint256 stageIndex) external payable nonReentrant whenNotPaused

USDC Payments

buyWithUSDC(usdcAmount, stageIndex) treats USDC as 1:1 with USD (both use 6 decimals). Requires prior ERC-20 approval. Only the exact USDC needed is transferred from the buyer.

function buyWithUSDC(uint256 usdcAmount, uint256 stageIndex) external nonReentrant whenNotPaused

Card Payments (Admin-Recorded)

recordCardPurchase(buyer, tokenAmount, usdPaid, stageIndex) is an owner-only function for off-chain credit card purchases processed via MoonPay. The admin records the buyer address, token amount, and USD paid after the off-chain payment is confirmed.

function recordCardPurchase(
    address buyer,
    uint256 tokenAmount,
    uint256 usdPaid,
    uint256 stageIndex
) external onlyOwner nonReentrant whenNotPaused

The contract uses a Chainlink AggregatorV3Interface for ETH/USD conversion. Three safety checks are enforced on every ETH purchase:

  1. Round completeness: answeredInRound >= roundId
  2. Positive price: price > 0
  3. Staleness threshold: block.timestamp - updatedAt <= 1 hour

If any check fails, the transaction reverts with StalePriceFeed() or InvalidPriceFeed().


Vesting Schedule

All presale purchases follow the same vesting schedule, regardless of stage or payment method.

PhaseUnlockTiming
TGE20% of total purchased tokensImmediately when triggerTGE() is called
Linear VestingRemaining 80%Linearly over 180 days after TGE

Claiming

After TGE is triggered, buyers call claim() to withdraw their vested tokens. The claimable amount is calculated as:

tgeAmount = totalTokens * 20 / 100
vestedFromSchedule = (totalTokens - tgeAmount) * elapsed / VESTING_DURATION
totalClaimable = tgeAmount + vestedFromSchedule - alreadyClaimed

Buyers can claim multiple times as tokens continue to vest. After 180 days, the full allocation is claimable.


Contract Interface

Constants

uint256 public constant TOTAL_PRESALE_TOKENS = 15_000_000 * 10 ** 18;
uint256 public constant NUM_STAGES = 10;
uint256 public constant TGE_BPS = 2000;                  // 20%
uint256 public constant BPS_DENOMINATOR = 10_000;
uint256 public constant VESTING_DURATION = 180 days;
uint256 public constant MIN_PURCHASE_USD = 10 * 10 ** 6;  // $10
uint256 public constant MAX_PURCHASE_USD = 50_000 * 10 ** 6; // $50,000
uint256 public constant PRICE_FEED_STALENESS = 1 hours;

State Variables

IERC20 public immutable privToken;          // PRIV token contract
IERC20 public immutable usdc;               // USDC token contract
AggregatorV3Interface public immutable ethPriceFeed; // Chainlink ETH/USD

Stage[10] public stages;                     // 10 presale stages
mapping(address => VestingSchedule) public vestingSchedules;
mapping(address => mapping(uint256 => uint256)) public walletStagePurchases;

uint256 public tgeTimestamp;                 // 0 until triggerTGE() called
uint256 public totalTokensSold;
uint256 public totalUsdRaised;               // 6 decimals
uint256 public currentStageIndex;
uint256 public totalBuyers;
bool public initialized;

Structs

Stage

struct Stage {
    uint256 priceUsd;           // Price per token in USD (6 decimals)
    uint256 tokenAllocation;    // Total tokens in this stage (18 decimals)
    uint256 tokensSold;         // Tokens sold so far (18 decimals)
    uint256 maxPerWallet;       // Per-wallet cap (18 decimals)
    bool active;                // Currently active stage
}

VestingSchedule

struct VestingSchedule {
    uint256 totalTokens;    // Total tokens purchased
    uint256 claimedTokens;  // Tokens already claimed
}

Functions

Buyer Functions

buyWithETH

Purchase tokens with ETH. Converts to USD via Chainlink. Refunds excess ETH.

function buyWithETH(uint256 stageIndex) external payable nonReentrant whenNotPaused
ParameterTypeDescription
stageIndexuint256Stage to purchase from (0-9)

Requirements:

  • Contract must be initialized
  • msg.value > 0
  • Stage must be active
  • USD equivalent must be between $10 and $50,000
  • Wallet must not exceed per-stage cap

buyWithUSDC

Purchase tokens with USDC. Requires prior approval.

function buyWithUSDC(uint256 usdcAmount, uint256 stageIndex) external nonReentrant whenNotPaused
ParameterTypeDescription
usdcAmountuint256USDC to spend (6 decimals)
stageIndexuint256Stage to purchase from (0-9)

Requirements:

  • Contract must be initialized
  • usdcAmount > 0
  • Sufficient USDC allowance granted to contract

claim

Claim vested tokens after TGE.

function claim() external nonReentrant

Requirements:

  • TGE must have been triggered
  • Caller must have claimable tokens greater than 0

View Functions

getCurrentStage

Returns the current active stage details.

function getCurrentStage() external view returns (
    uint256 stageIndex,
    uint256 priceUsd,
    uint256 tokenAllocation,
    uint256 tokensSold,
    uint256 maxPerWallet,
    bool isActive
)

getStageInfo

Returns details for a specific stage.

function getStageInfo(uint256 stageIndex) external view returns (
    uint256 priceUsd,
    uint256 tokenAllocation,
    uint256 tokensSold,
    uint256 maxPerWallet,
    bool active
)

getAllStages

Returns all 10 stage configurations.

function getAllStages() external view returns (Stage[10] memory)

getUserAllocation

Returns a user's purchase and vesting details.

function getUserAllocation(address user) external view returns (
    uint256 totalTokens,
    uint256 claimedTokens,
    uint256 claimableTokens,
    uint256 vestingStart,
    uint256 vestingEnd
)

getClaimable

Returns the number of tokens currently claimable by a user.

function getClaimable(address user) external view returns (uint256)

getEthPrice

Returns the current ETH/USD price from Chainlink (8 decimals).

function getEthPrice() external view returns (uint256)

calculateTokensForUsd

Calculates how many tokens a given USD amount would purchase at the current stage.

function calculateTokensForUsd(uint256 usdAmount) external view returns (uint256)

Admin Functions

initialize

Verifies the contract holds at least 15M PRIV and marks the presale as initialized. Must be called before any purchases.

function initialize() external onlyOwner

triggerTGE

Triggers the Token Generation Event, starting the vesting clock for all buyers. Can only be called once.

function triggerTGE() external onlyOwner

withdrawETH

Withdraws all collected ETH to a specified address.

function withdrawETH(address to) external onlyOwner

withdrawTokens

Withdraws collected USDC or other ERC-20 tokens. When withdrawing PRIV, only unallocated tokens (above total sold) can be withdrawn.

function withdrawTokens(address token, address to) external onlyOwner

pause / unpause

Emergency pause controls. Pausing blocks all purchases and card recordings.

function pause() external onlyOwner
function unpause() external onlyOwner

Events

event PresaleInitialized(uint256 timestamp);

event TokensPurchased(
    address indexed buyer,
    uint256 indexed stageIndex,
    uint256 tokenAmount,
    uint256 usdAmount,
    string paymentMethod       // "eth", "usdc", or "card"
);

event StageCompleted(uint256 indexed stageIndex, uint256 tokensSold);
event StageActivated(uint256 indexed stageIndex, uint256 priceUsd);
event TGETriggered(uint256 timestamp);
event TokensClaimed(address indexed buyer, uint256 amount);
event FundsWithdrawn(address indexed token, address indexed to, uint256 amount);
event ETHWithdrawn(address indexed to, uint256 amount);

Errors

error InvalidAddress();
error AlreadyInitialized();
error NotInitialized();
error StageNotActive();
error InvalidStageIndex();
error StageSoldOut();
error BelowMinPurchase();          // Below $10
error AboveMaxPurchase();          // Above $50,000
error WalletLimitExceeded();
error InsufficientPayment();
error TGEAlreadyTriggered();
error TGENotTriggered();
error NothingToClaim();
error StalePriceFeed();            // Chainlink data > 1 hour old
error InvalidPriceFeed();          // Chainlink returned <= 0
error PresaleComplete();
error InsufficientTokenBalance();  // Contract does not hold 15M PRIV
error ZeroAmount();
error RefundFailed();              // ETH refund transfer failed

Usage Examples

Buy with ETH

import { useWriteContract } from 'wagmi'
import { parseEther } from 'viem'

function useBuyWithETH() {
  const { writeContract } = useWriteContract()

  return (ethAmount: string, stageIndex: number) => {
    writeContract({
      address: PRESALE_ADDRESS,
      abi: presaleAbi,
      functionName: 'buyWithETH',
      args: [BigInt(stageIndex)],
      value: parseEther(ethAmount),
    })
  }
}

Buy with USDC

import { useWriteContract } from 'wagmi'

function useBuyWithUSDC() {
  const { writeContract } = useWriteContract()

  return async (usdcAmount: bigint, stageIndex: number) => {
    // 1. Approve USDC spending
    await writeContract({
      address: USDC_ADDRESS,
      abi: erc20Abi,
      functionName: 'approve',
      args: [PRESALE_ADDRESS, usdcAmount],
    })

    // 2. Execute purchase
    await writeContract({
      address: PRESALE_ADDRESS,
      abi: presaleAbi,
      functionName: 'buyWithUSDC',
      args: [usdcAmount, BigInt(stageIndex)],
    })
  }
}

Check Allocation and Claim

import { useReadContract, useWriteContract } from 'wagmi'

function usePresaleAllocation(address: string) {
  const { data: allocation } = useReadContract({
    address: PRESALE_ADDRESS,
    abi: presaleAbi,
    functionName: 'getUserAllocation',
    args: [address],
  })

  const { writeContract } = useWriteContract()

  const claim = () => {
    writeContract({
      address: PRESALE_ADDRESS,
      abi: presaleAbi,
      functionName: 'claim',
    })
  }

  return { allocation, claim }
}

Read Current Stage

function useCurrentStage() {
  const { data: stage } = useReadContract({
    address: PRESALE_ADDRESS,
    abi: presaleAbi,
    functionName: 'getCurrentStage',
  })

  // stage = [stageIndex, priceUsd, tokenAllocation, tokensSold, maxPerWallet, isActive]
  return stage
}

Security Notes

  1. Chainlink Staleness Protection: ETH purchases revert if the price feed is older than 1 hour, preventing stale-price exploits.

  2. Per-Wallet Caps: Each stage enforces a maximum token purchase per wallet address, tracked independently per stage via walletStagePurchases.

  3. ETH Refund Safety: If a purchase is capped by remaining stage allocation, excess ETH is refunded. If the refund transfer fails, the entire transaction reverts.

  4. Reentrancy Protection: All purchase and claim functions use the nonReentrant modifier from OpenZeppelin.

  5. PRIV Token Protection: The withdrawTokens function prevents the owner from withdrawing PRIV tokens that are allocated to buyers. Only unallocated surplus can be removed.

  6. Emergency Pause: The owner can pause all purchases in an emergency. Claiming is not affected by pause (it uses nonReentrant only, not whenNotPaused).

  7. Direct ETH Rejection: The receive() function reverts, forcing buyers to use buyWithETH() with a valid stage index.


Source Code

View on GitHub