-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
57 changed files
with
9,726 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
docusaurus/versioned_docs/version-1.2.0/advanced/01-delegate.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
# Delegate | ||
|
||
Managing boilerplate code can often lead to code that is cumbersome and challenging to comprehend. The Odra library introduces a solution to this issue with its Delegate feature. As the name implies, the Delegate feature permits the delegation of function calls to child modules, effectively minimizing the redundancy of boilerplate code and maintaining a lean and orderly parent module. | ||
|
||
The main advantage of this feature is that it allows you to inherit the default behavior of child modules seamlessly, making your contracts more readable. | ||
|
||
## Overview | ||
|
||
To utilize the delegate feature in your contract, use the `delegate!` macro provided by Odra. This macro allows you to list the functions you wish to delegate to the child modules. By using the `delegate!` macro, your parent module remains clean and easy to understand. | ||
|
||
You can delegate functions to as many child modules as you like. The functions will be available as if they were implemented in the parent module itself. | ||
|
||
## Code Examples | ||
|
||
Consider the following basic example for better understanding: | ||
|
||
```rust | ||
use crate::{erc20::Erc20, ownable::Ownable}; | ||
use odra::{ | ||
Address, casper_types::U256, | ||
module::SubModule, | ||
prelude::* | ||
}; | ||
|
||
#[odra::module] | ||
pub struct OwnedToken { | ||
ownable: SubModule<Ownable>, | ||
erc20: SubModule<Erc20> | ||
} | ||
|
||
#[odra::module] | ||
impl OwnedToken { | ||
pub fn init(&mut self, name: String, symbol: String, decimals: u8, initial_supply: U256) { | ||
let deployer = self.env().caller(); | ||
self.ownable.init(deployer); | ||
self.erc20.init(name, symbol, decimals, initial_supply); | ||
} | ||
|
||
delegate! { | ||
to self.erc20 { | ||
fn transfer(&mut self, recipient: Address, amount: U256); | ||
fn transfer_from(&mut self, owner: Address, recipient: Address, amount: U256); | ||
fn approve(&mut self, spender: Address, amount: U256); | ||
fn name(&self) -> String; | ||
fn symbol(&self) -> String; | ||
fn decimals(&self) -> u8; | ||
fn total_supply(&self) -> U256; | ||
fn balance_of(&self, owner: Address) -> U256; | ||
fn allowance(&self, owner: Address, spender: Address) -> U256; | ||
} | ||
|
||
to self.ownable { | ||
fn get_owner(&self) -> Address; | ||
fn change_ownership(&mut self, new_owner: Address); | ||
} | ||
} | ||
|
||
pub fn mint(&mut self, address: Address, amount: U256) { | ||
self.ownable.ensure_ownership(self.env().caller()); | ||
self.erc20.mint(address, amount); | ||
} | ||
} | ||
``` | ||
|
||
This `OwnedToken` contract includes two modules: `Erc20` and `Ownable`. We delegate various functions from both modules using the `delegate!` macro. As a result, the contract retains its succinctness without compromising on functionality. | ||
|
||
The above example basically merges the functionalities of modules and adds some control over the minting process. But you can use delegation to build more complex contracts, cherry-picking just a few module functionalities. | ||
|
||
Let's take a look at another example. | ||
|
||
```rust | ||
use crate::{erc20::Erc20, ownable::Ownable, exchange::Exchange}; | ||
use odra::{ | ||
Address, casper_types::U256, | ||
module::SubModule, | ||
prelude::* | ||
}; | ||
|
||
#[odra::module] | ||
pub struct DeFiPlatform { | ||
ownable: SubModule<Ownable>, | ||
erc20: SubModule<Erc20>, | ||
exchange: SubModule<Exchange> | ||
} | ||
|
||
#[odra::module] | ||
impl DeFiPlatform { | ||
pub fn init(&mut self, name: String, symbol: String, decimals: u8, initial_supply: U256, exchange_rate: u64) { | ||
let deployer = self.env().caller(); | ||
self.ownable.init(deployer); | ||
self.erc20.init(name, symbol, decimals, initial_supply); | ||
self.exchange.init(exchange_rate); | ||
} | ||
|
||
delegate! { | ||
to self.erc20 { | ||
fn transfer(&mut self, recipient: Address, amount: U256); | ||
fn balance_of(&self, owner: Address) -> U256; | ||
} | ||
|
||
to self.ownable { | ||
fn get_owner(&self) -> Address; | ||
} | ||
|
||
to self.exchange { | ||
fn swap(&mut self, sender: Address, recipient: Address); | ||
fn set_exchange_rate(&mut self, new_rate: u64); | ||
} | ||
} | ||
|
||
pub fn mint(&mut self, address: Address, amount: U256) { | ||
self.ownable.ensure_ownership(self.env().caller()); | ||
self.erc20.mint(address, amount); | ||
} | ||
} | ||
``` | ||
|
||
In this `DeFiPlatform` contract, we include `Erc20`, `Ownable`, and `Exchange` modules. By delegating functions from these modules, the parent contract becomes a powerhouse of functionality while retaining its readability and structure. | ||
|
||
Remember, the possibilities are endless with Odra's. By leveraging this feature, you can write cleaner, more efficient, and modular smart contracts. |
127 changes: 127 additions & 0 deletions
127
docusaurus/versioned_docs/version-1.2.0/advanced/02-advanced-storage.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
# Advanced Storage Concepts | ||
|
||
The Odra Framework provides advanced storage interaction capabilities that extend beyond the basic storage interaction. This document will focus on the `Mapping` and `Sequence` modules, which are key components of the advanced storage interaction in Odra. | ||
|
||
## Recap and Basic Concepts | ||
|
||
Before we delve into the advanced features, let's recap some basic storage concepts in Odra. In the realm of basic storage interaction, Odra provides several types for interacting with contract storage, including `Var`, `Mapping`, and `List`. These types enable contracts to store and retrieve data in a structured manner. The Var type is used to store a single value, while the List and Mapping types store collections of values. | ||
|
||
**Var**: A Var in Odra is a fundamental building block used for storing single values. Each Var is uniquely identified by its name in the contract. | ||
|
||
**Mapping**: Mapping in Odra serves as a key-value storage system. It stores an association of unique keys to values, and the value can be retrieved using the key. | ||
|
||
**List**: Built on top of the Var and Mapping building blocks, List in Odra allows storing an ordered collection of values that can be iterated over. | ||
|
||
If you need a refresher on these topics, please refer to our [guide](../basics/05-storage-interaction.md) on basic storage in Odra. | ||
|
||
## Advanced Storage Concepts | ||
|
||
### Sequence | ||
|
||
The `Sequence` in Odra is a basic module that stores a single value in the storage that can be read or incremented. Internally, holds a `Var` which keeps track of the current value. | ||
|
||
```rust | ||
pub struct Sequence<T> | ||
where | ||
T: Num + One + ToBytes + FromBytes + CLTyped | ||
{ | ||
value: Var<T> | ||
} | ||
``` | ||
|
||
The Sequence module provides functions `get_current_value` and `next_value` to get the current value and increment the value respectively. | ||
|
||
### Advanced Mapping | ||
|
||
In Odra, a `Mapping` is a key-value storage system where the key is associated with a value. | ||
In previous examples, the value of the `Mapping` typically comprised a standard serializable type (such as number, string, or bool) or a custom type marked with the `#[odra::odra_type]` attribute. | ||
|
||
However, there are more advanced scenarios where the value of the Mapping represents a module itself. This approach is beneficial when managing a collection of modules, each maintaining its unique state. | ||
|
||
Let's consider the following example: | ||
|
||
```rust title="examples/src/features/storage/mapping.rs" | ||
use odra::{casper_types::U256, Mapping, UnwrapOrRevert}; | ||
use odra::prelude::*; | ||
use crate::owned_token::OwnedToken; | ||
|
||
#[odra::module] | ||
pub struct Mappings { | ||
strings: Mapping<(String, u32, String), String>, | ||
tokens: Mapping<String, OwnedToken> | ||
} | ||
|
||
#[odra::module] | ||
impl Mappings { | ||
|
||
... | ||
|
||
pub fn total_supply(&mut self, token_name: String) -> U256 { | ||
self.tokens.module(&token_name).total_supply() | ||
} | ||
|
||
pub fn get_string_api( | ||
&self, | ||
key1: String, | ||
key2: u32, | ||
key3: String | ||
) -> String { | ||
let opt_string = self.strings.get(&(key1, key2, key3)); | ||
opt_string.unwrap_or_revert(&self.env()) | ||
} | ||
} | ||
``` | ||
|
||
As you can see, a `Mapping` key can consist of a tuple of values, not limited to a single value. | ||
|
||
:::note | ||
Accessing Odra modules differs from accessing regular values such as strings or numbers. | ||
|
||
Firstly, within a `Mapping`, you don't encapsulate the module with `Submodule`. | ||
|
||
Secondly, rather than utilizing the `Mapping::get()` function, call `Mapping::module()`, which returns `SubModule<T>` and sets the appropriate namespace for nested modules. | ||
::: | ||
|
||
## AdvancedStorage Contract | ||
|
||
The given code snippet showcases the `AdvancedStorage` contract that incorporates these storage concepts. | ||
|
||
```rust | ||
use odra::{Address, casper_types::U512, Sequence, Mapping}; | ||
use odra::prelude::*; | ||
use crate::modules::Token; | ||
|
||
#[odra::module] | ||
pub struct AdvancedStorage { | ||
counter: Sequence<u32>, | ||
tokens: Mapping<(String, String), Token>, | ||
} | ||
|
||
impl AdvancedStorage { | ||
pub fn current_value(&self) -> u32 { | ||
self.counter.get_current_value() | ||
} | ||
|
||
pub fn increment_and_get(&mut self) -> u32 { | ||
self.counter.next_value() | ||
} | ||
|
||
pub fn balance_of(&mut self, token_name: String, creator: String, address: Address) -> U512 { | ||
let token = self.tokens.module(&(token_name, creator)); | ||
token.balance_of(&address) | ||
} | ||
|
||
pub fn mint(&self, token_name: String, creator: String, amount: U512, to: Address) { | ||
let mut token = self.tokens.module(&(token_name, creator)); | ||
token.mint(amount, to); | ||
} | ||
} | ||
``` | ||
|
||
## Conclusion | ||
|
||
Advanced storage features in Odra offer robust options for managing contract state. Two key takeaways from this document are: | ||
1. Odra offers a Sequence module, enabling contracts to store and increment a single value. | ||
2. Mappings support composite keys expressed as tuples and can store modules as values. | ||
|
||
Understanding these concepts can help developers design and implement more efficient and flexible smart contracts. |
117 changes: 117 additions & 0 deletions
117
docusaurus/versioned_docs/version-1.2.0/advanced/03-attributes.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
# 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 | ||
|
||
```rust title=examples/src/contracts/tlw.rs | ||
#[odra(payable)] | ||
pub fn deposit(&mut self) { | ||
// Extract values | ||
let caller: Address = self.env().caller(); | ||
let amount: U256 = 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 | ||
|
||
```rust | ||
#[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(); | ||
ReentrancyMockRef::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}, ExecutionError}; | ||
|
||
#[test] | ||
fn ref_recursion_not_allowed() { | ||
let test_env = odra_test::env(); | ||
let mut contract = NonReentrantCounterHostRef::deploy(&test_env, NoArgs); | ||
|
||
let result = contract.count_ref_recursive(11); | ||
assert_eq!(result, ExecutionError::ReentrantCall.into()); | ||
} | ||
} | ||
``` | ||
|
||
## Mixing attributes | ||
|
||
A function can accept more than one attribute. The only exclusion is a constructor cannot be payable. | ||
To apply multiple attributes, you can write: | ||
|
||
```rust | ||
#[odra(payable, non_reentrant)] | ||
fn deposit() { | ||
// your logic... | ||
} | ||
``` | ||
|
||
or | ||
|
||
```rust | ||
#[odra(payable)] | ||
#[odra(non_reentrant)] | ||
fn deposit() { | ||
// your logic... | ||
} | ||
``` | ||
|
||
In both cases attributes order does not matter. |
Oops, something went wrong.