Skip to content
arshamhaqPublic

Repository files navigation

Spades ERC20 (SPD)

Spades ERC20 architecture

Spades is an educational ERC-20 built with Foundry and OpenZeppelin Contracts. It demonstrates capped minting, burning, emergency pausing, allowances, and role-based access control. It is a learning project, not an audited production token or an investment product.

Sepolia deployment

Supply policy

  • initialSupply and maximumSupply are fixed at deployment input time and use the token's smallest unit.
  • Both values must be nonzero, and the initial supply cannot exceed the cap.
  • The immutable cap limits the current totalSupply; no mint can push supply above it.
  • The initial supply is minted to the deployment broadcaster, stored as i_treasury.
  • An authorized minter may mint to any address while the token is unpaused.
  • Holders may burn tokens. Burning lowers totalSupply and therefore creates room that can later be minted again, up to the same cap.

Roles and permissions

Actor or role Permissions
DEFAULT_ADMIN_ROLE Grant and revoke all roles, including additional admin roles.
MINTER_ROLE Call mint(to, amount), subject to the cap and pause state.
PAUSER_ROLE Call pause() and unpause().
Treasury Receives the initial supply. Treasury status itself grants no special role.
Token holder Transfer, approve, and burn their own tokens.
Approved spender Use transferFrom or burnFrom within the granted allowance.

The deployment script currently assigns the admin, minter, and pauser roles to the selected owner address. The treasury is the address derived from the private key that broadcasts the deployment, so the owner address should match that key.

Architecture

ERC20SPD combines these OpenZeppelin modules:

  • ERC20 for balances, transfers, and allowances.
  • ERC20Capped for the immutable maximum supply.
  • ERC20Burnable for burn and burnFrom.
  • ERC20Pausable for stopping transfers, minting, and burning.
  • AccessControl for admin, minter, and pauser permissions.

All balance and supply changes pass through the shared _update override, allowing the cap and pause checks to apply consistently.

Installation and testing

Requirements: Foundry, Git, and Make.

git clone --recurse-submodules <repository-url>
cd ERC20
cp .env.example .env
forge build
forge test

If the repository was cloned without submodules:

git submodule update --init --recursive

The same build and test commands are available as:

make build
make test

Environment configuration

Copy .env.example to .env and fill in the appropriate network values. Never commit .env or use an Anvil test key on a public network.

  • Anvil uses PRIVATE_KEY_ANVIL, OWNER_ADDRESS_ANVIL, RPC_URL_ANVIL, and CONTRACT_ADDRESS_ANVIL.
  • Sepolia uses PRIVATE_KEY, OWNER_ADDRESS, RPC_URL, and CONTRACT_ADDRESS_SEPOLIA.
  • INITIAL_SUPPLY and MAXIMUM_SUPPLY are raw 18-decimal token units.

Deployment

Start Anvil in a separate terminal:

anvil

Then deploy locally:

make deploy-anvil

Copy the deployed address from the output into CONTRACT_ADDRESS_ANVIL in .env.

For Sepolia, provide a funded Sepolia deployer key, matching owner address, and RPC endpoint, then run:

make deploy-sepolia

Make targets and shell input

Once .env is configured, these targets require no command-line input:

Command Purpose
make build Compile the project.
make test Run the Foundry test suite.
make deploy-anvil Deploy using the Anvil environment values.
make deploy-sepolia Deploy using the Sepolia environment values.
make show-anvil Show token metadata plus the default Anvil owner's balance and minter role.
make show-sepolia Show token metadata plus the default Sepolia owner's balance and minter role.
make balance-anvil Show the default Anvil owner's balance.
make balance-sepolia Show the default Sepolia owner's balance.

The read targets accept optional account overrides:

make balance-anvil ACCOUNT_ANVIL=0xAddress
make balance-sepolia ACCOUNT_SEPOLIA=0xAddress
make show-anvil ACCOUNT_ANVIL=0xAddress
make show-sepolia ACCOUNT_SEPOLIA=0xAddress

