Coins vs. Tokens
Before writing a single line of code, it's worth being precise about what you're actually building. A coin operates on its own blockchain, Bitcoin, Ether, and SOL are coins because they're native assets of their respective networks. A token, by contrast, is a smart contract deployed on top of an existing blockchain. When you create an ERC-20 token on Ethereum, you're not building a new chain; you're deploying a contract that tracks balances and enforces transfer rules. This distinction matters because it determines your infrastructure, your toolchain, your deployment costs, and your token's technical capabilities.
Most founders building in 2024 don't need their own chain. They need a token with defined supply mechanics, perhaps some governance or utility functionality, and a reliable deployment on a network where users and liquidity already exist. This tutorial focuses on that path, specifically deploying fungible tokens on EVM-compatible chains, with Ethereum mainnet and its L2s (Arbitrum, Base, Optimism, Polygon) as the target environments.
Choosing the Right Blockchain for Deployment
Chain selection is an architectural decision that affects cost, user reach, tooling maturity, and long-term maintainability. It is not something to choose based on hype.
Ethereum Mainnet remains the most battle-tested environment. Its security model is the strongest, its tooling ecosystem is the deepest, and liquidity on Uniswap and other DEXs is unmatched. The tradeoff is cost, deploying a standard ERC-20 token can run anywhere from $20 to several hundred dollars depending on gas conditions, and every on-chain interaction your users perform will cost real money. Mainnet is appropriate for tokens that will hold significant value or where the trust assumptions of a high-security L1 matter (treasury tokens, governance tokens for large protocols).
Layer 2 networks - Arbitrum, Base, Optimism, zkSync, Polygon zkEVM, offer EVM compatibility with dramatically lower fees, often 10–100x cheaper than mainnet. Since they share Ethereum's security model through fraud proofs or ZK proofs, they're a strong choice for most startups. Base in particular has seen rapid ecosystem growth and developer tooling that matches mainnet quality. Bridging complexity and the smaller number of integrated dApps are the primary drawbacks.
Polygon PoS is a sidechain (not a true L2), with fees often under a cent. It's appropriate for high-volume use cases, gaming tokens, loyalty tokens, microtransactions, where the Ethereum security model is less critical than throughput and cost. Solana and BNB Smart Chain are alternatives if you have specific ecosystem requirements, though they require different toolchains (Anchor/Rust for Solana, which is outside the scope of this tutorial).
Once you select a chain, you're committed to that environment for your initial deployment of crypto token development. Cross-chain expansion comes later via bridges and multi-chain deployment,don't over-engineer at the start.
Setting Up Your Development Environment
A professional token deployment requires a reproducible, auditable development setup. The standard stack in 2024 is either Hardhat (Node.js-based) or Foundry (Rust-based). Foundry has become the preferred choice among serious smart contract developers because its testing framework is written in Solidity, it compiles faster, and its fuzzing capabilities are built-in.
Install Foundry with:
curl -L https://foundry.paradigm.xyz | bash
foundryup
Initialize a new project:
forge init my-token
cd my-token
Foundry creates a src/, test/, and script/ directory structure. Your token contract goes in src/, deployment scripts in script/, and tests in test/. You'll also need to install OpenZeppelin contracts, which provide the audited base implementations you should be building on rather than writing from scratch:
forge install OpenZeppelin/openzeppelin-contracts
Add the remapping in your foundry.toml:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = ["@openzeppelin/=lib/openzeppelin-contracts/"]
For environment variables, specifically your deployer private key and RPC URLs, use a .env file and never commit it to version control. Add it to .gitignore immediately. Use dotenv or Foundry's --env-file flag to load it during deployment. Many developers prefer hardware wallet signing for mainnet deployments using tools like cast wallet with a Ledger device, which keeps private keys off the machine entirely.
Writing the ERC-20 Token Contract
The ERC-20 standard is defined in EIP-20 and specifies a minimal interface: totalSupply, balanceOf, transfer, transferFrom, approve, and allowance, plus two events (Transfer and Approval). OpenZeppelin's implementation handles all of this correctly, including edge cases around allowance manipulation and zero-address transfers that naive implementations often miss.
Here is a production-grade token contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, ERC20Burnable, ERC20Permit, Ownable {
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10 ** 18; // 1 billion tokens
constructor(
address initialOwner,
address treasury
)
ERC20("My Token", "MTK")
ERC20Permit("My Token")
Ownable(initialOwner)
{
// Mint initial supply to treasury wallet, not the deployer
_mint(treasury, MAX_SUPPLY);
}
// Optional: owner-controlled minting up to MAX_SUPPLY
function mint(address to, uint256 amount) external onlyOwner {
require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
_mint(to, amount);
}
}
Several decisions embedded in this contract deserve explanation. First, the ERC20Permit extension (EIP-2612) allows users to approve token transfers via a signed message rather than an on-chain transaction. This enables gasless approvals and is essentially required for any token that will be used with modern DeFi protocols, without it, users need two transactions (approve + action) for every interaction. The permit function uses EIP-712 typed structured data signing, and OpenZeppelin handles the nonce management and signature verification.
Second, the ERC20Burnable extension adds burn and burnFrom methods. Whether you include this depends on your tokenomics, if your design involves deflationary mechanisms or protocol fee burns, you need it. If not, excluding it reduces attack surface slightly.
Third, note that the initial mint goes to a treasury address, not msg.sender or address(this). Minting directly to a multisig treasury wallet (which we'll set up shortly) is better practice than minting to the deployer EOA and then transferring, because it creates a cleaner on-chain history and reduces the number of transactions where something could go wrong.
The MAX_SUPPLY constant with the require check in mint enforces a hard cap even if the owner account is compromised, in most cases, minting post-deployment should be disabled entirely or protected by a timelock. If your token has a fixed supply at launch, simply remove the mint function and your MAX_SUPPLY check becomes implicit through the constructor.
Tokenomics Implementation in the Contract
Tokenomics is where business logic meets smart contract code. The allocation of your token supply, how much goes to the team, investors, ecosystem fund, public sale, liquidity, must be either enforced in code or carefully managed off-chain. On-chain enforcement is always preferable.
A vesting contract is the standard mechanism for team and investor allocations. Rather than minting all tokens to a treasury and trusting manual distribution, you deploy a VestingWallet (OpenZeppelin provides one) or a custom vesting contract that holds tokens and releases them according to a schedule.
import "@openzeppelin/contracts/finance/VestingWallet.sol";
// Deploy separately for each beneficiary
// Constructor: beneficiary address, start timestamp, duration in seconds
VestingWallet teamVesting = new VestingWallet(
teamMultisig,
uint64(block.timestamp + 365 days), // 1 year cliff
uint64(2 * 365 days) // 2 year vesting duration
);
OpenZeppelin's VestingWallet implements linear vesting with an optional cliff by setting the start time in the future. Once deployed, you transfer the team's token allocation to the vesting contract address. The beneficiary can call release(tokenAddress) at any time to receive whatever has vested so far. This is auditable, trustless, and standard enough that investors and community members will recognize it immediately.
For a typical startup token structure, you might deploy the following allocations at launch: team tokens go to a vesting contract with a 12-month cliff and 36-month linear vesting, investor tokens to similar vesting contracts negotiated per round, an ecosystem/grant wallet controlled by a multisig, a liquidity pool allocation sent directly to the DEX deployment script, and a community rewards allocation held in a contract that distributes based on protocol activity.
The MAX_SUPPLY needs to be set accounting for all these allocations. If your total planned distribution is 1 billion tokens and all of them are minted at genesis, you mint the full supply to a distribution contract or to the treasury multisig and execute transfers from there. If you plan phased minting (for example, staking rewards minted over time), you keep the mint function but protect it with a onlyMinter role that only the staking contract holds.
Testing Your Token Contract
No token should be deployed to mainnet without thorough testing. With Foundry, tests are written in Solidity:
// test/MyToken.t.sol
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/MyToken.sol";
contract MyTokenTest is Test {
MyToken token;
address owner = address(0x1);
address treasury = address(0x2);
address user = address(0x3);
function setUp() public {
token = new MyToken(owner, treasury);
}
function test_InitialSupply() public view {
assertEq(token.totalSupply(), token.MAX_SUPPLY());
assertEq(token.balanceOf(treasury), token.MAX_SUPPLY());
}
function test_TransferSucceeds() public {
vm.prank(treasury);
token.transfer(user, 1000 * 10 ** 18);
assertEq(token.balanceOf(user), 1000 * 10 ** 18);
}
function test_MintExceedsMaxSupplyReverts() public {
vm.prank(owner);
vm.expectRevert("Exceeds max supply");
token.mint(user, 1);
}
function testFuzz_TransferNeverExceedsBalance(uint256 amount) public {
amount = bound(amount, 1, token.MAX_SUPPLY());
vm.prank(treasury);
if (amount > token.balanceOf(treasury)) {
vm.expectRevert();
}
token.transfer(user, amount);
}
}
Run tests with forge test -vvv for verbose output. The fuzz test (testFuzz_) will run 256 iterations by default with random inputs, finding edge cases your manual tests might miss. Increase iterations with --fuzz-runs 10000 for critical paths before mainnet deployment.
Beyond unit tests, write integration tests that simulate the full deployment sequence: deploy token, deploy vesting contracts, transfer allocations, fast-forward time with vm.warp, and verify the correct amounts are releasable. Foundry's cheatcodes (vm.prank, vm.warp, vm.roll, vm.expectRevert) make this straightforward.
Also run a local fork of mainnet or your target chain to test against real deployed contracts like Uniswap:
forge test --fork-url $MAINNET_RPC_URL --fork-block-number 19000000
This lets you test adding liquidity to an actual Uniswap V3 pool in a local environment where you control all state.
Setting Up a Multisig for Token Administration
Controlling administrative functions of your token from a single EOA (externally owned account) is a security anti-pattern that has resulted in catastrophic losses across the industry. Any privileged function, minting, ownership transfer, pausing, must be controlled by a multisig wallet.
Safe (formerly Gnosis Safe) is the industry standard. For a startup, a 2-of-3 or 3-of-5 configuration is typical: you need M of N key holders to sign any transaction. Deploy a Safe at safe.global with your core team members as signers. The Safe contract address becomes your initialOwner in the token deployment.
Before deploying your token, you need the Safe address. Deploy the Safe first, then use its address as the owner parameter in your token constructor. This means from the moment the token contract is live, no single person can exercise admin functions.
For tokens where you want to eventually renounce all admin control (fully immutable supply), call renounceOwnership() from the Safe after confirming the deployment is correct and all allocations are properly distributed. This action is irreversible, verify everything before doing it.
For protocols that need ongoing governance, the multisig eventually gets replaced by an on-chain governance system (using OpenZeppelin's Governor contracts), but that's a more advanced topic beyond a token launch.
Deploying to Testnet
Always deploy to a public testnet before mainnet. Use Sepolia for Ethereum-compatible networks, or the specific L2 testnet (Arbitrum Sepolia, Base Sepolia, etc.). Get testnet ETH from faucets, Alchemy, Chainlink, and Infura all maintain faucets for these networks.
Write a deployment script in Foundry:
// script/Deploy.s.sol
pragma solidity ^0.8.20;
import "forge-std/Script.sol";
import "../src/MyToken.sol";
contract DeployScript is Script {
function run() external {
address owner = vm.envAddress("OWNER_ADDRESS");
address treasury = vm.envAddress("TREASURY_ADDRESS");
vm.startBroadcast();
MyToken token = new MyToken(owner, treasury);
vm.stopBroadcast();
console.log("Token deployed to:", address(token));
}
}
Deploy to Sepolia:
forge script script/Deploy.s.sol:DeployScript \
--rpc-url $SEPOLIA_RPC_URL \
--private-key $DEPLOYER_PRIVATE_KEY \
--broadcast \
--verify \
--etherscan-api-key $ETHERSCAN_API_KEY
The --verify flag automatically submits your contract source to Etherscan for verification after deployment. Verified contracts display their source code publicly, which is a baseline trust requirement, no serious user or investor should interact with an unverified token contract.
After deployment, verify everything on the testnet explorer: check the totalSupply, confirm the treasury address holds the correct balance, confirm the owner is your multisig address (not your deployer EOA), and try a few transfers and approvals. Then have other team members test interactions from their own wallets.
Mainnet Deployment and Contract Verification
When testnet is confirmed and your team has reviewed everything, proceed to mainnet. The command is nearly identical but targets mainnet:
forge script script/Deploy.s.sol:DeployScript \
--rpc-url $MAINNET_RPC_URL \
--ledger \
--sender $DEPLOYER_ADDRESS \
--broadcast \
--verify \
--etherscan-api-key $ETHERSCAN_API_KEY
Using --ledger instead of --private-key signs the transaction with a hardware wallet. The deployer pays for deployment gas from the --sender address, which must hold ETH on mainnet. The cost to deploy this contract will be roughly 500,000–800,000 gas units; at 20 gwei gas price, that's approximately 0.01–0.016 ETH.
After deployment, immediately perform the post-deployment checklist: verify the contract on Etherscan (if the --verify flag succeeded, this is done; if not, use forge verify-contract separately), transfer the owner to your multisig if the deployer script used an EOA as a temporary owner, confirm all initial allocations arrived at the correct addresses, and add the token to your Safe's asset tracking.
Log the deployment transaction hash and contract address in your project documentation. These are permanent artifacts of your project's history.
Adding Liquidity and Making Your Token Tradeable
A deployed token that can't be traded has no market. To establish initial liquidity, you'll typically use a decentralized exchange, Uniswap V2 or V3 on Ethereum and its L2s, or the equivalent DEX on your chosen chain.
Uniswap V3 offers concentrated liquidity, which makes your initial liquidity more capital-efficient but requires choosing a price range. Uniswap V2 (and its clones like SushiSwap) uses the simpler x*y=k constant product formula with liquidity spread across the entire price range, which is easier to manage for a new token.
To add liquidity on Uniswap V2 programmatically:
IUniswapV2Router02 router = IUniswapV2Router02(UNISWAP_V2_ROUTER);
// Approve router to spend tokens
IERC20(tokenAddress).approve(address(router), tokenAmount);
// Add liquidity
router.addLiquidityETH{value: ethAmount}(
tokenAddress,
tokenAmount,
tokenAmountMin, // slippage tolerance
ethAmountMin, // slippage tolerance
lpRecipient, // address to receive LP tokens
block.timestamp + 300
);
The LP tokens you receive represent your share of the liquidity pool. A common practice is to immediately lock or burn these LP tokens, locking through a service like Team Finance or Unicrypt, or burning by sending to the zero address. LP locking proves to potential buyers that you can't "rug" the liquidity (withdraw it and crash the price), which is a meaningful trust signal in an environment where such attacks are common.
Determine your initial listing price before adding liquidity. The price is set by the ratio of tokens to ETH (or stablecoin) you add to the pool. If you add 1,000,000 tokens and 10 ETH, the initial price is 10 ETH / 1,000,000 = 0.00001 ETH per token. Think carefully about what market cap this implies given your circulating supply at launch.
Token Security Considerations
Security vulnerabilities in token contracts have drained billions of dollars. Even ERC-20 tokens, simpler than full DeFi protocols, have had exploits. The following categories deserve specific attention.
Centralization risk is often overlooked as a "security" issue, but it's the most common vector for token-related losses. If a single address can mint unlimited tokens, that address becomes an attack target. Minimize privileged functions, protect them with multisigs, and eventually renounce or decentralize control entirely.
Approval exploits remain a concern. The classic ERC-20 approve/transferFrom flow has a known race condition when changing an allowance, an attacker who sees a pending approve transaction can front-run it and spend both the old and new allowance. ERC20Permit avoids this by making approvals signature-based and single-use. For contracts that don't implement permit, the pattern of setting allowance to 0 before setting a new value is the mitigation.
Fee-on-transfer tokens and rebasing tokens are modifications to standard ERC-20 behavior that break compatibility with most DeFi protocols. Uniswap, Aave, and Compound all assume that transfer(to, amount) results in exactly amount being received. If your token takes a fee or adjusts balances globally, it will malfunction in most integrations. Avoid these patterns unless you have a very specific reason and are prepared to handle the integration complexity.
Honeypot patterns, where tokens can be bought but not sold, are sometimes introduced accidentally through poorly written transfer hooks. If you override _update or _transfer in your contract, test that both buying and selling work before deployment. Traders and contract auditors check this, and a token that can't be sold will be labeled a scam regardless of intent.
Get your contract audited before launch if any real value will flow through it. Firms like Trail of Bits, OpenZeppelin, Spearbit, and Code4rena competitive audits are the established options. For smaller launches, at minimum run Slither (static analysis) and have multiple experienced Solidity developers review the code:
pip install slither-analyzer
slither src/MyToken.sol
Token Metadata and Standards Compliance
Once deployed, your token needs to be discoverable and correctly displayed by wallets, explorers, and DeFi interfaces. The name(), symbol(), and decimals() functions (returning 18 by default in OpenZeppelin's implementation) handle the basics. Etherscan and most wallets will display these automatically after contract verification.
For token logo and metadata, submit your token to the major token lists. The Uniswap Default Token List, CoinGecko, and Trust Wallet's assets repository on GitHub are the primary targets. Each has their own submission process, typically a pull request to a GitHub repository with your token's metadata JSON and a logo image (usually 256x256 PNG). Being on these lists makes your token appear correctly in Uniswap's interface and most wallets without users needing to add it manually.
Submit to CoinGecko and CoinMarketCap for price tracking. Both have official listing request forms and require proof of liquidity and basic project information. These listings matter for discoverability and are often a prerequisite for centralized exchange listings later.
Regulatory and Legal Considerations
This tutorial is technical documentation, not legal advice. That said, ignoring the regulatory environment is how founders create liability for themselves. Token issuance intersects with securities law in most jurisdictions, and the question of whether a token is a security is determined by tests like the Howey Test in the United States, primarily whether buyers expect profits from the efforts of others.
Utility tokens that provide actual access to a service, governance tokens that represent voting rights, and payment tokens all occupy different regulatory positions that vary by country. The SEC's actions against various token issuers over 2023–2024 have made clear that simply calling a token "utility" doesn't determine its legal classification. Consult with a lawyer experienced in digital asset law in your relevant jurisdictions before conducting any public token sale.
At a minimum: don't make promises about token price appreciation, don't make representations that sound like investment pitches, be clear about what the token actually does, and avoid selling tokens to retail investors in jurisdictions where you haven't done the compliance work. The technical work of deploying a token is the easy part, the legal framework around how you distribute it is where real risk lives.
Post-Deployment Operations
Deployment is not the end of the process; it's the beginning of ongoing operations. Smart contract deployments are immutable, you cannot patch bugs in production the way you would a traditional application. If your contract has an upgrade mechanism (OpenZeppelin's UUPS or Transparent proxy patterns), make sure it's properly secured and that the community understands the implications. If your contract is immutable, that needs to be communicated clearly as a feature.
Monitor your contract for unusual activity using services like Tenderly or OpenZeppelin Defender. Set up alerts for large transfers, unusual minting activity, or interactions from known exploit contracts. Tenderly's alerting can notify you within seconds of suspicious on-chain activity.
Manage your token's total supply information carefully. Circulating supply, the amount of tokens actually in circulation, excluding locked, vesting, or burned tokens, is what most metrics platforms display and what investors use to calculate market cap. Maintain a clear accounting of what's locked where, update token list metadata as locked tokens vest and enter circulation, and communicate supply changes transparently.
Finally, the contracts you've deployed will outlast any frontend or documentation you build around them. Comment your contract code thoroughly, maintain a deployment registry documenting every contract address and its role in your system, and treat the on-chain state as the source of truth. Everything else is an interface layer on top of what the contracts actually do.
Summary
Building a production token involves substantially more than copying an ERC-20 template and hitting deploy. The technical decisions, which chain, which extensions, how supply is managed, how admin keys are secured, how liquidity is structured, each have downstream consequences that are difficult or impossible to reverse once you're live. The framework outlined here, Foundry for development and testing, OpenZeppelin for contract primitives, Safe for multisig administration, Uniswap for liquidity, and rigorous pre-deployment testing against forked mainnet, represents the current standard for serious token deployments. The contracts themselves are the least complex part of the work; the tokenomics design, security review, legal analysis, and ongoing operational discipline are what determine whether a token launch actually succeeds.
Top comments (0)