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:
forge init my-token-project
cd my-token-project
rm src/Counter.sol script/Counter.s.sol test/Counter.t.sol - Install OpenZeppelin Contracts:
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 {
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");
_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 {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
address deployerAddress = vm.addr(deployerPrivateKey);
string memory name = "My Token";
string memory symbol = "MTK";
uint256 initialSupply = 100_000_000 * 10**18;
vm.startBroadcast(deployerPrivateKey);
MyToken token = new MyToken(name, symbol, initialSupply, deployerAddress);
vm.stopBroadcast();
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 token was deployed with 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 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 own 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 trying to mint more than max supply
uint256 excessiveAmount = token.MAX_SUPPLY() + 1;
vm.prank(owner);
token.mint(user, excessiveAmount);
}
function testFailUnauthorizedMinting() public {
// This test should fail when non-owner tries to mint
vm.prank(user);
token.mint(user, 1000 * 10**18);
}
} -
Run Tests:
forge test -vv
Deployment and verification
-
Deploy to Arbitrum Sepolia (testnet):
source .env
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