Another Update on Stable Cadence

In January 2022 the Cadence team shared their thoughts on the path to “Stable Cadence”. We have come a long way since: We have released the Secure Cadence milestone, which enabled permissionless deployment, while we continued working on the Stable Cadence milestone and other language features and improvements.

In this post we would like to update everyone on the progress that has been made with the amazing Flow development community on the Stable Cadence milestone so far, what you can expect when it is released around mid-next year, and how you can start preparing for it.

We would also like to thank all community members that are active and contribute – it would be much harder without you!


:checkered_flag: Ready/Merged

View Functions / Function Calls in Conditions

Click here to read more

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 the declaration.

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 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
  • Emitting an event
  • Calling any non-view function

Additionally, function preconditions 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 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.

Missing or Incorrect Argument Labels Not Reported

Click here to read more

Function calls with missing or incorrect argument labels were previously not reported as an error.
Such incorrect function calls are now an error and should be fixed by providing the expected argument labels.

For example:

// Contract "TestContract" deployed at address 0x1

pub contract TestContract {
    pub struct TestStruct {
        pub let a: Int
        pub 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

pub 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

pub fun main() {
    TestContract.TestStruct(first: 123, second: "abc")
}

We would like to thank community member justjoolz for reporting this bug.

Incorrect Operator in Reference Expressions Not Reported

Click here to read more

The syntax for reference expressions is &v as &T, which represents taking a reference to value v as type T.

Reference expressions with other incorrect operators such as as? and as! were previously not reported as an error.
Such incorrect reference expressions are now an error and should be fixed by using the as operator.

For 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

Identifiers cannot be keywords in most cases

Click here to read more

https://github.com/onflow/cadence/pull/1937

Previously, we allowed keywords (e.g. continue, for, etc.) to be used in identifier position. For example, the following program was allowed by the parser.

pub fun continue(import: Int, break: String) {...}

This led to a lot of ambiguities, so the consensus was to disallow most keywords from being used as names. After a careful review of the language grammar, we currently designate the following keywords as “soft”, meaning that they’re still allowed to be used as identifiers due to having limited significance within the language. These allowed keywords are as follows:

  • from: only used in import statements import foo from ...
  • account: used in access modifiers access(account) let ...
  • set: used in access modifier pub(set) var globalMutVar = ...
  • all: used in access modifier access(all) let ...

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

Migrating contracts to Stable Cadence is a matter of renaming any identifiers that clash with reserved keywords. For instance, let contractlet nftContract, etc.

Result of toBigEndianBytes() for U?Int(128|256) changed

Click here to read more

https://github.com/onflow/cadence/pull/1917

The previous implementation of .toBigEndianBytes() was incorrect for the large integer types:

  • Int128
  • Int256
  • UInt128
  • UInt256

Calling .toBigEndianBytes() on smaller sized integer types usually returns the exact number of bytes that fit into the parent type, left-padded with zeros. For instance, (x: Int64).toBigEndianBytes() returns an array of 8 bytes. These larger types erroneously returned variable-length byte arrays without padding, enough to store their highest set bit. 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.

let someNum: UInt128 = 123456789
let someBytes: [UInt8] = someNum.toBigEndianBytes()
// old behaviour:
// someBytes = [7, 91, 205, 21]
// new behaviour:
// someBytes = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 91, 205, 21] 

If your contract relied on the previous behaviour of truncating the leading zeros, the arbitrary-precision Int and UInt types retain variable-length byte representations. The old behaviour can be recovered by first converting to a variable-length type, for example:

let someNum: UInt128 = 123456789
let someBytes: [UInt8] = UInt(someNum).toBigEndianBytes()
// someBytes = [7, 91, 205, 21]

Deprecated key management API got removed

Click here to read more

https://github.com/onflow/cadence/pull/1842

The first account key management API, which was deprecated last year, was removed. Instead, the new key management API should be used:

Removed Replacement
AuthAccount.addPublicKey AuthAccount.keys.add
AuthAcount.removePublicKey AuthAccount.keys.revoke

For more information, please refer to the documentation.

Resource tracking for optional binding

Click here to read more

https://github.com/onflow/cadence/pull/2044

Resource tracking for optional bindings (”if-let statements”) was implemented incorrectly and fixed. This may affect existing code that relied on the incorrect behaviour.

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.

However, programs that previously resolved the incorrect resource loss error, 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
    }
}

Improved definite return analysis

Click here to read more

https://github.com/onflow/cadence/pull/2090

Definite return analysis determines if a function exited, e.g. through a return statement, or by calling a function that never returns, like panic.

This analysis was improved in Stable Cadence.

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")
    }
}

However, this also means that some programs that were previously accepted are now being rejected.

For example, 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.

As a workaround, a developer might have previously added an additional exit at the end of the function:

resource R {}

fun mint(id: UInt64): @R {
    if id > 100 {
        return <- create R()
    } else {
        panic("bad id")
    }

    // added to avoid "missing return statement"
    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.


:compass: Upcoming

The following changes may get included in Stable Cadence. Details and decisions are still outstanding.

The Capability API may get replaced by an improved one

Click here to read more

A FLIP proposes replacing the existing capability API with one which is more powerful and easier to use. The private storage domain will be removed, creating new capabilities will be simpler, revocation of capabilities will be more straightforward. This will not change how capabilities are consumed/used, only how they are created. Any code calling AuthAccount::(un)link will need to change.

References to resource-kinded values may get invalidated when the referenced value is transferred

Click here to read more

Previously, when a reference is taken to a resource, that reference remains valid even if the resource was transferred. In other words, references stay alive forever. This could be a potential foot-gun, where one could gain/give/retain unintended access to resources through references.

With this change, references are invalidated if the underlying resource is transferred after the reference was taken. The reference is invalidated upon the first transfer, regardless of the origin and the destination.

e.g:

// Create a resource.
let r <-create R()

// And take a reference.
let ref = &r as &R

// Then transfer the resource into an account.
account.save(<-r, to: /storage/r)

// Update the reference.
ref.id = 2

Old behaviour:

// This will also update the referenced resource in the account.
ref.id = 2

New behaviour:

// 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:

pub fun test(ref: &R) {
    ref.id = 2
}

In 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.

Nested type requirements may get limited to just events

Click here to read more

Nested Type Requirements are currently supported for all kinds of composite types.

A future proposal may limit support to just events.

Nested Type Requirements are a fairly advanced concept. Just like an interface may
require a conforming type to provide a certain field or function, it may also require the conforming type to provide a nested type. This is uncommon in other programming languages and hard to understand. In addition, the value of nested type requirements was never realized.

Language support for preventing re-entrancy attacks may get introduced

Click here to read more

A future proposal may introduce additional language features and possibly change existing language semantics to allow developers to automatically or at least more easily prevent re-entrancy attacks.

Restrictions to external mutation may get added

Click here to read more

It is currently possible to mutate fields with mutable values from outside the containing type.

A future proposal may restrict this.

Semantics for variables in for-loop statements may change

Click here to read more

https://github.com/onflow/flips/pull/13

A FLIP proposes to change the behaviour of variables in for-loop statements. This will remove an often surprising behaviour from the language and reduce the likelihood of bugs. This change will only affect few programs, as the behaviour change is only noticeable if the program captures the for-loop statement variables in a function value (closure).

// 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())
}

Previously, values would result in [3, 3, 3], which might be surprising and unexpected. This is because currently x is reassigned the current array element on each iteration, leading to each function in fs returning the last element of the array.

If the proposed change gets approved and implemented, values will result in [1, 2, 3], which is likely what the author of the program expected.


:arrow_right: 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.

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.

7 Likes