INSTRUCTOR ONLY -- DO NOT DISTRIBUTE TO STUDENTS

Complete Reference Implementation

Below is the full ERC-20 token contract with all three custom features (burn, mint, pause). This is the complete solution that fills in every TODO from the starter code template.

Full ERC-20 Token Contract
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract MyToken { // ── State Variables ────────────────────────────────────────── string public name; string public symbol; uint8 public decimals = 18; uint256 public totalSupply; address public owner; bool public paused; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; // ── Events ─────────────────────────────────────────────────── event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); event Burn(address indexed from, uint256 value); event Mint(address indexed to, uint256 value); event Paused(bool isPaused); // ── Modifier ───────────────────────────────────────────────── modifier onlyOwner() { require(msg.sender == owner, "Not the owner"); _; } modifier whenNotPaused() { require(!paused, "Token is paused"); _; } // ── Constructor ────────────────────────────────────────────── constructor(string memory _name, string memory _symbol, uint256 _initialSupply) { name = _name; symbol = _symbol; owner = msg.sender; totalSupply = _initialSupply; balanceOf[msg.sender] = _initialSupply; emit Transfer(address(0), msg.sender, _initialSupply); } // ── Core ERC-20 Functions ──────────────────────────────────── function transfer(address _to, uint256 _amount) public whenNotPaused returns (bool) { require(balanceOf[msg.sender] >= _amount, "Insufficient balance"); require(_to != address(0), "Cannot transfer to zero address"); balanceOf[msg.sender] -= _amount; balanceOf[_to] += _amount; emit Transfer(msg.sender, _to, _amount); return true; } function approve(address _spender, uint256 _amount) public returns (bool) { allowance[msg.sender][_spender] = _amount; emit Approval(msg.sender, _spender, _amount); return true; } function transferFrom(address _from, address _to, uint256 _amount) public whenNotPaused returns (bool) { require(balanceOf[_from] >= _amount, "Insufficient balance"); require(allowance[_from][msg.sender] >= _amount, "Insufficient allowance"); require(_to != address(0), "Cannot transfer to zero address"); balanceOf[_from] -= _amount; balanceOf[_to] += _amount; allowance[_from][msg.sender] -= _amount; emit Transfer(_from, _to, _amount); return true; } // ── Custom Feature: Burn ───────────────────────────────────── function burn(uint256 _amount) public { require(balanceOf[msg.sender] >= _amount, "Insufficient balance to burn"); balanceOf[msg.sender] -= _amount; totalSupply -= _amount; emit Burn(msg.sender, _amount); emit Transfer(msg.sender, address(0), _amount); } // ── Custom Feature: Mint ───────────────────────────────────── function mint(address _to, uint256 _amount) public onlyOwner { require(_to != address(0), "Cannot mint to zero address"); totalSupply += _amount; balanceOf[_to] += _amount; emit Mint(_to, _amount); emit Transfer(address(0), _to, _amount); } // ── Custom Feature: Pause ──────────────────────────────────── function pause() public onlyOwner { paused = true; emit Paused(true); } function unpause() public onlyOwner { paused = false; emit Paused(false); } }

Part 1: Base Contract -- Grading Details

1A. Constructor

Required elements:

  • Set name and symbol from constructor parameters
  • Set totalSupply to the initial supply parameter
  • Assign all tokens to msg.sender via balanceOf[msg.sender] = _initialSupply
  • Set owner = msg.sender

Acceptable variations:

  • Hardcoding name/symbol instead of passing as parameters (minor deduction only)
  • Not emitting a Transfer event from address(0) in constructor (this is optional for BSc level)
  • Using a simple uint256 parameter for supply without multiplying by 10**decimals
Grading: Full credit (5 pts) if contract compiles, deploys, and balanceOf(deployer) returns the correct amount. Deduct 1 point for each missing element.
1B. transfer()
function transfer(address _to, uint256 _amount) public returns (bool) { require(balanceOf[msg.sender] >= _amount, "Insufficient balance"); balanceOf[msg.sender] -= _amount; balanceOf[_to] += _amount; emit Transfer(msg.sender, _to, _amount); return true; }

Key elements (6 points):

  • Balance check with require: 2 points
  • Balance deduction + addition: 2 points
  • Event emission: 1 point
  • Returns true: 1 point
Common Student Mistakes:
  • Forgetting the returns (bool) in the function signature
  • Not checking balance before subtracting (works in Solidity 0.8+ due to underflow protection, but bad practice)
  • Forgetting to emit the Transfer event
  • Using = instead of -= or +=
1C. approve()
function approve(address _spender, uint256 _amount) public returns (bool) { allowance[msg.sender][_spender] = _amount; emit Approval(msg.sender, _spender, _amount); return true; }

Key elements (4 points):

  • Sets allowance mapping correctly: 2 points
  • Emits Approval event: 1 point
  • Returns true: 1 point
Note: This is the simplest function. Most students get full credit here. If they struggle with the nested mapping syntax allowance[msg.sender][_spender], point them to the Lesson 10 slides on nested mappings.
1D. transferFrom()
function transferFrom(address _from, address _to, uint256 _amount) public returns (bool) { require(balanceOf[_from] >= _amount, "Insufficient balance"); require(allowance[_from][msg.sender] >= _amount, "Insufficient allowance"); balanceOf[_from] -= _amount; balanceOf[_to] += _amount; allowance[_from][msg.sender] -= _amount; emit Transfer(_from, _to, _amount); return true; }

Key elements (6 points):

  • Balance check on _from: 1 point
  • Allowance check on allowance[_from][msg.sender]: 2 points
  • Balance updates (deduct from _from, add to _to): 1 point
  • Allowance decrease: 1 point (MOST COMMONLY MISSED)
  • Event emission: 1 point
Common Student Mistakes:
  • Most common: Forgetting to decrease the allowance after transfer. Without this line, a spender could transfer unlimited tokens after a single approval
  • Checking balanceOf[msg.sender] instead of balanceOf[_from]
  • Using allowance[msg.sender][_from] instead of allowance[_from][msg.sender] (reversed)

Part 2: Custom Features -- Grading Details

Burn Feature
function burn(uint256 _amount) public { require(balanceOf[msg.sender] >= _amount, "Insufficient balance to burn"); balanceOf[msg.sender] -= _amount; totalSupply -= _amount; emit Burn(msg.sender, _amount); emit Transfer(msg.sender, address(0), _amount); }

Grading (10 + 3 + 2 = 15 points):

  • Balance check + deduction: 4 points
  • TotalSupply decrease: 3 points (key differentiator from just discarding tokens)
  • Event emission: 3 points
  • Access control: 3 points (full credit -- burn needs no access control since users burn their own tokens)
  • Demo: 2 points (show totalSupply decreasing)
Acceptable variation: Not emitting Transfer(sender, address(0), amount) is fine. The custom Burn event alone is sufficient.
Mint Feature
function mint(address _to, uint256 _amount) public onlyOwner { require(_to != address(0), "Cannot mint to zero address"); totalSupply += _amount; balanceOf[_to] += _amount; emit Mint(_to, _amount); emit Transfer(address(0), _to, _amount); }

Grading (10 + 3 + 2 = 15 points):

  • TotalSupply increase: 3 points
  • Balance increase for target: 3 points
  • Event emission: 2 points
  • Zero-address check: 2 points
  • Access control (onlyOwner): 3 points (CRITICAL -- mint without access control is a serious bug)
  • Demo: 2 points (show new tokens appearing)
Common Mistake: Students implement mint without onlyOwner. This means anyone can create unlimited tokens. Deduct full access control points (3 pts) if this is missing. Discuss why this is dangerous (token value would drop to zero).
Pause Feature
bool public paused; modifier whenNotPaused() { require(!paused, "Token is paused"); _; } function pause() public onlyOwner { paused = true; emit Paused(true); } function unpause() public onlyOwner { paused = false; emit Paused(false); }

Then add whenNotPaused modifier to transfer() and transferFrom().

Grading (10 + 3 + 2 = 15 points):

  • Paused boolean state variable: 2 points
  • whenNotPaused modifier with require: 3 points
  • pause() and unpause() functions: 2 points
  • Modifier applied to transfer/transferFrom: 3 points
  • Access control (onlyOwner on pause/unpause): 3 points
  • Demo: 2 points (show transfer failing when paused, succeeding after unpause)
Note: This is the most complex feature. Students who attempt it demonstrate strong understanding of modifiers. Grade generously -- even a partially working pause mechanism deserves significant credit.

Testing Checklist for Instructor

Quick Verification Steps in Remix
Test Expected Result Points at Risk
Deploy with supply = 1000 balanceOf(deployer) = 1000, totalSupply = 1000 5 pts
transfer(account2, 100) deployer = 900, account2 = 100 6 pts
transfer(account2, 10000) -- too much Reverts with "Insufficient balance" 2 pts (part of transfer)
approve(account2, 50) from deployer allowance(deployer, account2) = 50 4 pts
transferFrom(deployer, account3, 30) from account2 deployer = 870, account3 = 30, allowance = 20 6 pts
burn(100) from deployer deployer -= 100, totalSupply -= 100 10 pts (feature)
mint(account2, 500) from deployer account2 += 500, totalSupply += 500 10 pts (feature)
mint(account2, 500) from account2 (not owner) Reverts with "Not the owner" 3 pts (access control)
pause() then transfer() Transfer reverts with "Token is paused" 10 pts (feature)

© Joerg Osterrieder 2025-2026. All rights reserved.