Deploy an ERC20 Token Learn how to create, deploy, and interact with your own ERC20 token on Nexis Appchain. This tutorial covers token creation with OpenZeppelin, deployment strategies, and best practices. 
Prerequisites 
Wallet Setup MetaMask or compatible wallet connected to Nexis Testnet 
Testnet Tokens Get testnet NZT from the faucet  
Development Tools Hardhat or Foundry installed locally 
Basic Solidity Understanding of Solidity and smart contracts 
Network Configuration Add Nexis Testnet to your development environment: 
hardhat.config.js
foundry.toml
require ( "@nomicfoundation/hardhat-toolbox" ); require ( "dotenv" ). config (); module . exports  =  {   solidity:  {     version:  "0.8.20" ,     settings:  {       optimizer:  {         enabled:  true ,         runs:  200       }     }   },   networks:  {     nexisTestnet:  {       url:  "https://testnet-rpc.nex-t1.ai" ,       chainId:  84532 ,       accounts:  [ process . env . PRIVATE_KEY ],       gasPrice:  1000000000 ,  // 1 gwei     },     nexisMainnet:  {       url:  "https://rpc.nex-t1.ai" ,       chainId:  84532 ,       accounts:  [ process . env . PRIVATE_KEY ],     }   },   etherscan:  {     apiKey:  {       nexisTestnet:  "your-api-key"     }   } }; 
Token Contract Create a feature-rich ERC20 token using OpenZeppelin: 
Install Dependencies
Install OpenZeppelin contracts library # For Hardhat npm  install  @openzeppelin/contracts # For Foundry forge  install  OpenZeppelin/openzeppelin-contracts 
Create Token Contract
Create your token contract with essential features 
Add Extensions
Implement minting, burning, pausing, and access control 
Test & Deploy
Write comprehensive tests before mainnet deployment 
Basic ERC20 Token // SPDX-License-Identifier: MIT pragma  solidity  ^0.8.20 ; import  "@openzeppelin/contracts/token/ERC20/ERC20.sol" ; import  "@openzeppelin/contracts/access/Ownable.sol" ; /**  *  @title  MyToken  *  @dev  Basic ERC20 token with fixed supply  */ contract  MyToken  is  ERC20 ,  Ownable  {     uint256  public  constant  INITIAL_SUPPLY  =  1_000_000  *  10 ** 18 ;  // 1 million tokens     constructor ()  ERC20 ("My Token", "MTK")  Ownable (msg.sender) {         _mint ( msg.sender , INITIAL_SUPPLY);     } } Advanced Token with Features contracts/AdvancedToken.sol
// 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/ERC20Pausable.sol" ; import  "@openzeppelin/contracts/access/AccessControl.sol" ; import  "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol" ; /**  *  @title  AdvancedToken  *  @dev  Feature-rich ERC20 token with:  * - Minting (controlled)  * - Burning  * - Pausing  * - Role-based access control  * - EIP-2612 Permit (gasless approvals)  */ contract  AdvancedToken  is     ERC20 ,     ERC20Burnable ,     ERC20Pausable ,     AccessControl ,     ERC20Permit {     bytes32  public  constant  MINTER_ROLE  =  keccak256 ( "MINTER_ROLE" );     bytes32  public  constant  PAUSER_ROLE  =  keccak256 ( "PAUSER_ROLE" );     uint256  public  constant  MAX_SUPPLY  =  10_000_000  *  10 ** 18 ;  // 10 million cap     event  TokensMinted ( address  indexed  to ,  uint256  amount );     event  TokensBurned ( address  indexed  from ,  uint256  amount );     constructor (         string  memory  name ,         string  memory  symbol ,         uint256  initialSupply     )  ERC20 (name, symbol)  ERC20Permit (name) {         require (initialSupply  <=  MAX_SUPPLY,  "Initial supply exceeds max" );         _grantRole (DEFAULT_ADMIN_ROLE,  msg.sender );         _grantRole (MINTER_ROLE,  msg.sender );         _grantRole (PAUSER_ROLE,  msg.sender );         if  (initialSupply  >  0 ) {             _mint ( msg.sender , initialSupply);         }     }     /**      *  @dev  Mint new tokens (only MINTER_ROLE)      */     function  mint ( address  to ,  uint256  amount )  public  onlyRole ( MINTER_ROLE ) {         require ( totalSupply ()  +  amount  <=  MAX_SUPPLY,  "Exceeds max supply" );         _mint (to, amount);         emit  TokensMinted (to, amount);     }     /**      *  @dev  Pause all token transfers (only PAUSER_ROLE)      */     function  pause ()  public  onlyRole ( PAUSER_ROLE ) {         _pause ();     }     /**      *  @dev  Unpause token transfers (only PAUSER_ROLE)      */     function  unpause ()  public  onlyRole ( PAUSER_ROLE ) {         _unpause ();     }     /**      *  @dev  Burn tokens from caller      */     function  burn ( uint256  amount )  public  override  {         super . burn (amount);         emit  TokensBurned ( msg.sender , amount);     }     // Required overrides     function  _update (         address  from ,         address  to ,         uint256  value     )  internal  override ( ERC20 ,  ERC20Pausable ) {         super . _update (from, to, value);     } } Token with Tax Mechanism // SPDX-License-Identifier: MIT pragma  solidity  ^0.8.20 ; import  "@openzeppelin/contracts/token/ERC20/ERC20.sol" ; import  "@openzeppelin/contracts/access/Ownable.sol" ; /**  *  @title  TaxToken  *  @dev  ERC20 token with configurable buy/sell tax  */ contract  TaxToken  is  ERC20 ,  Ownable  {     uint256  public  buyTaxPercent  =  5 ;    // 5% buy tax     uint256  public  sellTaxPercent  =  5 ;   // 5% sell tax     address  public  taxReceiver;     mapping ( address  =>  bool )  public  isExcludedFromTax;     mapping ( address  =>  bool )  public  isPair;  // DEX pairs     event  TaxCollected ( address  indexed  from ,  uint256  amount );     event  TaxRatesUpdated ( uint256  buyTax ,  uint256  sellTax );     constructor (         string  memory  name ,         string  memory  symbol ,         uint256  initialSupply ,         address  _taxReceiver     )  ERC20 (name, symbol)  Ownable (msg.sender) {         require (_taxReceiver  !=  address ( 0 ),  "Invalid tax receiver" );         taxReceiver  =  _taxReceiver;         isExcludedFromTax[ msg.sender ]  =  true ;         isExcludedFromTax[_taxReceiver]  =  true ;         _mint ( msg.sender , initialSupply);     }     function  _update (         address  from ,         address  to ,         uint256  amount     )  internal  override  {         // Skip tax if sender or receiver is excluded         if  (isExcludedFromTax[from]  ||  isExcludedFromTax[to]) {             super . _update (from, to, amount);             return ;         }         uint256  taxAmount  =  0 ;         // Buy tax (from pair to user)         if  (isPair[from]  &&  buyTaxPercent  >  0 ) {             taxAmount  =  (amount  *  buyTaxPercent)  /  100 ;         }         // Sell tax (from user to pair)         else  if  (isPair[to]  &&  sellTaxPercent  >  0 ) {             taxAmount  =  (amount  *  sellTaxPercent)  /  100 ;         }         if  (taxAmount  >  0 ) {             super . _update (from, taxReceiver, taxAmount);             emit  TaxCollected (from, taxAmount);             amount  -=  taxAmount;         }         super . _update (from, to, amount);     }     function  setTaxRates ( uint256  _buyTax ,  uint256  _sellTax )  external  onlyOwner  {         require (_buyTax  <=  25  &&  _sellTax  <=  25 ,  "Tax too high" );         buyTaxPercent  =  _buyTax;         sellTaxPercent  =  _sellTax;         emit  TaxRatesUpdated (_buyTax, _sellTax);     }     function  setTaxReceiver ( address  _taxReceiver )  external  onlyOwner  {         require (_taxReceiver  !=  address ( 0 ),  "Invalid address" );         taxReceiver  =  _taxReceiver;     }     function  excludeFromTax ( address  account ,  bool  excluded )  external  onlyOwner  {         isExcludedFromTax[account]  =  excluded;     }     function  setPair ( address  pairAddress ,  bool  value )  external  onlyOwner  {         isPair[pairAddress]  =  value;     } } Deployment Scripts scripts/deploy-token.js
Foundry Deploy
scripts/deploy-token.ts
const  hre  =  require ( "hardhat" ); async  function  main () {   const  [ deployer ]  =  await  ethers . getSigners ();   console . log ( "Deploying token with account:" ,  deployer . address );   console . log ( "Account balance:" ,  ethers . formatEther ( await  ethers . provider . getBalance ( deployer . address )));   // Deploy AdvancedToken   const  AdvancedToken  =  await  ethers . getContractFactory ( "AdvancedToken" );   const  token  =  await  AdvancedToken . deploy (     "Nexis Token" ,            // name     "NXT" ,                    // symbol     ethers . parseEther ( "1000000" )  // initial supply (1M tokens)   );   await  token . waitForDeployment ();   const  tokenAddress  =  await  token . getAddress ();   console . log ( "Token deployed to:" ,  tokenAddress );   console . log ( "Token name:" ,  await  token . name ());   console . log ( "Token symbol:" ,  await  token . symbol ());   console . log ( "Total supply:" ,  ethers . formatEther ( await  token . totalSupply ()));   // Verify contract (optional)   if  ( network . name  !==  "hardhat"  &&  network . name  !==  "localhost" ) {     console . log ( "Waiting for block confirmations..." );     await  token . deploymentTransaction (). wait ( 5 );     console . log ( "Verifying contract..." );     try  {       await  hre . run ( "verify:verify" , {         address:  tokenAddress ,         constructorArguments:  [           "Nexis Token" ,           "NXT" ,           ethers . parseEther ( "1000000" )         ],       });     }  catch  ( error ) {       console . log ( "Verification error:" ,  error . message );     }   }   // Save deployment info   const  fs  =  require ( 'fs' );   const  deploymentInfo  =  {     network:  network . name ,     token:  tokenAddress ,     deployer:  deployer . address ,     timestamp:  new  Date (). toISOString (),     blockNumber:  await  ethers . provider . getBlockNumber ()   };   fs . writeFileSync (     `deployments/ ${ network . name } -token.json` ,     JSON . stringify ( deploymentInfo ,  null ,  2 )   );   console . log ( "Deployment info saved to deployments/" ); } main ()   . then (()  =>  process . exit ( 0 ))   . catch (( error )  =>  {     console . error ( error );     process . exit ( 1 );   }); 
Deploy Your Token # Deploy to testnet npx  hardhat  run  scripts/deploy-token.js  --network  nexisTestnet # Deploy to mainnet npx  hardhat  run  scripts/deploy-token.js  --network  nexisMainnet Interact with Your Token JavaScript (ethers.js)
Python (Web3.py)
cURL (JSON-RPC)
const  {  ethers  }  =  require ( "ethers" ); const  provider  =  new  ethers . JsonRpcProvider ( "https://testnet-rpc.nex-t1.ai" ); const  wallet  =  new  ethers . Wallet ( process . env . PRIVATE_KEY ,  provider ); const  tokenAddress  =  "0x..." ;  // Your deployed token const  tokenABI  =  [ ... ];  // Your token ABI const  token  =  new  ethers . Contract ( tokenAddress ,  tokenABI ,  wallet ); // Check balance const  balance  =  await  token . balanceOf ( wallet . address ); console . log ( "Balance:" ,  ethers . formatEther ( balance )); // Transfer tokens const  transferTx  =  await  token . transfer (   "0xRecipientAddress" ,   ethers . parseEther ( "100" ) ); await  transferTx . wait (); console . log ( "Transfer complete:" ,  transferTx . hash ); // Approve spending const  approveTx  =  await  token . approve (   "0xSpenderAddress" ,   ethers . parseEther ( "1000" ) ); await  approveTx . wait (); // Mint new tokens (if you have MINTER_ROLE) const  mintTx  =  await  token . mint (   wallet . address ,   ethers . parseEther ( "10000" ) ); await  mintTx . wait (); console . log ( "Minted 10,000 tokens" ); // Burn tokens const  burnTx  =  await  token . burn ( ethers . parseEther ( "100" )); await  burnTx . wait (); console . log ( "Burned 100 tokens" ); 
Testing Your Token Write comprehensive tests before deployment: 
const  {  expect  }  =  require ( "chai" ); const  {  ethers  }  =  require ( "hardhat" ); describe ( "AdvancedToken" ,  function  () {   let  token ;   let  owner ;   let  addr1 ;   let  addr2 ;   beforeEach ( async  function  () {     [ owner ,  addr1 ,  addr2 ]  =  await  ethers . getSigners ();     const  Token  =  await  ethers . getContractFactory ( "AdvancedToken" );     token  =  await  Token . deploy (       "Test Token" ,       "TST" ,       ethers . parseEther ( "1000000" )     );   });   describe ( "Deployment" ,  function  () {     it ( "Should set the correct name and symbol" ,  async  function  () {       expect ( await  token . name ()). to . equal ( "Test Token" );       expect ( await  token . symbol ()). to . equal ( "TST" );     });     it ( "Should mint initial supply to owner" ,  async  function  () {       const  balance  =  await  token . balanceOf ( owner . address );       expect ( balance ). to . equal ( ethers . parseEther ( "1000000" ));     });     it ( "Should grant roles to deployer" ,  async  function  () {       const  DEFAULT_ADMIN_ROLE  =  await  token . DEFAULT_ADMIN_ROLE ();       const  MINTER_ROLE  =  await  token . MINTER_ROLE ();       expect ( await  token . hasRole ( DEFAULT_ADMIN_ROLE ,  owner . address )). to . be . true ;       expect ( await  token . hasRole ( MINTER_ROLE ,  owner . address )). to . be . true ;     });   });   describe ( "Transfers" ,  function  () {     it ( "Should transfer tokens between accounts" ,  async  function  () {       await  token . transfer ( addr1 . address ,  ethers . parseEther ( "100" ));       expect ( await  token . balanceOf ( addr1 . address )). to . equal ( ethers . parseEther ( "100" ));       await  token . connect ( addr1 ). transfer ( addr2 . address ,  ethers . parseEther ( "50" ));       expect ( await  token . balanceOf ( addr2 . address )). to . equal ( ethers . parseEther ( "50" ));     });     it ( "Should fail when sender has insufficient balance" ,  async  function  () {       await  expect (         token . connect ( addr1 ). transfer ( addr2 . address ,  ethers . parseEther ( "1" ))       ). to . be . reverted ;     });   });   describe ( "Minting" ,  function  () {     it ( "Should mint tokens with MINTER_ROLE" ,  async  function  () {       await  token . mint ( addr1 . address ,  ethers . parseEther ( "1000" ));       expect ( await  token . balanceOf ( addr1 . address )). to . equal ( ethers . parseEther ( "1000" ));     });     it ( "Should not exceed max supply" ,  async  function  () {       const  maxSupply  =  await  token . MAX_SUPPLY ();       const  currentSupply  =  await  token . totalSupply ();       const  mintAmount  =  maxSupply  -  currentSupply  +  ethers . parseEther ( "1" );       await  expect (         token . mint ( addr1 . address ,  mintAmount )       ). to . be . revertedWith ( "Exceeds max supply" );     });     it ( "Should fail without MINTER_ROLE" ,  async  function  () {       await  expect (         token . connect ( addr1 ). mint ( addr2 . address ,  ethers . parseEther ( "100" ))       ). to . be . reverted ;     });   });   describe ( "Burning" ,  function  () {     it ( "Should burn tokens" ,  async  function  () {       await  token . transfer ( addr1 . address ,  ethers . parseEther ( "1000" ));       await  token . connect ( addr1 ). burn ( ethers . parseEther ( "500" ));       expect ( await  token . balanceOf ( addr1 . address )). to . equal ( ethers . parseEther ( "500" ));     });   });   describe ( "Pausing" ,  function  () {     it ( "Should pause and unpause transfers" ,  async  function  () {       await  token . pause ();       await  expect (         token . transfer ( addr1 . address ,  ethers . parseEther ( "100" ))       ). to . be . reverted ;       await  token . unpause ();       await  token . transfer ( addr1 . address ,  ethers . parseEther ( "100" ));       expect ( await  token . balanceOf ( addr1 . address )). to . equal ( ethers . parseEther ( "100" ));     });   }); }); Run tests: 
# Hardhat npx  hardhat  test # Foundry forge  test  -vvv Best Practices 
Use OpenZeppelin audited contracts 
Implement access control for privileged functions 
Set reasonable max supply caps 
Add reentrancy guards for complex transfers 
Test thoroughly before mainnet deployment 
Consider getting a professional audit for tokens with tax mechanisms 
 
Use uint256 instead of smaller uints (counter-intuitive but cheaper) 
Cache storage variables in memory 
Use events instead of storing data on-chain when possible 
Minimize storage writes 
Batch operations when possible 
 
Choose appropriate initial supply and max cap 
Consider vesting schedules for team/investors 
Plan liquidity provision strategy 
Set reasonable tax rates (if applicable) 
Implement anti-whale mechanisms if needed 
 
Consider using proxy patterns for upgradeability 
Document all state variables for upgrade compatibility 
Test upgrade paths thoroughly 
Have emergency pause mechanisms 
Plan for governance if upgrades are community-controlled 
 Common Use Cases Governance Token // Add voting power tracking import  "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol" ; contract  GovernanceToken  is  ERC20 ,  ERC20Votes ,  Ownable  {     constructor ()         ERC20 ("Governance Token", "GOV")         ERC20Permit ("Governance Token")         Ownable (msg.sender)     {         _mint ( msg.sender ,  10_000_000  *  10 ** 18 );     }     function  _update ( address  from ,  address  to ,  uint256  value )         internal         override ( ERC20 ,  ERC20Votes )     {         super . _update (from, to, value);     }     function  nonces ( address  owner )         public         view         override ( ERC20Permit ,  Nonces )         returns  ( uint256 )     {         return  super . nonces (owner);     } } Wrapped Token // Wrap native NZT to ERC20 contract  WrappedNZT  is  ERC20  {     event  Deposit ( address  indexed  account ,  uint256  amount );     event  Withdrawal ( address  indexed  account ,  uint256  amount );     constructor ()  ERC20 ("Wrapped NZT", "wNZT") {}     function  deposit ()  public  payable  {         _mint ( msg.sender ,  msg .value);         emit  Deposit ( msg.sender ,  msg .value);     }     function  withdraw ( uint256  amount )  public  {         _burn ( msg.sender , amount);         payable ( msg.sender ). transfer (amount);         emit  Withdrawal ( msg.sender , amount);     }     receive ()  external  payable  {         deposit ();     } } Next Steps Troubleshooting 
Deployment fails with 'insufficient funds'
Make sure you have enough testnet NZT. Get more from the faucet . 
Contract verification fails
Ensure constructor arguments match exactly. Use --constructor-args flag with Foundry or provide them in Hardhat verify config. 
Transfers fail after deployment
Check if token is paused, if recipient is blacklisted, or if there are any transfer restrictions in your contract. 
Increase gas limit or check for revert conditions. Use eth_estimateGas to debug. 
Additional Resources Ready for Production?  Always conduct thorough testing and consider a professional audit before deploying to mainnet, especially for tokens with complex mechanics or large value.