Upgradeable Contracts
Even though smart contracts are intended to be immutable by design, it is often necessary to perform an upgrade of a smart contract.
The developer may need to fix a critical bug or introduce a new feature. ink! supports different upgrade strategies that we describe on this page.
Proxy Forwarding
This method relies on the ability of contracts to proxy calls to other contracts.
Properties
- Forwards any call that does not match a selector of itself to another contract.
- The other contract needs to be deployed on-chain.
- State is stored in the storage of the contract to which calls are forwarded.
User ---- tx ---> Proxy ----------> Implementation_v0
|
------------> Implementation_v1
|
------------> Implementation_v2
Example
Our proxy contract will have these 2 storage fields:
#[ink(storage)]
pub struct Proxy {
/// The `AccountId` of a contract where any call that does not match a
/// selector of this contract is forwarded to.
forward_to: AccountId,
/// The `AccountId` of a privileged account that can update the
/// forwarding address. This address is set to the account that
/// instantiated this contract.
admin: AccountId,
}
We then need a way to change the address of a contract to which we forward calls to and the actual message selector to proxy the call:
impl Proxy {
/// Changes the `AccountId` of the contract where any call that does
/// not match a selector of this contract is forwarded to.
///
/// # Note
/// Only one extra message with a well-known selector `@` is allowed.
#[ink(message, selector = @)]
pub fn change_forward_address(&mut self, new_address: AccountId) {
assert_eq!(
self.env().caller(),
self.admin,
"caller {:?} does not have sufficient permissions, only {:?} does",
self.env().caller(),
self.admin,
);
self.forward_to = new_address;
}
/// Fallback message for a contract call that doesn't match any
/// of the other message selectors.
///
/// # Note:
///
/// - We allow payable messages here and would forward any optionally supplied
/// value as well.
/// - If the self receiver were `forward(&mut self)` here, this would not
/// have any effect whatsoever on the contract we forward to.
#[ink(message, payable, selector = _)]
pub fn forward(&self) -> u32 {
ink::env::call::build_call::<ink::env::DefaultEnvironment>()
.call(self.forward_to)
.transferred_value(self.env().transferred_value())
.call_flags(
ink::env::CallFlags::default()
.set_forward_input(true)
.set_tail_call(true),
)
.invoke()
.unwrap_or_else(|err| {
panic!(
"cross-contract call to {:?} failed due to {:?}",
self.forward_to, err
)
});
unreachable!(
"the forwarded call will never return since `tail_call` was set"
);
}
}
Take a look at the selector pattern in the attribute macro: by declaring selector = _
we specify that all other messages should be handled by this message selector.
Using this pattern, you can introduce other message to your proxy contract. Any messages that are not matched in the proxy contract will be forwarded to the specified contract address.
Delegating execution to foreign Contract Code with delegate_call
Similar to proxy-forwarding we can delegate execution to another code hash uploaded on-chain.
Properties
- Delegates any call that does not match a selector of itself to another contract.
- Code is required to be uploaded on-chain, but is not required to be instantiated.
- State is stored in the storage of the original contract which submits the call.
- Storage layout must be identical between both contract codes.
(Storage of Contract A)
User ---- tx ---> Contract A ----------> Code_v0
| ^
| |
⌊_____________________⌋
Storage is delegated to
Example
Suppose we have defined of the caller contract as following:
#[ink(storage)]
pub struct Delegator {
addresses: Mapping<AccountId, i32, ManualKey<0x23>>,
counter: i32,
}
Then let's define two messages that separately calls to update addresses
and counter
separately:
/// Increment the current value using delegate call.
#[ink(message)]
pub fn inc_delegate(&self, hash: Hash) {
let selector = ink::selector_bytes!("inc");
let _ = build_call::<DefaultEnvironment>()
.delegate(hash)
// if the receiver is set to `&mut self`,
// then any changes made in `inc_delegate()` before the delegate call
// will be persisted, and any changes made within delegate call will be discarded.
// Therefore, it is advised to use `&self` receiver with a mutating delegate call,
// or `.set_tail_call(true)` to flag that any changes made by delegate call should be flushed into storage.
// .call_flags(CallFlags::default().set_tail_call(true))
.exec_input(ExecutionInput::new(Selector::new(selector)))
.returns::<()>()
.try_invoke();
}
/// Adds entry to `addresses` using delegate call.
/// Note that we don't need `set_tail_call(true)` flag
/// because `Mapping` updates the storage instantly on-demand.
#[ink(message)]
pub fn add_entry_delegate(&mut self, hash: Hash) {
let selector = ink::selector_bytes!("append_address_value");
let _ = build_call::<DefaultEnvironment>()
.delegate(hash)
.exec_input(ExecutionInput::new(Selector::new(selector)))
.returns::<()>()
.try_invoke();
}
ink! provides an intuitive call builder API for you to compose your call.
As you can see that inc_delegate()
can be built a call in slightly different manner than add_entry_delegate()
.
That's because if the delegated code modifies layout-full storage
(i.e. it contains at least non-Lazy
, non-Mapping
field),
either the receiver should be set to &self
or the .set_tail_call(true)
flag of CallFlags
needs to be specified, and the storage layouts must match.
This is due to the way ink! execution call stack is operated. Non-Lazy
, non-Mapping
field are first loaded into the memory.
If &mut self
receiver is used, then when delegate call is completed, the original state before the call will be persisted and flushed into the storage.
Therefore, .set_tail_call(true)
needs to be set to indicate that, that delegate call's storage context is the final (i.e. _tail) one that needs to be flushed.
This also makes any code after the delegate call unreachable.
With &self
receiver, .set_tail_call(true)
is not required since no storage flushing happens at the end of the original caller's function.
(see Stack Exchange Answer for details on how changes are flushed into storage).
If the delegated code modifies Lazy
or Mapping
field, the keys must be identical and .set_tail_call(true)
is optional
regardless of the function receiver.
This is because Lazy
and Mapping
interact with the storage directly instead of loading and flushing storage states.
Now let's look at the "delegatee" code:
#[ink::contract]
pub mod delegatee {
use ink::storage::{
traits::ManualKey,
Mapping,
};
#[ink(storage)]
pub struct Delegatee {
// `ManualKey` must be the same as in the original contract.
addresses: Mapping<AccountId, i32, ManualKey<0x23>>,
counter: i32,
// Uncommenting below line will break storage compatibility.
// flag: bool,
}
impl Delegatee {
/// When using the delegate call. You only upload the code of the delegatee
/// contract. However, the code and storage do not get initialized.
///
/// Because of this. The constructor actually never gets called.
#[allow(clippy::new_without_default)]
#[ink(constructor)]
pub fn new() -> Self {
unreachable!(
"Constructors are not called when upgrading using `set_code_hash`."
)
}
/// Increments the current value.
#[ink(message)]
pub fn inc(&mut self) {
self.counter = self.counter.checked_add(2).unwrap();
}
/// Adds current value of counter to the `addresses`
#[ink(message)]
pub fn append_address_value(&mut self) {
let caller = self.env().caller();
self.addresses.insert(caller, &self.counter);
}
}
}
As you can see, delegatee's code looks like a normal ink! Smart Contract with some important features:
- Storage layout is identical to the original contract's storage
addresses
mapping key is identical- Constructor does not have any logic, as the code is never instantiated. (It can be, but plays no effect on the execution)
Note on the usage of wildcard selectors
When working with cross-contract calls, developers are required to be aware of the some important changes.
Since ink! 5 we have restricted the usage of the wildcard selector due to security reasons.
Due to IIP-2, ink! only allows
to contain a single message with a well-known selector @
when the other message
with the wildcard selector _
is defined.
See example for illustration on how it can be used in practice.
Note on CallFlags
CallFlags
provide fine-grained control over the cross-contract execution.
Some useful properties:
- Re-entry is disable by default. It can be enabled with
.set_allow_reentry(true)
flag. - The call execution context is returned to the caller by default. You can finish execution in the callee with
.set_tail_call(true)
flag. .set_clone_input(true)
clones the input of the caller's messages. It can be used with when.set_tail_call(false)
..set_forward_input(true)
consumes the input of the caller's message which can be used after. It can be used with when.set_tail_call(true)
.
Replacing Contract Code with set_code_hash()
Following Substrate's runtime upgradeability
philosophy, ink! also supports an easy way to update your contract code via the special function
set_code_hash()
.
Properties
- Updates the contract code using
set_code_hash()
. This effectively replaces the code which is executed for the contract address. - The other contract needs to be deployed on-chain.
- State is stored in the storage of the originally instantiated contract.
Example
Just add the following function to the contract you want to upgrade in the future.
/// Modifies the code which is used to execute calls to this contract address (`AccountId`).
///
/// We use this to upgrade the contract logic. We don't do any authorization here, any caller
/// can execute this method. In a production contract you would do some authorization here.
#[ink(message)]
pub fn set_code(&mut self, code_hash: [u8; 32]) {
ink::env::set_code_hash(&code_hash).unwrap_or_else(|err| {
panic!(
"Failed to `set_code_hash` to {:?} due to {:?}",
code_hash, err
)
});
ink::env::debug_println!("Switched code hash to {:?}.", code_hash);
}
Storage Compatibility
It is the developer's responsibility to ensure that the new contract's storage is compatible with the storage of the contract that is replaced.
You should not change the order in which the contract state variables are declared, nor their type!
Violating the restriction will not prevent a successful compilation, but will result in the mix-up of values or failure to read the storage correctly. This can be a result of severe errors in the application utilizing the contract.
If the storage of your contract looks like this:
#[ink(storage)]
pub struct YourContract {
x: u32,
y: bool,
}
The procedures listed below will make it invalid
Changing the order of variables:
#[ink(storage)]
pub struct YourContract {
y: bool,
x: u32,
}
Removing an existing variable:
#[ink(storage)]
pub struct YourContract {
x: u32,
}
Changing the type of a variable:
#[ink(storage)]
pub struct YourContract {
x: u64,
y: bool,
}
Introducing a new variable before any of the existing ones:
#[ink(storage)]
pub struct YourContract {
z: Vec<u32>,
x: u32,
y: bool,
}
A little note on the determinism of contract addresses
If your contract utilizes this approach, it no-longer holds a deterministic address assumption. You can no longer assume that a contract address identifies a specific code hash. Please refer to the issue for more details.
Examples
Examples of upgradable contracts can be found in the ink! repository.