Native upgradability#

The goal of this tutorial is to deploy a contract-version1, upgrade its logic, and migrate its state to contract-version2.

The contract-version1 includes an upgrade function and the contract-version2 includes a migration function. These functions are explored in the next two sections.

The migration function#

When deploying contract-version1, you might not know the exact logic of the contract-version2 yet. Contract-version2 might be a smart contract that fixes a security bug in your smart contract code which will be discovered at some point in the future. For that reason, it is good to have the option to call a migration function in contract-version2 when executing the upgrade mechanism in contract-version1. This allows you to add additional execution logic when upgrading the smart contract.

A migration function could have a variety of different purposes such as:

  • Log some events.

  • Change values in the state.

  • Change the shape of the smart contract state.

Note

There is no universal migration function and it is not necessary for all upgrades to call a migration function. In the example smart contract, a migration function is provided that should be suitable for most use cases. In more detail, the example migration function focuses on how to change the shape of your smart contract state since this bullet point from above is non-trivial.

Contract-version1 allows you to call a migration function as part of the upgrade. The migration function in contract-version2 changes the shape of the state from contract-version1 to contract-version2.

The upgrade function#

A native smart contract upgrade can be triggered on the host as seen in the code snippet below:

#[receive(
   ...
   low_level
)]
fn contract_name(
   ...
   host: &mut LowLevelHost,
   ...
)  ... {
   ...
   let result = host.upgrade(moduleReference);
   ...
}

The upgrade functions in the example smart contracts are marked as low_level. This is necessary since the default (high_level) mutable functions store the state of the contract at the end of execution. This conflicts with migration since the shape of the state might be changed by the migration function. If the state is then written by the default (high_level) upgrade function, it would overwrite the state stored by the migration function.

Greater control is given to the smart contract developer when using low_level functions (e.g., additional state manipulation capabilities are exposed which are unavailable when using high_level functions). These additional state manipulation features are necessary to change the shape of the state during the migration but they bear the risk that the state becomes corrupted if the migration function has some coding bugs. Make sure you test your upgrade thoroughly (with integration tests as well as manual tests on testnet) and ensure your state is migrated as intended before doing the same smart contract upgrade on mainnet.

Note

To write smart contract code safely and to avoid accidentally corrupting your state, you shouldn’t use low-level functions unless strictly necessary (e.g., for upgrade/migration functions). Smart contract functions are by default high_level (no need to add this attribute explicitly).

Note

You can also write a very simple upgrade mechanism using a high_level function with no migration function at all. This upgrade mechanism requires that the shape of the smart contract state is the same in contract-version1 and contract-version2. This means, the State struct in contract-version1 and contract-version2 has to be identical.

#[receive(
   contract = "contract_name",
   name = "upgrade",
   error = "ContractError",
   mutable
)]
fn contract_upgrade(
   _ctx: &ReceiveContext,
   host: &mut Host<State>,
) -> ContractResult<()> {
   // TODO: Include some authorization checks.

   // Trigger the upgrade.
   host.upgrade(params.module)?;

   // No `migration` function.

   Ok(())
}

The upgrade function in contract-version1 can be invoked with the following JSON file (input parameter):

{
   "migrate": {
      "Some": [
         [
         "<EntryPointNameOfMigrationFunction>",
         "<InputParametersOfMigrationFunction>"
         ]
      ]
   },
   "module": "<ModuleReferenceContractVersion2>"
}

The section executing an upgrade will give examples of how to set the input parameter for the upgrade function.

Note

If the module reference should be upgraded without invoking a migration function, use the JSON file (input parameter) below instead:

{
   "migrate": {
      "None": [
      ]
   },
   "module": "<ModuleReferenceContractVersion2>"
}

Warning

Since more responsibility is given to the smart contract developer, smart contract upgrade/migration functions should be coded and executed by experts that know about the underlying risks. You are about to become such an expert.

You now have the required knowledge to execute the upgrade mechanism on testnet in the next section.

Executing an upgrade#

Clone the smart contract upgrade example.

$git clone --recurse-submodules [email protected]:Concordium/concordium-rust-smart-contracts.git

Navigate to the contract-version1 example folder:

$cd ./examples/smart-contract-upgrade/contract-version1

Compile the smart contract to a Wasm module which you will deploy to testnet in the next step.

$cargo concordium build -e --out smart_contract_upgrade.wasm.v1
Compile contract version 1

Note

Comprehensive instructions on how to download and set up cargo-concordium can be found in Setup the development environment.

