Skip to main content
NEP-20 is the fungible token standard for NEXUS, equivalent to ERC-20 on Ethereum. Every token deployed on NEXUS follows this interface, enabling wallets, DEXes, and other contracts to interact with any token in a uniform way.

Interface

Required Functions

FunctionArgumentsReturnsDescription
init_tokenname, symbol, decimals, supply-Initialize (one-time, owner becomes deployer)
namebytesToken name
symbolbytesToken symbol
decimalsu32Decimal places (default: 18)
totalSupplyU256Total tokens in existence
balanceOfaddressU256Balance of a specific address
transferto, amountu32 (1/0)Send tokens to an address
approvespender, amountu32 (1)Allow spender to use your tokens
allowanceowner, spenderU256How much spender can use
transferFromfrom, to, amountu32 (1/0)Transfer using an allowance
increaseAllowancespender, amountu32 (1)Safely increase allowance
decreaseAllowancespender, amountu32 (1)Safely decrease allowance
mintto, amountu32 (1)Mint new tokens (owner only)
burnfrom, amountu32 (1)Burn tokens (owner only)
ownerbytesGet current owner address
transferOwnershipnew_owneru32 (1)Transfer ownership
renounceOwnershipu32 (1)Renounce ownership (irreversible)

Events

EventWhen
TransferOn any token transfer, including mints and burns
ApprovalWhen an allowance is set
TokenInitializedWhen init_token is called
OwnershipTransferredWhen ownership changes
OwnershipRenouncedWhen ownership is renounced

Storage Layout

pub static BALANCES:     Mapping<Address, U256>                = Mapping::new(b"bal");
pub static ALLOWANCES:   DoubleMapping<Address, Address, U256> = DoubleMapping::new(b"allow");
pub static TOTAL_SUPPLY: Mapping<&[u8], U256>                  = Mapping::new(b"supply");
pub static OWNER:        Mapping<&[u8], Address>               = Mapping::new(b"owner");
pub static INITIALIZED:  Mapping<&[u8], bool>                  = Mapping::new(b"init");

Production Source

The full production implementation (apps/contracts/nep20/src/lib.rs):
//! NEP-20 Token Standard - Production Implementation

#![no_std]
extern crate alloc;
use alloc::string::String;
use nexus_sdk::{
    solidity::{*, SafeMath},
    contract_api::{ez::prelude::*, ez::ret},
    require,
};
use nexus_sdk::solidity::uint256 as U256;

// State Variables
pub static BALANCES: Mapping<Address, U256> = Mapping::new(b"bal");
pub static ALLOWANCES: DoubleMapping<Address, Address, U256> = DoubleMapping::new(b"allow");
pub static TOTAL_SUPPLY: Mapping<&[u8], U256> = Mapping::new(b"supply");
pub static OWNER: Mapping<&[u8], Address> = Mapping::new(b"owner");
pub static INITIALIZED: Mapping<&[u8], bool> = Mapping::new(b"init");

// Constants
pub const NAME_KEY: &[u8] = b"name";
pub const SYMBOL_KEY: &[u8] = b"symbol";
pub const DECIMALS_KEY: &[u8] = b"decimals";

/// Internal module containing reusable logic
pub mod internal {
    use super::*;

    /// Manual U256 serialization to avoid WASM intrinsics
    pub fn u256_to_bytes(value: U256) -> [u8; 32] {
        value.to_little_endian()
    }

    /// Returns true if a < b
    pub fn u256_lt(a: &U256, b: &U256) -> bool {
        if a.0[3] < b.0[3] { return true; }
        if a.0[3] > b.0[3] { return false; }
        if a.0[2] < b.0[2] { return true; }
        if a.0[2] > b.0[2] { return false; }
        if a.0[1] < b.0[1] { return true; }
        if a.0[1] > b.0[1] { return false; }
        if a.0[0] < b.0[0] { return true; }
        false
    }

    /// Returns true if a > b
    pub fn u256_gt(a: &U256, b: &U256) -> bool {
        if a.0[3] > b.0[3] { return true; }
        if a.0[3] < b.0[3] { return false; }
        if a.0[2] > b.0[2] { return true; }
        if a.0[2] < b.0[2] { return false; }
        if a.0[1] > b.0[1] { return true; }
        if a.0[1] < b.0[1] { return false; }
        if a.0[0] > b.0[0] { return true; }
        false
    }

    /// Returns true if a == b
    pub fn u256_eq(a: &U256, b: &U256) -> bool {
        for i in 0..4 {
            if a.0[i] != b.0[i] { return false; }
        }
        true
    }

