Considerations for NFT developers


Although there are many applications of NFTs, for many of them the defining characteristic is their permanence and immutability (or evolution according to predefined rules). Whereas on most other blockchain platforms the actual assets have to be stored off-chain, the Internet Computer (IC) allows to host everything on-chain, including audio and video. But with great power comes great responsibility. Canisters are mutable by default and canisters have to pay for the resources they use on the IC. If canisters run out of cycles, they get deallocated, i.e. all state is lost and only the canister ID and the list of controllers is retained by the IC. NFT collections typically need at least the following functions: 1. A registry that tracks ownership and allows transfers 2. A ledger or transaction history 3. The actual assets Depending on the architecture, all of these functions may be in one canister or spread across multiple canisters right up to an asset canister per individual NFT. As a NFT developer you need to make sure that each of these canisters does not run out of cycles. In the following we discuss some best practices and topics to think about when creating or evaluating an NFT implementation. The Basics
Top up all canisters very generously Make sure that all canisters have enough cycles to sustain a few years to begin with. Storage and computation on the IC are magnitudes less expensive than on other platforms, so don’t be stingy. Set a generous freezing threshold The IC has a useful mechanism to save your canister from running out of cycles. Canisters have a configurable `freezing_threshold`. The `freezing_threshold` can be set by the controller of a canister and is given in seconds. The IC dynamically evaluates this as a threshold value in cycles. The value is such that the canister will be able to pay for its idle resources for at least the time given in `freezing_threshold`. To guarantee that, the canister is frozen when the cycle balance reaches the threshold, and all update calls, including the heartbeat, are immediately rejected and won’t affect the canister’s cycles balance. The default value is approximately 30 days, but for NFTs, developers should set the `freezing_threshold` to at least 90 days, preferably 180 days. This makes sure that NFT developers and their users have enough time to react and top up the canisters before they finally run out of cycles. Reject query calls if the cycles balance of a canister is low One issue right now is that query calls to frozen canisters aren’t rejected, because they aren’t affecting the cycles balance. Besides transfers, NFT canisters are mostly queried. This is particularly true if there are dedicated asset canisters. This may lead to a false security for NFT owners, since the NFT will show up in their wallets and on marketplaces until the moment they are deallocated without prior warning. Since query calls will be charged at some point anyway, it seems sensible to start rejecting query calls when a canister is frozen. Until this is enforced by the IC itself, we encourage NFT developers to reject query calls below a certain threshold. Make sure your canisters can be monitored On the IC, the cycles balance of a canister is only visible for the controller. Since an NFT (collection) might outlive its creator, you should plan for monitoring by third parties. You can do this via implementing a simple query method as included in the DIP721 and EXT standards or use a more complete monitoring solution like Canistergeek. The team behind Canistergeek added a new feature to their Nftgeek product that allows to observe the cycles balance of popular NFT collections. In general, there should be a standardized metrics endpoint that includes stable memory and heap memory and you should make sure that a canister can not grow out of the bounds provided by the system (i.e. currently 4GB for heap memory and 8GB for stable memory). Additionally, make your users aware that they are able to top up canisters, e.g. via the Canister Tip Jar service. Follow best practices for efficient implementations There are a few footguns which could make your canister more expensive than you’d expect. Here are a few examples that you might encounter when implementing NFT canisters.
  • Use of the heartbeat: A plain heartbeat without doing anything will cost ~0.055 T cycles/day. There are discussions about [implementing alternatives that allow for cheaper scheduling](https://forum.dfinity.org/t/heartbeat-improvements-timers-community-consideration/14201)
Motoko specific
  • Use `TrieMap` instead of `HashMap` to avoid the performance cliff of automatic resizing associated with HashMaps.
  • Use `Buffer` instead of `Array` if you need to dynamically resize the structure.
  • Use `Blob` instead of `[Nat8]` for storing large binary assets.
  • Consider using `Blob` instead of `[Nat8]` when sending or receiving Candid `vec nat8/blob` values. The choice is yours but `Blob`s are 4x more compact and much less taxing on garbage collection.
  • Consider storing large `Blob`s in stable memory, to reduce pressure on the GC even further, especially when the manual memory management of those Blob is simple (e.g. they are only added, never deleted).
  • Consider using the `compacting-gc` setting, especially in append-only scenarios, to allow access to larger heaps and reduce the cost of copying large, stationary objects.
Rust specific
  • Be careful with extensive use of `Vec<u8>` and hence the `String` type if you need to (de-)serialize state for upgrades.
A general article with good practices for canister development by Joachim Breitner. To make sure you won’t get surprised, you can use the recently added performance counter API to profile your canisters even before going live. Implement mechanisms to backup and restore state The IC itself does not yet support backup and restoration of canister state, but it can be implemented in the canister itself. Regular backups are an insurance against the worst case scenario that a canister gets deallocated or there are issues with upgrading a canister. Advanced Topics
Think about governance The value proposition of most NFTs is their permanence and immutability, e.g. by setting the blackhole canister as a controller. As long as NFT canisters have their developers as controllers, users depend on the trustworthiness (and operational security) of the developers. Developers should therefore either make the canisters immutable or manage the canisters with a DAO. A middle ground are mechanisms like Launchtrail that makes changes to a canister auditable. Blackholing a canister has its issues as well. If there are bugs in the canister code or you’re using experimental system APIs that might get deprecated later on, the canister might stop functioning. Think about economic sustainability Ideally, your canisters implement mechanisms to generate fees that the canisters can use to pay for their existence indefinitely. A simple approach is to utilize (parts of) the transfer fee to fuel the canisters, but more elaborate schemes could involve staking or other advanced mechanisms. We’re not aware of any good blueprints, but please share if you know of projects that implement clever mechanisms.
Made with Papyrs