Build a Token¶
Create a complete token contract with transfers, approvals, and minting.
Prerequisites¶
- SolScript installed
- Basic understanding of tokens
What We'll Build¶
A token with: - Minting by owner - Transfers between users - Approval and transferFrom - Total supply tracking - Events for all operations
Step 1: Contract Structure¶
Create token.sol:
contract Token {
// Token metadata
string public name;
string public symbol;
uint8 public decimals = 9;
// Token state
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
// Owner
address public owner;
// Events
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
event Mint(address indexed to, uint256 amount);
// Errors
error InsufficientBalance(uint256 available, uint256 required);
error InsufficientAllowance(uint256 available, uint256 required);
error Unauthorized();
error InvalidAddress();
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
}
Step 2: Constructor¶
Initialize the token:
constructor(string memory _name, string memory _symbol, uint256 initialSupply) {
name = _name;
symbol = _symbol;
owner = msg.sender;
// Mint initial supply to deployer
if (initialSupply > 0) {
_mint(msg.sender, initialSupply);
}
}
Step 3: Internal Mint Function¶
function _mint(address to, uint256 amount) internal {
if (to == address(0)) revert InvalidAddress();
totalSupply += amount;
balanceOf[to] += amount;
emit Transfer(address(0), to, amount);
}
Step 4: Public Mint (Owner Only)¶
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
emit Mint(to, amount);
}
Step 5: Transfer Function¶
function transfer(address to, uint256 amount) public returns (bool) {
return _transfer(msg.sender, to, amount);
}
function _transfer(address from, address to, uint256 amount) internal returns (bool) {
if (to == address(0)) revert InvalidAddress();
if (balanceOf[from] < amount) {
revert InsufficientBalance(balanceOf[from], amount);
}
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
Step 6: Approval System¶
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 returns (bool) {
uint256 currentAllowance = allowance[from][msg.sender];
if (currentAllowance < amount) {
revert InsufficientAllowance(currentAllowance, amount);
}
// Decrease allowance
allowance[from][msg.sender] = currentAllowance - amount;
return _transfer(from, to, amount);
}
Step 7: Utility Functions¶
function increaseAllowance(address spender, uint256 addedValue) public returns (bool) {
allowance[msg.sender][spender] += addedValue;
emit Approval(msg.sender, spender, allowance[msg.sender][spender]);
return true;
}
function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) {
uint256 currentAllowance = allowance[msg.sender][spender];
require(currentAllowance >= subtractedValue, "Decreased below zero");
allowance[msg.sender][spender] = currentAllowance - subtractedValue;
emit Approval(msg.sender, spender, allowance[msg.sender][spender]);
return true;
}
Complete Contract¶
contract Token {
// Metadata
string public name;
string public symbol;
uint8 public decimals = 9;
// State
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
address public owner;
// Events
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
event Mint(address indexed to, uint256 amount);
// Errors
error InsufficientBalance(uint256 available, uint256 required);
error InsufficientAllowance(uint256 available, uint256 required);
error Unauthorized();
error InvalidAddress();
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
constructor(string memory _name, string memory _symbol, uint256 initialSupply) {
name = _name;
symbol = _symbol;
owner = msg.sender;
if (initialSupply > 0) {
_mint(msg.sender, initialSupply);
}
}
function transfer(address to, uint256 amount) public returns (bool) {
return _transfer(msg.sender, to, amount);
}
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 returns (bool) {
uint256 currentAllowance = allowance[from][msg.sender];
if (currentAllowance < amount) {
revert InsufficientAllowance(currentAllowance, amount);
}
allowance[from][msg.sender] = currentAllowance - amount;
return _transfer(from, to, amount);
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
emit Mint(to, amount);
}
function increaseAllowance(address spender, uint256 addedValue) public returns (bool) {
allowance[msg.sender][spender] += addedValue;
emit Approval(msg.sender, spender, allowance[msg.sender][spender]);
return true;
}
function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) {
uint256 currentAllowance = allowance[msg.sender][spender];
require(currentAllowance >= subtractedValue, "Decreased below zero");
allowance[msg.sender][spender] = currentAllowance - subtractedValue;
emit Approval(msg.sender, spender, allowance[msg.sender][spender]);
return true;
}
function _transfer(address from, address to, uint256 amount) internal returns (bool) {
if (to == address(0)) revert InvalidAddress();
if (balanceOf[from] < amount) {
revert InsufficientBalance(balanceOf[from], amount);
}
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
function _mint(address to, uint256 amount) internal {
if (to == address(0)) revert InvalidAddress();
totalSupply += amount;
balanceOf[to] += amount;
emit Transfer(address(0), to, amount);
}
}
Build and Deploy¶
# Build
solscript build token.sol -o ./build
# Deploy to devnet
solscript deploy token.sol --network devnet
Testing¶
import * as anchor from "@coral-xyz/anchor";
describe("Token", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
it("mints initial supply", async () => {
// Deploy with initial supply
const initialSupply = 1_000_000;
// ... deployment code
});
it("transfers tokens", async () => {
// Test transfer functionality
});
it("handles approvals", async () => {
// Test approve and transferFrom
});
});
Extensions¶
Ideas for extending this token:
- Burnable: Add a
burnfunction - Pausable: Add pause/unpause functionality
- Capped: Add maximum supply limit
- Snapshot: Track historical balances