Skip to Content

⚠️ 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]

OpcodeNameParametersReturns
0x01BalanceOf[owner:32]u128 count
0x02OwnerOf[token_id:16]address:32
0x03Approve[to:32][token_id:16]-
0x04SetApprovalForAll[owner:32][operator:32][approved:1]-
0x05TransferFrom[from:32][to:32][token_id:16]-
0x06Mint[to:32][token_id:16]-
0x07Burn[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} -> bool

Core 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

CodeNameDescription
1201ZeroAddressCannot transfer to/mint to zero address
1202TokenNotExistsToken ID does not exist
1203TokenAlreadyMintedToken ID already minted
1204TransferFromIncorrectOwnerFrom address is not the owner
1205SameAddressCannot approve/set operator to same address
1206StorageErrorStorage operation failed
1207InvalidInputInvalid instruction input
1208InsufficientBalanceBalance 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

  1. Zero Address Check: Prevent burning by transferring to zero
  2. Ownership Verification: Always verify owner before transfers
  3. Approval Clearing: Clear approvals on transfer to prevent stale approvals
  4. Reentrancy: Use TAKO’s atomic storage operations
Last updated on