Skip to content

Standards and Contracts

The devpack provides complete implementations of Neo N3 token standards — NEP-17 (fungible tokens), NEP-11 (non-fungible tokens), and NEP-24 (royalty metadata). These standards are the Neo equivalents of Ethereum's ERC-20, ERC-721, and ERC-2981 respectively. The compiler auto-detects standard compliance and populates the manifest's supportedstandards array.

Supported Standards

StandardTypeEthereum EquivalentDevpack File
NEP-17Fungible TokenERC-20standards/NEP17.sol
NEP-11Non-Fungible TokenERC-721standards/NEP11.sol
NEP-24Royalty MetadataERC-2981standards/NEP24.sol

Ethereum to Neo Standard Mapping

EthereumNeoKey Differences
ERC-20NEP-174-parameter transfer, witness auth, no approve/allowance
ERC-721NEP-11bytes32 token IDs, 3-param transfer, witness auth, required tokensOf/properties
ERC-2981NEP-24Multiple royalty recipients, royaltyToken parameter
EIP-165Manifest supportedstandardsInterface detection is manifest-based, no supportsInterface()
EIP-2612 (Permit)Runtime.checkWitness()No permit needed — witness model replaces approvals
EIP-1967 (Proxy)NEP-26 (ContractManagement.update)Native in-place upgrade, no proxy pattern

NEP-17: Fungible Tokens

NEP-17 is Neo's fungible token standard. It replaces the ERC-20 approve/allowance authorization model with cryptographic witness verification, eliminating entire classes of vulnerabilities (infinite approvals, front-running attacks) at the protocol level.

Spec: NEP-17 Proposal

Required Interface

MethodParametersReturnSafeDescription
symbolStringYesToken symbol
decimalsIntegerYesDecimal places (max 18)
totalSupplyIntegerYesTotal supply
balanceOfHash160 accountIntegerYesAccount balance
transferHash160 from, Hash160 to, Integer amount, Any dataBooleanNoTransfer tokens

Required event:

Transfer(Hash160 from, Hash160 to, Integer amount)

The Any Type

The data parameter uses the Neo-Solidity Any type, which maps to NeoVM's unconstrained StackItem. This allows callers to pass any serializable value to the recipient's onNEP17Payment callback. In the devpack, Any is defined as type Any is bytes;.

Authorization Model

The most significant difference between ERC-20 and NEP-17 is the authorization model:

ERC-20 (Ethereum)NEP-17 (Neo)
Auth mechanismmsg.sender + approve/allowanceRuntime.checkWitness(from)
Delegated transfersapprove()transferFrom()Not needed — witness proves ownership
Approval storageOn-chain mapping(owner => spender => amount)None
Infinite approval riskYes — common vulnerabilityEliminated by design
Front-running attacksPossible on approve()Not applicable

Neo transactions include witness scopes that cryptographically prove the caller controls an address. Runtime.checkWitness(from) verifies this at the VM level — no off-chain signatures, no nonce tracking, no approval state.

solidity
// ERC-20: relies on msg.sender identity
function transfer(address to, uint256 amount) public returns (bool) {
    require(msg.sender == from); // implicit via msg.sender
    // ...
}

// NEP-17: cryptographic witness verification
function transfer(address from, address to, uint256 amount, Any calldata data)
    public returns (bool)
{
    require(Runtime.checkWitness(from), "NEP17: unauthorized");
    // ...
}

No approve/allowance

NEP-17 does not define approve(), transferFrom(), or allowance(). The devpack's NEP17.sol includes them as optional ERC-20 compatibility extensions, but they are not part of the NEP-17 spec and do not contribute to standard detection.

Payment Callbacks

When tokens are transferred to a contract, the receiving contract's onNEP17Payment callback is invoked automatically:

solidity
interface INEP17Receiver {
    function onNEP17Payment(address from, uint256 amount, Any calldata data) external;
}
Solidity (EVM)Neo-Solidity (NeoVM)
Callbackreceive() / fallback()onNEP17Payment(from, amount, data)
TriggerETH sent to contractNEP-17 tokens transferred to contract
Failure behaviorTransfer revertsTransfer reverts
Data passingmsg.dataExplicit data parameter

TIP

Any contract that receives NEP-17 tokens must implement onNEP17Payment. If the recipient is a contract and does not implement this callback, the transfer reverts with NEP17InvalidReceiver.

Implementation Example

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "standards/NEP17.sol";
import "libraries/Runtime.sol";

/// @title MyToken — A production NEP-17 fungible token
contract MyToken is NEP17 {
    using Runtime for *;

    address private _admin;

    constructor()
        NEP17(
            "My Token",     // name
            "MYT",          // symbol
            8,              // decimals
            100_000_000,    // initial supply (1 MYT = 1e8 units)
            0               // max supply (0 = unlimited)
        )
    {
        _admin = msg.sender;
    }

    /// @dev NEP-17 4-parameter transfer with witness authorization
    function transfer(
        address from,
        address to,
        uint256 amount,
        Any calldata data
    ) public override returns (bool) {
        require(Runtime.checkWitness(from), "MyToken: unauthorized");
        return super.transfer(from, to, amount, data);
    }

    /// @dev Callback for receiving NEP-17 tokens
    function onNEP17Payment(address from, uint256 amount, Any calldata data) external {
        // Accept all incoming token payments
        // Add custom logic here (e.g., token swap, staking deposit)
    }

    /// @dev Mint new tokens — restricted to admin with witness check
    function mintTokens(address to, uint256 amount) external {
        require(Runtime.checkWitness(_admin), "MyToken: not admin");
        mint(to, amount);
    }
}

Manifest Output

Compiling this contract produces supportedstandards: ["NEP-17"] in the manifest. The compiler detects all 5 required methods (symbol, decimals, totalSupply, balanceOf, transfer) and confirms ownerOf is absent (distinguishing it from NEP-11).

ERC-20 Migration Checklist

  1. Replace 2-param transfer(to, amount) with 4-param transfer(from, to, amount, data)
  2. Replace require(msg.sender == ...) with require(Runtime.checkWitness(...))
  3. Remove approve(), transferFrom(), allowance() — not needed in NEP-17 spec
  4. Add onNEP17Payment(from, amount, data) callback for receiving tokens
  5. Replace receive() / fallback() with onNEP17Payment
  6. Use the Any type for the data parameter
  7. Verify manifest shows supportedstandards: ["NEP-17"]

NEP-11: Non-Fungible Tokens

NEP-11 is Neo's non-fungible token standard. Compared to ERC-721, it requires tokensOf() and properties() methods, uses bytes32 token IDs, and supports both indivisible and divisible (fractional ownership) NFTs in a single standard.

Spec: NEP-11 Proposal

Required Interface

MethodParametersReturnSafeDescription
symbolStringYesToken symbol
decimalsIntegerYes0 for indivisible, >0 for divisible
totalSupplyIntegerYesTotal minted tokens
balanceOfHash160 ownerIntegerYesToken count for owner
ownerOfByteArray tokenIdHash160YesToken owner
transferHash160 to, ByteArray tokenId, Any dataBooleanNoTransfer NFT
tokensOfHash160 ownerIteratorYesEnumerate owner's tokens
propertiesByteArray tokenIdMapYesToken metadata

Required event:

Transfer(Hash160 from, Hash160 to, Integer amount, ByteArray tokenId)

NEP-11 Transfer has 4 parameters

Unlike NEP-17's 3-parameter Transfer event, NEP-11 includes the tokenId as a fourth parameter. The amount field is 1 for indivisible NFTs.

Divisible vs Indivisible

NEP-11 supports two modes in a single standard:

IndivisibleDivisible
decimals()Returns 0Returns > 0
ownerOf(tokenId)Single Hash160Iterator of Hash160 (multiple owners)
transfertransfer(to, tokenId, data)transfer(from, to, amount, tokenId, data)
Use caseUnique collectibles, artFractional real estate, shared assets

The devpack provides the INEP11Divisible interface for divisible NFTs:

solidity
interface INEP11Divisible is INEP11 {
    function balanceOf(address owner, bytes32 tokenId) external view returns (uint256);
    function transfer(address from, address to, uint256 amount, bytes32 tokenId, bytes calldata data)
        external returns (bool);
    function ownersOf(bytes32 tokenId) external view returns (address[] memory);
}

Token IDs

ERC-721 (Ethereum)NEP-11 (Neo)
Typeuint256ByteArray (mapped to bytes32 in Solidity)
Neo ABI typeHash256 / ByteArray
GenerationSequential counter or hashSequential counter, hash, or arbitrary bytes
solidity
// ERC-721: uint256 token IDs
function ownerOf(uint256 tokenId) public view returns (address) { ... }

// NEP-11: bytes32 token IDs
function ownerOf(bytes32 tokenId) public view returns (address) { ... }

Implementation Example

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "standards/NEP11.sol";
import "libraries/Runtime.sol";

/// @title MyNFT — A production NEP-11 non-fungible token
contract MyNFT is NEP11 {
    using Runtime for *;

    constructor()
        NEP11(
            "My NFT Collection",  // name
            "MNFT",               // symbol
            0,                    // decimals (0 = indivisible)
            "https://api.example.com/nft/",  // base URI
            10000,                // max supply
            false                 // not divisible
        )
    {}

    /// @dev Mint a new NFT with metadata properties
    function mintNFT(
        address to,
        bytes32 tokenId,
        bytes memory props
    ) external onlyMinter {
        require(Runtime.checkWitness(msg.sender), "MyNFT: unauthorized");
        mint(to, tokenId, props);
    }

    /// @dev NEP-11 3-parameter transfer with witness authorization
    function transfer(
        address to,
        bytes32 tokenId,
        bytes calldata data
    ) public override returns (bool) {
        address tokenOwner = ownerOf(tokenId);
        require(Runtime.checkWitness(tokenOwner), "MyNFT: unauthorized");
        return super.transfer(to, tokenId, data);
    }

    /// @dev Required: enumerate tokens owned by address
    function tokensOf(address owner) public view override returns (bytes32[] memory) {
        return super.tokensOf(owner);
    }

    /// @dev Required: return token metadata
    function properties(bytes32 tokenId) public view override returns (bytes memory) {
        return super.properties(tokenId);
    }

    /// @dev Callback for receiving NEP-11 tokens
    function onNEP11Payment(
        address from,
        uint256 amount,
        bytes32 tokenId,
        bytes calldata data
    ) external {
        // Handle incoming NFT
    }
}

ERC-721 Migration Checklist

  1. Replace uint256 tokenId with bytes32
  2. Replace transferFrom(from, to, tokenId) with transfer(to, tokenId, data) (3 params)
  3. Remove approve(), setApprovalForAll(), getApproved() — use witness model
  4. Add required tokensOf(owner) method returning token ID array
  5. Add required properties(tokenId) method returning serialized metadata
  6. Add decimals() returning 0 for indivisible NFTs
  7. Add onNEP11Payment(from, amount, tokenId, data) callback for receiving NFTs
  8. Verify manifest shows supportedstandards: ["NEP-11"]

NEP-24: Royalty Standard

NEP-24 specifies a royaltyInfo method for NEP-11 NFTs so marketplaces can fetch royalty recipients and amounts for secondary sales. Unlike ERC-2981 which returns a single recipient, NEP-24 supports multiple royalty recipients in a single response.

Spec: NEP-24 Proposal

Interface