    /// Returns true if a != b
    pub fn u256_ne(a: &U256, b: &U256) -> bool {
        !u256_eq(a, b)
    }

    /// Manual U256 addition — no WASM compiler intrinsics
    pub fn u256_add(a: &U256, b: &U256) -> U256 {
        let mut result = U256::zero();
        let mut carry: u64 = 0;

        let sum0 = a.0[0].wrapping_add(b.0[0]);
        let c0 = if sum0 < a.0[0] { 1u64 } else { 0u64 };
        let sum0_c = sum0.wrapping_add(carry);
        let c0_2 = if sum0_c < sum0 { 1u64 } else { 0u64 };
        result.0[0] = sum0_c;
        carry = c0 + c0_2;

        let sum1 = a.0[1].wrapping_add(b.0[1]);
        let c1 = if sum1 < a.0[1] { 1u64 } else { 0u64 };
        let sum1_c = sum1.wrapping_add(carry);
        let c1_2 = if sum1_c < sum1 { 1u64 } else { 0u64 };
        result.0[1] = sum1_c;
        carry = c1 + c1_2;

        let sum2 = a.0[2].wrapping_add(b.0[2]);
        let c2 = if sum2 < a.0[2] { 1u64 } else { 0u64 };
        let sum2_c = sum2.wrapping_add(carry);
        let c2_2 = if sum2_c < sum2 { 1u64 } else { 0u64 };
        result.0[2] = sum2_c;
        carry = c2 + c2_2;

        let sum3 = a.0[3].wrapping_add(b.0[3]);
        let c3 = if sum3 < a.0[3] { 1u64 } else { 0u64 };
        let sum3_c = sum3.wrapping_add(carry);
        let c3_2 = if sum3_c < sum3 { 1u64 } else { 0u64 };
        result.0[3] = sum3_c;
        carry = c3 + c3_2;
        let _ = carry;

        result
    }

    /// Checked U256 addition — returns None on overflow
    #[inline]
    pub fn u256_checked_add(a: &U256, b: &U256) -> Option<U256> {
        let result = u256_add(a, b);
        if u256_lt(&result, a) || u256_lt(&result, b) {
            None
        } else {
            Some(result)
        }
    }

    /// Manual U256 subtraction — no WASM compiler intrinsics
    pub fn u256_sub(a: &U256, b: &U256) -> U256 {
        let mut result = U256::zero();
        let mut borrow = 0u64;

        for i in 0..4 {
            let diff = a.0[i].wrapping_sub(b.0[i]).wrapping_sub(borrow);
            result.0[i] = diff;
            borrow = if (a.0[i] < b.0[i]) || (diff > a.0[i]) { 1 } else { 0 };
        }

        result
    }

    pub fn init(name: String, symbol: String, decimals: u8, supply: U256) {
        nexus_sdk::contract_api::storage::set(NAME_KEY, name.as_bytes());
        nexus_sdk::contract_api::storage::set(SYMBOL_KEY, symbol.as_bytes());
        nexus_sdk::contract_api::storage::set(DECIMALS_KEY, &[decimals]);

        let sender = Blockchain::msg.sender();
        OWNER.set(&b"val".as_slice(), sender.clone());

        if u256_gt(&supply, &U256::zero()) {
            mint(sender, supply);
        }
    }

    pub fn mint(to: Address, amount: U256) {
        let current_supply = TOTAL_SUPPLY.get(&b"total".as_slice());
        let new_supply = u256_checked_add(&current_supply, &amount)
            .expect("Mint: total supply overflow");
        TOTAL_SUPPLY.set(&b"total".as_slice(), new_supply);

        let current_bal = BALANCES.get(&to);
        let new_bal = u256_checked_add(&current_bal, &amount)
            .expect("Mint: balance overflow");
        BALANCES.set(&to, new_bal);

        emit("Transfer", &[]);
    }

    nexus_fn! {
        fn burn(from: Address, amount: U256) {
            let sender = Blockchain::msg.sender();
            let owner = OWNER.get(&b"val".as_slice());
            require!(sender == owner, "Not owner");

            let bal = BALANCES.get(&from);
            require!(!internal::u256_lt(&bal, &amount), "Burn exceeds balance");
            BALANCES.set(&from, internal::u256_sub(&bal, &amount));

            let supply = TOTAL_SUPPLY.get(&b"total".as_slice());
            require!(!internal::u256_lt(&supply, &amount), "Burn exceeds supply");
            TOTAL_SUPPLY.set(&b"total".as_slice(), internal::u256_sub(&supply, &amount));

            emit("Transfer", &[]);
            ret::u32(1)
        }
    }

