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:
- Fungible Token — Used to create new tokens
- Non-Fungible Token — Used to create NFTs
- Metadata Views — Used as a metadata pattern for NFTs
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.
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!