Skip to main content

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

  1. Install Foundry:
    curl -L https://foundry.paradigm.xyz | bash
    foundryup
  2. Get Test ETH: Obtain Arbitrum Sepolia ETH from 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.
Resources

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.

  1. 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

  1. Initialize Foundry Project:
    forge init my-token-project
    cd my-token-project
    rm src/Counter.sol script/Counter.s.sol test/Counter.t.sol
  2. 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

  1. Create .env file:

    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
Resources

A list of RPCs, and chain IDs are available on the Chain Info page.

  1. 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 }
Resources

A list of chain IDs is available on the Chain Info page.

Testing

  1. 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);
    }
    }
  2. Run Tests:

    forge test -vv

Deployment and verification

  1. 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.
  2. Deploy to Arbitrum One (mainnet):

    • Replace arbitrum_sepolia with arbitrum_one in the command.
    • Uses https://arb1.arbitrum.io/rpc (RPC URL).
    • Chain ID: 42161.
    • Verifies on Arbiscan.
    • Requires sufficient ETH on Arbitrum One for gas fees (bridge from Ethereum mainnet if needed).
  3. 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
  • 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 --verify flag 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 ETH for gas on the target network. Arbitrum fees are low, but mainnet deployments still cost real ETH.
  • 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:

  1. 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.
  1. Deployment on Ethereum and bridging to Arbitrum One

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