    pub fn transfer(sender: Address, to: Address, amount: U256) -> bool {
        if to.is_zero() { return false; }
        if amount == U256::zero() { return false; }

        let sender_bal = BALANCES.get(&sender);
        if u256_lt(&sender_bal, &amount) { return false; }

        BALANCES.set(&sender, u256_sub(&sender_bal, &amount));

        let to_bal = BALANCES.get(&to);
        BALANCES.set(&to, u256_add(&to_bal, &amount));

        emit("Transfer", &[]);
        true
    }

    pub fn approve(owner: Address, spender: Address, amount: U256) {
        ALLOWANCES.set(&owner, &spender, amount);
        emit("Approval", &[]);
    }

    pub fn transfer_from(spender: Address, from: Address, to: Address, amount: U256) -> bool {
        if amount == U256::zero() { return false; }

        let allowed = ALLOWANCES.get(&from, &spender);
        if u256_lt(&allowed, &amount) { return false; }

        // Reduce allowance unless it's a max (unlimited) approval
        if u256_ne(&allowed, &U256::max_value()) {
            ALLOWANCES.set(&from, &spender, u256_sub(&allowed, &amount));
        }

        internal::transfer(from, to, amount)
    }

    pub fn balance_of(addr: Address) -> U256 {
        BALANCES.get(&addr)
    }
}

// ABI Entry Points

nexus_fn! {
    fn init_token(name: String, symbol: String, decimals: u64, supply: U256) {
        let is_initialized = INITIALIZED.get(&b"val".as_slice());
        require!(!is_initialized, "AI"); // Already Initialized

        INITIALIZED.set(&b"val".as_slice(), true);
        internal::init(name, symbol, decimals as u8, supply);
        emit("TokenInitialized", &[]);

        ret::u32(1)
    }
}

nexus_fn! {
    fn mint(to: Address, amount: U256) {
        let sender = Blockchain::msg.sender();
        let owner = OWNER.get(&b"val".as_slice());
        require!(sender == owner, "Not owner");

        internal::mint(to, amount);
        ret::u32(1)
    }
}

nexus_fn! {
    fn transferOwnership(new_owner: Address) {
        let sender = Blockchain::msg.sender();
        let owner = OWNER.get(&b"val".as_slice());
        require!(sender == owner, "Not owner");
        require!(!new_owner.is_zero(), "Zero address");

        OWNER.set(&b"val".as_slice(), new_owner.clone());
        emit("OwnershipTransferred", &new_owner.0);
        ret::u32(1)
    }
}

nexus_fn! {
    fn renounceOwnership() {
        let sender = Blockchain::msg.sender();
        let owner = OWNER.get(&b"val".as_slice());
        require!(sender == owner, "Not owner");

        OWNER.set(&b"val".as_slice(), Address::zero());
        emit("OwnershipRenounced", &[]);
        ret::u32(1)
    }
}

nexus_fn! {
    fn owner() {
        let owner = OWNER.get(&b"val".as_slice());
        ret::bytes(&owner.0)
    }
}

nexus_fn! {
    fn balanceOf(addr: Address) {
        let bal = internal::balance_of(addr);
        let out = internal::u256_to_bytes(bal);
        ret::u256(&out)
    }
}

nexus_fn! {
    fn totalSupply() {
        let supply = TOTAL_SUPPLY.get(&b"total".as_slice());
        let out = internal::u256_to_bytes(supply);
        ret::u256(&out)
    }
}

nexus_fn! {
    fn name() {
        let name_bytes = nexus_sdk::contract_api::storage::get(NAME_KEY);
        ret::bytes(&name_bytes)
    }
}

nexus_fn! {
    fn symbol() {
        let symbol_bytes = nexus_sdk::contract_api::storage::get(SYMBOL_KEY);
        ret::bytes(&symbol_bytes)
    }
}

nexus_fn! {
    fn decimals() {
        let dec_bytes = nexus_sdk::contract_api::storage::get(DECIMALS_KEY);
        let decimals = if dec_bytes.is_empty() { 18u8 } else { dec_bytes[0] };
        ret::u32(decimals as u32)
    }
}

nexus_fn! {
    fn transfer(to: Address, amount: U256) {
        let _guard = ReentrancyGuard::new();
        let sender = Blockchain::msg.sender();
        let success = internal::transfer(sender, to, amount);
        if success { ret::u32(1) } else { ret::u32(0) }
    }
}

nexus_fn! {
    fn approve(spender: Address, amount: U256) {
        let sender = Blockchain::msg.sender();
        internal::approve(sender, spender, amount);
        ret::u32(1)
    }
}

