Near Learn

NEP-245: Multi token

NEP-245 multi token standard on NEAR: many FT and NFT types in one contract, mt_transfer, batch transfers, mt_transfer_call, ERC-1155 and NEAR Intents.

Intermediate6 min read3-question check

NEP-245 lets one contract manage many token types at once — each identified by a string token_id, each with its own balances. A token ID with a supply of one behaves like an NFT; one with a large supply behaves like a fungible token. It is NEAR’s counterpart to ERC-1155 and is the natural fit for game items, editions, and any system that wraps many assets.

Its most prominent user today is NEAR Intents: the intents contract tracks users’ deposited assets as multi-token balances, with token IDs that name the underlying asset (for example nep141:wrap.near for wNEAR deposited from its NEP-141 contract).

Core methods#

MethodKindWhat it doesDeposit
mt_transfer(receiver_id, token_id, amount, approval?, memo?)changeMove amount of one token ID.exactly 1 yoctoNEAR
mt_batch_transfer(receiver_id, token_ids, amounts, approvals?, memo?)changeMove several token IDs to one receiver in one call.exactly 1 yoctoNEAR
mt_transfer_call(receiver_id, token_id, amount, approval?, memo?, msg)changeTransfer, then call receiver_id.mt_on_transfer(...); unused amounts are refunded.exactly 1 yoctoNEAR
mt_batch_transfer_call(receiver_id, token_ids, amounts, approvals?, memo?, msg)changeBatch version of the above.exactly 1 yoctoNEAR
mt_balance_of(account_id, token_id) / mt_batch_balance_of(account_id, token_ids)viewBalance(s) as strings.—
mt_supply(token_id) / mt_batch_supply(token_ids)viewTotal supply per token ID (or null if unknown).—
mt_token(token_ids)viewToken objects (token_id, owner_id — owner is meaningful for NFT-like IDs).—
mt_on_transfer(sender_id, previous_owner_ids, token_ids, amounts, msg)change (on the receiver)Returns, per token, the amount to refund.—

The spec also defines optional extensions — metadata, approval management and enumeration — following the same pattern as the NFT family. The approval argument is a tuple [owner_id, approval_id]: an approved account names whose tokens it is moving and the approval ID it was granted. Events (mt_mint, mt_transfer, mt_burn) use the NEP-297 format with standard nep245.

ERC-1155 vs NEP-245
Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IERC1155 {
    function balanceOf(address account, uint256 id) external view returns (uint256);
    function safeTransferFrom(
        address from, address to, uint256 id, uint256 amount, bytes calldata data
    ) external;
    function safeBatchTransferFrom(
        address from, address to,
        uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data
    ) external;
    function setApprovalForAll(address operator, bool approved) external;
}

// receiver hook: return a magic value to ACCEPT
interface IERC1155Receiver {
    function onERC1155Received(
        address operator, address from, uint256 id, uint256 value, bytes calldata data
    ) external returns (bytes4);
}
Rust
use near_sdk::json_types::U128;
use near_sdk::{env, near, require, AccountId, PanicOnDefault, PromiseOrValue};

// A contract that RECEIVES multi tokens via mt_transfer_call.
#[near(contract_state)]
#[derive(PanicOnDefault)]
pub struct Vault {
    mt_contract: AccountId,
}

#[near]
impl Vault {
    // NEP-245 receiver hook: return the amount to refund, per token
    pub fn mt_on_transfer(
        &mut self,
        sender_id: AccountId,
        previous_owner_ids: Vec<AccountId>,
        token_ids: Vec<String>,
        amounts: Vec<U128>,
        msg: String,
    ) -> PromiseOrValue<Vec<U128>> {
        let _ = (sender_id, previous_owner_ids, msg);
        require!(env::predecessor_account_id() == self.mt_contract, "unknown MT contract");
        require!(token_ids.len() == amounts.len(), "length mismatch");

        // ...credit the deposits...

        // keep everything: refund 0 of each token
        PromiseOrValue::Value(vec![U128(0); amounts.len()])
    }
}
Coming from EVM

safeTransferFrom(from, to, id, amount, data) ↔ mt_transfer_call(receiver_id, token_id, amount, approval, memo, msg). There is no from: the sender is the caller, or the owner named in the approval tuple.

ERC-1155 receivers return a selector to accept; NEP-245 receivers return refund amounts, like NEP-141’s ft_on_transfer. Token IDs are strings, so nep141:wrap.near-style IDs are possible.

Check yourself

3 questions · progress saved in this browser

  1. 1.In mt_transfer, what is the approval argument for?
  2. 2.A receiver gets 10 units via mt_transfer_call but only needs 9. What should mt_on_transfer return for that token?
  3. 3.Why is NEP-245 a good fit for NEAR Intents?