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.
- Contract:
0x805540aC3b8AE7dE3c991Df03999AD6fa36ca914 - Network: Ethereum Sepolia (
chainId 11155111) - Name:
Spades - Symbol:
SPD - Decimals:
18
initialSupplyandmaximumSupplyare 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
totalSupplyand therefore creates room that can later be minted again, up to the same cap.
| 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.
ERC20SPD combines these OpenZeppelin modules:
ERC20for balances, transfers, and allowances.ERC20Cappedfor the immutable maximum supply.ERC20BurnableforburnandburnFrom.ERC20Pausablefor stopping transfers, minting, and burning.AccessControlfor 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.
Requirements: Foundry, Git, and Make.
git clone --recurse-submodules <repository-url>
cd ERC20
cp .env.example .env
forge build
forge testIf the repository was cloned without submodules:
git submodule update --init --recursiveThe same build and test commands are available as:
make build
make testCopy .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, andCONTRACT_ADDRESS_ANVIL. - Sepolia uses
PRIVATE_KEY,OWNER_ADDRESS,RPC_URL, andCONTRACT_ADDRESS_SEPOLIA. INITIAL_SUPPLYandMAXIMUM_SUPPLYare raw 18-decimal token units.
Start Anvil in a separate terminal:
anvilThen deploy locally:
make deploy-anvilCopy 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-sepoliaOnce .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=0xAddressThese 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=1000000000000000000For 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.
Export the variables from .env before using raw cast commands:
set -a
source .env
set +aRead 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"- 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.
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.
