Launch a token using Foundry (Quickstart)
Deploying an ERC-20 token on Arbitrum is fully permissionless and is possible using standard Ethereum tooling.
Projects can deploy using Foundry, Hardhat, or Remix, then configure bridging, liquidity, and smart-contract infrastructure on Arbitrum One.
Prerequisites
- Install Foundry:
curl -L https://foundry.paradigm.xyz | bash
foundryup - Get Test
ETH: Obtain Arbitrum SepoliaETHfrom a faucet like Alchemy's Arbitrum Sepolia Faucet, Chainlink's faucet, or QuickNode's faucet. You'll need to connect a wallet (e.g., MetaMask) configured for Arbitrum Sepolia and request funds.
A list of faucets is available on the Chain Info page.
You may need to bridge ETH from Ethereum Sepolia to Arbitrum Sepolia first, using the official Arbitrum Bridge if the faucet requires it.
- Set Up Development Environment: Configure your wallet and tools for Arbitrum testnet deployment. Sign up for an Arbiscan account to get an API key for contract verification.
Project setup
-
Initialize Foundry Project:
# Create new project
forge init my-token-project
cd my-token-project
# Remove extra files
rm src/Counter.sol script/Counter.s.sol test/Counter.t.sol -
Install OpenZeppelin Contracts:
# Install OpenZeppelin contracts library
forge install OpenZeppelin/openzeppelin-contracts
Smart contract development
Create src/MyToken.sol (this is a standard ERC-20 contract and works on any EVM chain like Arbitrum):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
// Max number of tokens that will exist
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10**18;
constructor(
string memory name,
string memory symbol,
uint256 initialSupply,
address initialOwner
) ERC20(name, symbol) Ownable(initialOwner) {
require(initialSupply <= MAX_SUPPLY, "Initial supply exceeds max supply");
// Mints the initial supply to the contract deployer
_mint(initialOwner, initialSupply);
}
function mint(address to, uint256 amount) public onlyOwner {
require(totalSupply() + amount <= MAX_SUPPLY, "Minting would exceed max supply");
_mint(to, amount);
}
function burn(uint256 amount) public {
_burn(msg.sender, amount);
}
}
Deployment script
Create script/DeployToken.s.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Script, console} from "forge-std/Script.sol";
import {MyToken} from "../src/MyToken.sol";
contract DeployToken is Script {
function run() external {
// Load contract deployer's private key from environment variables
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
address deployerAddress = vm.addr(deployerPrivateKey);
// Token configuration parameters
string memory name = "My Token";
string memory symbol = "MTK";
uint256 initialSupply = 100_000_000 * 10**18;
// Initiates broadcasting transactions
vm.startBroadcast(deployerPrivateKey);
// Deploys the token contract
MyToken token = new MyToken(name, symbol, initialSupply, deployerAddress);
// Stops broadcasting transactions
vm.stopBroadcast();
// Logs deployment information
console.log("Token deployed to:", address(token));
console.log("Token name:", token.name());
console.log("Token symbol:", token.symbol());
console.log("Initial supply:", token.totalSupply());
console.log("Deployer balance:", token.balanceOf(deployerAddress));
}
}
Environment configuration
-
Create
.envfile:PRIVATE_KEY=your_private_key_here
ARBITRUM_SEPOLIA_RPC_URL=https://sepolia.arbitrum.io/rpc
ARBITRUM_ONE_RPC_URL=https://arb1.arbitrum.io/rpc
ARBISCAN_API_KEY=your_arbiscan_api_key_here
A list of RPCs, and chain IDs are available on the Chain Info page.
-
Update
foundry.toml(add chain IDs for verification, as Arbiscan requires them for non-Ethereum chains):[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = ["@openzeppelin/=lib/openzeppelin-contracts/"]
[rpc_endpoints]
arbitrum_sepolia = "${ARBITRUM_SEPOLIA_RPC_URL}"
arbitrum_one = "${ARBITRUM_ONE_RPC_URL}"
[etherscan]
arbitrum_sepolia = { key = "${ARBISCAN_API_KEY}", url = "https://api-sepolia.arbiscan.io/api", chain = 421614 }
arbitrum_one = { key = "${ARBISCAN_API_KEY}", url = "https://api.arbiscan.io/api", chain = 42161 }
A list of chain IDs is available on the Chain Info page.
Testing
-
Create
test/MyToken.t.sol// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {MyToken} from "../src/MyToken.sol";
contract MyTokenTest is Test {
MyToken public token;
address public owner = address(0x1);
address public user = address(0x2);
uint256 constant INITIAL_SUPPLY = 100_000_000 * 10**18;
function setUp() public {
// Deploy token contract before each test
vm.prank(owner);
token = new MyToken("Test Token", "TEST", INITIAL_SUPPLY, owner);
}
function testInitialState() public {
// Verify the token was deployed with the correct parameters
assertEq(token.name(), "Test Token");
assertEq(token.symbol(), "TEST");
assertEq(token.totalSupply(), INITIAL_SUPPLY);
assertEq(token.balanceOf(owner), INITIAL_SUPPLY);
}
function testMinting() public {
uint256 mintAmount = 1000 * 10**18;
// Only the owner should be able to mint
vm.prank(owner);
token.mint(user, mintAmount);
assertEq(token.balanceOf(user), mintAmount);
assertEq(token.totalSupply(), INITIAL_SUPPLY + mintAmount);
}
function testBurning() public {
uint256 burnAmount = 1000 * 10**18;
// Owner burns their tokens
vm.prank(owner);
token.burn(burnAmount);
assertEq(token.balanceOf(owner), INITIAL_SUPPLY - burnAmount);
assertEq(token.totalSupply(), INITIAL_SUPPLY - burnAmount);
}
function testFailMintExceedsMaxSupply() public {
// This test should fail when attempting to mint more than the max supply
uint256 excessiveAmount = token.MAX_SUPPLY() + 1;
vm.prank(owner);
token.mint(user, excessiveAmount);
}
function testFailUnauthorizedMinting() public {
// This test should fail when a non-owner tries to mint tokens
vm.prank(user);
token.mint(user, 1000 * 10**18);
}
} -
Run Tests:
# Runs all tests with verbose output
forge test -vv
Deployment and verification
-
Deploy to Arbitrum Sepolia (testnet):
# Load environment variables
source .env
# Deploy to Arbitrum Sepolia with automatic verification
forge script script/DeployToken.s.sol:DeployToken \
--rpc-url arbitrum_sepolia \
--broadcast \
--verify- Uses
https://sepolia.arbitrum.io/rpc(RPC URL). - Chain ID: 421614.
- Verifies on Sepolia Arbiscan.
- Uses
-
Deploy to Arbitrum One (mainnet):
- Replace
arbitrum_sepoliawitharbitrum_onein the command. - Uses
https://arb1.arbitrum.io/rpc(RPC URL). - Chain ID: 42161.
- Verifies on Arbiscan.
- Requires sufficient
ETHon Arbitrum One for gas fees (bridge from Ethereum mainnet if needed).
- Replace
-
Example of verifying on Arbiscan:
forge verify-contract <contract_address> <contract_path>:YourToken \
--verifier etherscan \
--verifier-url https://api.arbiscan.io/api \
--chain-id 42161 \
--num-of-optimizations 200
Arbitrum-specific configurations
- RPC URLs:
- Arbitrum Sepolia:
https://sepolia.arbitrum.io/rpc - Arbitrum One:
https://arb1.arbitrum.io/rpc
- Arbitrum Sepolia:
- Chain IDs: Arbitrum Sepolia: 421614; Arbitrum One: 42161.
- Contract Addresses: Logged in console output after deployment (e.g.,
console.log("Token deployed to:", address(token));). - Verification: Uses Arbiscan API with your API key. The
--verifyflag enables automatic verification.
Important notes
- Always conduct security audits (e.g., via tools like Slither or professional reviews) before mainnet deployment, as token contracts handle value.
- Ensure your wallet has enough
ETHfor gas on the target network. Arbitrum fees are low, but mainnet deployments still cost realETH. - If you encounter verification issues, double-check your Arbiscan API key and foundry.toml configs. For more advanced deployments, refer to general Foundry deployment docs or Arbitrum developer resources.
Bridging considerations
Two deployment paths are possible:
- Native Deployment (recommended)
- Token is deployed directly on Arbitrum One
- Ideal for a Token Generation Event (TGE), liquidity bootstrapping, airdrops, and L2-native user flows.
- Deployment on Ethereum and bridging to Arbitrum One
- Use the Arbitrum Token Bridge to create an L2 counterpart
Post-deployment considerations
After deploying a token contract on Arbitrum, you may choose to complete additional setup steps depending on the needs of your project. These may include:
- Verifying the contract on Arbiscan to improve transparency and readability
- Creating liquidity pools on Arbitrum-based DEXs
- Publishing token metadata to relevant indexing or aggregation services
- Ensuring wallet compatibility by submitting basic token information
- Configuring operational security components such as multisigs or timelocks
- Connecting to market infrastructure providers where applicable
- Setting up monitoring or observability tools for contract activity