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
nameandsymbolfrom constructor parameters - Set
totalSupplyto the initial supply parameter - Assign all tokens to
msg.senderviabalanceOf[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
uint256parameter 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 ofbalanceOf[_from] - Using
allowance[msg.sender][_from]instead ofallowance[_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.