Flow & Cadence Best Practices, Patterns, and Anti-Patterns

Flow & Cadence Best Practices, Patterns, and Anti-Patterns

Learn tips that will help you create a better user experience and ensure a consistent level of security throughout the Flow ecosystem.

Throughout this article series, we’ve introduced you to the Flow blockchain, its smart contract language Cadence, and some of the most essential tools developers should know, all while comparing and contrasting to Ethereum.

In this article, we will talk about best practices and patterns that should be followed when using the Cadence language and developing solutions on the Flow network, as well as patterns to avoid.

Best practices

Use Flow Test Network and Flow Emulator

One of the most important steps in developing on any blockchain is testing your projects in a simulated environment. Flow blockchain is no different. For this purpose, the Flow testnet and Flow Emulator are both crucial components to include in your development process. Not only do they allow you to understand how your dapp will perform, but they will also help you catch any critical bugs before deploying to mainnet.

Use Standard Contracts

Two of the most common use cases for any blockchain are digital currencies and providing proof of digital ownership through NFTs. Instead of trying to rewrite the necessary contracts from scratch, Flow developers should always import the official core contracts to ensure user safety and consistency with the rest of the network.

There are currently three standard contracts you should use:

These contracts are essentially interfaces that force whoever is implementing them to add their variable and method declarations, enabling them to be interoperable with other smart contracts in the Flow ecosystem. They already exist on both testnet and mainnet networks, so be sure to import from the official addresses linked above when implementing them in your contract.

You can find other core contracts in the Flow documentation.

Create Tests for Your Smart Contracts, Transactions, and Scripts

The Flow JavaScript Testing Framework is crucial for testing deployment scenarios for your contracts. However, it’s important to note that you’ll need the Flow CLI running in the background for full functionality. With these, you can test creating new accounts, sending transactions, running queries, executing scripts, and more. Additionally, it integrates with the Flow Emulator so you can create, run, and stop emulator instances.

Add Your Contract to the Flow NFT Catalog

The Flow NFT Catalog exists as a database to store NFT contracts and metadata. By uploading your contract, your NFTs become interoperable with the rest of the Flow ecosystem, and other developers can easily support your collection in their marketplaces or other dapps.

You can add your contract to the catalog by completing a simple four-step process.

image1

Be Specific With Types

Cadence is a strong and statically typed language that allows the developer to specify which types contain or return variables, interfaces, and functions. Therefore, you should be as specific as possible when making declarations; use generic types only in necessary situations. Not doing so can result in clumsy errors.

Access Control

Whenever possible, you should be deliberate with access control. Luckily, on the Flow blockchain, a caller cannot access objects in another user’s account storage without a reference to it. This is called reference-based security, and it means that nothing is truly public by default.

However, when writing Cadence code, care must be taken when declaring variables, structs, functions, resources, and so on. There are four levels of access control a developer can include in their declarations:

  • pub/access(all) — accessible/visible in all scopes
  • access(account) — only accessible in the entire account where it’s defined (other contracts in the same account can access)
  • access(contract) — only accessible in the scope of the contract in which it is defined (cannot be accessed outside of the contract)
  • priv/access(self) — only accessible in current and inner scopes

Avoid using pub/access(all) whenever possible. Check out the Flow documentation for more information on access control.

Create StoragePath and PublicPath Constants Inside Your Contract

The paths to your contract’s resources are extremely important and must be consistent across all transactions and scripts. To ensure uniformity, you should create constants for both PublicPath and StoragePath.

pub contract BestPractices {
    pub let CollectionStoragePath: StoragePath
    pub let CollectionPublicPath: PublicPath

    init(){
        self.CollectionStoragePath = /storage/bestPracticesCollection
        self.CollectionPublicPath = /public/bestPracticesCollection
    }
}

When you create a transaction that uses one of these paths, you only need to call its respective variable with the imported contract to reference the required path.

//This script checks if a user has the public Collection from BestPractices contract
import BestPractices from 0x...

pub fun main(addr: Address): Bool {
    return getAccount(addr).getLinkTarget(BestPractices.CollectionPublicPath) != nil
}

Admin Resource

It's good practice to create an administrative resource that contains specific functions to perform actions under the contract, such as a mint function. This convention ensures that only accounts with that resource or capability can perform administrative functions.

pub contract BestPractices {
    pub let CollectionStoragePath: StoragePath
    pub let CollectionPublicPath: PublicPath
    pub let AdminStoragePath: StoragePath

    pub resource AdminResource {
        pub fun mintNFT(){
            //your mint NFT code here! 
        }
    }

    init(){
        self.CollectionStoragePath = /storage/bestPracticesCollection
        self.CollectionPublicPath = /public/bestPracticesCollection
        self.AdminStoragePath = /storage/bestPracticesAdmin

        //Create the adminResource and store it inside Contract Deployer account
        let adminResource <- create AdminResource()
        self.account.save(<- adminResource, to: self.AdminStoragePath)
    }
}

Create Custom Events

Events are values that can be emitted during the execution of your Cadence code. For example, when defining important actions in your contracts, you can emit events to signal their completion or deliver a specific value. As a result, transactions that interact with your smart contracts can receive additional information through these events.

pub contract BestPractices {
    pub let CollectionStoragePath: StoragePath
    pub let CollectionPublicPath: PublicPath
    pub let AdminStoragePath: StoragePath

//Create your own events
pub event AdminCreated()


    pub resource AdminResource {
        pub fun mintNFT(){
            //your mint NFT code here! 
        }
    }

    init(){
        self.CollectionStoragePath = /storage/bestPracticesCollection
        self.CollectionPublicPath = /public/bestPracticesCollection
        self.AdminStoragePath = /storage/bestPracticesAdmin

        //Create the adminResource and store it inside Contract Deployer account
        let adminResource <- create AdminResource()
        self.account.save(<- adminResource, to: self.AdminStoragePath)
    }
}

