⚠️ Disclaimer: This document is provided as a smart contract coding example only. The code has NOT been professionally audited. Use at your own risk - you bear full responsibility for any losses if deployed to production.
ERC721 NFT Contract
A complete ERC-721 Non-Fungible Token implementation for TOS Network’s TAKO runtime.
Features
- Full ERC721 compliance
- Minting and burning capabilities
- Approval system for transfers
- Operator approval (approve all)
- Safe transfer checks
- Comprehensive error handling
Instruction Format
All instructions use format: [opcode:1][params:N]
| Opcode | Name | Parameters | Returns |
|---|---|---|---|
| 0x01 | BalanceOf | [owner:32] | u128 count |
| 0x02 | OwnerOf | [token_id:16] | address:32 |
| 0x03 | Approve | [to:32][token_id:16] | - |
| 0x04 | SetApprovalForAll | [owner:32][operator:32][approved:1] | - |
| 0x05 | TransferFrom | [from:32][to:32][token_id:16] | - |
| 0x06 | Mint | [to:32][token_id:16] | - |
| 0x07 | Burn | [token_id:16] | - |
Storage Layout
// Storage key prefixes
const KEY_TOTAL_SUPPLY: &[u8] = b"supply"; // Total minted tokens
const KEY_OWNER_PREFIX: &[u8] = b"own:"; // own:{token_id} -> owner address
const KEY_BALANCE_PREFIX: &[u8] = b"bal:"; // bal:{address} -> token count
const KEY_APPROVAL_PREFIX: &[u8] = b"apr:"; // apr:{token_id} -> approved address
const KEY_OPERATOR_PREFIX: &[u8] = b"opr:"; // opr:{owner}{operator} -> boolCore Implementation
Balance and Ownership
/// Get balance of owner (number of NFTs owned)
fn balance_of(owner: &Address32) -> u128 {
let key = make_balance_key(owner);
read_u128(&key)
}
/// Get owner of a specific token
fn owner_of(token_id: TokenId) -> entrypoint::Result<Address32> {
let key = make_owner_key(token_id);
let mut buffer = [0u8; 32];
let len = storage_read(&key, &mut buffer);
if len != 32 {
return Err(TokenNotExists);
}
Ok(buffer)
}
/// Check if token exists
fn exists(token_id: TokenId) -> bool {
let key = make_owner_key(token_id);
let mut buffer = [0u8; 32];
let len = storage_read(&key, &mut buffer);
len == 32
}Minting NFTs
/// Mint a new NFT to an address
fn mint(to: &Address32, token_id: TokenId) -> entrypoint::Result<()> {
// Cannot mint to zero address
if *to == [0u8; 32] {
return Err(ZeroAddress);
}
// Token must not already exist
if exists(token_id) {
return Err(TokenAlreadyMinted);
}
// Update balance
let balance = balance_of(to);
let balance_key = make_balance_key(to);
storage_write(&balance_key, &(balance + 1).to_le_bytes())?;
// Set owner
let owner_key = make_owner_key(token_id);
storage_write(&owner_key, to)?;
// Update total supply
let supply = read_u128(KEY_TOTAL_SUPPLY);
storage_write(KEY_TOTAL_SUPPLY, &(supply + 1).to_le_bytes())?;
log("Token minted");
Ok(())
}Token Transfers
/// Transfer token from one address to another
fn transfer_from(
from: &Address32,
to: &Address32,
token_id: TokenId
) -> entrypoint::Result<()> {
// Cannot transfer to zero address
if *to == [0u8; 32] {
return Err(ZeroAddress);
}
// Verify ownership
let owner = owner_of(token_id)?;
if *from != owner {
return Err(TransferFromIncorrectOwner);
}
// Clear approvals for this token
let approval_key = make_approval_key(token_id);
storage_delete(&approval_key);
// Update balances
let from_balance = balance_of(from);
if from_balance == 0 {
return Err(InsufficientBalance);
}
let from_key = make_balance_key(from);
storage_write(&from_key, &(from_balance - 1).to_le_bytes())?;
let to_balance = balance_of(to);
let to_key = make_balance_key(to);
storage_write(&to_key, &(to_balance + 1).to_le_bytes())?;
// Transfer ownership
let owner_key = make_owner_key(token_id);
storage_write(&owner_key, to)?;
log("Token transferred");
Ok(())
}Approval System
/// Approve an address to transfer a specific token
fn approve(to: &Address32, token_id: TokenId) -> entrypoint::Result<()> {
let owner = owner_of(token_id)?;
// Cannot approve to current owner
if *to == owner {
return Err(SameAddress);
}
let key = make_approval_key(token_id);
storage_write(&key, to)?;
log("Token approved");
Ok(())
}
/// Set or unset operator approval for all tokens
fn set_approval_for_all(
owner: &Address32,
operator: &Address32,
approved: bool,
) -> entrypoint::Result<()> {
if owner == operator {
return Err(SameAddress);
}
let key = make_operator_key(owner, operator);
let value: u8 = if approved { 1 } else { 0 };
storage_write(&key, &[value])?;
log("Operator approval updated");
Ok(())
}
/// Check if operator is approved for all tokens of an owner
fn is_approved_for_all(owner: &Address32, operator: &Address32) -> bool {
let key = make_operator_key(owner, operator);
let mut buffer = [0u8; 1];
let len = storage_read(&key, &mut buffer);
len == 1 && buffer[0] != 0
}Burning NFTs
/// Burn (destroy) a token
fn burn(token_id: TokenId) -> entrypoint::Result<()> {
let owner = owner_of(token_id)?;
// Clear approvals
let approval_key = make_approval_key(token_id);
storage_delete(&approval_key);
// Update balance
let balance = balance_of(&owner);
if balance == 0 {
return Err(InsufficientBalance);
}
let balance_key = make_balance_key(&owner);
storage_write(&balance_key, &(balance - 1).to_le_bytes())?;
// Remove owner (delete token)
let owner_key = make_owner_key(token_id);
storage_delete(&owner_key);
// Update total supply
let supply = read_u128(KEY_TOTAL_SUPPLY);
storage_write(KEY_TOTAL_SUPPLY, &supply.saturating_sub(1).to_le_bytes())?;
log("Token burned");
Ok(())
}Helper Functions
fn make_owner_key(token_id: TokenId) -> [u8; 20] {
// own: (4) + token_id (16) = 20
let mut key = [0u8; 20];
key[0..4].copy_from_slice(KEY_OWNER_PREFIX);
key[4..20].copy_from_slice(&token_id.to_le_bytes());
key
}
fn make_balance_key(address: &Address32) -> [u8; 36] {
// bal: (4) + address (32) = 36
let mut key = [0u8; 36];
key[0..4].copy_from_slice(KEY_BALANCE_PREFIX);
key[4..36].copy_from_slice(address);
key
}
fn make_operator_key(owner: &Address32, operator: &Address32) -> [u8; 68] {
// opr: (4) + owner (32) + operator (32) = 68
let mut key = [0u8; 68];
key[0..4].copy_from_slice(KEY_OPERATOR_PREFIX);
key[4..36].copy_from_slice(owner);
key[36..68].copy_from_slice(operator);
key
}Error Codes
| Code | Name | Description |
|---|---|---|
| 1201 | ZeroAddress | Cannot transfer to/mint to zero address |
| 1202 | TokenNotExists | Token ID does not exist |
| 1203 | TokenAlreadyMinted | Token ID already minted |
| 1204 | TransferFromIncorrectOwner | From address is not the owner |
| 1205 | SameAddress | Cannot approve/set operator to same address |
| 1206 | StorageError | Storage operation failed |
| 1207 | InvalidInput | Invalid instruction input |
| 1208 | InsufficientBalance | Balance is zero |
Usage Example
// 1. Mint a new NFT
let mint_data = [0u8; 49];
mint_data[0] = 6; // Opcode: Mint
mint_data[1..33].copy_from_slice(&recipient_address);
mint_data[33..49].copy_from_slice(&1u128.to_le_bytes()); // token_id = 1
// 2. Approve another address
let approve_data = [0u8; 49];
approve_data[0] = 3; // Opcode: Approve
approve_data[1..33].copy_from_slice(&spender_address);
approve_data[33..49].copy_from_slice(&1u128.to_le_bytes());
// 3. Transfer NFT
let transfer_data = [0u8; 81];
transfer_data[0] = 5; // Opcode: TransferFrom
transfer_data[1..33].copy_from_slice(&from_address);
transfer_data[33..65].copy_from_slice(&to_address);
transfer_data[65..81].copy_from_slice(&1u128.to_le_bytes());
// 4. Query owner
let query_data = [0u8; 17];
query_data[0] = 2; // Opcode: OwnerOf
query_data[1..17].copy_from_slice(&1u128.to_le_bytes());Use Cases
- Digital Art: Unique artwork with provable ownership
- Gaming Assets: In-game items, characters, skins
- Collectibles: Trading cards, virtual collectibles
- Domain Names: Blockchain-based domain ownership
- Tickets: Event tickets with transfer capability
- Certificates: Diplomas, licenses, certifications
Security Considerations
- Zero Address Check: Prevent burning by transferring to zero
- Ownership Verification: Always verify owner before transfers
- Approval Clearing: Clear approvals on transfer to prevent stale approvals
- Reentrancy: Use TAKO’s atomic storage operations
Related Examples
- ERC20 Token - Fungible tokens
- AMM DEX - NFT marketplace integration
Last updated on