8-Upgradeable contracts
Overview
Code of a smart contract deployed on chain is immutable, however, we can update the code hash of a contract to point to a different code (therefore changing the code of the smart contract). This functionality can be used for bug fixing and potential product improvements. For this type of scenario, there are different upgrade strategies.
- Replacing Contract Code with
set_code_hash()
- Proxy Forwarding
In this tutorial we will focus on the first strategy, ie replacing the contract code with the method set_code_hash().
Exemple - V1
The logic layer
In this tutorial, we use “a renter of cars” as example and we define the logic layer via the following trait :
pub type CarId = AccountId;
#[openbrush::trait_definition]
pub trait Renter {
#[ink(message)]
fn add_car(&mut self, car_id: CarId, owner: AccountId) -> Result<(), Error>;
#[ink(message)]
fn rent(&mut self, car_id: CarId) -> Result<(), Error>;
#[ink(message)]
fn give_back(&mut self, car_id: CarId) -> Result<(), Error>;
}
We defined three methods:
add_car
: the caller add a new car for rentrent
: the caller rents the cargive_back
: the caller returns the previously rented car
The storage
The default implentation of this trait can be found here.
Mainly we will define this storage:
#[derive(Default, Debug)]
#[openbrush::storage_item]
pub struct Data {
owners: Mapping<CarId, AccountId>,
leaseholders: Mapping<CarId, AccountId>,
}
We use the openbrush::storage_item
macro that implements the required traits for a struct, as well as automatically generating unique storage keys for each of the struct's fields which are either marked as #[lazy]
or are of type Mapping
/MultiMapping
. You can have more information here to know how the storage works.
The macro openbrush::storage_item
is an easy way to automatically generate unique storage keys and help the developer to build upgradable contracts.
The contract
The contract uses the storage item defined in the previous logic layer and exposes the method upradable_code_hash()
to update the code hash of the contract.
#[openbrush::contract]
pub mod renter {
use logics::impls::renter::{self, *};
use openbrush::traits::Storage;
#[ink(storage)]
#[derive(Default, Storage)]
pub struct MyContract {
#[storage_field]
renter: renter::Data,
}
impl Renter for MyContract {}
impl MyContract {
#[ink(constructor)]
pub fn new() -> Self {
let instance = Self::default();
instance
}
#[ink(message)]
pub fn upgrade_contract(&mut self, new_code_hash: [u8; 32]) -> Result<(), ContractError> {
ink::env::set_code_hash(&new_code_hash).map_err(|_| ContractError::UpgradeError)?;
Ok(())
}
}
#[derive(Debug, Eq, PartialEq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum ContractError {
UpgradeError,
}
}
It’s important to not forget to expose the method set_code_hash()
from te beginning if you want your contract can be upgraded because you will not be able to do it later.
It’s also very important to restrict the access to this method, only the admin of the contract should be able to call the function. You can find moer information about the security here.
Upgrade the version to v2
Let’s imagine we want to add extra features in our contract:
- set a rental price by car when the car is rented.
- get the leaseholder of a car
- know if a car is rent or not
The logic layer
Here the new version of our logic layer
#[openbrush::trait_definition]
pub trait Renter {
#[ink(message)]
fn add_car(&mut self, car_id: CarId, owner: AccountId) -> Result<(), Error>;
#[ink(message)]
fn update_rental_price(
&mut self,
car_id: CarId,
rental_price: Option<Balance>,
) -> Result<(), Error>;
#[ink(message)]
fn get_leaseholder(&self, car_id: CarId) -> Result<Option<AccountId>, Error>;
#[ink(message)]
fn is_rent(&self, car_id: CarId) -> Result<bool, Error>;
#[ink(message, payable)]
fn rent(&mut self, car_id: CarId) -> Result<(), Error>;
#[ink(message)]
fn give_back(&mut self, car_id: CarId) -> Result<(), Error>;
}
The storage
The new storage used to implement this trait:
#[derive(Default, Debug)]
#[openbrush::storage_item]
pub struct Data {
owners: Mapping<CarId, AccountId>,
leaseholders: Mapping<CarId, AccountId>,
rental_prices: Mapping<CarId, Balance>,
received_rents: Mapping<(CarId, AccountId), Balance>,
}
We still to use the openbrush::storage_item
macro to automatically generates unique storage keys and we respect the following rules about the storage compatibility:
- You must not change the order in which the contract state variables are declared, nor their type!
- You must not remove an existing variable
- You must not change the type of a variable
- You must not add a new variable before any of the existing ones.
The contract
There is no change in the contract except that we are using the new version of our logic layer.
To upgrade the first version of the contract, we need to first deploy a smart contract with the new code to register its code hash to the chain, and then call the ink::env::set_code_hash function.
Conclusion
Upgradeability allows experimenting and deploying the product at the early stage, always leaving the chance to fix vulnerabilities and progressively add features. Upgradeable contracts are not a Bug if they are developed consciously with decentralization in mind.
Decentralization can be achieved by providing the right to upgrade only to decentralized authority like governance, multisig, or another analog.
There is also a possibility of smart contract upgradeability via Proxy and Diamond patterns, which use DelegateCall to perform operations over own storage with code of a different deployed contract.
Git repository
https://github.com/GuiGou12358/astar-tutorials/tree/main/tuto8
Source
https://use.ink/basics/upgradeable-contracts
https://learn.brushfam.io/docs/OpenBrush/smart-contracts/upgradeable
Crypto-enthusiast, Defi & NFT believer, Dotsam Fam Astar Tech Amb & Phala Amb Web2 builder gradually migrating to Web3
Tutorials to write Smart Contracts in Rust and Ink!
0 comments