In January 2022 the Cadence team shared their thoughts on the Path to Stable Cadence (aka Cadence 1.0).
The team has come a long way since: We have released the Secure Cadence milestone, which introduced huge security improvements and enabled permissionless deployment on Flow Mainnet in summer 2022, while we continued working on the Cadence 1.0 milestone and other language features and improvements.
In this post we would like to update you on the progress that has been made, what you can expect when it is released, and how you can start preparing for the upgrade.
We want to give all builders on Flow time to learn about the new features and improvements that are coming with Cadence 1.0, and give them enough time to migrate and test their dapps.
We estimate that the earliest launch of Cadence 1.0 is Q1 2024. We will be posting more regular updates now that we are nearing completion, and once the release candidate is out we will start focusing on supporting the community with migrating their dapps.
We will launch Cadence 1.0 as soon as we can, but not before the developer community on Flow is ready for it.
We would like to thank all community members for their feedback and contributions – it would be much harder without you!
For each topic, we’ll go over the following:
-
Motivation: Why was this done, and why should you care?
-
Description: What is the improvement / feature, how will dapps benefit from it, and what needs to change as a result?
-
Adoption: How do I need to update my programs to take advantage of these improvements?
New features
View Functions added
Click here to read more
Motivation
View functions allow developers to improve the reliability and safety of their programs, and helps them to reason about the effects of their and the programs 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.
access(all)
resource Collection {
access(all)
var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
init () {
self.ownedNFTs <- {}
}
access(all)
fun getCount(): Int {
return self.ownedNFTs.length
}
/* ... rest of implementation ... */
}
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.
access(all)
view fun getCount(): Int {
// ^^^^ added
return self.ownedNFTs.length
}
Interface Inheritance added
Click here to read more
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.
access(all)
resource interface Receiver {
access(all)
fun deposit(_ something: @AnyResource)
}
access(all)
resource interface Vault: Receiver {
access(all)
fun withdraw(_ amount: Int): @Vault
}
Thus, anyone implementing the Vault
interface would also have to implement the Receiver
interface as well.
access(all)
resource MyVault: Vault {
// Required!
access(all)
fun withdraw(_ amount: Int): @Vault {}
// Required!
access(all)
fun deposit(_ something: @AnyResource) {}
}
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 very much understand that it is painful for developers to have their code get broken and require them to update it.
However, we believe that the pain is worth it, given the significant improvements that make Cadence development significantly more powerful and pleasant, and enabling developers to for write and deploy immutable contracts.
The improvements were intentionally bundled into one release to avoid breaking Cadence programs multiple times.
Conditions No Longer Allow State Changes
Click here to read more
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.
access(all)
resource interface Collection {
access(all)
fun getCount(): Int
access(all)
fun withdraw(id: UInt64): @NFT {
post {
getCount() == before(getCount()) - 1
}
}
/* ... rest of interface ... */
}
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
.
access(all)
view fun getCount(): Int
// ^^^^
Missing or Incorrect Argument Labels Get Reported
Click here to read more
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
:
access(all)
contract TestContract {
access(all)
struct TestStruct {
access(all)
let a: Int
access(all)
let b: String
init(first: Int, second: String) {
self.a = first
self.b = second
}
}
}
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.
// Script
import TestContract from 0x1
access(all)
fun main() {
TestContract.TestStruct(wrong: 123, "abc")
}
This now results in the following errors:
error: incorrect argument label
--> script:4:34
|
4 | TestContract.TestStruct(wrong: 123, "abc")
| ^^^^^ expected `first`, got `wrong`
error: missing argument label: `second`
--> script:4:46
|
4 | TestContract.TestStruct(wrong: 123, "abc")
| ^^^^^
Corrected program:
// Script
import TestContract from 0x1
access(all)
fun main() {
TestContract.TestStruct(first: 123, second: "abc")
}
We would like to thank community member justjoolz for reporting this bug.
Incorrect Operators in Reference Expressions Get Reported
Click here to read more
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!
.
let number = 1
let ref = &number as! &Int
This now results in the following error:
error: cannot infer type from reference expression: requires an explicit type annotation
--> test:3:17
|
3 | let ref = &number as! &Int
| ^
Corrected program:
let number = 1
let ref = &number as &Int
Alternatively, the same code can now also be written as follows:
let number = 1
let ref: &Int = &number
Naming Rules Got Tightened
Click here to read more
Motivation
Previously, Cadence allowed language keywords (e.g. continue
, for
, etc.) to be used as names. For example, the following program was allowed:
fun 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:
let break: Int = 0
// ^ 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.
let contract = signer.borrow<&MyContract>(name: "MyContract")
// ^ error: expected identifier after start of variable declaration, got keyword contract
After:
The variable is renamed to avoid the clash with the language keyword.
let myContract = signer.borrow<&MyContract>(name: "MyContract")
Result of toBigEndianBytes()
for U?Int(128|256)
Fixed
Click here to read more
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
let someNum: UInt128 = 123456789
let someBytes: [UInt8] = someNum.toBigEndianBytes()
// OLD behavior:
// someBytes = [7, 91, 205, 21]
// NEW behavior:
// 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.
let someNum: UInt128 = 123456789
let someBytes: [UInt8] = UInt(someNum).toBigEndianBytes()
// someBytes = [7, 91, 205, 21]
Syntax for Function Types Improved
Click here to read more
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.
Description and
examples
Function types are now expressed using the fun
keyword, just like expressions and declarations. This improves readability and makes function types more obvious.
For example, given the following function declaration:
fun foo(n: Int8, s: String): Int16 { /* ... */ }
The function foo
now has the type fun(Int8, String): Int16
.
The :
token is right-associative, so functions that return other functions can have their types written without nested parentheses:
fun curriedAdd(_ x: Int): fun(Int): Int {
return fun(_ y: Int): Int {
return x + y
}
}
// function `curriedAdd` has the type `fun(Int): fun(Int): Int`
To further bring the syntax for function types closer to the syntax of function declarations expressions, it is now possible to omit the return type, in which case the return type defaults to Void
.
fun logTwice(_ value: AnyStruct) { // Return type is implicitly `Void`
log(value)
log(value)
}
// The function types of these variables are equivalent
let logTwice1: fun(AnyStruct): Void = logTwice
let logTwice2: fun(AnyStruct) = logTwice
As a bonus consequence, it is now allowed for any type to be parenthesized. This is useful for complex type signatures, or for expressing optional functions:
// A function that returns an optional Int16
let optFun1: fun (Int8): Int16? =
fun (_: Int8): Int? { return nil }
// An optional function that returns an Int16
let optFun2: (fun (Int8): Int16)? = nil
This improvement was proposed in ****FLIP 43.
Adoption
Programs that use the old function type syntax need to be updated by replacing the surrounding parentheses of function types with the fun
keyword.
Before:
let baz: ((Int8, String): Int16) = foo
// ^ ^
// surrounding parentheses of function type
After:
let baz: fun (Int8, String): Int16 = foo
Entitlements and Safe Down-casting
Click here to read more
Motivation
Previously, Cadence’s main access-control mechanism, restricted reference types, has been a source of confusion and mistakes for contract developers.
Developers new to Cadence often were surprised and did not understand why access-restricted functions, like the withdraw
function of the fungible token Vault
resource type, were declared as pub
, making the function publicly accessible – access would later be restricted through a restricted type.
It was too easy to accidentally give out a Capability
with a more permissible type than intended, leading to security problems.
Additionally, because what fields and functions were available to a reference depended on what the type of the reference was, references could not be downcast, leading to ergonomic issues.
Description
Access control has improved significantly.
When giving another user a reference or Capability
to a value you own, the fields and functions that the user can access is determined by the type of the reference or Capability
.
Previously, access to a value of type T
, e.g. via a reference &T
, would give access to all fields and functions of T
. Access could be restricted, by using a restricted type. For example, a restricted reference &T{I}
could only access members that were pub
on I
. Since references could not be downcast, any members defined on T
but not on I
were unavailable to this reference, even if they were pub
.
Access control is now handled using a new feature called Entitlements, as originally proposed across **FLIP 54** and FLIP 94.
A reference can now be “entitled” to certain facets of an object. For example, the reference auth(Withdraw) &Vault
is entitled to access fields and functions of Vault
which require the Withdraw
entitlement.
Entitlements can be are declared using the new entitlement
syntax.
Members can be made to require entitlements using the access modifier syntax access(E)
, where E
is an entitlement that the user must posses.
For example:
entitlement Withdraw
access(Withdraw)
fun withdraw(amount: UFix64): @Vault
References can now always be down-casted, the standalone auth
modifier is not necessary anymore, and got removed.
For example, the reference &{Provider}
can now be downcast to &Vault
, so access control is now handled entirely through entitlements, rather than types.
To learn more, please refer to the documentation.
Adoption
The access modifiers of fields and functions need to be carefully audited and updated.
Fields and functions that have the pub
access modifier are now callable by anyone with any reference to that type. If access to the member should be restricted, the pub
access modifier needs to be replaced with an entitlement access modifier.
When creating a Capability
or a reference to a value, it must be carefully considered which entitlements are provided to the recipient of that Capability
or reference – only the entitlements which are necessary and not more should be include in the auth
modifier of the reference type.
Example
Before:
The Vault
resource was originally written like so:
access(all)
resource interface Provider {
access(all)
fun withdraw(amount: UFix64): @Vault {
// ...
}
}
access(all)
resource Vault: Provider, Receiver, Balance {
access(all)
fun withdraw(amount: UFix64): @Vault {
// ...
}
access(all)
fun deposit(from: @Vault) {
// ...
}
access(all)
var balance: UFix64
}
After:
The Vault
resource might now be written like this:
entitlement Withdraw
access(all)
resource interface Provider {
access(Withdraw)
fun withdraw(amount: UFix64): @Vault {
// ...
}
}
access(all)
resource Vault: Provider, Receiver, Balance {
access(Withdraw) // withdrawal requires permission
fun withdraw(amount: UFix64): @Vault {
// ...
}
access(all)
fun deposit(from: @Vault) {
// ...
}
access(all)
var balance: UFix64
}
Here, the access(Withdraw)
syntax means that a reference to Vault
must possess the Withdraw
entitlement in order to be allowed to call the withdraw
function, which can be given when a reference or Capability
is created by using a new syntax: auth(Withdraw) &Vault
.
This would allow developers to safely downcast &{Provider}
references to &Vault
references if they want to access functions like deposit
and balance
, without enabling them to call withdraw
.
pub
and priv
Access Modifiers Got Removed
Click here to read more
Motivation
With the previously mentioned entitlements feature, which uses access(E)
syntax to denote entitled access, the pub
, priv
and pub(set)
modifiers became the only access modifiers that did not use the access
syntax.
This made the syntax inconsistent, making it harder to read and understand programs.
In addition, pub
and priv
already had alternatives/equivalents: access(all)
and access(self)
.
Description
The pub
, priv
and pub(set)
access modifiers got removed from the language, in favor of their more explicit access(all)
and access(self)
equivalents (for pub
and priv
, respectively).
This makes access modifiers more uniform and better match the new entitlements syntax.
This improvement was originally proposed in FLIP 84.
Adoption
Users should replace any pub
modifiers with access(all)
, and any priv
modifiers with access(self)
.
Fields that were defined as pub(set)
will no longer be publicly assignable, and no access modifier now exists that replicates this old behavior. If the field should stay publicly assignable, a access(all)
setter function that updates the field needs to be added, and users have to switch to using it instead of directly assigning to the field.
Example
Before:
Types and members could be declared with pub
and priv
:
pub resource interface Collection {
pub fun getCount(): Int
priv fun myPrivateFunction()
pub(set) let settableInt: Int
/* ... rest of interface ... */
}
After:
The same behavior can be achieved with access(all)
and access(self)
access(all)
resource interface Collection {
access(all)
fun getCount(): Int
access(self)
fun myPrivateFunction()
access(all)
let settableInt: Int
access(all)
let setIntValue(_ i: Int): Int
/* ... rest of interface ... */
}
Restricted Types Got Replaced with Intersection Types
Click here to read more
Motivation
With the improvements to access control enabled by entitlements and safe down-casting, the restricted type feature is redundant.
Description
Restricted types have been removed. All types, including references, can now be down-casted, restricted types are no longer used for access control.
At the same time intersection types got introduced. Intersection types have the syntax {I1, I2, ... In}
, where all elements of the set of types (I1, I2, ... In
) are interface types. A value is part of the intersection type if it conforms to all the interfaces in the intersection type’s interface set. This functionality is equivalent to restricted types that restricted AnyStruct
and AnyResource.
This improvement was proposed in FLIP 85. To learn more, please consult the FLIP and documentation.
Adoption
Code that relies on the restriction behavior of restricted types can be safely changed to just use the concrete type directly, as entitlements will make this safe. For example, &Vault{Balance}
can be replaced with just &Vault
, as access to &Vault
only provides access to safe operations, like getting the balance – privileged operations, like withdrawal, need additional entitlements.
Code that uses AnyStruct
or AnyResource
explicitly as the restricted type, e.g. in a reference, &AnyResource{I}
, needs to remove the use of AnyStruct
/ AnyResource
. Code that already uses the syntax &{I}
can stay as-is.
Example
Before:
This function accepted a reference to a T
value, but restricted what functions were allowed to be called on it to those defined on the X
, Y
, and Z
interfaces.
access(all)
resource interface X {
access(all)
fun foo()
}
access(all)
resource interface Y {
access(all)
fun bar()
}
access(all)
resource interface Z {
access(all)
fun baz()
}
access(all)
resource T: X, Y, Z {
// implement interfaces
access(all)
fun qux() {
// ...
}
}
access(all)
fun exampleFun(param: &T{X, Y, Z}) {
// `param` cannot call `qux` here, because it is restricted to
// `X`, `Y` and `Z`.
}
After:
This function can be safely rewritten as:
access(all)
resource interface X {
access(all)
fun foo()
}
access(all)
resource interface Y {
access(all)
fun bar()
}
resource interface Z {
access(all)
fun baz()
}
access(all)
entitlement Q
access(all)
resource T: X, Y, Z {
// implement interfaces
access(Q)
fun qux() {
// ...
}
}
access(all)
fun exampleFun(param: &T) {
// `param` still cannot call `qux` here, because it lacks entitlement `Q`
}
Any functions on T
that the author of T
does not want users to be able to call publicly should be defined with entitlements, and thus will not be accessible to the unauthorized param
reference, like with qux
above.
Account Access Got Improved
Click here to read more
Motivation
Previously, access to accounts was granted wholesale: Users would sign a transaction, authorizing the code of the transaction to perform any kind of operation, for example, write to storage, but also add keys or contracts.
Users had to trust that a transaction would only perform supposed access, e.g. storage access to withdraw tokens, but still had to grant full access, which would allow the transaction to perform other operations.
Dapp developers who require users to sign transactions should be able to request the minimum amount of access to perform the intended operation, i.e. developers should be able to follow the principle of least privilege (PoLA).
This allows users to trust the transaction and Dapp.
Description
Previously, access to accounts was provided through the built-in types AuthAccount
and PublicAccount
: AuthAccount
provided full write access to an account, whereas PublicAccount
only provided read access.
With the introduction of entitlements, this access is now expressed using entitlements and references, and only a single Account
type is necessary. In addition, storage related functionality were moved to the field Account.storage
.
Access to administrative account operations, such as writing to storage, adding keys, or adding contracts, is now gated by both coarse grained entitlements (e.g. Storage
, which grants access to all storage related functions, and Keys
, which grants access to all key management functions), as well as fine-grained entitlements (e.g. SaveValue
to save a value to storage, or AddKey
to add a new key to the account).
Transactions can now request the particular entitlements necessary to perform the operations in the transaction.
This improvement was proposed in FLIP 92. To learn more, consult the FLIP and the documentation.
Adoption
Code that previously used PublicAccount
can simply be replaced with an unauthorized account reference, &Account.
Code that previously used AuthAccount
must be replaced with an authorized account reference. Depending on what functionality of the account is accessed, the appropriate entitlements have to be specified.
For example, if the save
function of AuthAccount
was used before, the function call must be replaced with storage.save
, and the SaveValue
or Storage
entitlement is required.
Example
Before:
The transactions wants to save a value to storage. It must request access to the whole account, even though it does not need access beyond writing to storage.
transaction {
prepare(signer: AuthAccount) {
signer.save("Test", to: /storage/test)
}
}
After:
The transaction requests the fine-grained account entitlement SaveValue
, which allows the transaction to call the save
function.
transaction {
prepare(signer: auth(SaveValue) &Account) {
signer.storage.save("Test", to: /storage/test)
}
}
If the transaction attempts to perform other operations, such as adding a new key, it is rejected:
transaction {
prepare(signer: auth(SaveValue) &Account) {
signer.storage.save("Test", to: /storage/test)
signer.keys.add(/* ... */)
// ^^^ Error: Cannot call function, requires `AddKey` or `Keys` entitlement
}
}
Deprecated Key Management API Got Removed
Click here to read more
Motivation
Cadence provides two key management APIs:
- The original, low-level API, which worked with RLP-encoded keys
- The improved, high-level API, which works with convenient data types like
PublicKey
,HashAlgorithm
, andSignatureAlgorithm
The improved API was introduced, as the original API was difficult to use and error-prone.
The original API was deprecated in early 2022.
Description
The original account key management API, got removed. Instead, the improved key management API should be used.
To learn more,
Adoption
Replace uses of the original account key management API functions with equivalents of the improved API:
Removed | Replacement |
---|---|
AuthAccount.addPublicKey | Account.keys.add |
AuthAccount.removePublicKey | Account.keys.revoke |
To learn more, please refer to the documentation.
Example
Before:
transaction(encodedPublicKey: [UInt8]) {
prepare(signer: AuthAccount) {
signer.addPublicKey(encodedPublicKey)
}
}
After:
transaction(publicKey: [UInt8]) {
prepare(signer: auth(Keys) &Account) {
signer.keys.add(
publicKey: PublicKey(
publicKey: publicKey,
signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
),
hashAlgorithm: HashAlgorithm.SHA3_256,
weight: 100.0
)
}
}
Resource Tracking for Optional Bindings Improved
Click here to read more
Motivation
Previously, resource tracking for optional bindings (”if-let statements”) was implemented incorrectly, leading to errors for valid code.
This required developers to add workarounds to their code.
Description
Resource tracking for optional bindings (”if-let statements”) was fixed.
For example, the following program used to be invalid, reporting a resource loss error for optR
:
resource R {}
fun asOpt(_ r: @R): @R? {
return <-r
}
fun test() {
let r <- create R()
let optR <- asOpt(<-r)
if let r2 <- optR {
destroy r2
}
}
This program is now considered valid.
Adoption
New programs do not need workarounds anymore, and can be written naturally.
Programs that previously resolved the incorrect resource loss error with a workaround, for example by invalidating the resource also in the else-branch or after the if-statement, are now invalid:
fun test() {
let r <- create R()
let optR <- asOpt(<-r)
if let r2 <- optR {
destroy r2
} else {
destroy optR
// unnecessary, but added to avoid error
}
}
The unnecessary workaround needs to be removed.
Definite Return Analysis Got Improved
Click here to read more
Motivation
Definite return analysis determines if a function always exits, in all possible execution paths, e.g. through a return
statement, or by calling a function that never returns, like panic
.
This analysis was incomplete and required developers to add workarounds to their code.
Description
The definite return analysis got significantly improved.
This means that the following program is now accepted: both branches of the if-statement exit, one using a return
statement, the other using a function that never returns, panic
:
resource R {}
fun mint(id: UInt64): @R {
if id > 100 {
return <- create R()
} else {
panic("bad id")
}
}
The program above was previously rejected with a “missing return statement” error – even though we can convince ourselves that the function will exit in both branches of the if-statement, and that any code after the if-statement is unreachable, the type checker was not able to detect that – it now does.
Adoption
New programs do not need workarounds anymore, and can be written naturally.
Programs that previously resolved the incorrect error with a workaround, for example by adding an additional exit at the end of the function, are now invalid:
resource R {}
fun mint(id: UInt64): @R {
if id > 100 {
return <- create R()
} else {
panic("bad id")
}
// unnecessary, but added to avoid error
panic("unreachable")
}
The improved type checker now detects and reports the unreachable code after the if-statement as an error:
error: unreachable statement
--> test.cdc:12:4
|
12 | panic("unreachable")
| ^^^^^^^^^^^^^^^^^^^^
exit status 1
To make the code valid, simply remove the unreachable code.
Semantics for Variables in For-Loop Statements Got Improved
Click here to read more
Motivation
Previously, the iteration variable of for-in
loops was re-assigned on each iteration.
Even though this is a common behavior in many programming languages, it is surprising behavior and a source of bugs.
The behavior was improved to the often assumed/expected behavior of a new iteration variable being introduced for each iteration, which reduces the likelihood for a bug.
Description
The behavior of for-in
loops improved, so that a new iteration variable is introduced for each iteration.
This change only affects few programs, as the behavior change is only noticeable if the program captures the iteration variable in a function value (closure).
This improvement was proposed in FLIP 13. To learn more, consult the FLIP and documentation.
Example
Previously, values
would result in [3, 3, 3]
, which might be surprising and unexpected. This is because x
was reassigned the current array element on each iteration, leading to each function in fs
returning the last element of the array.
// Capture the values of the array [1, 2, 3]
let fs: [((): Int)] = []
for x in [1, 2, 3] {
// Create a list of functions that return the array value
fs.append(fun (): Int {
return x
})
}
// Evaluate each function and gather all array values
let values: [Int] = []
for f in fs {
values.append(f())
}
References to Resource-Kinded Values Get Invalidated When the Referenced Values Are Moved
Click here to read more
Motivation
Previously, when a reference is taken to a resource, that reference remains valid even if the resource was moved, for example when created and moved into an account, or moved from one account into another.
In other words, references to resources stayed alive forever. This could be a potential safety foot-gun, where one could gain/give/retain unintended access to resources through references.
Description
References are now invalidated if the referenced resource is moved after the reference was taken. The reference is invalidated upon the first move, regardless of the origin and the destination.
This feature was proposed in FLIP 1043. To learn more, please consult the FLIP and documentation.
Example
// Create a resource.
let r <-create R()
// And take a reference.
let ref = &r as &R
// Then move the resource into an account.
account.save(<-r, to: /storage/r)
// Update the reference.
ref.id = 2
Old behavior:
// This will also update the referenced resource in the account.
ref.id = 2
The above operation will now result in a static error.
// Trying to update/access the reference will produce a static error:
// "invalid reference: referenced resource may have been moved or destroyed"
ref.id = 2
However, not all scenarios can be detected statically. e.g:
fun test(ref: &R) {
ref.id = 2
}
In the above function, it is not possible to determine whether the resource to which the reference was taken has been moved or not. Therefore, such cases are checked at run-time, and a run-time error will occur if the resource has been moved.
Adoption
Review code that uses references to resources, and check for cases where the referenced resource is moved. Such code may now be reported as invalid, or result in the program being aborted with an error when a reference to a moved resource is de-referenced.
Capability Controller API Replaced Existing Linking-based Capability API
Click here to read more
Motivation
Cadence encourages a capability-based security model. Capabilities are themselves a new concept that most Cadence programmers need to understand.
The existing API for capabilities was centered around “links” and “linking”, and the associated concepts of the public and private storage domains, led to capabilities being even confusing and awkward to use.
An better API is easier to understand and easier to work with.
Description
The existing linking-based capability API has been replaced by a more powerful and easier to use API based on the notion of Capability Controllers. The new API makes the creation of new and the revocation of existing capabilities simpler.
This improvement was proposed in FLIP 798. To learn more, consult the FLIP and the documentation.
Adoption
Existing uses of the linking-based capability API must be replaced with the new Capability Controller API.
Removed | Replacement |
---|---|
AuthAccount.link, with private path | Account.capabilities.storage.issue |
AuthAccount.link, with public path | Account.capabilities.storage.issue and Account.capabilities.publish |
AuthAccount.linkAccount | AuthAccount.capabilities.account.issue |
AuthAccount.unlink, with private path | - Get capability controller: Account.capabilities.storage/account.get |
- Revoke controller: Storage/AccountCapabilityController.delete | |
AuthAccount.unlink, with public path | - Get capability controller: Account.capabilities.storage/account.get |
- Revoke controller: Storage/AccountCapabilityController.delete
- Unpublish capability:
Account.capabilities.unpublish |
| AuthAccount/PublicAccount.getCapability | Account.capabilities.get |
| AuthAccount/PublicAccount.getCapability with followed borrow | Account.capabilities.borow |
| AuthAccount.getLinkTarget | N/A |
Example
Assume there is a Counter
resource which stores a count, and it implements an interface HasCount
which is used to allow read access to the count.
access(all)
resource interface HasCount {
access(all)
count: Int
}
access(all)
resource Counter {
access(all)
var count: Int
init(count: Int) {
self.count = count
}
}
Granting access, before:
transaction {
prepare(signer: AuthAccount) {
signer.save(
<-create Counter(count: 42),
to: /storage/counter
)
signer.link<&{HasCount}>(/public/hasCount, target: /storage/counter)
}
}
Granting access, after:
transaction {
prepare(signer: auth(Storage, Capabilities) &Account) {
signer.save(
<-create Counter(count: 42),
to: /storage/counter
)
let cap = signer.capabilities.storage.issue<&{HasCount}>(/storage/counter)
signer.capabilities.publish(cap, at: /public/hasCount)
}
}
Getting access, before:
access(all)
fun main(): Int {
let counterRef = getAccount(0x1)
.getCapabilities<&{HasCount}>(/public/hasCount)
.borrow()!
return counterRef.count
}
Getting access, after:
access(all)
fun main(): Int {
let counterRef = getAccount(0x1)
.capabilities.borrow<&{HasCount}>(/public/hasCount)!
return counterRef.count
}
External Mutation Got Improved
Click here to read more
Motivation
A previous version of Cadence (“Secure Cadence”), attempted to prevent a common safety foot-gun: Developers might use the let
keyword for a container-typed field, assuming it would be immutable.
Though Secure Cadence implements the Cadence mutability restrictions FLIP, it did not fully solve the problem / prevent the foot-gun and there were still ways to mutate such fields, so a proper solution was devised.
To learn more about the problem and motivation to solve it, please read the associated Vision document.
Description
The mutability of containers (updating a field of a composite value, key of a map, or index of an array) through references has changed:
When a field/element is accessed through a reference, a reference to the accessed inner object is returned, instead of the actual object. These returned references are unauthorized by default, and the author of the object (struct/resource/etc.) can control what operations are permitted on these returned references by using entitlements and entitlement mappings.
This improvement was proposed in two FLIPs:
To learn more, please consult the FLIPs and the documentation.
Adoption
As mentioned in the previous section, the most notable change in this improvement is that, when a field/element is accessed through a reference, a reference to the accessed inner object is returned, instead of the actual object. So developers would need to change their code to:
- Work with references, instead of the actual object, when accessing nested objects through a reference.
- Use proper entitlements for fields when they declare their ow
struct
andresource
types.
Example
Consider the below resource collection:
pub resource MasterCollection {
pub let kittyCollection: @Collection
pub let topshotCollection: @Collection
}
pub resource Collection {
pub(set) var id: String
access(all) var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
access(all) fun deposit(token: @NonFungibleToken.NFT) { ... }
}
Earlier, it was possible to mutate the inner collections, even if someone only had a reference to the MasterCollection
. e.g:
var masterCollectionRef: &MasterCollection = ...
// Directly updating the field
masterCollectionRef.kittyCollection.id = "NewID"
// Calling a mutating function
masterCollectionRef.kittyCollection.deposit(<-nft)
// Updating via the reference
let ownedNFTsRef = &masterCollectionRef.kittyCollection.ownedNFTs as &{UInt64: NonFungibleToken.NFT}
destroy ownedNFTsRef.insert(key: 1234, <-nft)
Once this change is introduced, the above collection can be re-written as below:
pub resource MasterCollection {
access(KittyCollectorMapping)
let kittyCollection: @Collection
access(TopshotCollectorMapping)
let topshotCollection: @Collection
}
pub resource Collection {
pub(set) var id: String
access(Identity)
var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
access(Insert)
fun deposit(token: @NonFungibleToken.NFT) { /* ... */ }
}
// Entitlements and mappings for `kittyCollection`
entitlement KittyCollector
entitlement mapping KittyCollectorMapping {
KittyCollector -> Insert
KittyCollector -> Remove
}
// Entitlements and mappings for `topshotCollection`
entitlement TopshotCollector
entitlement mapping TopshotCollectorMapping {
TopshotCollector -> Insert
TopshotCollector -> Remove
}
Then for a reference with no entitlements, none of the previously mentioned operations would be allowed:
var masterCollectionRef: &MasterCollection <- ...
// Error: Cannot update the field. Doesn't have sufficient entitlements.
masterCollectionRef.kittyCollection.id = "NewID"
// Error: Cannot directly update the dictionary. Doesn't have sufficient entitlements.
destroy masterCollectionRef.kittyCollection.ownedNFTs.insert(key: 1234, <-nft)
destroy masterCollectionRef.ownedNFTs.remove(key: 1234)
// Error: Cannot call mutating function. Doesn't have sufficient entitlements.
masterCollectionRef.kittyCollection.deposit(<-nft)
// Error: `masterCollectionRef.kittyCollection.ownedNFTs` is already a non-auth reference.
// Thus cannot update the dictionary. Doesn't have sufficient entitlements.
let ownedNFTsRef = &masterCollectionRef.kittyCollection.ownedNFTs as &{UInt64: NonFungibleToken.NFT}
destroy ownedNFTsRef.insert(key: 1234, <-nft)
To perform these operations on the reference, one would need to have obtained a reference with proper entitlements:
var masterCollectionRef: auth{KittyCollector} &MasterCollection <- ...
// Directly updating the field
masterCollectionRef.kittyCollection.id = "NewID"
// Updating the dictionary
destroy masterCollectionRef.kittyCollection.ownedNFTs.insert(key: 1234, <-nft)
destroy masterCollectionRef.kittyCollection.ownedNFTs.remove(key: 1234)
// Calling a mutating function
masterCollectionRef.kittyCollection.deposit(<-nft)
Nested Type Requirements Got Removed
Click here to read more
Motivation
Nested Type Requirements were a fairly advanced concept of the language.
Just like an interface could require a conforming type to provide a certain field or function, it could also have required the conforming type to provide a nested type.
This is an uncommon feature in other programming languages and hard to understand.
In addition, the value of nested type requirements was never realized. While it was previously used in the FT and NFT contracts, the addition of other language features like interface inheritance and events being emittable from interfaces, there were no more uses case compelling enough to justify a feature of this complexity.
Description
Contract interfaces can no longer declare any concrete types (struct
, resource
or enum
) in their declarations, as this would create a type requirement. event
declarations are still allowed, but these create an event
type limited to the scope of that contract interface; this event
is not inherited by any implementing contracts. Nested interface declarations are still permitted, however.
This improvement was proposed in FLIP 118.
Adoption
Any existing code that made use of the type requirements feature should be rewritten not to use this feature.
Event Definition And Emission In Interfaces
Click here to read more
Motivation
In order to support the removal of nested type requirements, events have been made define-able and emit-able from contract interfaces, as events were among the only common uses of the type requirements feature.
Description
Contract interfaces may now define event types, and these events can be emitted from function conditions and default implementations in those contract interfaces.
This improvement was proposed in FLIP 111.
Adoption
Contract interfaces that previously used type requirements to enforce that concrete contracts which implement the interface should also declare a specific event, should instead define and emit that event in the interface.
Example
Before:
A contract interface like the one below (SomeInterface
) used a type requirement to enforce that contracts which implement the interface also define a certain event (Foo
):
contract interface SomeInterface {
event Foo()
// ^^^^^^^^^^^ type requirement
fun inheritedFunction()
}
contract MyContract: SomeInterface {
event Foo()
// ^^^^^^^^^^^ type definition to satisfy type requirement
fun inheritedFunction() {
// ...
emit Foo()
}
}
After:
This can be rewritten to emit the event directly from the interface, so that any contracts that implement Intf
will always emit Foo
when inheritedFunction
is called:
contract interface Intf {
event Foo()
// ^^^^^^^^^^^ type definition
fun inheritedFunction() {
pre {
emit Foo()
}
}
}
Upcoming
The following changes may get included in the final release.
Details and decisions are still outstanding.
Force Destruction of Resources
Related
FT / NFT Standard changes (Still WIP)
Click here to read more
The Fungible Token and Non-Fungible Token Standard interfaces are being upgraded to allow for multiple tokens per contract, fix some issues with the original standards, and introduce other various improvements suggested by the community.
Original Proposal: http://forum.flow.com/t/streamlined-token-standards-proposal/3075
Fungible Token Changes PR: https://github.com/onflow/flow-ft/pull/77
NFT Changes PR: https://github.com/onflow/flow-nft/pull/126
The changes and the FLIP are still a work in progress, so the expected actions that developers will need to take are not yet finalized, but it will involve upgrading your token contracts with changes to events, function signatures, resource interface conformances, and other small changes. More examples coming soon.
Preview Build
Flow CLI preview build for Cadence 1.0 can be found at: https://github.com/onflow/flow-cli/releases/tag/v1.5.0-stable-cadence.1
To install, run the below command:
-
Linux/macOS
sudo sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v1.5.0-stable-cadence.1
-
Windows (in PowerShell):
iex "& { $(irm 'https://raw.githubusercontent.com/onflow/flow-cli/master/install.ps1') } v1.5.0-stable-cadence.1"
Note that this preview release does not contain all the completed features mentioned in the previous section. It only includes the features listed in the Cadence v1.5.0-stable-cadence.1 release.
As such, the following major improvements are not yet included in this preview build:
- "Account Access Got Improved"
- "External Mutation Got Improved"
- "Nested Type Requirements Got Removed"
- "Event Definition And Emission In Interfaces"
- Interface conformance improvements (FLIP#83, FLIP#134)
We are currently working on the next preview build which will also include these improvements, and will announce that build here as soon as it is ready. Stay tuned!