Skip to main content
Version: 2.10.*

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:

tip

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.

note

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 wasm directory, 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 for wasm/<Contract>.wasm in 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 build and cargo odra clean delete the copies an older cargo-odra left in the members.
  • Contracts from dependency crates. The first segment of an fqn in Odra.toml may 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; in bin/build_contract.rs and bin/build_schema.rs), see Odra.toml.
  • ODRA_MODULE is 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 new resolves the latest Odra release from the releases/latest redirect 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.0 to 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_value and events_count return Result<Option<_>>: Ok(None) is a value the node does not have, Err is a node that could not be asked. Handle both cases where you used to get the value directly.
  • CasperClient::deploy_wasm takes &self instead of &mut self.
  • The blocking CasperClient calls panic inside a current-thread Tokio runtime and point to the new xxx_async methods. Use those, or a multi-thread runtime (#[tokio::main]).
  • A deploy or upgrade on livenet whose init or upgrade reverts 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 returns OutOfGas. It used to be ContractDeploymentError("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::Other instead of panicking. See Livenet errors.
  • A getter or #[odra(offchain)] function that reverts on livenet returns Err from its try_* call instead of panicking, and sees the calling account as caller() (it used to see its own address).
  • Reading a stored value as the wrong type reverts with the concrete bytesrepr error (LeftOverBytes, EarlyEndOfStream, ...) instead of Formatting. A named argument that exists but has the wrong type reverts with ExecutionError::InvalidArg (138) instead of MissingArg.

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.92.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) -> EntryPointsCallerfn() -> 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.