Solidity A Comprehensive Guide

July 29, 2025
Solidity

Solidity Language Features: A Comprehensive Guide

Introduction

Solidity is a contract-oriented programming language designed for writing smart contracts on the Ethereum blockchain. It is statically typed, supports inheritance, and provides a rich set of features for secure and efficient blockchain development.

This guide covers the core features of Solidity, helping you understand how to build robust smart contracts.

1. Pragma Directive

The pragma keyword specifies the Solidity compiler version to use.

pragma solidity ^0.8.0;

This ensures that your contract compiles with Solidity 0.8.0 or any newer minor version. Using the caret symbol (^) allows compatibility with future patch versions but not with breaking changes.

2. Data Types

Solidity supports several data types:


โœ… Boolean

  • Type: bool
  • Size: 1 byte
  • Range: true or false
  • Usage: Represents simple logical values.

๐Ÿ”ข Unsigned Integers

Unsigned integers (uint) can only store non-negative numbers. You can choose their size in 8-bit increments.

  • uint8 โ€“ 1 byte โ†’ 0 to 2โธโ€“1 (0 to 255)
  • uint16 โ€“ 2 bytes โ†’ 0 to 65,535
  • uint32 โ€“ 4 bytes โ†’ 0 to 4,294,967,295
  • uint64 โ€“ 8 bytes โ†’ up to ~1.84 ร— 10ยนโน
  • uint128 โ€“ 16 bytes โ†’ up to 3.4 ร— 10ยณโธ
  • uint256 / uint โ€“ 32 bytes โ†’ up to 1.15 ร— 10โทโท

โœ… Use these for balances, counters, and timestamps.


โž– Signed Integers

Signed integers (int) allow both positive and negative values.

  • int8 โ€“ 1 byte โ†’ โ€“128 to 127
  • int16 โ€“ 2 bytes โ†’ โ€“32,768 to 32,767
  • int32 โ€“ 4 bytes โ†’ โ€“2,147,483,648 to 2,147,483,647
  • int64 โ€“ 8 bytes โ†’ โ€“2โถยณ to 2โถยณโ€“1
  • int128 โ€“ 16 bytes โ†’ โ€“2ยนยฒโท to 2ยนยฒโทโ€“1
  • int256 / int โ€“ 32 bytes โ†’ โ€“2ยฒโตโต to 2ยฒโตโตโ€“1

โœ… Use these when working with signed values like temperature, profit/loss, etc.


๐Ÿ“ฎ Address Types

address

  • Size: 20 bytes (160 bits)
  • Purpose: Stores Ethereum addresses (EOA or contract)
  • Ether Transfers: โŒ Cannot receive Ether directly

address payable

  • Size: 20 bytes (160 bits)
  • Purpose: Same as address, but can receive Ether
  • Ether Transfers: โœ… Use .transfer() or .send()

๐Ÿงต Byte Arrays

Fixed-Size (bytes1 to bytes32)

  • Size: 1 to 32 bytes
  • Purpose: Binary data of known size
  • Example: bytes32 is commonly used for keccak256 hashes

Dynamic-Size (bytes)

  • Size: Dynamic
  • Purpose: Flexible byte data
  • Example: File contents, encoded data

โœ๏ธ String

  • Type: string
  • Size: Dynamic
  • Encoding: UTF-8
  • Use: Names, messages, text data
  • โš ๏ธ Note: Expensive for manipulation and comparison

๐Ÿ“ฆ Arrays

  • Fixed-size array: uint[3]
  • Dynamic-size array: uint[]

โœ… Use arrays to store ordered collections of elements.


๐Ÿงญ Mapping

  • Type: mapping(KeyType => ValueType)
  • Size: Dynamic
  • Use: Efficient key-value lookup
  • Note: Not iterable, no length or enumeration

Example:

mapping(address => uint) public balances;

### Integer Types

- **uint** (Unsigned integer): Positive whole numbers only
- **int** (Signed integer): Can be positive or negative
- **uint8 to uint256** / **int8 to int256** (Specifies bit size in 8-bit increments)
- Default sizes are uint256 and int256 if not specified

