Skip to main content
Version: 2.9.*

Attributes

Smart contract developers with Ethereum background are familiar with Solidity's concept of modifiers in Solidity - a feature that allows developers to embed common checks into function definitions in a readable and reusable manner. These are essentially prerequisites for function execution.

Odra defines a few attributes that can be applied to functions to equip them with superpowers.

Payable

When writing a smart contract, you need to make sure that money can be both sent to and extracted from the contract. The 'payable' attribute helps wit this. Any function, except for a constructor, with the #[odra(payable)] attribute can send and take money in the form of native tokens.

Example

#[odra(payable)]
pub fn deposit(&mut self) {
// Extract values
let caller: Address = self.env().caller();
let amount: U512 = self.env().attached_value();
let current_block_time: u64 = self.env().get_block_time();

// Multiple lock check
if self.balances.get(&caller).is_some() {
self.env().revert(Error::CannotLockTwice)
}

// Update state, emit event
self.balances.set(&caller, amount);
self.lock_expiration_map
.set(&caller, current_block_time + self.lock_duration());
self.env()
.emit_event(Deposit {
address: caller,
amount
});
}

If you try to send tokens to a non-payable function, the transaction will be automatically rejected.

Non Reentrant

Reentrancy attacks in smart contracts exploit the possibility of a function being called multiple times before its initial execution is completed, leading to the repeated unauthorized withdrawal of funds.

To prevent such attacks, developers should ensure that all effects on the contract's state and balance checks occur before calling external contracts.

They can also use reentrancy guards to block recursive calls to sensitive functions.

In Odra you can just apply the #[odra(non_reentrant)] attribute to your function.

Example

use odra::prelude::*;
use odra::ContractRef;

#[odra::module]
pub struct NonReentrantCounter {
counter: Var<u32>
}

#[odra::module]
impl NonReentrantCounter {
#[odra(non_reentrant)]
pub fn count_ref_recursive(&mut self, n: u32) {
if n > 0 {
self.count();
NonReentrantCounterContractRef::new(self.env(), self.env().self_address())
.count_ref_recursive(n - 1);
}
}
}

impl NonReentrantCounter {
fn count(&mut self) {
let c = self.counter.get_or_default();
self.counter.set(c + 1);
}
}

#[cfg(test)]
mod test {
use super::*;
use odra::host::{Deployer, NoArgs};

#[test]
fn ref_recursion_not_allowed() {
let test_env = odra_test::env();
let mut contract = NonReentrantCounter::deploy(&test_env, NoArgs);

let result = contract.try_count_ref_recursive(11);
assert_eq!(result.unwrap_err(), ExecutionError::ReentrantCall.into());
}
}

Mixing attributes

A function can accept more than one attribute, with one exception: a constructor cannot be payable.

caution

Since Odra 2.9.1, marking init or upgrade with #[odra(payable)] or #[odra(non_reentrant)] is a compile error - those entrypoints are generated by a path that never applied the attributes, so accepting them would mean silently ignoring them. There is no way to attach tokens to a deploy in the first place, so fund the contract with a separate payable entrypoint called right after deployment.

In Odra 2.9.0 and earlier the same code compiles and the attribute is quietly dropped.

To apply multiple attributes, you can write:

#[odra(payable, non_reentrant)]
fn deposit() {
// your logic...
}

or

#[odra(payable)]
#[odra(non_reentrant)]
fn deposit() {
// your logic...
}

In both cases attributes order does not matter.