Cadence 1.0 Improvements & New Features
💫 New features
View Functions added (FLIP 1056)
💡 Motivation
View functions enable developers to enhance the reliability and safety of their programs, facilitating a clearer understanding of the impacts of their own code and that of others.
Developers can mark their functions as view
, which disallows the function from performing state changes. That also makes the intent of functions clear to other programmers, as it allows them to distinguish between functions that change state and ones that do not.
ℹ️ Description
Cadence has added support for annotating functions with the view
keyword, which enforces that no “mutating” operations occur inside the body of the function. The view
keyword is placed before the fun
keyword in a function declaration or function expression.
If a function has no view
annotation, it is considered “non-view”, and users should encounter no difference in behavior in these functions from what they are used to.
If a function does have a view
annotation, then the following mutating operations are not allowed:
- Writing to, modifying, or destroying any resources
- Writing to or modifying any references
- Assigning to or modifying any variables that cannot be determined to have been created locally inside of the
view
function in question. In particular, this means that captured and global variables cannot be written in these functions - Calling a non-
view
function
This feature was proposed in FLIP 1056. To learn more, please consult the FLIP and documentation.
🔄 Adoption
You can adopt view functions by adding the view
modifier to all functions that do not perform mutating operations.
✨ Example
Before:
The function getCount
of a hypothetical NFT collection returns the number of NFTs in the collection.
_17access(all)_17resource Collection {_17_17 access(all)_17 var ownedNFTs: @{UInt64: NonFungibleToken.NFT}_17_17 init () {_17 self.ownedNFTs <- {}_17 }_17_17 access(all)_17 fun getCount(): Int {_17 returnself.ownedNFTs.length_17 }_17_17 /* ... rest of implementation ... */_17}
After:
The function getCount
does not perform any state changes, it only reads the length of the collection and returns it. Therefore it can be marked as view.
_10 access(all)_10 view fun getCount(): Int {_10// ^^^^ addedreturnself.ownedNFTs.length_10 }
Interface Inheritance Added (FLIP 40)
💡 Motivation
Previously, interfaces could not inherit from other interfaces, which required developers to repeat code. Interface inheritance allows code abstraction and code reuse.
ℹ️ Description and ✨ Example
Interfaces can now inherit from other interfaces of the same kind. This makes it easier for developers to structure their conformances and reduces a lot of redundant code.
For example, suppose there are two resource interfaces Receiver
and Vault
, and suppose all implementations of the Vault
would also need to conform to the interface Receiver
.
Previously, there was no way to enforce this. Anyone who implements the Vault
would have to explicitly specify that their concrete type also implements the Receiver
. But it was not always guaranteed that all implementations would follow this informal agreement.
With interface inheritance, the Vault
interface can now inherit/conform to the Receiver
interface.
_11access(all)_11resource interface Receiver {_11 access(all)_11 fun deposit(_ something:@AnyResource)_11}_11_11access(all)_11resource interface Vault: Receiver {_11 access(all)_11 fun withdraw(_ amount: Int):@Vault_11}
Thus, anyone implementing the Vault
interface would also have to implement the Receiver
interface as well.
_10access(all)_10resource MyVault: Vault {_10 // Required!_10 access(all)_10 fun withdraw(_ amount: Int):@Vault {}_10 // Required!_10 access(all)_10 fun deposit(_ something:@AnyResource) {}_10}
This feature was proposed in FLIP 40. To learn more, please consult the FLIP and documentation.
⚡ Breaking Improvements
Many of the improvements of Cadence 1.0 are fundamentally changing how Cadence works and how it is used. However, that also means it is necessary to break existing code to release this version, which will guarantee stability (no more planned breaking changes) going forward.
Once Cadence 1.0 is live, breaking changes will simply not be acceptable.
So we have, and need to use, this last chance to fix and improve Cadence, so it can deliver on its promise of being a language that provides security and safety, while also providing composability and simplicity.
We fully recognize the frustration developers feel when updates break their code, necessitating revisions. Nonetheless, we are convinced that this inconvenience is justified by the substantial enhancements to Cadence development. These improvements not only make development more effective and enjoyable but also empower developers to write and deploy immutable contracts.
The improvements were intentionally bundled into one release to avoid breaking Cadence programs multiple times.
2024-04-24 Public Capability Acquisition No Longer Returns Optional Capabilities (FLIP 242)
Note This is a recent change that may not be reflected in emulated migrations or all tools yet. Likewise, this may affect existing staged contracts which do not conform to this new requirement. Please ensure your contracts are updated and re-staged, if necessary, to match this new requirement.
💡 Motivation
In the initial implementation of the new Capability Controller API (a change that is new in Cadence 1.0, proposed in FLIP 798), capabilities.get<T>
would return an optional capability, Capability<T>?
. When the no capability was published under the requested path, or when type argument T
was not a subtype of the runtime type of the capability published under the requested path, the capability would be nil
.
This was a source of confusion among developers, as previously account.getCapability<T>
did not return an optional capability, but rather one that would simply fail capability.borrow
if the capability was invalid.
It was concluded that this new behaviour was not ideal, and that there a benefit to an invalid Capability not being nil
, even if it is not borrowable. A nil
capability lacked information that was previously available with an invalid capability - primarily the type and address of the capability. Developers may have wanted to make use of this information, and react to the capability being invalid, as opposed to an uninformative nil
value and encountering a panic scenario.
ℹ️ Description
The capabilities.get<T>
function now returns an invalid capability when no capability is published under the requested path, or when the type argument T
is not a subtype of the runtime type of the capability published under the requested path.
This capability has the following properties:
- Always return
false
whenCapability<T>.check
is called. - Always return
nil
whenCapability<T>.borrow
is called. - Have an ID of
0
. - Have a runtime type that is the same as the type requested in the type argument of
capabilities.get<T>
.
🔄 Adoption
If you have not updated your code to Cadence 1.0 yet, you will need to follow the same guidelines for updating to the Capability Controller API as you would have before, but will need to handle the new invalid capability type instead of an optional capability.
If you have already updated your code to use capabilities.get<T>
, and are handling the capability as an optional type, you may need to update your code to handle the new non-optional invalid capability type instead.
✨ Example
Before:
_10let capability = account.capabilities.get<&MyNFT.Collection>(/public/NFTCollection)_10if capability == nil {_10 // Handle the case where the capability is nil_10}
After:
_10let capability = account.capabilities.get<&MyNFT.Collection>(/public/NFTCollection)_10if !capability.check() {_10 // Handle the case where the capability is invalid_10}
2024-04-23 Matching Access Modifiers for Interface Implementation Members are now Required (FLIP 262)
Note This is a recent change that may not be reflected in emulated migrations or all tools yet. Likewise, this may affect existing staged contracts which do not conform to this new requirement. Please ensure your contracts are updated and re-staged, if necessary, to match this new requirement.
💡 Motivation
Previously, the access modifier of a member in a type conforming to / implementing an interface could not be more restrictive than the access modifier of the member in the interface. That meant an implementation may have choosen to use a more permissive access modifier than the interface.
This may have been surprising to developers, as they may have assumed that the access modifier of the member
in the interface was a requirement / maximum, not just a minimum, especially when using
a non-public / non-entitled access modifier (e.g. access(contract)
, access(account)
).
Requiring access modifiers of members in the implementation to match the access modifiers of members given in the interface, helps avoid confusion and potential footguns.
ℹ️ Description
If an interface member has an access modifier, a composite type that conforms to it / implements the interface must use exactly the same access modifier.
🔄 Adoption
Update the access modifiers of members in composite types that conform to / implement interfaces if they do not match the access modifiers of the members in the interface.
✨ Example
Before:
_11access(all)_11resource interface I {_11 access(account)_11 fun foo()_11}_11_11access(all)_11resource R: I {_11 access(all)_11 fun foo() {}_11}
After:
_11access(all)_11resource interface I {_11 access(account)_11 fun foo()_11}_11_11access(all)_11resource R: I {_11 access(account)_11 fun foo() {}_11}
Conditions No Longer Allow State Changes (FLIP 1056)
💡 Motivation
In the current version of Cadence, pre-conditions and post-conditions may perform state changes, e.g. by calling a function that performs a mutation. This may result in unexpected behavior, which might lead to bugs.
To make conditions predictable, they are no longer allowed to perform state changes.
ℹ️ Description
Pre-conditions and post-conditions are now considered view
contexts, meaning that any operations that would be prevented inside of a view
function are also not permitted in a pre-condition or post-condition.
This is to prevent underhanded code wherein a user modifies global or contract state inside of a condition, where they are meant to simply be asserting properties of that state.
In particular, since only expressions were permitted inside conditions already, this means that if users wish to call any functions in conditions, these functions must now be made view
functions.
This improvement was proposed in FLIP 1056. To learn more, please consult the FLIP and documentation.
🔄 Adoption
Conditions which perform mutations will now result in the error “Impure operation performed in view context”. Adjust the code in the condition so it does not perform mutations.
The condition may be considered mutating, because it calls a mutating, i.e. non-view
function. It might be possible to mark the called function as view
, and the body of the function may need to get updated in turn.
✨ Example
Before:
The function withdraw
of a hypothetical NFT collection interface allows the withdrawal of an NFT with a specific ID. In its post-condition, the function states that at the end of the function, the collection should have exactly one fewer item than at the beginning of the function.
_15access(all)_15resource interface Collection {_15_15 access(all)_15 fun getCount(): Int_15_15 access(all)_15 fun withdraw(id: UInt64):@NFT {_15 post {_15 getCount() == before(getCount()) - 1_15 }_15 }_15_15 /* ... rest of interface ... */_15}
After:
The calls to getCount
in the post-condition are not allowed and result in the error “Impure operation performed in view context”, because the getCount
function is considered a mutating function, as it does not have the view
modifier.
Here, as the getCount
function only performs a read-only operation and does not change any state, it can be marked as view
.
_10 access(all)_10 view fun getCount(): Int_10// ^^^^
Missing or Incorrect Argument Labels Get Reported
💡 Motivation
Previously, missing or incorrect argument labels of function calls were not reported. This had the potential to confuse developers or readers of programs, and could potentially lead to bugs.
ℹ️ Description
Function calls with missing argument labels are now reported with the error message “missing argument label”, and function calls with incorrect argument labels are now reported with the error message “incorrect argument label”.
🔄 Adoption
- Function calls with missing argument labels should be updated to include the required argument labels.
- Function calls with incorrect argument labels should be fixed by providing the correct argument labels.
✨ Example
Contract TestContract
deployed at address 0x1
:
_18access(all)_18contract TestContract {_18_18 access(all)_18 structTestStruct {_18_18 access(all)_18 let a: Int_18_18 access(all)_18 let b: String_18_18 init(first: Int, second: String) {_18 self.a = first_18 self.b = second_18 }_18 }_18}
Incorrect program:
The initializer of TestContract.TestStruct
expects the argument labels first
and second
.
However, the call of the initializer provides the incorrect argument label wrong
for the first argument, and is missing the label for the second argument.
_10// Script_10import TestContract from 0x1_10_10access(all)_10fun main() {_10 TestContract.TestStruct(wrong: 123, "abc")_10}
This now results in the following errors:
_11error: incorrect argument label_11 --> script:4:34_11 |_11 4 | TestContract.TestStruct(wrong: 123, "abc")_11 | ^^^^^ expected `first`, got `wrong`_11_11error: missing argument label: `second`_11 --> script:4:46_11 |_11 4 | TestContract.TestStruct(wrong: 123, "abc")_11 | ^^^^^
Corrected program:
_10// Script_10import TestContract from 0x1_10_10access(all)_10fun main() {_10 TestContract.TestStruct(first: 123, second: "abc")_10}
We would like to thank community member @justjoolz for reporting this bug.
Incorrect Operators In Reference Expressions Get Reported (FLIP 941)
💡 Motivation
Previously, incorrect operators in reference expressions were not reported.
This had the potential to confuse developers or readers of programs, and could potentially lead to bugs.
ℹ️ Description
The syntax for reference expressions is &v as &T
, which represents taking a reference to value v
as type T
.
Reference expressions that used other operators, such as as?
and as!
, e.g. &v as! &T
, were incorrect and were previously not reported as an error.
The syntax for reference expressions improved to just &v
. The type of the resulting reference must still be provided explicitly.
If the type is not explicitly provided, the error “cannot infer type from reference expression: requires an explicit type annotation” is reported.
For example, existing expressions like &v as &T
provide an explicit type, as they statically assert the type using as &T
. Such expressions thus keep working and do not have to be changed.
Another way to provide the type for the reference is by explicitly typing the target of the expression, for example, in a variable declaration, e.g. via let ref: &T = &v
.
This improvement was proposed in FLIP 941. To learn more, please consult the FLIP and documentation.
🔄 Adoption
Reference expressions which use an operator other than as
need to be changed to use the as
operator.
In cases where the type is already explicit, the static type assertion (as &T
) can be removed.
✨ Example
Incorrect program:
The reference expression uses the incorrect operator as!
.
_10let number = 1_10let ref = &number as! &Int
This now results in the following error:
_10error: cannot infer type from reference expression: requires an explicit type annotation_10 --> test:3:17_10 |_103 |let ref = &number as! &Int_10 | ^
Corrected program:
_10let number = 1_10let ref = &number as &Int
Alternatively, the same code can now also be written as follows:
_10let number = 1_10let ref: &Int = &number
Tightening Of Naming Rules
💡 Motivation
Previously, Cadence allowed language keywords (e.g. continue
, for
, etc.) to be used as names. For example, the following program was allowed:
_10fun continue(import: Int, break: String) { ... }
This had the potential to confuse developers or readers of programs, and could potentially lead to bugs.
ℹ️ Description
Most language keywords are no longer allowed to be used as names. Some keywords are still allowed to be used as names, as they have limited significance within the language. These allowed keywords are as follows:
from
: only used in import statementsimport foo from ...
account
: used in access modifiersaccess(account) let ...
all
: used in access modifieraccess(all) let ...
view
: used as modifier for function declarations and expressionsview fun foo()...
, letf = view fun () ...
Any other keywords will raise an error during parsing, such as:
_10let break: Int = 0_10// ^ error: expected identifier after start of variable declaration, got keyword break
🔄 Adoption
Names which use language keywords must be renamed.
✨ Example
Before: A variable is named after a language keyword.
_10let contract = signer.borrow<&MyContract>(name: "MyContract")_10// ^ error: expected identifier after start of variable declaration, got keyword contract
After: The variable is renamed to avoid the clash with the language keyword.
_10let myContract = signer.borrow<&MyContract>(name: "MyContract")
Result of toBigEndianBytes()
for U?Int(128|256)
Fixed
💡 Motivation
Previously, the implementation of .toBigEndianBytes()
was incorrect for the large integer types Int128
, Int256
, UInt128
, and UInt256
.
This had the potential to confuse developers or readers of programs, and could potentially lead to bugs.
ℹ️ Description
Calling the toBigEndianBytes
function on smaller sized integer types returns the exact number of bytes that fit into the type, left-padded with zeros. For instance, Int64(1).toBigEndianBytes()
returns an array of 8 bytes, as the size of Int64
is 64 bits, 8 bytes.
Previously, the toBigEndianBytes
function erroneously returned variable-length byte arrays without padding for the large integer types Int128
, Int256
, UInt128
, and UInt256
. This was inconsistent with the smaller fixed-size numeric types, such as Int8
, and Int32
.
To fix this inconsistency, Int128
and UInt128
now always return arrays of 16 bytes, while Int256
and UInt256
return 32 bytes.
✨ Example
_10let someNum: UInt128 = 123456789_10let someBytes: [UInt8] = someNum.toBigEndianBytes()_10// OLD behavior;_10// someBytes = [7, 91, 205, 21]_10// NEW behavior:_10// someBytes = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 91, 205, 21]
🔄 Adoption
Programs that use toBigEndianBytes
directly, or indirectly by depending on other programs, should be checked for how the result of the function is used. It might be necessary to adjust the code to restore existing behavior.
If a program relied on the previous behavior of truncating the leading zeros, then the old behavior can be recovered by first converting to a variable-length type, Int
or UInt
, as the toBigEndianBytes
function retains the variable-length byte representations, i.e. the result has no padding bytes.
_10let someNum: UInt128 = 123456789_10let someBytes: [UInt8] = UInt(someNum).toBigEndianBytes()_10// someBytes = [7, 91, 205, 21]
Syntax for Function Types Improved (FLIP 43)
💡 Motivation
Previously, function types were expressed using a different syntax from function declarations or expressions. The previous syntax was unintuitive for developers, making it hard to write and read code that used function types.