ICRC1, ICRC2, and ICRC3 Tokens in Motoko

We have added an ICRC1, 2, and 3 compatible token framework it includes:

  • icrc1.mo - A Class+ style motoko icrc1 token based on a fork of GitHub - NatLabs/icrc1: A full implementation of the ICRC-1 fungible token standard but with a set of simplifications(removing archiving) and a set of new features(event listeners, event intercepts).
  • icrc2.mo - A Class+ style motoko icrc2 token add on that provides the approve and transfer_from workflow.
  • icrc3.mo - A Class+ style motoko icrc3 component that will take care of creating, managing, and archiving an icrc3 style transaction log.
  • icrc-fungible - A set of example actors that put th three components in place for fully functioning Fungible Token. This includes examples like an allowlist token that requires token users to be authorized to send the token(but not receive) and a lotto token that will double any burns after a coin flip to the burner that demonstrate the event handling and event intercept mechanism of the classes.

Warning: ICRC3 has not yet been finalized.
Warning2: These components have not yet been audited.
Warning3: Don’t use these in production yet. But please use them and provide feed back.

We will be seeking to formalize a community audit procedure to burn in these items for production use in the near future and would love the community’s help.

This work follows up on our Motoko - NFT with Approve and Transaction Log - An ICRC7/30/30 Canister work.

Our next steps will be formalizing ICRC4(batch transfer for fungibles), ICRCX(batch approve/transfer_from), ICRC_ROSETTA, and a plug in indexer that follows the standard put forward by DFINITY.


Great work on getting closer to finalizing these much needed token standards.

One thing that catches my eye is the naming conventions used.

The Motoko style guide, under naming recommends that UpperCamelCase be used for type names, and lowerCamelCase be used for all other names (functions, variables, etc).
In the proposed standards variable and function names mostly snake_case, with some lowerCamelCase examples. If these are standards to be widely used across the ecosystem it would be better to stick to the style guide recommends.

The Motoko standard library already mostly uses lowerCamelCase, thus, having these standards use snake_case would result in less clean code.

In the interest of code cleanliness I’d suggest sticking to what the style guide recommends.

Very cool. I’ll be needing something like this soon. I’ll give feedback if i have any

It is a bit of a conundrum since the icrc_methods are being designated with snake_case format and these(at least the public methods) are meant to make it no-duh easy for devs to play around and be productive with.

We could limit the snake case to those public functions that expose the root icrc functionality, but I’d like to hear @cryptoschindler’s opinion(as well as others!) as he suggested making sure we have the top level items in the class with no handling needed.


I’ve had issues with seeing naming be different in places as well, but I think its more of a Candid definition consistency thing. Motoko might need a case conversion to satisfy both where we could decorate a method/properties with candid names that differ from the motoko case

Good job :slightly_smiling_face:

Is there any other ICRC2 or ICRC3 standard motoko package that I can use in production?

Probably not yet. Now that ICRC3 is getting an update I need to do some refactoring and adding of top level type and fall back for 1 and 2.

As these have not been audited I would highly recommend doing some test releases as well. We have some toy tokens we want to release and try out, but have not gotten to that point yet.

@PanIndustrial, I was looking into the icrc3.mo code and I could not figure out the motivation of a parameter in one of the main library functions:

  • Library: icrc3.mo/main/src/lib.mo
  • Method interface: public func add_record<system>(new_record: Transaction, top_level: ?Value) : Nat

What is the motivation of the second parameter top_level ?

The first parameter is what gets put in the “op” value and should be a #Map. the second is any items you want at the top level. If you provide null then there sill just be “op” item in your block. You should likely have at least a btype. This signature is a bit of a legacy from before there was a btype.

Typically I have

let top  = #Map([
("btype", #Text("blockType"),
("ts", #Nat(Int.abs(Time.now()]);

let op = #Map([
   ("amount", #Nat(1000),
   ("btype", #Text("blockType"), //see note
   ("caller", #Blob(Principal.toBlob(caller));

add_record<sysem(op, ?top);

add_record will mash them together.

Note: If you want deduplication, the component uses just the op section for that. So if you have blocks that are similar in their structure but of different types you may want to put the btype in the op as well as in the top.