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.
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.
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.
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.
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.
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.
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 scopesaccess(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.
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
}
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)
}
}
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)
}
}
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.
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.
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.
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.
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.
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")
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)
}
}
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 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
)
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!