Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CEP-78 #410

Merged
merged 41 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
31aef8b
cep-78 basics
kpob Apr 12, 2024
c00f941
cep-78 entrypoints implementation
kpob Apr 12, 2024
17bd2e8
reverse lookup wip
kpob Apr 15, 2024
70bf5f2
Skip args validation
kpob Apr 16, 2024
abc8b1d
Impl Default for Maybe
kpob Apr 16, 2024
116987e
update token impl
kpob Apr 16, 2024
4eccc45
ACL test wip
kpob Apr 16, 2024
f4b17b3
Add missing acl test
kpob Apr 17, 2024
4aca069
test set_varialbes endpoint
kpob Apr 17, 2024
ca7e6a7
minting tests
kpob Apr 18, 2024
637fdbe
Add burn tests
kpob Apr 19, 2024
e5d7166
Add events tests
kpob Apr 19, 2024
00cdaee
metadata tests
kpob Apr 19, 2024
2949e1b
Add transfer tests
kpob Apr 23, 2024
039e933
refactor
kpob Apr 24, 2024
facb85c
fix tests
kpob Apr 24, 2024
961f3c1
Refactor CEP78 module and fix tests
kpob Apr 24, 2024
24fa20e
fix linter issues
kpob Apr 24, 2024
af5e8e5
Reorganize test contracts and rename CEP78 to Cep78
kpob Apr 25, 2024
622ac03
wip
kpob Apr 25, 2024
3865982
Refactor contract deployment functions to return Result types
kpob Apr 25, 2024
f9f740e
Add missing installation time checks and tests
kpob Apr 25, 2024
32489d4
Add missing tests
kpob Apr 25, 2024
dd33780
make clippy happy again
kpob Apr 25, 2024
bd86080
Merge branch 'release/1.0.0' into feature/cep-78
kpob Apr 26, 2024
0aef4cb
cep78 using named keys - wip
kpob Apr 26, 2024
297bb72
Use named keys instead of Var and Mappings
kpob May 6, 2024
5aadae9
Allow token id as hash + mutable metadata. (#421)
zie1ony May 6, 2024
958dd73
fmt, add remove_dictionary to ContractEnv
kpob May 6, 2024
7aae548
bug fix
kpob May 6, 2024
ff35352
fix tests
kpob May 7, 2024
acd62a9
`get_named_key` does not revert if the key does not exists
kpob May 7, 2024
958a817
register events
kpob May 7, 2024
7dda987
Merge branch 'release/1.0.0' into feature/cep-78
kpob May 7, 2024
67c04cf
Update some args names to match the casper impl args
kpob May 7, 2024
a777639
Add livenet example
kpob May 7, 2024
f561155
Fix variable name in NftContract trait
kpob May 7, 2024
9be2222
Update CEP-78 livenet example
kpob May 8, 2024
f653b6e
Compile cargo-odra with stable
zie1ony May 9, 2024
29adf33
Merge branch 'release/1.0.0' into feature/cep-78
zie1ony May 10, 2024
08e3f49
Remove 'new' from cep78 events.
zie1ony May 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ examples/.env.testnet
examples/.env.integration
modules/.builder_casper
modules/wasm
benchmark/gas_report.json
benchmark/gas_report.json
.DS_Store
4 changes: 2 additions & 2 deletions benchmark/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ impl DictionaryStorage {
/// Sets the value.
pub fn set(&self, key: String, value: U256) {
self.env()
.set_dictionary_value(DICT_KEY, self.key(key), value);
.set_dictionary_value(DICT_KEY, self.key(key).as_bytes(), value);
}

/// Gets the value.
pub fn get_or_default(&self, key: String) -> U256 {
self.env()
.get_dictionary_value(DICT_KEY, self.key(key))
.get_dictionary_value(DICT_KEY, self.key(key).as_bytes())
.unwrap_or_default()
}

Expand Down
11 changes: 10 additions & 1 deletion core/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ use casper_types::{
};

/// A type that represents an entrypoint arg that may or may not be present.
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub enum Maybe<T> {
/// A value is present.
Some(T),
/// No value is present.
#[default]
None
}

Expand All @@ -34,6 +35,14 @@ impl<T> Maybe<T> {
Maybe::None => env.revert(ExecutionError::UnwrapError)
}
}

/// Unwraps the value or returns the default value.
pub fn unwrap_or(self, default: T) -> T {
match self {
Maybe::Some(value) => value,
Maybe::None => default
}
}
}

