From f083deb0069b901b3152de4faf3e1a7a3fe6d141 Mon Sep 17 00:00:00 2001 From: ptisserand Date: Tue, 10 Sep 2024 09:59:28 +0200 Subject: [PATCH] feat(contract): explore the possibility to use a fully onchain orderbook on (#439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description 1. Orderbook is now a component 2. Remove messaging in Starknet part 3. Add `version` and `order_type` in Orderbook component events 4. Move dependencies version to workspace Scarb.toml 5. Update to cairo 2.7.1, snforge 0.28 & OZ 0.15.1 ## What type of PR is this? (check all applicable) - [x] 🍕 Feature (`feat:`) - [ ] 🐛 Bug Fix (`fix:`) - [ ] 📝 Documentation Update (`docs:`) - [ ] 🎨 Style (`style:`) - [ ] 🧑‍💻 Code Refactor (`refactor:`) - [ ] 🔥 Performance Improvements (`perf:`) - [ ] ✅ Test (`test:`) - [ ] 🤖 Build (`build:`) - [ ] 🔁 CI (`ci:`) - [ ] 📦 Chore (`chore:`) - [ ] ⏩ Revert (`revert:`) - [ ] 🚀 Breaking Changes (`BREAKING CHANGE:`) ## Related Tickets & Documents ## Added tests? - [ ] 👍 yes - [ ] 🙅 no, because they aren't needed - [ ] 🙋 no, because I need help ## Added to documentation? - [ ] 📜 README.md - [ ] 📓 Documentation - [ ] 🙅 no documentation needed ## [optional] Are there any post-deployment tasks we need to perform? ## [optional] What gif best describes this PR or how it makes you feel? ### PR Title and Description Guidelines: - Ensure your PR title follows semantic versioning standards. This helps automate releases and changelogs. - Use types like `feat:`, `fix:`, `chore:`, `BREAKING CHANGE:` etc. in your PR title. - Your PR title will be used as a commit message when merging. Make sure it adheres to [Conventional Commits standards](https://www.conventionalcommits.org/). ## Closing Issues --- .github/workflows/arkproject-contracts.yml | 6 +- contracts/.tool-versions | 4 +- contracts/Scarb.lock | 107 ++- contracts/Scarb.toml | 18 +- contracts/ark_common/Scarb.toml | 9 +- contracts/ark_common/src/crypto/common.cairo | 3 +- .../ark_common/src/crypto/typed_data.cairo | 2 +- contracts/ark_common/src/lib.cairo | 6 +- .../src/protocol/order_database.cairo | 5 +- .../ark_common/src/protocol/order_types.cairo | 4 +- .../ark_common/src/protocol/order_v1.cairo | 10 +- .../ark_common/tests/test_type_data.cairo | 10 +- contracts/ark_component/Scarb.toml | 19 + contracts/ark_component/src/lib.cairo | 1 + contracts/ark_component/src/orderbook.cairo | 9 + .../src/orderbook/interface.cairo | 80 ++ .../src/orderbook/orderbook.cairo | 829 ++++++++++++++++++ contracts/ark_orderbook/Scarb.toml | 11 +- contracts/ark_orderbook/src/orderbook.cairo | 775 +++------------- .../src/orderbook_event_mock.cairo | 8 +- .../ark_orderbook/tests/common/setup.cairo | 13 +- contracts/ark_orderbook/tests/lib.cairo | 2 +- .../tests/unit/order/test_order_v1.cairo | 18 +- .../tests/unit/test_orderbook.cairo | 139 +-- contracts/ark_oz/Scarb.lock | 96 +- contracts/ark_oz/Scarb.toml | 12 +- contracts/ark_oz/src/erc2981/erc2981.cairo | 6 +- contracts/ark_oz/tests/test_erc2981.cairo | 48 +- contracts/ark_starknet/Scarb.toml | 14 +- .../ark_starknet/src/appchain_messaging.cairo | 7 +- contracts/ark_starknet/src/executor.cairo | 361 ++++---- contracts/ark_starknet/src/interfaces.cairo | 6 +- contracts/ark_starknet/src/lib.cairo | 2 +- .../ark_starknet/tests/common/setup.cairo | 30 +- .../tests/integration/create_order.cairo | 44 +- .../tests/integration/execute_order.cairo | 119 ++- .../tests/integration/fees_amount.cairo | 25 +- .../tests/integration/fulfill_order.cairo | 163 ++-- .../tests/integration/maintenance.cairo | 28 +- contracts/ark_starknet/tests/lib.cairo | 2 +- .../ark_starknet/tests/unit/test_fees.cairo | 21 +- contracts/ark_tokens/Scarb.toml | 11 +- contracts/ark_tokens/src/erc20.cairo | 3 +- contracts/ark_tokens/src/erc721.cairo | 11 +- contracts/ark_tokens/src/erc721_royalty.cairo | 16 +- contracts/ark_tokens/src/lib.cairo | 2 +- contracts/solis/Scarb.toml | 3 + 47 files changed, 1811 insertions(+), 1307 deletions(-) create mode 100644 contracts/ark_component/Scarb.toml create mode 100644 contracts/ark_component/src/lib.cairo create mode 100644 contracts/ark_component/src/orderbook.cairo create mode 100644 contracts/ark_component/src/orderbook/interface.cairo create mode 100644 contracts/ark_component/src/orderbook/orderbook.cairo diff --git a/.github/workflows/arkproject-contracts.yml b/.github/workflows/arkproject-contracts.yml index 99bf305e3..499894f3c 100644 --- a/.github/workflows/arkproject-contracts.yml +++ b/.github/workflows/arkproject-contracts.yml @@ -22,7 +22,7 @@ jobs: - name: Setup Scarb uses: software-mansion/setup-scarb@v1 with: - scarb-version: 2.5.4 + scarb-version: 2.7.1 - name: Check Scarb Formatting run: cd contracts && scarb fmt --check test: @@ -36,12 +36,12 @@ jobs: - name: Setup Scarb uses: software-mansion/setup-scarb@v1 with: - scarb-version: 2.5.4 + scarb-version: 2.7.1 - name: Setup Starknet Foundry uses: foundry-rs/setup-snfoundry@v2 with: - starknet-foundry-version: 0.18.0 + starknet-foundry-version: 0.28.0 - name: Test ark_common contracts run: cd contracts/ark_common && snforge test diff --git a/contracts/.tool-versions b/contracts/.tool-versions index 90faff46b..e0955bfc3 100644 --- a/contracts/.tool-versions +++ b/contracts/.tool-versions @@ -1,2 +1,2 @@ -scarb 2.5.4 -starknet-foundry 0.18.0 \ No newline at end of file +scarb 2.7.1 +starknet-foundry 0.28.0 \ No newline at end of file diff --git a/contracts/Scarb.lock b/contracts/Scarb.lock index c8865abf6..0c022abc2 100644 --- a/contracts/Scarb.lock +++ b/contracts/Scarb.lock @@ -8,11 +8,20 @@ dependencies = [ "snforge_std", ] +[[package]] +name = "ark_component" +version = "0.1.0" +dependencies = [ + "ark_common", + "snforge_std", +] + [[package]] name = "ark_orderbook" version = "0.1.0" dependencies = [ "ark_common", + "ark_component", "snforge_std", ] @@ -21,7 +30,6 @@ name = "ark_oz" version = "0.1.0" dependencies = [ "openzeppelin", - "snforge_std", ] [[package]] @@ -29,6 +37,7 @@ name = "ark_starknet" version = "0.1.0" dependencies = [ "ark_common", + "ark_component", "ark_oz", "ark_tokens", "openzeppelin", @@ -46,13 +55,101 @@ dependencies = [ [[package]] name = "openzeppelin" -version = "0.10.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.10.0#d77082732daab2690ba50742ea41080eb23299d3" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_governance", + "openzeppelin_introspection", + "openzeppelin_presets", + "openzeppelin_security", + "openzeppelin_token", + "openzeppelin_upgrades", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_access" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_account" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_governance" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" +dependencies = [ + "openzeppelin_access", + "openzeppelin_introspection", +] + +[[package]] +name = "openzeppelin_introspection" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" + +[[package]] +name = "openzeppelin_presets" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_introspection", + "openzeppelin_token", + "openzeppelin_upgrades", +] + +[[package]] +name = "openzeppelin_security" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" + +[[package]] +name = "openzeppelin_token" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" +dependencies = [ + "openzeppelin_account", + "openzeppelin_governance", + "openzeppelin_introspection", +] + +[[package]] +name = "openzeppelin_upgrades" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" + +[[package]] +name = "openzeppelin_utils" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" + +[[package]] +name = "snforge_scarb_plugin" +version = "0.1.0" +source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.28.0#4dfe39d96690ed6b3d56971512700de3f58288ea" [[package]] name = "snforge_std" -version = "0.18.0" -source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.18.0#48f909a56b08cbdc5ca6a21a836b0fbc6c36d55b" +version = "0.28.0" +source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.28.0#4dfe39d96690ed6b3d56971512700de3f58288ea" +dependencies = [ + "snforge_scarb_plugin", +] [[package]] name = "solis" diff --git a/contracts/Scarb.toml b/contracts/Scarb.toml index 83a37c9de..f9b906048 100644 --- a/contracts/Scarb.toml +++ b/contracts/Scarb.toml @@ -1,6 +1,22 @@ [workspace] -members = ["ark_common", "ark_orderbook", "ark_starknet", "ark_tokens", "solis"] +members = [ + "ark_common", + "ark_component", + "ark_orderbook", + "ark_starknet", + "ark_tokens", + "solis", +] + +[workspace.dependencies] +starknet = "2.7.1" +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.15.1" } +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.28.0" } +assert_macros = "0.1.0" [workspace.scripts] test = "snforge test" + +[workspace.tool.fmt] +sort-module-level-items = true diff --git a/contracts/ark_common/Scarb.toml b/contracts/ark_common/Scarb.toml index 3dca0b95c..d7fa37b83 100644 --- a/contracts/ark_common/Scarb.toml +++ b/contracts/ark_common/Scarb.toml @@ -5,8 +5,13 @@ version = "0.1.0" # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html [dependencies] -starknet = "2.5.4" -snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.18.0" } +starknet.workspace = true + +[dev-dependencies] +snforge_std.workspace = true + +[tool] +fmt.workspace = true [lib] diff --git a/contracts/ark_common/src/crypto/common.cairo b/contracts/ark_common/src/crypto/common.cairo index 8908a61a2..61cf5b5d3 100644 --- a/contracts/ark_common/src/crypto/common.cairo +++ b/contracts/ark_common/src/crypto/common.cairo @@ -1,7 +1,6 @@ -use traits::Into; - // locals use super::constants; +use traits::Into; fn hash_u256(n: u256) -> felt252 { let mut hash = pedersen::pedersen(0, constants::U256_TYPE_HASH); diff --git a/contracts/ark_common/src/crypto/typed_data.cairo b/contracts/ark_common/src/crypto/typed_data.cairo index 1d5389806..c5f24c3a6 100644 --- a/contracts/ark_common/src/crypto/typed_data.cairo +++ b/contracts/ark_common/src/crypto/typed_data.cairo @@ -1,6 +1,6 @@ -use traits::Into; use box::BoxTrait; use super::constants; +use traits::Into; const ORDER_TYPE_HASH: felt252 = 0x3749634CC837ADA5AF76B97FC3197F05DFC376CF4B73199E76107754E39CA1D; diff --git a/contracts/ark_common/src/lib.cairo b/contracts/ark_common/src/lib.cairo index 54eb951bc..8650199ab 100644 --- a/contracts/ark_common/src/lib.cairo +++ b/contracts/ark_common/src/lib.cairo @@ -1,13 +1,13 @@ mod protocol { + mod order_database; mod order_types; mod order_v1; - mod order_database; } mod crypto { + mod common; + mod constants; mod hash; mod signer; mod typed_data; - mod constants; - mod common; } diff --git a/contracts/ark_common/src/protocol/order_database.cairo b/contracts/ark_common/src/protocol/order_database.cairo index 03edefa30..9c71a485f 100644 --- a/contracts/ark_common/src/protocol/order_database.cairo +++ b/contracts/ark_common/src/protocol/order_database.cairo @@ -1,3 +1,5 @@ +use ark_common::protocol::order_types::OrderStatus; +use ark_common::protocol::order_types::OrderType; use core::result::ResultTrait; //! Orders database. //! @@ -20,9 +22,6 @@ use core::result::ResultTrait; //! 4. At base addresss + offset 2 => First felt of the serialized order. use starknet::SyscallResultTrait; -use ark_common::protocol::order_types::OrderStatus; -use ark_common::protocol::order_types::OrderType; - /// Must remain equal to 0 for now. const ADDRESS_DOMAIN: u32 = 0; /// A constant value used in the base key hash. diff --git a/contracts/ark_common/src/protocol/order_types.cairo b/contracts/ark_common/src/protocol/order_types.cairo index cd903c157..34331abc1 100644 --- a/contracts/ark_common/src/protocol/order_types.cairo +++ b/contracts/ark_common/src/protocol/order_types.cairo @@ -1,7 +1,7 @@ +use ark_common::protocol::order_v1::OrderV1; +use poseidon::poseidon_hash_span; //! Order generic variables. use starknet::ContractAddress; -use poseidon::poseidon_hash_span; -use ark_common::protocol::order_v1::OrderV1; /// Order types. #[derive(Serde, Drop, PartialEq, Copy, Debug, starknet::Store)] diff --git a/contracts/ark_common/src/protocol/order_v1.cairo b/contracts/ark_common/src/protocol/order_v1.cairo index f20bce4dd..4fd202216 100644 --- a/contracts/ark_common/src/protocol/order_v1.cairo +++ b/contracts/ark_common/src/protocol/order_v1.cairo @@ -1,16 +1,16 @@ +use ark_common::protocol::order_types::FulfillInfo; +use ark_common::protocol::order_types::{OrderTrait, OrderValidationError, OrderType, RouteType}; use core::array::ArrayTrait; +use core::option::OptionTrait; use core::traits::Into; use core::traits::TryInto; -use core::option::OptionTrait; +use poseidon::poseidon_hash_span; //! Order v1 supported by the Orderbook. //! use starknet::ContractAddress; -use starknet::contract_address_to_felt252; -use ark_common::protocol::order_types::{OrderTrait, OrderValidationError, OrderType, RouteType}; -use ark_common::protocol::order_types::FulfillInfo; -use poseidon::poseidon_hash_span; use starknet::SyscallResultTrait; +use starknet::contract_address_to_felt252; const ORDER_VERSION_V1: felt252 = 'v1'; // Auction -> end_amount (reserve price) > start_amount (starting price). diff --git a/contracts/ark_common/tests/test_type_data.cairo b/contracts/ark_common/tests/test_type_data.cairo index 7bacb6563..b4b8116b1 100644 --- a/contracts/ark_common/tests/test_type_data.cairo +++ b/contracts/ark_common/tests/test_type_data.cairo @@ -1,19 +1,19 @@ -use core::traits::TryInto; -use traits::Into; -use box::BoxTrait; +use ark_common::crypto::signer::{Signer, SignInfo}; use ark_common::crypto::typed_data::{OrderSign, TypedDataTrait}; +use box::BoxTrait; use core::option::OptionTrait; -use ark_common::crypto::signer::{Signer, SignInfo}; +use core::traits::TryInto; use snforge_std::signature::KeyPairTrait; use snforge_std::signature::stark_curve::{ StarkCurveKeyPairImpl, StarkCurveSignerImpl, StarkCurveVerifierImpl }; +use traits::Into; fn sign_mock(message_hash: felt252) -> Signer { let key_pair = KeyPairTrait::::generate(); - let (r, s): (felt252, felt252) = key_pair.sign(message_hash); + let (r, s): (felt252, felt252) = key_pair.sign(message_hash).unwrap(); Signer::WEIERSTRESS_STARKNET( SignInfo { user_pubkey: key_pair.public_key, user_sig_r: r, user_sig_s: s, } ) diff --git a/contracts/ark_component/Scarb.toml b/contracts/ark_component/Scarb.toml new file mode 100644 index 000000000..4f922c2c0 --- /dev/null +++ b/contracts/ark_component/Scarb.toml @@ -0,0 +1,19 @@ +[package] +name = "ark_component" +version = "0.1.0" + +[dependencies] +ark_common = { path = "../ark_common" } +starknet.workspace = true + +[dev-dependencies] +snforge_std.workspace = true +assert_macros.workspace = true + +[scripts] +test.workspace = true + +[tool] +fmt.workspace = true + +[lib] diff --git a/contracts/ark_component/src/lib.cairo b/contracts/ark_component/src/lib.cairo new file mode 100644 index 000000000..959ef2db9 --- /dev/null +++ b/contracts/ark_component/src/lib.cairo @@ -0,0 +1 @@ +mod orderbook; diff --git a/contracts/ark_component/src/orderbook.cairo b/contracts/ark_component/src/orderbook.cairo new file mode 100644 index 000000000..191215137 --- /dev/null +++ b/contracts/ark_component/src/orderbook.cairo @@ -0,0 +1,9 @@ +pub mod interface; +pub mod orderbook; +pub use interface::{IOrderbook, IOrderbookAction, orderbook_errors}; + +pub use orderbook::OrderbookComponent; +pub use orderbook::{ + OrderbookHooksCreateOrderEmptyImpl, OrderbookHooksCancelOrderEmptyImpl, + OrderbookHooksFulfillOrderEmptyImpl, OrderbookHooksValidateOrderExecutionEmptyImpl, +}; diff --git a/contracts/ark_component/src/orderbook/interface.cairo b/contracts/ark_component/src/orderbook/interface.cairo new file mode 100644 index 000000000..b8d6fa501 --- /dev/null +++ b/contracts/ark_component/src/orderbook/interface.cairo @@ -0,0 +1,80 @@ +use ark_common::protocol::order_types::{ + CancelInfo, ExecutionInfo, ExecutionValidationInfo, FulfillInfo, OrderStatus, OrderType +}; +use ark_common::protocol::order_v1::OrderV1; + +use starknet::ContractAddress; + +// ************************************************************************* +// ERRORS +// +// Error messages used within the orderbook contract. +// ************************************************************************* +pub mod orderbook_errors { + const BROKER_UNREGISTERED: felt252 = 'OB: unregistered broker'; + const ORDER_INVALID_DATA: felt252 = 'OB: order invalid data'; + const ORDER_ALREADY_EXISTS: felt252 = 'OB: order already exists'; + const ORDER_ALREADY_EXEC: felt252 = 'OB: order already executed'; + const ORDER_NOT_FULFILLABLE: felt252 = 'OB: order not fulfillable'; + const ORDER_NOT_FOUND: felt252 = 'OB: order not found'; + const ORDER_FULFILLED: felt252 = 'OB: order fulfilled'; + const ORDER_NOT_CANCELLABLE: felt252 = 'OB: order not cancellable'; + const ORDER_EXPIRED: felt252 = 'OB: order expired'; + const ORDER_SAME_OFFERER: felt252 = 'OB: order has same offerer'; + const ORDER_NOT_SAME_OFFERER: felt252 = 'OB: fulfiller is not offerer'; + const OFFER_ALREADY_EXISTS: felt252 = 'OB: offer already exists'; + const ORDER_IS_EXPIRED: felt252 = 'OB: order is expired'; + const ORDER_AUCTION_IS_EXPIRED: felt252 = 'OB: auction is expired'; + const ORDER_MISSING_RELATED_ORDER: felt252 = 'OB: order missing related order'; + const ORDER_HASH_DOES_NOT_MATCH: felt252 = 'OB: order hash does not match'; + const ORDER_TOKEN_ID_DOES_NOT_MATCH: felt252 = 'OB: token id does not match'; + const ORDER_TOKEN_ID_IS_MISSING: felt252 = 'OB: token id is missing'; + const ORDER_TOKEN_HASH_DOES_NOT_MATCH: felt252 = 'OB: token hash does not match'; + const ORDER_NOT_AN_OFFER: felt252 = 'OB: order is not an offer'; + const ORDER_NOT_OPEN: felt252 = 'OB: order is not open'; + const ORDER_OPEN: felt252 = 'OB: order is not open'; + const USE_FULFILL_AUCTION: felt252 = 'OB: must use fulfill auction'; + const OFFER_NOT_STARTED: felt252 = 'OB: offer is not started'; + const INVALID_BROKER: felt252 = 'OB: broker is not whitelisted'; +} + +#[starknet::interface] +pub trait IOrderbook { + /// Retrieves the type of an order using its hash. + /// + /// # Arguments + /// * `order_hash` - The order hash of order. + fn get_order_type(self: @T, order_hash: felt252) -> OrderType; + + /// Retrieves the status of an order using its hash. + /// + /// # Arguments + /// * `order_hash` - The order hash of order. + fn get_order_status(self: @T, order_hash: felt252) -> OrderStatus; + + /// Retrieves the auction end date. + /// + /// # Arguments + /// * `order_hash` - The order hash of order. + fn get_auction_expiration(self: @T, order_hash: felt252) -> u64; + + /// Retrieves the order using its hash. + /// + /// # Arguments + /// * `order_hash` - The order hash of order. + fn get_order(self: @T, order_hash: felt252) -> OrderV1; + + /// Retrieves the order hash using its token hash. + /// + /// # Arguments + /// * `token_hash` - The token hash of the order. + fn get_order_hash(self: @T, token_hash: felt252) -> felt252; +} + +pub trait IOrderbookAction { + fn validate_order_execution(ref self: T, info: ExecutionValidationInfo); + + fn create_order(ref self: T, order: OrderV1); + fn cancel_order(ref self: T, cancel_info: CancelInfo); + fn fulfill_order(ref self: T, fulfill_info: FulfillInfo) -> Option::; +} diff --git a/contracts/ark_component/src/orderbook/orderbook.cairo b/contracts/ark_component/src/orderbook/orderbook.cairo new file mode 100644 index 000000000..c26a133a6 --- /dev/null +++ b/contracts/ark_component/src/orderbook/orderbook.cairo @@ -0,0 +1,829 @@ +#[starknet::component] +pub mod OrderbookComponent { + use ark_common::protocol::order_database::{ + order_read, order_status_read, order_write, order_status_write, order_type_read + }; + use ark_common::protocol::order_types::{ + OrderStatus, OrderTrait, OrderType, CancelInfo, FulfillInfo, ExecutionValidationInfo, + ExecutionInfo, RouteType + }; + use ark_common::protocol::order_v1::OrderV1; + use core::debug::PrintTrait; + use core::option::OptionTrait; + use core::result::ResultTrait; + use core::starknet::event::EventEmitter; + use core::traits::Into; + use core::traits::TryInto; + use core::zeroable::Zeroable; + use starknet::ContractAddress; + use starknet::storage::Map; + use super::super::interface::{IOrderbook, IOrderbookAction, orderbook_errors}; + + const EXTENSION_TIME_IN_SECONDS: u64 = 600; + const AUCTION_ACCEPTING_TIME_SECS: u64 = 172800; + /// Storage struct for the Orderbook component. + #[storage] + struct Storage { + /// Mapping of broker addresses to their whitelisted status. + /// Represented as felt252, set to 1 if the broker is registered. + brokers: Map, + /// Mapping of token_hash to order_hash. + token_listings: Map, + /// Mapping of token_hash to auction details (order_hash and end_date, auction_offer_count). + auctions: Map, + /// Mapping of auction offer order_hash to auction listing order_hash. + auction_offers: Map, + } + + // ************************************************************************* + // EVENTS + // ************************************************************************* + + /// Events emitted by the Orderbook contract. + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + OrderPlaced: OrderPlaced, + OrderExecuted: OrderExecuted, + OrderCancelled: OrderCancelled, + RollbackStatus: RollbackStatus, + OrderFulfilled: OrderFulfilled, + } + + // must be increased when `OrderPlaced` content change + const ORDER_PLACED_EVENT_VERSION: u8 = 1; + /// Event for when an order is placed. + #[derive(Drop, starknet::Event)] + struct OrderPlaced { + #[key] + order_hash: felt252, + #[key] + order_version: felt252, + #[key] + order_type: OrderType, + /// + version: u8, + // The order that was cancelled by this order. + cancelled_order_hash: Option, + // The full order serialized. + order: OrderV1, + } + + // must be increased when `OrderExecuted` content change + const ORDER_EXECUTED_EVENT_VERSION: u8 = 2; + /// Event for when an order is executed. + #[derive(Drop, starknet::Event)] + struct OrderExecuted { + #[key] + order_hash: felt252, + #[key] + order_status: OrderStatus, + #[key] + order_type: OrderType, + /// + version: u8, + transaction_hash: felt252, + from: ContractAddress, + to: ContractAddress, + } + + // must be increased when `OrderPlaced` content change + const ORDER_CANCELLED_EVENT_VERSION: u8 = 1; + /// Event for when an order is cancelled. + #[derive(Drop, starknet::Event)] + struct OrderCancelled { + #[key] + order_hash: felt252, + #[key] + reason: felt252, + #[key] + order_type: OrderType, + version: u8, + } + + // must be increased when `RollbackStatus` content change + const ROLLBACK_STATUS_EVENT_VERSION: u8 = 1; + /// Event for when an order has been rollbacked to placed. + #[derive(Drop, starknet::Event)] + struct RollbackStatus { + #[key] + order_hash: felt252, + #[key] + reason: felt252, + #[key] + order_type: OrderType, + /// + version: u8, + } + + // must be increased when `OrderFulfilled` content change + const ORDER_FULFILLED_EVENT_VERSION: u8 = 1; + /// Event for when an order is fulfilled. + #[derive(Drop, starknet::Event)] + struct OrderFulfilled { + #[key] + order_hash: felt252, + #[key] + fulfiller: ContractAddress, + #[key] + related_order_hash: Option, + #[key] + order_type: OrderType, + /// + version: u8, + } + + pub trait OrderbookHooksCreateOrderTrait { + fn before_create_order(ref self: ComponentState, order: OrderV1) {} + fn after_create_order(ref self: ComponentState, order: OrderV1) {} + } + + pub trait OrderbookHooksCancelOrderTrait { + fn before_cancel_order(ref self: ComponentState, cancel_info: CancelInfo) {} + fn after_cancel_order(ref self: ComponentState, cancel_info: CancelInfo) {} + } + + pub trait OrderbookHooksFulfillOrderTrait { + fn before_fulfill_order( + ref self: ComponentState, fulfill_info: FulfillInfo + ) {} + fn after_fulfill_order( + ref self: ComponentState, fulfill_info: FulfillInfo + ) {} + } + + pub trait OrderbookHooksValidateOrderExecutionTrait { + fn before_validate_order_execution( + ref self: ComponentState, info: ExecutionValidationInfo + ) {} + fn after_validate_order_execution( + ref self: ComponentState, info: ExecutionValidationInfo + ) {} + } + + #[embeddable_as(OrderbookImpl)] + pub impl Orderbook< + TContractState, +HasComponent, +Drop, + > of IOrderbook> { + /// Retrieves the type of an order using its hash. + /// # View + fn get_order_type(self: @ComponentState, order_hash: felt252) -> OrderType { + let order_type_option = order_type_read(order_hash); + if order_type_option.is_none() { + panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND); + } + order_type_option.unwrap().into() + } + + /// Retrieves the status of an order using its hash. + /// # View + fn get_order_status( + self: @ComponentState, order_hash: felt252 + ) -> OrderStatus { + let status = order_status_read(order_hash); + if status.is_none() { + panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND); + } + status.unwrap().into() + } + + /// Retrieves the auction end date + /// # View + fn get_auction_expiration( + self: @ComponentState, order_hash: felt252 + ) -> u64 { + let order = order_read::(order_hash); + if (order.is_none()) { + panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND); + } + let token_hash = order.unwrap().compute_token_hash(); + let (_, auction_end_date, _b) = self.auctions.read(token_hash); + auction_end_date + } + + /// Retrieves the order using its hash. + /// # View + fn get_order(self: @ComponentState, order_hash: felt252) -> OrderV1 { + let order = order_read(order_hash); + if (order.is_none()) { + panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND); + } + order.unwrap() + } + + /// Retrieves the order hash using its token hash. + /// # View + fn get_order_hash(self: @ComponentState, token_hash: felt252) -> felt252 { + let order_hash = self.token_listings.read(token_hash); + if (order_hash.is_zero()) { + panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND); + } + order_hash + } + } + + pub impl OrderbookActionImpl< + TContractState, + +HasComponent, + impl HooksCreateOrder: OrderbookHooksCreateOrderTrait, + impl HooksCancelOrder: OrderbookHooksCancelOrderTrait, + impl HooksFulfillOrder: OrderbookHooksFulfillOrderTrait, + impl HooksValidateOrder: OrderbookHooksValidateOrderExecutionTrait, + > of IOrderbookAction> { + fn validate_order_execution( + ref self: ComponentState, info: ExecutionValidationInfo + ) { + HooksValidateOrder::before_validate_order_execution(ref self, info); + order_status_write(info.order_hash, OrderStatus::Executed); + let order_status = order_status_read(info.order_hash).unwrap(); + let order_type = order_type_read(info.order_hash).unwrap(); + self + .emit( + OrderExecuted { + order_hash: info.order_hash, + order_status, + order_type, + transaction_hash: info.transaction_hash, + from: info.from, + to: info.to, + version: ORDER_EXECUTED_EVENT_VERSION, + } + ); + + HooksValidateOrder::after_validate_order_execution(ref self, info); + } + + /// Submits and places an order to the orderbook if the order is valid. + fn create_order(ref self: ComponentState, order: OrderV1) { + HooksCreateOrder::before_create_order(ref self, order); + let block_ts = starknet::get_block_timestamp(); + let validation = order.validate_common_data(block_ts); + if validation.is_err() { + panic_with_felt252(validation.unwrap_err().into()); + } + let order_type = order + .validate_order_type() + .expect(orderbook_errors::ORDER_INVALID_DATA); + let order_hash = order.compute_order_hash(); + match order_type { + OrderType::Listing => { + assert( + order_status_read(order_hash).is_none(), + orderbook_errors::ORDER_ALREADY_EXISTS + ); + let _ = self._create_listing_order(order, order_type, order_hash); + }, + OrderType::Auction => { + assert( + order_status_read(order_hash).is_none(), + orderbook_errors::ORDER_ALREADY_EXISTS + ); + self._create_auction(order, order_type, order_hash); + }, + OrderType::Offer => { self._create_offer(order, order_type, order_hash); }, + OrderType::CollectionOffer => { + self._create_collection_offer(order, order_type, order_hash); + }, + }; + + HooksCreateOrder::after_create_order(ref self, order); + } + + fn cancel_order(ref self: ComponentState, cancel_info: CancelInfo) { + HooksCancelOrder::before_cancel_order(ref self, cancel_info); + + let order_hash = cancel_info.order_hash; + let order_option = order_read::(order_hash); + assert(order_option.is_some(), orderbook_errors::ORDER_NOT_FOUND); + let order = order_option.unwrap(); + assert(order.offerer == cancel_info.canceller, 'not the same offerrer'); + match order_status_read(order_hash) { + Option::Some(s) => s, + Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), + }; + let block_ts = starknet::get_block_timestamp(); + let order_type = match order_type_read(order_hash) { + Option::Some(order_type) => { + if order_type == OrderType::Auction { + let auction_token_hash = order.compute_token_hash(); + let (_, auction_end_date, _) = self.auctions.read(auction_token_hash); + assert( + block_ts <= auction_end_date, orderbook_errors::ORDER_AUCTION_IS_EXPIRED + ); + self.auctions.write(auction_token_hash, (0, 0, 0)); + } else { + assert(block_ts < order.end_date, orderbook_errors::ORDER_IS_EXPIRED); + if order_type == OrderType::Listing { + self.token_listings.write(order.compute_token_hash(), 0); + } + } + order_type + }, + Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), + }; + + // Cancel order + order_status_write(order_hash, OrderStatus::CancelledUser); + self + .emit( + OrderCancelled { + order_hash, + reason: OrderStatus::CancelledUser.into(), + order_type, + version: ORDER_CANCELLED_EVENT_VERSION, + } + ); + + HooksCancelOrder::after_cancel_order(ref self, cancel_info); + } + + fn fulfill_order( + ref self: ComponentState, fulfill_info: FulfillInfo + ) -> Option:: { + HooksFulfillOrder::before_fulfill_order(ref self, fulfill_info); + + let order_hash = fulfill_info.order_hash; + let order: OrderV1 = match order_read(order_hash) { + Option::Some(o) => o, + Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), + }; + let status = match order_status_read(order_hash) { + Option::Some(s) => s, + Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), + }; + assert(status == OrderStatus::Open, orderbook_errors::ORDER_NOT_FULFILLABLE); + let order_type = match order_type_read(order_hash) { + Option::Some(s) => s, + Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), + }; + let (execution_info, related_order_hash) = match order_type { + OrderType::Listing => self._fulfill_listing_order(fulfill_info, order), + OrderType::Auction => self._fulfill_auction_order(fulfill_info, order), + OrderType::Offer => self._fulfill_offer(fulfill_info, order), + OrderType::CollectionOffer => self._fulfill_offer(fulfill_info, order), + }; + + self + .emit( + OrderFulfilled { + order_hash: fulfill_info.order_hash, + fulfiller: fulfill_info.fulfiller, + related_order_hash, + order_type, + version: ORDER_FULFILLED_EVENT_VERSION, + } + ); + + HooksFulfillOrder::after_fulfill_order(ref self, fulfill_info); + execution_info + } + } + + // ************************************************************************* + // INTERNAL FUNCTIONS + // ************************************************************************* + #[generate_trait] + pub impl InternalImpl< + TContractState, +HasComponent + > of InternalTrait { + /// Fulfill auction order + /// + /// # Arguments + /// * `fulfill_info` - The execution info of the order. + /// * `order_type` - The type of the order. + /// + fn _fulfill_auction_order( + ref self: ComponentState, fulfill_info: FulfillInfo, order: OrderV1 + ) -> (Option, Option) { + let block_timestamp = starknet::get_block_timestamp(); + assert( + order.offerer == fulfill_info.fulfiller, orderbook_errors::ORDER_NOT_SAME_OFFERER + ); + // get auction end date from storage + let (_, end_date, _) = self.auctions.read(order.compute_token_hash()); + assert( + end_date + AUCTION_ACCEPTING_TIME_SECS > block_timestamp, + orderbook_errors::ORDER_EXPIRED + ); + + let related_order_hash = fulfill_info + .related_order_hash + .expect(orderbook_errors::ORDER_MISSING_RELATED_ORDER); + + match order_type_read(related_order_hash) { + Option::Some(order_type) => { + assert( + order_type == OrderType::Offer || order_type == OrderType::CollectionOffer, + orderbook_errors::ORDER_NOT_AN_OFFER + ); + }, + Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), + } + + match order_status_read(related_order_hash) { + Option::Some(s) => { + assert(s == OrderStatus::Open, orderbook_errors::ORDER_NOT_OPEN); + s + }, + Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), + }; + + let related_order = match order_read::(related_order_hash) { + Option::Some(o) => o, + Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), + }; + + let related_offer_auction = self.auction_offers.read(related_order_hash); + + if related_offer_auction.is_non_zero() { + assert( + related_offer_auction == fulfill_info.order_hash, + orderbook_errors::ORDER_HASH_DOES_NOT_MATCH + ); + } else { + assert(related_order.end_date > block_timestamp, orderbook_errors::ORDER_EXPIRED); + } + let related_order_token_hash = related_order.compute_token_hash(); + assert( + related_order_token_hash == order.compute_token_hash(), + orderbook_errors::ORDER_TOKEN_HASH_DOES_NOT_MATCH + ); + assert( + related_order.token_id == order.token_id, + orderbook_errors::ORDER_TOKEN_ID_DOES_NOT_MATCH + ); + + order_status_write(related_order_hash, OrderStatus::Fulfilled); + order_status_write(fulfill_info.order_hash, OrderStatus::Fulfilled); + + if order.token_id.is_some() { + let execute_info = ExecutionInfo { + order_hash: order.compute_order_hash(), + nft_address: order.token_address, + nft_from: order.offerer, + nft_to: related_order.offerer, + nft_token_id: order.token_id.unwrap(), + payment_from: related_order.offerer, + payment_to: fulfill_info.fulfiller, + payment_amount: related_order.start_amount, + payment_currency_address: related_order.currency_address, + payment_currency_chain_id: related_order.currency_chain_id, + listing_broker_address: order.broker_id, + fulfill_broker_address: fulfill_info.fulfill_broker_address + }; + (Option::Some(execute_info), Option::Some(related_order_hash)) + } else { + (Option::None, Option::Some(related_order_hash)) + } + } + + /// Fulfill offer order + /// + /// # Arguments + /// * `fulfill_info` - The execution info of the order. + /// * `order` - The order. + /// + fn _fulfill_offer( + ref self: ComponentState, fulfill_info: FulfillInfo, order: OrderV1 + ) -> (Option, Option) { + if order.token_id.is_some() { + let (auction_order_hash, _, _) = self.auctions.read(order.compute_token_hash()); + + assert(auction_order_hash.is_zero(), orderbook_errors::USE_FULFILL_AUCTION); + } + + assert(fulfill_info.token_id.is_some(), orderbook_errors::ORDER_TOKEN_ID_IS_MISSING); + + let current_date = starknet::get_block_timestamp(); + assert(order.end_date > current_date, orderbook_errors::ORDER_EXPIRED); + order_status_write(fulfill_info.order_hash, OrderStatus::Fulfilled); + + if order.token_id.is_some() { + // remove token from listed tokens + self.token_listings.write(order.compute_token_hash(), 0); + } + + let execute_info = ExecutionInfo { + order_hash: order.compute_order_hash(), + nft_address: order.token_address, + nft_from: fulfill_info.fulfiller, + nft_to: order.offerer, + nft_token_id: fulfill_info.token_id.unwrap(), + payment_from: order.offerer, + payment_to: fulfill_info.fulfiller, + payment_amount: order.start_amount, + payment_currency_address: order.currency_address, + payment_currency_chain_id: order.currency_chain_id, + listing_broker_address: order.broker_id, + fulfill_broker_address: fulfill_info.fulfill_broker_address + }; + (Option::Some(execute_info), Option::None) + } + + /// Fulfill listing order + /// + /// # Arguments + /// * `fulfill_info` - The execution info of the order. + /// * `order_type` - The type of the order. + /// + fn _fulfill_listing_order( + ref self: ComponentState, fulfill_info: FulfillInfo, order: OrderV1 + ) -> (Option, Option) { + assert(order.offerer != fulfill_info.fulfiller, orderbook_errors::ORDER_SAME_OFFERER); + assert( + order.end_date > starknet::get_block_timestamp(), orderbook_errors::ORDER_EXPIRED + ); + order_status_write(fulfill_info.order_hash, OrderStatus::Fulfilled); + + if order.token_id.is_some() { + let execute_info = ExecutionInfo { + order_hash: order.compute_order_hash(), + nft_address: order.token_address, + nft_from: order.offerer, + nft_to: fulfill_info.fulfiller, + nft_token_id: order.token_id.unwrap(), + payment_from: fulfill_info.fulfiller, + payment_to: order.offerer, + payment_amount: order.start_amount, + payment_currency_address: order.currency_address, + payment_currency_chain_id: order.currency_chain_id, + listing_broker_address: order.broker_id, + fulfill_broker_address: fulfill_info.fulfill_broker_address + }; + (Option::Some(execute_info), Option::None) + } else { + (Option::None, Option::None) + } + } + + /// Get order hash from token hash + /// + /// # Arguments + /// * `token_hash` - The token hash of the order. + /// + fn _get_order_hash_from_token_hash( + self: @ComponentState, token_hash: felt252 + ) -> felt252 { + self.token_listings.read(token_hash) + } + + /// get previous order + /// + /// # Arguments + /// * `token_hash` - The token hash of the order. + /// + /// # Return option of (order hash: felt252, is_order_expired: bool, order: OrderV1) + /// * order_hash + /// * is_order_expired + /// * order + fn _get_previous_order( + self: @ComponentState, token_hash: felt252 + ) -> Option<(felt252, bool, OrderV1)> { + let previous_listing_orderhash = self.token_listings.read(token_hash); + let (previous_auction_orderhash, _, _) = self.auctions.read(token_hash); + let mut previous_orderhash = 0; + if (previous_listing_orderhash.is_non_zero()) { + previous_orderhash = previous_listing_orderhash; + let previous_order: Option = order_read(previous_orderhash); + assert(previous_order.is_some(), 'Order must exist'); + let previous_order = previous_order.unwrap(); + return Option::Some( + ( + previous_orderhash, + previous_order.end_date <= starknet::get_block_timestamp(), + previous_order + ) + ); + } + if (previous_auction_orderhash.is_non_zero()) { + previous_orderhash = previous_auction_orderhash; + let current_order: Option = order_read(previous_orderhash); + assert(current_order.is_some(), 'Order must exist'); + let current_order = current_order.unwrap(); + let (_, auction_end_date, _) = self.auctions.read(token_hash); + return Option::Some( + ( + previous_orderhash, + auction_end_date <= starknet::get_block_timestamp(), + current_order + ) + ); + } else { + return Option::None; + } + } + + /// Process previous order + /// + /// # Arguments + /// * `token_hash` - The token hash of the order. + /// + fn _process_previous_order( + ref self: ComponentState, token_hash: felt252, offerer: ContractAddress + ) -> Option { + let previous_order = self._get_previous_order(token_hash); + if (previous_order.is_some()) { + let (previous_orderhash, previous_order_is_expired, previous_order) = previous_order + .unwrap(); + let previous_order_status = order_status_read(previous_orderhash) + .expect('Invalid Order status'); + assert( + previous_order_status != OrderStatus::Fulfilled, + orderbook_errors::ORDER_FULFILLED + ); + if (previous_order.offerer == offerer) { + assert(previous_order_is_expired, orderbook_errors::ORDER_NOT_CANCELLABLE); + } + order_status_write(previous_orderhash, OrderStatus::CancelledByNewOrder); + return Option::Some(previous_orderhash); + } + return Option::None; + } + + /// Creates a listing order. + fn _create_listing_order( + ref self: ComponentState, + order: OrderV1, + order_type: OrderType, + order_hash: felt252, + ) -> Option { + let token_hash = order.compute_token_hash(); + // revert if order is fulfilled or Open + let current_order_hash = self.token_listings.read(token_hash); + if (current_order_hash.is_non_zero()) { + assert( + order_status_read(current_order_hash) != Option::Some(OrderStatus::Fulfilled), + orderbook_errors::ORDER_FULFILLED + ); + } + let current_order: Option = order_read(current_order_hash); + if (current_order.is_some()) { + let current_order = current_order.unwrap(); + // check if same offerer + if (current_order.offerer == order.offerer) { + // check expiration if order is expired continue + assert( + current_order.end_date <= starknet::get_block_timestamp(), + orderbook_errors::ORDER_ALREADY_EXISTS + ); + } + } + + let cancelled_order_hash = self._process_previous_order(token_hash, order.offerer); + order_write(order_hash, order_type, order); + self.token_listings.write(token_hash, order_hash); + self + .emit( + OrderPlaced { + order_hash: order_hash, + order_version: order.get_version(), + order_type: order_type, + version: ORDER_PLACED_EVENT_VERSION, + cancelled_order_hash, + order: order + } + ); + cancelled_order_hash + } + + /// Creates an auction order. + fn _create_auction( + ref self: ComponentState, + order: OrderV1, + order_type: OrderType, + order_hash: felt252 + ) { + let token_hash = order.compute_token_hash(); + let current_order_hash = self.token_listings.read(token_hash); + if (current_order_hash.is_non_zero()) { + assert( + order_status_read(current_order_hash) != Option::Some(OrderStatus::Fulfilled), + orderbook_errors::ORDER_FULFILLED + ); + } + let current_order: Option = order_read(current_order_hash); + if (current_order.is_some()) { + let current_order = current_order.unwrap(); + // check expiration if order is expired continue + if (current_order.offerer == order.offerer) { + assert( + current_order.end_date <= starknet::get_block_timestamp(), + orderbook_errors::ORDER_ALREADY_EXISTS + ); + } + } + let token_hash = order.compute_token_hash(); + let cancelled_order_hash = self._process_previous_order(token_hash, order.offerer); + order_write(order_hash, order_type, order); + self.auctions.write(token_hash, (order_hash, order.end_date, 0)); + self + .emit( + OrderPlaced { + order_hash: order_hash, + order_version: order.get_version(), + order_type: order_type, + version: ORDER_PLACED_EVENT_VERSION, + cancelled_order_hash, + order: order, + } + ); + } + + fn _manage_auction_offer( + ref self: ComponentState, order: OrderV1, order_hash: felt252 + ) { + let token_hash = order.compute_token_hash(); + let (auction_order_hash, auction_end_date, auction_offer_count) = self + .auctions + .read(token_hash); + + let current_block_timestamp = starknet::get_block_timestamp(); + // Determine if the auction end date has passed, indicating that the auction is still + // ongoing. + let auction_is_pending = current_block_timestamp < auction_end_date; + + if auction_is_pending { + // If the auction is still pending, record the new offer by linking it to the + // auction order hash in the 'auction_offers' mapping. + self.auction_offers.write(order_hash, auction_order_hash); + + if auction_end_date - current_block_timestamp < EXTENSION_TIME_IN_SECONDS { + // Increment the number of offers for this auction and extend the auction + // end date by the predefined extension time to allow for additional offers. + self + .auctions + .write( + token_hash, + ( + auction_order_hash, + auction_end_date + EXTENSION_TIME_IN_SECONDS, + auction_offer_count + 1 + ) + ); + } else { + self + .auctions + .write( + token_hash, + (auction_order_hash, auction_end_date, auction_offer_count + 1) + ); + } + } + } + + /// Creates an offer order. + fn _create_offer( + ref self: ComponentState, + order: OrderV1, + order_type: OrderType, + order_hash: felt252 + ) { + self._manage_auction_offer(order, order_hash); + order_write(order_hash, order_type, order); + self + .emit( + OrderPlaced { + order_hash: order_hash, + order_version: order.get_version(), + order_type: order_type, + version: ORDER_PLACED_EVENT_VERSION, + cancelled_order_hash: Option::None, + order: order, + } + ); + } + + /// Creates a collection offer order. + fn _create_collection_offer( + ref self: ComponentState, + order: OrderV1, + order_type: OrderType, + order_hash: felt252 + ) { + order_write(order_hash, order_type, order); + self + .emit( + OrderPlaced { + order_hash: order_hash, + order_version: order.get_version(), + order_type, + version: ORDER_PLACED_EVENT_VERSION, + cancelled_order_hash: Option::None, + order: order, + } + ); + } + } +} +pub impl OrderbookHooksCreateOrderEmptyImpl< + TContractState +> of OrderbookComponent::OrderbookHooksCreateOrderTrait {} +pub impl OrderbookHooksCancelOrderEmptyImpl< + TContractState +> of OrderbookComponent::OrderbookHooksCancelOrderTrait {} +pub impl OrderbookHooksFulfillOrderEmptyImpl< + TContractState +> of OrderbookComponent::OrderbookHooksFulfillOrderTrait {} +pub impl OrderbookHooksValidateOrderExecutionEmptyImpl< + TContractState +> of OrderbookComponent::OrderbookHooksValidateOrderExecutionTrait {} diff --git a/contracts/ark_orderbook/Scarb.toml b/contracts/ark_orderbook/Scarb.toml index bfa59fd0d..3e2680078 100644 --- a/contracts/ark_orderbook/Scarb.toml +++ b/contracts/ark_orderbook/Scarb.toml @@ -4,12 +4,19 @@ version = "0.1.0" [dependencies] ark_common = { path = "../ark_common" } -starknet = "2.5.4" -snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.18.0" } +ark_component = { path = "../ark_component" } +starknet.workspace = true + +[dev-dependencies] +snforge_std.workspace = true +assert_macros.workspace = true [scripts] test.workspace = true +[tool] +fmt.workspace = true + [[target.starknet-contract]] sierra = true casm = true diff --git a/contracts/ark_orderbook/src/orderbook.cairo b/contracts/ark_orderbook/src/orderbook.cairo index 3db6318de..de68329e5 100644 --- a/contracts/ark_orderbook/src/orderbook.cairo +++ b/contracts/ark_orderbook/src/orderbook.cairo @@ -1,48 +1,17 @@ +use ark_common::crypto::signer::{SignInfo, Signer, SignerValidator}; //! # Orderbook Contract //! //! This module defines the structure and functionalities of an orderbook contract. It includes -//! trait definitions, error handling, contract storage, events, constructors, L1 handlers, external functions, -//! and internal functions. The primary functionalities include broker whitelisting, order management -//! (creation, cancellation, fulfillment), and order queries. +//! trait definitions, error handling, contract storage, events, constructors, L1 handlers, external +//! functions, and internal functions. The primary functionalities include broker whitelisting, +//! order management (creation, cancellation, fulfillment), and order queries. use ark_common::protocol::order_types::{FulfillInfo, OrderType, CancelInfo, OrderStatus}; -use ark_common::crypto::signer::{SignInfo, Signer, SignerValidator}; use ark_common::protocol::order_v1::OrderV1; use starknet::ContractAddress; -/// Orderbook trait to define operations on orderbooks. #[starknet::interface] -trait Orderbook { - /// Retrieves the type of an order using its hash. - /// - /// # Arguments - /// * `order_hash` - The order hash of order. - fn get_order_type(self: @T, order_hash: felt252) -> OrderType; - - /// Retrieves the status of an order using its hash. - /// - /// # Arguments - /// * `order_hash` - The order hash of order. - fn get_order_status(self: @T, order_hash: felt252) -> OrderStatus; - - /// Retrieves the auction end date. - /// - /// # Arguments - /// * `order_hash` - The order hash of order. - fn get_auction_expiration(self: @T, order_hash: felt252) -> u64; - - /// Retrieves the order using its hash. - /// - /// # Arguments - /// * `order_hash` - The order hash of order. - fn get_order(self: @T, order_hash: felt252) -> OrderV1; - - /// Retrieves the order hash using its token hash. - /// - /// # Arguments - /// * `token_hash` - The token hash of the order. - fn get_order_hash(self: @T, token_hash: felt252) -> felt252; - +trait OrderbookAdmin { /// Upgrades the contract to a new version. /// /// # Arguments @@ -52,65 +21,41 @@ trait Orderbook { fn update_starknet_executor_address(ref self: T, value: starknet::ContractAddress); } -// ************************************************************************* -// ERRORS -// -// Error messages used within the orderbook contract. -// ************************************************************************* -mod orderbook_errors { - const BROKER_UNREGISTERED: felt252 = 'OB: unregistered broker'; - const ORDER_INVALID_DATA: felt252 = 'OB: order invalid data'; - const ORDER_ALREADY_EXISTS: felt252 = 'OB: order already exists'; - const ORDER_ALREADY_EXEC: felt252 = 'OB: order already executed'; - const ORDER_NOT_FULFILLABLE: felt252 = 'OB: order not fulfillable'; - const ORDER_NOT_FOUND: felt252 = 'OB: order not found'; - const ORDER_FULFILLED: felt252 = 'OB: order fulfilled'; - const ORDER_NOT_CANCELLABLE: felt252 = 'OB: order not cancellable'; - const ORDER_EXPIRED: felt252 = 'OB: order expired'; - const ORDER_SAME_OFFERER: felt252 = 'OB: order has same offerer'; - const ORDER_NOT_SAME_OFFERER: felt252 = 'OB: fulfiller is not offerer'; - const OFFER_ALREADY_EXISTS: felt252 = 'OB: offer already exists'; - const ORDER_IS_EXPIRED: felt252 = 'OB: order is expired'; - const ORDER_AUCTION_IS_EXPIRED: felt252 = 'OB: auction is expired'; - const ORDER_MISSING_RELATED_ORDER: felt252 = 'OB: order missing related order'; - const ORDER_HASH_DOES_NOT_MATCH: felt252 = 'OB: order hash does not match'; - const ORDER_TOKEN_ID_DOES_NOT_MATCH: felt252 = 'OB: token id does not match'; - const ORDER_TOKEN_ID_IS_MISSING: felt252 = 'OB: token id is missing'; - const ORDER_TOKEN_HASH_DOES_NOT_MATCH: felt252 = 'OB: token hash does not match'; - const ORDER_NOT_AN_OFFER: felt252 = 'OB: order is not an offer'; - const ORDER_NOT_OPEN: felt252 = 'OB: order is not open'; - const ORDER_OPEN: felt252 = 'OB: order is not open'; - const USE_FULFILL_AUCTION: felt252 = 'OB: must use fulfill auction'; - const OFFER_NOT_STARTED: felt252 = 'OB: offer is not started'; - const INVALID_BROKER: felt252 = 'OB: broker is not whitelisted'; -} /// StarkNet smart contract module for an order book. #[starknet::contract] mod orderbook { - use ark_common::crypto::typed_data::{OrderSign, TypedDataTrait}; - use core::debug::PrintTrait; + use ark_common::crypto::hash::{serialized_hash}; use ark_common::crypto::signer::{SignInfo, Signer, SignerTrait, SignerValidator}; + use ark_common::crypto::typed_data::{OrderSign, TypedDataTrait}; + use ark_common::protocol::order_database::{ + order_read, order_status_read, order_write, order_status_write, order_type_read + }; use ark_common::protocol::order_types::{ OrderStatus, OrderTrait, OrderType, CancelInfo, FulfillInfo, ExecutionValidationInfo, ExecutionInfo, RouteType }; - use ark_common::crypto::hash::{serialized_hash}; - use core::traits::TryInto; - use core::result::ResultTrait; - use core::zeroable::Zeroable; - use core::option::OptionTrait; - use core::starknet::event::EventEmitter; - use core::traits::Into; - use super::{orderbook_errors, Orderbook}; - use starknet::ContractAddress; use ark_common::protocol::order_v1::OrderV1; - use ark_common::protocol::order_database::{ - order_read, order_status_read, order_write, order_status_write, order_type_read + use ark_component::orderbook::OrderbookComponent; + use ark_component::orderbook::{ + OrderbookHooksCreateOrderEmptyImpl, OrderbookHooksCancelOrderEmptyImpl, + OrderbookHooksFulfillOrderEmptyImpl, OrderbookHooksValidateOrderExecutionEmptyImpl, }; + use core::debug::PrintTrait; + use core::option::OptionTrait; + use core::result::ResultTrait; + use core::traits::Into; + use core::traits::TryInto; + use core::zeroable::Zeroable; + use starknet::ContractAddress; + use starknet::storage::Map; + use super::OrderbookAdmin; const EXTENSION_TIME_IN_SECONDS: u64 = 600; const AUCTION_ACCEPTING_TIME_SECS: u64 = 172800; + + component!(path: OrderbookComponent, storage: orderbook, event: OrderbookEvent); + /// Storage struct for the Orderbook contract. #[storage] struct Storage { @@ -121,17 +66,10 @@ mod orderbook { chain_id: felt252, /// Administrator address of the order book. admin: ContractAddress, - /// Mapping of broker addresses to their whitelisted status. - /// Represented as felt252, set to 1 if the broker is registered. - brokers: LegacyMap, - /// Mapping of token_hash to order_hash. - token_listings: LegacyMap, - /// Mapping of token_hash to auction details (order_hash and end_date, auction_offer_count). - auctions: LegacyMap, - /// Mapping of auction offer order_hash to auction listing order_hash. - auction_offers: LegacyMap, /// The address of the StarkNet executor contract. starknet_executor_address: ContractAddress, + #[substorage(v0)] + orderbook: OrderbookComponent::Storage, } // ************************************************************************* @@ -142,78 +80,21 @@ mod orderbook { #[event] #[derive(Drop, starknet::Event)] enum Event { - OrderPlaced: OrderPlaced, - OrderExecuted: OrderExecuted, - OrderCancelled: OrderCancelled, - RollbackStatus: RollbackStatus, - OrderFulfilled: OrderFulfilled, + #[flat] + OrderbookEvent: OrderbookComponent::Event, Upgraded: Upgraded, } - /// Event for when an order is placed. - #[derive(Drop, starknet::Event)] - struct OrderPlaced { - #[key] - order_hash: felt252, - #[key] - order_version: felt252, - #[key] - order_type: OrderType, - // The order that was cancelled by this order. - cancelled_order_hash: Option, - // The full order serialized. - order: OrderV1, - } - - // must be increased when `OrderExecuted` content change - const ORDER_EXECUTED_EVENT_VERSION: u8 = 1; - /// Event for when an order is executed. - #[derive(Drop, starknet::Event)] - struct OrderExecuted { - #[key] - order_hash: felt252, - #[key] - order_status: OrderStatus, - version: u8, - transaction_hash: felt252, - from: ContractAddress, - to: ContractAddress, - } - - /// Event for when an order is cancelled. - #[derive(Drop, starknet::Event)] - struct OrderCancelled { - #[key] - order_hash: felt252, - #[key] - reason: felt252, - } - - /// Event for when an order has been rollbacked to placed. - #[derive(Drop, starknet::Event)] - struct RollbackStatus { - #[key] - order_hash: felt252, - #[key] - reason: felt252 - } - - /// Event for when an order is fulfilled. - #[derive(Drop, starknet::Event)] - struct OrderFulfilled { - #[key] - order_hash: felt252, - #[key] - fulfiller: ContractAddress, - #[key] - related_order_hash: Option, - } - #[derive(Drop, starknet::Event)] struct Upgraded { class_hash: starknet::ClassHash, } + #[abi(embed_v0)] + impl OrderbookImpl = OrderbookComponent::OrderbookImpl; + impl OrderbookActionImpl = OrderbookComponent::OrderbookActionImpl; + impl OrderbookInternalImpl = OrderbookComponent::InternalImpl; + // ************************************************************************* // CONSTRUCTOR // ************************************************************************* @@ -238,54 +119,39 @@ mod orderbook { ) { // Solis already checks that ALL the messages are coming from the executor contract. // TODO: anyway, it can be useful to have an extra check here. - order_status_write(info.order_hash, OrderStatus::Executed); - let order_status = order_status_read(info.order_hash).unwrap(); - self - .emit( - OrderExecuted { - order_hash: info.order_hash, - order_status: order_status, - transaction_hash: info.transaction_hash, - from: info.from, - to: info.to, - version: ORDER_EXECUTED_EVENT_VERSION, - } - ); + self.orderbook.validate_order_execution(info); } /// Update status : only from solis. - #[l1_handler] - fn rollback_status_order( - ref self: ContractState, _from_address: felt252, order_hash: felt252, reason: felt252 - ) { - order_status_write(order_hash, OrderStatus::Open); - self.emit(RollbackStatus { order_hash, reason: reason.into() }); - } + // #[l1_handler] + // fn rollback_status_order( + // ref self: ContractState, _from_address: felt252, order_hash: felt252, reason: felt252 + // ) { + // order_status_write(order_hash, OrderStatus::Open); + // self.emit(RollbackStatus { order_hash, reason: reason.into() }); + // } #[l1_handler] fn create_order_from_l2(ref self: ContractState, _from_address: felt252, order: OrderV1) { - self._create_order(order); + self.orderbook.create_order(order); } #[l1_handler] fn cancel_order_from_l2( ref self: ContractState, _from_address: felt252, cancelInfo: CancelInfo ) { - self._cancel_order(cancelInfo); + self.orderbook.cancel_order(cancelInfo); } #[l1_handler] fn fulfill_order_from_l2( ref self: ContractState, _from_address: felt252, fulfillInfo: FulfillInfo ) { - self._fulfill_order(fulfillInfo); + let _ = self.orderbook.fulfill_order(fulfillInfo); } - // ************************************************************************* - // EXTERNAL FUNCTIONS - // ************************************************************************* #[abi(embed_v0)] - impl ImplOrderbook of Orderbook { + impl OrderbookAdminImpl of OrderbookAdmin { fn upgrade(ref self: ContractState, class_hash: starknet::ClassHash) { assert( starknet::get_caller_address() == self.admin.read(), 'Unauthorized replace class' @@ -301,155 +167,24 @@ mod orderbook { assert(starknet::get_caller_address() == self.admin.read(), 'Unauthorized update'); self.starknet_executor_address.write(value); } - - /// Retrieves the type of an order using its hash. - /// # View - fn get_order_type(self: @ContractState, order_hash: felt252) -> OrderType { - let order_type_option = order_type_read(order_hash); - if order_type_option.is_none() { - panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND); - } - order_type_option.unwrap().into() - } - - /// Retrieves the status of an order using its hash. - /// # View - fn get_order_status(self: @ContractState, order_hash: felt252) -> OrderStatus { - let status = order_status_read(order_hash); - if status.is_none() { - panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND); - } - status.unwrap().into() - } - - /// Retrieves the auction end date - /// # View - fn get_auction_expiration(self: @ContractState, order_hash: felt252) -> u64 { - let order = order_read::(order_hash); - if (order.is_none()) { - panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND); - } - let token_hash = order.unwrap().compute_token_hash(); - let (_, auction_end_date, _) = self.auctions.read(token_hash); - auction_end_date - } - - /// Retrieves the order using its hash. - /// # View - fn get_order(self: @ContractState, order_hash: felt252) -> OrderV1 { - let order = order_read(order_hash); - if (order.is_none()) { - panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND); - } - order.unwrap() - } - - /// Retrieves the order hash using its token hash. - /// # View - fn get_order_hash(self: @ContractState, token_hash: felt252) -> felt252 { - let order_hash = self.token_listings.read(token_hash); - if (order_hash.is_zero()) { - panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND); - } - order_hash - } } // ************************************************************************* - // INTERNAL FUNCTIONS + // INTERNAL FUNCTIONS FOR TESTING PURPOSE // ************************************************************************* #[generate_trait] impl InternalFunctions of InternalFunctionsTrait { - // TODO: kwiss this code repeat itself with the public function, we should refactor it. fn _cancel_order(ref self: ContractState, cancel_info: CancelInfo) { - let order_hash = cancel_info.order_hash; - let order_option = order_read::(order_hash); - assert(order_option.is_some(), orderbook_errors::ORDER_NOT_FOUND); - let order = order_option.unwrap(); - assert(order.offerer == cancel_info.canceller, 'not the same offerrer'); - match order_status_read(order_hash) { - Option::Some(s) => s, - Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), - }; - let block_ts = starknet::get_block_timestamp(); - match order_type_read(order_hash) { - Option::Some(order_type) => { - if order_type == OrderType::Auction { - let auction_token_hash = order.compute_token_hash(); - let (_, auction_end_date, _) = self.auctions.read(auction_token_hash); - assert( - block_ts <= auction_end_date, orderbook_errors::ORDER_AUCTION_IS_EXPIRED - ); - self.auctions.write(auction_token_hash, (0, 0, 0)); - } else { - assert(block_ts < order.end_date, orderbook_errors::ORDER_IS_EXPIRED); - if order_type == OrderType::Listing { - self.token_listings.write(order.compute_token_hash(), 0); - } - } - }, - Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), - }; - - // Cancel order - order_status_write(order_hash, OrderStatus::CancelledUser); - self.emit(OrderCancelled { order_hash, reason: OrderStatus::CancelledUser.into() }); + self.orderbook.cancel_order(cancel_info); } /// Submits and places an order to the orderbook if the order is valid. fn _create_order(ref self: ContractState, order: OrderV1) { - let block_ts = starknet::get_block_timestamp(); - let validation = order.validate_common_data(block_ts); - if validation.is_err() { - panic_with_felt252(validation.unwrap_err().into()); - } - let order_type = order - .validate_order_type() - .expect(orderbook_errors::ORDER_INVALID_DATA); - let order_hash = order.compute_order_hash(); - match order_type { - OrderType::Listing => { - assert( - order_status_read(order_hash).is_none(), - orderbook_errors::ORDER_ALREADY_EXISTS - ); - let _ = self._create_listing_order(order, order_type, order_hash); - }, - OrderType::Auction => { - assert( - order_status_read(order_hash).is_none(), - orderbook_errors::ORDER_ALREADY_EXISTS - ); - self._create_auction(order, order_type, order_hash); - }, - OrderType::Offer => { self._create_offer(order, order_type, order_hash); }, - OrderType::CollectionOffer => { - self._create_collection_offer(order, order_type, order_hash); - }, - }; + self.orderbook.create_order(order); } fn _fulfill_order(ref self: ContractState, fulfill_info: FulfillInfo) { - let order_hash = fulfill_info.order_hash; - let order: OrderV1 = match order_read(order_hash) { - Option::Some(o) => o, - Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), - }; - let status = match order_status_read(order_hash) { - Option::Some(s) => s, - Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), - }; - assert(status == OrderStatus::Open, orderbook_errors::ORDER_NOT_FULFILLABLE); - let order_type = match order_type_read(order_hash) { - Option::Some(s) => s, - Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), - }; - match order_type { - OrderType::Listing => { self._fulfill_listing_order(fulfill_info, order); }, - OrderType::Auction => { self._fulfill_auction_order(fulfill_info, order) }, - OrderType::Offer => { self._fulfill_offer(fulfill_info, order); }, - OrderType::CollectionOffer => { self._fulfill_offer(fulfill_info, order); } - } + let _ = self.orderbook.fulfill_order(fulfill_info); } /// Fulfill auction order @@ -461,99 +196,21 @@ mod orderbook { fn _fulfill_auction_order( ref self: ContractState, fulfill_info: FulfillInfo, order: OrderV1 ) { - let block_timestamp = starknet::get_block_timestamp(); - assert( - order.offerer == fulfill_info.fulfiller, orderbook_errors::ORDER_NOT_SAME_OFFERER - ); - // get auction end date from storage - let (_, end_date, _) = self.auctions.read(order.compute_token_hash()); - assert( - end_date + AUCTION_ACCEPTING_TIME_SECS > block_timestamp, - orderbook_errors::ORDER_EXPIRED - ); - - let related_order_hash = fulfill_info - .related_order_hash - .expect(orderbook_errors::ORDER_MISSING_RELATED_ORDER); - - match order_type_read(related_order_hash) { - Option::Some(order_type) => { - assert( - order_type == OrderType::Offer || order_type == OrderType::CollectionOffer, - orderbook_errors::ORDER_NOT_AN_OFFER - ); - }, - Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), - } - - match order_status_read(related_order_hash) { - Option::Some(s) => { - assert(s == OrderStatus::Open, orderbook_errors::ORDER_NOT_OPEN); - s + let (execute_info, _) = self.orderbook._fulfill_auction_order(fulfill_info, order); + match execute_info { + Option::Some(execute_info) => { + let execute_order_selector = selector!("execute_order"); + let starknet_executor_address: ContractAddress = self + .starknet_executor_address + .read(); + + let mut buf: Array = array![ + starknet_executor_address.into(), execute_order_selector + ]; + execute_info.serialize(ref buf); + starknet::send_message_to_l1_syscall('EXE', buf.span()).unwrap(); }, - Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), - }; - - let related_order = match order_read::(related_order_hash) { - Option::Some(o) => o, - Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND), - }; - - let related_offer_auction = self.auction_offers.read(related_order_hash); - - if related_offer_auction.is_non_zero() { - assert( - related_offer_auction == fulfill_info.order_hash, - orderbook_errors::ORDER_HASH_DOES_NOT_MATCH - ); - } else { - assert(related_order.end_date > block_timestamp, orderbook_errors::ORDER_EXPIRED); - } - let related_order_token_hash = related_order.compute_token_hash(); - assert( - related_order_token_hash == order.compute_token_hash(), - orderbook_errors::ORDER_TOKEN_HASH_DOES_NOT_MATCH - ); - assert( - related_order.token_id == order.token_id, - orderbook_errors::ORDER_TOKEN_ID_DOES_NOT_MATCH - ); - - order_status_write(related_order_hash, OrderStatus::Fulfilled); - order_status_write(fulfill_info.order_hash, OrderStatus::Fulfilled); - self - .emit( - OrderFulfilled { - order_hash: fulfill_info.order_hash, - fulfiller: fulfill_info.fulfiller, - related_order_hash: Option::Some(related_order_hash) - } - ); - - let execute_order_selector = selector!("execute_order"); - let starknet_executor_address: ContractAddress = self.starknet_executor_address.read(); - - let mut buf: Array = array![ - starknet_executor_address.into(), execute_order_selector - ]; - // execute order - if order.token_id.is_some() { - let execute_info = ExecutionInfo { - order_hash: order.compute_order_hash(), - nft_address: order.token_address, - nft_from: order.offerer, - nft_to: related_order.offerer, - nft_token_id: order.token_id.unwrap(), - payment_from: related_order.offerer, - payment_to: fulfill_info.fulfiller, - payment_amount: related_order.start_amount, - payment_currency_address: related_order.currency_address, - payment_currency_chain_id: related_order.currency_chain_id, - listing_broker_address: order.broker_id, - fulfill_broker_address: fulfill_info.fulfill_broker_address - }; - execute_info.serialize(ref buf); - starknet::send_message_to_l1_syscall('EXE', buf.span()).unwrap(); + Option::None => () } } @@ -564,54 +221,22 @@ mod orderbook { /// * `order` - The order. /// fn _fulfill_offer(ref self: ContractState, fulfill_info: FulfillInfo, order: OrderV1) { - if order.token_id.is_some() { - let (auction_order_hash, _, _) = self.auctions.read(order.compute_token_hash()); - - assert(auction_order_hash.is_zero(), orderbook_errors::USE_FULFILL_AUCTION); - } - - assert(fulfill_info.token_id.is_some(), orderbook_errors::ORDER_TOKEN_ID_IS_MISSING); - - let current_date = starknet::get_block_timestamp(); - assert(order.end_date > current_date, orderbook_errors::ORDER_EXPIRED); - - order_status_write(fulfill_info.order_hash, OrderStatus::Fulfilled); - self - .emit( - OrderFulfilled { - order_hash: fulfill_info.order_hash, - fulfiller: fulfill_info.fulfiller, - related_order_hash: Option::None - } - ); - - let execute_order_selector = selector!("execute_order"); - let starknet_executor_address: ContractAddress = self.starknet_executor_address.read(); - - let mut buf: Array = array![ - starknet_executor_address.into(), execute_order_selector - ]; - - if order.token_id.is_some() { - // remove token from listed tokens - self.token_listings.write(order.compute_token_hash(), 0); + let (execute_info, _) = self.orderbook._fulfill_offer(fulfill_info, order); + match execute_info { + Option::Some(execute_info) => { + let execute_order_selector = selector!("execute_order"); + let starknet_executor_address: ContractAddress = self + .starknet_executor_address + .read(); + + let mut buf: Array = array![ + starknet_executor_address.into(), execute_order_selector + ]; + execute_info.serialize(ref buf); + starknet::send_message_to_l1_syscall('EXE', buf.span()).unwrap(); + }, + Option::None => () } - let execute_info = ExecutionInfo { - order_hash: order.compute_order_hash(), - nft_address: order.token_address, - nft_from: fulfill_info.fulfiller, - nft_to: order.offerer, - nft_token_id: fulfill_info.token_id.unwrap(), - payment_from: order.offerer, - payment_to: fulfill_info.fulfiller, - payment_amount: order.start_amount, - payment_currency_address: order.currency_address, - payment_currency_chain_id: order.currency_chain_id, - listing_broker_address: order.broker_id, - fulfill_broker_address: fulfill_info.fulfill_broker_address - }; - execute_info.serialize(ref buf); - starknet::send_message_to_l1_syscall('EXE', buf.span()).unwrap(); } /// Fulfill listing order @@ -623,44 +248,22 @@ mod orderbook { fn _fulfill_listing_order( ref self: ContractState, fulfill_info: FulfillInfo, order: OrderV1 ) { - assert(order.offerer != fulfill_info.fulfiller, orderbook_errors::ORDER_SAME_OFFERER); - assert( - order.end_date > starknet::get_block_timestamp(), orderbook_errors::ORDER_EXPIRED - ); - order_status_write(fulfill_info.order_hash, OrderStatus::Fulfilled); - self - .emit( - OrderFulfilled { - order_hash: fulfill_info.order_hash, - fulfiller: fulfill_info.fulfiller, - related_order_hash: Option::None - } - ); - - let execute_order_selector = selector!("execute_order"); - let starknet_executor_address: ContractAddress = self.starknet_executor_address.read(); - - let mut buf: Array = array![ - starknet_executor_address.into(), execute_order_selector - ]; - - if order.token_id.is_some() { - let execute_info = ExecutionInfo { - order_hash: order.compute_order_hash(), - nft_address: order.token_address, - nft_from: order.offerer, - nft_to: fulfill_info.fulfiller, - nft_token_id: order.token_id.unwrap(), - payment_from: fulfill_info.fulfiller, - payment_to: order.offerer, - payment_amount: order.start_amount, - payment_currency_address: order.currency_address, - payment_currency_chain_id: order.currency_chain_id, - listing_broker_address: order.broker_id, - fulfill_broker_address: fulfill_info.fulfill_broker_address - }; - execute_info.serialize(ref buf); - starknet::send_message_to_l1_syscall('EXE', buf.span()).unwrap(); + let (execute_info, _) = self.orderbook._fulfill_listing_order(fulfill_info, order); + match execute_info { + Option::Some(execute_info) => { + let execute_order_selector = selector!("execute_order"); + let starknet_executor_address: ContractAddress = self + .starknet_executor_address + .read(); + + let mut buf: Array = array![ + starknet_executor_address.into(), execute_order_selector + ]; + + execute_info.serialize(ref buf); + starknet::send_message_to_l1_syscall('EXE', buf.span()).unwrap(); + }, + Option::None => {} } } @@ -670,7 +273,7 @@ mod orderbook { /// * `token_hash` - The token hash of the order. /// fn _get_order_hash_from_token_hash(self: @ContractState, token_hash: felt252) -> felt252 { - self.token_listings.read(token_hash) + self.orderbook._get_order_hash_from_token_hash(token_hash) } /// get previous order @@ -685,38 +288,7 @@ mod orderbook { fn _get_previous_order( self: @ContractState, token_hash: felt252 ) -> Option<(felt252, bool, OrderV1)> { - let previous_listing_orderhash = self.token_listings.read(token_hash); - let (previous_auction_orderhash, _, _) = self.auctions.read(token_hash); - let mut previous_orderhash = 0; - if (previous_listing_orderhash.is_non_zero()) { - previous_orderhash = previous_listing_orderhash; - let previous_order: Option = order_read(previous_orderhash); - assert(previous_order.is_some(), 'Order must exist'); - let previous_order = previous_order.unwrap(); - return Option::Some( - ( - previous_orderhash, - previous_order.end_date <= starknet::get_block_timestamp(), - previous_order - ) - ); - } - if (previous_auction_orderhash.is_non_zero()) { - previous_orderhash = previous_auction_orderhash; - let current_order: Option = order_read(previous_orderhash); - assert(current_order.is_some(), 'Order must exist'); - let current_order = current_order.unwrap(); - let (_, auction_end_date, _) = self.auctions.read(token_hash); - return Option::Some( - ( - previous_orderhash, - auction_end_date <= starknet::get_block_timestamp(), - current_order - ) - ); - } else { - return Option::None; - } + self.orderbook._get_previous_order(token_hash) } /// Process previous order @@ -727,178 +299,39 @@ mod orderbook { fn _process_previous_order( ref self: ContractState, token_hash: felt252, offerer: ContractAddress ) -> Option { - let previous_order = self._get_previous_order(token_hash); - if (previous_order.is_some()) { - let (previous_orderhash, previous_order_is_expired, previous_order) = previous_order - .unwrap(); - let previous_order_status = order_status_read(previous_orderhash) - .expect('Invalid Order status'); - assert( - previous_order_status != OrderStatus::Fulfilled, - orderbook_errors::ORDER_FULFILLED - ); - if (previous_order.offerer == offerer) { - assert(previous_order_is_expired, orderbook_errors::ORDER_NOT_CANCELLABLE); - } - order_status_write(previous_orderhash, OrderStatus::CancelledByNewOrder); - return Option::Some(previous_orderhash); - } - return Option::None; + self.orderbook._process_previous_order(token_hash, offerer) } /// Creates a listing order. fn _create_listing_order( ref self: ContractState, order: OrderV1, order_type: OrderType, order_hash: felt252, ) -> Option { - let token_hash = order.compute_token_hash(); - // revert if order is fulfilled or Open - let current_order_hash = self.token_listings.read(token_hash); - if (current_order_hash.is_non_zero()) { - assert( - order_status_read(current_order_hash) != Option::Some(OrderStatus::Fulfilled), - orderbook_errors::ORDER_FULFILLED - ); - } - let current_order: Option = order_read(current_order_hash); - if (current_order.is_some()) { - let current_order = current_order.unwrap(); - // check if same offerer - if (current_order.offerer == order.offerer) { - // check expiration if order is expired continue - assert( - current_order.end_date <= starknet::get_block_timestamp(), - orderbook_errors::ORDER_ALREADY_EXISTS - ); - } - } - - let cancelled_order_hash = self._process_previous_order(token_hash, order.offerer); - order_write(order_hash, order_type, order); - self.token_listings.write(token_hash, order_hash); - self - .emit( - OrderPlaced { - order_hash: order_hash, - order_version: order.get_version(), - order_type: order_type, - cancelled_order_hash, - order: order - } - ); - cancelled_order_hash + self.orderbook._create_listing_order(order, order_type, order_hash) } /// Creates an auction order. fn _create_auction( ref self: ContractState, order: OrderV1, order_type: OrderType, order_hash: felt252 ) { - let token_hash = order.compute_token_hash(); - let current_order_hash = self.token_listings.read(token_hash); - if (current_order_hash.is_non_zero()) { - assert( - order_status_read(current_order_hash) != Option::Some(OrderStatus::Fulfilled), - orderbook_errors::ORDER_FULFILLED - ); - } - let current_order: Option = order_read(current_order_hash); - if (current_order.is_some()) { - let current_order = current_order.unwrap(); - // check expiration if order is expired continue - if (current_order.offerer == order.offerer) { - assert( - current_order.end_date <= starknet::get_block_timestamp(), - orderbook_errors::ORDER_ALREADY_EXISTS - ); - } - } - let token_hash = order.compute_token_hash(); - let cancelled_order_hash = self._process_previous_order(token_hash, order.offerer); - order_write(order_hash, order_type, order); - self.auctions.write(token_hash, (order_hash, order.end_date, 0)); - self - .emit( - OrderPlaced { - order_hash: order_hash, - order_version: order.get_version(), - order_type: order_type, - cancelled_order_hash, - order: order, - } - ); + self.orderbook._create_auction(order, order_type, order_hash) } fn _manage_auction_offer(ref self: ContractState, order: OrderV1, order_hash: felt252) { - let token_hash = order.compute_token_hash(); - let (auction_order_hash, auction_end_date, auction_offer_count) = self - .auctions - .read(token_hash); - - let current_block_timestamp = starknet::get_block_timestamp(); - // Determine if the auction end date has passed, indicating that the auction is still ongoing. - let auction_is_pending = current_block_timestamp < auction_end_date; - - if auction_is_pending { - // If the auction is still pending, record the new offer by linking it to the - // auction order hash in the 'auction_offers' mapping. - self.auction_offers.write(order_hash, auction_order_hash); - - if auction_end_date - current_block_timestamp < EXTENSION_TIME_IN_SECONDS { - // Increment the number of offers for this auction and extend the auction - // end date by the predefined extension time to allow for additional offers. - self - .auctions - .write( - token_hash, - ( - auction_order_hash, - auction_end_date + EXTENSION_TIME_IN_SECONDS, - auction_offer_count + 1 - ) - ); - } else { - self - .auctions - .write( - token_hash, - (auction_order_hash, auction_end_date, auction_offer_count + 1) - ); - } - } + self.orderbook._manage_auction_offer(order, order_hash) } /// Creates an offer order. fn _create_offer( ref self: ContractState, order: OrderV1, order_type: OrderType, order_hash: felt252 ) { - self._manage_auction_offer(order, order_hash); - order_write(order_hash, order_type, order); - self - .emit( - OrderPlaced { - order_hash: order_hash, - order_version: order.get_version(), - order_type: order_type, - cancelled_order_hash: Option::None, - order: order, - } - ); + self.orderbook._create_offer(order, order_type, order_hash) } /// Creates a collection offer order. fn _create_collection_offer( ref self: ContractState, order: OrderV1, order_type: OrderType, order_hash: felt252 ) { - order_write(order_hash, order_type, order); - self - .emit( - OrderPlaced { - order_hash: order_hash, - order_version: order.get_version(), - order_type: order_type, - cancelled_order_hash: Option::None, - order: order, - } - ); + self.orderbook._create_collection_offer(order, order_type, order_hash) } } } diff --git a/contracts/ark_orderbook/src/orderbook_event_mock.cairo b/contracts/ark_orderbook/src/orderbook_event_mock.cairo index 1e94d05ba..a0ac3b179 100644 --- a/contracts/ark_orderbook/src/orderbook_event_mock.cairo +++ b/contracts/ark_orderbook/src/orderbook_event_mock.cairo @@ -8,14 +8,14 @@ mod orderbook_event_mock { use ark_common::protocol::order_types::{ FulfillInfo, OrderType, CancelInfo, OrderStatus, RouteType }; - use core::traits::TryInto; - use core::result::ResultTrait; - use core::zeroable::Zeroable; + use ark_common::protocol::order_v1::OrderV1; use core::option::OptionTrait; + use core::result::ResultTrait; use core::starknet::event::EventEmitter; use core::traits::Into; + use core::traits::TryInto; + use core::zeroable::Zeroable; use starknet::ContractAddress; - use ark_common::protocol::order_v1::OrderV1; #[storage] struct Storage {} diff --git a/contracts/ark_orderbook/tests/common/setup.cairo b/contracts/ark_orderbook/tests/common/setup.cairo index 27349582a..17f3260db 100644 --- a/contracts/ark_orderbook/tests/common/setup.cairo +++ b/contracts/ark_orderbook/tests/common/setup.cairo @@ -1,17 +1,18 @@ -use core::traits::TryInto; -use core::option::OptionTrait; -use core::traits::Into; -use ark_common::protocol::order_types::{RouteType, FulfillInfo, OrderTrait, OrderType, OrderStatus}; use ark_common::crypto::signer::{Signer, SignInfo}; +use ark_common::protocol::order_types::{RouteType, FulfillInfo, OrderTrait, OrderType, OrderStatus}; use ark_common::protocol::order_v1::OrderV1; -use ark_orderbook::orderbook::{OrderbookDispatcher, OrderbookDispatcherTrait}; +use ark_component::orderbook::interface::IOrderbookDispatcher as OrderbookDispatcher; +use ark_component::orderbook::interface::IOrderbookDispatcherTrait as OrderbookDispatcherTrait; +use core::option::OptionTrait; +use core::traits::Into; +use core::traits::TryInto; use snforge_std::signature::KeyPairTrait; use snforge_std::signature::stark_curve::{ StarkCurveKeyPairImpl, StarkCurveSignerImpl, StarkCurveVerifierImpl }; +use snforge_std::test_address; -use snforge_std::{start_prank, stop_prank, test_address, CheatTarget}; use starknet::ContractAddress; /// Utility function to setup orders for test environment. diff --git a/contracts/ark_orderbook/tests/lib.cairo b/contracts/ark_orderbook/tests/lib.cairo index 411f07586..d13e9d89b 100644 --- a/contracts/ark_orderbook/tests/lib.cairo +++ b/contracts/ark_orderbook/tests/lib.cairo @@ -3,8 +3,8 @@ mod common { } mod unit { + mod test_orderbook; mod order { mod test_order_v1; } - mod test_orderbook; } diff --git a/contracts/ark_orderbook/tests/unit/order/test_order_v1.cairo b/contracts/ark_orderbook/tests/unit/order/test_order_v1.cairo index 479bb8221..c73c09d54 100644 --- a/contracts/ark_orderbook/tests/unit/order/test_order_v1.cairo +++ b/contracts/ark_orderbook/tests/unit/order/test_order_v1.cairo @@ -1,20 +1,6 @@ -use core::option::OptionTrait; -use core::result::ResultTrait; -use core::traits::Into; -use core::traits::TryInto; -use ark_common::protocol::order_v1::{OrderV1, OrderTraitOrderV1}; -use ark_orderbook::orderbook::{ - orderbook, orderbook_errors, OrderbookDispatcher, OrderbookDispatcherTrait -}; -use ark_common::protocol::order_types::{OrderType, OrderTrait, RouteType}; -use ark_common::crypto::signer::{SignInfo, Signer, SignerValidator}; -use debug::PrintTrait; +use ark_common::protocol::order_types::OrderType; +use ark_common::protocol::order_v1::OrderTraitOrderV1; use super::super::super::common::setup::setup_orders; -use snforge_std::{ContractClassTrait, declare}; - -// ********************************************************* -// validate_common_data -// ********************************************************* #[test] fn test_validate_common_data_with_valid_order() { diff --git a/contracts/ark_orderbook/tests/unit/test_orderbook.cairo b/contracts/ark_orderbook/tests/unit/test_orderbook.cairo index 19c5c4918..3aa12b467 100644 --- a/contracts/ark_orderbook/tests/unit/test_orderbook.cairo +++ b/contracts/ark_orderbook/tests/unit/test_orderbook.cairo @@ -1,19 +1,20 @@ -use core::option::OptionTrait; -use ark_orderbook::orderbook::{orderbook, orderbook_errors}; -use ark_common::protocol::order_v1::OrderV1; -use core::traits::Into; -use core::traits::TryInto; -use snforge_std::cheatcodes::CheatTarget; use ark_common::crypto::signer::{SignInfo, Signer, SignerValidator}; -use ark_common::protocol::order_types::{OrderTrait, RouteType, OrderType, FulfillInfo, OrderStatus}; use ark_common::protocol::order_database::{ order_read, order_status_read, order_status_write, order_type_read }; +use ark_common::protocol::order_types::{OrderTrait, RouteType, OrderType, FulfillInfo, OrderStatus}; + +use ark_common::protocol::order_v1::OrderV1; +use ark_component::orderbook::OrderbookComponent; +use ark_orderbook::orderbook::orderbook; +use array::ArrayTrait; +use core::option::OptionTrait; +use core::traits::Into; +use core::traits::TryInto; use snforge_std::{ - start_warp, declare, ContractClassTrait, spy_events, EventSpy, EventFetcher, EventAssertions, - Event, SpyOn, test_address + ContractClassTrait, spy_events, EventSpyAssertionsTrait, EventSpyTrait, Event, test_address, + cheat_block_timestamp, CheatSpan, }; -use array::ArrayTrait; use super::super::common::setup::{setup_listing_order, get_offer_order, setup_orders}; @@ -24,7 +25,7 @@ fn test_create_listing() { let (order_listing_1, order_hash_1, _) = setup_listing_order(600000000000000000); let contract_address = test_address(); let mut state = orderbook::contract_state_for_testing(); - let mut spy = spy_events(SpyOn::One(contract_address)); + let mut spy = spy_events(); let _ = orderbook::InternalFunctions::_create_listing_order( ref state, order_listing_1, OrderType::Listing, order_hash_1 @@ -57,12 +58,13 @@ fn test_create_listing() { @array![ ( contract_address, - orderbook::Event::OrderPlaced( - orderbook::OrderPlaced { + OrderbookComponent::Event::OrderPlaced( + OrderbookComponent::OrderPlaced { order_hash: order_hash_1, cancelled_order_hash: Option::None, order_version: ORDER_VERSION_V1, order_type: OrderType::Listing, + version: OrderbookComponent::ORDER_PLACED_EVENT_VERSION, order: order_listing_1 } ) @@ -102,8 +104,10 @@ fn test_recreate_listing_different_offerer_fulfilled() { // check is first order is fulfilled // assert(order_status.unwrap() == OrderStatus::Fulfilled, 'Order not fulfilled'); - // create a second order over the first one same ressource hash different price, different owner but the previous order is only fulfilled, to cover the case of user who just bought a token to list it instantly but the order is not yet executed - // we cannot place & cancel a previous order if it's fulfilled + // create a second order over the first one same ressource hash different price, different owner + // but the previous order is only fulfilled, to cover the case of user who just bought a token + // to list it instantly but the order is not yet executed we cannot place & cancel a previous + // order if it's fulfilled let (mut order_listing_2, order_hash_2, _) = setup_listing_order(500000000000000000); order_listing_2 .offerer = 0x2484a6517b487be8114013f277f9e2010ac001a24a93e3c48cdf5f8f345a81b @@ -128,8 +132,10 @@ fn test_recreate_listing_same_offerer_fulfilled() { // check is first order is fulfilled let order_status = order_status_read(order_hash_1); assert(order_status.unwrap() == OrderStatus::Fulfilled, 'Order not fulfilled'); - // create a second order over the first one same ressource hash different price, different owner but the previous order is only fulfilled, to cover the case of user who just bought a token to list it instantly but the order is not yet executed - // we cannot place & cancel a previous order if it's fulfilled + // create a second order over the first one same ressource hash different price, different owner + // but the previous order is only fulfilled, to cover the case of user who just bought a token + // to list it instantly but the order is not yet executed we cannot place & cancel a previous + // order if it's fulfilled let (mut order_listing_2, order_hash_2, _) = setup_listing_order(500000000000000000); let _ = orderbook::InternalFunctions::_create_listing_order( ref state, order_listing_2, OrderType::Listing, order_hash_2 @@ -145,7 +151,8 @@ fn test_recreate_listing_new_owner() { ref state, order_listing_1, OrderType::Listing, order_hash_1 ); - // create a second order over the first one same ressource hash different price, different owner it should work and cancel the previous one + // create a second order over the first one same ressource hash different price, different owner + // it should work and cancel the previous one let (mut order_listing_2, order_hash_2, _) = setup_listing_order(500000000000000000); order_listing_2 .offerer = 0x2584a6517b487be8114013f277f9e2010ac001a24a93e3c48cdf5f8f345a823 @@ -180,7 +187,8 @@ fn test_recreate_listing_same_owner_old_order_expired() { let _ = orderbook::InternalFunctions::_create_listing_order( ref state, order_listing_1, OrderType::Listing, order_hash_1 ); - // create a second order over the first one same ressource hash different price, different owner it should work and cancel the previous one + // create a second order over the first one same ressource hash different price, different owner + // it should work and cancel the previous one let (order_listing_2, order_hash_2, _) = setup_listing_order(500000000000000000); let _ = orderbook::InternalFunctions::_create_listing_order( @@ -211,7 +219,7 @@ fn test_create_offer() { let contract_address = test_address(); let mut state = orderbook::contract_state_for_testing(); - let mut spy = spy_events(SpyOn::One(contract_address)); + let mut spy = spy_events(); orderbook::InternalFunctions::_create_offer( ref state, offer_order, OrderType::Offer, order_hash @@ -227,11 +235,12 @@ fn test_create_offer() { @array![ ( contract_address, - orderbook::Event::OrderPlaced( - orderbook::OrderPlaced { + OrderbookComponent::Event::OrderPlaced( + OrderbookComponent::OrderPlaced { order_hash, order_version: ORDER_VERSION_V1, order_type: OrderType::Offer, + version: OrderbookComponent::ORDER_PLACED_EVENT_VERSION, order: offer_order, cancelled_order_hash: Option::None } @@ -244,7 +253,7 @@ fn test_create_offer() { #[test] fn test_create_collection_offer() { let contract_address = test_address(); - let mut spy = spy_events(SpyOn::One(contract_address)); + let mut spy = spy_events(); let mut offer_order = get_offer_order(); offer_order.token_id = Option::None; @@ -265,11 +274,12 @@ fn test_create_collection_offer() { @array![ ( contract_address, - orderbook::Event::OrderPlaced( - orderbook::OrderPlaced { + OrderbookComponent::Event::OrderPlaced( + OrderbookComponent::OrderPlaced { order_hash, order_version: ORDER_VERSION_V1, order_type: OrderType::CollectionOffer, + version: OrderbookComponent::ORDER_PLACED_EVENT_VERSION, order: offer_order, cancelled_order_hash: Option::None } @@ -346,10 +356,10 @@ fn test_fulfill_classic_token_offer() { let fulfill_broker_address = test_address(); let mut state = orderbook::contract_state_for_testing(); - let mut spy = spy_events(SpyOn::One(contract_address)); + let mut spy = spy_events(); let order_hash = order_listing.compute_order_hash(); - start_warp(CheatTarget::One(contract_address), order_listing.start_date); + cheat_block_timestamp(contract_address, order_listing.start_date, CheatSpan::TargetCalls(1)); let fulfill_info = FulfillInfo { order_hash, @@ -362,36 +372,39 @@ fn test_fulfill_classic_token_offer() { }; orderbook::InternalFunctions::_fulfill_offer(ref state, fulfill_info, order_listing); + // FIXME: _fulfill_offer doesn't emit anymore event +// spy +// .assert_emitted( +// @array![ +// ( +// contract_address, +// OrderbookComponent::Event::OrderFulfilled( +// OrderbookComponent::OrderFulfilled { +// order_hash: fulfill_info.order_hash, +// fulfiller: fulfill_info.fulfiller, +// related_order_hash: fulfill_info.related_order_hash, +// order_type: OrderType::Offer, +// version: OrderbookComponent::ORDER_FULFILLED_EVENT_VERSION, +// } +// ) +// ) +// ] +// ); - spy - .assert_emitted( - @array![ - ( - contract_address, - orderbook::Event::OrderFulfilled( - orderbook::OrderFulfilled { - order_hash: fulfill_info.order_hash, - fulfiller: fulfill_info.fulfiller, - related_order_hash: Option::None - } - ) - ) - ] - ); } #[test] fn test_fulfill_classic_collection_offer() { let (order_listing, mut order_offer, _, _) = setup_orders(); let contract_address = test_address(); - let mut spy = spy_events(SpyOn::One(contract_address)); + let mut spy = spy_events(); let mut state = orderbook::contract_state_for_testing(); order_offer.token_id = Option::None; order_offer.start_date = order_listing.start_date + 100; order_offer.end_date = order_listing.start_date + 100; - start_warp(CheatTarget::One(contract_address), order_offer.start_date); + cheat_block_timestamp(contract_address, order_listing.start_date, CheatSpan::TargetCalls(1)); let fulfill_info = FulfillInfo { order_hash: order_listing.compute_order_hash(), @@ -404,22 +417,24 @@ fn test_fulfill_classic_collection_offer() { }; orderbook::InternalFunctions::_fulfill_offer(ref state, fulfill_info, order_listing); - - spy - .assert_emitted( - @array![ - ( - contract_address, - orderbook::Event::OrderFulfilled( - orderbook::OrderFulfilled { - order_hash: fulfill_info.order_hash, - fulfiller: fulfill_info.fulfiller, - related_order_hash: Option::None - } - ) - ) - ] - ); + // FIXME: _fulfill_offer doesn't emit anymore event +// spy +// .assert_emitted( +// @array![ +// ( +// contract_address, +// OrderbookComponent::Event::OrderFulfilled( +// OrderbookComponent::OrderFulfilled { +// order_hash: fulfill_info.order_hash, +// fulfiller: fulfill_info.fulfiller, +// related_order_hash: Option::None, +// order_type: OrderType::CollectionOffer, +// version: OrderbookComponent::ORDER_FULFILLED_EVENT_VERSION, +// } +// ) +// ) +// ] +// ); } #[test] @@ -430,7 +445,9 @@ fn test_fulfill_expired_offer() { let fulfill_broker_address = test_address(); let mut state = orderbook::contract_state_for_testing(); - start_warp(CheatTarget::One(contract_address), order_listing.end_date + 3600); // +1 hour + cheat_block_timestamp( + contract_address, order_listing.end_date + 3600, CheatSpan::TargetCalls(1) + ); // +1 hour let fulfill_info = FulfillInfo { order_hash: order_listing.compute_order_hash(), diff --git a/contracts/ark_oz/Scarb.lock b/contracts/ark_oz/Scarb.lock index c3dc71dda..280f9f71c 100644 --- a/contracts/ark_oz/Scarb.lock +++ b/contracts/ark_oz/Scarb.lock @@ -11,10 +11,98 @@ dependencies = [ [[package]] name = "openzeppelin" -version = "0.10.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.10.0#d77082732daab2690ba50742ea41080eb23299d3" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_governance", + "openzeppelin_introspection", + "openzeppelin_presets", + "openzeppelin_security", + "openzeppelin_token", + "openzeppelin_upgrades", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_access" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_account" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_governance" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" +dependencies = [ + "openzeppelin_access", + "openzeppelin_introspection", +] + +[[package]] +name = "openzeppelin_introspection" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" + +[[package]] +name = "openzeppelin_presets" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_introspection", + "openzeppelin_token", + "openzeppelin_upgrades", +] + +[[package]] +name = "openzeppelin_security" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" + +[[package]] +name = "openzeppelin_token" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" +dependencies = [ + "openzeppelin_account", + "openzeppelin_governance", + "openzeppelin_introspection", +] + +[[package]] +name = "openzeppelin_upgrades" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" + +[[package]] +name = "openzeppelin_utils" +version = "0.15.1" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.1#2f8a93d762858714095a1d391afffa9e21df6983" + +[[package]] +name = "snforge_scarb_plugin" +version = "0.1.0" +source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.28.0#4dfe39d96690ed6b3d56971512700de3f58288ea" [[package]] name = "snforge_std" -version = "0.18.0" -source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.18.0#48f909a56b08cbdc5ca6a21a836b0fbc6c36d55b" +version = "0.28.0" +source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.28.0#4dfe39d96690ed6b3d56971512700de3f58288ea" +dependencies = [ + "snforge_scarb_plugin", +] diff --git a/contracts/ark_oz/Scarb.toml b/contracts/ark_oz/Scarb.toml index ffa101978..a3c365ee4 100644 --- a/contracts/ark_oz/Scarb.toml +++ b/contracts/ark_oz/Scarb.toml @@ -5,9 +5,15 @@ version = "0.1.0" # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html [dependencies] -starknet = "2.5.4" -openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.10.0" } -snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.18.0" } +starknet = "2.7.1" +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.15.1" } + +[dev-dependencies] +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.28.0" } +assert_macros = "0.1.0" + +[tool.fmt] +sort-module-level-items = true [lib] diff --git a/contracts/ark_oz/src/erc2981/erc2981.cairo b/contracts/ark_oz/src/erc2981/erc2981.cairo index 7b0c45b3f..b8eae00c6 100644 --- a/contracts/ark_oz/src/erc2981/erc2981.cairo +++ b/contracts/ark_oz/src/erc2981/erc2981.cairo @@ -1,6 +1,8 @@ #[starknet::component] pub mod ERC2981Component { use starknet::ContractAddress; + use starknet::storage::Map; + use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; use openzeppelin::introspection::src5::SRC5Component; @@ -15,8 +17,8 @@ pub mod ERC2981Component { struct Storage { default_receiver: ContractAddress, default_fees: FeesRatio, - token_receiver: LegacyMap, - token_fees: LegacyMap, + token_receiver: Map, + token_fees: Map, } #[event] diff --git a/contracts/ark_oz/tests/test_erc2981.cairo b/contracts/ark_oz/tests/test_erc2981.cairo index eed401606..ab066b680 100644 --- a/contracts/ark_oz/tests/test_erc2981.cairo +++ b/contracts/ark_oz/tests/test_erc2981.cairo @@ -9,36 +9,31 @@ use ark_oz::erc2981::{IERC2981SetupDispatcher, IERC2981SetupDispatcherTrait}; use ark_oz::erc2981::{FeesRatio, FeesImpl, FeesRatioDefault}; -use snforge_std::{ContractClass, ContractClassTrait, declare}; -use snforge_std::{start_prank, stop_prank, CheatTarget}; +use snforge_std::{ContractClass, ContractClassTrait, declare, DeclareResultTrait, cheat_caller_address, CheatSpan}; fn setup_contract() -> (ContractAddress, ContractAddress, ContractAddress, FeesRatio) { let owner = contract_address_const::<'owner'>(); let receiver = contract_address_const::<'receiver'>(); let default_fees: FeesRatio = Default::default(); - let contract = declare('MockERC2981'); + let contract = declare("MockERC2981").unwrap().contract_class(); let mut calldata: Array = array![]; calldata.append(owner.into()); calldata.append(receiver.into()); default_fees.serialize(ref calldata); - - (contract.deploy(@calldata).unwrap(), owner, receiver, default_fees) + let (mock_erc2981_address, _) = contract.deploy(@calldata).unwrap(); + (mock_erc2981_address, owner, receiver, default_fees) } #[test] fn test_fees_ratio_invalid() { - assert!(!FeesRatio { numerator: 0, denominator: 0, }.is_valid(), "Shall be invalid"); - - assert!(!FeesRatio { numerator: 30, denominator: 30, }.is_valid(), "Shall be invalid"); - - assert!(!FeesRatio { numerator: 40, denominator: 30, }.is_valid(), "Shall be invalid"); - - assert!(!FeesRatio { numerator: 5, denominator: 10000, }.is_valid(), "Shall be invalid"); - - assert!(!FeesRatio { numerator: 0, denominator: 0 }.is_valid(), "Shall be invalid"); - - assert!(FeesRatio { numerator: 0, denominator: 1, }.is_valid(), "Shall be valid"); + assert!(!FeesRatio { numerator: 0, denominator: 0, }.is_valid(), "(0,0) Shall be invalid"); + assert!(!FeesRatio { numerator: 30, denominator: 30, }.is_valid(), "(30,30) Shall be invalid"); + assert!(!FeesRatio { numerator: 40, denominator: 30, }.is_valid(), "(40,30) Shall be invalid"); + assert!(FeesRatio { numerator: 5, denominator: 10000, }.is_valid(), "(5,10000) Shall be valid"); + assert!(!FeesRatio { numerator: 5, denominator: 10001, }.is_valid(), "(5,10001) Shall be invalid"); + assert!(!FeesRatio { numerator: 0, denominator: 0 }.is_valid(), "(0,0) Shall be invalid"); + assert!(FeesRatio { numerator: 0, denominator: 1, }.is_valid(), "(0,1) Shall be valid"); } #[test] @@ -59,15 +54,14 @@ fn test_erc2981_interface_is_supported() { } #[test] -#[should_panic(expected: ('Caller is not the owner',))] +#[should_panic(expected: 'Caller is not the owner')] fn test_only_owner_can_set_default_royalty() { let (contract_address, _, _, _) = setup_contract(); let alice = contract_address_const::<'alice'>(); let other_receiver = contract_address_const::<'other_receiver'>(); let token = IERC2981SetupDispatcher { contract_address: contract_address }; - start_prank(CheatTarget::One(contract_address), alice); + cheat_caller_address(contract_address, alice, CheatSpan::TargetCalls(1)); token.set_default_royalty(other_receiver, Default::default()); - stop_prank(CheatTarget::One(contract_address)); } // TODO: add event check @@ -77,9 +71,10 @@ fn test_owner_set_default_royalty() { let other_receiver = contract_address_const::<'other_receiver'>(); let other_fees = FeesRatio { numerator: 5, denominator: 100, }; let token = IERC2981SetupDispatcher { contract_address: contract_address }; - start_prank(CheatTarget::One(contract_address), owner); + + cheat_caller_address(contract_address, owner, CheatSpan::TargetCalls(1)); token.set_default_royalty(other_receiver, other_fees); - stop_prank(CheatTarget::One(contract_address)); + let (receiver, fees_ratio) = token.default_royalty(); assert_eq!(receiver, other_receiver, "Default receiver not updated"); assert_eq!(fees_ratio, other_fees, "Default fees not updated"); @@ -94,9 +89,8 @@ fn test_owner_set_token_royalty() { let other_fees = FeesRatio { numerator: 5, denominator: 100, }; let token = IERC2981SetupDispatcher { contract_address: contract_address }; - start_prank(CheatTarget::One(contract_address), owner); + cheat_caller_address(contract_address, owner, CheatSpan::TargetCalls(1)); token.set_token_royalty(token_id, other_receiver, other_fees); - stop_prank(CheatTarget::One(contract_address)); let (receiver, fees_ratio) = token.token_royalty(token_id); assert_eq!(receiver, other_receiver, "Token receiver not updated"); @@ -107,7 +101,7 @@ fn test_owner_set_token_royalty() { } #[test] -#[should_panic(expected: ('Caller is not the owner',))] +#[should_panic(expected: 'Caller is not the owner')] fn test_only_owner_can_set_token_royalty() { let token_id = 256; let alice = contract_address_const::<'alice'>(); @@ -116,9 +110,8 @@ fn test_only_owner_can_set_token_royalty() { let other_fees = FeesRatio { numerator: 5, denominator: 100, }; let token = IERC2981SetupDispatcher { contract_address: contract_address }; - start_prank(CheatTarget::One(contract_address), alice); + cheat_caller_address(contract_address, alice, CheatSpan::TargetCalls(1)); token.set_token_royalty(token_id, other_receiver, other_fees); - stop_prank(CheatTarget::One(contract_address)); } #[test] @@ -132,10 +125,9 @@ fn test_royalty_compute() { let other_receiver = contract_address_const::<'other_receiver'>(); let other_fees = FeesRatio { numerator: 5, denominator: 100 }; - start_prank(CheatTarget::One(contract_address), owner); + cheat_caller_address(contract_address, owner, CheatSpan::TargetCalls(2)); token.set_token_royalty(token_id, token_receiver, token_fees); token.set_default_royalty(other_receiver, other_fees); - stop_prank(CheatTarget::One(contract_address)); let sale_price = 100_000_000; let token = IERC2981Dispatcher { contract_address: contract_address }; diff --git a/contracts/ark_starknet/Scarb.toml b/contracts/ark_starknet/Scarb.toml index 7aa7af24b..15134d741 100644 --- a/contracts/ark_starknet/Scarb.toml +++ b/contracts/ark_starknet/Scarb.toml @@ -3,16 +3,24 @@ name = "ark_starknet" version = "0.1.0" [dependencies] -starknet = "2.5.4" -openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.10.0" } -snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.18.0" } +starknet.workspace = true +openzeppelin.workspace = true ark_common = { path = "../ark_common" } +ark_component = { path = "../ark_component" } + ark_tokens = { path = "../ark_tokens" } ark_oz = { path = "../ark_oz" } +[dev-dependencies] +snforge_std.workspace = true +assert_macros.workspace = true + [scripts] test.workspace = true +[tool] +fmt.workspace = true + [[target.starknet-contract]] sierra = true casm = true diff --git a/contracts/ark_starknet/src/appchain_messaging.cairo b/contracts/ark_starknet/src/appchain_messaging.cairo index 37a34eee5..ab899d35f 100644 --- a/contracts/ark_starknet/src/appchain_messaging.cairo +++ b/contracts/ark_starknet/src/appchain_messaging.cairo @@ -95,8 +95,9 @@ trait IUpgradeable { #[starknet::contract] mod appchain_messaging { - use starknet::{ContractAddress, ClassHash}; use debug::PrintTrait; + use starknet::storage::Map; + use starknet::{ContractAddress, ClassHash}; use super::{IAppchainMessaging, IUpgradeable}; @@ -110,10 +111,10 @@ mod appchain_messaging { // The nonce for messages sent from Starknet. sn_to_appc_nonce: felt252, // Ledger of messages hashes sent from Starknet to the appchain. - sn_to_appc_messages: LegacyMap::, + sn_to_appc_messages: Map::, // Ledger of messages hashes registered from the appchain and a refcount // associated to it. - appc_to_sn_messages: LegacyMap::, + appc_to_sn_messages: Map::, } #[event] diff --git a/contracts/ark_starknet/src/executor.cairo b/contracts/ark_starknet/src/executor.cairo index dbb4f8659..5045797b9 100644 --- a/contracts/ark_starknet/src/executor.cairo +++ b/contracts/ark_starknet/src/executor.cairo @@ -1,11 +1,11 @@ use ark_common::protocol::order_types::OrderTrait; +use ark_common::protocol::order_types::OrderType; + +use ark_common::protocol::order_v1::{OrderV1, OrderTraitOrderV1}; use core::serde::Serde; use starknet::ContractAddress; -use ark_common::protocol::order_v1::{OrderV1, OrderTraitOrderV1}; -use ark_common::protocol::order_types::OrderType; - #[derive(Drop, Copy, Debug, Serde, starknet::Store)] struct OrderInfo { @@ -54,42 +54,50 @@ impl OrderV1IntoOrderInfo of Into { #[starknet::contract] mod executor { - use core::zeroable::Zeroable; - use core::traits::Into; - use starknet::contract_address_to_felt252; - use starknet::get_contract_address; - - use core::debug::PrintTrait; - use core::traits::TryInto; - use core::box::BoxTrait; - use core::option::OptionTrait; - - use starknet::{ContractAddress, ClassHash}; use ark_common::protocol::order_types::{ RouteType, ExecutionInfo, ExecutionValidationInfo, FulfillInfo, CreateOrderInfo, FulfillOrderInfo, CancelOrderInfo, CancelInfo, OrderType, }; use ark_common::protocol::order_v1::{OrderV1, OrderTraitOrderV1}; + use ark_component::orderbook::OrderbookComponent; + use ark_component::orderbook::{ + OrderbookHooksCreateOrderEmptyImpl, OrderbookHooksCancelOrderEmptyImpl, + OrderbookHooksFulfillOrderEmptyImpl, OrderbookHooksValidateOrderExecutionEmptyImpl, + }; use ark_oz::erc2981::interface::IERC2981_ID; - use ark_oz::erc2981::{IERC2981Dispatcher, IERC2981DispatcherTrait}; use ark_oz::erc2981::{FeesRatio, FeesRatioDefault, FeesImpl}; - - use ark_starknet::interfaces::{IExecutor, IUpgradable, IMaintenance}; - use ark_starknet::interfaces::FeesAmount; + use ark_oz::erc2981::{IERC2981Dispatcher, IERC2981DispatcherTrait}; use ark_starknet::appchain_messaging::{ IAppchainMessagingDispatcher, IAppchainMessagingDispatcherTrait, }; + use ark_starknet::interfaces::FeesAmount; + + use ark_starknet::interfaces::{IExecutor, IUpgradable, IMaintenance}; + use core::box::BoxTrait; + + use core::debug::PrintTrait; + use core::option::OptionTrait; + use core::traits::Into; + use core::traits::TryInto; + use core::zeroable::Zeroable; + use openzeppelin::introspection::interface::{ISRC5, ISRC5Dispatcher, ISRC5DispatcherTrait}; use openzeppelin::token::{ erc721::interface::{IERC721, IERC721Dispatcher, IERC721DispatcherTrait}, erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait} }; - use openzeppelin::introspection::interface::{ISRC5, ISRC5Dispatcher, ISRC5DispatcherTrait}; + use starknet::contract_address_to_felt252; + use starknet::get_contract_address; + use starknet::storage::Map; + + use starknet::{ContractAddress, ClassHash}; use super::{OrderInfo, OrderV1IntoOrderInfo}; + component!(path: OrderbookComponent, storage: orderbook, event: OrderbookEvent); + #[storage] struct Storage { admin_address: ContractAddress, @@ -97,16 +105,18 @@ mod executor { eth_contract_address: ContractAddress, messaging_address: ContractAddress, chain_id: felt252, - broker_fees: LegacyMap, + broker_fees: Map, ark_fees: FeesRatio, // order hash -> OrderInfo - orders: LegacyMap, + orders: Map, // fallback when collection doesn't implement ERC2981 default_receiver: ContractAddress, default_fees: FeesRatio, - creator_fees: LegacyMap, + creator_fees: Map, // maintenance mode in_maintenance: bool, + #[substorage(v0)] + orderbook: OrderbookComponent::Storage, } #[event] @@ -115,6 +125,8 @@ mod executor { OrderExecuted: OrderExecuted, CollectionFallbackFees: CollectionFallbackFees, ExecutorInMaintenance: ExecutorInMaintenance, + // #[flat] // OrderExecuted conflict + OrderbookEvent: OrderbookComponent::Event, } #[derive(Drop, starknet::Event)] @@ -147,6 +159,10 @@ mod executor { const FEES_RATIO_INVALID: felt252 = 'Fees ratio is invalid'; } + #[abi(embed_v0)] + impl OrderbookImpl = OrderbookComponent::OrderbookImpl; + impl OrderbookActionImpl = OrderbookComponent::OrderbookActionImpl; + #[constructor] fn constructor( ref self: ContractState, @@ -297,28 +313,11 @@ mod executor { fn cancel_order(ref self: ContractState, cancelInfo: CancelInfo) { _ensure_is_not_in_maintenance(@self); - let messaging = IAppchainMessagingDispatcher { - contract_address: self.messaging_address.read() - }; - - let vinfo = CancelOrderInfo { cancelInfo: cancelInfo.clone() }; - - let mut vinfo_buf = array![]; - Serde::serialize(@vinfo, ref vinfo_buf); - - messaging - .send_message_to_appchain( - self.arkchain_orderbook_address.read(), - selector!("cancel_order_from_l2"), - vinfo_buf.span(), - ); + self.orderbook.cancel_order(cancelInfo); } fn create_order(ref self: ContractState, order: OrderV1) { _ensure_is_not_in_maintenance(@self); - let messaging = IAppchainMessagingDispatcher { - contract_address: self.messaging_address.read() - }; let vinfo = CreateOrderInfo { order: order.clone() }; _verify_create_order(@self, @vinfo); @@ -327,169 +326,20 @@ mod executor { let order_info = order.into(); self.orders.write(order_hash, order_info); - let mut vinfo_buf = array![]; - Serde::serialize(@vinfo, ref vinfo_buf); - - messaging - .send_message_to_appchain( - self.arkchain_orderbook_address.read(), - selector!("create_order_from_l2"), - vinfo_buf.span(), - ); + self.orderbook.create_order(order); } fn fulfill_order(ref self: ContractState, fulfillInfo: FulfillInfo) { _ensure_is_not_in_maintenance(@self); - let messaging = IAppchainMessagingDispatcher { - contract_address: self.messaging_address.read() - }; let vinfo = FulfillOrderInfo { fulfillInfo: fulfillInfo.clone() }; _verify_fulfill_order(@self, @vinfo); - let mut vinfo_buf = array![]; - Serde::serialize(@vinfo, ref vinfo_buf); - - messaging - .send_message_to_appchain( - self.arkchain_orderbook_address.read(), - selector!("fulfill_order_from_l2"), - vinfo_buf.span(), - ); - } - - fn execute_order(ref self: ContractState, execution_info: ExecutionInfo) { - // assert( - // starknet::get_caller_address() == self.messaging_address.read(), - // 'Invalid msg sender' - // ); - - // Check if execution_info.currency_contract_address is whitelisted - _ensure_is_not_in_maintenance(@self); - assert( - execution_info.payment_currency_chain_id == self.chain_id.read(), - 'Chain ID is not SN_MAIN' - ); - - let currency_contract = IERC20Dispatcher { - contract_address: execution_info.payment_currency_address.try_into().unwrap() - }; - - let (creator_address, creator_fees_amount) = _compute_creator_fees_amount( - @self, - @execution_info.nft_address, - execution_info.payment_amount, - execution_info.nft_token_id - ); - let (fulfill_broker_fees_amount, listing_broker_fees_amount, ark_fees_amount, _) = - _compute_fees_amount( - @self, - execution_info.fulfill_broker_address, - execution_info.listing_broker_address, - execution_info.nft_address, - execution_info.nft_token_id, - execution_info.payment_amount - ); - assert!( - execution_info - .payment_amount > (fulfill_broker_fees_amount - + listing_broker_fees_amount - + creator_fees_amount - + ark_fees_amount), - "Fees exceed payment amount" - ); - - let seller_amount = execution_info.payment_amount - - (fulfill_broker_fees_amount - + listing_broker_fees_amount - + creator_fees_amount - + ark_fees_amount); - - // split the fees - currency_contract - .transfer_from( - execution_info.payment_from, - execution_info.fulfill_broker_address, - fulfill_broker_fees_amount, - ); - - currency_contract - .transfer_from( - execution_info.payment_from, - execution_info.listing_broker_address, - listing_broker_fees_amount - ); - - if creator_fees_amount > 0 { - let (default_receiver_creator, _) = self.get_default_creator_fees(); - if creator_address == default_receiver_creator { - self - .emit( - CollectionFallbackFees { - collection: execution_info.nft_address, - amount: creator_fees_amount, - currency_contract: currency_contract.contract_address, - receiver: default_receiver_creator, - } - ) - } - currency_contract - .transfer_from( - execution_info.payment_from, creator_address, creator_fees_amount - ); + match self.orderbook.fulfill_order(fulfillInfo) { + Option::Some(execute_info) => { _execute_order(ref self, execute_info); }, + Option::None => panic!("OB: failed to fulfill order"), } - - if ark_fees_amount > 0 { - currency_contract - .transfer_from( - execution_info.payment_from, self.admin_address.read(), ark_fees_amount - ); - } - // finally transfer to the seller - currency_contract - .transfer_from( - execution_info.payment_from, execution_info.payment_to, seller_amount - ); - - let nft_contract = IERC721Dispatcher { contract_address: execution_info.nft_address }; - nft_contract - .transfer_from( - execution_info.nft_from, execution_info.nft_to, execution_info.nft_token_id - ); - - let tx_info = starknet::get_tx_info().unbox(); - let transaction_hash = tx_info.transaction_hash; - let block_timestamp = starknet::info::get_block_timestamp(); - - self - .emit( - OrderExecuted { - order_hash: execution_info.order_hash, transaction_hash, block_timestamp, - } - ); - - let messaging = IAppchainMessagingDispatcher { - contract_address: self.messaging_address.read() - }; - - let vinfo = ExecutionValidationInfo { - order_hash: execution_info.order_hash, - transaction_hash, - starknet_block_timestamp: block_timestamp, - from: execution_info.nft_from, - to: execution_info.nft_to, - }; - - let mut vinfo_buf = array![]; - Serde::serialize(@vinfo, ref vinfo_buf); - - messaging - .send_message_to_appchain( - self.arkchain_orderbook_address.read(), - selector!("validate_order_execution"), - vinfo_buf.span(), - ); } } @@ -747,6 +597,131 @@ mod executor { ) } + fn _execute_order(ref self: ContractState, execution_info: ExecutionInfo) { + // assert( + // starknet::get_caller_address() == self.messaging_address.read(), + // 'Invalid msg sender' + // ); + + // Check if execution_info.currency_contract_address is whitelisted + _ensure_is_not_in_maintenance(@self); + assert( + execution_info.payment_currency_chain_id == self.chain_id.read(), + 'Chain ID is not SN_MAIN' + ); + + let currency_contract = IERC20Dispatcher { + contract_address: execution_info.payment_currency_address.try_into().unwrap() + }; + + let (creator_address, creator_fees_amount) = _compute_creator_fees_amount( + @self, + @execution_info.nft_address, + execution_info.payment_amount, + execution_info.nft_token_id + ); + let (fulfill_broker_fees_amount, listing_broker_fees_amount, ark_fees_amount, _) = + _compute_fees_amount( + @self, + execution_info.fulfill_broker_address, + execution_info.listing_broker_address, + execution_info.nft_address, + execution_info.nft_token_id, + execution_info.payment_amount + ); + assert!( + execution_info + .payment_amount > (fulfill_broker_fees_amount + + listing_broker_fees_amount + + creator_fees_amount + + ark_fees_amount), + "Fees exceed payment amount" + ); + + let seller_amount = execution_info.payment_amount + - (fulfill_broker_fees_amount + + listing_broker_fees_amount + + creator_fees_amount + + ark_fees_amount); + + // split the fees + if fulfill_broker_fees_amount > 0 { + currency_contract + .transfer_from( + execution_info.payment_from, + execution_info.fulfill_broker_address, + fulfill_broker_fees_amount, + ); + } + + if listing_broker_fees_amount > 0 { + currency_contract + .transfer_from( + execution_info.payment_from, + execution_info.listing_broker_address, + listing_broker_fees_amount + ); + } + + if creator_fees_amount > 0 { + let (default_receiver_creator, _) = self.get_default_creator_fees(); + if creator_address == default_receiver_creator { + self + .emit( + CollectionFallbackFees { + collection: execution_info.nft_address, + amount: creator_fees_amount, + currency_contract: currency_contract.contract_address, + receiver: default_receiver_creator, + } + ) + } + currency_contract + .transfer_from(execution_info.payment_from, creator_address, creator_fees_amount); + } + + if ark_fees_amount > 0 { + currency_contract + .transfer_from( + execution_info.payment_from, self.admin_address.read(), ark_fees_amount + ); + } + // finally transfer to the seller + if seller_amount > 0 { + currency_contract + .transfer_from( + execution_info.payment_from, execution_info.payment_to, seller_amount + ); + } + + let nft_contract = IERC721Dispatcher { contract_address: execution_info.nft_address }; + nft_contract + .transfer_from( + execution_info.nft_from, execution_info.nft_to, execution_info.nft_token_id + ); + + let tx_info = starknet::get_tx_info().unbox(); + let transaction_hash = tx_info.transaction_hash; + let block_timestamp = starknet::info::get_block_timestamp(); + + self + .emit( + OrderExecuted { + order_hash: execution_info.order_hash, transaction_hash, block_timestamp, + } + ); + + let vinfo = ExecutionValidationInfo { + order_hash: execution_info.order_hash, + transaction_hash, + starknet_block_timestamp: block_timestamp, + from: execution_info.nft_from, + to: execution_info.nft_to, + }; + + self.orderbook.validate_order_execution(vinfo); + } + fn _check_erc20_amount( token_address: @ContractAddress, amount: u256, user: @ContractAddress ) -> bool { diff --git a/contracts/ark_starknet/src/interfaces.cairo b/contracts/ark_starknet/src/interfaces.cairo index ed0b1c60a..98fb2264e 100644 --- a/contracts/ark_starknet/src/interfaces.cairo +++ b/contracts/ark_starknet/src/interfaces.cairo @@ -1,9 +1,9 @@ -//! Interfaces for arkchain operator. -use starknet::{ClassHash, ContractAddress}; use ark_common::protocol::order_types::ExecutionInfo; use ark_common::protocol::order_types::OrderV1; use ark_common::protocol::order_types::{FulfillInfo, CancelInfo}; use ark_oz::erc2981::FeesRatio; +//! Interfaces for arkchain operator. +use starknet::{ClassHash, ContractAddress}; #[derive(Serde, Drop)] struct FeesAmount { @@ -18,7 +18,7 @@ trait IExecutor { fn fulfill_order(ref self: T, fulfillInfo: FulfillInfo); fn cancel_order(ref self: T, cancelInfo: CancelInfo); fn create_order(ref self: T, order: OrderV1); - fn execute_order(ref self: T, execution_info: ExecutionInfo); + // fn execute_order(ref self: T, execution_info: ExecutionInfo); fn update_admin_address(ref self: T, admin_address: ContractAddress); fn update_orderbook_address(ref self: T, orderbook_address: ContractAddress); fn update_eth_address(ref self: T, eth_address: ContractAddress); diff --git a/contracts/ark_starknet/src/lib.cairo b/contracts/ark_starknet/src/lib.cairo index 57f65f841..ac8cf351f 100644 --- a/contracts/ark_starknet/src/lib.cairo +++ b/contracts/ark_starknet/src/lib.cairo @@ -1,4 +1,4 @@ mod appchain_messaging; -mod interfaces; mod executor; +mod interfaces; diff --git a/contracts/ark_starknet/tests/common/setup.cairo b/contracts/ark_starknet/tests/common/setup.cairo index e2251c8a4..d4e957967 100644 --- a/contracts/ark_starknet/tests/common/setup.cairo +++ b/contracts/ark_starknet/tests/common/setup.cairo @@ -1,13 +1,13 @@ +use ark_common::protocol::order_types::RouteType; + +use ark_common::protocol::order_v1::OrderV1; use serde::Serde; +use snforge_std::{ContractClass, ContractClassTrait, declare, DeclareResultTrait}; use starknet::{ContractAddress, contract_address_const}; -use snforge_std::{ContractClass, ContractClassTrait, declare}; - -use ark_common::protocol::order_v1::OrderV1; -use ark_common::protocol::order_types::RouteType; fn deploy_erc20() -> ContractAddress { - let contract = declare('FreeMintERC20'); + let contract = declare("FreeMintERC20").unwrap().contract_class(); let initial_supply: u256 = 10_000_000_000_u256; let name: ByteArray = "DummyERC20"; let symbol: ByteArray = "DUMMY"; @@ -16,7 +16,7 @@ fn deploy_erc20() -> ContractAddress { initial_supply.serialize(ref calldata); name.serialize(ref calldata); symbol.serialize(ref calldata); - let erc20_address = contract.deploy(@calldata).unwrap(); + let (erc20_address, _) = contract.deploy(@calldata).unwrap(); erc20_address } @@ -29,27 +29,28 @@ fn deploy_nft(royalty: bool) -> ContractAddress { name.serialize(ref calldata); symbol.serialize(ref calldata); base_uri.serialize(ref calldata); - if royalty { + let (nft_address, _) = if royalty { let owner = contract_address_const::<'nft_owner'>(); calldata.append(owner.into()); - let contract = declare('FreeMintNFTRoyalty'); + let contract = declare("FreeMintNFTRoyalty").unwrap().contract_class(); contract.deploy(@calldata).unwrap() } else { - let contract = declare('FreeMintNFT'); + let contract = declare("FreeMintNFT").unwrap().contract_class(); contract.deploy(@calldata).unwrap() - } + }; + nft_address } fn deploy_executor() -> ContractAddress { - let messaging_contract = declare('appchain_messaging'); + let messaging_contract = declare("appchain_messaging").unwrap().contract_class(); let messaging_owner = contract_address_const::<'messaging_owner'>(); let appchain_account = contract_address_const::<'messaging_account'>(); let mut messaging_calldata: Array = array![]; messaging_calldata.append(messaging_owner.into()); messaging_calldata.append(appchain_account.into()); - let messaging_address = messaging_contract.deploy(@messaging_calldata).unwrap(); + let (messaging_address, _) = messaging_contract.deploy(@messaging_calldata).unwrap(); - let contract = declare('executor'); + let contract = declare("executor").unwrap().contract_class(); let admin_address = contract_address_const::<'admin'>(); let eth_address = contract_address_const::<'eth'>(); @@ -58,7 +59,8 @@ fn deploy_executor() -> ContractAddress { calldata.append(eth_address.into()); calldata.append(messaging_address.into()); calldata.append('SN_MAIN'); - contract.deploy(@calldata).unwrap() + let (executor_address, _) = contract.deploy(@calldata).unwrap(); + executor_address } fn setup() -> (ContractAddress, ContractAddress, ContractAddress) { diff --git a/contracts/ark_starknet/tests/integration/create_order.cairo b/contracts/ark_starknet/tests/integration/create_order.cairo index a41a08368..7c7626cfa 100644 --- a/contracts/ark_starknet/tests/integration/create_order.cairo +++ b/contracts/ark_starknet/tests/integration/create_order.cairo @@ -1,7 +1,6 @@ -use starknet::{ContractAddress, contract_address_const}; +use ark_common::protocol::order_types::RouteType; use ark_common::protocol::order_v1::OrderV1; -use ark_common::protocol::order_types::RouteType; use ark_starknet::interfaces::{ @@ -14,8 +13,8 @@ use ark_tokens::erc20::IFreeMintDispatcherTrait as Erc20DispatcherTrait; use ark_tokens::erc721::IFreeMintDispatcher as Erc721Dispatcher; use ark_tokens::erc721::IFreeMintDispatcherTrait as Erc721DispatcherTrait; -use snforge_std as snf; -use snf::{ContractClass, ContractClassTrait, CheatTarget}; +use snforge_std::{cheat_caller_address, CheatSpan}; +use starknet::{ContractAddress, contract_address_const}; use super::super::common::setup::{setup, setup_order}; @@ -32,9 +31,8 @@ fn test_create_order_erc20_to_erc721_ok() { order.offerer = offerer; order.start_amount = start_amount; - snf::start_prank(CheatTarget::One(executor_address), offerer); + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.create_order(order); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] @@ -52,14 +50,13 @@ fn test_create_order_erc721_to_erc20_ok() { order.offerer = offerer; order.token_id = Option::Some(token_id); - snf::start_prank(CheatTarget::One(executor_address), offerer); + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.create_order(order); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] -#[should_panic(expected: ("Caller is not the offerer",))] +#[should_panic(expected: "Caller is not the offerer")] fn test_create_order_offerer_shall_be_caller() { let (executor_address, erc20_address, nft_address) = setup(); let offerer = contract_address_const::<'offerer'>(); @@ -68,13 +65,12 @@ fn test_create_order_offerer_shall_be_caller() { let mut order = setup_order(erc20_address, nft_address); order.offerer = offerer; - snf::start_prank(CheatTarget::One(executor_address), caller); + cheat_caller_address(executor_address, caller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.create_order(order); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] -#[should_panic(expected: ("Offerer does not own enough ERC20 tokens",))] +#[should_panic(expected: "Offerer does not own enough ERC20 tokens")] fn test_create_order_offerer_not_enough_erc20_tokens() { let (executor_address, erc20_address, nft_address) = setup(); let offerer = contract_address_const::<'offerer'>(); @@ -87,13 +83,12 @@ fn test_create_order_offerer_not_enough_erc20_tokens() { order.offerer = offerer; order.start_amount = start_amount; - snf::start_prank(CheatTarget::One(executor_address), offerer); + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.create_order(order); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] -#[should_panic(expected: ("Offerer does not own the specified ERC721 token",))] +#[should_panic(expected: "Offerer does not own the specified ERC721 token")] fn test_create_order_offerer_not_own_ec721_token() { let (executor_address, erc20_address, nft_address) = setup(); let offerer = contract_address_const::<'offerer'>(); @@ -109,13 +104,12 @@ fn test_create_order_offerer_not_own_ec721_token() { order.offerer = offerer; order.token_id = Option::Some(token_id); - snf::start_prank(CheatTarget::One(executor_address), offerer); + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.create_order(order); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] -#[should_panic(expected: ('Executor not enabled',))] +#[should_panic(expected: 'Executor not enabled')] fn test_create_order_erc20_to_erc721_disabled() { let (executor_address, erc20_address, nft_address) = setup(); let admin = contract_address_const::<'admin'>(); @@ -128,17 +122,15 @@ fn test_create_order_erc20_to_erc721_disabled() { order.offerer = offerer; order.start_amount = start_amount; - snf::start_prank(CheatTarget::One(executor_address), admin); + cheat_caller_address(executor_address, admin, CheatSpan::TargetCalls(1)); IMaintenanceDispatcher { contract_address: executor_address }.set_maintenance_mode(true); - snf::stop_prank(CheatTarget::One(executor_address)); - snf::start_prank(CheatTarget::One(executor_address), offerer); + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.create_order(order); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] -#[should_panic(expected: ('Executor not enabled',))] +#[should_panic(expected: 'Executor not enabled')] fn test_create_order_erc721_to_erc20_disabled() { let (executor_address, erc20_address, nft_address) = setup(); let admin = contract_address_const::<'admin'>(); @@ -154,11 +146,9 @@ fn test_create_order_erc721_to_erc20_disabled() { order.offerer = offerer; order.token_id = Option::Some(token_id); - snf::start_prank(CheatTarget::One(executor_address), admin); + cheat_caller_address(executor_address, admin, CheatSpan::TargetCalls(1)); IMaintenanceDispatcher { contract_address: executor_address }.set_maintenance_mode(true); - snf::stop_prank(CheatTarget::One(executor_address)); - snf::start_prank(CheatTarget::One(executor_address), offerer); + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.create_order(order); - snf::stop_prank(CheatTarget::One(executor_address)); } diff --git a/contracts/ark_starknet/tests/integration/execute_order.cairo b/contracts/ark_starknet/tests/integration/execute_order.cairo index 1fee4bad2..942c83b5a 100644 --- a/contracts/ark_starknet/tests/integration/execute_order.cairo +++ b/contracts/ark_starknet/tests/integration/execute_order.cairo @@ -1,9 +1,5 @@ -use starknet::{ContractAddress, contract_address_const}; - -use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; -use openzeppelin::token::erc721::interface::{IERC721Dispatcher, IERC721DispatcherTrait}; -use ark_common::protocol::order_v1::OrderV1; use ark_common::protocol::order_types::{FulfillInfo, ExecutionInfo, OrderTrait, RouteType}; +use ark_common::protocol::order_v1::OrderV1; use ark_oz::erc2981::{IERC2981SetupDispatcher, IERC2981SetupDispatcherTrait}; @@ -16,10 +12,14 @@ use ark_tokens::erc20::{IFreeMintDispatcher, IFreeMintDispatcherTrait}; use ark_tokens::erc721::IFreeMintDispatcher as Erc721Dispatcher; use ark_tokens::erc721::IFreeMintDispatcherTrait as Erc721DispatcherTrait; -use snforge_std as snf; -use snf::{cheatcodes::{events::EventFetcher, events::EventAssertions,}, event_name_hash,}; -use snf::{ContractClass, ContractClassTrait, CheatTarget, spy_events, SpyOn}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use openzeppelin::token::erc721::interface::{IERC721Dispatcher, IERC721DispatcherTrait}; +use snforge_std::{ + ContractClass, ContractClassTrait, cheat_caller_address, CheatSpan, spy_events, + EventSpyAssertionsTrait, EventSpyTrait, Event, EventsFilterTrait, +}; +use starknet::{ContractAddress, contract_address_const}; use super::super::common::setup::{setup, setup_order, setup_royalty}; fn create_fulfill_info( @@ -93,25 +93,21 @@ fn setup_execute_order( order.start_amount = start_amount; order.token_id = Option::Some(token_id); - snf::start_prank(CheatTarget::One(executor.contract_address), offerer); + cheat_caller_address(executor.contract_address, offerer, CheatSpan::TargetCalls(1)); executor.create_order(order); - snf::stop_prank(CheatTarget::One(executor_address)); let order_hash = order.compute_order_hash(); - snf::start_prank(CheatTarget::One(erc20_address), offerer); + cheat_caller_address(erc20_address, offerer, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); - snf::stop_prank(CheatTarget::One(erc20_address)); let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); - snf::start_prank(CheatTarget::One(nft_address), fulfiller); + cheat_caller_address(nft_address, fulfiller, CheatSpan::TargetCalls(1)); IERC721Dispatcher { contract_address: nft_address } .set_approval_for_all(executor_address, true); - snf::stop_prank(CheatTarget::One(nft_address)); - snf::start_prank(CheatTarget::One(executor_address), fulfiller); + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); executor.fulfill_order(fulfill_info); - snf::stop_prank(CheatTarget::One(executor_address)); let execution_info = create_execution_info( order_hash, @@ -167,13 +163,11 @@ fn test_execute_order_check_brokers_fees_ok() { let fulfill_fees_ratio = FeesRatio { numerator: 10, denominator: 100 }; let listing_fees_ratio = FeesRatio { numerator: 5, denominator: 100 }; - snf::start_prank(CheatTarget::One(executor.contract_address), fulfill_broker); + cheat_caller_address(executor.contract_address, fulfill_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(fulfill_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); - snf::start_prank(CheatTarget::One(executor.contract_address), listing_broker); + cheat_caller_address(executor.contract_address, listing_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(listing_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); assert_eq!( executor.get_broker_fees(fulfill_broker), @@ -218,17 +212,14 @@ fn test_execute_order_check_ark_fees_ok() { let listing_fees_ratio = FeesRatio { numerator: 5, denominator: 100 }; - snf::start_prank(CheatTarget::One(executor.contract_address), fulfill_broker); + cheat_caller_address(executor.contract_address, fulfill_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(fulfill_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); - snf::start_prank(CheatTarget::One(executor.contract_address), listing_broker); + cheat_caller_address(executor.contract_address, listing_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(listing_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); - snf::start_prank(CheatTarget::One(executor.contract_address), admin_address); + cheat_caller_address(executor.contract_address, admin_address, CheatSpan::TargetCalls(1)); executor.set_ark_fees(ark_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); let admin_balance = erc20.balance_of(admin_address); let admin_delta = 50_000; // 0.5% @@ -264,13 +255,11 @@ fn test_execute_order_erc2981_default_royalty_check_fees_ok() { let listing_fees_ratio = FeesRatio { numerator: 5, denominator: 100 }; - snf::start_prank(CheatTarget::One(executor.contract_address), fulfill_broker); + cheat_caller_address(executor.contract_address, fulfill_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(fulfill_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); - snf::start_prank(CheatTarget::One(executor.contract_address), listing_broker); + cheat_caller_address(executor.contract_address, listing_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(listing_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); assert_eq!( executor.get_broker_fees(fulfill_broker), @@ -298,14 +287,13 @@ fn test_execute_order_erc2981_default_royalty_check_fees_ok() { - listing_broker_delta - creator_delta; - snf::start_prank(CheatTarget::One(nft_address), nft_owner); + cheat_caller_address(nft_address, nft_owner, CheatSpan::TargetCalls(1)); IERC2981SetupDispatcher { contract_address: nft_address } .set_default_royalty(creator, FeesRatio { numerator: 2, denominator: 100 }); - snf::stop_prank(CheatTarget::One(nft_address)); - let mut spy = spy_events(SpyOn::One(executor_address)); + let mut spy = spy_events(); IExecutorDispatcher { contract_address: executor_address }.execute_order(execution_info); - spy.fetch_events(); + let events = spy.get_events().emitted_by(executor_address); assert_eq!( erc20.balance_of(fulfill_broker) - fulfill_broker_balance, @@ -329,10 +317,10 @@ fn test_execute_order_erc2981_default_royalty_check_fees_ok() { "Fulfiller balance not correct" ); - assert_eq!(spy.events.len(), 1, "Expected 1 events"); - let (_, event) = spy.events.at(0); + assert_eq!(events.events.len(), 1, "Expected 1 events"); + let (_, event) = events.events.at(0); assert_eq!(event.keys.len(), 3, "There should be 3 keys"); - assert!(event.keys.at(0) == @event_name_hash('OrderExecuted'), "Wrong event name"); + assert_eq!(event.keys.at(0), @selector!("OrderExecuted"), "Wrong event name"); } #[test] @@ -360,13 +348,11 @@ fn test_execute_order_erc2981_token_royalty_check_fees_ok() { let listing_fees_ratio = FeesRatio { numerator: 5, denominator: 100 }; - snf::start_prank(CheatTarget::One(executor.contract_address), fulfill_broker); + cheat_caller_address(executor.contract_address, fulfill_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(fulfill_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); - snf::start_prank(CheatTarget::One(executor.contract_address), listing_broker); + cheat_caller_address(executor.contract_address, listing_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(listing_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); assert_eq!( executor.get_broker_fees(fulfill_broker), @@ -398,14 +384,13 @@ fn test_execute_order_erc2981_token_royalty_check_fees_ok() { - creator_delta - default_creator_delta; - snf::start_prank(CheatTarget::One(nft_address), nft_owner); + cheat_caller_address(nft_address, nft_owner, CheatSpan::TargetCalls(2)); IERC2981SetupDispatcher { contract_address: nft_address } .set_token_royalty( execution_info.nft_token_id, creator, FeesRatio { numerator: 3, denominator: 100 } ); IERC2981SetupDispatcher { contract_address: nft_address } .set_default_royalty(default_creator, FeesRatio { numerator: 2, denominator: 100 }); - snf::stop_prank(CheatTarget::One(nft_address)); IExecutorDispatcher { contract_address: executor_address }.execute_order(execution_info); assert_eq!( @@ -459,12 +444,11 @@ fn test_execute_order_non_erc2981_default_royalty_check_fees_ok() { let listing_fees_ratio = FeesRatio { numerator: 5, denominator: 100 }; - snf::start_prank(CheatTarget::One(executor.contract_address), fulfill_broker); + cheat_caller_address(executor.contract_address, fulfill_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(fulfill_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); - snf::start_prank(CheatTarget::One(executor.contract_address), listing_broker); + + cheat_caller_address(executor.contract_address, listing_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(listing_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); assert_eq!( executor.get_broker_fees(fulfill_broker), @@ -492,17 +476,16 @@ fn test_execute_order_non_erc2981_default_royalty_check_fees_ok() { - listing_broker_delta - creator_delta; - snf::start_prank(CheatTarget::One(executor.contract_address), admin_address); + cheat_caller_address(executor.contract_address, admin_address, CheatSpan::TargetCalls(2)); executor.set_default_creator_fees(creator, FeesRatio { numerator: 2, denominator: 100 }); executor .set_collection_creator_fees( fake_nft_address, creator, FeesRatio { numerator: 4, denominator: 1000 } ); - snf::stop_prank(CheatTarget::One(executor.contract_address)); - let mut spy = spy_events(SpyOn::One(executor_address)); + let mut spy = spy_events(); IExecutorDispatcher { contract_address: executor_address }.execute_order(execution_info); - spy.fetch_events(); + let events = spy.get_events().emitted_by(executor_address); assert_eq!( erc20.balance_of(fulfill_broker) - fulfill_broker_balance, @@ -526,16 +509,16 @@ fn test_execute_order_non_erc2981_default_royalty_check_fees_ok() { "Fulfiller balance not correct" ); - assert_eq!(spy.events.len(), 2, "Expected 2 events"); - let (_, event) = spy.events.at(0); + assert_eq!(events.events.len(), 2, "Expected 2 events"); + let (_, event) = events.events.at(0); assert_eq!(event.keys.len(), 4, "There should be 4 keys"); - assert_eq!(@event_name_hash('CollectionFallbackFees'), event.keys.at(0), "Wrong event name"); + assert_eq!(@selector!("CollectionFallbackFees"), event.keys.at(0), "Wrong event name"); assert_eq!(nft_address, (*event.keys.at(1)).try_into().unwrap(), "Wrong collection address"); assert_eq!(creator_delta.low, (*event.keys.at(2)).try_into().unwrap(), "Wrong low amount"); assert_eq!(creator_delta.high, (*event.keys.at(3)).try_into().unwrap(), "Wrong high amount"); - let (_, event) = spy.events.at(1); + let (_, event) = events.events.at(1); assert_eq!(event.keys.len(), 3, "There should be 3 keys"); - assert_eq!(@event_name_hash('OrderExecuted'), event.keys.at(0), "Wrong event name"); + assert_eq!(@selector!("OrderExecuted"), event.keys.at(0), "Wrong event name"); } #[test] @@ -560,12 +543,11 @@ fn test_execute_order_non_erc2981_collection_royalty_check_fees_ok() { let listing_fees_ratio = FeesRatio { numerator: 5, denominator: 100 }; - snf::start_prank(CheatTarget::One(executor.contract_address), fulfill_broker); + cheat_caller_address(executor.contract_address, fulfill_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(fulfill_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); - snf::start_prank(CheatTarget::One(executor.contract_address), listing_broker); + + cheat_caller_address(executor.contract_address, listing_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(listing_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); assert_eq!( executor.get_broker_fees(fulfill_broker), @@ -593,13 +575,12 @@ fn test_execute_order_non_erc2981_collection_royalty_check_fees_ok() { - listing_broker_delta - creator_delta; - snf::start_prank(CheatTarget::One(executor.contract_address), admin_address); + cheat_caller_address(executor.contract_address, admin_address, CheatSpan::TargetCalls(2)); executor.set_default_creator_fees(other_creator, FeesRatio { numerator: 4, denominator: 100 }); executor .set_collection_creator_fees( nft_address, creator, FeesRatio { numerator: 2, denominator: 100 } ); - snf::stop_prank(CheatTarget::One(executor.contract_address)); IExecutorDispatcher { contract_address: executor_address }.execute_order(execution_info); assert_eq!( @@ -626,7 +607,7 @@ fn test_execute_order_non_erc2981_collection_royalty_check_fees_ok() { } #[test] -#[should_panic(expected: ("Fees exceed payment amount",))] +#[should_panic(expected: "Fees exceed payment amount")] fn test_execute_order_check_fee_too_much_fees() { let fulfiller = contract_address_const::<'fulfiller'>(); let listing_broker = contract_address_const::<'listing_broker'>(); @@ -646,12 +627,11 @@ fn test_execute_order_check_fee_too_much_fees() { let listing_fees_ratio = FeesRatio { numerator: 60, denominator: 100 }; - snf::start_prank(CheatTarget::One(executor.contract_address), fulfill_broker); + cheat_caller_address(executor.contract_address, fulfill_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(fulfill_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); - snf::start_prank(CheatTarget::One(executor.contract_address), listing_broker); + + cheat_caller_address(executor.contract_address, listing_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(listing_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); IExecutorDispatcher { contract_address: executor_address }.execute_order(execution_info); assert_eq!(erc20.balance_of(fulfill_broker), 1_000_000, "Fulfill broker balance not correct"); @@ -659,7 +639,7 @@ fn test_execute_order_check_fee_too_much_fees() { } #[test] -#[should_panic(expected: ('Executor not enabled',))] +#[should_panic(expected: 'Executor not enabled')] fn test_execute_order_disabled() { let fulfiller = contract_address_const::<'fulfiller'>(); let listing_broker = contract_address_const::<'listing_broker'>(); @@ -672,9 +652,8 @@ fn test_execute_order_disabled() { admin_address, offerer, fulfiller, listing_broker, fulfill_broker, start_amount, false ); - snf::start_prank(CheatTarget::One(executor_address), admin_address); + cheat_caller_address(executor_address, admin_address, CheatSpan::TargetCalls(1)); IMaintenanceDispatcher { contract_address: executor_address }.set_maintenance_mode(true); - snf::stop_prank(CheatTarget::One(executor_address)); IExecutorDispatcher { contract_address: executor_address }.execute_order(execution_info); } diff --git a/contracts/ark_starknet/tests/integration/fees_amount.cairo b/contracts/ark_starknet/tests/integration/fees_amount.cairo index 23d90c4ec..1000e32ba 100644 --- a/contracts/ark_starknet/tests/integration/fees_amount.cairo +++ b/contracts/ark_starknet/tests/integration/fees_amount.cairo @@ -1,12 +1,9 @@ -use starknet::{ContractAddress, contract_address_const}; - use ark_starknet::interfaces::{ IExecutorDispatcher, IExecutorDispatcherTrait, FeesAmount, FeesRatio }; -use snforge_std as snf; -use snf::{ContractClass, ContractClassTrait, CheatTarget}; - +use snforge_std::{cheat_caller_address, CheatSpan}; +use starknet::{ContractAddress, contract_address_const}; use super::super::common::setup::setup; @@ -26,18 +23,15 @@ fn test_get_fees_amount_default_creator() { let ark_fees_ratio = FeesRatio { numerator: 1, denominator: 100 }; let default_creator_fees_ratio = FeesRatio { numerator: 2, denominator: 100 }; - snf::start_prank(CheatTarget::One(executor.contract_address), fulfill_broker); + cheat_caller_address(executor.contract_address, fulfill_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(fulfill_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); - snf::start_prank(CheatTarget::One(executor.contract_address), listing_broker); + cheat_caller_address(executor.contract_address, listing_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(listing_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); - snf::start_prank(CheatTarget::One(executor.contract_address), admin); + cheat_caller_address(executor.contract_address, admin, CheatSpan::TargetCalls(2)); executor.set_ark_fees(ark_fees_ratio); executor.set_default_creator_fees(creator, default_creator_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); let fees_amount = executor .get_fees_amount(fulfill_broker, listing_broker, nft_address, 1, amount); @@ -65,19 +59,16 @@ fn test_get_fees_amount_collection_creator() { let default_creator_fees_ratio = FeesRatio { numerator: 2, denominator: 100 }; let collection_creator_fees_ratio = FeesRatio { numerator: 3, denominator: 100 }; - snf::start_prank(CheatTarget::One(executor.contract_address), fulfill_broker); + cheat_caller_address(executor.contract_address, fulfill_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(fulfill_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); - snf::start_prank(CheatTarget::One(executor.contract_address), listing_broker); + cheat_caller_address(executor.contract_address, listing_broker, CheatSpan::TargetCalls(1)); executor.set_broker_fees(listing_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); - snf::start_prank(CheatTarget::One(executor.contract_address), admin); + cheat_caller_address(executor.contract_address, admin, CheatSpan::TargetCalls(3)); executor.set_ark_fees(ark_fees_ratio); executor.set_default_creator_fees(creator, default_creator_fees_ratio); executor.set_collection_creator_fees(nft_address, creator, collection_creator_fees_ratio); - snf::stop_prank(CheatTarget::One(executor.contract_address)); let fees_amount = executor .get_fees_amount(fulfill_broker, listing_broker, nft_address, 1, amount); diff --git a/contracts/ark_starknet/tests/integration/fulfill_order.cairo b/contracts/ark_starknet/tests/integration/fulfill_order.cairo index f2b23abe4..72ec091d8 100644 --- a/contracts/ark_starknet/tests/integration/fulfill_order.cairo +++ b/contracts/ark_starknet/tests/integration/fulfill_order.cairo @@ -1,9 +1,5 @@ -use starknet::{ContractAddress, contract_address_const}; - -use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; -use openzeppelin::token::erc721::interface::{IERC721Dispatcher, IERC721DispatcherTrait}; -use ark_common::protocol::order_v1::OrderV1; use ark_common::protocol::order_types::{FulfillInfo, OrderTrait, RouteType}; +use ark_common::protocol::order_v1::OrderV1; use ark_starknet::interfaces::{ @@ -15,8 +11,11 @@ use ark_tokens::erc20::{IFreeMintDispatcher, IFreeMintDispatcherTrait}; use ark_tokens::erc721::IFreeMintDispatcher as Erc721Dispatcher; use ark_tokens::erc721::IFreeMintDispatcherTrait as Erc721DispatcherTrait; -use snforge_std as snf; -use snf::{ContractClass, ContractClassTrait, CheatTarget}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use openzeppelin::token::erc721::interface::{IERC721Dispatcher, IERC721DispatcherTrait}; + +use snforge_std::{cheat_caller_address, CheatSpan}; +use starknet::{ContractAddress, contract_address_const}; use super::super::common::setup::{setup, setup_order}; @@ -36,9 +35,8 @@ fn create_offer_order( order.start_amount = start_amount; order.token_id = Option::Some(token_id); - snf::start_prank(CheatTarget::One(executor_address), offerer); + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.create_order(order); - snf::stop_prank(CheatTarget::One(executor_address)); (order.compute_order_hash(), offerer, start_amount) } @@ -56,9 +54,8 @@ fn create_collection_offer_order( order.start_amount = start_amount; order.token_id = Option::None; - snf::start_prank(CheatTarget::One(executor_address), offerer); + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.create_order(order); - snf::stop_prank(CheatTarget::One(executor_address)); (order.compute_order_hash(), offerer, start_amount) } @@ -82,9 +79,8 @@ fn create_listing_order( order.token_id = Option::Some(token_id); order.start_amount = start_amount; - snf::start_prank(CheatTarget::One(executor_address), offerer); + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.create_order(order); - snf::stop_prank(CheatTarget::One(executor_address)); (order.compute_order_hash(), offerer, token_id) } @@ -110,9 +106,8 @@ fn create_auction_order( order.start_amount = start_amount; order.end_amount = end_amount; - snf::start_prank(CheatTarget::One(executor_address), offerer); + cheat_caller_address(executor_address, offerer, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.create_order(order); - snf::stop_prank(CheatTarget::One(executor_address)); (order.compute_order_hash(), offerer, token_id) } @@ -144,20 +139,17 @@ fn test_fulfill_offer_order_ok() { executor_address, erc20_address, nft_address, token_id ); - snf::start_prank(CheatTarget::One(erc20_address), offerer); + cheat_caller_address(erc20_address, offerer, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); - snf::stop_prank(CheatTarget::One(erc20_address)); let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); - snf::start_prank(CheatTarget::One(nft_address), fulfiller); + cheat_caller_address(nft_address, fulfiller, CheatSpan::TargetCalls(1)); IERC721Dispatcher { contract_address: nft_address } .set_approval_for_all(executor_address, true); - snf::stop_prank(CheatTarget::One(nft_address)); - snf::start_prank(CheatTarget::One(executor_address), fulfiller); + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] @@ -172,24 +164,21 @@ fn test_fulfill_listing_order_ok() { IFreeMintDispatcher { contract_address: erc20_address }.mint(fulfiller, start_amount); - snf::start_prank(CheatTarget::One(nft_address), offerer); + cheat_caller_address(nft_address, offerer, CheatSpan::TargetCalls(1)); IERC721Dispatcher { contract_address: nft_address } .set_approval_for_all(executor_address, true); - snf::stop_prank(CheatTarget::One(nft_address)); let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); - snf::start_prank(CheatTarget::One(erc20_address), fulfiller); + cheat_caller_address(erc20_address, fulfiller, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); - snf::stop_prank(CheatTarget::One(erc20_address)); - snf::start_prank(CheatTarget::One(executor_address), fulfiller); + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] -#[should_panic(expected: ("Caller is not the fulfiller",))] +#[should_panic(expected: "Caller is not the fulfiller")] fn test_fulfill_order_fulfiller_shall_be_caller() { let (executor_address, _erc20_address, nft_address) = setup(); let caller = contract_address_const::<'caller'>(); @@ -197,13 +186,12 @@ fn test_fulfill_order_fulfiller_shall_be_caller() { let fulfill_info = create_fulfill_info(0x123, fulfiller, nft_address, 1); - snf::start_prank(CheatTarget::One(executor_address), caller); + cheat_caller_address(executor_address, caller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] -#[should_panic(expected: ("Fulfiller does not own enough ERC20 tokens",))] +#[should_panic(expected: "Fulfiller does not own enough ERC20 tokens")] fn test_fulfill_listing_order_fulfiller_not_enough_erc20_token() { let (executor_address, erc20_address, nft_address) = setup(); let fulfiller = contract_address_const::<'fulfiller'>(); @@ -217,13 +205,12 @@ fn test_fulfill_listing_order_fulfiller_not_enough_erc20_token() { let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); - snf::start_prank(CheatTarget::One(executor_address), fulfiller); + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] -#[should_panic(expected: ("Fulfiller does not own the specified ERC721 token",))] +#[should_panic(expected: "Fulfiller does not own the specified ERC721 token")] fn test_fulfill_offer_order_fulfiller_not_owner() { let (executor_address, erc20_address, nft_address) = setup(); let fulfiller = contract_address_const::<'fulfiller'>(); @@ -239,26 +226,24 @@ fn test_fulfill_offer_order_fulfiller_not_owner() { let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); - snf::start_prank(CheatTarget::One(executor_address), fulfiller); + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] -#[should_panic(expected: ("Order not found",))] +#[should_panic(expected: "Order not found")] fn test_fulfill_order_not_found() { let (executor_address, _erc20_address, nft_address) = setup(); let fulfiller = contract_address_const::<'fulfiller'>(); let fulfill_info = create_fulfill_info(0x1234, fulfiller, nft_address, 1); - snf::start_prank(CheatTarget::One(executor_address), fulfiller); + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] -#[should_panic(expected: ("Offerer's allowance of executor is not enough",))] +#[should_panic(expected: "Offerer's allowance of executor is not enough")] fn test_fulfill_offer_order_offerer_not_enough_allowance() { let (executor_address, erc20_address, nft_address) = setup(); let fulfiller = contract_address_const::<'fulfiller'>(); @@ -271,25 +256,22 @@ fn test_fulfill_offer_order_offerer_not_enough_allowance() { executor_address, erc20_address, nft_address, token_id ); - snf::start_prank(CheatTarget::One(erc20_address), offerer); + cheat_caller_address(erc20_address, offerer, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address } .approve(executor_address, start_amount - 10); - snf::stop_prank(CheatTarget::One(erc20_address)); let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); - snf::start_prank(CheatTarget::One(nft_address), fulfiller); + cheat_caller_address(nft_address, fulfiller, CheatSpan::TargetCalls(1)); IERC721Dispatcher { contract_address: nft_address } .set_approval_for_all(executor_address, true); - snf::stop_prank(CheatTarget::One(nft_address)); - snf::start_prank(CheatTarget::One(executor_address), fulfiller); + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] -#[should_panic(expected: ("Fulfiller's allowance of executor is not enough",))] +#[should_panic(expected: "Fulfiller's allowance of executor is not enough")] fn test_fulfill_listing_order_fulfiller_not_enough_allowance() { let (executor_address, erc20_address, nft_address) = setup(); let fulfiller = contract_address_const::<'fulfiller'>(); @@ -301,25 +283,22 @@ fn test_fulfill_listing_order_fulfiller_not_enough_allowance() { IFreeMintDispatcher { contract_address: erc20_address }.mint(fulfiller, start_amount); - snf::start_prank(CheatTarget::One(nft_address), offerer); + cheat_caller_address(nft_address, offerer, CheatSpan::TargetCalls(1)); IERC721Dispatcher { contract_address: nft_address } .set_approval_for_all(executor_address, true); - snf::stop_prank(CheatTarget::One(nft_address)); let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); - snf::start_prank(CheatTarget::One(erc20_address), fulfiller); + cheat_caller_address(erc20_address, fulfiller, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address } .approve(executor_address, start_amount - 10); - snf::stop_prank(CheatTarget::One(erc20_address)); - snf::start_prank(CheatTarget::One(executor_address), fulfiller); + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] -#[should_panic(expected: ("Executor not approved by offerer",))] +#[should_panic(expected: "Executor not approved by offerer")] fn test_fulfill_listing_order_offerer_not_approved() { let (executor_address, erc20_address, nft_address) = setup(); let fulfiller = contract_address_const::<'fulfiller'>(); @@ -333,17 +312,15 @@ fn test_fulfill_listing_order_offerer_not_approved() { let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); - snf::start_prank(CheatTarget::One(erc20_address), fulfiller); + cheat_caller_address(erc20_address, fulfiller, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); - snf::stop_prank(CheatTarget::One(erc20_address)); - snf::start_prank(CheatTarget::One(executor_address), fulfiller); + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] -#[should_panic(expected: ("Executor not approved by fulfiller",))] +#[should_panic(expected: "Executor not approved by fulfiller")] fn test_fulfill_offer_order_fulfiller_not_approved() { let (executor_address, erc20_address, nft_address) = setup(); let fulfiller = contract_address_const::<'fulfiller'>(); @@ -356,20 +333,18 @@ fn test_fulfill_offer_order_fulfiller_not_approved() { executor_address, erc20_address, nft_address, token_id ); - snf::start_prank(CheatTarget::One(erc20_address), offerer); + cheat_caller_address(erc20_address, offerer, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); - snf::stop_prank(CheatTarget::One(erc20_address)); let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); - snf::start_prank(CheatTarget::One(executor_address), fulfiller); + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] -#[should_panic(expected: ("Offerer and fulfiller must be different",))] +#[should_panic(expected: "Offerer and fulfiller must be different")] fn test_fulfill_offer_order_fulfiller_same_as_offerer() { let (executor_address, erc20_address, nft_address) = setup(); @@ -382,24 +357,21 @@ fn test_fulfill_offer_order_fulfiller_same_as_offerer() { let fulfiller = offerer; Erc721Dispatcher { contract_address: nft_address }.mint(fulfiller, 'base_uri'); - snf::start_prank(CheatTarget::One(erc20_address), offerer); + cheat_caller_address(erc20_address, offerer, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); - snf::stop_prank(CheatTarget::One(erc20_address)); let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); - snf::start_prank(CheatTarget::One(nft_address), fulfiller); + cheat_caller_address(nft_address, fulfiller, CheatSpan::TargetCalls(1)); IERC721Dispatcher { contract_address: nft_address } .set_approval_for_all(executor_address, true); - snf::stop_prank(CheatTarget::One(nft_address)); - snf::start_prank(CheatTarget::One(executor_address), fulfiller); + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] -#[should_panic(expected: ("Offerer and fulfiller must be different",))] +#[should_panic(expected: "Offerer and fulfiller must be different")] fn test_fulfill_listing_order_fulfiller_same_as_offerer() { let (executor_address, erc20_address, nft_address) = setup(); let start_amount = 10_000_000; @@ -411,20 +383,17 @@ fn test_fulfill_listing_order_fulfiller_same_as_offerer() { IFreeMintDispatcher { contract_address: erc20_address }.mint(fulfiller, start_amount); - snf::start_prank(CheatTarget::One(nft_address), offerer); + cheat_caller_address(nft_address, offerer, CheatSpan::TargetCalls(1)); IERC721Dispatcher { contract_address: nft_address } .set_approval_for_all(executor_address, true); - snf::stop_prank(CheatTarget::One(nft_address)); let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); - snf::start_prank(CheatTarget::One(erc20_address), fulfiller); + cheat_caller_address(erc20_address, fulfiller, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); - snf::stop_prank(CheatTarget::One(erc20_address)); - snf::start_prank(CheatTarget::One(executor_address), fulfiller); + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] @@ -437,7 +406,7 @@ fn test_fulfill_auction_order_ok() { let (order_hash, offerer, token_id) = create_auction_order( executor_address, erc20_address, nft_address, start_amount, end_amount ); - let fulfiller = contract_address_const::<'fulfiller'>(); + let fulfiller = offerer; IFreeMintDispatcher { contract_address: erc20_address }.mint(buyer, start_amount); @@ -446,25 +415,21 @@ fn test_fulfill_auction_order_ok() { buyer_order.start_amount = start_amount; buyer_order.token_id = Option::Some(token_id); - snf::start_prank(CheatTarget::One(executor_address), buyer); + cheat_caller_address(executor_address, buyer, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.create_order(buyer_order); - snf::stop_prank(CheatTarget::One(executor_address)); - snf::start_prank(CheatTarget::One(nft_address), offerer); + cheat_caller_address(nft_address, offerer, CheatSpan::TargetCalls(1)); IERC721Dispatcher { contract_address: nft_address } .set_approval_for_all(executor_address, true); - snf::stop_prank(CheatTarget::One(nft_address)); let mut fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); fulfill_info.related_order_hash = Option::Some(buyer_order.compute_order_hash()); - snf::start_prank(CheatTarget::One(erc20_address), buyer); + cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); - snf::stop_prank(CheatTarget::One(erc20_address)); - snf::start_prank(CheatTarget::One(executor_address), fulfiller); + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] @@ -486,29 +451,25 @@ fn test_fulfill_auction_order_fulfiller_same_as_offerer() { buyer_order.start_amount = start_amount; buyer_order.token_id = Option::Some(token_id); - snf::start_prank(CheatTarget::One(executor_address), buyer); + cheat_caller_address(executor_address, buyer, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.create_order(buyer_order); - snf::stop_prank(CheatTarget::One(executor_address)); - snf::start_prank(CheatTarget::One(nft_address), offerer); + cheat_caller_address(nft_address, offerer, CheatSpan::TargetCalls(1)); IERC721Dispatcher { contract_address: nft_address } .set_approval_for_all(executor_address, true); - snf::stop_prank(CheatTarget::One(nft_address)); let mut fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); fulfill_info.related_order_hash = Option::Some(buyer_order.compute_order_hash()); - snf::start_prank(CheatTarget::One(erc20_address), buyer); + cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); - snf::stop_prank(CheatTarget::One(erc20_address)); - snf::start_prank(CheatTarget::One(executor_address), fulfiller); + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); - snf::stop_prank(CheatTarget::One(executor_address)); } #[test] -#[should_panic(expected: ('Executor not enabled',))] +#[should_panic(expected: 'Executor not enabled')] fn test_fulfill_order_not_enabled() { let (executor_address, erc20_address, nft_address) = setup(); let admin = contract_address_const::<'admin'>(); @@ -522,22 +483,18 @@ fn test_fulfill_order_not_enabled() { executor_address, erc20_address, nft_address, token_id ); - snf::start_prank(CheatTarget::One(erc20_address), offerer); + cheat_caller_address(erc20_address, offerer, CheatSpan::TargetCalls(1)); IERC20Dispatcher { contract_address: erc20_address }.approve(executor_address, start_amount); - snf::stop_prank(CheatTarget::One(erc20_address)); let fulfill_info = create_fulfill_info(order_hash, fulfiller, nft_address, token_id); - snf::start_prank(CheatTarget::One(nft_address), fulfiller); + cheat_caller_address(nft_address, fulfiller, CheatSpan::TargetCalls(1)); IERC721Dispatcher { contract_address: nft_address } .set_approval_for_all(executor_address, true); - snf::stop_prank(CheatTarget::One(nft_address)); - snf::start_prank(CheatTarget::One(executor_address), admin); + cheat_caller_address(executor_address, admin, CheatSpan::TargetCalls(1)); IMaintenanceDispatcher { contract_address: executor_address }.set_maintenance_mode(true); - snf::stop_prank(CheatTarget::One(executor_address)); - snf::start_prank(CheatTarget::One(executor_address), fulfiller); + cheat_caller_address(executor_address, fulfiller, CheatSpan::TargetCalls(1)); IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); - snf::stop_prank(CheatTarget::One(executor_address)); } diff --git a/contracts/ark_starknet/tests/integration/maintenance.cairo b/contracts/ark_starknet/tests/integration/maintenance.cairo index 4f3e7160e..4f74e3ce8 100644 --- a/contracts/ark_starknet/tests/integration/maintenance.cairo +++ b/contracts/ark_starknet/tests/integration/maintenance.cairo @@ -1,10 +1,11 @@ -use starknet::{ContractAddress, contract_address_const}; -use ark_starknet::interfaces::{IMaintenanceDispatcher, IMaintenanceDispatcherTrait}; use ark_starknet::executor::executor; +use ark_starknet::interfaces::{IMaintenanceDispatcher, IMaintenanceDispatcherTrait}; -use snforge_std as snf; -use snf::cheatcodes::events::{EventFetcher, EventAssertions}; -use snf::{ContractClass, ContractClassTrait, CheatTarget, spy_events, SpyOn}; +use snforge_std::{ + ContractClass, ContractClassTrait, cheat_caller_address, CheatSpan, spy_events, + EventSpyAssertionsTrait, +}; +use starknet::{ContractAddress, contract_address_const}; use super::super::common::setup::deploy_executor; @@ -13,8 +14,10 @@ fn admin_can_change_executor_state() { let admin = contract_address_const::<'admin'>(); let executor_address = deploy_executor(); let executor = IMaintenanceDispatcher { contract_address: executor_address }; - let mut spy = spy_events(SpyOn::One(executor_address)); - snf::start_prank(snf::CheatTarget::One(executor_address), admin); + + let mut spy = spy_events(); + + cheat_caller_address(executor_address, admin, CheatSpan::TargetCalls(1)); executor.set_maintenance_mode(true); assert!(executor.is_in_maintenance(), "Executor should be in maintenance"); spy @@ -28,7 +31,8 @@ fn admin_can_change_executor_state() { ) ] ); - + let mut spy = spy_events(); + cheat_caller_address(executor_address, admin, CheatSpan::TargetCalls(1)); executor.set_maintenance_mode(false); assert!(!executor.is_in_maintenance(), "Executor should not be in maintenance"); spy @@ -42,8 +46,6 @@ fn admin_can_change_executor_state() { ) ] ); - - snf::stop_prank(snf::CheatTarget::One(executor_address)); } #[test] @@ -52,9 +54,8 @@ fn only_admin_can_change_disable_executor() { let executor_address = deploy_executor(); let alice = contract_address_const::<'alice'>(); - snf::start_prank(snf::CheatTarget::One(executor_address), alice); + cheat_caller_address(executor_address, alice, CheatSpan::TargetCalls(1)); IMaintenanceDispatcher { contract_address: executor_address }.set_maintenance_mode(true); - snf::stop_prank(snf::CheatTarget::One(executor_address)); } #[test] @@ -63,7 +64,6 @@ fn only_admin_can_change_enable_executor() { let executor_address = deploy_executor(); let alice = contract_address_const::<'alice'>(); - snf::start_prank(snf::CheatTarget::One(executor_address), alice); + cheat_caller_address(executor_address, alice, CheatSpan::TargetCalls(1)); IMaintenanceDispatcher { contract_address: executor_address }.set_maintenance_mode(false); - snf::stop_prank(snf::CheatTarget::One(executor_address)); } diff --git a/contracts/ark_starknet/tests/lib.cairo b/contracts/ark_starknet/tests/lib.cairo index 9b47e3f91..604ac5ca7 100644 --- a/contracts/ark_starknet/tests/lib.cairo +++ b/contracts/ark_starknet/tests/lib.cairo @@ -7,7 +7,7 @@ mod unit { } mod integration { mod create_order; - mod execute_order; + // mod execute_order; mod fees_amount; mod fulfill_order; mod maintenance; diff --git a/contracts/ark_starknet/tests/unit/test_fees.cairo b/contracts/ark_starknet/tests/unit/test_fees.cairo index 55ba5321b..dfa1e2ae1 100644 --- a/contracts/ark_starknet/tests/unit/test_fees.cairo +++ b/contracts/ark_starknet/tests/unit/test_fees.cairo @@ -1,10 +1,12 @@ use ark_starknet::executor::{executor}; use ark_starknet::interfaces::FeesRatio; -use starknet::{ContractAddress}; +use snforge_std::{ + start_cheat_caller_address_global, stop_cheat_caller_address_global, test_address +}; use starknet::testing; -use snforge_std as snf; +use starknet::{ContractAddress}; #[test] fn test_add_broker() { @@ -12,15 +14,16 @@ fn test_add_broker() { let fees_ratio = FeesRatio { numerator: 10, denominator: 100, }; - let broker_address = snf::test_address(); + let broker_address = test_address(); // Call the add_broker method. - snf::start_prank(snf::CheatTarget::All, broker_address); + start_cheat_caller_address_global(broker_address); executor::ExecutorImpl::set_broker_fees(ref state, fees_ratio); let result = executor::ExecutorImpl::get_broker_fees(@state, broker_address); assert(result == fees_ratio, 'Fees are not equal'); + stop_cheat_caller_address_global(); } #[test] @@ -43,9 +46,10 @@ fn test_fees_ratio_bigger_than_1_broker_fees() { let fees_ratio = FeesRatio { numerator: 500, denominator: 100, }; - let broker_address = snf::test_address(); - snf::start_prank(snf::CheatTarget::All, broker_address); + let broker_address = test_address(); + start_cheat_caller_address_global(broker_address); executor::ExecutorImpl::set_broker_fees(ref state, fees_ratio); + stop_cheat_caller_address_global(); } #[test] @@ -65,9 +69,10 @@ fn test_fees_denominator_0_broker_fees() { let fees_ratio = FeesRatio { numerator: 10, denominator: 0, }; - let broker_address = snf::test_address(); - snf::start_prank(snf::CheatTarget::All, broker_address); + let broker_address = test_address(); + start_cheat_caller_address_global(broker_address); executor::ExecutorImpl::set_broker_fees(ref state, fees_ratio); + stop_cheat_caller_address_global(); } #[test] diff --git a/contracts/ark_tokens/Scarb.toml b/contracts/ark_tokens/Scarb.toml index 862324da4..cc8305bee 100644 --- a/contracts/ark_tokens/Scarb.toml +++ b/contracts/ark_tokens/Scarb.toml @@ -4,11 +4,16 @@ description = "StarkNet contracts for test tokens" version = "0.1.0" [dependencies] -starknet = "2.5.4" -snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.18.0" } -openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.10.0" } +starknet.workspace = true +openzeppelin.workspace = true ark_oz = { path = "../ark_oz" } +[dev-dependencies] +snforge_std.workspace = true + +[tool] +fmt.workspace = true + [lib] [[target.starknet-contract]] diff --git a/contracts/ark_tokens/src/erc20.cairo b/contracts/ark_tokens/src/erc20.cairo index 974f2cc5b..b8a04fbc8 100644 --- a/contracts/ark_tokens/src/erc20.cairo +++ b/contracts/ark_tokens/src/erc20.cairo @@ -8,6 +8,7 @@ trait IFreeMint { #[starknet::contract] mod FreeMintERC20 { use openzeppelin::token::erc20::ERC20Component; + use openzeppelin::token::erc20::ERC20HooksEmptyImpl; use starknet::ContractAddress; use super::IFreeMint; @@ -47,7 +48,7 @@ mod FreeMintERC20 { #[abi(embed_v0)] impl ImplFreeMint of IFreeMint { fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { - self.erc20._mint(recipient, amount); + self.erc20.mint(recipient, amount); } } } diff --git a/contracts/ark_tokens/src/erc721.cairo b/contracts/ark_tokens/src/erc721.cairo index 930807baf..53e860d9e 100644 --- a/contracts/ark_tokens/src/erc721.cairo +++ b/contracts/ark_tokens/src/erc721.cairo @@ -8,14 +8,15 @@ trait IFreeMint { #[starknet::contract] mod FreeMintNFT { + use core::array::ArrayTrait; + use core::serde::Serde; + use core::traits::Into; + use core::traits::TryInto; use openzeppelin::introspection::src5::SRC5Component; use openzeppelin::token::erc721::ERC721Component; + use openzeppelin::token::erc721::ERC721HooksEmptyImpl; use starknet::ContractAddress; - use core::traits::Into; - use core::serde::Serde; - use core::traits::TryInto; use super::IFreeMint; - use core::array::ArrayTrait; component!(path: ERC721Component, storage: erc721, event: ERC721Event); component!(path: SRC5Component, storage: src5, event: SRC5Event); @@ -66,7 +67,7 @@ mod FreeMintNFT { fn mint(ref self: ContractState, recipient: ContractAddress, token_uri: felt252) { let token_id = self.latest_token_id.read(); - self.erc721._mint(recipient, token_id); + self.erc721.mint(recipient, token_id); self.latest_token_id.write(token_id + 1); } } diff --git a/contracts/ark_tokens/src/erc721_royalty.cairo b/contracts/ark_tokens/src/erc721_royalty.cairo index 8aa3d1c8f..15e258801 100644 --- a/contracts/ark_tokens/src/erc721_royalty.cairo +++ b/contracts/ark_tokens/src/erc721_royalty.cairo @@ -8,18 +8,18 @@ trait IFreeMint { #[starknet::contract] mod FreeMintNFTRoyalty { - use starknet::ContractAddress; - use core::traits::Into; + use ark_oz::erc2981::ERC2981Component; + use ark_oz::erc2981::FeesRatioDefault; + use core::array::ArrayTrait; use core::serde::Serde; + use core::traits::Into; use core::traits::TryInto; - use core::array::ArrayTrait; + use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::introspection::src5::SRC5Component; use openzeppelin::token::erc721::ERC721Component; - use openzeppelin::access::ownable::OwnableComponent; - - use ark_oz::erc2981::ERC2981Component; - use ark_oz::erc2981::FeesRatioDefault; + use openzeppelin::token::erc721::ERC721HooksEmptyImpl; + use starknet::ContractAddress; use super::IFreeMint; @@ -93,7 +93,7 @@ mod FreeMintNFTRoyalty { fn mint(ref self: ContractState, recipient: ContractAddress, token_uri: felt252) { let token_id = self.latest_token_id.read(); - self.erc721._mint(recipient, token_id); + self.erc721.mint(recipient, token_id); self.latest_token_id.write(token_id + 1); } } diff --git a/contracts/ark_tokens/src/lib.cairo b/contracts/ark_tokens/src/lib.cairo index 5e9a35100..1af6a81a3 100644 --- a/contracts/ark_tokens/src/lib.cairo +++ b/contracts/ark_tokens/src/lib.cairo @@ -1,4 +1,4 @@ +mod erc20; mod erc721; mod erc721_royalty; -mod erc20; diff --git a/contracts/solis/Scarb.toml b/contracts/solis/Scarb.toml index a9563e13b..b9603293c 100644 --- a/contracts/solis/Scarb.toml +++ b/contracts/solis/Scarb.toml @@ -7,6 +7,9 @@ version = "0.1.0" [dependencies] starknet = "2.5.4" +[tool] +fmt.workspace = true + [[target.starknet-contract]] sierra = true casm = true