Example:
```solidity
uint256 public myNumber = 100;
int8 public smallSigned = -10; // Range from -128 to 127

Boolean

Simple true/false values:

bool public isActive = true;
bool public isCompleted = false;

Address

Used to store Ethereum addresses (20 bytes):

address public owner;
address payable public beneficiary; // Can receive Ether

String & Bytes

string public name = "Solidity";
bytes32 public hash = keccak256("Hello");
bytes public dynamicBytes = "Dynamic content";

Fixed-size Arrays

Arrays with predetermined length:

uint[5] public staticArray = [1, 2, 3, 4, 5];

Dynamic Arrays

Arrays that can change in size:

uint[] public dynamicArray;

3. Variables

State Variables

Stored permanently on the blockchain.

uint256 public count;
string public projectName;

Local Variables

Exist only during function execution.

function getValue() public pure returns (uint) {
    uint tempValue = 10;
    uint result = tempValue * 2;
    return result;
}

Global Variables

Provide blockchain-related information.

uint public currentBlock = block.number;
address public sender = msg.sender;
uint public timestamp = block.timestamp;
uint public chainId = block.chainid;

Constants

Values that cannot be changed after contract deployment:

uint256 public constant MAX_SUPPLY = 1000000;
address public constant DEAD_ADDRESS = 0x000000000000000000000000000000000000dEaD;

4. Functions

Functions allow interaction with the contract.

Basic Function

function setNumber(uint _number) public {
    count = _number;
}

Return Values

function getSum(uint a, uint b) public pure returns (uint) {
    return a + b;
}

// Multiple return values
function getMinMax(uint a, uint b) public pure returns (uint min, uint max) {
    if (a > b) {
        return (b, a);
    } else {
        return (a, b);
    }
}

Function Visibility

  • public โ€“ Accessible externally and internally
  • private โ€“ Accessible only within the contract
  • internal โ€“ Accessible within contract and derived contracts
  • external โ€“ Accessible only externally (more gas efficient for large data)

Function Modifiers

  • pure โ€“ Does not read or modify state
  • view โ€“ Reads but doesn't modify state
  • payable โ€“ Can receive Ether
// External payable function
function deposit() external payable {
    // Function accepts ETH
}

// View function
function getBalance() public view returns (uint) {
    return address(this).balance;
}

Function Overloading

Multiple functions with the same name but different parameters:

function getValue(uint id) public view returns (uint) {
    return values[id];
}

function getValue(string memory name) public view returns (uint) {
    return nameValues[name];
}

5. Modifiers

Used to change function behavior.

modifier onlyOwner() {
    require(msg.sender == owner, "Not the owner");
    _;  // Continues execution at the function
}

// Using the modifier
function withdrawFunds() public onlyOwner {
    payable(owner).transfer(address(this).balance);
}

Modifiers with Parameters

modifier minimum(uint amount) {
    require(msg.value >= amount, "Not enough Ether provided");
    _;
}

function purchase() public payable minimum(0.01 ether) {
    // Execute purchase logic
}

6. Events

Used to log information on the blockchain that can be efficiently accessed by off-chain applications.

event Transfer(address indexed from, address indexed to, uint256 value);

function transfer(address to, uint256 amount) public {
    balances[msg.sender] -= amount;
    balances[to] += amount;
    emit Transfer(msg.sender, to, amount); // Logging the transfer
}

Indexed Parameters

Up to three parameters can be marked as indexed, making them more efficiently searchable:

event UserAction(address indexed user, string action, uint256 indexed timestamp);

7. Structs and Enums

Structs

Custom data structures:

struct User {
    string name;
    uint age;
    address wallet;
    bool isActive;
}

User public user = User("Alice", 25, msg.sender, true);

// Struct arrays and mappings
User[] public users;
mapping(address => User) public usersByAddress;

function addUser(string memory _name, uint _age) public {
    User memory newUser = User(_name, _age, msg.sender, true);
    users.push(newUser);
    usersByAddress[msg.sender] = newUser;
}

Enums

Custom type with a finite set of values:

enum Status { Pending, Approved, Rejected, Canceled }
Status public orderStatus = Status.Pending;

function approve() public onlyOwner {
    orderStatus = Status.Approved;
}

function reject() public onlyOwner {
    orderStatus = Status.Rejected;
}

8. Mappings

Key-value storage for efficient data retrieval.

mapping(address => uint) public balances;
mapping(address => mapping(address => uint)) public allowances; // Nested mapping

Example usage:

function transfer(address to, uint amount) public {
    require(balances[msg.sender] >= amount, "Insufficient balance");
    balances[msg.sender] -= amount;
    balances[to] += amount;
}

function approve(address spender, uint amount) public {
    allowances[msg.sender][spender] = amount;
}

9. Inheritance

Allows contracts to inherit properties from other contracts.

contract Ownable {
    address public owner;
    
    constructor() {
        owner = msg.sender;
    }
    
    modifier onlyOwner() {
        require(msg.sender == owner, "Not the owner");
        _;
    }
    
    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "Invalid address");
        owner = newOwner;
    }
}

contract MyToken is Ownable {
    string public name;
    mapping(address => uint) public balances;
    
    constructor(string memory _name) {
        name = _name;
    }
    
    // Uses the onlyOwner modifier from parent contract
    function mint(address to, uint amount) public onlyOwner {
        balances[to] += amount;
    }
}

Multiple Inheritance

contract A {
    function foo() public pure virtual returns (string memory) {
        return "A";
    }
}

contract B {
    function foo() public pure virtual returns (string memory) {
        return "B";
    }
}

// C inherits from both A and B
contract C is A, B {
    // Must override function that exists in multiple parent contracts
    function foo() public pure override(A, B) returns (string memory) {
        return "C";
    }
}

Abstract Contracts

Contracts that contain at least one function without implementation:

abstract contract BaseContract {
    function calculateValue() public virtual returns (uint);
}

contract ImplementedContract is BaseContract {
    function calculateValue() public pure override returns (uint) {
        return 100;
    }
}

Interfaces

Similar to abstract contracts but with more restrictions:

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
}

10. Error Handling

Using require, assert, and revert for different error scenarios:

require()

Used for input validation and checking conditions:

function withdraw(uint amount) public {
    require(amount > 0, "Amount must be greater than 0");
    require(balances[msg.sender] >= amount, "Insufficient balance");
    
    balances[msg.sender] -= amount;
    payable(msg.sender).transfer(amount);
}

assert()

Used for internal error checking and invariant validation:

function safeOperation() public {
    uint startBalance = address(this).balance;
    
    // Perform operations
    
    // Ensures no funds were lost
    assert(address(this).balance >= startBalance);
}

revert()

Used to revert a transaction with an error message:

function complexCheck(uint x) public {
    if (x < 10) {
        revert("Value too small");
    } else if (x > 100) {
        revert("Value too large");
    }
    // Continue with function
}

Custom Errors (Solidity 0.8.4+)

More gas-efficient than string error messages:

error InsufficientBalance(address user, uint256 available, uint256 required);

function transfer(address to, uint256 amount) public {
    if (balances[msg.sender] < amount) {
        revert InsufficientBalance(msg.sender, balances[msg.sender], amount);
    }
    
    balances[msg.sender] -= amount;
    balances[to] += amount;
}

11. Contract Creation and Interaction

Contract Constructor

Executes once when the contract is deployed:

contract Token {
    string public name;
    address public owner;
    
    constructor(string memory _name) {
        name = _name;
        owner = msg.sender;
    }
}

Creating Contracts from Contracts

contract Factory {
    Token[] public tokens;
    
    function createToken(string memory name) public {
        Token newToken = new Token(name);
        tokens.push(newToken);
    }
}

Contract Interaction

interface ICounter {
    function increment() external;
    function getCount() external view returns (uint);
}

contract Caller {
    function incrementCounter(address counterAddress) public {
        ICounter counter = ICounter(counterAddress);
        counter.increment();
    }
    
    function getCountFromCounter(address counterAddress) public view returns (uint) {
        return ICounter(counterAddress).getCount();
    }
}

12. Gas Optimization Techniques

Storage vs Memory

function processUsers(User[] storage users) internal {
    // References storage directly - gas intensive but persistent
}

function processUsersInMemory(User[] memory users) public pure {
    // Works with memory copy - more gas efficient for read-only operations
}

Packing Variables

// Inefficient - uses multiple storage slots
uint8 public a;
uint256 public b;
uint8 public c;

// Efficient - packs variables into a single storage slot
uint8 public x;
uint8 public y;
uint8 public z;

Using unchecked for Gas Savings (Solidity 0.8.0+)

function increment() public {
    // With overflow checks (default in 0.8.0+)
    count += 1;
    
    // Without overflow checks (more gas efficient)
    unchecked {
        count += 1;
    }
}

Solidity Language Features: A Comprehensive Guide

[...previous content continues above...]

13. Libraries

Reusable code that can be deployed once and used by multiple contracts:

library SafeMath {
    function add(uint a, uint b) internal pure returns (uint) {
        uint c = a + b;
        require(c >= a, "Addition overflow");
        return c;
    }
    
    function sub(uint a, uint b) internal pure returns (uint) {
        require(b <= a, "Subtraction overflow");
        return a - b;
    }
    
    function mul(uint a, uint b) internal pure returns (uint) {
        if (a == 0) return 0;
        uint c = a * b;
        require(c / a == b, "Multiplication overflow");
        return c;
    }
}

contract UsingLibrary {
    using SafeMath for uint;
    
    function doAdd(uint x, uint y) public pure returns (uint) {
        return x.add(y); // Uses the library function
    }
    
    function doSub(uint x, uint y) public pure returns (uint) {
        return x.sub(y);
    }
}

Types of Library Functions

library ArrayUtils {
    // Internal functions can access storage
    function removeByValue(uint[] storage array, uint value) internal returns (bool) {
        for (uint i = 0; i < array.length; i++) {
            if (array[i] == value) {
                array[i] = array[array.length - 1];
                array.pop();
                return true;
            }
        }
        return false;
    }
    
    // Pure functions don't access storage
    function sum(uint[] memory array) internal pure returns (uint) {
        uint total = 0;
        for (uint i = 0; i < array.length; i++) {
            total += array[i];
        }
        return total;
    }
}

Library Deployment

Libraries can be deployed independently and linked to contracts:

// During compilation/deployment, the address of the deployed library
// will be filled in place of the placeholder
using AdvancedMath for uint;

function complexCalculation(uint x) public pure returns (uint) {
    // Calls the library function
    return x.calculateComplex();
}

14. Security Best Practices

Reentrancy Protection

contract Secure {
    mapping(address => uint) public balances;
    bool private locked;
    
    modifier nonReentrant() {
        require(!locked, "Reentrant call");
        locked = true;
        _;
        locked = false;
    }
    
    function withdraw() public nonReentrant {
        uint amount = balances[msg.sender];
        require(amount > 0, "No balance");
        
        balances[msg.sender] = 0; // Update state before external call
        
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

Check-Effects-Interactions Pattern

function transfer(address payable recipient, uint amount) public {
    // Checks
    require(balances[msg.sender] >= amount, "Insufficient balance");
    
    // Effects
    balances[msg.sender] -= amount;
    balances[recipient] += amount;
    
    // Interactions (external calls)
    emit Transfer(msg.sender, recipient, amount);
}

Integer Overflow/Underflow Protection

Automatic in Solidity 0.8.0+, but can be implemented manually in earlier versions:

// For Solidity < 0.8.0
function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    require(c >= a, "SafeMath: addition overflow");
    return c;
}

Access Control Patterns

contract AccessControl {
    address public admin;
    mapping(address => bool) public operators;
    
    constructor() {
        admin = msg.sender;
        operators[msg.sender] = true;
    }
    
    modifier onlyAdmin() {
        require(msg.sender == admin, "Not admin");
        _;
    }
    
    modifier onlyOperator() {
        require(operators[msg.sender], "Not operator");
        _;
    }
    
    function addOperator(address operator) public onlyAdmin {
        operators[operator] = true;
    }
    
    function removeOperator(address operator) public onlyAdmin {
        operators[operator] = false;
    }
}

Avoiding TX.ORIGIN

// Vulnerable to phishing
function badWithdraw() public {
    require(tx.origin == owner, "Not owner"); // VULNERABLE
    // ...
}

// Secure approach
function goodWithdraw() public {
    require(msg.sender == owner, "Not owner"); // SECURE
    // ...
}

15. Advanced Features

Fallback and Receive Functions

// Called when no function matches the call data
fallback() external payable {
    emit FallbackCalled(msg.sender, msg.value);
}

// Called when Ether is sent to the contract with empty calldata
receive() external payable {
    emit EtherReceived(msg.sender, msg.value);
}

Assembly

Low-level inline assembly:

function getUpper(uint x) public pure returns (uint) {
    uint result;
    
    assembly {
        // Get upper 128 bits
        result := shr(128, x)
    }
    
    return result;
}

function callWithCustomGas() public returns (bool success) {
    address target = 0x123...;
    bytes memory data = abi.encodeWithSignature("someFunction()");
    
    assembly {
        success := call(
            21000,     // Gas limit
            target,    // Target address
            0,         // No ETH sent
            add(data, 0x20), // Input data offset
            mload(data),     // Input data size
            0,         // Output offset (not used)
            0          // Output size (not used)
        )
    }
}

Using Delegatecall

function execute(address _target, bytes memory _data) public returns (bytes memory) {
    (bool success, bytes memory result) = _target.delegatecall(_data);
    require(success, "Delegatecall failed");
    return result;
}

Time-Based Logic

contract TimeLock {
    uint public lockTime = 1 weeks;
    mapping(address => uint) public lockUntil;
    
    function lock() public payable {
        require(msg.value > 0, "Must lock some ETH");
        lockUntil[msg.sender] = block.timestamp + lockTime;
    }
    
    function withdraw() public {
        require(block.timestamp >= lockUntil[msg.sender], "Still locked");
        require(lockUntil[msg.sender] > 0, "No funds locked");
        
        uint amount = address(this).balance;
        payable(msg.sender).transfer(amount);
        lockUntil[msg.sender] = 0;
    }
}

Create2 for Predictable Addresses

function deployWithCreate2(bytes32 salt, bytes memory bytecode) public returns (address) {
    address addr;
    
    assembly {
        addr := create2(
            0,          // No ETH sent
            add(bytecode, 0x20), // Bytecode offset
            mload(bytecode),     // Bytecode length
            salt        // Salt for address generation
        )
    }
    
    require(addr != address(0), "Deployment failed");
    return addr;
}

16. Contract Patterns

Proxy Pattern

contract Proxy {
    address implementation;
    address owner;
    
    constructor(address _implementation) {
        implementation = _implementation;
        owner = msg.sender;
    }
    
    function upgrade(address _newImplementation) public {
        require(msg.sender == owner, "Not authorized");
        implementation = _newImplementation;
    }
    
    fallback() external payable {
        address _impl = implementation;
        
        assembly {
            let ptr := mload(0x40)
            calldatacopy(ptr, 0, calldatasize())
            let result := delegatecall(gas(), _impl, ptr, calldatasize(), 0, 0)
            let size := returndatasize()
            returndatacopy(ptr, 0, size)
            
            switch result
            case 0 { revert(ptr, size) }
            default { return(ptr, size) }
        }
    }
    
    receive() external payable {}
}

Factory Pattern

contract Token {
    string public name;
    address public owner;
    
    constructor(string memory _name, address _owner) {
        name = _name;
        owner = _owner;
    }
}

contract TokenFactory {
    mapping(address => address[]) public createdTokens;
    
    event TokenCreated(address indexed creator, address tokenAddress, string name);
    
    function createToken(string memory name) public returns (address) {
        Token newToken = new Token(name, msg.sender);
        address tokenAddress = address(newToken);
        
        createdTokens[msg.sender].push(tokenAddress);
        emit TokenCreated(msg.sender, tokenAddress, name);
        
        return tokenAddress;
    }
    
    function getTokenCount(address creator) public view returns (uint) {
        return createdTokens[creator].length;
    }
}

Withdrawal Pattern

contract Auction {
    address public highestBidder;
    uint public highestBid;
    mapping(address => uint) public pendingReturns;
    
    function bid() public payable {
        require(msg.value > highestBid, "Bid too low");
        
        if (highestBidder != address(0)) {
            // Add to pending returns instead of immediately sending
            pendingReturns[highestBidder] += highestBid;
        }
        
        highestBidder = msg.sender;
        highestBid = msg.value;
    }
    
    // Users call this to withdraw their funds
    function withdraw() public {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {
            pendingReturns[msg.sender] = 0;
            
            (bool success, ) = msg.sender.call{value: amount}("");
            require(success, "Withdrawal failed");
        }
    }
}

17. Ethereum Token Standards

ERC-20 Token

contract ERC20Token {
    string public name;
    string public symbol;
    uint8 public decimals = 18;
    uint256 public totalSupply;
    
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
    
    constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
        name = _name;
        symbol = _symbol;
        totalSupply = _initialSupply * 10**uint256(decimals);
        balanceOf[msg.sender] = totalSupply;
    }
    
    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "Zero address");
        require(balanceOf[msg.sender] >= amount, "Insufficient balance");
        
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
    
    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) {
        require(from != address(0), "Zero address");
        require(to != address(0), "Zero address");
        require(balanceOf[from] >= amount, "Insufficient balance");
        require(allowance[from][msg.sender] >= amount, "Insufficient allowance");
        
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        allowance[from][msg.sender] -= amount;
        
        emit Transfer(from, to, amount);
        return true;
    }
}

ERC-721 NFT

contract ERC721Token {
    string public name;
    string public symbol;
    
    mapping(uint256 => address) private _owners;
    mapping(address => uint256) private _balances;
    mapping(uint256 => address) private _tokenApprovals;
    mapping(address => mapping(address => bool)) private _operatorApprovals;
    
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    
    constructor(string memory _name, string memory _symbol) {
        name = _name;
        symbol = _symbol;
    }
    
    function balanceOf(address owner) public view returns (uint256) {
        require(owner != address(0), "Zero address");
        return _balances[owner];
    }
    
    function ownerOf(uint256 tokenId) public view returns (address) {
        address owner = _owners[tokenId];
        require(owner != address(0), "Invalid token ID");
        return owner;
    }
    
    function approve(address to, uint256 tokenId) public {
        address owner = ownerOf(tokenId);
        require(msg.sender == owner || isApprovedForAll(owner, msg.sender), "Not authorized");
        
        _tokenApprovals[tokenId] = to;
        emit Approval(owner, to, tokenId);
    }
    
    function getApproved(uint256 tokenId) public view returns (address) {
        require(_owners[tokenId] != address(0), "Invalid token ID");
        return _tokenApprovals[tokenId];
    }
    
    function setApprovalForAll(address operator, bool approved) public {
        require(operator != msg.sender, "Self approval");
        _operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }
    
    function isApprovedForAll(address owner, address operator) public view returns (bool) {
        return _operatorApprovals[owner][operator];
    }
    
    function transferFrom(address from, address to, uint256 tokenId) public {
        address owner = ownerOf(tokenId);
        require(
            msg.sender == owner ||
            getApproved(tokenId) == msg.sender ||
            isApprovedForAll(owner, msg.sender),
            "Not authorized"
        );
        require(owner == from, "Not owner");
        require(to != address(0), "Zero address");
        
        _tokenApprovals[tokenId] = address(0);
        _balances[from] -= 1;
        _balances[to] += 1;
        _owners[tokenId] = to;
        
        emit Transfer(from, to, tokenId);
    }
    
    function _mint(address to, uint256 tokenId) internal {
        require(to != address(0), "Zero address");
        require(_owners[tokenId] ==