impl<T: Default> Maybe<T> {
Expand Down
111 changes: 6 additions & 105 deletions core/src/contract_container.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use crate::entry_point_callback::{Argument, EntryPointsCaller};
use crate::{prelude::*, ExecutionError, OdraResult};
use crate::entry_point_callback::EntryPointsCaller;
use crate::{prelude::*, OdraResult};
use crate::{CallDef, OdraError, VmError};
use casper_types::bytesrepr::Bytes;
use casper_types::RuntimeArgs;

/// A wrapper struct for a EntryPointsCaller that is a layer of abstraction between the host and the entry points caller.
///
Expand All @@ -23,56 +22,25 @@ impl ContractContainer {
/// Calls the entry point with the given call definition.
pub fn call(&self, call_def: CallDef) -> OdraResult<Bytes> {
// find the entry point
let ep = self
.entry_points_caller
self.entry_points_caller
.entry_points()
.iter()
.find(|ep| ep.name == call_def.entry_point())
.ok_or_else(|| {
OdraError::VmError(VmError::NoSuchMethod(call_def.entry_point().to_string()))
OdraError::VmError(VmError::NoSuchMethod(call_def.entry_point().to_owned()))
})?;
// validate the args, return an error if the args are invalid
self.validate_args(&ep.args, call_def.args())?;
self.entry_points_caller.call(call_def)
}

fn validate_args(&self, args: &[Argument], input_args: &RuntimeArgs) -> OdraResult<()> {
for arg in args {
// check if the input args contain the arg
if let Some(input) = input_args
.named_args()
.find(|input| input.name() == arg.name.as_str())
{
// check if the input arg has the expected type
let input_ty = input.cl_value().cl_type();
let expected_ty = &arg.ty;
if input_ty != expected_ty {
return Err(OdraError::VmError(VmError::TypeMismatch {
expected: expected_ty.clone(),
found: input_ty.clone()
}));
}
} else {
return Err(OdraError::ExecutionError(ExecutionError::MissingArg));
}
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use casper_types::CLType;

use super::ContractContainer;
use crate::contract_context::MockContractContext;
use crate::entry_point_callback::{Argument, EntryPoint, EntryPointsCaller};
use crate::host::{HostEnv, MockHostContext};
use crate::{
casper_types::{runtime_args, RuntimeArgs},
OdraError, VmError
};
use crate::{prelude::*, CallDef, ContractEnv, ExecutionError};
use crate::{casper_types::RuntimeArgs, OdraError, VmError};
use crate::{prelude::*, CallDef, ContractEnv};

const TEST_ENTRYPOINT: &str = "ep";

Expand Down Expand Up @@ -102,73 +70,6 @@ mod tests {
assert!(result.is_ok());
}

#[test]
fn test_call_valid_entrypoint_with_wrong_arg_name() {
// Given an instance with a single entrypoint with one arg named "first".
let instance = ContractContainer::with_entrypoint(vec!["first"]);

// When call the registered entrypoint with an arg named "second".
let call_def = CallDef::new(TEST_ENTRYPOINT, false, runtime_args! { "second" => 0u32 });
let result = instance.call(call_def);

// Then MissingArg error is returned.
assert_eq!(
result.unwrap_err(),
OdraError::ExecutionError(ExecutionError::MissingArg)
);
}

#[test]
fn test_call_valid_entrypoint_with_wrong_arg_type() {
// Given an instance with a single entrypoint with one arg named "first".
let instance = ContractContainer::with_entrypoint(vec!["first"]);

// When call the registered entrypoint with an arg named "second".
let call_def = CallDef::new(TEST_ENTRYPOINT, false, runtime_args! { "first" => true });
let result = instance.call(call_def);

// Then MissingArg error is returned.
assert_eq!(
result.unwrap_err(),
OdraError::VmError(VmError::TypeMismatch {
expected: CLType::U32,
found: CLType::Bool
})
);
}

#[test]
fn test_call_valid_entrypoint_with_missing_arg() {
// Given an instance with a single entrypoint with one arg named "first".
let instance = ContractContainer::with_entrypoint(vec!["first"]);

// When call a valid entrypoint without args.
let call_def = CallDef::new(TEST_ENTRYPOINT, false, RuntimeArgs::new());
let result = instance.call(call_def);

// Then MissingArg error is returned.
assert_eq!(
result.unwrap_err(),
OdraError::ExecutionError(ExecutionError::MissingArg)
);
}

#[test]
fn test_many_missing_args() {
// Given an instance with a single entrypoint with "first", "second" and "third" args.
let instance = ContractContainer::with_entrypoint(vec!["first", "second", "third"]);

// When call a valid entrypoint with a single valid args,
let call_def = CallDef::new(TEST_ENTRYPOINT, false, runtime_args! { "third" => 0u32 });
let result = instance.call(call_def);

// Then MissingArg error is returned.
assert_eq!(
result.unwrap_err(),
OdraError::ExecutionError(ExecutionError::MissingArg)
);
}

impl ContractContainer {
fn empty() -> Self {
let ctx = Rc::new(RefCell::new(MockHostContext::new()));
Expand Down
10 changes: 8 additions & 2 deletions core/src/contract_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ pub trait ContractContext {
///
/// * `dictionary_name` - The name of the dictionary.
/// * `key` - The key to retrieve the value for.
fn get_dictionary_value(&self, dictionary_name: &str, key: &str) -> Option<Bytes>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure this works by running a livenet example of cep-18 against original contract implementation.

fn get_dictionary_value(&self, dictionary_name: &str, key: &[u8]) -> Option<Bytes>;

/// Sets the key value behind a named dictionary.
///
Expand All @@ -58,7 +58,13 @@ pub trait ContractContext {
/// * `dictionary_name` - The name of the dictionary.
/// * `key` - The key to set the value for.
/// * `value` - The value to set.
fn set_dictionary_value(&self, dictionary_name: &str, key: &str, value: CLValue);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why only changed in dictionaries, what about named keys?

fn set_dictionary_value(&self, dictionary_name: &str, key: &[u8], value: CLValue);

/// Removes the named key from the storage.
///
/// # Arguments
/// * `dictionary_name` - The name of the dictionary.
fn remove_dictionary(&self, dictionary_name: &str);

/// Retrieves the address of the caller.
fn caller(&self) -> Address;
Expand Down
16 changes: 10 additions & 6 deletions core/src/contract_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,12 @@ impl ContractEnv {
}

/// Retrieves the value associated with the given named key from the named dictionary in the contract storage.
pub fn get_dictionary_value<T: FromBytes + CLTyped, U: AsRef<str>, V: AsRef<str>>(
pub fn get_dictionary_value<T: FromBytes + CLTyped, U: AsRef<str>>(
&self,
dictionary_name: U,
key: V
key: &[u8]
) -> Option<T> {
let dictionary_name = dictionary_name.as_ref();
let key = key.as_ref();
let bytes = self
.backend
.borrow()
Expand All @@ -123,14 +122,13 @@ impl ContractEnv {
}

/// Sets the value associated with the given named key in the named dictionary in the contract storage.
pub fn set_dictionary_value<T: CLTyped + ToBytes, U: AsRef<str>, V: AsRef<str>>(
pub fn set_dictionary_value<T: CLTyped + ToBytes, U: AsRef<str>>(
&self,
dictionary_name: U,
key: V,
key: &[u8],
Comment on lines +125 to +128
Copy link
Contributor

@coderabbitai coderabbitai bot May 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

Codebase Verification

The examination of the set_dictionary_value function across different environments and modules in the codebase reveals varying approaches to error handling:

  1. WASM and VM Environments: These environments use the function normally, passing the parameters to underlying systems without explicit error handling in the function itself. Errors are likely handled at a lower level or expected to be managed by the calling function.

  2. Host Functions: Here, there is a check for the dictionary item key length, and an error is reverted if the key exceeds the maximum length. This shows explicit error handling within the function.

  3. Livenet Environment: This environment explicitly panics if the function is called, indicating that such operations are not supported in this environment under normal circumstances.

  4. Usage in Modules: In various modules, the function is used without additional error handling, suggesting reliance on the underlying implementations to manage errors.

Given these observations, the error handling for set_dictionary_value is not entirely consistent across the project. Different environments handle errors in distinct ways, ranging from no explicit handling to specific checks and panic conditions. This inconsistency might be intentional due to the differing nature of operations in each environment, but it could benefit from a review to ensure that error handling strategies are clear and appropriate for each context.

Recommendation: Review the error handling strategies used in different environments for set_dictionary_value to ensure they are appropriate and consider implementing more uniform error handling practices if feasible.

  • WASM and VM Environments: Confirm if error handling at lower levels is sufficient.
  • Host Functions: Maintain or adjust the explicit error checks as necessary.
  • Livenet Environment: Ensure that the panic conditions are well-documented and understood by users of the environment.
  • Module Usages: Consider adding error handling or documentation to clarify how errors should be managed.
Analysis chain

Ensure consistent error handling for set_dictionary_value.

Scripts Executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Description: Verify consistent error handling for `set_dictionary_value` across the project.

# Test: Search for the function usage. Expect: Consistent error handling.
rg --type rust $'set_dictionary_value'

Length of output: 1715



Script:

#!/bin/bash
# Description: Examine the implementations and usages of `set_dictionary_value` to verify error handling.

# Search for the function implementations and their surrounding code to check error handling mechanisms.
rg --type rust -C 10 "fn set_dictionary_value" 

# Search for usages of `set_dictionary_value` and check how errors are handled or propagated.
rg --type rust -C 10 "set_dictionary_value\("

Length of output: 27173

Copy link
Contributor

@kubaplas kubaplas May 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DEEP RABBIT

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kubaplas, it seems like my previous message might have come as a surprise! If there's any part of the analysis you'd like to discuss further or if you have specific questions about the error handling strategies in different environments, feel free to let me know. I'm here to help!

value: T
) {
let dictionary_name = dictionary_name.as_ref();
let key = key.as_ref();
let cl_value = CLValue::from_t(value)
.map_err(|_| Formatting)
.unwrap_or_revert(self);
Expand All @@ -139,6 +137,12 @@ impl ContractEnv {
.set_dictionary_value(dictionary_name, key, cl_value);
}

/// Removes the dictionary from the contract storage.
pub fn remove_dictionary<U: AsRef<str>>(&self, dictionary_name: U) {
let dictionary_name = dictionary_name.as_ref();
self.backend.borrow().remove_dictionary(dictionary_name);
}
Comment on lines +140 to +144
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

Codebase Verification

The investigation into the remove_dictionary function across various modules reveals that while the function's conceptual role remains consistent (i.e., removing a dictionary from some form of storage), the implementation details vary. These variations are primarily in the parameters accepted (string references vs. byte arrays) and the internal handling within the function. Here are the key observations:

  • VM and Contract Environments: In odra-vm and odra-casper modules, the function primarily accepts string references. However, in the VM state management (odra-vm/src/vm/odra_vm_state.rs), a byte array is used, likely due to lower-level data handling requirements.
  • Host Functions and WASM Environment: The WASM environment uses host functions to bridge between WASM and the host environment, which consistently use string references.
  • Core and Module-Specific Implementations: The core implementation in core/src/contract_env.rs is flexible, allowing any type that can be referenced as a string, which is a higher-level abstraction compared to other implementations.

These differences seem justified based on the specific needs and contexts of each module. Therefore, while the implementations are not identical, they are consistent in purpose and adapted to their respective environments.

Analysis chain

Verify that remove_dictionary is implemented consistently across different modules.

Scripts Executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Description: Verify consistent implementation of `remove_dictionary` across different modules.

# Test: Search for the function usage. Expect: Consistent implementation.
rg --type rust $'remove_dictionary'

Length of output: 1254


/// Returns the address of the caller of the contract.
pub fn caller(&self) -> Address {
let backend = self.backend.borrow();
Expand Down
6 changes: 6 additions & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ path = "bin/cep18_on_livenet.rs"
required-features = ["livenet"]
test = false

[[bin]]
name = "cep78_on_livenet"
path = "bin/cep78_on_livenet.rs"
required-features = ["livenet"]
test = false

[[bin]]
name = "livenet_tests"
path = "bin/livenet_tests.rs"
Expand Down
79 changes: 79 additions & 0 deletions examples/bin/cep78_on_livenet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//! Deploys a CEP-78 contract, mints an nft token and transfers it to another address.
use std::str::FromStr;

use odra::args::Maybe;
use odra::casper_types::U256;
use odra::host::{Deployer, HostEnv, HostRef, HostRefLoader};
use odra::Address;
use odra_modules::cep78::modalities::{
EventsMode, MetadataMutability, NFTIdentifierMode, NFTKind, NFTMetadataKind, OwnershipMode
};
use odra_modules::cep78::token::{Cep78HostRef, Cep78InitArgs};
use odra_modules::cep78::utils::InitArgsBuilder;

const CEP78_METADATA: &str = r#"{
"name": "John Doe",
"token_uri": "https://www.barfoo.com",
"checksum": "940bffb3f2bba35f84313aa26da09ece3ad47045c6a1292c2bbd2df4ab1a55fb"
}"#;
const CASPER_CONTRACT_ADDRESS: &str =
"hash-d4b8fa492d55ac7a515c0c6043d72ba43c49cd120e7ba7eec8c0a330dedab3fb";
const ODRA_CONTRACT_ADDRESS: &str =
"hash-3d35238431c5c6fa1d7df70d73bfc2efd5a03fd5af99ab8c7828a56b2f330274";
const RECIPIENT_ADDRESS: &str =
"hash-7821386ecdda83ff100379a06558b69a675d5a170d1c5bf5fbe9fd35262d091f";

fn main() {
let env = odra_casper_livenet_env::env();

// Deploy new contract.
let mut token = deploy_contract(&env);
println!("Token address: {}", token.address().to_string());

// Uncomment to load existing contract.
// let mut token = load_contract(&env, CASPER_CONTRACT_ADDRESS);
// println!("Token name: {}", token.get_collection_name());

env.set_gas(3_000_000_000u64);
let owner = env.caller();
let recipient =
Address::from_str(RECIPIENT_ADDRESS).expect("Should be a valid recipient address");
// casper contract may return a result or not, so deserialization may fail and it's better to use `try_transfer`/`try_mint`/`try_burn` methods
let _ = token.try_mint(owner, CEP78_METADATA.to_string(), Maybe::None);
println!("Owner's balance: {:?}", token.balance_of(owner));
println!("Recipient's balance: {:?}", token.balance_of(recipient));
let token_id = token.get_number_of_minted_tokens() - 1;
let _ = token.try_transfer(Maybe::Some(token_id), Maybe::None, owner, recipient);

println!("Owner's balance: {:?}", token.balance_of(owner));
println!("Recipient's balance: {:?}", token.balance_of(recipient));
}
Comment on lines +26 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve error handling and avoid hardcoded values.

Consider enhancing the error handling around try_mint and try_transfer methods to ensure that failures are handled gracefully. Additionally, avoid using hardcoded values such as gas amounts and instead retrieve them from a configuration or environment variable to increase flexibility.


/// Loads a Cep78 contract.
pub fn load_contract(env: &HostEnv, address: &str) -> Cep78HostRef {
let address = Address::from_str(address).expect("Should be a valid contract address");
Cep78HostRef::load(env, address)
}

/// Deploys a Cep78 contract.
pub fn deploy_contract(env: &HostEnv) -> Cep78HostRef {
let name: String = String::from("PlascoinCollection with CES");
let symbol = String::from("CEP78-PLS-CES");
let receipt_name = String::from("PlascoinReceipt");

let init_args = InitArgsBuilder::default()
.collection_name(name)
.collection_symbol(symbol)
.total_token_supply(1_000)
.ownership_mode(OwnershipMode::Transferable)
.nft_metadata_kind(NFTMetadataKind::CEP78)
.identifier_mode(NFTIdentifierMode::Ordinal)
.nft_kind(NFTKind::Digital)
.metadata_mutability(MetadataMutability::Mutable)
.receipt_name(receipt_name)
.events_mode(EventsMode::CES)
.build();

env.set_gas(400_000_000_000u64);
Cep78HostRef::deploy(env, init_args)
}
7 changes: 6 additions & 1 deletion modules/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,16 @@ categories = ["wasm"]

[dependencies]
odra = { path = "../odra", version = "0.9.1", default-features = false }
hex = { version = "0.4.3", default-features = false }
serde = { version = "1.0.80", default-features = false }
serde_json = { version = "1.0.59", default-features = false }
serde-json-wasm = { version = "1.0.1", default-features = false }
base16 = { version = "0.2.1", default-features = false }
base64 = { version = "0.22.0", default-features = false, features = ["alloc"] }

[dev-dependencies]
odra-test = { path = "../odra-test", version = "0.9.1" }
once_cell = "1"
blake2 = "0.10.6"

[build-dependencies]
odra-build = { path = "../odra-build", version = "0.9.1" }
Expand Down
12 changes: 12 additions & 0 deletions modules/Odra.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ fqn = "access::Ownable"
[[contracts]]
fqn = "ownable_2step::Ownable2Step"

[[contracts]]
fqn = "cep78::token::Cep78"

[[contracts]]
fqn = "cep78::utils::MockContract"

[[contracts]]
fqn = "cep78::utils::MockDummyContract"

[[contracts]]
fqn = "cep78::utils::MockTransferFilterContract"

[[contracts]]
fqn = "cep18_token::Cep18"

Expand Down
Loading
Loading