Resources and the Move (<-) Operator
This tutorial builds your understanding of accounts and how to interact with them by introducing resources. Resources are a special type found in Cadence that are used for any virtual items, properties, or any other sort of data that are owned by an account. They can only exist in one place at a time, which means they can be moved or borrowed, but they cannot be copied.
Working with resources requires you to take a few more steps to complete some tasks, but this level of explicit control makes it nearly impossible to accidentally duplicate, break, or burn an asset.
Objectives
After completing this tutorial, you'll be able to:
- Instantiate a
resourcein a smart contract with thecreatekeyword. - Save, move, and load resources using the Account Storage API and the move operator (
<-). - Use
borrowto access and use a function in a resource. - Use the
preparephase of a transaction to load resources from account storage. - Set and use variables in both the
prepareandexecutephase. - Use the nil-coalescing operator (
??) topanicif a resource is not found.
Resources
Resources are one of the most important and unique features in Cadence. They're a composite type, like a struct or a class in other languages, but with some special rules designed to avoid many of the traditional dangers in smart contract development. The short version is that resources can only exist in one location at a time — they cannot be copied, duplicated, or have multiple references.
Here is an example definition of a resource:
_10access(all) resource Money {_10 access(all) let balance: Int_10_10 init() {_10 self.balance = 0_10 }_10}
As you can see, it looks just like a regular struct definition. The difference, however, is in the behavior.
Resources are useful when you want to model direct ownership of an asset or an object. By direct ownership, we mean the ability to own an actual object in your storage that represents your asset, instead of just a password or certificate that allows you to access it somewhere else.
Traditional structs or classes from other conventional programming languages are not an ideal way to represent direct ownership because they can be copied. This means that a coding error can easily result in creating multiple copies of the same asset, which breaks the scarcity requirements needed for these assets to have real value.
We must consider loss and theft at the scale of a house, a car, a bank account, or even a horse. It's worth a little bit of extra code to avoid accidentally duplicating ownership of one of these properties!
Resources solve this problem by making creation, destruction, and movement of assets explicit.
Implementing a contract with resources
Open the starter code for this tutorial in the Flow Playground at play.flow.com/b999f656-5c3e-49fa-96f2-5b0a4032f4f1.
The HelloResource.cdc file contains the following code:
Defining a resource
Similar to other languages, Cadence can declare type definitions within deployed contracts. A type definition is simply a description of how a particular set of data is organized. It is not a copy or instance of that data on its own.
Any account can import these definitions to interact with objects of those types.
The key difference between a resource and a struct or class is the access scope for resources:
- Each instance of a resource can only exist in exactly one location and cannot be copied.
- Here, location refers to account storage, a temporary variable in a function, a storage field in a contract, and so on.
- Resources must be explicitly moved from one location to another when accessed.
- Resources also cannot go out of scope at the end of function execution. They must be explicitly stored somewhere or explicitly destroyed.
- A resource can only be created in the scope that it is defined in.
- This prevents anyone from being able to create arbitrary amounts of resource objects that others have defined.
These characteristics make it impossible to accidentally lose a resource from a coding mistake.
Add a resource called HelloAsset that contains a function to return a string containing "Hello Resources!":
A few notes on this function:
access(all)makes the function publicly accessible.viewindicates that the function does not modify state.- The function return type is a
String. - The function is not present on the contract itself and cannot be called by interacting with the contract.
If you're used to Solidity, you'll want to take note that the view keyword in Cadence is used in the same cases as both view and pure in Solidity.
Creating a resource
The following steps show you how to create a resource with the create keyword and the move operator (<-).
You use the create keyword to initialize a resource. Resources can only be created by the contract that defines them and must be created before they can be used.
The move operator <- is used to move a resource — you cannot use the assignment operator =. When you initialize them or assign then to a new variable, you use the move operator <- to literally move the resource from one location to another. The old variable or location that was holding it will no longer be valid after the move.
- Create a resource called
first_resource:_10// Note the `@` symbol to specify that it is a resource_10var first_resource: @AnyResource <- create AnyResource() - Move the resource:
The name_10var second_resource <- first_resource
first_resourceis no longer valid or usable:_10// Bad code, will generate an error_10var third_resource <- first_resource - Add a function called
createHelloAssetthat creates and returns aHelloAssetresource:- Unlike the
hello()function, this function does exist on the contract and can be called directly. Doing so creates an instance of theHelloAssetresource, moves it through thereturnof the function to the location calling the function — the same as you'd expect for other languages. - Remember, when resources are referenced, the
@symbol is placed at the beginning. In the function above, the return type is a resource of theHelloAssettype.
- Unlike the
- Deploy this code to account
0x06by clicking theDeploybutton.
Creating a Hello transaction
The following shows you how to create a transaction that calls the createHelloAsset() function and saves a HelloAsset resource to the account's storage.
Open the transaction named Create Hello, which contains the following code:
We've already imported the HelloResource contract for you and stubbed out a transaction. Unlike the transaction in Hello World, you will need to modify the user's account, which means you will need to use the prepare phase to access and modify the account that is going to get an instance of the resource.
Prepare phase
To prepare:
-
Create a
preparephase with theSaveValueauthorization entitlement to the user's account. -
Use
createto create a new instance of theHelloAsset. -
Save the new resource in the user's account.
-
Inside the
transaction, stub out thepreparephase with the authorization entitlement:_10import HelloResource from 0x06_10_10transaction {_10prepare(acct: auth(SaveValue) &Account) {_10// TODO_10}_10} -
Use the
createHelloAssetfunction inHelloResourcetocreatean instance of the resource inside of theprepeareand move it into a constant:_10let newHello <- HelloResource.createHelloAsset()
You'll get an error for loss of resource, which is one of the best features of Cadence! The language prevents you from accidentally destroying a resource at the syntax level.
Storage paths
In Cadence Accounts, objects are stored in paths. Paths represent a file system for user accounts, where an object can be stored at any user-defined path. Usually, contracts will specify for the user where objects from that contract should be stored. This enables any code to know how to access these objects in a standard way.
Paths start with the character /, followed by the domain, the path separator /, and finally the identifier. The identifier must start with a letter and can only be followed by letters, numbers, or the underscore _. For example, the path /storage/test has the domain storage and the identifier test.
There are two valid domains: storage and public.
Paths in the storage domain have type StoragePath, and paths in the public domain have the type PublicPath. Both StoragePath and PublicPath are subtypes of Path.
Paths are not strings and do not have quotes around them.
Use the account reference with the SaveValue authorization entitlement to move the new resource into storage located in /storage/HelloAssetTutorial:
_10acct.storage.save(<-newHello, to: /storage/HelloAssetTutorial)
The first parameter in save is the object that is being stored, and the to parameter is the path that the object is being stored at. The path must be a storage path, so only the domain /storage/ is allowed in the to parameter.
Notice that the error for loss of resource has been resolved.
If there is already an object stored under the given path, the program aborts. Remember, the Cadence type system ensures that a resource can never be accidentally lost. When moving a resource to a field, into an array, into a dictionary, or into storage, there is the possibility that the location already contains a resource.
Cadence forces the developer to explicitly handle the case of an existing resource so that it is not accidentally lost through an overwrite.
It is also very important when choosing the name of your paths to pick an identifier that is very specific and unique to your project.
Currently, account storage paths are global, so there is a chance that projects could use the same storage paths, which could cause path conflicts! This could be a headache for you, so choose unique path names to avoid this problem.
Execute phase
Use the execute phase to log a message that the resource was successfully saved:
_10execute {_10 log("Saved Hello Resource to account.")_10}
We'll learn more realistic uses of this phase soon.
You should have something similar to:
_12import HelloResource from 0x06_12_12transaction {_12 prepare(acct: auth(SaveValue) &Account) {_12 let newHello <- HelloResource.createHelloAsset()_12 acct.storage.save(<-newHello, to: /storage/HelloAssetTutorial)_12 }_12_12 execute {_12 log("Saved Hello Resource to account.")_12 }_12}
This is our first transaction using the prepare phase!
The prepare phase is the only place that has access to the signing account, via account references (&Account).
Account references have access to many different methods that are used to interact with an account, such as to save a resource to the account's storage.
By not allowing the execute phase to access account storage and using entitlements, we can statically verify which assets and areas/paths of the signers' account a given transaction can modify.
Browser wallets and applications that submit transactions for users can use this to show what a transaction could alter, giving users information about transactions that wallets will be executing for them, and confidence that they aren't getting fed a malicious or dangerous transaction from an app or wallet.
To execute:
- Select account
0x06as the only signer. - Click the
Sendbutton to submit the transaction. You'll see in the log:_10"Saved Hello Resource to account." - Use
Sendto send the transaction again from account0x06You'll now get an error, because there's already a resource in/storage/HelloAssetTutorial:_10execution error code 1: [Error Code: 1101] error caused by: 1 error occurred:_10* transaction execute failed: [Error Code: 1101] cadence runtime error: Execution failed:_10error: failed to save object: path /storage/HelloAssetTutorial in account 0x0000000000000009 already stores an object_10--> 805f4e247a920635abf91969b95a63964dcba086bc364aedc552087334024656:19:8_10|_1019 | acct.storage.save(<-newHello, to: /storage/HelloAssetTutorial)_10| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Remove the line of code that saves
newHelloto storage.- Again, you'll get an error for
newHellothat saysloss of resource. This means that you are not handling the resource properly. Remember that if you ever see this error in any of your programs, it means there is a resource somewhere that is not being explicitly stored or destroyed. Add the line back before you forget!
- Again, you'll get an error for
Review storage
Now that you have executed the transaction, account 0x06 has the newly created HelloWorld.HelloAsset resource stored in its storage. You can verify this by clicking on account 0x06 on the bottom left. This opens a view of the different contracts and objects in the account.
The resource you created appears in Account Storage:
_49{_49 "value": [_49 {_49 "key": {_49 "value": "value",_49 "type": "String"_49 },_49 "value": {_49 "value": {_49 "id": "A.0000000000000006.HelloResource.HelloAsset",_49 "fields": [_49 {_49 "value": {_49 "value": "269380348805120",_49 "type": "UInt64"_49 },_49 "name": "uuid"_49 }_49 ]_49 },_49 "type": "Resource"_49 }_49 },_49 {_49 "key": {_49 "value": "type",_49 "type": "String"_49 },_49 "value": {_49 "value": "A.0000000000000006.HelloResource.HelloAsset",_49 "type": "String"_49 }_49 },_49 {_49 "key": {_49 "value": "path",_49 "type": "String"_49 },_49 "value": {_49 "value": {_49 "domain": "storage",_49 "identifier": "HelloAssetTutorial"_49 },_49 "type": "Path"_49 }_49 }_49 ],_49 "type": "Dictionary"_49}
You'll also see FlowToken objects and the HelloResource Contract.
Run the transaction from account 0x07 and compare the differences between the accounts.
Check for existing storage
In real applications, you need to check the location path you are storing in to make sure both cases are handled properly.
-
Update the authorization entitlement in the prepare phase to include
BorrowValue:_10prepare(acct: auth(BorrowValue, SaveValue) &Account) {_10// Existing code..._10} -
Add a
transaction-level (similar to contract-level or class-level) variable to store a resultString.- Similar to a class-level variable in other languages, these go at the top, inside the
transactionscope, but not inside anything else. They are accessible in both theprepareandexecutestatements of a transaction:
_10import HelloResource from 0x06_10_10transaction {_10var result: String_10// Other code..._10}- You'll get an error:
missing initialization of field 'result' in type 'Transaction'. not initialized - In transactions, variables at the
transactionlevel must be initialized in thepreparephase.
- Similar to a class-level variable in other languages, these go at the top, inside the
-
Initialize the
resultmessage and create a constant for the storage path:_10self.result = "Saved Hello Resource to account."_10let storagePath = /storage/HelloAssetTutorial
In Cadence, storage paths are a type. They are not Strings and are not enclosed by quotes.
One way to check whether or not a storage path has an object in it is to use the built-in storage.check function with the type and path. If the result is true, then there is an object in account storage that matches the type requested. If it's false, there is not.
A response of false does not mean the location is empty. If you ask for an apple and the location contains an orange, this function will return false.
This is not likely to occur because projects are encouraged to create storage and public paths that are very unique, but is theoretically possible if projects don't follow this best practice or if there is a malicious app that tries to store things in other projects' paths.
Depending on the needs of your app, you'll use this pattern to decide what to do in each case. For this example, we'll simply use it to change the log message if the storage is in use or create and save the HelloAsset if it is not.
-
Refactor your prepare statement to check and see if the storage path is in use. If it is, update the
resultmessage. Otherwise, create and save aHelloAsset:_10if acct.storage.check<@HelloResource.HelloAsset>(from: storagePath) {_10self.result = "Unable to save, resource already present."_10} else {_10let newHello <- HelloResource.createHelloAsset()_10acct.storage.save(<-newHello, to: storagePath)_10}- When you [
check] a resource, you must put the type of the resource to be borrowed inside the<>after the call toborrow, before the parentheses. Thefromparameter is the storage path to the object you are borrowing.
- When you [
-
Update the
login execute to useself.resultinstead of the hardcoded string:_10execute {_10log(self.result)_10}You should end up with something similar to:
_21import HelloResource from 0x06_21_21transaction {_21var result: String_21_21prepare(acct: auth(BorrowValue, SaveValue) &Account) {_21self.result = "Saved Hello Resource to account."_21let storagePath = /storage/HelloAssetTutorial_21_21if acct.storage.check<@HelloResource.HelloAsset>(from: storagePath) {_21self.result = "Unable to save, resource already present."_21} else {_21let newHello <- HelloResource.createHelloAsset()_21acct.storage.save(<-newHello, to: storagePath)_21}_21}_21_21execute {_21log(self.result)_21}_21} -
Use
Sendto send the transaction again, both with accounts that have and have not yet created and stored an instance ofHelloAsset.
Now you'll see an appropriate log whether or not a new resource was created and saved.
Loading a Hello transaction
The following shows you how to use a transaction to call the hello() method from the HelloAsset resource.
-
Open the transaction named
Load Hello, which is empty. -
Stub out a transaction that imports
HelloResourceand passes in an account reference with theBorrowValueauthorization entitlement, which looks something like this:- You just learned how to
borrowa reference to a resource. You could use anifstatement to handle the possibility that the resource isn't there, but if you want to simply terminate execution, a common practice is to combine apanicstatement with the nil-coalescing operator (??). - This operator executes the statement on the left side. If that is
nil, the right side is evaluated and returned. In this case, the return is irrelevant, because we're going to cause apanicand terminate execution.
- You just learned how to
-
Create a variable with a reference to the
HelloAssetresource stored in the user's account. Usepanicif this resource is not found:_10let helloAsset = acct.storage.borrow<&HelloResource.HelloAsset>(from: /storage/HelloAssetTutorial)_10?? panic("The signer does not have the HelloAsset resource stored at /storage/HelloAssetTutorial. Run the `Create Hello` Transaction to store the resource") -
Use
logto log the return from a call to thehello()function.
Borrowing a reference does not allow you to move or destroy a resource, but it does allow you to mutate data inside that resource via one of the resource's functions.
Your transaction should be similar to:
_10import HelloResource from 0x06_10_10transaction {_10 prepare(acct: auth(BorrowValue, LoadValue, SaveValue) &Account) {_10 let helloAsset = acct.storage.borrow<&HelloResource.HelloAsset>(from: /storage/HelloAssetTutorial)_10 ?? panic("The signer does not have the HelloAsset resource stored at /storage/HelloAssetTutorial. Run the `Create Hello` Transaction again to store the resource")_10_10 log(helloAsset.hello())_10 }_10}
In Cadence, we have the resources to leave very detailed error messages. Check out the error messages in the Non-Fungible Token Contract and Generic NFT Transfer transaction in the Flow NFT GitHub repo for examples of production error messages.
Test your transaction with several accounts to evaluate all possible cases.
Reviewing the resource contract
In this tutorial you learned how to create resources in Cadence. You implemented a smart contract that is accessible in all scopes. The smart contract has a resource declared that implemented a function called hello(), that returns the string "Hello, World!". It also declares a function that can create a resource.
Next, you implemented a transaction to create the resource and save it in the account calling it.
Finally, you used a transaction to borrow a reference to the HelloAsset resource from account storage and call the hello method
Now that you have completed the tutorial, you can:
- Instantiate a
resourcein a smart contract with thecreatekeyword. - Save, move, and load resources using the Account Storage API and the move operator (
<-). - Use
borrowto access and use a function in a resource. - Use the
preparephase of a transaction to load resources from account storage. - Set and use variables in both the
prepareandexecutephase. - Use the nil-coalescing operator (
??) topanicif a resource is not found.
Reference Solution
You are not saving time by skipping the reference implementation. You'll learn much faster by doing the tutorials as presented!
Reference solutions are functional, but may not be optimal.