These targets require input on the command line:

Command Required input
make transfer-anvil TO, AMOUNT
make transfer-sepolia TO, AMOUNT
make approve-anvil SPENDER, AMOUNT
make approve-sepolia SPENDER, AMOUNT
make transfer-from-anvil FROM, TO, AMOUNT; the selected Anvil key must belong to the approved spender
make transfer-from-sepolia FROM, TO, AMOUNT; the selected Sepolia key must belong to the approved spender

Example write operations:

make transfer-anvil TO=0xRecipient AMOUNT=1000000000000000000
make approve-anvil SPENDER=0xSpender AMOUNT=1000000000000000000
make transfer-from-anvil FROM=0xOwner TO=0xRecipient AMOUNT=1000000000000000000

For transferFrom, the transaction signer must be the spender that received the allowance. Override PRIVATE_KEY_ANVIL or PRIVATE_KEY only with disposable test credentials; putting a real private key directly on a command line exposes it in shell history.

Direct Cast examples

Export the variables from .env before using raw cast commands:

set -a
source .env
set +a

Read calls:

cast call "$CONTRACT_ADDRESS_SEPOLIA" "name()(string)" --rpc-url "$RPC_URL"
cast call "$CONTRACT_ADDRESS_SEPOLIA" "symbol()(string)" --rpc-url "$RPC_URL"
cast call "$CONTRACT_ADDRESS_SEPOLIA" "totalSupply()(uint256)" --rpc-url "$RPC_URL"
cast call "$CONTRACT_ADDRESS_SEPOLIA" "cap()(uint256)" --rpc-url "$RPC_URL"
cast call "$CONTRACT_ADDRESS_SEPOLIA" "balanceOf(address)(uint256)" "$OWNER_ADDRESS" --rpc-url "$RPC_URL"

MINTER_ROLE=$(cast keccak "MINTER_ROLE")
cast call "$CONTRACT_ADDRESS_SEPOLIA" "hasRole(bytes32,address)(bool)" "$MINTER_ROLE" "$OWNER_ADDRESS" --rpc-url "$RPC_URL"

Write calls:

cast send "$CONTRACT_ADDRESS_SEPOLIA" "transfer(address,uint256)" "$TO" "$AMOUNT" --private-key "$PRIVATE_KEY" --rpc-url "$RPC_URL"
cast send "$CONTRACT_ADDRESS_SEPOLIA" "approve(address,uint256)" "$SPENDER" "$AMOUNT" --private-key "$PRIVATE_KEY" --rpc-url "$RPC_URL"
cast send "$CONTRACT_ADDRESS_SEPOLIA" "transferFrom(address,address,uint256)" "$FROM" "$TO" "$AMOUNT" --private-key "$SPENDER_PRIVATE_KEY" --rpc-url "$RPC_URL"

Security assumptions

  • This project is educational and has not been professionally audited.
  • OpenZeppelin Contracts supply the core ERC-20, cap, burn, pause, and access-control behavior.
  • The configured owner address is assumed to match the deployment key.
  • The minter is trusted not to mint unexpectedly, although the immutable cap limits the maximum current supply.
  • The pauser is trusted to use emergency controls responsibly; pausing blocks transfers, minting, and burning.
  • Private keys and RPC credentials must remain outside version control.
  • The contract is not upgradeable, rejects direct native-currency transfers, and contains no recovery function for assets sent incorrectly.

Why the admin role is sensitive

DEFAULT_ADMIN_ROLE administers every role and is also its own administrator. A compromised admin can appoint new minters, appoint pausers, remove legitimate operators, or grant another account full admin control. The cap limits inflation, but it does not prevent a malicious admin from changing who can mint or from disrupting the token through role changes.

For a production system, place admin control behind a multisig or timelock and consider OpenZeppelin's AccessControlDefaultAdminRules instead of using a single externally owned account.

Releases

Packages

Contributors

Languages