MethodParametersReturnDescription
royaltyInfoByteArray tokenId, Hash160 royaltyToken, Integer salePriceArray<[Hash160, Integer]>Royalty recipients and amounts
solidity
interface INEP24Royalty {
    struct RoyaltyInfo {
        address royaltyRecipient;
        uint256 royaltyAmount;
    }

    function royaltyInfo(bytes32 tokenId, address royaltyToken, uint256 salePrice)
        external view returns (RoyaltyInfo[] memory);
}

Key Differences from ERC-2981

ERC-2981 (Ethereum)NEP-24 (Neo)
Return typeSingle (address, uint256)Array of [recipient, amount] pairs
Multiple recipientsNo — single recipient onlyYes — split royalties natively
Royalty tokenImplied (same as sale token)Explicit royaltyToken parameter
Basis points10000 = 100%10000 = 100%
Interface detectionsupportsInterface(0x2a55205a)Manifest supportedstandards: ["NEP-24"]

Royalty Token Parameter

The royaltyToken parameter specifies which token royalties should be paid in (e.g., GAS, a specific NEP-17 token). This is reserved for future use in the devpack's minimal implementation but enables token-specific royalty rules.

Implementation Example

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "standards/NEP11.sol";
import "standards/NEP24.sol";
import "libraries/Runtime.sol";

/// @title RoyaltyNFT — NEP-11 NFT with NEP-24 royalties
contract RoyaltyNFT is NEP11, NEP24Royalty {

    constructor()
        NEP11("Royalty NFT", "RNFT", 0, "", 10000, false)
    {
        // Set default royalty: 5% (500 basis points) to deployer
        _setDefaultRoyalty(msg.sender, 500);
    }

    /// @dev Mint with per-token royalty override
    function mintWithRoyalty(
        address to,
        bytes32 tokenId,
        bytes memory props,
        address royaltyRecipient,
        uint96 royaltyBps
    ) external onlyMinter {
        require(Runtime.checkWitness(msg.sender), "unauthorized");
        mint(to, tokenId, props);

        // Override default royalty for this specific token
        if (royaltyRecipient != address(0) && royaltyBps > 0) {
            _setTokenRoyalty(tokenId, royaltyRecipient, royaltyBps);
        }
    }

    /// @dev Query royalty info — returns array of [recipient, amount] pairs
    /// For a sale of 100 GAS with 5% royalty: returns [(creator, 5 GAS)]
    function royaltyInfo(bytes32 tokenId, address royaltyToken, uint256 salePrice)
        public view override returns (RoyaltyInfo[] memory)
    {
        return super.royaltyInfo(tokenId, royaltyToken, salePrice);
    }
}

The NEP24Royalty abstract contract provides:

Internal MethodDescription
_setDefaultRoyalty(recipient, bps)Set fallback royalty for all tokens
_setTokenRoyalty(tokenId, recipient, bps)Override royalty for a specific token
_clearTokenRoyalty(tokenId)Remove per-token override, fall back to default
_getRoyaltyRule(tokenId)Get effective royalty rule (per-token or default)

Basis Points Validation

The devpack enforces bps <= 10000 and recipient != address(0). Setting invalid values reverts with NEP24InvalidRoyaltyBps or NEP24InvalidRoyaltyRecipient.

Standards Auto-Detection

The neo-solc compiler analyzes your contract's public methods and events to automatically detect which NEP standards it implements. Detection results populate the manifest's supportedstandards array.

NEP-17 Detection

All 5 methods must be present as public/external functions:

symbol, decimals, totalSupply, balanceOf, transfer

Additional rules:

  • ownerOf must not be present (its presence signals NEP-11 instead)
  • A Transfer event with 3 parameters is expected
  • The transfer method should have 4 parameters (from, to, amount, data)
  • Near-miss warning: if 3+ of 5 methods are present but not all, the compiler emits a warning listing the missing methods

NEP-11 Detection

Core requirement:

  • balanceOf and ownerOf must both be present

Plus at least one of:

  • transfer
  • transferFrom
  • tokensOf

