In a token sale, the smart contract is the core of the business. It holds investor funds, enforces the rules, and cannot be recalled once deployed. A flaw is not a bug report. It is a permanent, public, financial event.
This guide covers ICO smart contract development end to end: architecture, token and sale contract design, vesting, security, testing, audits, deployment, and Solana. Code samples use current OpenZeppelin contracts v5 patterns, are educational only, and are not audited for production use.
Key Takeaways
- A complete ICO system is several small, focused contracts, not one large one.
- Fewer privileged roles mean a smaller attack surface and a stronger trust story.
- Pull-based claims, refunds, and withdrawals are safer than pushing funds to many addresses, or tying fund transfers to state changes.
- Testing must cover failure paths (soft cap missed, refunds, underfunded sale), not only the happy path.
- An independent audit reduces risk but does not eliminate it. Every material fix should be re-audited.
- Solana projects use a different language, account model, and toolchain from EVM chains, and need a separate audit.
In This Guide
- What is ICO smart contract development?
- Architecture, chain choice, and tooling
- Stage 1: The token contract
- Stage 2: The sale contract
- Stage 3: Vesting contracts
- Stage 4: Access control, admin functions, and upgradeability
- Multi-asset sales and price oracles
- Solana: building a token sale program
- Security: vulnerabilities and mitigations
- Testing before the audit
- The audit process
- Deployment and verification
- Cost and timeline factors
- Choosing a development partner
- Pre-audit checklist, common mistakes, and FAQ
What Is ICO Smart Contract Development?
ICO smart contract development is the engineering of the on-chain programs that create a token, accept contributions, enforce sale rules, and distribute tokens to investors. A standard ICO system includes a token contract, a sale (crowdsale) contract, vesting contracts, whitelist and access control, and a multi-signature treasury, all tested, independently audited, and verified on a block explorer before launch.
On EVM chains this work is done in Solidity with libraries such as OpenZeppelin Contracts. On Solana it is done in Rust, usually with the Anchor framework.
ICO Smart Contract Architecture at a Glance
| Component | Responsibility | Key design decisions |
|---|---|---|
| Token contract | Defines the asset (supply, transfers, burning) | Standard (ERC-20), fixed or capped supply, no hidden mint |
| Sale contract | Accepts contributions and enforces sale rules | Pricing, soft/hard caps, per-wallet limits, time window, funding checks |
| Whitelist / access control | Restricts participation to verified addresses | Merkle proofs; immutable root (simplest) or an updatable root behind a timelock |
| Vesting contracts | Release locked tokens on a schedule | Cliff, linear release, beneficiary, revocability (requires custom code) |
| Treasury | Receives raised funds | Multi-signature wallet, never a single key |
| Timelock | Delays sensitive admin actions | Delay length, who can propose and execute |
| Price oracle (optional) | Converts multi-asset contributions to a common value | Decentralized feed, staleness and sanity checks, L2 sequencer checks |
Choosing the Chain and Language
| Chain family | Language | Token standard | Typical tooling |
|---|---|---|---|
| Ethereum and EVM chains (BNB Chain, Polygon, Arbitrum, Optimism, Base) | Solidity | ERC-20, BEP-20 | Foundry, Hardhat, OpenZeppelin |
| Solana | Rust | SPL Token / Token-2022 | Anchor, Solana CLI |
EVM chains share one contract model, so a well-tested Solidity codebase ports across them with chain-specific configuration, including the EVM target version (see Deployment below). Solana requires a separate implementation. See the Solana section below.
The Development Stack
| Layer | Common tools |
|---|---|
| Language | Solidity (EVM), Rust (Solana) |
| Libraries | OpenZeppelin Contracts |
| Frameworks | Foundry, Hardhat |
| Testing and analysis | Forge tests and fuzzing, invariant tests, Slither, Echidna |
| Formal verification (high-value logic) | Certora Prover, Halmos |
| Whitelist trees | @openzeppelin/merkle-tree |
| Treasury security | Safe (multi-signature wallets) |
| Client libraries | ethers.js, viem |
| Verification | Block explorer source verification |
Stage 1: The Token Contract
The token contract defines the asset investors receive. For most ICOs, the safest design is the simplest one: a fixed supply minted once to a treasury address, with no mint function afterwards.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
/// @notice Educational token. Not audited.
contract ProjectToken is ERC20, ERC20Burnable, ERC20Permit {
constructor(address treasury, uint256 supply)
ERC20("Project Token", "PRJ")
ERC20Permit("Project Token")
{
require(treasury != address(0), "zero treasury");
_mint(treasury, supply * 10 ** decimals());
}
}
Why this design works
- Fixed supply, no mint function. Investors can verify that supply cannot be inflated.
- Burnable. Holders can reduce supply voluntarily, which supports deflationary tokenomics.
- Permit (EIP-2612). Allows gasless approvals through signed messages. The token’s nonces and deadline already prevent replay. Apps that call
permit()should tolerate a front-run permit (for example, wrap the call in try/catch) so a griefing transaction cannot block the user’s action. - No owner. There is no privileged key to compromise.
Add features only when the business case demands them. Pausable transfers, blacklists, or mint roles each add an attack surface and a trust question, and each must be disclosed to investors in plain language.
The sale contract below assumes a plain ERC-20 like this one. Fee-on-transfer, rebasing, or other non-standard tokens break its accounting and should not be used as the sale token.
Stage 2: The Sale (Crowdsale) Contract
The token sale smart contract holds investor funds, so it deserves the most scrutiny. The example below is an ERC-20 token sale contract that implements a whitelisted, capped, fixed-price sale with pull-based claims, refunds, and treasury withdrawal; overflow bounds on pricing math; a funding check on every contribution; and accurate recovery of excess tokens.
Design decisions
- No admin keys. Parameters are immutable.
finalize(),withdrawRaised(), andrecoverExcessTokens()can be called by anyone, and each only moves funds to the buyer or the immutable treasury. - Pull over push. Buyers claim tokens or refunds themselves, which avoids looping over many addresses.
finalize()only updates state. Moving ETH to the treasury is a separatewithdrawRaised()call, so a problem on the treasury side can never block buyers from claiming or refunding. - Funded before filled. Every contribution must be fully backed by tokens the contract already holds. An underfunded sale cannot accept money it could not pay out, and nobody can finalize a sale into failure by calling
finalize()before the tokens arrive. - Overflow-safe rate bounds. The constructor requires a non-zero hard cap and ensures
rate * hardCapcannot overflowuint256. - Checks-effects-interactions plus a reentrancy guard. State is updated before any external call.
- Accurate token accounting.
tokensOutstandingrecords what buyers are still owed and falls as they claim, so excess recovery never locks or over-releases tokens. - Treasury is immutable and must be a multi-signature wallet that can receive ETH. Send a test transfer to it and confirm the address on-chain before deploying, because an immutable treasury that rejects ETH cannot be changed.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
/// @notice Educational token sale. Not audited. Do not use it in production without independent review.
/// @dev Assumes a plain ERC-20 sale token (no fees on transfer, no rebasing).
contract TokenSale is ReentrancyGuard {
using SafeERC20 for IERC20;
IERC20 public immutable token;
address payable public immutable treasury;
bytes32 public immutable merkleRoot;
uint256 public immutable rate; // token base units per wei (integer)
uint256 public immutable softCap; // in wei
uint256 public immutable hardCap; // in wei
uint256 public immutable maxPerWallet; // in wei
uint256 public immutable minPerWallet; // in wei (0 = no minimum)
uint64 public immutable startTime;
uint64 public immutable endTime;
uint256 public totalRaised;
uint256 public tokensOutstanding; // tokens buyers can still claim (set at finalize)
bool public finalized;
bool public successful;
bool public fundsWithdrawn;
mapping(address => uint256) public contributions;
event Contributed(address indexed buyer, uint256 amount);
event Finalized(bool successful, uint256 totalRaised);
event Withdrawn(address indexed treasury, uint256 amount);
event Claimed(address indexed buyer, uint256 tokens);
event Refunded(address indexed buyer, uint256 amount);
event ExcessTokensRecovered(address indexed to, uint256 amount);
error SaleNotActive();
error ZeroContribution();
error BelowMinimum();
error NotWhitelisted();
error CapExceeded();
error SaleUnderfunded();
error AlreadyFinalized();
error SaleNotEnded();
error ClaimUnavailable();
error RefundUnavailable();
error WithdrawUnavailable();
error NothingOwed();
error TransferFailed();
error NothingToRecover();
error RecoverUnavailable();
constructor(
IERC20 token_,
address payable treasury_,
bytes32 merkleRoot_,
uint256 rate_,
uint256 softCap_,
uint256 hardCap_,
uint256 minPerWallet_,
uint256 maxPerWallet_,
uint64 startTime_,
uint64 endTime_
) {
require(address(token_) != address(0) && treasury_ != address(0), "zero address");
require(rate_ > 0 && hardCap_ > 0, "zero rate or cap");
require(softCap_ <= hardCap_ && startTime_ < endTime_, "bad params");
require(minPerWallet_ <= maxPerWallet_, "min > max");
// Prevent rate * hardCap from overflowing uint256 in funding and claim math.
require(rate_ <= type(uint256).max / hardCap_, "rate overflow");
token = token_;
treasury = treasury_;
merkleRoot = merkleRoot_;
rate = rate_;
softCap = softCap_;
hardCap = hardCap_;
minPerWallet = minPerWallet_;
maxPerWallet = maxPerWallet_;
startTime = startTime_;
endTime = endTime_;
}
function contribute(bytes32[] calldata proof) external payable nonReentrant {
if (finalized || block.timestamp < startTime || block.timestamp > endTime) {
revert SaleNotActive();
}
if (msg.value == 0) revert ZeroContribution();
// Matches OpenZeppelin StandardMerkleTree leaf encoding for ["address"]
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender))));
if (!MerkleProof.verifyCalldata(proof, merkleRoot, leaf)) revert NotWhitelisted();
uint256 newTotal = totalRaised + msg.value;
uint256 newPersonal = contributions[msg.sender] + msg.value;
if (newTotal > hardCap || newPersonal > maxPerWallet) revert CapExceeded();
if (newPersonal < minPerWallet) revert BelowMinimum();
// Every accepted contribution must be fully backed by tokens already held.
// Safe: newTotal <= hardCap, and the constructor bounds rate * hardCap.
if (token.balanceOf(address(this)) < newTotal * rate) revert SaleUnderfunded();
totalRaised = newTotal;
contributions[msg.sender] = newPersonal;
emit Contributed(msg.sender, msg.value);
}
/// @notice Closes the sale. Only updates state; it moves no funds.
function finalize() external nonReentrant {
if (finalized) revert AlreadyFinalized();
if (block.timestamp <= endTime && totalRaised < hardCap) revert SaleNotEnded();
finalized = true;
successful = totalRaised >= softCap;
if (successful) {
tokensOutstanding = totalRaised * rate;
}
emit Finalized(successful, totalRaised);
}
/// @notice Sends raised funds to the treasury after a successful sale. Callable by anyone.
function withdrawRaised() external nonReentrant {
if (!finalized || !successful || fundsWithdrawn) revert WithdrawUnavailable();
fundsWithdrawn = true;
uint256 amount = totalRaised;
(bool ok, ) = treasury.call{value: amount}("");
if (!ok) revert TransferFailed();
emit Withdrawn(treasury, amount);
}
function claim() external nonReentrant {
if (!finalized || !successful) revert ClaimUnavailable();
uint256 paid = contributions[msg.sender];
if (paid == 0) revert NothingOwed();
contributions[msg.sender] = 0;
uint256 amount = paid * rate;
tokensOutstanding -= amount;
token.safeTransfer(msg.sender, amount);
emit Claimed(msg.sender, amount);
}
function refund() external nonReentrant {
if (!finalized || successful) revert RefundUnavailable();
uint256 paid = contributions[msg.sender];
if (paid == 0) revert NothingOwed();
contributions[msg.sender] = 0;
(bool ok, ) = payable(msg.sender).call{value: paid}("");
if (!ok) revert TransferFailed();
emit Refunded(msg.sender, paid);
}
/// @notice Sends tokens not owed to buyers to the treasury.
/// @dev Successful sale: balance minus tokensOutstanding (which falls as buyers claim).
/// Unsuccessful sale: the entire token balance.
function recoverExcessTokens() external nonReentrant {
if (!finalized) revert RecoverUnavailable();
uint256 bal = token.balanceOf(address(this));
uint256 reserved = successful ? tokensOutstanding : 0;
if (bal <= reserved) revert NothingToRecover();
uint256 toSend = bal - reserved;
token.safeTransfer(treasury, toSend);
emit ExcessTokensRecovered(treasury, toSend);
}
}
Pricing limits.rate is an integer number of token base units per wei. With an 18-decimal token, rate = 1000 means 1,000 tokens per 1 ETH. Prices that need a fractional rate (for example 0.5 tokens per ETH) cannot be expressed. If you need finer pricing, add a rate denominator and apply it consistently in contribute(), finalize(), and claim().
Production scope. This code is educational and has not been audited. A production sale often also needs multi-asset contributions, multiple rounds, pause logic, a claim deadline before any broader residual sweep, and operational runbooks. Adapt, test, and audit before any real use.
Before the sale starts. Transfer at least hardCap * rate token base units into the sale contract, and confirm the balance on a block explorer before the start time. Because contribute() checks funding, contributions revert until the contract holds enough tokens, so a late or partial deposit cannot lock the sale into failure. Contributions beyond what the balance covers are rejected, so fund for the full hard cap.
Generating the whitelist root
Build the Merkle tree off-chain from KYC-approved addresses using @openzeppelin/merkle-tree with an ["address"] leaf encoding:
import { StandardMerkleTree } from "@openzeppelin/merkle-tree";
const addresses = ["0xabc...", "0xdef..."]; // KYC-approved wallets
const tree = StandardMerkleTree.of(
addresses.map((a) => [a]),
["address"]
);
console.log(tree.root); // pass into the sale constructor
// For each investor: tree.getProof([investorAddress])
The root is immutable in this sample, so the whitelist is frozen at deployment. If you need to add investors later, an updatable root must sit behind a multi-signature wallet and a timelock, and that role must be disclosed. Serve each investor their proof through the sale dashboard. The on-chain leaf format matches the library’s double-hash convention.
Stage 3: Vesting Contracts
Vesting enforces the schedule from your tokenomics on-chain. OpenZeppelin’s VestingWallet releases tokens linearly from a start time over a duration to a single beneficiary. VestingWalletCliff (available since OpenZeppelin Contracts v5.1) adds a cliff. Confirm availability in the version you pin.
Two properties matter for your disclosures. First, VestingWallet is not revocable: once funded, the schedule cannot be cancelled or clawed back. Second, the wallet’s owner is the beneficiary, and ownership is transferable, so the beneficiary can hand the schedule to another address. If you need revocation, you must write custom vesting code, which adds a privileged role, a larger audit scope, and a trust question to disclose.
Design rules
- Deploy one vesting contract per beneficiary and publish every address. VestingWallet supports a single beneficiary, so a group schedule needs custom code or a multi-signature wallet as the beneficiary.
- Decide whether vesting is revocable. If it is, disclose who can revoke and why, and put that role under a multi-signature wallet.
- Fund each vesting contract directly from the treasury so the allocation is verifiable on a block explorer.
- Stagger cliffs across groups to avoid a single large unlock date.
For allocation and unlock design, see our ICO Tokenomics: Complete Guide.
Stage 4: Access Control, Admin Functions, and Upgradeability
Role minimization. Every privileged function (pause, parameter change, upgrade) is a trust assumption. Remove what you can and disclose what remains.
Multi-signature plus timelock. Place remaining admin roles under a multi-signature wallet and a timelock so investors can see a change before it takes effect.
Upgradeable or immutable? Proxy patterns (transparent or UUPS, following ERC-1967) allow fixes after deployment but add storage-collision risk, an upgrade authority to protect, and an extra trust assumption. For sale and token contracts, immutable designs are simpler to audit and to trust. If you use proxies, protect the upgrade path with a timelock.
Compliance hooks. Smart contracts can enforce on-chain restrictions such as a whitelist, but they do not make a sale legally compliant. Security tokens use permissioned standards such as ERC-3643, which build transfer restrictions into the token itself.
Multi-Asset Sales and Price Oracles
If the sale accepts several assets (for example ETH and stablecoins), each contribution must be converted to a common value. Use a decentralized price feed such as Chainlink Data Feeds, reject stale, zero, or non-positive prices, and avoid pricing from a single DEX spot price, which can be manipulated.
(, int256 answer, , uint256 updatedAt, ) = priceFeed.latestRoundData();
require(
answer > 0 && updatedAt != 0 && block.timestamp - updatedAt <= MAX_STALENESS,
"stale price"
);
On L2s such as Arbitrum, Optimism, and Base, also check the chain’s sequencer uptime feed before trusting a price, and allow a grace period after the sequencer restarts. Set MAX_STALENESS per feed, based on its heartbeat.
Accepting only a stablecoin removes the oracle dependency but narrows who can participate. Whichever assets you accept, review each token’s behavior before listing it:
- Decimals differ. Many stablecoins use 6 decimals, so normalize amounts carefully.
- Non-standard returns. Some tokens (USDT is the well-known case) do not return a boolean from
transfer.SafeERC20handles this. - Fee-on-transfer and rebasing tokens. The amount received can differ from the amount sent.
SafeERC20does not fix this, so measure balances before and after, or do not accept these tokens. - Blocklists and pausing. Some stablecoins can freeze addresses, which can block refunds. Disclose this risk.
Solana: Building a Token Sale Program
Solana uses a different model. An EVM contract cannot be recompiled for it.
| Topic | EVM | Solana |
|---|---|---|
| Language | Solidity | Rust |
| Token creation | Deploy an ERC-20 contract | Create a mint with the SPL Token or Token-2022 program |
| Sale logic | One contract holds state | A program plus separate accounts holding state |
| Framework | Foundry, Hardhat | Anchor |
| Funds custody | Contract balance | Escrow accounts controlled by program-derived addresses (PDAs) |
| Post-deployment changes | Immutable unless built with a proxy | Programs are upgradeable by default; the upgrade authority can replace the code |
What changes in practice
- Accounts, not global storage. Sale state lives in accounts the client passes into each instruction. Validate owner, mint, PDA seeds, and signers on every instruction. Missing checks are a common Solana vulnerability class.
- PDAs for escrow. Raise funds into an escrow account owned by a PDA derived from fixed seeds (for example
["sale", sale_id]). Only the program should move those funds when sale rules allow. - Program upgrade authority. Programs deploy as upgradeable by default, so whoever holds the upgrade authority can replace the sale logic, including logic that controls escrowed funds. This is one of the most important trust points. Revoke the upgrade authority once the program is final, or place it under a multi-signature (for example Squads) with a timelock, and disclose which you chose.
- Mint and freeze authority. Decide who holds mint authority (and freeze authority, if enabled), disclose it, and revoke it or place it under a multi-signature once supply is final.
- Token-2022. Extensions such as transfer fees and transfer hooks change product behavior. Choose standard SPL Token versus Token-2022 early so auditors know the exact program surface.
- Typical Anchor flow. Instructions often include
initialize_sale(config plus escrow PDA),contribute,finalize, andclaimorrefund. Each instruction’s accounts struct must constrain every account. - Audits. Use firms with Solana-specific experience. EVM audit skills do not transfer directly to account validation, cross-program invocations (CPI), and PDA patterns.
A Solana token sale program is a separate codebase and a separate audit scope from your EVM contracts.
Security: Vulnerabilities and Mitigations
| Risk | What happens | Mitigation |
|---|---|---|
| Reentrancy | An external call re-enters before state updates | Checks-effects-interactions, ReentrancyGuard, pull payments |
| Access-control flaws | Unprotected sensitive functions | Role-based access, least privilege, tests per function |
| Oracle manipulation | Attacker moves the price source | Decentralized feeds, staleness and sanity checks, L2 sequencer checks |
| Front-running | Transactions are observed and reordered | Caps and limits, commit-reveal where relevant |
| Unsafe external calls | Unchecked returns, malicious or non-standard tokens | SafeERC20, check call results, restrict token addresses, avoid fee-on-transfer tokens |
| Arithmetic edge cases | Overflow in pricing math, rounding and precision loss | Checked arithmetic; bound rate * hardCap in the constructor; test rounding at the boundaries |
| Signature replay | A signed permit or message is reused | Nonces, deadlines, and domain separation (EIP-712) |
| Denial of service | Unbounded loops, blocked refunds, reverting fund transfers | Pull-based claims, withdrawals, and refunds; no unbounded loops |
| Centralization risk | One key controls funds or supply | Multi-signature, timelock, minimal roles |
| Upgrade risk | Storage collisions or a compromised upgrade key | Prefer immutable contracts, or timelock the upgrade path |
Testing: What to Cover Before the Audit
| Scenario | What to verify |
|---|---|
| Happy path | Whitelisted buyer contributes, sale finalizes, buyer claims the correct amount |
| Caps | Hard cap and per-wallet min/max enforced at the boundaries |
| Soft cap missed | Sale finalizes as unsuccessful; every contributor can refund in full |
| Underfunded sale | Contributions that the token balance cannot cover revert; a late deposit lets contributions resume |
| Whitelist | Invalid proofs, wrong addresses, and malformed proofs revert |
| Time window | Contributions before start or after end revert |
| Double actions | Claim twice, refund twice, finalize twice, and withdraw twice all revert |
| Treasury withdrawal | finalize() moves no ETH; withdrawRaised() sends exactly totalRaised once; a reverting treasury blocks only the withdrawal, not claims |
| Excess recovery | Successful sale recovers balance - tokensOutstanding, and still works after every buyer has claimed; unsuccessful sale recovers the full token balance |
| Constructor bounds | Zero hard cap, zero rate, and rate * hardCap overflow all revert |
| Reentrancy | A malicious receiver cannot drain funds |
| Fuzzing | Random amounts and callers cannot break accounting invariants |
| Invariants | Current contributions plus contributions already settled by claim or refund equal totalRaised; after a successful finalize, tokensOutstanding equals unclaimed contributions times rate; the token balance is never below tokensOutstanding |
forge build
forge test -vvv
forge coverage
slither .
For the highest-value logic, such as accounting and pricing math, consider formal verification with tools like Certora or Halmos in addition to fuzzing.
Rehearse the full ICO launch on a public testnet before mainnet: same scripts, same multi-signature setup, and end-to-end paths for contribution, hard-cap reached, refund, claim, treasury withdrawal, excess recovery, and vesting release.
The Audit Process
- Prepare an audit-ready package: frozen code, specifications, test suite, deployment scripts, and documentation.
- Independent review: manual review plus automated analysis by experienced security engineers.
- Findings report: issues classified by severity (critical, high, medium, low, informational).
- Remediation: fix every finding in code, not only in documentation.
- Re-audit: every material change after the audit needs review.
- Publish the report so investors can verify it.
Never deploy unaudited contracts that will hold investor funds. An audit reduces risk; it does not eliminate it. Audits have scope limits and cannot guarantee the absence of bugs, so consider a bug bounty and an incident-response plan before launch.
Deployment and Verification
- Pin dependency and compiler versions, and record them in the repository.
- Match the EVM target to the chain before deploying. The default target depends on the compiler: Solidity 0.8.24 defaults to Shanghai, 0.8.25 and later default to Cancun, and 0.8.30 and later default to Prague. For chains that have not activated Cancun, set an older target in foundry.toml, for example
evm_version = "paris"(or"shanghai"if the chain supports PUSH0), and confirm with a testnet deployment. - Deploy with scripts, not by hand. Example:
forge script script/Deploy.s.sol --rpc-url $RPC_URL --broadcast --verify - Verify source code on the block explorer.
- Confirm the treasury address, send it a small test transfer, and test a multi-signature signing round.
- Transfer any admin roles to the multi-signature wallet.
- Fund the sale contract with the planned token allocation (
hardCap * ratebase units) and confirm balances on-chain before the start time. - Publish all contract addresses with the audit report.
- Monitor contribution, finalization, withdrawal, claim, refund, and recovery events during and after the sale.
Cost and Timeline Factors
Cost varies with scope. Treat flat quotes that ignore complexity with caution. The ranges below are indicative engineering estimates commonly seen in the market. They cover development and testing only. Independent audit fees, ICO marketing, and legal costs are separate. Depending on code size, chain, and the firm, an audit can cost as much as or more than development, so budget for it from the start and compare quotes on a like-for-like basis.
| Scope | Indicative range (development only) | Typical duration |
|---|---|---|
| Token + basic sale (caps, whitelist, refunds) | Low-to-mid four figures USD | About 2–4 weeks |
| Sale + vesting + test suite + testnet | Mid four figures USD | About 3–6 weeks |
| Multi-round / multi-asset / oracle + audit coordination | Higher four to five figures USD | Several weeks, plus the audit queue |
| Solana program in addition to EVM | Separate language and audit scope | Parallel or sequential track |
| Driver | Why it matters |
|---|---|
| Number and complexity of contracts | Token, sale, vesting, and related modules each add scope |
| Blockchain | EVM and Solana need different languages, tooling, and auditors |
| Sale mechanics | Multi-round pricing, multi-asset contributions, and oracles add risk and test effort |
| Test depth | Fuzzing and invariant tests take time but catch high-impact bugs |
| Audit scope | Code size and number of rounds drive cost and calendar time |
| Documentation and handover | Specifications, scripts, and runbooks |
| Post-launch support | Monitoring, vesting administration, incident response |
Audit availability is a frequent source of schedule slip, so book it early. Request an itemized proposal that separates development, testing, audit coordination, and support, and states clearly whether audit fees are included.
What to Expect From an ICO Smart Contract Development Partner
| Deliverable | Why it matters |
|---|---|
| Specification document | Defines intended behavior before code is written |
| Source code with clear license and ownership | You should own the code you pay for |
| Test suite and coverage report | Evidence of failure-path testing |
| Deployment scripts | Repeatable, reviewable mainnet deployment |
| Audit coordination | Preparation, remediation, and re-audit support |
| Verified contracts | Source published on the block explorer |
| Handover documentation | Roles, addresses, multi-signature setup, emergency procedures |
Ask for verifiable past work: deployed, verified contracts you can inspect yourself.
Pre-Audit Checklist
- Specification written and matched to the code
- Privileged roles minimized, documented, and disclosed
- Treasury and admin roles under a multi-signature wallet, with a timelock on admin actions
- Treasury address confirmed on-chain and able to receive funds
- Pull-based claims, refunds, and withdrawals; no unbounded loops
- Reentrancy protection on every external-call path
- Soft-cap-missed and underfunded paths tested
rate * hardCapoverflow bound (or equivalent safe math) in place, with a non-zero hard cap- Token accounting tracks outstanding obligations; excess and unsold token recovery tested after claims
- Oracle staleness, zero-value, and sanity checks in place, plus L2 sequencer checks (if used)
- Unit, integration, fuzz, and invariant tests passing; coverage reviewed
- Static analysis findings reviewed and resolved
- Full sale rehearsal completed on a public testnet
- Dependency and compiler versions pinned; EVM target matched to the chain
- Deployment scripts reviewed; verified-source plan ready
- Solana only: program upgrade authority and mint authority decisions documented
Download the ICO Smart Contract Pre-Audit Checklist as a printable PDF.
Common ICO Smart Contract Mistakes
- Launching unaudited contracts that hold investor funds
- Keeping an owner key that can mint, pause, or withdraw without disclosure
- Pushing refunds or tokens to many addresses in one loop
- Tying a fund transfer to a state change, so one failing recipient can freeze the whole sale
- Testing only the happy path
- Treating the whitelist as legal compliance rather than an enforcement mechanism
- Using a single DEX spot price as an oracle
- Deploying from a personal wallet and never moving admin rights to a multi-signature wallet
- Making changes after the audit without a re-audit
- Leaving no path to recover unsold or excess tokens after finalization
- Leaving a Solana program’s upgrade authority with a single key
Also Read
Frequently Asked Questions
What is an ICO smart contract?
An ICO smart contract is the on-chain program that governs a token sale. It accepts contributions, enforces rules such as caps and whitelists, and distributes tokens or refunds according to the outcome. A full system also includes the token, vesting, and treasury components.
Which language is used for ICO smart contracts?
Solidity is standard on EVM chains such as Ethereum, BNB Chain, Polygon, Arbitrum, Optimism, and Base. Solana programs are written in Rust, usually with the Anchor framework.
How do I restrict an ICO to KYC-verified investors?
KYC is performed off-chain by a verification provider. Approved wallet addresses are encoded into an on-chain whitelist, commonly a Merkle tree whose root is stored in the contract. Each approved investor submits a proof when contributing.
What is the difference between a token contract and a sale contract?
The token contract defines the asset: supply, transfers, and burning. The sale contract defines how investors acquire it: pricing, caps, timing, whitelist, and refunds. Keeping them separate reduces risk and simplifies audits.
Should ICO smart contracts be upgradeable?
Immutable contracts are simpler to audit and to trust, so they are often preferred for sale and token contracts. Upgradeable proxies allow later fixes but add storage-collision risk and an upgrade authority that must be protected, ideally with a multi-signature wallet and a timelock. On Solana, programs are upgradeable by default, so decide whether to revoke or secure the upgrade authority.
Is a smart contract audit required for an ICO?
It is not a universal legal requirement, but it is an essential trust and risk-control measure. Investors, launchpads, and exchanges commonly expect an independent audit report. An audit reduces risk but does not guarantee that a contract is free of flaws.
What are the most common ICO smart contract vulnerabilities?
Reentrancy, access-control flaws, oracle manipulation, unsafe external calls, arithmetic and rounding edge cases, denial of service through unbounded loops or failing transfers, and over-centralized admin keys. Testing, minimal privileges, and independent audits mitigate them.
How long does ICO smart contract development take?
Several weeks for development, plus an audit window of additional weeks. Scope, chain, and audit availability drive the schedule.
How much does ICO smart contract development cost?
It depends on contract count, sale mechanics, chain, test depth, and audit scope. Basic token-plus-sale development often sits in the low-to-mid four figures USD; multi-asset, multi-round, or dual-chain (EVM + Solana) work costs more. Independent audit fees are separate and can be significant. Request an itemized proposal.
Do smart contracts make an ICO legally compliant?
No. Contracts can enforce rules such as whitelists and caps, but legal classification, KYC/AML obligations, disclosures, and tax treatment depend on the jurisdiction and need qualified legal counsel. Depending on where you sell and who can buy, relevant regimes can include U.S. securities law, the EU’s Markets in Crypto-Assets Regulation (MiCA), and, in India, the tax and reporting rules for virtual digital assets. Rules change often, so confirm current requirements with counsel. See our ICO Development Process: From Idea to Token Sale guide for the regulatory stage.
Build ICO Smart Contracts You Can Defend
A token sale is only as trustworthy as the code that runs it. At Coin Developer India, we help projects design, build, test, and prepare contracts for audit and deployment.
Related: Smart contract development · ICO development company in India · ICO Token Development: Complete Guide
Sources and Further Reading
- OpenZeppelin Contracts documentation
- OpenZeppelin Merkle Tree library
- EIP-2612: Permit
- ERC-1967: Proxy storage slots
- ERC-3643: Permissioned tokens
- Chainlink Data Feeds and L2 sequencer uptime feeds
- Foundry Book
- Anchor documentation
- Squads
This article is for informational purposes only and is not legal, tax, or investment advice. Code examples are educational and unaudited. Obtain independent security review before deploying contracts that hold value.