nexus_fn! {
    fn increaseAllowance(spender: Address, added_value: U256) {
        let sender = Blockchain::msg.sender();
        let current = ALLOWANCES.get(&sender, &spender);
        let new_allowance = internal::u256_add(&current, &added_value);
        internal::approve(sender, spender, new_allowance);
        ret::u32(1)
    }
}

nexus_fn! {
    fn decreaseAllowance(spender: Address, subtracted_value: U256) {
        let sender = Blockchain::msg.sender();
        let current = ALLOWANCES.get(&sender, &spender);
        if internal::u256_lt(&current, &subtracted_value) {
            panic!("Allowance underflow");
        }
        let new_allowance = internal::u256_sub(&current, &subtracted_value);
        internal::approve(sender, spender, new_allowance);
        ret::u32(1)
    }
}

nexus_fn! {
    fn allowance(owner: Address, spender: Address) {
        let amount = ALLOWANCES.get(&owner, &spender);
        let out = internal::u256_to_bytes(amount);
        ret::u256(&out)
    }
}

nexus_fn! {
    fn transferFrom(from: Address, to: Address, amount: U256) {
        let _guard = ReentrancyGuard::new();
        let spender = Blockchain::msg.sender();
        let success = internal::transfer_from(spender, from, to, amount);
        if success { ret::u32(1) } else { ret::u32(0) }
    }
}
U256 arithmetic is implemented manually (word-by-word carry/borrow) to avoid WASM compiler intrinsics that are not available in no_std environments. This is a hard requirement for NEXUS contracts.
ReentrancyGuard::new() is a built-in NEXUS primitive. It prevents reentrancy attacks on any function that modifies balances or makes external calls.

Deploying a Token

import { NexusClient, Wallet } from '@yattacorp/nexus-sdk';
import { readFileSync } from 'fs';

// Wallet must be mnemonic-based — required for vault creation and renewal key
const wallet = Wallet.fromMnemonic(process.env.MNEMONIC!, 'mainnet', undefined, 'zcash');
const client = new NexusClient(
  { rpcUrl: 'https://api.yattacorp.xyz', network: 'mainnet', chain: 'zcash' },
  wallet
);

// 1. Build (from nexus-contract-template)
//    cargo build --target wasm32-unknown-unknown --release

// 2. Deploy the NEP-20 WASM
const wasm = readFileSync(
  './target/wasm32-unknown-unknown/release/my_nexus_contract.wasm'
);
const { contractId } = await client.deployContract(wasm);

// 3. Initialize (one-time — sets name, symbol, supply, owner = deployer)
await client.callContract(contractId, 'init_token', [
  'My Token',                   // name
  'MTK',                        // symbol
  18,                           // decimals
  '1000000000000000000000000',  // initial supply (1M tokens at 18 decimals)
]);

console.log('Token deployed:', contractId);

Interacting with a Token

// Read balance (no gas)
const balance = await client.queryContract(contractId, 'balanceOf', [userAddress]);

// Transfer tokens
await client.callContract(contractId, 'transfer', [
  recipientAddress,
  '1000000000000000000',  // 1 token (18 decimals)
]);

// Approve a spender
await client.callContract(contractId, 'approve', [
  spenderAddress,
  '500000000000000000',
]);

// Check allowance
const allowance = await client.queryContract(contractId, 'allowance', [
  ownerAddress,
  spenderAddress,
]);

// Transfer on behalf of (requires prior approval)
await client.callContract(contractId, 'transferFrom', [
  ownerAddress,
  recipientAddress,
  '500000000000000000',
]);

// Mint new tokens (owner only)
await client.callContract(contractId, 'mint', [
  recipientAddress,
  '1000000000000000000000',
]);

// Burn tokens (owner only)
await client.callContract(contractId, 'burn', [
  holderAddress,
  '1000000000000000000',
]);

Security Notes

  • init_token can only be called once — it checks the INITIALIZED flag ("AI" error = Already Initialized) and reverts if already set
  • mint and burn are owner-only — after renounceOwnership(), no one can ever mint or burn again
  • transfer and transferFrom both use ReentrancyGuard — reentrancy is blocked at the SDK level
  • increaseAllowance/decreaseAllowance are safer than calling approve directly — they prevent the ERC-20 approval race condition
  • transferFrom does not reduce allowance if the allowance is U256::max_value() (unlimited approval)
  • All U256 arithmetic uses manual word-by-word operations — no floating point, no compiler intrinsics, full WASM compatibility