MultiToken

MultiToken is a specification for contracts that manage multiple token types. This module is an approximation of EIP-1155 written in the Compact programming language for the Midnight network.

ERC1155 Compatbility

Even though Midnight is not EVM-compatible, this implementation attempts to be an approximation of the standard. Some features and behaviors are either not possible, not possible yet, or changed because of the vastly different tech stack and Compact language constraints.

Notable changes

  • Uint<128> as value and id type - Since 256-bit unsigned integers are not supported, the library uses the Compact type Uint<128>.

Features and specifications NOT supported

  • Events - Midnight does not currently support events, but this is planned on being supported in the future.

  • Uint256 type - There’s ongoing research on ways to support uint256 in the future.

  • Interface - Compact currently does not have a way to define a contract interface. This library offers modules of contracts with free floating circuits; nevertheless, there’s no means of enforcing that all circuits are provided.

  • Batch mint, burn, transfer - Without support for dynamic arrays, batching transfers is difficult to do without a hacky solution. For instance, we could change the to and from parameters to be vectors. This would change the signature and would be both difficult to use and easy to misuse.

  • Querying batched balances - This can be somewhat supported. The issue, without dynamic arrays, is that the module circuit must use Vector<n> for accounts and ids; therefore, the implementing contract must explicitly define the number of balances to query in the circuit i.e.

balanceOfBatch_10(
   accounts: Vector<10, Either<ZswapCoinPublicKey, ContractAddress>>,
   ids: Vector<10, Uint<128>>
): Vector<10, Uint<128>>

Since this module does not offer mint or transfer batching, balance batching is also not included at this time.

  • Introspection - Compact currently cannot support contract-to-contract queries for introspection. ERC165 (or an equivalent thereof) is NOT included in the contract.

  • Safe transfers - The lack of an introspection mechanism means safe transfers of any kind can not be supported.

Contract-to-contract calls

Contract-to-contract calls are currently not supported in the Compact language. Due to this limitation, the current iteration of MultiToken disallows transfers and mints to the ContractAddress type. Transferring tokens to a contract may result in those tokens being locked forever. The MultiToken module, however, does provide unsafe circuit variants for users who wish to experiment with sending tokens to contracts.

The unsafe circuits will eventually be deprecated after Compact supports contract-to-contract calls—meaning transferFrom, _mint, etc. are planned to eventually allow the recipients to be of the ContractAddress type.

Usage

Import the MultiToken module into the implementing contract. It’s recommended to prefix the module with MultiToken_ to avoid circuit signature clashes.

pragma language_version >= 0.16.0;

import CompactStandardLibrary;
import "./node_modules/@openzeppelin-compact/contracts/src/token/MultiToken"
  prefix MultiToken_;

constructor(
  uri: Opaque<"string">,
) {
  MultiToken_initialize(uri);
}

Next, expose the ciruits that users may call in the contract. This library enables extensibility by following the rules of the Module/Contract Pattern. Note that circuits with a preceding underscore (_likeThis) are meant to be building blocks for implementing contracts. Exposing _mint without some sort of access control, for example, would allow ANYONE to mint tokens.

export circuit uri(id: Uint<128>): Opaque<"string"> {
  return MultiToken_uri();
}

export circuit balanceOf(
  account: Either<ZswapCoinPublicKey, ContractAddress>
): Uint<128> {
  return MultiToken_balanceOf(account);
}

(...)

The following example is a simple multi-token contract that creates both a fixed-supply fungible token and an NFT using the same contract.

// MultiTokenTwoTokenTypes.compact

pragma language_version >= 0.16.0;

import CompactStandardLibrary;
import "./node_modules/@openzeppelin-compact/contracts/src/token/MultiToken"
  prefix MultiToken_;

constructor(
  _uri: Opaque<"string">,
  recipient: Either<ZswapCoinPublicKey, ContractAddress>,
  fungibleFixedSupply: Uint<128>,
) {
  // `initialize` sets the URI (base) for all tokens minted from this contract
  MultiToken_initialize(_uri);

  // Token id `123` is a fungible token (with a fixed supply of `fungibleFixedSupply`)
  const fungibleTokenId = 123;
  MultiToken__mint(recipient, fungibleTokenId, fungibleFixedSupply);

  // Token id `987` is a non-fungible token (so the supply is only 1)
  const nonFungibleTokenId = 987;
  MultiToken__mint(recipient, nonFungibleTokenId, 1);
}

export circuit uri(id: Uint<128>): Opaque<"string"> {
  return MultiToken_uri(id);
}

export circuit balanceOf(
  account: Either<ZswapCoinPublicKey, ContractAddress>,
  id: Uint<128>,
): Uint<128> {
  return MultiToken_balanceOf(account, id);
}

export circuit setApprovalForAll(
  operator: Either<ZswapCoinPublicKey, ContractAddress>,
  approved: Boolean
): [] {
  return MultiToken_setApprovalForAll(operator, approved);
}

export circuit isApprovedForAll(
  account: Either<ZswapCoinPublicKey, ContractAddress>,
  operator: Either<ZswapCoinPublicKey, ContractAddress>
): Boolean {
  return MultiToken_isApprovedForAll(account, operator);
}

export circuit transferFrom(
  from: Either<ZswapCoinPublicKey, ContractAddress>,
  to: Either<ZswapCoinPublicKey, ContractAddress>,
  id: Uint<128>,
  value: Uint<128>,
): [] {
  return MultiToken_transferFrom(from, to, id, value);
}