Patterns on Cadence/Flow Blockchain

Be Specific With Types in Type Constraints

One of the most powerful features of the Cadence language is undoubtedly capabilities. Through capabilities, the scope of resource access expands.

An important point when creating a capability is to specify which features of your resource should be available to others. This can be done at the time of link creation using type constraints.

In this example, we use the ExampleNFT contract to create a basic functionality where any account that wants to receive an ExampleNFT must have a collection.

import NonFungibleToken from 0x...
import ExampleNFT from 0x...

transaction{
    prepare(acct: AuthAccount){
        let collection <- ExampleNFT.createEmptyCollection()
    // Put the new collection in storage
      acct.save(<-collection, to: ExampleNFT.CollectionStoragePath)
    // Create a public Capability for the collection
    acct.link<&ExampleNFT.Collection{ExampleNFT.CollectionPublic}>(ExampleNFT.CollectionPublicPath, target: ExampleNFT.CollectionStoragePath)
    }
}

The & symbol in &ExampleNFT specifies that we are using a reference. After the reference symbol, we add the type to which the capability we are creating will have access. At this point, we need to be as specific as possible.

This pattern strengthens security and limits the functionality that the user calling the borrow function of this capability can use.

Omitting the {ExampleNFT.CollectionPublic} type will give you access to all the functions that exist in the ExampleNFT.Collection reference, including the withdraw function, so that anyone can access the user's collection and steal their NFTs.

Use the Borrow Function

To use the resource’s features, you could call the Load function to remove the resource from the account, use its features, and call the Save function to save it again. However, this approach is costly and inefficient.

To avoid this, use the borrow function instead. It allows you to use a reference to the resource you are calling. This method makes your transaction much more efficient and cost-effective.

Use the check and getLinkTarget Functions

When building applications on the Flow blockchain, you will discover the user’s account plays a vital role. Unlike other blockchains, such as Ethereum, Flow stores resources, assets, and more directly in the user’s account rather than as a reference to an address on a public digital ledger.

This approach requires the account to have a specific storage location, such as a collection or vault for fungible tokens. However, this also adds complexity. One must be sure that the user either does or does not have the collection in their account.

Both the check() function (which checks whether a capability exists in a given path) and the getLinkTarget() function (which returns the path of a given capability) must be used when adding collections and capabilities to the user account. These functions ensure that the transaction executes without problems.

Use Panic

Panic is a built-in function in Cadence that allows you to terminate a program unconditionally. This can occur during the execution of your smart contract code and returns an error message, which makes it easier to understand when something does not go as expected.

When declaring variables, it is possible to define them as optional; meaning, if they are not of the specified type, they have a value of nil.

Thus, in Cadence, it is possible to use two question marks followed by the panic("treatment message") function when querying the value of a particular variable or function that returns an optional.

let optionalAccount: AuthAccount? = //...
let account = optionalAccount ?? panic("missing account")

This command ??panic("treatment message") attempts to return the value with the specified type. If the returned value is the wrong type, or nil, the execution aborts, and the selected treatment message displays on the console.

Anti-Patterns

Although Cadence is designed to avoid many of the potential bugs and exploits found in other blockchain ecosystems, there are some anti-patterns developers should be aware of while building. Listed below are a few important ones to consider. For a complete list of anti-patterns, check out the Flow documentation.

Failing to Specify the Type When Using the Borrow Function

Developers should use the borrow function mentioned above to take advantage of the features available in a capability. However, it should be clear that users can store anything in their memory. Therefore, it is vital to make sure that what is borrowed is of the correct type.

Not specifying the type is an anti-pattern that can end up causing errors or even breakage in your transaction and application.

//Bad Practice. Should be avoided
let collection = getAccount(address).getCapability(ExampleNFT.CollectionPublicPath)
                    .borrow<&ExampleNFT.Collection>()?? panic("Could not borrow a reference to the nft collection")

//Correct! 
let collection = getAccount(address).getCapability(ExampleNFT.CollectionPublicPath)
                    .borrow<&ExampleNFT.Collection{NonFungibleToken.CollectionPublic}>()
                    ?? panic("Could not borrow a reference to the nft collection")

Using AuthAccount as a Function Parameter

For a transaction in the preparation phase, it is possible to access the AuthAccount field of the user. This object allows access to the memory storage and all other private areas of the account for the user who provides it.

Passing this field as an argument is not recommended and should be avoided, as cases where this method is necessary are extremely rare.

//Bad Practice. Should be avoided

transaction() {
    prepare(acct: AuthAccount){
        //You take sensitive and important user data out of the scope of the transaction prepare phase
        ExampleNFT.function(acct: acct)
    }
}

Using Public Access Control on Dictionaries and Arrays

By storing dictionaries and array variables in your contract with pub/access(all) scope, your smart contract becomes vulnerable, as anyone can manipulate and change these values.

Creating Capabilities and References Using the Auth Keyword

Creating capabilities and references with the auth keyword exposes the value to downcasting, which could provide access to functionality that was not originally intended.

//AVOID THIS!
signer.link<auth &ExampleNFT.CollectionPublic{NonFungibleToken.Receiver}>(
    /public/exampleNFT,
    target: /storage/exampleNFT
)

Conclusion

When developing on the Flow blockchain using Cadence, it helps to be aware of design patterns, anti-patterns, and best practices.

By following these best practices, we can ensure a consistent level of security throughout the Flow ecosystem, thus providing the best experience for users. For a more thorough understanding, read through the Flow documentation.

Have a really great day!