Migrating from ink! 3.x to 4.0
We've made a couple of breaking changes from ink! 3.x to ink! 4.0. On this page we outline how you can migrate existing clients and contracts from 3.x to 4.0.
This migration guide is only for your code base!
If you have an existing contract on-chain you cannot just upgrade the code on-chain ‒ you also have to migrate your data, since the way ink! 4.0 stores data and reads it (i.e. the storage layout) changes from ink! 3.x to 4.0.
Compatibility
ink! 4.0 is compatible with:
- Stable Rust >= 1.63.0
scale
>=3scale-info
>= 2.3pallet-contracts
>=polkadot-v0.9.37
substrate-contracts-node
>=v0.24.0
polkadot-js/api
andpolkadot-js/api-contract
>= 9.10.2
cargo-contract
2.0
Together with ink! 4.0 we've released cargo-contract
2.0.
You have to use this latest version of cargo-contract
for ink! 4.0
contracts.
You can upgrade via:
cargo install cargo-contract --force --version 2
Make sure that e.g. your CI also uses cargo-contract
2 with ink! 4.
If you have wrapper scripts around cargo-contract
you should
ensure that this version is enforced, otherwise users will get an error.
cargo-contract
no longer requires binaryen
or wasm-opt
as an
external dependency. We required those because of wasm-opt
tool
(which is part of binaryen
). Fortunately we were able to find a way of
installing wasm-opt
now as part of the cargo-contract
installation
process.
Rust stable
instead of nightly
ink! 4.0 and cargo-contract
use stable
Rust now.
This means no more cargo +nightly contract
is required, you
can just use a stable Rust toolchain now (>= Rust 1.63).
New entrance ink
crate
The ink_lang
crate has been replaced in #1223
by a new top level ink
crate. All existing sub-crates are reexported and should be used via
the new ink
crate, so e.g. ink::env
instead of ink_env
. Contract authors should now import
the top level ink
crate instead of the individual crates.
Migration
- In
Cargo.toml
Replace all individualink_*
crate dependencies with theink
crate. - In the contract source:
- Remove the commonly used
use ink_lang as ink
idiom. - Replace all usages of individual crates with reexports, e.g.
ink_env
➜ink::env
.
- Remove the commonly used
Storage API + Layout
With #1331 the way ink!
reads and writes
to a contract's storage changed. Storage keys are generated at compile-time, and user facing
abstractions which determine how contract data is laid out in storage are different now.
Migration
- Initialize
Mapping
fields withMapping::default()
instead ofink_lang::utils::initialize_contract
in constructors. Seeerc20
and other examples which use aMapping
. SpreadAllocate
,SpreadLayout
,PackedLayout
,PackedAllocate
have been removed.
Removal of wee-alloc
support
ink! uses a bump allocator by default, additionally we supported another allocator
(wee-alloc
) through a feature flag. wee-alloc
is no longer maintained and
we removed support for it in #1403.
Removal of eth_compatibility
crate
As part of #1233
the eth_compatibility
crate was removed. The ecdsa_to_eth_address()
function from it can now be found in the ink_env
crate.
ink_env::ecdsa_to_eth_address(&pub_key, &mut output);
ink_storage::Mapping
The function signature of Mapping::insert(key, val)
changed to
Mapping::insert(key, val) -> Option<u32>
.
The return value is the size of the pre-existing value at the specified key if any (in bytes).
Two new useful functions were added:
Mapping::contains(key)
in #1224.Mapping::take()
to get a value while removing it from storage in #1461.
In case you were working around those two functions you can now
use them directly; they are more gas-efficient than e.g. executing
a get(key).is_none()
instead of contains(key)
.
Storage functions in ink_env
As part of #1224 the return type
of ink_env::set_contract_storage()
was changed to return an Option<u32>
instead of ()
.
A new function ink_env::take_contract_storage
was introduced.
Removal of ink_env::random
function
We had to remove the ink_env::random
function (in #1442).
This function allowed contract developers getting random entropy.
There is unfortunately no way how this can be done safely enough
with built-in Substrate primitives on-chain currently. We're
following the recommendation of our auditors to remove it.
The alternative right now is to provide random entropy off-chain to the contract, to use a random entropy oracle, or to have a chain-extension that does this, in case the chain has a possibility to do so.
We hope to bring this function back in a future release of ink!, the best hope right now is that it could come back with Sassafras, a block production protocol for future versions of Polkadot.
If you're interested in more information on this check out
the Substrate PR which
deprecated the random interface of pallet-contracts
.
Constructors can now return Result<Self, MyContractError>
With #1446 we introduced
the possibility for constructors to return either Self
(as usual) or
Result<Self, MyContractError>
.
This enables contract developers to bubble up encoded error objects to clients/frontends about a failure. In ink! 3.x it was only possible to panic in the constructor in case an error occurred, resulting in loss of this information.
Chain extension's returns_result
removed
The returns_result
flag has been removed from the #[ink(extension = …)]
attribute in
#1569.
We now infer this information at compile time. If handle_status
is set to true
,
the return type will still be wrapped into Result
as before.
Contract Metadata (ABI)
The most detailed way to grasp what changed is to look at this PR, which updated the metadata page in our documentation.
Add support for language level errors (LangError
)
Under the hood, ink! now generates code that results in each message
and constructor returning a Result<Message::Output, LangError>
(or
for constructors Result<Constructor::Output, LangError>
).
This happens even if the message/constructor doesn't have a return type,
we default to the unit type ()
in that case.
A LangError
is a type of error which doesn't originate from the contract itself,
nor from the underlying execution environment (so the Contracts pallet
in this case).
An example of where this would arise is if a caller tries to use a non-existent message selector for a contract. Previously, the contract would trap and not allow the caller to do any sort of error handling if it encountered a non-existent selector.
This change doesn't affect how you write a contract! It affects clients and frontends though, since it breaks the API in two ways:
first, all contract messages now have a Result
return type, and second a new field,
lang_error
, will be introduced as part of the contract spec. The second change allows
other languages (such as Solang) to use an equivalent LangError
.
Click here for a snippet of the new metadata for the Flipper contract.
"messages": [
{
"args": [],
"docs": [
" Flips the current value of the Flipper's boolean."
],
"label": "flip",
"mutates": true,
"payable": false,
"returnType": {
"displayName": [
"ink",
"MessageResult"
],
"type": 1
},
"selector": "0x633aa551"
}],
"lang_error": {
"displayName": [
"ink",
"LangError"
],
"type": 3
},
{
"id": 3,
"type": {
"def": {
"variant": {
"variants": [
{
"index": 1,
"name": "CouldNotReadInput"
}
]
}
},
"path": [
"ink_primitives",
"LangError"
]
}
}
Version field
As part of #1313 the ink! ABI was changed to have a proper version field as part of the ink! metadata object. This enables querying the ABI version in a less-ambiguous way.
Before:
"source": {...},
"contract": {...},
"V3": {
"spec": {...},
"storage": {...},
"types": {...}
}
After:
{
"source": {...},
"contract": {...},
"spec": {...},
"storage": {...},
"types": [...],
"version": "4"
}
The Storage Layout (storage
)
The storage layout under the storage
key changed for v4. If you have an application
that is using it consider reading the updated documentation:
Removal of AccountId
Default
implementation
In #1255 we removed the Default
trait
implementation on AccountId
s.
The Default
implementation of AccountId
returned the zero-address, which is
problematic since the
zero-address in the sr25519
and ed25519
curves has a known private key.
Developers commonly reach for defaults, and the zero-address in particular, making it an unsafe trait implementation to have given the consequences.
Imagine a developer sending tokens to the zero-address to be burned, only to find that they've been stolen because the private key is known.
If you were previously using AccountId
's Default
implementation in your code you
have a couple of different options for how to move forward. These will depend on what
exactly you were using the zero-address for.
If you were using it as a burn address:
- You can pick another address to use, assuming that you've actually picked a random address
- Consider a solution that involves reducing total issuance, instead of transferring tokens to a random address
If you were using it as a privileged account:
- Change the account
- Add checks to ensure that calls coming from the zero-address are rejected
You should also now consider dealing with AccountId
's as Option<AccountId>
's. This is
more idiomatic Rust, and also conveys the meaning of a "null" or "empty" address much
better.
Updates to the CallBuilder
and CreateBuilder
APIs
There's been several changes to the
CallBuilder
and
CreateBuilder
APIs.
In #1604 we renamed the
CallBuilder::fire()
method to
CallBuilder::invoke()
.
This brings more consistency across our APIs which were already using the invoke
terminology.
In #1512 and #1525
we added support for handing
LangError
s from the CreateBuilder
and CallBuilder
, respectively.
If you want to handle errors from either Builder
you can use the new
CreateBuilder::try_instantiate()
or
CallBuilder::try_invoke()
methods.
Because of the addition of those methods we also removed any error handling from the
non-try_
methods in #1602. This means
that the CallBuilder::invoke()
and CreateBuilder::instantiate()
methods return values
directly, and panic when they encounter an error.
Lastly, in #1636 we added two methods to
the CallBuilder
to streamline
Call
and
DelegateCall
workflows:
- For
Call
you can useCallBuilder::call()
(this replacesCallBuilder::callee()
) - For
DelegateCall
you can useCallBuilder::delegate()
Removal of [lib.crate-type]
and [lib.name]
from contract manifest
Earlier versions of cargo-contract
required that these two fields were specified in the
contract manifest explicitly, as follows:
[lib]
name = "flipper"
path = "lib.rs"
crate-type = [
# Used for normal contract Wasm blobs.
"cdylib",
# Use to generate ABI
"rlib",
]
However, with cargo-contract#929 we changed this behavior to:
- Use the contract name by default, removing the need for the
name
field - Compile contracts as
rlib
s by default, and automatically changing tocdylib
as needed
This means that your new manifest should look like:
[lib]
path = "lib.rs"