diff --git a/crates/compiler/src/compiler/annotation.rs b/crates/compiler/src/compiler/annotation.rs index e809510..df7b6ea 100644 --- a/crates/compiler/src/compiler/annotation.rs +++ b/crates/compiler/src/compiler/annotation.rs @@ -35,6 +35,8 @@ const DOJO_ANNOTATION_FILE_NAME: &str = "annotations"; pub trait AnnotationInfo { fn filename(&self) -> String; + fn qualified_path(&self) -> String; + fn tag(&self) -> String; } /// Represents a member of a struct. @@ -115,30 +117,60 @@ impl AnnotationInfo for ModelAnnotation { fn filename(&self) -> String { naming::get_filename_from_tag(&self.tag) } + fn qualified_path(&self) -> String { + self.qualified_path.clone() + } + fn tag(&self) -> String { + self.tag.clone() + } } impl AnnotationInfo for EventAnnotation { fn filename(&self) -> String { naming::get_filename_from_tag(&self.tag) } + fn qualified_path(&self) -> String { + self.qualified_path.clone() + } + fn tag(&self) -> String { + self.tag.clone() + } } impl AnnotationInfo for ContractAnnotation { fn filename(&self) -> String { naming::get_filename_from_tag(&self.tag) } + fn qualified_path(&self) -> String { + self.qualified_path.clone() + } + fn tag(&self) -> String { + self.tag.clone() + } } impl AnnotationInfo for StarknetContractAnnotation { fn filename(&self) -> String { self.qualified_path.replace(CAIRO_PATH_SEPARATOR, "_") } + fn qualified_path(&self) -> String { + self.qualified_path.clone() + } + fn tag(&self) -> String { + self.name.clone() + } } impl AnnotationInfo for WorldAnnotation { fn filename(&self) -> String { WORLD_CONTRACT_TAG.to_string() } + fn qualified_path(&self) -> String { + self.qualified_path.clone() + } + fn tag(&self) -> String { + self.tag.clone() + } } /// An abstract representation of the annotations of dojo resources. diff --git a/crates/compiler/src/plugin/attribute_macros/element.rs b/crates/compiler/src/plugin/attribute_macros/common.rs similarity index 95% rename from crates/compiler/src/plugin/attribute_macros/element.rs rename to crates/compiler/src/plugin/attribute_macros/common.rs index 1d697b0..4aa9275 100644 --- a/crates/compiler/src/plugin/attribute_macros/element.rs +++ b/crates/compiler/src/plugin/attribute_macros/common.rs @@ -258,16 +258,7 @@ fn get_version( match arg_value { Expr::Literal(ref value) => { if let Ok(value) = value.text(db).parse::() { - if value <= DEFAULT_VERSION { - value - } else { - diagnostics.push(PluginDiagnostic { - message: format!("{attribute_name} version {} not supported", value), - stable_ptr: arg_value.stable_ptr().untyped(), - severity: Severity::Error, - }); - DEFAULT_VERSION - } + value } else { diagnostics.push(PluginDiagnostic { message: format!( diff --git a/crates/compiler/src/plugin/attribute_macros/event.rs b/crates/compiler/src/plugin/attribute_macros/event.rs index 83154a9..614887b 100644 --- a/crates/compiler/src/plugin/attribute_macros/event.rs +++ b/crates/compiler/src/plugin/attribute_macros/event.rs @@ -25,7 +25,7 @@ use crate::plugin::derive_macros::{ extract_derive_attr_names, handle_derive_attrs, DOJO_INTROSPECT_DERIVE, DOJO_PACKED_DERIVE, }; -use super::element::{ +use super::common::{ compute_namespace, parse_members, serialize_keys_and_values, CommonStructParameters, StructParameterParser, }; diff --git a/crates/compiler/src/plugin/attribute_macros/mod.rs b/crates/compiler/src/plugin/attribute_macros/mod.rs index e667419..b685744 100644 --- a/crates/compiler/src/plugin/attribute_macros/mod.rs +++ b/crates/compiler/src/plugin/attribute_macros/mod.rs @@ -2,8 +2,8 @@ //! //! An attribute macros is a macro that is used to generate code generally for a struct, enum, module or trait. +pub mod common; pub mod contract; -pub mod element; pub mod event; pub mod interface; pub mod model; diff --git a/crates/compiler/src/plugin/attribute_macros/model.rs b/crates/compiler/src/plugin/attribute_macros/model.rs index 65330fe..bbc6fa8 100644 --- a/crates/compiler/src/plugin/attribute_macros/model.rs +++ b/crates/compiler/src/plugin/attribute_macros/model.rs @@ -21,9 +21,9 @@ use crate::plugin::derive_macros::{ extract_derive_attr_names, handle_derive_attrs, DOJO_INTROSPECT_DERIVE, DOJO_PACKED_DERIVE, }; -use super::element::{ +use super::common::{ compute_namespace, deserialize_keys_and_values, parse_members, serialize_keys_and_values, - serialize_member_ty, CommonStructParameters, StructParameterParser, DEFAULT_VERSION, + serialize_member_ty, CommonStructParameters, StructParameterParser, }; use super::patches::MODEL_PATCH; use super::DOJO_MODEL_ATTR; @@ -86,19 +86,10 @@ impl DojoModel { let model_name_hash = naming::compute_bytearray_hash(&model_name); let model_namespace_hash = naming::compute_bytearray_hash(&model_namespace); - let (model_version, model_selector) = match parameters.version { - 0 => ( - RewriteNode::Text("0".to_string()), - RewriteNode::Text(format!("\"{model_name}\"")), - ), - _ => ( - RewriteNode::Text(DEFAULT_VERSION.to_string()), - RewriteNode::Text( - naming::compute_selector_from_hashes(model_namespace_hash, model_name_hash) - .to_string(), - ), - ), - }; + let model_version = RewriteNode::Text(parameters.version.to_string()); + let model_selector = RewriteNode::Text( + naming::compute_selector_from_hashes(model_namespace_hash, model_name_hash).to_string(), + ); let members = parse_members(db, &struct_ast.members(db).elements(db), &mut diagnostics); let mut serialized_keys: Vec = vec![]; diff --git a/crates/compiler/src/plugin/attribute_macros/patches.rs b/crates/compiler/src/plugin/attribute_macros/patches.rs index 476ed1a..d7ed9a4 100644 --- a/crates/compiler/src/plugin/attribute_macros/patches.rs +++ b/crates/compiler/src/plugin/attribute_macros/patches.rs @@ -390,6 +390,16 @@ pub impl $type_name$ModelImpl of dojo::model::Model<$type_name$> { Self::layout() } + #[inline(always)] + fn schema() -> dojo::meta::introspect::Struct { + if let dojo::meta::introspect::Ty::Struct(s) = dojo::meta::introspect::Introspect::<$type_name$>::ty() { + s + } + else { + panic!(\"Model `$type_name$`: invalid schema.\") + } + } + #[inline(always)] fn packed_size() -> Option { dojo::meta::layout::compute_packed_size(Self::layout()) @@ -440,8 +450,38 @@ pub mod $contract_name$ { use super::$type_name$; use super::I$contract_name$; + use dojo::contract::components::world_provider::{world_provider_cpt, world_provider_cpt::InternalTrait as WorldProviderInternal, IWorldProvider}; + use dojo::contract::components::upgradeable::upgradeable_cpt; + + component!(path: world_provider_cpt, storage: world_provider, event: WorldProviderEvent); + component!(path: upgradeable_cpt, storage: upgradeable, event: UpgradeableEvent); + + #[abi(embed_v0)] + impl WorldProviderImpl = world_provider_cpt::WorldProviderImpl; + + #[abi(embed_v0)] + impl UpgradeableImpl = upgradeable_cpt::UpgradeableImpl; + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + UpgradeableEvent: upgradeable_cpt::Event, + WorldProviderEvent: world_provider_cpt::Event, + } + #[storage] - struct Storage {} + struct Storage { + #[substorage(v0)] + upgradeable: upgradeable_cpt::Storage, + + #[substorage(v0)] + world_provider: world_provider_cpt::Storage, + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.world_provider.initializer(); + } #[abi(embed_v0)] impl DojoModelImpl of dojo::model::IModel{ @@ -485,8 +525,8 @@ pub mod $contract_name$ { dojo::model::Model::<$type_name$>::layout() } - fn schema(self: @ContractState) -> dojo::meta::introspect::Ty { - dojo::meta::introspect::Introspect::<$type_name$>::ty() + fn schema(self: @ContractState) -> dojo::meta::introspect::Struct { + dojo::model::Model::<$type_name$>::schema() } } @@ -543,11 +583,6 @@ pub impl $type_name$EventImpl of dojo::event::Event<$type_name$> { $event_selector$ } - #[inline(always)] - fn instance_selector(self: @$type_name$) -> felt252 { - Self::selector() - } - #[inline(always)] fn name_hash() -> felt252 { $event_name_hash$ @@ -564,8 +599,13 @@ pub impl $type_name$EventImpl of dojo::event::Event<$type_name$> { } #[inline(always)] - fn schema(self: @$type_name$) -> dojo::meta::introspect::Ty { - dojo::meta::introspect::Introspect::<$type_name$>::ty() + fn schema() -> dojo::meta::introspect::Struct { + if let dojo::meta::introspect::Ty::Struct(s) = dojo::meta::introspect::Introspect::<$type_name$>::ty() { + s + } + else { + panic!(\"Event `$type_name$`: invalid schema.\") + } } #[inline(always)] @@ -608,8 +648,38 @@ pub impl $type_name$EventImplTest of dojo::event::EventTest<$type_name$> { pub mod $contract_name$ { use super::$type_name$; + use dojo::contract::components::world_provider::{world_provider_cpt, world_provider_cpt::InternalTrait as WorldProviderInternal, IWorldProvider}; + use dojo::contract::components::upgradeable::upgradeable_cpt; + + component!(path: world_provider_cpt, storage: world_provider, event: WorldProviderEvent); + component!(path: upgradeable_cpt, storage: upgradeable, event: UpgradeableEvent); + + #[abi(embed_v0)] + impl WorldProviderImpl = world_provider_cpt::WorldProviderImpl; + + #[abi(embed_v0)] + impl UpgradeableImpl = upgradeable_cpt::UpgradeableImpl; + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + UpgradeableEvent: upgradeable_cpt::Event, + WorldProviderEvent: world_provider_cpt::Event, + } + #[storage] - struct Storage {} + struct Storage { + #[substorage(v0)] + upgradeable: upgradeable_cpt::Storage, + + #[substorage(v0)] + world_provider: world_provider_cpt::Storage, + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.world_provider.initializer(); + } #[abi(embed_v0)] impl DojoEventImpl of dojo::event::IEvent{ @@ -645,8 +715,8 @@ pub mod $contract_name$ { dojo::event::Event::<$type_name$>::layout() } - fn schema(self: @ContractState) -> dojo::meta::introspect::Ty { - dojo::meta::introspect::Introspect::<$type_name$>::ty() + fn schema(self: @ContractState) -> dojo::meta::introspect::Struct { + dojo::event::Event::<$type_name$>::schema() } } } diff --git a/crates/compiler/src/plugin/inline_macros/get_models_test_class_hashes.rs b/crates/compiler/src/plugin/inline_macros/get_models_test_class_hashes.rs index a54dbee..cffd8d7 100644 --- a/crates/compiler/src/plugin/inline_macros/get_models_test_class_hashes.rs +++ b/crates/compiler/src/plugin/inline_macros/get_models_test_class_hashes.rs @@ -8,7 +8,7 @@ use cairo_lang_diagnostics::Severity; use cairo_lang_syntax::node::{ast, TypedStablePtr, TypedSyntaxNode}; use super::unsupported_arg_diagnostic; -use super::utils::{extract_namespaces, load_manifest_models_and_namespaces}; +use super::utils::{extract_namespaces, load_resources_and_namespaces_from_annotations}; #[derive(Debug, Default)] pub struct GetModelsTestClassHashes; @@ -62,22 +62,24 @@ impl InlineMacroExprPlugin for GetModelsTestClassHashes { vec![] }; - let (_namespaces, models) = - match load_manifest_models_and_namespaces(metadata.cfg_set, &whitelisted_namespaces) { - Ok((namespaces, models)) => (namespaces, models), - Err(_e) => { - return InlinePluginResult { - code: None, - diagnostics: vec![PluginDiagnostic { - stable_ptr: syntax.stable_ptr().untyped(), - message: "Failed to load models and namespaces, ensure you have run \ + let (_namespaces, models) = match load_resources_and_namespaces_from_annotations( + metadata.cfg_set, + &whitelisted_namespaces, + ) { + Ok((namespaces, models, _)) => (namespaces, models), + Err(_e) => { + return InlinePluginResult { + code: None, + diagnostics: vec![PluginDiagnostic { + stable_ptr: syntax.stable_ptr().untyped(), + message: "Failed to load models and namespaces, ensure you have run \ `sozo build` first." - .to_string(), - severity: Severity::Error, - }], - }; - } - }; + .to_string(), + severity: Severity::Error, + }], + }; + } + }; let mut builder = PatchBuilder::new(db, syntax); diff --git a/crates/compiler/src/plugin/inline_macros/spawn_test_world.rs b/crates/compiler/src/plugin/inline_macros/spawn_test_world.rs index c89f7bd..48b8fbd 100644 --- a/crates/compiler/src/plugin/inline_macros/spawn_test_world.rs +++ b/crates/compiler/src/plugin/inline_macros/spawn_test_world.rs @@ -9,7 +9,7 @@ use cairo_lang_syntax::node::{ast, TypedStablePtr, TypedSyntaxNode}; use tracing::trace; use super::unsupported_arg_diagnostic; -use super::utils::{extract_namespaces, load_manifest_models_and_namespaces}; +use super::utils::{extract_namespaces, load_resources_and_namespaces_from_annotations}; #[derive(Debug, Default)] pub struct SpawnTestWorld; @@ -62,29 +62,36 @@ impl InlineMacroExprPlugin for SpawnTestWorld { vec![] }; - let (namespaces, models) = - match load_manifest_models_and_namespaces(metadata.cfg_set, &whitelisted_namespaces) { - Ok((namespaces, models)) => (namespaces, models), - Err(_e) => { - return InlinePluginResult { - code: None, - diagnostics: vec![PluginDiagnostic { - stable_ptr: syntax.stable_ptr().untyped(), - message: "failed to load models and namespaces, ensure you have run \ + let (namespaces, models, events) = match load_resources_and_namespaces_from_annotations( + metadata.cfg_set, + &whitelisted_namespaces, + ) { + Ok((namespaces, models, events)) => (namespaces, models, events), + Err(_e) => { + return InlinePluginResult { + code: None, + diagnostics: vec![PluginDiagnostic { + stable_ptr: syntax.stable_ptr().untyped(), + message: "failed to load resources and namespaces, ensure you have run \ `sozo build` first." - .to_string(), - severity: Severity::Error, - }], - }; - } - }; + .to_string(), + severity: Severity::Error, + }], + }; + } + }; - trace!(?namespaces, ?models, "Spawning test world from macro."); + trace!( + ?namespaces, + ?models, + ?events, + "Spawning test world from macro." + ); let mut builder = PatchBuilder::new(db, syntax); builder.add_str(&format!( - "dojo::utils::test::spawn_test_world([{}].span(), [{}].span())", + "dojo::utils::test::spawn_test_world([{}].span(), [{}].span(), [{}].span())", namespaces .iter() .map(|n| format!("\"{}\"", n)) @@ -94,6 +101,11 @@ impl InlineMacroExprPlugin for SpawnTestWorld { .iter() .map(|m| format!("{}::TEST_CLASS_HASH", m)) .collect::>() + .join(", "), + events + .iter() + .map(|e| format!("{}::TEST_CLASS_HASH", e)) + .collect::>() .join(", ") )); diff --git a/crates/compiler/src/plugin/inline_macros/utils.rs b/crates/compiler/src/plugin/inline_macros/utils.rs index 222ad37..3046d52 100644 --- a/crates/compiler/src/plugin/inline_macros/utils.rs +++ b/crates/compiler/src/plugin/inline_macros/utils.rs @@ -12,7 +12,7 @@ use dojo_types::naming; use scarb::compiler::Profile; use scarb::core::Config; -use crate::compiler::annotation::DojoAnnotation; +use crate::compiler::annotation::{AnnotationInfo, DojoAnnotation}; use crate::namespace_config::{DOJO_ANNOTATIONS_DIR_CFG_KEY, WORKSPACE_CURRENT_PROFILE_CFG_KEY}; #[derive(Debug)] @@ -36,17 +36,38 @@ pub fn parent_of_kind( None } -/// Reads all the models and namespaces from base manifests files. -pub fn load_manifest_models_and_namespaces( +/// Reads all the resources and namespaces from annotations. +pub fn load_resources_and_namespaces_from_annotations( cfg_set: &CfgSet, whitelisted_namespaces: &[String], -) -> anyhow::Result<(Vec, Vec)> { - let dojo_manifests_dir = get_dojo_manifests_dir(cfg_set.clone())?; - let scarb_toml = dojo_manifests_dir +) -> anyhow::Result<(Vec, Vec, Vec)> { + fn process_annotations( + whitelisted_namespaces: &[String], + annotations: &Vec, + namespaces: &mut HashSet, + ) -> anyhow::Result> { + let mut output = HashSet::::new(); + for annotation in annotations { + let qualified_path = annotation.qualified_path(); + let namespace = naming::split_tag(&annotation.tag())?.0; + + if !whitelisted_namespaces.is_empty() && !whitelisted_namespaces.contains(&namespace) { + continue; + } + + output.insert(qualified_path); + namespaces.insert(namespace); + } + + Ok(output.into_iter().collect()) + } + + let dojo_annotations_dir = get_dojo_annotations_dir(cfg_set.clone())?; + let scarb_toml = dojo_annotations_dir .parent() .expect("Profile dir should have parent") .parent() - .expect("Manifests dir should have parent") + .expect("Annotations dir dir should have parent") .join("Scarb.toml"); let config = Config::builder(scarb_toml.clone()) @@ -57,36 +78,27 @@ pub fn load_manifest_models_and_namespaces( let annotations = DojoAnnotation::read(&ws)?; - let mut models = HashSet::::new(); let mut namespaces = HashSet::::new(); - for model in annotations.models { - let qualified_path = model.qualified_path; - let namespace = naming::split_tag(&model.tag)?.0; - - if !whitelisted_namespaces.is_empty() && !whitelisted_namespaces.contains(&namespace) { - continue; - } - - models.insert(qualified_path); - namespaces.insert(namespace); - } + let models_vec = + process_annotations(whitelisted_namespaces, &annotations.models, &mut namespaces)?; + let events_vec = + process_annotations(whitelisted_namespaces, &annotations.events, &mut namespaces)?; - let models_vec: Vec = models.into_iter().collect(); let namespaces_vec: Vec = namespaces.into_iter().collect(); - Ok((namespaces_vec, models_vec)) + Ok((namespaces_vec, models_vec, events_vec)) } -/// Gets the dojo_manifests_dir for the current profile from the cfg_set. -pub fn get_dojo_manifests_dir(cfg_set: CfgSet) -> anyhow::Result { +/// Gets the Dojo annotations directory for the current profile from the cfg_set. +pub fn get_dojo_annotations_dir(cfg_set: CfgSet) -> anyhow::Result { for cfg in cfg_set.into_iter() { if cfg.key == DOJO_ANNOTATIONS_DIR_CFG_KEY { return Ok(Utf8PathBuf::from(cfg.value.unwrap().as_str().to_string())); } } - Err(anyhow::anyhow!("dojo_manifests_dir not found")) + Err(anyhow::anyhow!("dojo_annotations_dir not found")) } /// Gets the current profile from the cfg_set. diff --git a/crates/contracts/src/components/world_provider.cairo b/crates/contracts/src/components/world_provider.cairo new file mode 100644 index 0000000..407298f --- /dev/null +++ b/crates/contracts/src/components/world_provider.cairo @@ -0,0 +1,39 @@ +use dojo::world::IWorldDispatcher; + +#[starknet::interface] +pub trait IWorldProvider { + fn world(self: @T) -> IWorldDispatcher; +} + +#[starknet::component] +pub mod WorldProviderComponent { + use starknet::{ClassHash, ContractAddress, get_caller_address}; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + + use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + + #[storage] + pub struct Storage { + world_dispatcher: IWorldDispatcher, + } + + #[embeddable_as(WorldProviderImpl)] + impl WorldProvider< + TContractState, +HasComponent + > of super::IWorldProvider> { + fn world(self: @ComponentState) -> IWorldDispatcher { + self.world_dispatcher.read() + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, +HasComponent + > of InternalTrait { + fn initializer(ref self: ComponentState) { + self + .world_dispatcher + .write(IWorldDispatcher { contract_address: get_caller_address() }); + } + } +} diff --git a/crates/contracts/src/event/event.cairo b/crates/contracts/src/event/event.cairo index 89d687a..729f0c4 100644 --- a/crates/contracts/src/event/event.cairo +++ b/crates/contracts/src/event/event.cairo @@ -1,5 +1,5 @@ use dojo::meta::Layout; -use dojo::meta::introspect::Ty; +use dojo::meta::introspect::{Ty, Struct}; use dojo::world::IWorldDispatcher; pub trait Event { @@ -12,13 +12,12 @@ pub trait Event { fn version() -> u8; fn selector() -> felt252; - fn instance_selector(self: @T) -> felt252; fn name_hash() -> felt252; fn namespace_hash() -> felt252; fn layout() -> Layout; - fn schema(self: @T) -> Ty; + fn schema() -> Struct; fn historical() -> bool; fn keys(self: @T) -> Span; @@ -38,7 +37,7 @@ pub trait IEvent { fn namespace_hash(self: @T) -> felt252; fn layout(self: @T) -> Layout; - fn schema(self: @T) -> Ty; + fn schema(self: @T) -> Struct; } #[cfg(target: "test")] diff --git a/crates/contracts/src/lib.cairo b/crates/contracts/src/lib.cairo index 9ef03e7..1ab85d0 100644 --- a/crates/contracts/src/lib.cairo +++ b/crates/contracts/src/lib.cairo @@ -19,7 +19,7 @@ pub mod event { pub mod meta { pub mod introspect; pub mod layout; - pub use layout::{Layout, FieldLayout}; + pub use layout::{Layout, FieldLayout, LayoutCompareTrait}; } pub mod model { @@ -110,8 +110,12 @@ mod tests { mod helpers; mod world { mod acl; + mod contracts; mod entities; - mod resources; + mod events; + mod metadata; + mod models; + mod namespaces; mod world; } mod utils; diff --git a/crates/contracts/src/meta/introspect.cairo b/crates/contracts/src/meta/introspect.cairo index b980a81..4964bfc 100644 --- a/crates/contracts/src/meta/introspect.cairo +++ b/crates/contracts/src/meta/introspect.cairo @@ -1,7 +1,7 @@ use dojo::meta::{Layout, FieldLayout}; use dojo::storage::packing; -#[derive(Copy, Drop, Serde)] +#[derive(Copy, Drop, Serde, PartialEq, Debug)] pub enum Ty { Primitive: felt252, Struct: Struct, @@ -14,21 +14,46 @@ pub enum Ty { ByteArray, } -#[derive(Copy, Drop, Serde)] +#[derive(Copy, Drop, Serde, PartialEq, Debug)] pub struct Struct { pub name: felt252, pub attrs: Span, pub children: Span } -#[derive(Copy, Drop, Serde)] +#[generate_trait] +pub impl StructCompareImpl of StructCompareTrait { + fn is_an_upgrade_of(self: @Struct, old: @Struct) -> bool { + if self.name != old.name + || self.attrs != old.attrs + || (*self.children).len() <= (*old.children).len() { + return false; + } + + let mut i = 0; + + loop { + if i >= (*old.children).len() { + break true; + } + + if *old.children[i] != *self.children[i] { + break false; + } + + i += 1; + } + } +} + +#[derive(Copy, Drop, Serde, PartialEq, Debug)] pub struct Enum { pub name: felt252, pub attrs: Span, pub children: Span<(felt252, Ty)> } -#[derive(Copy, Drop, Serde)] +#[derive(Copy, Drop, Serde, PartialEq, Debug)] pub struct Member { pub name: felt252, pub attrs: Span, diff --git a/crates/contracts/src/meta/layout.cairo b/crates/contracts/src/meta/layout.cairo index 69bd49c..ce82b52 100644 --- a/crates/contracts/src/meta/layout.cairo +++ b/crates/contracts/src/meta/layout.cairo @@ -22,6 +22,21 @@ pub enum Layout { Enum: Span, } +#[generate_trait] +pub impl LayoutCompareImpl of LayoutCompareTrait { + fn is_same_type_of(self: @Layout, old: @Layout) -> bool { + match (self, old) { + (Layout::Fixed(_), Layout::Fixed(_)) => true, + (Layout::Struct(_), Layout::Struct(_)) => true, + (Layout::Tuple(_), Layout::Tuple(_)) => true, + (Layout::Array(_), Layout::Array(_)) => true, + (Layout::ByteArray, Layout::ByteArray) => true, + (Layout::Enum(_), Layout::Enum(_)) => true, + _ => false + } + } +} + /// Compute the full size in bytes of a layout, when all the fields /// are bit-packed. /// Could be None if at least a field has a dynamic size. diff --git a/crates/contracts/src/model/metadata.cairo b/crates/contracts/src/model/metadata.cairo index 0e12187..2090b9c 100644 --- a/crates/contracts/src/model/metadata.cairo +++ b/crates/contracts/src/model/metadata.cairo @@ -160,6 +160,15 @@ pub impl ResourceMetadataModel of Model { Self::layout() } + #[inline(always)] + fn schema() -> Struct { + if let Ty::Struct(s) = Introspect::::ty() { + s + } else { + panic!("Model `ResourceMetadata`: invalid schema.") + } + } + #[inline(always)] fn packed_size() -> Option { Option::None @@ -199,7 +208,7 @@ pub mod resource_metadata { use super::ResourceMetadata; use super::ResourceMetadataModel; - use dojo::meta::introspect::{Introspect, Ty}; + use dojo::meta::introspect::{Introspect, Ty, Struct}; use dojo::meta::Layout; #[storage] @@ -238,8 +247,8 @@ pub mod resource_metadata { } #[external(v0)] - fn schema(self: @ContractState) -> Ty { - Introspect::::ty() + fn schema(self: @ContractState) -> Struct { + ResourceMetadataModel::schema() } #[external(v0)] diff --git a/crates/contracts/src/model/model.cairo b/crates/contracts/src/model/model.cairo index 863cee7..54385cf 100644 --- a/crates/contracts/src/model/model.cairo +++ b/crates/contracts/src/model/model.cairo @@ -1,7 +1,7 @@ use starknet::SyscallResult; use dojo::meta::Layout; -use dojo::meta::introspect::Ty; +use dojo::meta::introspect::{Ty, Struct}; use dojo::world::IWorldDispatcher; use dojo::utils::{Descriptor, DescriptorTrait}; @@ -71,6 +71,9 @@ pub trait Model { fn values(self: @T) -> Span; fn layout() -> Layout; fn instance_layout(self: @T) -> Layout; + + fn schema() -> Struct; + fn packed_size() -> Option; } @@ -87,7 +90,7 @@ pub trait IModel { fn unpacked_size(self: @T) -> Option; fn packed_size(self: @T) -> Option; fn layout(self: @T) -> Layout; - fn schema(self: @T) -> Ty; + fn schema(self: @T) -> Struct; } #[cfg(target: "test")] diff --git a/crates/contracts/src/tests/benchmarks.cairo b/crates/contracts/src/tests/benchmarks.cairo index a3f3d8a..f474ada 100644 --- a/crates/contracts/src/tests/benchmarks.cairo +++ b/crates/contracts/src/tests/benchmarks.cairo @@ -49,7 +49,8 @@ fn deploy_world() -> IWorldDispatcher { ["dojo"].span(), [ case::TEST_CLASS_HASH, case_not_packed::TEST_CLASS_HASH, complex_model::TEST_CLASS_HASH - ].span() + ].span(), + [].span() ) } diff --git a/crates/contracts/src/tests/contract.cairo b/crates/contracts/src/tests/contract.cairo index 974a5e5..98fa281 100644 --- a/crates/contracts/src/tests/contract.cairo +++ b/crates/contracts/src/tests/contract.cairo @@ -106,7 +106,7 @@ pub mod test_contract_upgrade { // Utils fn deploy_world() -> IWorldDispatcher { - spawn_test_world(["dojo"].span(), [].span()) + spawn_test_world(["dojo"].span(), [].span(), [].span()) } #[test] diff --git a/crates/contracts/src/tests/expanded/selector_attack.cairo b/crates/contracts/src/tests/expanded/selector_attack.cairo index 1f9ba46..0c93f1a 100644 --- a/crates/contracts/src/tests/expanded/selector_attack.cairo +++ b/crates/contracts/src/tests/expanded/selector_attack.cairo @@ -101,8 +101,8 @@ pub mod attacker_model { dojo::meta::Layout::Fixed([].span()) } - fn schema(self: @ContractState) -> dojo::meta::introspect::Ty { - dojo::meta::introspect::Ty::Primitive('felt252') + fn schema(self: @ContractState) -> dojo::meta::introspect::Struct { + dojo::meta::introspect::Struct { name: 'm1', attrs: [].span(), children: [].span() } } } } diff --git a/crates/contracts/src/tests/helpers.cairo b/crates/contracts/src/tests/helpers.cairo index e36c431..d914167 100644 --- a/crates/contracts/src/tests/helpers.cairo +++ b/crates/contracts/src/tests/helpers.cairo @@ -204,7 +204,7 @@ pub mod bar { } pub fn deploy_world() -> IWorldDispatcher { - spawn_test_world(["dojo"].span(), [].span()) + spawn_test_world(["dojo"].span(), [].span(), [].span()) } pub fn deploy_world_and_bar() -> (IWorldDispatcher, IbarDispatcher) { @@ -229,3 +229,177 @@ pub fn drop_all_events(address: ContractAddress) { }; } } + +/// Note: These models must remain private to be able to redefine them +/// in `resources.cairo` test file, to test model upgrade. +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model] +struct FooNameChanged { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model] +struct FooBadLayoutType { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model] +struct FooMemberRemoved { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model] +struct FooMemberAddedButRemoved { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128 +} + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model] +struct FooMemberAddedButMoved { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model] +struct FooMemberAddedButSameVersion { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128 +} + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model] +struct FooMemberAdded { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128 +} + +pub fn deploy_world_for_model_upgrades() -> IWorldDispatcher { + spawn_test_world( + ["dojo"].span(), + [ + foo::TEST_CLASS_HASH.try_into().unwrap(), + foo_name_changed::TEST_CLASS_HASH.try_into().unwrap(), + foo_bad_layout_type::TEST_CLASS_HASH.try_into().unwrap(), + foo_member_removed::TEST_CLASS_HASH.try_into().unwrap(), + foo_member_added_but_moved::TEST_CLASS_HASH.try_into().unwrap(), + foo_member_added_but_removed::TEST_CLASS_HASH.try_into().unwrap(), + foo_member_added_but_same_version::TEST_CLASS_HASH.try_into().unwrap(), + foo_member_added::TEST_CLASS_HASH.try_into().unwrap(), + ].span(), + [].span() + ) +} + +/// Note: These events must remain private to be able to redefine them +/// in `resources.cairo` test file, to test event upgrade. + +#[dojo::event] +pub struct FooEvent { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +#[dojo::event(namespace: "another_namespace")] +pub struct BuzzEvent { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +#[dojo::event] +struct FooEventNameChanged { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +#[dojo::event] +struct FooEventBadLayoutType { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +#[dojo::event] +struct FooEventMemberRemoved { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +#[dojo::event] +struct FooEventMemberAddedButRemoved { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128 +} + +#[dojo::event] +struct FooEventMemberAddedButMoved { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +#[dojo::event] +struct FooEventMemberButSameVersion { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128 +} + +#[dojo::event] +struct FooEventMemberAdded { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128 +} + +pub fn deploy_world_for_event_upgrades() -> IWorldDispatcher { + spawn_test_world( + ["dojo"].span(), + [].span(), + [ + foo_event::TEST_CLASS_HASH.try_into().unwrap(), + foo_event_name_changed::TEST_CLASS_HASH.try_into().unwrap(), + foo_event_bad_layout_type::TEST_CLASS_HASH.try_into().unwrap(), + foo_event_member_removed::TEST_CLASS_HASH.try_into().unwrap(), + foo_event_member_added_but_moved::TEST_CLASS_HASH.try_into().unwrap(), + foo_event_member_added_but_removed::TEST_CLASS_HASH.try_into().unwrap(), + foo_event_member_but_same_version::TEST_CLASS_HASH.try_into().unwrap(), + foo_event_member_added::TEST_CLASS_HASH.try_into().unwrap(), + ].span(), + ) +} diff --git a/crates/contracts/src/tests/world/contracts.cairo b/crates/contracts/src/tests/world/contracts.cairo new file mode 100644 index 0000000..89c18f1 --- /dev/null +++ b/crates/contracts/src/tests/world/contracts.cairo @@ -0,0 +1,209 @@ +use starknet::{contract_address_const, ContractAddress, ClassHash}; + +use dojo::model::{Model, ResourceMetadata}; +use dojo::utils::{bytearray_hash, entity_id_from_keys}; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, world}; +use dojo::world::world::{ + Event, NamespaceRegistered, ModelRegistered, ModelUpgraded, MetadataUpdate, ContractRegistered, + ContractUpgraded +}; +use dojo::contract::{IContractDispatcher, IContractDispatcherTrait}; + +use dojo::tests::helpers::{ + deploy_world, deploy_world_for_model_upgrades, deploy_world_for_event_upgrades, drop_all_events, + Foo, foo, Buzz, buzz, test_contract, buzz_contract +}; +use dojo::utils::test::spawn_test_world; + +#[test] +fn test_deploy_contract_for_namespace_owner() { + let world = deploy_world(); + let class_hash = test_contract::TEST_CLASS_HASH.try_into().unwrap(); + + let bob = starknet::contract_address_const::<0xb0b>(); + world.grant_owner(bytearray_hash(@"dojo"), bob); + + // the account owns the 'test_contract' namespace so it should be able to deploy the contract. + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + + drop_all_events(world.contract_address); + + let contract_address = world.register_contract('salt1', class_hash, [].span()); + + let event = match starknet::testing::pop_log::(world.contract_address).unwrap() { + Event::ContractRegistered(event) => event, + _ => panic!("no ContractRegistered event"), + }; + + let dispatcher = IContractDispatcher { contract_address }; + + assert(event.salt == 'salt1', 'bad event salt'); + assert(event.class_hash == class_hash, 'bad class_hash'); + assert(event.selector == dispatcher.selector(), 'bad contract selector'); + assert( + event.address != core::num::traits::Zero::::zero(), 'bad contract address' + ); +} + +#[test] +#[should_panic( + expected: ("Account `2827` does NOT have OWNER role on namespace `dojo`", 'ENTRYPOINT_FAILED',) +)] +fn test_deploy_contract_for_namespace_writer() { + let world = deploy_world(); + + let bob = starknet::contract_address_const::<0xb0b>(); + world.grant_writer(bytearray_hash(@"dojo"), bob); + + // the account has write access to the 'test_contract' namespace so it should be able to deploy + // the contract. + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + + world.register_contract('salt1', test_contract::TEST_CLASS_HASH.try_into().unwrap(), [].span()); +} + + +#[test] +#[should_panic( + expected: ("Account `2827` does NOT have OWNER role on namespace `dojo`", 'ENTRYPOINT_FAILED',) +)] +fn test_deploy_contract_no_namespace_owner_access() { + let world = deploy_world(); + + let bob = starknet::contract_address_const::<0xb0b>(); + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + + world.register_contract('salt1', test_contract::TEST_CLASS_HASH.try_into().unwrap(), [].span()); +} + +#[test] +#[should_panic(expected: ("Namespace `buzz_namespace` is not registered", 'ENTRYPOINT_FAILED',))] +fn test_deploy_contract_with_unregistered_namespace() { + let world = deploy_world(); + world.register_contract('salt1', buzz_contract::TEST_CLASS_HASH.try_into().unwrap(), [].span()); +} + +// It's CONTRACT_NOT_DEPLOYED for now as in this example the contract is not a dojo contract +// and it's not the account that is calling the deploy_contract function. +#[test] +#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED',))] +fn test_deploy_contract_through_malicious_contract() { + let world = deploy_world(); + + let bob = starknet::contract_address_const::<0xb0b>(); + let malicious_contract = starknet::contract_address_const::<0xdead>(); + + world.grant_owner(bytearray_hash(@"dojo"), bob); + + // the account owns the 'test_contract' namespace so it should be able to deploy the contract. + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(malicious_contract); + + world.register_contract('salt1', test_contract::TEST_CLASS_HASH.try_into().unwrap(), [].span()); +} + +#[test] +fn test_upgrade_contract_from_resource_owner() { + let world = deploy_world(); + let class_hash = test_contract::TEST_CLASS_HASH.try_into().unwrap(); + + let bob = starknet::contract_address_const::<0xb0b>(); + + world.grant_owner(bytearray_hash(@"dojo"), bob); + + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + + let contract_address = world.register_contract('salt1', class_hash, [].span()); + let dispatcher = IContractDispatcher { contract_address }; + + drop_all_events(world.contract_address); + + world.upgrade_contract(class_hash); + + let event = starknet::testing::pop_log::(world.contract_address); + assert(event.is_some(), 'no event)'); + + if let Event::ContractUpgraded(event) = event.unwrap() { + assert(event.selector == dispatcher.selector(), 'bad contract selector'); + assert(event.class_hash == class_hash, 'bad class_hash'); + } else { + core::panic_with_felt252('no ContractUpgraded event'); + }; +} + +#[test] +#[should_panic( + expected: ( + "Account `659918` does NOT have OWNER role on contract (or its namespace) `dojo-test_contract`", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_upgrade_contract_from_resource_writer() { + let world = deploy_world(); + let class_hash = test_contract::TEST_CLASS_HASH.try_into().unwrap(); + + let bob = starknet::contract_address_const::<0xb0b>(); + let alice = starknet::contract_address_const::<0xa11ce>(); + + world.grant_owner(bytearray_hash(@"dojo"), bob); + + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + + let contract_address = world.register_contract('salt1', class_hash, [].span()); + + let dispatcher = IContractDispatcher { contract_address }; + + world.grant_writer(dispatcher.selector(), alice); + + starknet::testing::set_account_contract_address(alice); + starknet::testing::set_contract_address(alice); + + world.upgrade_contract(class_hash); +} + +#[test] +#[should_panic( + expected: ( + "Account `659918` does NOT have OWNER role on contract (or its namespace) `dojo-test_contract`", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_upgrade_contract_from_random_account() { + let world = deploy_world(); + let class_hash = test_contract::TEST_CLASS_HASH.try_into().unwrap(); + + let _contract_address = world.register_contract('salt1', class_hash, [].span()); + + let alice = starknet::contract_address_const::<0xa11ce>(); + + starknet::testing::set_account_contract_address(alice); + starknet::testing::set_contract_address(alice); + + world.upgrade_contract(class_hash); +} + +#[test] +#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED',))] +fn test_upgrade_contract_through_malicious_contract() { + let world = deploy_world(); + let class_hash = test_contract::TEST_CLASS_HASH.try_into().unwrap(); + + let bob = starknet::contract_address_const::<0xb0b>(); + let malicious_contract = starknet::contract_address_const::<0xdead>(); + + world.grant_owner(bytearray_hash(@"dojo"), bob); + + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + + let _contract_address = world.register_contract('salt1', class_hash, [].span()); + + starknet::testing::set_contract_address(malicious_contract); + + world.upgrade_contract(class_hash); +} diff --git a/crates/contracts/src/tests/world/entities.cairo b/crates/contracts/src/tests/world/entities.cairo index 46f88c6..d4904e7 100644 --- a/crates/contracts/src/tests/world/entities.cairo +++ b/crates/contracts/src/tests/world/entities.cairo @@ -236,7 +236,6 @@ fn test_set_entity_admin() { let foo: Foo = get!(world, alice, Foo); - println!("foo: {:?}", foo); assert(foo.a == 420, 'data not stored'); assert(foo.b == 1337, 'data not stored'); } diff --git a/crates/contracts/src/tests/world/events.cairo b/crates/contracts/src/tests/world/events.cairo new file mode 100644 index 0000000..2a807cf --- /dev/null +++ b/crates/contracts/src/tests/world/events.cairo @@ -0,0 +1,350 @@ +use starknet::{contract_address_const, ContractAddress, ClassHash}; + +use dojo::event::Event; +use dojo::utils::{bytearray_hash, entity_id_from_keys}; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, world}; +use dojo::world::world::{ + NamespaceRegistered, ModelRegistered, ModelUpgraded, MetadataUpdate, ContractRegistered, + ContractUpgraded +}; +use dojo::contract::{IContractDispatcher, IContractDispatcherTrait}; + +use dojo::tests::helpers::{ + deploy_world, deploy_world_for_event_upgrades, drop_all_events, FooEvent, foo_event, buzz_event +}; +use dojo::utils::test::spawn_test_world; + +#[dojo::event(version: 2)] +pub struct FooEventBadLayoutType { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +#[dojo::event(version: 2)] +struct FooEventNameChanged { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +#[dojo::event(version: 2)] +struct FooEventMemberRemoved { + #[key] + pub caller: ContractAddress, + pub b: u128, +} + +#[dojo::event(version: 2)] +struct FooEventMemberAddedButRemoved { + #[key] + pub caller: ContractAddress, + pub b: u128, + pub c: u256, + pub d: u256 +} + +#[dojo::event(version: 2)] +struct FooEventMemberAddedButMoved { + #[key] + pub caller: ContractAddress, + pub b: u128, + pub a: felt252, + pub c: u256 +} + +#[dojo::event] +struct FooEventMemberButSameVersion { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, + pub c: u256 +} + +#[dojo::event(version: 2)] +struct FooEventMemberAdded { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, + pub c: u256 +} + +#[test] +fn test_register_event_for_namespace_owner() { + let bob = starknet::contract_address_const::<0xb0b>(); + + let world = deploy_world(); + world.grant_owner(Event::::namespace_hash(), bob); + + drop_all_events(world.contract_address); + + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + world.register_event(foo_event::TEST_CLASS_HASH.try_into().unwrap()); + + let event = starknet::testing::pop_log::(world.contract_address); + assert(event.is_some(), 'no event)'); + + if let world::Event::EventRegistered(event) = event.unwrap() { + assert(event.name == Event::::name(), 'bad event name'); + assert(event.namespace == Event::::namespace(), 'bad event namespace'); + assert( + event.class_hash == foo_event::TEST_CLASS_HASH.try_into().unwrap(), + 'bad event class_hash' + ); + assert( + event.address != core::num::traits::Zero::::zero(), + 'bad event prev address' + ); + } else { + core::panic_with_felt252('no EventRegistered event'); + } + + assert(world.is_owner(Event::::selector(), bob), 'bob is not the owner'); +} + +#[test] +#[should_panic( + expected: ("Account `2827` does NOT have OWNER role on namespace `dojo`", 'ENTRYPOINT_FAILED',) +)] +fn test_register_event_for_namespace_writer() { + let bob = starknet::contract_address_const::<0xb0b>(); + + let world = deploy_world(); + world.grant_writer(Event::::namespace_hash(), bob); + + drop_all_events(world.contract_address); + + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + world.register_event(foo_event::TEST_CLASS_HASH.try_into().unwrap()); +} + +#[test] +fn test_upgrade_event_from_event_owner() { + let bob = starknet::contract_address_const::<0xb0b>(); + + let world = deploy_world_for_event_upgrades(); + world.grant_owner(Event::::selector(), bob); + + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + + drop_all_events(world.contract_address); + + world + .upgrade_event( + Event::::selector(), + foo_event_member_added::TEST_CLASS_HASH.try_into().unwrap() + ); + + let event = starknet::testing::pop_log::(world.contract_address); + assert(event.is_some(), 'no event)'); + + if let world::Event::EventUpgraded(event) = event.unwrap() { + assert( + event.class_hash == foo_event_member_added::TEST_CLASS_HASH.try_into().unwrap(), + 'bad model class_hash' + ); + + assert( + event.address != core::num::traits::Zero::::zero(), + 'bad model prev address' + ); + } else { + core::panic_with_felt252('no EventUpgraded event'); + } + + assert(world.is_owner(Event::::selector(), bob), 'bob is not the owner'); +} + +#[test] +fn test_upgrade_event() { + let world = deploy_world_for_event_upgrades(); + + drop_all_events(world.contract_address); + + world + .upgrade_event( + Event::::selector(), + foo_event_member_added::TEST_CLASS_HASH.try_into().unwrap() + ); + + let event = starknet::testing::pop_log::(world.contract_address); + + assert(event.is_some(), 'no ModelUpgraded event'); + let event = event.unwrap(); + assert( + event.class_hash == foo_event_member_added::TEST_CLASS_HASH.try_into().unwrap(), + 'bad model class_hash' + ); + assert( + event.address != core::num::traits::Zero::::zero(), 'bad model address' + ); +} + +#[test] +#[should_panic( + expected: ( + "Invalid new layout to upgrade the resource `3096059378939896478759206948098785810564909261576270289792934616419679543710`", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_upgrade_event_with_bad_layout_type() { + let world = deploy_world_for_event_upgrades(); + world + .upgrade_event( + Event::::selector(), + foo_event_bad_layout_type::TEST_CLASS_HASH.try_into().unwrap() + ); +} + +#[test] +#[should_panic( + expected: ( + "Invalid new schema to upgrade the resource `1978613259126754154559259544144231349453413846292589556290196962649661425572`", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_upgrade_event_with_name_change() { + let world = deploy_world_for_event_upgrades(); + world + .upgrade_event( + Event::::selector(), + foo_event_name_changed::TEST_CLASS_HASH.try_into().unwrap() + ); +} + +#[test] +#[should_panic( + expected: ( + "Invalid new schema to upgrade the resource `361072533419516152961739976459841981071623312081985419948600214596129482087`", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_upgrade_event_with_member_removed() { + let world = deploy_world_for_event_upgrades(); + world + .upgrade_event( + Event::::selector(), + foo_event_member_removed::TEST_CLASS_HASH.try_into().unwrap() + ); +} + +#[test] +#[should_panic( + expected: ( + "Invalid new schema to upgrade the resource `158342804955206503831721217884579656766969504799505203580537759170121480691`", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_upgrade_event_with_member_added_but_removed() { + let world = deploy_world_for_event_upgrades(); + world + .upgrade_event( + Event::::selector(), + foo_event_member_added_but_removed::TEST_CLASS_HASH.try_into().unwrap() + ); +} + +#[test] +#[should_panic( + expected: ( + "The new resource version of `3464629169105918854322628800815075086174820058177091358737862671011303488207` should be 2", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_upgrade_event_with_member_added_but_same_version() { + let world = deploy_world_for_event_upgrades(); + world + .upgrade_event( + Event::::selector(), + foo_event_member_but_same_version::TEST_CLASS_HASH.try_into().unwrap() + ); +} + + +#[test] +#[should_panic( + expected: ( + "Invalid new schema to upgrade the resource `2252659269513748615750074350172958465813667947162245103708863177206717341280`", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_upgrade_event_with_member_moved() { + let world = deploy_world_for_event_upgrades(); + world + .upgrade_event( + Event::::selector(), + foo_event_member_added_but_moved::TEST_CLASS_HASH.try_into().unwrap() + ); +} + +#[test] +#[should_panic( + expected: ( + "Account `659918` does NOT have OWNER role on event (or its namespace) `dojo-FooEventMemberAdded`", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_upgrade_event_from_event_writer() { + let alice = starknet::contract_address_const::<0xa11ce>(); + + let world = deploy_world_for_event_upgrades(); + + world.grant_writer(Event::::selector(), alice); + + starknet::testing::set_account_contract_address(alice); + starknet::testing::set_contract_address(alice); + world + .upgrade_event( + Event::::selector(), + foo_event_member_added::TEST_CLASS_HASH.try_into().unwrap() + ); +} + +#[test] +#[should_panic(expected: ("Resource `dojo-FooEvent` is already registered", 'ENTRYPOINT_FAILED',))] +fn test_upgrade_event_from_random_account() { + let bob = starknet::contract_address_const::<0xb0b>(); + let alice = starknet::contract_address_const::<0xa11ce>(); + + let world = deploy_world(); + world.grant_owner(Event::::namespace_hash(), bob); + world.grant_owner(Event::::namespace_hash(), alice); + + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + world.register_event(foo_event::TEST_CLASS_HASH.try_into().unwrap()); + + starknet::testing::set_account_contract_address(alice); + starknet::testing::set_contract_address(alice); + world.register_event(foo_event::TEST_CLASS_HASH.try_into().unwrap()); +} + +#[test] +#[should_panic(expected: ("Namespace `another_namespace` is not registered", 'ENTRYPOINT_FAILED',))] +fn test_register_event_with_unregistered_namespace() { + let world = deploy_world(); + world.register_event(buzz_event::TEST_CLASS_HASH.try_into().unwrap()); +} + +// It's CONTRACT_NOT_DEPLOYED for now as in this example the contract is not a dojo contract +// and it's not the account that is calling the register_event function. +#[test] +#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED',))] +fn test_register_event_through_malicious_contract() { + let bob = starknet::contract_address_const::<0xb0b>(); + let malicious_contract = starknet::contract_address_const::<0xdead>(); + + let world = deploy_world(); + world.grant_owner(Event::::namespace_hash(), bob); + + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(malicious_contract); + world.register_event(foo_event::TEST_CLASS_HASH.try_into().unwrap()); +} diff --git a/crates/contracts/src/tests/world/metadata.cairo b/crates/contracts/src/tests/world/metadata.cairo new file mode 100644 index 0000000..503d0e0 --- /dev/null +++ b/crates/contracts/src/tests/world/metadata.cairo @@ -0,0 +1,124 @@ +use starknet::{contract_address_const, ContractAddress, ClassHash}; + +use dojo::model::{Model, ResourceMetadata}; +use dojo::utils::{bytearray_hash, entity_id_from_keys}; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, world}; +use dojo::world::world::{ + Event, NamespaceRegistered, ModelRegistered, ModelUpgraded, MetadataUpdate, ContractRegistered, + ContractUpgraded +}; +use dojo::contract::{IContractDispatcher, IContractDispatcherTrait}; + +use dojo::tests::helpers::{ + deploy_world, deploy_world_for_model_upgrades, deploy_world_for_event_upgrades, drop_all_events, + Foo, foo, Buzz, buzz, test_contract, buzz_contract +}; +use dojo::utils::test::spawn_test_world; + +#[test] +fn test_set_metadata_world() { + let world = deploy_world(); + + let metadata = ResourceMetadata { + resource_id: 0, metadata_uri: format!("ipfs:world_with_a_long_uri_that") + }; + + world.set_metadata(metadata.clone()); + + assert(world.metadata(0) == metadata, 'invalid metadata'); +} + +#[test] +fn test_set_metadata_resource_owner() { + let world = spawn_test_world(["dojo"].span(), [foo::TEST_CLASS_HASH].span(), [].span()); + + let bob = starknet::contract_address_const::<0xb0b>(); + + world.grant_owner(Model::::selector(), bob); + + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + + let metadata = ResourceMetadata { + resource_id: Model::::selector(), metadata_uri: format!("ipfs:bob") + }; + + drop_all_events(world.contract_address); + + // Metadata must be updated by a direct call from an account which has owner role + // for the attached resource. + world.set_metadata(metadata.clone()); + assert(world.metadata(Model::::selector()) == metadata, 'bad metadata'); + + match starknet::testing::pop_log::(world.contract_address).unwrap() { + Event::MetadataUpdate(event) => { + assert(event.resource == metadata.resource_id, 'bad resource'); + assert(event.uri == metadata.metadata_uri, 'bad uri'); + }, + _ => panic!("no MetadataUpdate event"), + } +} + +#[test] +#[should_panic( + expected: ( + "Account `2827` does NOT have OWNER role on model (or its namespace) `dojo-Foo`", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_set_metadata_not_possible_for_resource_writer() { + let world = spawn_test_world(["dojo"].span(), [foo::TEST_CLASS_HASH].span(), [].span()); + + let bob = starknet::contract_address_const::<0xb0b>(); + + world.grant_writer(Model::::selector(), bob); + + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + + let metadata = ResourceMetadata { + resource_id: Model::::selector(), metadata_uri: format!("ipfs:bob") + }; + + world.set_metadata(metadata.clone()); +} + +#[test] +#[should_panic( + expected: ("Account `2827` does NOT have OWNER role on world", 'ENTRYPOINT_FAILED',) +)] +fn test_set_metadata_not_possible_for_random_account() { + let world = deploy_world(); + + let metadata = ResourceMetadata { // World metadata. + resource_id: 0, metadata_uri: format!("ipfs:bob"), + }; + + let bob = starknet::contract_address_const::<0xb0b>(); + starknet::testing::set_contract_address(bob); + starknet::testing::set_account_contract_address(bob); + + // Bob access follows the conventional ACL, he can't write the world + // metadata if he does not have access to it. + world.set_metadata(metadata); +} + +#[test] +#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED',))] +fn test_set_metadata_through_malicious_contract() { + let world = spawn_test_world(["dojo"].span(), [foo::TEST_CLASS_HASH].span(), [].span()); + + let bob = starknet::contract_address_const::<0xb0b>(); + let malicious_contract = starknet::contract_address_const::<0xdead>(); + + world.grant_owner(Model::::selector(), bob); + + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(malicious_contract); + + let metadata = ResourceMetadata { + resource_id: Model::::selector(), metadata_uri: format!("ipfs:bob") + }; + + world.set_metadata(metadata.clone()); +} diff --git a/crates/contracts/src/tests/world/models.cairo b/crates/contracts/src/tests/world/models.cairo new file mode 100644 index 0000000..ee1cd31 --- /dev/null +++ b/crates/contracts/src/tests/world/models.cairo @@ -0,0 +1,357 @@ +use starknet::{contract_address_const, ContractAddress, ClassHash}; + +use dojo::model::{Model, ResourceMetadata}; +use dojo::utils::{bytearray_hash, entity_id_from_keys}; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, world}; +use dojo::world::world::{ + Event, NamespaceRegistered, ModelRegistered, ModelUpgraded, MetadataUpdate, ContractRegistered, + ContractUpgraded +}; +use dojo::contract::{IContractDispatcher, IContractDispatcherTrait}; + +use dojo::tests::helpers::{ + deploy_world, deploy_world_for_model_upgrades, drop_all_events, Foo, foo, Buzz, buzz, + test_contract, buzz_contract +}; +use dojo::utils::test::spawn_test_world; + +#[derive(Copy, Drop, Serde, Debug, IntrospectPacked)] +#[dojo::model(version: 2)] +pub struct FooBadLayoutType { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model(version: 2)] +struct FooNameChanged { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model(version: 2)] +struct FooMemberRemoved { + #[key] + pub caller: ContractAddress, + pub b: u128, +} + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model(version: 2)] +struct FooMemberAddedButRemoved { + #[key] + pub caller: ContractAddress, + pub b: u128, + pub c: u256, + pub d: u256 +} + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model(version: 2)] +struct FooMemberAddedButMoved { + #[key] + pub caller: ContractAddress, + pub b: u128, + pub a: felt252, + pub c: u256 +} + + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model] +struct FooMemberAddedButSameVersion { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, + pub c: u256 +} + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model(version: 2)] +struct FooMemberAdded { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, + pub c: u256 +} + +#[test] +fn test_register_model_for_namespace_owner() { + let bob = starknet::contract_address_const::<0xb0b>(); + + let world = deploy_world(); + world.grant_owner(Model::::namespace_hash(), bob); + + drop_all_events(world.contract_address); + + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); + + let event = starknet::testing::pop_log::(world.contract_address); + assert(event.is_some(), 'no event)'); + + if let Event::ModelRegistered(event) = event.unwrap() { + assert(event.name == Model::::name(), 'bad model name'); + assert(event.namespace == Model::::namespace(), 'bad model namespace'); + assert( + event.class_hash == foo::TEST_CLASS_HASH.try_into().unwrap(), 'bad model class_hash' + ); + assert( + event.address != core::num::traits::Zero::::zero(), + 'bad model prev address' + ); + } else { + core::panic_with_felt252('no ModelRegistered event'); + } + + assert(world.is_owner(Model::::selector(), bob), 'bob is not the owner'); +} + +#[test] +#[should_panic( + expected: ("Account `2827` does NOT have OWNER role on namespace `dojo`", 'ENTRYPOINT_FAILED',) +)] +fn test_register_model_for_namespace_writer() { + let bob = starknet::contract_address_const::<0xb0b>(); + + let world = deploy_world(); + world.grant_writer(Model::::namespace_hash(), bob); + + drop_all_events(world.contract_address); + + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); +} + +#[test] +fn test_upgrade_model_from_model_owner() { + let bob = starknet::contract_address_const::<0xb0b>(); + + let world = deploy_world_for_model_upgrades(); + world.grant_owner(Model::::selector(), bob); + + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + + drop_all_events(world.contract_address); + + world + .upgrade_model( + Model::::selector(), + foo_member_added::TEST_CLASS_HASH.try_into().unwrap() + ); + + let event = starknet::testing::pop_log::(world.contract_address); + assert(event.is_some(), 'no event)'); + + if let Event::ModelUpgraded(event) = event.unwrap() { + assert( + event.class_hash == foo_member_added::TEST_CLASS_HASH.try_into().unwrap(), + 'bad model class_hash' + ); + + assert( + event.address != core::num::traits::Zero::::zero(), + 'bad model prev address' + ); + } else { + core::panic_with_felt252('no ModelUpgraded event'); + } + + assert(world.is_owner(Model::::selector(), bob), 'bob is not the owner'); +} + +#[test] +fn test_upgrade_model() { + let world = deploy_world_for_model_upgrades(); + + drop_all_events(world.contract_address); + + world + .upgrade_model( + Model::::selector(), + foo_member_added::TEST_CLASS_HASH.try_into().unwrap() + ); + + let event = starknet::testing::pop_log::(world.contract_address); + + assert(event.is_some(), 'no ModelUpgraded event'); + let event = event.unwrap(); + assert( + event.class_hash == foo_member_added::TEST_CLASS_HASH.try_into().unwrap(), + 'bad model class_hash' + ); + assert( + event.address != core::num::traits::Zero::::zero(), 'bad model address' + ); +} + +#[test] +#[should_panic( + expected: ( + "Invalid new layout to upgrade the resource `3299332835749357934986569383926439331000812010239905600952804594672861482231`", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_upgrade_model_with_bad_layout_type() { + let world = deploy_world_for_model_upgrades(); + world + .upgrade_model( + Model::::selector(), + foo_bad_layout_type::TEST_CLASS_HASH.try_into().unwrap() + ); +} + +#[test] +#[should_panic( + expected: ( + "Invalid new schema to upgrade the resource `3123252206139358744730647958636922105676576163624049771737508399526017186883`", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_upgrade_model_with_name_change() { + let world = deploy_world_for_model_upgrades(); + world + .upgrade_model( + Model::::selector(), foo_name_changed::TEST_CLASS_HASH.try_into().unwrap() + ); +} + +#[test] +#[should_panic( + expected: ( + "Invalid new schema to upgrade the resource `991779734441782832082403572095809709808010858956594544283871161035940786254`", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_upgrade_model_with_member_removed() { + let world = deploy_world_for_model_upgrades(); + world + .upgrade_model( + Model::::selector(), + foo_member_removed::TEST_CLASS_HASH.try_into().unwrap() + ); +} + +#[test] +#[should_panic( + expected: ( + "Invalid new schema to upgrade the resource `832347970429487891546414803397849087808560440584474009458460185937208465364`", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_upgrade_model_with_member_added_but_removed() { + let world = deploy_world_for_model_upgrades(); + world + .upgrade_model( + Model::::selector(), + foo_member_added_but_removed::TEST_CLASS_HASH.try_into().unwrap() + ); +} + +#[test] +#[should_panic( + expected: ( + "The new resource version of `1624956305639059314433508277897382957139753261232513354727598365317619941481` should be 2", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_upgrade_model_with_member_added_but_same_version() { + let world = deploy_world_for_model_upgrades(); + world + .upgrade_model( + Model::::selector(), + foo_member_added_but_same_version::TEST_CLASS_HASH.try_into().unwrap() + ); +} + + +#[test] +#[should_panic( + expected: ( + "Invalid new schema to upgrade the resource `24285692591026114610735893315325215980821705916443621541163513530524539878`", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_upgrade_model_with_member_moved() { + let world = deploy_world_for_model_upgrades(); + world + .upgrade_model( + Model::::selector(), + foo_member_added_but_moved::TEST_CLASS_HASH.try_into().unwrap() + ); +} + +#[test] +#[should_panic( + expected: ( + "Account `659918` does NOT have OWNER role on model (or its namespace) `dojo-FooMemberAdded`", + 'ENTRYPOINT_FAILED', + ) +)] +fn test_upgrade_model_from_model_writer() { + let alice = starknet::contract_address_const::<0xa11ce>(); + + let world = deploy_world_for_model_upgrades(); + + world.grant_writer(Model::::selector(), alice); + + starknet::testing::set_account_contract_address(alice); + starknet::testing::set_contract_address(alice); + world + .upgrade_model( + Model::::selector(), foo_member_added::TEST_CLASS_HASH.try_into().unwrap() + ); +} + +#[test] +#[should_panic(expected: ("Resource `dojo-Foo` is already registered", 'ENTRYPOINT_FAILED',))] +fn test_upgrade_model_from_random_account() { + let bob = starknet::contract_address_const::<0xb0b>(); + let alice = starknet::contract_address_const::<0xa11ce>(); + + let world = deploy_world(); + world.grant_owner(Model::::namespace_hash(), bob); + world.grant_owner(Model::::namespace_hash(), alice); + + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); + + starknet::testing::set_account_contract_address(alice); + starknet::testing::set_contract_address(alice); + world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); +} + +#[test] +#[should_panic(expected: ("Namespace `another_namespace` is not registered", 'ENTRYPOINT_FAILED',))] +fn test_register_model_with_unregistered_namespace() { + let world = deploy_world(); + world.register_model(buzz::TEST_CLASS_HASH.try_into().unwrap()); +} + +// It's CONTRACT_NOT_DEPLOYED for now as in this example the contract is not a dojo contract +// and it's not the account that is calling the register_model function. +#[test] +#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED',))] +fn test_register_model_through_malicious_contract() { + let bob = starknet::contract_address_const::<0xb0b>(); + let malicious_contract = starknet::contract_address_const::<0xdead>(); + + let world = deploy_world(); + world.grant_owner(Model::::namespace_hash(), bob); + + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(malicious_contract); + world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); +} diff --git a/crates/contracts/src/tests/world/namespaces.cairo b/crates/contracts/src/tests/world/namespaces.cairo new file mode 100644 index 0000000..620be10 --- /dev/null +++ b/crates/contracts/src/tests/world/namespaces.cairo @@ -0,0 +1,73 @@ +use starknet::{contract_address_const, ContractAddress, ClassHash}; + +use dojo::model::{Model, ResourceMetadata}; +use dojo::utils::{bytearray_hash, entity_id_from_keys}; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, world}; +use dojo::world::world::{ + Event, NamespaceRegistered, ModelRegistered, ModelUpgraded, MetadataUpdate, ContractRegistered, + ContractUpgraded +}; +use dojo::contract::{IContractDispatcher, IContractDispatcherTrait}; + +use dojo::tests::helpers::{ + deploy_world, deploy_world_for_model_upgrades, deploy_world_for_event_upgrades, drop_all_events, + Foo, foo, Buzz, buzz, test_contract, buzz_contract +}; +use dojo::utils::test::spawn_test_world; + +#[test] +fn test_register_namespace() { + let world = deploy_world(); + + let bob = starknet::contract_address_const::<0xb0b>(); + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + + drop_all_events(world.contract_address); + + let namespace = "namespace"; + let hash = bytearray_hash(@namespace); + + world.register_namespace(namespace.clone()); + + assert(world.is_owner(hash, bob), 'namespace not registered'); + + match starknet::testing::pop_log::(world.contract_address).unwrap() { + Event::NamespaceRegistered(event) => { + assert(event.namespace == namespace, 'bad namespace'); + assert(event.hash == hash, 'bad hash'); + }, + _ => panic!("no NamespaceRegistered event"), + } +} + +#[test] +#[should_panic(expected: ("Namespace `namespace` is already registered", 'ENTRYPOINT_FAILED',))] +fn test_register_namespace_already_registered_same_caller() { + let world = deploy_world(); + + let bob = starknet::contract_address_const::<0xb0b>(); + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + + world.register_namespace("namespace"); + world.register_namespace("namespace"); +} + +#[test] +#[should_panic(expected: ("Namespace `namespace` is already registered", 'ENTRYPOINT_FAILED',))] +fn test_register_namespace_already_registered_other_caller() { + let world = deploy_world(); + + let bob = starknet::contract_address_const::<0xb0b>(); + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bob); + + world.register_namespace("namespace"); + + let alice = starknet::contract_address_const::<0xa11ce>(); + starknet::testing::set_account_contract_address(alice); + starknet::testing::set_contract_address(alice); + + world.register_namespace("namespace"); +} diff --git a/crates/contracts/src/tests/world/resources.cairo b/crates/contracts/src/tests/world/resources.cairo deleted file mode 100644 index 504929d..0000000 --- a/crates/contracts/src/tests/world/resources.cairo +++ /dev/null @@ -1,525 +0,0 @@ -use starknet::{contract_address_const, ContractAddress, ClassHash}; - -use dojo::model::{Model, ResourceMetadata}; -use dojo::utils::{bytearray_hash, entity_id_from_keys}; -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, world}; -use dojo::world::world::{ - Event, NamespaceRegistered, ModelRegistered, ModelUpgraded, MetadataUpdate, ContractRegistered, - ContractUpgraded -}; -use dojo::contract::{IContractDispatcher, IContractDispatcherTrait}; - -use dojo::tests::helpers::{ - deploy_world, drop_all_events, Foo, foo, Buzz, buzz, test_contract, buzz_contract -}; -use dojo::utils::test::spawn_test_world; - -#[test] -fn test_set_metadata_world() { - let world = deploy_world(); - - let metadata = ResourceMetadata { - resource_id: 0, metadata_uri: format!("ipfs:world_with_a_long_uri_that") - }; - - world.set_metadata(metadata.clone()); - - assert(world.metadata(0) == metadata, 'invalid metadata'); -} - -#[test] -fn test_set_metadata_resource_owner() { - let world = spawn_test_world(["dojo"].span(), [foo::TEST_CLASS_HASH].span(),); - - let bob = starknet::contract_address_const::<0xb0b>(); - - world.grant_owner(Model::::selector(), bob); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - - let metadata = ResourceMetadata { - resource_id: Model::::selector(), metadata_uri: format!("ipfs:bob") - }; - - drop_all_events(world.contract_address); - - // Metadata must be updated by a direct call from an account which has owner role - // for the attached resource. - world.set_metadata(metadata.clone()); - assert(world.metadata(Model::::selector()) == metadata, 'bad metadata'); - - match starknet::testing::pop_log::(world.contract_address).unwrap() { - Event::MetadataUpdate(event) => { - assert(event.resource == metadata.resource_id, 'bad resource'); - assert(event.uri == metadata.metadata_uri, 'bad uri'); - }, - _ => panic!("no MetadataUpdate event"), - } -} - -#[test] -#[should_panic( - expected: ( - "Account `2827` does NOT have OWNER role on model (or its namespace) `dojo-Foo`", - 'ENTRYPOINT_FAILED', - ) -)] -fn test_set_metadata_not_possible_for_resource_writer() { - let world = spawn_test_world(["dojo"].span(), [foo::TEST_CLASS_HASH].span(),); - - let bob = starknet::contract_address_const::<0xb0b>(); - - world.grant_writer(Model::::selector(), bob); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - - let metadata = ResourceMetadata { - resource_id: Model::::selector(), metadata_uri: format!("ipfs:bob") - }; - - world.set_metadata(metadata.clone()); -} - -#[test] -#[should_panic( - expected: ("Account `2827` does NOT have OWNER role on world", 'ENTRYPOINT_FAILED',) -)] -fn test_set_metadata_not_possible_for_random_account() { - let world = deploy_world(); - - let metadata = ResourceMetadata { // World metadata. - resource_id: 0, metadata_uri: format!("ipfs:bob"), - }; - - let bob = starknet::contract_address_const::<0xb0b>(); - starknet::testing::set_contract_address(bob); - starknet::testing::set_account_contract_address(bob); - - // Bob access follows the conventional ACL, he can't write the world - // metadata if he does not have access to it. - world.set_metadata(metadata); -} - -#[test] -#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED',))] -fn test_set_metadata_through_malicious_contract() { - let world = spawn_test_world(["dojo"].span(), [foo::TEST_CLASS_HASH].span(),); - - let bob = starknet::contract_address_const::<0xb0b>(); - let malicious_contract = starknet::contract_address_const::<0xdead>(); - - world.grant_owner(Model::::selector(), bob); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(malicious_contract); - - let metadata = ResourceMetadata { - resource_id: Model::::selector(), metadata_uri: format!("ipfs:bob") - }; - - world.set_metadata(metadata.clone()); -} - -#[test] -fn test_register_model_for_namespace_owner() { - let bob = starknet::contract_address_const::<0xb0b>(); - - let world = deploy_world(); - world.grant_owner(Model::::namespace_hash(), bob); - - drop_all_events(world.contract_address); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - - let event = starknet::testing::pop_log::(world.contract_address); - assert(event.is_some(), 'no event)'); - - if let Event::ModelRegistered(event) = event.unwrap() { - assert(event.name == Model::::name(), 'bad model name'); - assert(event.namespace == Model::::namespace(), 'bad model namespace'); - assert( - event.class_hash == foo::TEST_CLASS_HASH.try_into().unwrap(), 'bad model class_hash' - ); - assert( - event.address != core::num::traits::Zero::::zero(), - 'bad model prev address' - ); - } else { - core::panic_with_felt252('no ModelRegistered event'); - } - - assert(world.is_owner(Model::::selector(), bob), 'bob is not the owner'); -} - -#[test] -#[should_panic( - expected: ("Account `2827` does NOT have OWNER role on namespace `dojo`", 'ENTRYPOINT_FAILED',) -)] -fn test_register_model_for_namespace_writer() { - let bob = starknet::contract_address_const::<0xb0b>(); - - let world = deploy_world(); - world.grant_writer(Model::::namespace_hash(), bob); - - drop_all_events(world.contract_address); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -fn test_upgrade_model_from_model_owner() { - let bob = starknet::contract_address_const::<0xb0b>(); - - let world = deploy_world(); - world.grant_owner(Model::::namespace_hash(), bob); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - - drop_all_events(world.contract_address); - - world.upgrade_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - - let event = starknet::testing::pop_log::(world.contract_address); - assert(event.is_some(), 'no event)'); - - if let Event::ModelUpgraded(event) = event.unwrap() { - assert(event.selector == Model::::selector(), 'bad model selector'); - assert( - event.class_hash == foo::TEST_CLASS_HASH.try_into().unwrap(), 'bad model class_hash' - ); - - assert( - event.address != core::num::traits::Zero::::zero(), - 'bad model prev address' - ); - } else { - core::panic_with_felt252('no ModelRegistered event'); - } - - assert(world.is_owner(Model::::selector(), bob), 'bob is not the owner'); -} - -#[test] -#[should_panic( - expected: ( - "Account `659918` does NOT have OWNER role on namespace `dojo`", 'ENTRYPOINT_FAILED', - ) -)] -fn test_upgrade_model_from_model_writer() { - let bob = starknet::contract_address_const::<0xb0b>(); - let alice = starknet::contract_address_const::<0xa11ce>(); - - let world = deploy_world(); - // dojo namespace is registered by the deploy_world function. - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - world.grant_owner(Model::::namespace_hash(), bob); - world.grant_writer(Model::::namespace_hash(), alice); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - world.upgrade_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(alice); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -#[should_panic(expected: ("Resource `dojo-Foo` is already registered", 'ENTRYPOINT_FAILED',))] -fn test_upgrade_model_from_random_account() { - let bob = starknet::contract_address_const::<0xb0b>(); - let alice = starknet::contract_address_const::<0xa11ce>(); - - let world = deploy_world(); - world.grant_owner(Model::::namespace_hash(), bob); - world.grant_owner(Model::::namespace_hash(), alice); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(alice); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -#[should_panic(expected: ("Namespace `another_namespace` is not registered", 'ENTRYPOINT_FAILED',))] -fn test_register_model_with_unregistered_namespace() { - let world = deploy_world(); - world.register_model(buzz::TEST_CLASS_HASH.try_into().unwrap()); -} - -// It's CONTRACT_NOT_DEPLOYED for now as in this example the contract is not a dojo contract -// and it's not the account that is calling the register_model function. -#[test] -#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED',))] -fn test_register_model_through_malicious_contract() { - let bob = starknet::contract_address_const::<0xb0b>(); - let malicious_contract = starknet::contract_address_const::<0xdead>(); - - let world = deploy_world(); - world.grant_owner(Model::::namespace_hash(), bob); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(malicious_contract); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -fn test_register_namespace() { - let world = deploy_world(); - - let bob = starknet::contract_address_const::<0xb0b>(); - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - - drop_all_events(world.contract_address); - - let namespace = "namespace"; - let hash = bytearray_hash(@namespace); - - world.register_namespace(namespace.clone()); - - assert(world.is_owner(hash, bob), 'namespace not registered'); - - match starknet::testing::pop_log::(world.contract_address).unwrap() { - Event::NamespaceRegistered(event) => { - assert(event.namespace == namespace, 'bad namespace'); - assert(event.hash == hash, 'bad hash'); - }, - _ => panic!("no NamespaceRegistered event"), - } -} - -#[test] -#[should_panic(expected: ("Namespace `namespace` is already registered", 'ENTRYPOINT_FAILED',))] -fn test_register_namespace_already_registered_same_caller() { - let world = deploy_world(); - - let bob = starknet::contract_address_const::<0xb0b>(); - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - - world.register_namespace("namespace"); - world.register_namespace("namespace"); -} - -#[test] -#[should_panic(expected: ("Namespace `namespace` is already registered", 'ENTRYPOINT_FAILED',))] -fn test_register_namespace_already_registered_other_caller() { - let world = deploy_world(); - - let bob = starknet::contract_address_const::<0xb0b>(); - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - - world.register_namespace("namespace"); - - let alice = starknet::contract_address_const::<0xa11ce>(); - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(alice); - - world.register_namespace("namespace"); -} - -#[test] -fn test_deploy_contract_for_namespace_owner() { - let world = deploy_world(); - let class_hash = test_contract::TEST_CLASS_HASH.try_into().unwrap(); - - let bob = starknet::contract_address_const::<0xb0b>(); - world.grant_owner(bytearray_hash(@"dojo"), bob); - - // the account owns the 'test_contract' namespace so it should be able to deploy the contract. - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - - drop_all_events(world.contract_address); - - let contract_address = world.register_contract('salt1', class_hash, [].span()); - - let event = match starknet::testing::pop_log::(world.contract_address).unwrap() { - Event::ContractRegistered(event) => event, - _ => panic!("no ContractRegistered event"), - }; - - let dispatcher = IContractDispatcher { contract_address }; - - assert(event.salt == 'salt1', 'bad event salt'); - assert(event.class_hash == class_hash, 'bad class_hash'); - assert(event.selector == dispatcher.selector(), 'bad contract selector'); - assert( - event.address != core::num::traits::Zero::::zero(), 'bad contract address' - ); -} - -#[test] -#[should_panic( - expected: ("Account `2827` does NOT have OWNER role on namespace `dojo`", 'ENTRYPOINT_FAILED',) -)] -fn test_deploy_contract_for_namespace_writer() { - let world = deploy_world(); - - let bob = starknet::contract_address_const::<0xb0b>(); - world.grant_writer(bytearray_hash(@"dojo"), bob); - - // the account has write access to the 'test_contract' namespace so it should be able to deploy - // the contract. - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - - world.register_contract('salt1', test_contract::TEST_CLASS_HASH.try_into().unwrap(), [].span()); -} - - -#[test] -#[should_panic( - expected: ("Account `2827` does NOT have OWNER role on namespace `dojo`", 'ENTRYPOINT_FAILED',) -)] -fn test_deploy_contract_no_namespace_owner_access() { - let world = deploy_world(); - - let bob = starknet::contract_address_const::<0xb0b>(); - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - - world.register_contract('salt1', test_contract::TEST_CLASS_HASH.try_into().unwrap(), [].span()); -} - -#[test] -#[should_panic(expected: ("Namespace `buzz_namespace` is not registered", 'ENTRYPOINT_FAILED',))] -fn test_deploy_contract_with_unregistered_namespace() { - let world = deploy_world(); - world.register_contract('salt1', buzz_contract::TEST_CLASS_HASH.try_into().unwrap(), [].span()); -} - -// It's CONTRACT_NOT_DEPLOYED for now as in this example the contract is not a dojo contract -// and it's not the account that is calling the deploy_contract function. -#[test] -#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED',))] -fn test_deploy_contract_through_malicious_contract() { - let world = deploy_world(); - - let bob = starknet::contract_address_const::<0xb0b>(); - let malicious_contract = starknet::contract_address_const::<0xdead>(); - - world.grant_owner(bytearray_hash(@"dojo"), bob); - - // the account owns the 'test_contract' namespace so it should be able to deploy the contract. - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(malicious_contract); - - world.register_contract('salt1', test_contract::TEST_CLASS_HASH.try_into().unwrap(), [].span()); -} - -#[test] -fn test_upgrade_contract_from_resource_owner() { - let world = deploy_world(); - let class_hash = test_contract::TEST_CLASS_HASH.try_into().unwrap(); - - let bob = starknet::contract_address_const::<0xb0b>(); - - world.grant_owner(bytearray_hash(@"dojo"), bob); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - - let contract_address = world.register_contract('salt1', class_hash, [].span()); - let dispatcher = IContractDispatcher { contract_address }; - - drop_all_events(world.contract_address); - - world.upgrade_contract(class_hash); - - let event = starknet::testing::pop_log::(world.contract_address); - assert(event.is_some(), 'no event)'); - - if let Event::ContractUpgraded(event) = event.unwrap() { - assert(event.selector == dispatcher.selector(), 'bad contract selector'); - assert(event.class_hash == class_hash, 'bad class_hash'); - } else { - core::panic_with_felt252('no ContractUpgraded event'); - }; -} - -#[test] -#[should_panic( - expected: ( - "Account `659918` does NOT have OWNER role on contract (or its namespace) `dojo-test_contract`", - 'ENTRYPOINT_FAILED', - ) -)] -fn test_upgrade_contract_from_resource_writer() { - let world = deploy_world(); - let class_hash = test_contract::TEST_CLASS_HASH.try_into().unwrap(); - - let bob = starknet::contract_address_const::<0xb0b>(); - let alice = starknet::contract_address_const::<0xa11ce>(); - - world.grant_owner(bytearray_hash(@"dojo"), bob); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - - let contract_address = world.register_contract('salt1', class_hash, [].span()); - - let dispatcher = IContractDispatcher { contract_address }; - - world.grant_writer(dispatcher.selector(), alice); - - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(alice); - - world.upgrade_contract(class_hash); -} - -#[test] -#[should_panic( - expected: ( - "Account `659918` does NOT have OWNER role on contract (or its namespace) `dojo-test_contract`", - 'ENTRYPOINT_FAILED', - ) -)] -fn test_upgrade_contract_from_random_account() { - let world = deploy_world(); - let class_hash = test_contract::TEST_CLASS_HASH.try_into().unwrap(); - - let _contract_address = world.register_contract('salt1', class_hash, [].span()); - - let alice = starknet::contract_address_const::<0xa11ce>(); - - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(alice); - - world.upgrade_contract(class_hash); -} - -#[test] -#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED',))] -fn test_upgrade_contract_through_malicious_contract() { - let world = deploy_world(); - let class_hash = test_contract::TEST_CLASS_HASH.try_into().unwrap(); - - let bob = starknet::contract_address_const::<0xb0b>(); - let malicious_contract = starknet::contract_address_const::<0xdead>(); - - world.grant_owner(bytearray_hash(@"dojo"), bob); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - - let _contract_address = world.register_contract('salt1', class_hash, [].span()); - - starknet::testing::set_contract_address(malicious_contract); - - world.upgrade_contract(class_hash); -} diff --git a/crates/contracts/src/tests/world/world.cairo b/crates/contracts/src/tests/world/world.cairo index 7054d5a..81e15a3 100644 --- a/crates/contracts/src/tests/world/world.cairo +++ b/crates/contracts/src/tests/world/world.cairo @@ -140,13 +140,13 @@ fn test_emit() { // Utils fn deploy_world() -> IWorldDispatcher { - spawn_test_world(["dojo"].span(), [].span()) + spawn_test_world(["dojo"].span(), [].span(), [].span()) } #[test] fn test_execute_multiple_worlds() { // Deploy world contract - let world1 = spawn_test_world(["dojo"].span(), [foo::TEST_CLASS_HASH].span(),); + let world1 = spawn_test_world(["dojo"].span(), [foo::TEST_CLASS_HASH].span(), [].span()); let contract_address = deploy_with_world_address(bar::TEST_CLASS_HASH, world1); world1.grant_writer(Model::::selector(), contract_address); @@ -154,7 +154,7 @@ fn test_execute_multiple_worlds() { let bar1_contract = IbarDispatcher { contract_address }; // Deploy another world contract - let world2 = spawn_test_world(["dojo"].span(), [foo::TEST_CLASS_HASH].span(),); + let world2 = spawn_test_world(["dojo"].span(), [foo::TEST_CLASS_HASH].span(), [].span()); let contract_address = deploy_with_world_address(bar::TEST_CLASS_HASH, world2); world2.grant_writer(Model::::selector(), contract_address); @@ -196,7 +196,7 @@ fn bench_execute() { #[test] fn bench_execute_complex() { - let world = spawn_test_world(["dojo"].span(), [character::TEST_CLASS_HASH].span(),); + let world = spawn_test_world(["dojo"].span(), [character::TEST_CLASS_HASH].span(), [].span()); let contract_address = deploy_with_world_address(bar::TEST_CLASS_HASH, world); let bar_contract = IbarDispatcher { contract_address }; diff --git a/crates/contracts/src/utils/test.cairo b/crates/contracts/src/utils/test.cairo index 9740c91..a5c1a35 100644 --- a/crates/contracts/src/utils/test.cairo +++ b/crates/contracts/src/utils/test.cairo @@ -45,11 +45,14 @@ pub fn deploy_with_world_address(class_hash: felt252, world: IWorldDispatcher) - /// /// * `namespaces` - Namespaces to register. /// * `models` - Models to register. +/// * `events` - Events to register. /// /// # Returns /// /// * World dispatcher -pub fn spawn_test_world(namespaces: Span, models: Span) -> IWorldDispatcher { +pub fn spawn_test_world( + namespaces: Span, models: Span, events: Span +) -> IWorldDispatcher { let salt = core::testing::get_available_gas(); let (world_address, _) = deploy_syscall( @@ -78,6 +81,16 @@ pub fn spawn_test_world(namespaces: Span, models: Span) -> I index += 1; }; + // Register all events. + let mut index = 0; + loop { + if index == events.len() { + break (); + } + world.register_event((*events[index]).try_into().unwrap()); + index += 1; + }; + world } diff --git a/crates/contracts/src/world/errors.cairo b/crates/contracts/src/world/errors.cairo index 44b0ab0..e4e00a1 100644 --- a/crates/contracts/src/world/errors.cairo +++ b/crates/contracts/src/world/errors.cairo @@ -73,3 +73,19 @@ pub fn no_model_write_access(tag: @ByteArray, caller: ContractAddress) -> ByteAr pub fn no_world_owner(caller: ContractAddress, target: @ByteArray) -> ByteArray { format!("Caller `{:?}` can't {} (not world owner)", caller, target) } + +pub fn invalid_class_content(selector: felt252) -> ByteArray { + format!("Invalid ClassHash to upgrade the resource `{}`", selector) +} + +pub fn invalid_resource_schema_upgrade(selector: felt252) -> ByteArray { + format!("Invalid new schema to upgrade the resource `{}`", selector) +} + +pub fn invalid_resource_layout_upgrade(selector: felt252) -> ByteArray { + format!("Invalid new layout to upgrade the resource `{}`", selector) +} + +pub fn invalid_resource_version_upgrade(selector: felt252, expected_version: u8) -> ByteArray { + format!("The new resource version of `{}` should be {}", selector, expected_version) +} diff --git a/crates/contracts/src/world/iworld.cairo b/crates/contracts/src/world/iworld.cairo index 7f52f51..d1cc13a 100644 --- a/crates/contracts/src/world/iworld.cairo +++ b/crates/contracts/src/world/iworld.cairo @@ -85,15 +85,17 @@ pub trait IWorld { /// /// # Arguments /// + /// * `selector` - The selector of the model to be upgraded. /// * `class_hash` - The class hash of the event to be upgraded. - fn upgrade_event(ref self: T, class_hash: ClassHash); + fn upgrade_event(ref self: T, selector: felt252, class_hash: ClassHash); /// Upgrades a model in the world. /// /// # Arguments /// + /// * `selector` - The selector of the model to be upgraded. /// * `class_hash` - The class hash of the model to be upgraded. - fn upgrade_model(ref self: T, class_hash: ClassHash); + fn upgrade_model(ref self: T, selector: felt252, class_hash: ClassHash); /// Upgrades an already deployed contract associated with the world and returns the new class /// hash. diff --git a/crates/contracts/src/world/world_contract.cairo b/crates/contracts/src/world/world_contract.cairo index f744690..9e6262d 100644 --- a/crates/contracts/src/world/world_contract.cairo +++ b/crates/contracts/src/world/world_contract.cairo @@ -47,12 +47,13 @@ pub mod world { IUpgradeableDispatcher, IUpgradeableDispatcherTrait }; use dojo::contract::{IContractDispatcher, IContractDispatcherTrait}; - use dojo::meta::Layout; + use dojo::meta::{Layout, LayoutCompareTrait}; use dojo::event::{IEventDispatcher, IEventDispatcherTrait}; use dojo::model::{ Model, IModelDispatcher, IModelDispatcherTrait, ResourceMetadata, ResourceMetadataTrait, metadata, ModelIndex }; + use dojo::meta::introspect::{Struct, Ty, StructCompareTrait}; use dojo::storage; use dojo::utils::{ entity_id_from_keys, bytearray_hash, Descriptor, DescriptorTrait, IDescriptorDispatcher, @@ -66,6 +67,13 @@ pub mod world { pub const WORLD: felt252 = 0; + #[starknet::interface] + trait IResource { + fn version(self: @T) -> u8; + fn layout(self: @T) -> Layout; + fn schema(self: @T) -> Struct; + } + #[event] #[derive(Drop, starknet::Event)] pub enum Event { @@ -449,17 +457,15 @@ pub mod world { ); } - fn upgrade_event(ref self: ContractState, class_hash: ClassHash) { - let salt = self.events_salt.read(); - - let (new_contract_address, _) = starknet::syscalls::deploy_syscall( - class_hash, salt.into(), [].span(), false, + fn upgrade_event(ref self: ContractState, selector: felt252, class_hash: ClassHash) { + // Using a library call is not safe as arbitrary code is executed. + // (see upgrade_model for more details). + let (new_event_address, _) = starknet::syscalls::deploy_syscall( + class_hash, starknet::get_tx_info().unbox().transaction_hash, [].span(), false, ) .unwrap_syscall(); - self.events_salt.write(salt + 1); - - let new_descriptor = DescriptorTrait::from_contract_assert(new_contract_address); + let new_descriptor = DescriptorTrait::from_contract_assert(new_event_address); if !self.is_namespace_registered(new_descriptor.namespace_hash()) { panic_with_byte_array( @@ -469,12 +475,33 @@ pub mod world { self.assert_caller_permissions(new_descriptor.selector(), Permission::Owner); - let mut prev_address = core::num::traits::Zero::::zero(); - // If the namespace or name of the event have been changed, the descriptor // will be different, hence not upgradeable. match self.resources.read(new_descriptor.selector()) { - Resource::Event((model_address, _)) => { prev_address = model_address; }, + Resource::Event(( + event_address, _ + )) => { + self.assert_resource_is_upgradeable(selector, event_address, new_event_address); + + IUpgradeableDispatcher { contract_address: new_event_address } + .upgrade(class_hash); + self + .resources + .write( + selector, + Resource::Event((new_event_address, new_descriptor.namespace_hash())) + ); + + self + .emit( + EventUpgraded { + selector, + class_hash, + address: new_event_address, + prev_address: event_address + } + ); + }, Resource::Unregistered => { panic_with_byte_array( @errors::event_not_registered( @@ -489,23 +516,6 @@ pub mod world { ) ) }; - - self - .resources - .write( - new_descriptor.selector(), - Resource::Event((new_contract_address, new_descriptor.namespace_hash())) - ); - - self - .emit( - EventUpgraded { - selector: new_descriptor.selector(), - prev_address, - address: new_contract_address, - class_hash, - } - ); } fn register_model(ref self: ContractState, class_hash: ClassHash) { @@ -552,17 +562,17 @@ pub mod world { ); } - fn upgrade_model(ref self: ContractState, class_hash: ClassHash) { - let salt = self.models_salt.read(); - - let (new_contract_address, _) = starknet::syscalls::deploy_syscall( - class_hash, salt.into(), [].span(), false, + fn upgrade_model(ref self: ContractState, selector: felt252, class_hash: ClassHash) { + // Using a library call is not safe as arbitrary code is executed. + // But deploying the contract we can check the new layout and schema of the model. + // If a new syscall supports calling library code with safety checks, we could switch + // back to using it. But for now, this is the safest option even if it's more expensive. + let (new_model_address, _) = starknet::syscalls::deploy_syscall( + class_hash, starknet::get_tx_info().unbox().transaction_hash, [].span(), false, ) .unwrap_syscall(); - self.models_salt.write(salt + 1); - - let new_descriptor = DescriptorTrait::from_contract_assert(new_contract_address); + let new_descriptor = DescriptorTrait::from_contract_assert(new_model_address); if !self.is_namespace_registered(new_descriptor.namespace_hash()) { panic_with_byte_array( @@ -572,12 +582,33 @@ pub mod world { self.assert_caller_permissions(new_descriptor.selector(), Permission::Owner); - let mut prev_address = core::num::traits::Zero::::zero(); - // If the namespace or name of the model have been changed, the descriptor // will be different, hence not upgradeable. match self.resources.read(new_descriptor.selector()) { - Resource::Model((model_address, _)) => { prev_address = model_address; }, + Resource::Model(( + model_address, _ + )) => { + self.assert_resource_is_upgradeable(selector, model_address, new_model_address); + + IUpgradeableDispatcher { contract_address: new_model_address } + .upgrade(class_hash); + self + .resources + .write( + selector, + Resource::Model((new_model_address, new_descriptor.namespace_hash())) + ); + + self + .emit( + ModelUpgraded { + selector, + class_hash, + address: new_model_address, + prev_address: model_address + } + ); + }, Resource::Unregistered => { panic_with_byte_array( @errors::model_not_registered( @@ -592,23 +623,6 @@ pub mod world { ) ) }; - - self - .resources - .write( - new_descriptor.selector(), - Resource::Model((new_contract_address, new_descriptor.namespace_hash())) - ); - - self - .emit( - ModelUpgraded { - selector: new_descriptor.selector(), - prev_address, - address: new_contract_address, - class_hash, - } - ); } fn register_namespace(ref self: ContractState, namespace: ByteArray) { @@ -706,6 +720,14 @@ pub mod world { ); IUpgradeableDispatcher { contract_address }.upgrade(class_hash); + + self + .resources + .write( + new_descriptor.selector(), + Resource::Contract((contract_address, new_descriptor.namespace_hash())) + ); + self.emit(ContractUpgraded { class_hash, selector: new_descriptor.selector() }); class_hash @@ -947,6 +969,52 @@ pub mod world { ) } + /// Panics if a model is not upgradable with the provided new class hash. + /// + /// Upgradable means: + /// - the version of the resource must be incremented by 1, + /// - the layout type must remain the same (Struct or Fixed), + /// - existing fields cannot be changed or moved inside the resource, + /// - new fields can only be appended at the end of the resource. + /// + /// # Arguments + /// * `resource_selector` - the selector of the resource. + /// * `resource_address` - the address of the current resource. + /// * `new_resource_address` - the address of the newly deployed resource. + /// + fn assert_resource_is_upgradeable( + self: @ContractState, + resource_selector: felt252, + resource_address: ContractAddress, + new_resource_address: ContractAddress + ) { + let resource = IResourceDispatcher { contract_address: resource_address }; + let old_layout = resource.layout(); + let old_schema = resource.schema(); + let old_version = resource.version(); + + let new_resource = IResourceDispatcher { contract_address: new_resource_address }; + let new_layout = new_resource.layout(); + let new_schema = new_resource.schema(); + let new_version = new_resource.version(); + + println!("old_version: {old_version} new_version: {new_version}"); + + if new_version != old_version + 1 { + panic_with_byte_array( + @errors::invalid_resource_version_upgrade(resource_selector, old_version + 1) + ) + } + + if !new_layout.is_same_type_of(@old_layout) { + panic_with_byte_array(@errors::invalid_resource_layout_upgrade(resource_selector)); + } + + if !new_schema.is_an_upgrade_of(@old_schema) { + panic_with_byte_array(@errors::invalid_resource_schema_upgrade(resource_selector)); + } + } + /// Indicates if the provided namespace is already registered /// /// # Arguments