Contract Updatability
Introduction​
A contract in Cadence is a collection of data (its state) and code (its functions) that lives in the contract storage area of an account. When a contract is updated, it is important to make sure that the changes introduced do not lead to runtime inconsistencies for already stored data. Cadence maintains this state consistency by validating the contracts and all their components before an update.
Validation Goals​
The contract update validation ensures that:
- Stored data doesn't change its meaning when a contract is updated.
- Decoding and using stored data does not lead to runtime crashes.
- For example, it is invalid to add a field because existing stored data won't have the new field.
- Loading the existing data will result in garbage/missing values for such fields.
- A static check of the access of the field would be valid, but the interpreter would crash when accessing the field, because the field has a missing/garbage value.
However, it does not ensure:
- Any program that imports the updated contract stays valid. e.g:
- Updated contract may remove an existing field or may change a function signature.
- Then any program that uses that field/function will get semantic errors.
Updating a Contract​
Changes to contracts can be introduced by adding new contracts, removing existing contracts, or updating existing contracts. However, some of these changes may lead to data inconsistencies as stated above.
Valid Changes​
- Adding a new contract is valid.
- Removing a contract/contract-interface that doesn't have enum declarations is valid.
- Updating a contract is valid, under the restrictions described in the below sections.
Invalid Changes​
- Removing a contract/contract-interface that contains enum declarations is not valid.
- Removing a contract allows adding a new contract with the same name.
- The new contract could potentially have enum declarations with the same names as in the old contract, but with different structures.
- This could change the meaning of the already stored values of those enum types.
A contract may consist of fields and other declarations such as composite types, functions, constructors, etc. When an existing contract is updated, all its inner declarations are also validated.
Contract Fields​
When a contract is deployed, the fields of the contract are stored in an account's contract storage. Changing the fields of a contract only changes the way the program treats the data, but does not change the already stored data itself, which could potentially result in runtime inconsistencies as mentioned in the previous section.
See the section about fields below for the possible updates that can be done to the fields, and the restrictions imposed on changing fields of a contract.
Nested Declarations​
Contracts can have nested composite type declarations such as structs, resources, interfaces, and enums. When a contract is updated, its nested declarations are checked, because:
- They can be used as type annotation for the fields of the same contract, directly or indirectly.
- Any third-party contract can import the types defined in this contract and use them as type annotations.
- Hence, changing the type definition is the same as changing the type annotation of such a field (which is also invalid, as described in the section about fields fields below).
Changes that can be done to the nested declarations, and the update restrictions are described in following sections:
Fields​
A field may belong to a contract, struct, resource, or interface.
Valid Changes:​
-
Removing a field is valid
_13// Existing contract_13_13pub contract Foo {_13pub var a: String_13pub var b: Int_13}_13_13_13// Updated contract_13_13pub contract Foo {_13pub var a: String_13}- It leaves data for the removed field unused at the storage, as it is no longer accessible.
- However, it does not cause any runtime crashes.
-
Changing the order of fields is valid.
_14// Existing contract_14_14pub contract Foo {_14pub var a: String_14pub var b: Int_14}_14_14_14// Updated contract_14_14pub contract Foo {_14pub var b: Int_14pub var a: String_14} -
Changing the access modifier of a field is valid.
_12// Existing contract_12_12pub contract Foo {_12pub var a: String_12}_12_12_12// Updated contract_12_12pub contract Foo {_12priv var a: String // access modifier changed to 'priv'_12}
Invalid Changes​
-
Adding a new field is not valid.
_13// Existing contract_13_13pub contract Foo {_13pub var a: String_13}_13_13_13// Updated contract_13_13pub contract Foo {_13pub var a: String_13pub var b: Int // Invalid new field_13}- Initializer of a contract only run once, when the contract is deployed for the first time. It does not rerun when the contract is updated. However it is still required to be present in the updated contract to satisfy type checks.
- Thus, the stored data won't have the new field, as the initializations for the newly added fields do not get executed.
- Decoding stored data will result in garbage or missing values for such fields.
-
Changing the type of existing field is not valid.
_12// Existing contract_12_12pub contract Foo {_12pub var a: String_12}_12_12_12// Updated contract_12_12pub contract Foo {_12pub var a: Int // Invalid type change_12}- In an already stored contract, the field
a
would have a value of typeString
. - Changing the type of the field
a
toInt
, would make the runtime read the already storedString
value as anInt
, which will result in deserialization errors. - Changing the field type to a subtype/supertype of the existing type is also not valid, as it would also
potentially cause issues while decoding/encoding.
- e.g: Changing an
Int64
field toInt8
- Stored field could have a numeric value624
, which exceeds the value space forInt8
. - However, this is a limitation in the current implementation, and the future versions of Cadence may support changing the type of field to a subtype, by providing means to migrate existing fields.
- e.g: Changing an
- In an already stored contract, the field
Structs, Resources and Interfaces​
Valid Changes:​
- Adding a new struct, resource, or interface is valid.
- Adding an interface conformance to a struct/resource is valid, since the stored data only
stores concrete type/value, but doesn't store the conformance info.
_10// Existing struct_10_10pub struct Foo {_10}_10_10_10// Upated struct_10_10pub struct Foo: T {_10}
- However, if adding a conformance also requires changing the existing structure (e.g: adding a new field that is enforced by the new conformance), then the other restrictions (such as restrictions on fields) may prevent performing such an update.
Invalid Changes:​
- Removing an existing declaration is not valid.
- Removing a declaration allows adding a new declaration with the same name, but with a different structure.
- Any program that uses that declaration would face inconsistencies in the stored data.
- Renaming a declaration is not valid. It can have the same effect as removing an existing declaration and adding a new one.
- Changing the type of declaration is not valid. i.e: Changing from a struct to interface, and vise versa.
_10// Existing struct_10_10pub struct Foo {_10}_10_10_10// Changed to a struct interface_10_10pub struct interface Foo { // Invalid type declaration change_10}
- Removing an interface conformance of a struct/resource is not valid.
This is because if there was a container of type_10// Existing struct_10_10pub struct Foo: T {_10}_10_10_10// Upated struct_10_10pub struct Foo {_10}
T
(e.g., an array[T]
or dictionary{R:T}
, etc.), or a field of typeT
, and if aFoo
value had been stored in that container or field, then that container value will become invalid, as it will now hold a value that does not conform toT
.
Updating Members​
Similar to contracts, these composite declarations: structs, resources, and interfaces also can have fields and other nested declarations as its member. Updating such a composite declaration would also include updating all of its members.
Below sections describes the restrictions imposed on updating the members of a struct, resource or an interface.
Enums​
Valid Changes:​
- Adding a new enum declaration is valid.
Invalid Changes:​
- Removing an existing enum declaration is invalid.
- Otherwise, it is possible to remove an existing enum and add a new enum declaration with the same name, but with a different structure.
- The new structure could potentially have incompatible changes (such as changed types, changed enum-cases, etc).
- Changing the name is invalid, as it is equivalent to removing an existing enum and adding a new one.
- Changing the raw type is invalid.
_14// Existing enum with `Int` raw type_14_14pub enum Color: Int {_14pub case RED_14pub case BLUE_14}_14_14_14// Updated enum with `UInt8` raw type_14_14pub enum Color: UInt8 { // Invalid change of raw type_14pub case RED_14pub case BLUE_14}
- When the enum value is stored, the raw value associated with the enum-case gets stored.
- If the type is changed, then deserializing could fail if the already stored values are not in the same value space as the updated type.
Updating Enum Cases​
Enums consist of enum-case declarations, and updating an enum may also include changing the enums cases as well. Enum cases are represented using their raw-value at the Cadence interpreter and runtime. Hence, any change that causes an enum-case to change its raw value is not permitted. Otherwise, a changed raw-value could cause an already stored enum value to have a different meaning than what it originally was (type confusion).
Valid Changes:​
- Adding an enum-case at the end of the existing enum-cases is valid.
_15// Existing enum_15_15pub enum Color: Int {_15pub case RED_15pub case BLUE_15}_15_15_15// Updated enum_15_15pub enum Color: Int {_15pub case RED_15pub case BLUE_15pub case GREEN // valid new enum-case at the bottom_15}
Invalid Changes​
-
Adding an enum-case at the top or in the middle of the existing enum-cases is invalid.
_15// Existing enum_15_15pub enum Color: Int {_15pub case RED_15pub case BLUE_15}_15_15_15// Updated enum_15_15pub enum Color: Int {_15pub case RED_15pub case GREEN // invalid new enum-case in the middle_15pub case BLUE_15} -
Changing the name of an enum-case is invalid.
_14// Existing enum_14_14pub enum Color: Int {_14pub case RED_14pub case BLUE_14}_14_14_14// Updated enum_14_14pub enum Color: Int {_14pub case RED_14pub case GREEN // invalid change of names_14}- Previously stored raw values for
Color.BLUE
now representsColor.GREEN
. i.e: The stored values have changed their meaning, and hence not a valid change. - Similarly, it is possible to add a new enum with the old name
BLUE
, which gets a new raw value. Then the same enum-caseColor.BLUE
may have used two raw-values at runtime, before and after the change, which is also invalid.
- Previously stored raw values for
-
Removing the enum case is invalid. Removing allows one to add and remove an enum-case which has the same effect as renaming.
_15// Existing enum_15_15pub enum Color: Int {_15pub case RED_15pub case BLUE_15}_15_15_15// Updated enum_15_15pub enum Color: Int {_15pub case RED_15_15// invalid removal of `case BLUE`_15} -
Changing the order of enum-cases is not permitted
_14// Existing enum_14_14pub enum Color: Int {_14pub case RED_14pub case BLUE_14}_14_14_14// Updated enum_14_14pub enum Color: UInt8 {_14pub case BLUE // invalid change of order_14pub case RED_14}- Raw value of an enum is implicit, and corresponds to the defined order.
- Changing the order of enum-cases has the same effect as changing the raw-value, which could cause storage inconsistencies and type-confusions as described earlier.
Functions​
Adding, changing, and deleting a function definition is always valid, as function definitions are never stored as data (function definitions are part of the code, but not data).
- Adding a function is valid.
- Deleting a function is valid.
- Changing a function signature (parameters, return types) is valid.
- Changing a function body is valid.
- Changing the access modifiers is valid.
However, changing a function type may or may not be valid, depending on where it is used: If a function type is used in the type annotation of a composite type field (direct or indirect), then changing the function type signature is the same as changing the type annotation of that field (which is invalid).
Constructors​
Similar to functions, constructors are also not stored. Hence, any changes to constructors are valid.
Imports​
A contract may import declarations (types, functions, variables, etc.) from other programs. These imported programs are already validated at the time of their deployment. Hence, there is no need for validating any declaration every time they are imported.
Cadence 1.0 Contract Updates​
For the upcoming network upgrade that will update the Cadence version to 1.0, all contracts will need to be upgraded to Cadence 1.0. Numerous changes have been made to the language, and many types and patterns that were previously valid are no longer permitted, or behave differently. In particulary, reference types work very differently after the introduction of entitlements, and restricted types have been replaced with the similar but different intersection types.
In order to allow contracts to be upgraded in anticipation of these changes, numerous migrations will be run on the network in order to transform data created in v0.42 of Cadence (the current version) into data that is compatible with version 1.0. The fact that these migrations are being run also allows for certain relaxations of the contract update rules outlined above. In general, all the previously described restrictions still apply to v0.42 -> v1.0 contract updates, with the exceptions enabled by the data migrations outlined below:
-
Composite (struct, resource, and contract) field types can be changed according to specific rules. Specifically, reference-typed fields (or fields whose types contain references, like a
Capability
) can be updated to have the appropriate entitlements, while restricted-typed fields can be updated to be the appropriate intersection types. The update validator that will run when scheduling a 1.0 contract upgrade will enforce that these types are correct, but those curious can read in depth about the update rules for these cases here. -
Given that type requirements have been removed in Cadence 1.0, the update rules have been relaxed to allow struct or resource types defined in contract interfaces to be converted to struct or resource interfaces. E.g. a contract interface originally written as:
_10access(all) contract interface C {_10access(all) resource R {}_10}can be updated to
_10access(all) contract interface C {_10access(all) resource interface R {}_10}