Additional checks:

  • A Transfer event with 4 parameters is expected
  • The transfer method should have 3 parameters (to, tokenId, data)
  • Near-miss warnings: ownerOf without a transfer mechanism, or ownerOf + transfer without balanceOf

NEP-24 Detection

Either of these methods triggers detection:

  • tokenUri
  • royaltyInfo

NEP-26 Detection

Both methods must be present:

  • update
  • destroy

If only one is present, the compiler emits an informational diagnostic suggesting the missing method.

Checking Detection Results

After compilation, inspect the manifest to verify detected standards:

bash
neo-solc MyToken.sol -I devpack -O2 -o build/MyToken
cat build/MyToken/MyToken.manifest.json | jq '.supportedstandards'
# Expected: ["NEP-17"]

Interface Detection: EIP-165 vs Manifest

Ethereum and Neo take fundamentally different approaches to interface detection:

EVM (EIP-165)Neo (Manifest)
MechanismRuntime query via supportsInterface(bytes4)Static manifest JSON
Query methodstaticcall to contractContractManagement.getContract()
Gas costPer-query gas costFree (manifest is metadata)
MaintenanceManual supportsInterface() implementationCompiler auto-populates

No supportsInterface() function is needed on Neo. The compiler automatically populates the supportedstandards array based on method signature analysis.

EIP-165 SelectorERC StandardNeo Manifest Entry
0x80ac58cdERC-721"NEP-11"
0x36372b07ERC-20"NEP-17"
0x2a55205aERC-2981"NEP-24"

supportsInterface() Is Unnecessary

If your contract includes supportsInterface(bytes4), the compiler emits warning W106:

warning[W106]: function 'supportsInterface' (EIP-165) is unnecessary on Neo N3.
  Neo uses the manifest 'supportedstandards' array for interface detection,
  which the compiler populates automatically.
  = suggestion: Remove — Neo N3 uses manifest-based interface discovery

Compiler Diagnostics for Standards

The compiler emits actionable diagnostics when it detects Ethereum patterns that need migration. These help catch common mistakes during ERC → NEP porting:

CodePattern DetectedSuggestion
W101transfer(to, amount) — 2-param ERC-20Add from and data params for NEP-17
W102transfer(from, to, amount) — 3-param partialAdd data parameter for NEP-17
W103approve/allowance/transferFrom presentNot in NEP-17 spec; keep as extensions or remove
W104transferFrom(from, to, tokenId) — ERC-721Replace with transfer(to, tokenId, data)
W105receive() / fallback() presentReplace with onNEP17Payment callback
W106supportsInterface(bytes4) presentRemove — manifest handles interface detection
W107ERC-1155 multi-token patternSplit into separate NEP-17 and NEP-11 contracts
W108ERC-2612 permit (7-param)Use Runtime.checkWitness() instead

Testing Standards Compliance

After compiling, verify your contract meets the target standard:

  1. Check manifest — confirm supportedstandards contains the expected entries:

    bash
    cat build/MyToken/MyToken.manifest.json | jq '.supportedstandards'
  2. Verify method signatures — ensure all required methods are present with correct parameter counts. The compiler warns on near-misses.

  3. Test transfer with witness — invoke transfer with a valid witness scope and verify Runtime.checkWitness() passes:

    bash
    neo-express invoke MyToken transfer \
      --account sender -- sender recipient 1000 null
  4. Test payment callbacks — transfer tokens to a contract and verify onNEP17Payment or onNEP11Payment is called.

  5. Integration testing — use neo-express for end-to-end testing on a local private network:

    bash
    neo-express create
    neo-express run
    neo-express contract deploy build/MyToken/MyToken.nef
    neo-express invoke MyToken symbol

Compiler Warnings Are Your Friend

Address all W1xx warnings before deploying. They indicate patterns that may compile but won't produce a standards-compliant manifest or may behave unexpectedly on Neo N3.

See Also

MIT Licensed