-
Notifications
You must be signed in to change notification settings - Fork 429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Upgradeable contracts #1889
Upgradeable contracts #1889
Changes from 7 commits
190f849
8a21dc6
7500a63
2f9485e
05dad8e
60d0899
fd7a656
3e674f7
b6434c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,8 @@ decodable | |
decrement | ||
defrag | ||
defragmentation | ||
delegatee | ||
delegator | ||
deploy | ||
dereferencing | ||
deserialize/S | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Upgradeable Contracts | ||
|
||
There are different ways a contract can be upgraded in ink! | ||
|
||
This folder illustrates some of the common and best practices to achieve upgradeability in your contracts. | ||
|
||
## [`set-code-hash`](set-code-hash/) | ||
|
||
ink! provides an ability to replace the code under the given contract's address. | ||
This is exactly what `set_code_hash()` function does. | ||
|
||
However, developers needs to be mindful of storage compatibility. | ||
You can read more about storage compatibility on [use.ink](https://use.ink/basics/upgradeable-contracts#replacing-contract-code-with-set_code_hash) | ||
|
||
## [Delegator](delegator/) | ||
|
||
Delegator patter is based around a low level cross contract call function `delegate_call`. | ||
It allows a contract to delegate its execution to some on-chain uploaded code. | ||
|
||
It is different from a traditional cross-contract call | ||
because the call is delegate to the **code**, not the contract. | ||
|
||
Similarly, the storage compatibility issue is also applicable here. | ||
However, there are certain nuances associated with using `delegate_call`. | ||
|
||
First of all, as demonstrated in the example, if the delegated code intends to mutate the caller's storage, | ||
a developer needs to be mindful. If the delegated code modifies layoutful storage | ||
(i.e. it contains at least non-`Lazy`, non-`Mapping` field), 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 | ||
(see [StackExchange Answer](https://substrate.stackexchange.com/a/3352/3098) for more explanation). | ||
|
||
If the delegated code only modifies `Lazy` or `Mapping` field, the keys must be identical and `.set_tail_call(true)` is optional. | ||
This is because `Lazy` and `Mapping` interact with the storage directly instead of loading and flushing storage states. | ||
|
||
If your storage is completely layoutless (it only contains `Lazy` and `Mapping` fields), the order of fields and layout do not need to match for the same reason as mentioned above. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd avoid usage of layoutfull and layoutless since they don't have well-defined meaning. Storage under There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, it is hard to explain in terms of Packed and Non-Packed because storage can be Non-Packed by still layoutful. Here the "layoutfulness" plays an important role. |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Ignore build artifacts from the local tests sub-crate. | ||
/target/ | ||
|
||
# Ignore backup files creates by cargo fmt. | ||
**/*.rs.bk | ||
|
||
# Remove Cargo.lock when creating an executable, leave it for libraries | ||
# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock | ||
Cargo.lock |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
[package] | ||
name = "delegator" | ||
version = "4.2.0" | ||
authors = ["Parity Technologies <admin@parity.io>"] | ||
edition = "2021" | ||
publish = false | ||
|
||
[dependencies] | ||
ink = { path = "../../../crates/ink", default-features = false } | ||
delegatee = { path = "delegatee", default-features = false, features = ["ink-as-dependency"] } | ||
|
||
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } | ||
scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } | ||
|
||
[dev-dependencies] | ||
ink_e2e = { path = "../../../crates/e2e" } | ||
|
||
[lib] | ||
path = "lib.rs" | ||
|
||
[features] | ||
default = ["std"] | ||
std = [ | ||
"ink/std", | ||
"scale/std", | ||
"scale-info/std", | ||
] | ||
ink-as-dependency = [] | ||
e2e-tests = [] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Ignore build artifacts from the local tests sub-crate. | ||
/target/ | ||
|
||
# Ignore backup files creates by cargo fmt. | ||
**/*.rs.bk | ||
|
||
# Remove Cargo.lock when creating an executable, leave it for libraries | ||
# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock | ||
Cargo.lock |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
[package] | ||
name = "delegatee" | ||
version = "4.2.0" | ||
authors = ["Parity Technologies <admin@parity.io>"] | ||
edition = "2021" | ||
publish = false | ||
|
||
[dependencies] | ||
ink = { path = "../../../../crates/ink", default-features = false } | ||
|
||
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } | ||
scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } | ||
|
||
[lib] | ||
path = "lib.rs" | ||
|
||
[features] | ||
default = ["std"] | ||
std = [ | ||
"ink/std", | ||
"scale/std", | ||
"scale-info/std", | ||
] | ||
ink-as-dependency = [] | ||
e2e-tests = [] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
#![cfg_attr(not(feature = "std"), no_std, no_main)] | ||
|
||
#[ink::contract] | ||
pub mod delegatee { | ||
use ink::storage::{Mapping, traits::ManualKey}; | ||
#[ink(storage)] | ||
pub struct Delegatee { | ||
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 += 2; | ||
} | ||
|
||
|
||
/// 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); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's a mistake to present these as two separate ways of upgrading storage -
set_code_hash
is almost always used together withdelegate_call
to migrate the contract's storage after upgrading its code.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While they can be used together, it is also possible to upgrade the storage using only
set_code_hash
anddelegate_call
. These are the atomic blocks for upgradeability. For more detailed practises on how to upgrade the contract in the best way, there should be a documentation pageThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
delegate_call
doesn't upgrade the contract - i.e. it doesn't change it's logic (one could argue that if all calls delegate then it is but I'd say it's a proxy then) it can only change its data. Hence the only way to upgrade the contract is to:set_code_hash
.Also, you can only delegate if that logic has been there since the beginning - i.e. since the moment the contract was uploaded. I'd argue that the contract isn't upgraded .
I think the nomenclature is important here b/c if used loosely it will only add to devs confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
delegate_call
allows to delegate an execution of logic to another on-chain. If used correctly, you can introduce the upgradeability functionality where delegated code hash can be replaced. The reason why it belongs to upgradeability is because that's how it is implemented in the EVM world - via proxies.set_code_hash
is the "Polkadot" way of upgradability.Nonetheless, these nuances should be elaborated in the documentation. The examples just provide a concrete illustration of how these "blocks" can be used individually.