🎓 A1Learner
🔍
0 of 48 lessons completed

Solidity ERC20 Mechanics

An ERC20 token is a fungible token where every unit is identical to every other unit. A contract tracks a total supply and a balance for each holder.

Supply starts with one holder

The constructor creates 1000 units and gives them all to the deployer. The total supply and the deployer balance both read 1000.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract CoinSupply {
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    constructor() {
        totalSupply = 1000;
        balanceOf[msg.sender] = 1000;
    }
}

Transfer moves units between balances

Transfer subtracts from the caller and adds to the receiver. The total supply does not change.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract SimpleCoin {
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    event Transfer(address indexed from, address indexed to, uint256 value);

    constructor() {
        totalSupply = 1000;
        balanceOf[msg.sender] = 1000;
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero receiver");
        require(balanceOf[msg.sender] >= amount, "not enough balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
}

Bad transfers revert

A transfer to the zero address reverts. A transfer above the caller balance also reverts.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract SimpleCoin {
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    event Transfer(address indexed from, address indexed to, uint256 value);

    constructor() {
        totalSupply = 1000;
        balanceOf[msg.sender] = 1000;
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero receiver");
        require(balanceOf[msg.sender] >= amount, "not enough balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
}

Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract ShopCoin {
    string public name = "ShopCoin";
    string public symbol = "SHOP";
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    event Transfer(address indexed from, address indexed to, uint256 value);

    constructor() {
        totalSupply = 10000;
        balanceOf[msg.sender] = 10000;
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero receiver");
        require(balanceOf[msg.sender] >= amount, "not enough balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
}

Key points

  • Fungible: every unit of the token is identical and interchangeable.
  • Total supply: the total number of units that exist.
  • Balance: the number of units one address holds.
  • Transfer: the function that moves units from the caller to another address.
  • Transfer event: a log that records each move, with the sender, the receiver, and the amount.
  • Zero address: the empty address that a transfer must never use as a receiver.

In this example, a named token starts with 10000 units held by the deployer. Transferring 4000 units moves them to the receiver, leaves 6000 with the sender, and leaves the total supply unchanged.

Note

the token standard is a shared set of function names, so wallets and other contracts can read any token the same way.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Remix IDE

Exercise

What does transfer change?