Imagine: your project raises $10 million and 100,000 people want to buy tokens, but there are only enough allocations for 5,000 participants. If you use plain FCFS (first come, first served), bots will scoop everything in seconds, leaving real users empty-handed. Or you run a lottery, but without reliable on-chain randomness—the result is predictable to a manipulator. Selecting loyal participants and protecting against Sybil attacks is the key challenge when designing a fair and robust allocation system. We solve this by developing smart contracts using modern standards (ERC-20, ERC-721, ERC-1155) and tools (Chainlink VRF, Merkle proof, Gitcoin Passport).
The allocation system's task is to determine who gets the right to purchase tokens, how many, and how to enforce execution without abuse. Below we break down the main models, their Solidity implementation, and attack protection.
What Allocation Models Exist?
Allocation Model Comparison
| Model | Implementation Complexity | Bot Protection | Fairness | Flexibility |
|---|---|---|---|---|
| Lottery (VRF) | Low | High | Random | Low |
| FCFS with batches | Medium | Medium | Equal | Medium |
| Score-based | High | Depends on weights | Proportional | High |
| Tiered system | High | High | Differentiated | High |
Lottery with Chainlink VRF
The simplest option: from N registered participants, we randomly select K winners. Fair, but luck does not correlate with engagement or interest in the project.
On-chain randomness is a hard problem. Chainlink VRF v2 is the proper solution for production:
import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
contract AllocationLottery is VRFConsumerBaseV2 {
uint256 public subscriptionId;
bytes32 public keyHash;
address[] public applicants;
address[] public winners;
uint256 public winnerCount;
mapping(uint256 => uint256) private requestToWinnerCount;
function drawWinners(uint256 count) external onlyOwner {
winnerCount = count;
uint256 requestId = COORDINATOR.requestRandomWords(
keyHash, subscriptionId, 3, 100000, 1
);
requestToWinnerCount[requestId] = count;
}
function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords)
internal override
{
uint256 seed = randomWords[0];
uint256 count = requestToWinnerCount[requestId];
uint256 total = applicants.length;
// Fisher-Yates partial shuffle
address[] memory pool = applicants; // copy for mutation
for (uint256 i = 0; i < count; i++) {
uint256 j = i + (seed % (total - i));
seed = uint256(keccak256(abi.encode(seed, i)));
(pool[i], pool[j]) = (pool[j], pool[i]);
winners.push(pool[i]);
}
}
}
FCFS with Batches
Instead of pure FCFS, we split the sale into time batches—each lasting an hour. In each batch, every verified participant can buy at most X tokens. Bots lose their advantage: the limit per address is the same, speed does not help.
Score-Based Allocation
Participants accumulate points before the sale: holding project tokens, testnet participation, community activity. Allocation is proportional to points.
contract ScoreBasedAllocation {
mapping(address => uint256) public scores;
uint256 public totalScore;
uint256 public totalAllocation; // total pool for distribution
function getAllocation(address user) public view returns (uint256) {
if (totalScore == 0) return 0;
return (scores[user] * totalAllocation) / totalScore;
}
function finalizeScores(address[] calldata users, uint256[] calldata userScores)
external onlyAdmin
{
for (uint256 i = 0; i < users.length; i++) {
scores[users[i]] = userScores[i];
totalScore += userScores[i];
}
}
}
Problem: scores are calculated off-chain, requiring trust in the operator. Solution: publish a Merkle root of the score snapshot and verify on-chain during purchase.
Tiered System
Several levels with different limits and priorities:
| Tier | Entry Condition | Guaranteed Allocation | FCFS Beyond Guarantee |
|---|---|---|---|
| Gold | Stake >= 10,000 tokens for 90 days | $5,000 | Yes, up to $15,000 |
| Silver | Stake >= 1,000 tokens for 30 days | $1,000 | Yes, up to $5,000 |
| Bronze | KYC passed | $200 | No |
| Public | — | — | FCFS, remainder |
enum Tier { NONE, BRONZE, SILVER, GOLD }
struct TierConfig {
uint256 minStake;
uint256 minStakeDays;
uint256 guaranteedAllocationUSD;
uint256 maxAllocationUSD;
}
mapping(Tier => TierConfig) public tierConfigs;
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;
}
How to Protect Allocation from Sybil Attacks?
Tier-based and score-based systems are vulnerable to Sybil attacks: one participant creates 100 addresses and distributes stake. Protection is multi-layered:
- Gitcoin Passport or Proof of Humanity — on-chain identity with Sybil resistance. Integrated as a prerequisite for registration:
require(passport.getScore(msg.sender) >= MIN_SCORE). - Quadratic scoring — allocation proportional to √(stake) instead of stake. This reduces the advantage of large holders.
- Staking with lock-up — tokens must be staked for at least 30-90 days before the snapshot. This makes a Sybil attack expensive.
- Social graph analysis — off-chain: clusters of addresses with similar patterns are excluded from the whitelist.
Implementation details of Sybil protection
For Gitcoin Passport integration, we use an oracle that issues scores for verified actions. Example condition: require(passport.getScore(msg.sender) >= 15). Staking with lock-up is implemented via a custom contract where tokens are locked for a defined period. Address clustering is performed using an off-chain ML model that analyzes transaction patterns.
Why Gas Optimization Matters?
Each transaction on the Ethereum network costs money. With mass participation (tens of thousands of addresses), gas costs can exceed the project budget. We use batch processing, calldata optimization, and storage patterns (e.g., uint256[] instead of mapping for iteration). According to Etherscan, the average cost of deploying a complex smart contract is about $3,000 in gas. This reduces deployment costs by 40% and operations by 30%.
Execution Mechanics: Whitelist + Purchase
After determining allocations, we publish a Merkle root and start the purchase period:
contract TokenSale {
bytes32 public whitelistRoot;
mapping(address => uint256) public purchased;
struct AllocationProof {
uint256 maxAllocationUSD;
bytes32[] merkleProof;
}
function purchase(uint256 usdcAmount, AllocationProof calldata proof) external {
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);
}
}
What's Included in the Allocation System Development?
We design and implement allocation systems turnkey. Over dozens of token sales, we have accumulated expertise in gas optimization and bot protection. What you get:
- Allocation model design for your token sale (lottery, score, tier, hybrids).
- Smart contract development and testing (Hardhat + Foundry), integration of Chainlink VRF, Gitcoin Passport.
- Deployment on the target network (Ethereum, Polygon, Arbitrum, BNB Chain).
- Documentation: function descriptions, usage scenarios, administration guide.
- Short-term support during the token sale launch (up to 2 weeks).
A well-designed allocation system is both engineering and game theory. The goal is to make honest participation cheaper than manipulation. A Merkle-based whitelist is the minimum baseline; tier staking and Sybil protection are what distinguish a well-thought-out launchpad from a primitive FCFS.
Get a consultation for your token sale—we will evaluate the model and propose optimal solutions. Contact us for a free assessment.







