Skip to main content

ERC20 Token Example

The fastest way to create an ERC20 token is extending the built-in ERC20 contract from the standard library:

contracts/Token.ts
import { address, msg } from "skittles";
import { ERC20 } from "skittles/contracts";

export class Token extends ERC20 {
private _owner: address;

constructor(initialSupply: number) {
super("MyToken", "MTK");
this._owner = msg.sender;
this._mint(msg.sender, initialSupply);
}

public mint(to: address, amount: number): void {
if (msg.sender != this._owner) {
throw new Error("Caller is not the owner");
}
this._mint(to, amount);
}

public burn(amount: number): void {
this._burn(msg.sender, amount);
}
}

This gives you a complete ERC20 with name, symbol, decimals, totalSupply, balanceOf, transfer, approve, transferFrom, and allowance — all from a few lines of code. See the Standard Library guide for details on all available contracts.

From Scratch

You can also build an ERC20 from scratch for full control over the implementation.

Interface

First, define the token interface:

contracts/IToken.ts
import { address } from "skittles";

export default interface IToken {
name: string;
symbol: string;
decimals: number;
totalSupply: number;
balanceOf(account: address): number;
transfer(to: address, amount: number): boolean;
approve(spender: address, amount: number): boolean;
transferFrom(from: address, to: address, amount: number): boolean;
allowance(owner: address, spender: address): number;
}

Contract

contracts/Token.ts
import { address, msg, SkittlesEvent, SkittlesError, Indexed } from "skittles";
import IToken from "./IToken";

export class Token implements IToken {
Transfer: SkittlesEvent<{
from: Indexed<address>;
to: Indexed<address>;
value: number;
}>;
Approval: SkittlesEvent<{
owner: Indexed<address>;
spender: Indexed<address>;
value: number;
}>;

InsufficientBalance: SkittlesError<{
sender: address;
balance: number;
required: number;
}>;
InsufficientAllowance: SkittlesError<{
spender: address;
allowance: number;
required: number;
}>;

public name: string = "Skittles Token";
public symbol: string = "SKT";
public decimals: number = 18;
public totalSupply: number = 0;

private balances: Record<address, number> = {};
private allowances: Record<address, Record<address, number>> = {};

constructor(initialSupply: number) {
this.totalSupply = initialSupply;
this.balances[msg.sender] = initialSupply;
this.Transfer.emit(
"0x0000000000000000000000000000000000000000",
msg.sender,
initialSupply,
);
}

public balanceOf(account: address): number {
return this.balances[account];
}

public allowance(owner: address, spender: address): number {
return this.allowances[owner][spender];
}

public transfer(to: address, amount: number): boolean {
this._transfer(msg.sender, to, amount);
return true;
}

public approve(spender: address, amount: number): boolean {
this._approve(msg.sender, spender, amount);
return true;
}

public transferFrom(from: address, to: address, amount: number): boolean {
let currentAllowance: number = this.allowances[from][msg.sender];

if (currentAllowance < amount) {
throw this.InsufficientAllowance(msg.sender, currentAllowance, amount);
}

if (currentAllowance != Number.MAX_VALUE) {
this._approve(from, msg.sender, currentAllowance - amount);
}

this._transfer(from, to, amount);
return true;
}

private _transfer(from: address, to: address, amount: number): void {
if (this.balances[from] < amount) {
throw this.InsufficientBalance(from, this.balances[from], amount);
}
this.balances[from] -= amount;
this.balances[to] += amount;
this.Transfer.emit(from, to, amount);
}

private _approve(owner: address, spender: address, amount: number): void {
this.allowances[owner][spender] = amount;
this.Approval.emit(owner, spender, amount);
}
}

Key Patterns

This example demonstrates several Skittles features:

  • implements IToken: TypeScript interface enforcement for type safety in the IDE
  • SkittlesEvent<T> with Indexed<T>: Events with indexed parameters for efficient off chain filtering
  • SkittlesError<T>: Gas efficient custom errors with typed parameters
  • Nested mappings: Record<address, Record<address, number>> for the allowances mapping
  • Number.MAX_VALUE: Compiles to type(uint256).max for unlimited allowance patterns
  • Private helper functions: _transfer and _approve encapsulate reusable logic
  • State mutability inference: balanceOf and allowance are automatically view, transfer and approve are automatically nonpayable

Running This Example

This contract is part of the example project in the Skittles repository:

git clone https://github.com/chase-manning/skittles.git
cd skittles/example
npm install
npm run compile
npm test