Token Sale Allocation System Development
Standard problem at token sale launch: either whitelist oversubscribed 20x and most participants get zero, or allocations distributed "first-come, first-served" and bots buy from the side. Allocation system—mechanism for fair distribution of buying rights before sale starts.
System's task: determine who gets allocation, how much, ensure on-chain execution without abuse.
Allocation Calculation Models
Lottery (Random Selection)
Simplest: from N registered, randomly select K winners. Fair, but luck doesn't correlate with involvement or interest.
Randomness source—always a problem on-chain. Chainlink VRF v2—correct production solution:
import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
contract AllocationLottery is VRFConsumerBaseV2 {
address[] public applicants;
address[] public winners;
function drawWinners(uint256 count) external onlyOwner {
uint256 requestId = COORDINATOR.requestRandomWords(
keyHash, subscriptionId, 3, 100000, 1
);
}
function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords)
internal override
{
uint256 seed = randomWords[0];
// Fisher-Yates shuffle to select winners
}
}
FCFS with Time Batching
Divide sales into time batches: each batch 1 hour, each verified participant buys max X tokens. Bots lose advantage: per-address limit same, speed doesn't help.
Score-Based Allocations
Participants accumulate points pre-sale: token holding, testnet participation, community activity. Allocation proportional to points.
contract ScoreBasedAllocation {
mapping(address => uint256) public scores;
uint256 public totalScore;
uint256 public totalAllocation;
function getAllocation(address user) public view returns (uint256) {
if (totalScore == 0) return 0;
return (scores[user] * totalAllocation) / totalScore;
}
}
Tiered System
Multiple levels with different limits and priorities:
| Tier | Entry Condition | Guaranteed Allocation | FCFS Beyond |
|---|---|---|---|
| Gold | Stake >= 10,000 tokens 90 days | $5,000 | Up to $15,000 |
| Silver | Stake >= 1,000 tokens 30 days | $1,000 | Up to $5,000 |
| Bronze | KYC passed | $200 | No |
| Public | — | — | FCFS, remainder |
enum Tier { NONE, BRONZE, SILVER, GOLD }
mapping(address => Tier) public userTier;
function computeTier(address user) public view returns (Tier) {
uint256 staked = stakingContract.stakedAmountFor(user);
uint256 stakeDuration = stakingContract.stakeDurationFor(user);
if (staked >= tierConfigs[Tier.GOLD].minStake &&
stakeDuration >= tierConfigs[Tier.GOLD].minStakeDays * 1 days)
return Tier.GOLD;
if (staked >= tierConfigs[Tier.SILVER].minStake &&
stakeDuration >= tierConfigs[Tier.SILVER].minStakeDays * 1 days)
return Tier.SILVER;
if (kycRegistry.isVerified(user))
return Tier.BRONZE;
return Tier.NONE;
}
Execution: Whitelist + Purchase
After allocation determination—publish whitelist (merkle root) and purchase period:
contract TokenSale {
bytes32 public whitelistRoot;
mapping(address => uint256) public purchased;
function purchase(uint256 usdcAmount, AllocationProof calldata proof) external {
// Verify allocation
bytes32 leaf = keccak256(bytes.concat(
keccak256(abi.encode(msg.sender, proof.maxAllocationUSD))
));
require(MerkleProof.verify(proof.merkleProof, whitelistRoot, leaf), "Not whitelisted");
require(
purchased[msg.sender] + usdcAmount <= proof.maxAllocationUSD,
"Exceeds allocation"
);
uint256 tokenAmount = (usdcAmount * TOKEN_PRICE_DENOMINATOR) / tokenPriceUSD;
purchased[msg.sender] += usdcAmount;
usdc.transferFrom(msg.sender, treasury, usdcAmount);
token.transfer(msg.sender, tokenAmount);
emit Purchase(msg.sender, usdcAmount, tokenAmount);
}
}
Sybil Attack Protection
Tier-based and score-based systems vulnerable to Sybil: one participant creates 100 addresses, distributes stake. Protection:
Gitcoin Passport / Proof of Humanity — on-chain identity with Sybil resistance. Integrated as prerequisite: require(passport.getScore(msg.sender) >= MIN_SCORE).
Quadratic scoring — allocation proportional to √(stake), not stake. Reduces large-holder advantage, increases relative reward for small participants.
Staking with lockup — tokens must be staked minimum 30-90 days before snapshot. Expensive for Sybil: must buy tokens early.
Social graph analysis — off-chain: clusters of addresses with similar on-chain patterns (created same day, funded from one source) excluded from whitelist.
Well-designed allocation system—both technique and game theory. Goal: make honest participation cheaper than manipulation. Merkle-based whitelist—minimum baseline; tier staking and Sybil protection—what distinguishes thoughtful launchpad from primitive FCFS.







