Migration guide to v2.10.0 from 2.9
Odra v2.10.0 is a feature release on the same Casper stack as 2.9. Most projects only bump the version:
odra = "2.10.0"
Install the matching build tool as well. cargo-odra 0.2.0 goes with Odra 2.10:
cargo install cargo-odra --locked
See cargo-odra 0.2.0 below for what changed there.
A few changes can still need attention:
- Compile-time changes: arguments of
#[odra::module]on animplblock, a duplicated#[odra::external_contract]trait, and unknown or repeated#[odra(..)]arguments. - Events with struct fields are refused at deploy and schema generation.
- OdraVM rolls back reverted calls completely, which can change what a test sees.
Erc20::mintandErc20::burnare no longer entry points (a security fix).#[odra::module(name = "..")]also names the package-hash key.cargo odra schemano longer writesresources/legacy.- Livenet client and error codes, for code that uses
CasperClientdirectly or matches on error codes, including the error of a failed livenet deploy. - Custom backends and hand-written
EntryPointsCallers. - cargo-odra 0.2.0: one
wasmdirectory per workspace, contracts from dependency crates.
HostEnv::advance_block_time and advance_with_auctions now also accept a core::time::Duration,
so env.advance_block_time(Duration::from_secs(60)) states the unit itself. A number still means
milliseconds, so existing calls keep working.
Compile-time changes
Both are things that used to compile and silently did the wrong thing. If your project builds, you are not affected.
#[odra::module(...)] arguments on an impl block are an error
events, errors, name, version and layout belong to the module struct. Put on an impl
block they were parsed and ignored - the events never made it into the contract schema. Now the
compiler points at the misplaced argument:
#[odra::module(events = [Transfer])] // error: `events` is not allowed on an impl block
impl Token { ... }
Move the argument to the struct:
#[odra::module(events = [Transfer])]
pub struct Token { ... }
#[odra::module]
impl Token { ... }
Only factory = on is accepted on an impl block, and #[odra::module] on a trait takes no
arguments.
#[odra::external_contract] keeps the trait
The annotated trait is now emitted as written, and both XxxContractRef and XxxHostRef
implement it. Previously the trait disappeared, so a common workaround was to declare it twice:
#[odra::external_contract]
pub trait Adapter { fn owner_of(&self, token_id: TokenId) -> Option<Address>; }
pub trait Adapter { fn owner_of(&self, token_id: TokenId) -> Option<Address>; } // remove this copy
That copy now fails with the name Adapter is defined multiple times - delete it. The trait can
be used as a bound (fn check<T: Adapter>(a: &T)) or implemented by one of your modules.
Names that clashed with the generated code now compile: entry point arguments such as contract,
exec_env, named_args or result, a module field named env, and bytes or result fields of
#[odra::odra_type]s and events. The generated locals got an __odra_ prefix instead, and so did
the hidden env field of a module: __env is now __odra_env. Only code that touched that field
directly is affected - use self.env(). #[odra::event] no longer derives odra::Event but
generates the same impls itself; storage and serialized bytes are unchanged.
Unknown or repeated #[odra(..)] arguments are an error
An unknown argument (#[odra(paybale)], #[odra(view)]) or a repeated one
(#[odra(payable, payable)]) used to compile and was silently ignored - the function was not
payable, for example. It is now a compile error pointing at the argument:
error: unknown odra attribute `paybale`, expected one of: payable, non_reentrant, offchain
Such an attribute never had an effect, so fix the spelling or remove it. In return, private helper
functions of a module are no longer checked like entry points and may use names such as new or
an argument named gas.
Events with struct fields
An event field whose CLType is or contains Any - an #[odra::odra_type] struct or an enum with
data, also inside an Option, a Vec, a map or a tuple - could never be emitted: every
emit_event reverted with ExecutionError::Formatting. Such a contract used to deploy fine; now
deploying or upgrading it (in tests, scripts and the CLI) and cargo odra schema panic with a
message naming the event and the field. Flatten the fields into the event or use a unit enum, see
Events.
OdraVM rolls back reverted calls completely
OdraVM used to roll back only the storage of a call that reverted. Like Casper, it now also drops the
events and native events emitted during the call (also by nested calls), the contracts it deployed
and the new code of a failed upgrade: after a failing try_upgrade the old code keeps running. A
test that counted events of a reverted call, or relied on a half-done upgrade, sees different
results now - the same ones it gets with cargo odra test -b casper.
HostEnv::restore_snapshot restores the deployed contracts and their code as well.
Erc20::mint and Erc20::burn are no longer entry points
In odra-modules, Erc20::mint, Erc20::burn and Ownable::unchecked_transfer_ownership moved out
of the #[odra::module] impl blocks. A contract built directly from Erc20 had an unprotected mint
entry point; now these functions exist only in Rust, for a wrapping module to call behind its own
check (as OwnedToken in the examples does):
#[odra::module]
impl OwnedToken {
pub fn mint(&mut self, address: &Address, amount: &U256) {
self.ownable.assert_owner(&self.env().caller());
self.erc20.mint(address, amount);
}
}
Erc20HostRef::mint / try_mint and burn / try_burn are gone; if a test relied on them, mint
through your wrapping contract or use initial_supply in init.
#[odra::module(name = "..")] names the package
Until now name only changed the contract's name in the schema. It now also decides the named key
the package hash is stored under when the contract is installed through Odra (InstallConfig,
UpgradeConfig, load_or_deploy): <name>_package_hash instead of <StructName>_package_hash.
A contract with a name that was installed with 2.9 or earlier keeps its old key; a fresh install with 2.10
uses the new one, so scripts that look the package up by the named key have to follow. Modules
without name are unaffected.
Upgrades are not affected: the package is found by its address and authorized by the access URef
the account holds, so an upgrade of a 2.9 deployment simply stores the package hash under the new
key as well. One thing to watch: the "already installed" guard (allow_key_override = false)
checks the new key name, so it no longer stops a fresh install next to a 2.9 deployment of the
same contract - use load_or_deploy or check contracts.toml rather than relying on the revert.
cargo odra schema no longer writes resources/legacy
Only the contract schema is generated now, in resources/casper_contract_schemas. The old
resources/legacy directory is not updated anymore - delete it.
Your bin/build_schema.rs keeps working as it is, but you can drop the legacy schema from it:
#[cfg(all(not(odra_module = ""), not(target_arch = "wasm32")))]
extern "Rust" {
fn casper_contract_schema() -> odra::schema::casper_contract_schema::ContractSchema;
}
#[cfg(all(not(odra_module = ""), not(target_arch = "wasm32")))]
fn main() {
odra_build::contract_schema(unsafe { crate::casper_contract_schema() });
}
cargo-odra 0.2.0
cargo-odra 0.1.8 still builds and tests a single-crate Odra 2.10 project, but 0.2.0 is the release
made for 2.10:
- A workspace has one
wasmdirectory, at its root. 0.1.x also copied every wasm into the crate defining the contract, leaving a duplicate that went stale and that the Casper test VM could pick up instead of the fresh build. Odra 2.10 looks forwasm/<Contract>.wasmin the working directory and in every directory above it, so the root copy serves every member. This is also why a workspace project built with cargo-odra 0.2.0 needs Odra 2.10: Odra 2.9 looks in the working directory only.cargo odra buildandcargo odra cleandelete the copies an older cargo-odra left in the members. - Contracts from dependency crates. The first segment of an
fqninOdra.tomlmay be the name of a crate the project depends on (fqn = "odra_modules::erc20::Erc20"); the wasm and schema are built into the project like a local contract's. The building crate must reference the dependency (use odra_modules;inbin/build_contract.rsandbin/build_schema.rs), see Odra.toml. ODRA_MODULEis passed crate-qualified (odra_modules::Erc20) to Odra 2.10 and newer, so two crates defining a module of the same name no longer compile their entry points into one wasm. Older Odra releases keep getting the bare struct name.cargo odra newresolves the latest Odra release from thereleases/latestredirect on github.com instead of the rate-limited GitHub API, and no longer joins a generated project to an enclosing Cargo workspace. Pass-s 2.10.0to pin the version explicitly.
Livenet client and error codes
Only code that uses odra-casper-rpc-client directly, matches on errors returned by livenet, or calls
getters there is affected.
CasperClient::get_value,get_named_value,get_dictionary_valueandevents_countreturnResult<Option<_>>:Ok(None)is a value the node does not have,Erris a node that could not be asked. Handle both cases where you used to get the value directly.CasperClient::deploy_wasmtakes&selfinstead of&mut self.- The blocking
CasperClientcalls panic inside a current-thread Tokio runtime and point to the newxxx_asyncmethods. Use those, or a multi-thread runtime (#[tokio::main]). - A deploy or upgrade on livenet whose
initorupgradereverts returns the contract's error (e.g.MyError::InstallRefused), named after the schema of the deployed contract, as on OdraVM and CasperVM; running out of gas returnsOutOfGas. It used to beContractDeploymentError("Livenet execution error"), which is now left for deployments that did not get to run the contract (gas not set, node unreachable, ...). - User errors are named after the schema of the called contract first, so a revert no longer comes
back as another contract's error with the same code. An error code livenet does not know returns
VmError::Otherinstead of panicking. See Livenet errors. - A getter or
#[odra(offchain)]function that reverts on livenet returnsErrfrom itstry_*call instead of panicking, and sees the calling account ascaller()(it used to see its own address). - Reading a stored value as the wrong type reverts with the concrete
bytesreprerror (LeftOverBytes,EarlyEndOfStream, ...) instead ofFormatting. A named argument that exists but has the wrong type reverts withExecutionError::InvalidArg(138) instead ofMissingArg.
Custom backends
ContractContext gained call_stack and debug, and HostContext gained take_snapshot,
restore_snapshot and thread_env_factory. All of them have default implementations, so an existing
backend keeps compiling; override them to support the new features.
odra::entry_point_callback::EntryPoint has a new is_offchain field. If you build entry points
with a struct literal, switch to EntryPoint::new, new_payable or new_offchain.
HostContext also gained take_child_contracts, with a default implementation returning no
contracts. A backend that runs factory modules returns the children it deployed since the last call,
so that HostEnv tracks their events.
EntryPointsCaller no longer holds the HostEnv
An EntryPointsCaller used to keep the HostEnv it was built with. Backends store the callers of
the deployed contracts, so the environment and the backend kept each other alive and were never
freed. The caller now keeps only the entry points and the function, and the backend passes the
ContractEnv on every call:
| 2.9 | 2.10 |
|---|---|
EntryPointsCaller::new(env, entry_points, f) | EntryPointsCaller::new(entry_points, f) |
caller.call(call_def) | caller.call(contract_env, call_def) |
caller.host_env() | removed |
HostRef::entry_points_caller(&env) | HostRef::entry_points_caller() |
EntryPointsCallerFn = fn(&HostEnv) -> EntryPointsCaller | fn() -> EntryPointsCaller |
ContractRegister::call(address, call_def), ContractContainer::call(call_def) | take a ContractEnv before the CallDef |
OdraVm::default() | OdraVm::new() |
Contracts and tests that use #[odra::module], Deployer, HostRef and HostRefLoader::load are
not affected: the macros generate the new form. Only hand-written callers, code that calls
entry_points_caller itself, and custom backends need the changes above.