This command will create the smart_contract_upgrade.wasm.v1 file in your current folder. Deploy the smart contract module to testnet with the command:

$concordium-client module deploy ./smart_contract_upgrade.wasm.v1 --sender <YourAccountAddress> --grpc-port 20000 --grpc-ip node.testnet.concordium.com
Deploy contract version 1

Note

Comprehensive instructions on how to download and set up concordium-client and how to import an account that you can use for the placeholder YourAccountAddress above can be found in Setup the development environment.

Since other users will read this tutorial, you will likely get an error that the module above is already deployed. If the module is already deployed or not deployed, it does not make a difference because in both cases the console output will print the moduleReference that you need for the next step.

Initialize a contract-version1 smart contract instance with the command:

$concordium-client contract init <ModuleReference> --contract smart_contract_upgrade --energy 30000 --sender <YourAccountAddress> --grpc-port 20000  --grpc-ip node.testnet.concordium.com

For example, when using the module reference from the image above, the command will look as follows:

$concordium-client contract init 8fc09d2519f516cfbb3d139b1e567753780fbb52854ecbf9a12c447756d18eb0 --contract smart_contract_upgrade --energy 30000 --sender <YourAccountAddress> --grpc-port 20000  --grpc-ip node.testnet.concordium.com
Initialize contract version 1

As seen in the image above, this command returns your smart contract index 4462.

Before upgrading the contract-version1, you can check its state with the view function as follows:

$concordium-client contract invoke <YourContractVersion1Index> --entrypoint view --grpc-port 20000 --grpc-ip node.testnet.concordium.com

For example, when using the smart contract index from the image above, the command will look as follows:

$concordium-client contract invoke 4462 --entrypoint view --grpc-port 20000 --grpc-ip node.testnet.concordium.com

You should see the following output.

View contract version 1

You need to compile and deploy contract-version2 before you can upgrade your smart contract instance contract-version1.

Navigate to the contract-version2 example folder:

$cd ./examples/smart-contract-upgrade/contract-version2

Compile the smart contract to a Wasm module which you will deploy to testnet in the next step.

$cargo concordium build -e --out smart_contract_upgrade.wasm.v1
Compile contract version 2

This command will create the smart_contract_upgrade.wasm.v1 file in your current folder. Deploy the smart contract module to testnet with the command:

$concordium-client module deploy ./smart_contract_upgrade.wasm.v1 --sender <YourAccountAddress> --grpc-port 20000 --grpc-ip node.testnet.concordium.com
Deploy contract version 2

The moduleReference returned when deploying contract-version2 is different from the moduleReference when deploying contract-version1. This shows that the module’s code is not identical.

Create a file named upgrade.json with the following content by inserting the moduleReference returned by the previous step:

{
   "migrate": {
      "Some": [
         [
         "migration",
         ""
         ]
      ]
   },
   "module": "<ModuleReferenceContractVersion2>"
}

For example, when using the module reference from the image above, the file will look as follows:

{
   "migrate": {
      "Some": [
         [
         "migration",
         ""
         ]
      ]
   },
   "module": "31539c983f2ee56822041230d7fd20a3516da9271837e23bb77111bb8c4c7dcd"
}

When invoking the upgrade function with the file above, it specifies that the smart contract code should be upgraded to the module 31539c983f2ee56822041230d7fd20a3516da9271837e23bb77111bb8c4c7dcd and the migration function should be invoked with no input parameters (empty string: "") as part of the upgrade.

Upgrade the smart contract as follows:

$concordium-client contract update <YourContractVersion1Index> --entrypoint upgrade --parameter-json upgrade.json --energy 5000 --sender <YourAccountAddress> --grpc-port 20000 --grpc-ip node.testnet.concordium.com

For example, when using the smart contract index from the image above, the command will look as follows:

$concordium-client contract update 4462 --entrypoint upgrade --parameter-json upgrade.json --energy 5000 --sender <YourAccountAddress> --grpc-port 20000 --grpc-ip node.testnet.concordium.com

You should see the following output.

Upgrade contract version 1

You can check that the state migration was successful by inspecting the state with the view function:

$concordium-client contract invoke <YourContractVersion1Index> --entrypoint view --grpc-port 20000 --grpc-ip node.testnet.concordium.com
View contract version 2

If the migration function had a bug and changed the shape of the state incorrectly, your state might be corrupted. You might see different output errors in case of a corrupted state. One example is shown below:

Corrupted state
Was this article helpful?