Runtime failure after successful upgrade

Hi there!
I wanted to try out the upgradability feature with cis2-wccd example.

When I changed the logic slightly without changing the state parameter, there was no problem.
However, when I upgraded to add new parameters to the state, the upgrade itself was successful, but after that, calling any update function(even view function) results in an error like Error: Updating contract instance failed: Runtime failure.

So, I have two questions.

  1. should state not be changed?
  2. is there any way to deal with runtime failure?
  1. Yes, the state should not be changed unless you also run a migration function to migrate it.
  2. That depends a bit on where you added a parameter to the state. But likely you cannot fix it.

Thanks for your reply.
So if I call a migration function properly, it is allowed to change the state, right?

In my case, I just added new_param: Option<u8> to the State.
Could you give me an example of how to write a migration function?

struct State<S: HasStateApi> {
    admin: Address,
    paused: bool,
    token: StateMap<Address, AddressState<S>, S>,
    implementors: StateMap<StandardIdentifierOwned, Vec<ContractAddress>, S>,
    metadata_url: StateBox<concordium_cis2::MetadataUrl, S>,
    new_param: Option<u8>, // add this line
}

I have modified the example in the wccd contract to use the low_level interface. This will allow you to modify the state in the migration function.

See concordium-rust-smart-contracts/lib.rs at main · Concordium/concordium-rust-smart-contracts · GitHub

The key point is that the high-level intefrace (the default one) tries to do things for you, such as make sure that you always have a consistent view of the state. However that means that it would try to deserialize state in the old shape after the migration function returns. And of course that does not work.

The low-level interface is more direct, and does not do things for you implicitly. And this is what you want in this instance.

Sorry for not responding sooner and thanks for the example.
I think I almost understand and now interested in the low-level interface.
Are there any other use cases for low-level interface?

The low-level interface allows you more control, but also needs you to be aware of more.

For example, in the normal, high-level interface, when you .invoke_contract then

  • any local changes you have made to the state are committed to the persistent state
  • a contract is invoked
  • if the state of the invoking contract has been changed then the host.state() is reloaded from persistent storage.

The low-level interface just invokes another contract, and it is up to the contract writer to be careful and do steps (1) and/or (3) if necessary.

However that does mean you can write more efficient contracts since you may know that some operations are not necessary. That is the primary motivation for the low-level interface.

I would recommend to only use the low-level interface sparingly and not try to optimize too much too early. There are safety features built into the high-level interface to prevent many common mistakes.

2 Likes