From efca10928ddfcc093fae03c173db8f0b8faf49dd Mon Sep 17 00:00:00 2001 From: Patrice Tisserand Date: Thu, 22 Aug 2024 16:51:11 +0200 Subject: [PATCH] feat(starknet): add support for maintenance mode --- contracts/ark_starknet/src/executor.cairo | 98 +++++++++++-------- contracts/ark_starknet/src/interfaces.cairo | 6 ++ .../tests/integration/create_order.cairo | 54 +++++++++- .../tests/integration/execute_order.cairo | 26 ++++- .../tests/integration/fulfill_order.cairo | 40 +++++++- .../tests/integration/maintenance.cairo | 65 ++++++++++++ contracts/ark_starknet/tests/lib.cairo | 3 +- 7 files changed, 246 insertions(+), 46 deletions(-) create mode 100644 contracts/ark_starknet/tests/integration/maintenance.cairo diff --git a/contracts/ark_starknet/src/executor.cairo b/contracts/ark_starknet/src/executor.cairo index 08f8652b9..d9eb61437 100644 --- a/contracts/ark_starknet/src/executor.cairo +++ b/contracts/ark_starknet/src/executor.cairo @@ -75,7 +75,7 @@ mod executor { use ark_oz::erc2981::{IERC2981Dispatcher, IERC2981DispatcherTrait}; use ark_oz::erc2981::{FeesRatio, FeesRatioDefault, FeesImpl}; - use ark_starknet::interfaces::{IExecutor, IUpgradable}; + use ark_starknet::interfaces::{IExecutor, IUpgradable, IMaintenance}; use ark_starknet::appchain_messaging::{ IAppchainMessagingDispatcher, IAppchainMessagingDispatcherTrait, @@ -104,6 +104,8 @@ mod executor { default_receiver: ContractAddress, default_fees: FeesRatio, creator_fees: LegacyMap, + // maintenance mode + enabled: bool, } #[event] @@ -111,6 +113,7 @@ mod executor { enum Event { OrderExecuted: OrderExecuted, CollectionFallbackFees: CollectionFallbackFees, + ExecutorEnabled: ExecutorEnabled, } #[derive(Drop, starknet::Event)] @@ -132,6 +135,17 @@ mod executor { receiver: ContractAddress, } + #[derive(Drop, starknet::Event)] + struct ExecutorEnabled { + enable: bool + } + + mod Errors { + const NOT_ENABLED: felt252 = 'Executor not enabled'; + const UNAUTHORIZED_ADMIN: felt252 = 'Unauthorized admin address'; + const FEES_RATIO_INVALID: felt252 = 'Fees ratio is invalid'; + } + #[constructor] fn constructor( ref self: ContractState, @@ -147,13 +161,14 @@ mod executor { self.ark_fees.write(Default::default()); self.default_receiver.write(admin_address); self.default_fees.write(Default::default()); + self.enabled.write(true); // enabled by default } #[abi(embed_v0)] impl ExecutorImpl of IExecutor { fn set_broker_fees(ref self: ContractState, fees_ratio: FeesRatio) { - assert(fees_ratio.is_valid(), 'Fees ratio is invalid'); + assert(fees_ratio.is_valid(), Errors::FEES_RATIO_INVALID); self.broker_fees.write(starknet::get_caller_address(), fees_ratio); } @@ -168,11 +183,8 @@ mod executor { } fn set_ark_fees(ref self: ContractState, fees_ratio: FeesRatio) { - assert( - starknet::get_caller_address() == self.admin_address.read(), - 'Unauthorized admin address' - ); - assert(fees_ratio.is_valid(), 'Fees ratio is invalid'); + _ensure_admin(@self); + assert(fees_ratio.is_valid(), Errors::FEES_RATIO_INVALID); self.ark_fees.write(fees_ratio); } @@ -188,11 +200,8 @@ mod executor { fn set_default_creator_fees( ref self: ContractState, receiver: ContractAddress, fees_ratio: FeesRatio ) { - assert( - starknet::get_caller_address() == self.admin_address.read(), - 'Unauthorized admin address' - ); - assert(fees_ratio.is_valid(), 'Fees ratio is invalid'); + _ensure_admin(@self); + assert(fees_ratio.is_valid(), Errors::FEES_RATIO_INVALID); self.default_receiver.write(receiver); self.default_fees.write(fees_ratio); } @@ -214,11 +223,8 @@ mod executor { receiver: ContractAddress, fees_ratio: FeesRatio ) { - assert( - starknet::get_caller_address() == self.admin_address.read(), - 'Unauthorized admin address' - ); - assert(fees_ratio.is_valid(), 'Fees ratio is invalid'); + _ensure_admin(@self); + assert(fees_ratio.is_valid(), Errors::FEES_RATIO_INVALID); self.creator_fees.write(nft_address, (receiver, fees_ratio)); } @@ -233,51 +239,37 @@ mod executor { fn update_arkchain_orderbook_address( ref self: ContractState, orderbook_address: ContractAddress ) { - assert( - starknet::get_caller_address() == self.admin_address.read(), - 'Unauthorized admin address' - ); + _ensure_admin(@self); self.arkchain_orderbook_address.write(orderbook_address); } fn update_messaging_address(ref self: ContractState, msger_address: ContractAddress) { - assert( - starknet::get_caller_address() == self.admin_address.read(), - 'Unauthorized admin address' - ); + _ensure_admin(@self); self.messaging_address.write(msger_address); } fn update_eth_address(ref self: ContractState, eth_address: ContractAddress) { - assert( - starknet::get_caller_address() == self.admin_address.read(), - 'Unauthorized admin address' - ); + _ensure_admin(@self); self.eth_contract_address.write(eth_address); } fn update_orderbook_address(ref self: ContractState, orderbook_address: ContractAddress) { - assert( - starknet::get_caller_address() == self.admin_address.read(), - 'Unauthorized admin address' - ); + _ensure_admin(@self); self.arkchain_orderbook_address.write(orderbook_address); } fn update_admin_address(ref self: ContractState, admin_address: ContractAddress) { - assert( - starknet::get_caller_address() == self.admin_address.read(), - 'Unauthorized admin address' - ); + _ensure_admin(@self); self.admin_address.write(admin_address); } fn cancel_order(ref self: ContractState, cancelInfo: CancelInfo) { + _ensure_is_enabled(@self); let messaging = IAppchainMessagingDispatcher { contract_address: self.messaging_address.read() }; @@ -296,6 +288,7 @@ mod executor { } fn create_order(ref self: ContractState, order: OrderV1) { + _ensure_is_enabled(@self); let messaging = IAppchainMessagingDispatcher { contract_address: self.messaging_address.read() }; @@ -319,6 +312,7 @@ mod executor { } fn fulfill_order(ref self: ContractState, fulfillInfo: FulfillInfo) { + _ensure_is_enabled(@self); let messaging = IAppchainMessagingDispatcher { contract_address: self.messaging_address.read() }; @@ -345,7 +339,7 @@ mod executor { // ); // Check if execution_info.currency_contract_address is whitelisted - + _ensure_is_enabled(@self); assert( execution_info.payment_currency_chain_id == self.chain_id.read(), 'Chain ID is not SN_MAIN' @@ -476,10 +470,7 @@ mod executor { #[abi(embed_v0)] impl ExecutorUpgradeImpl of IUpgradable { fn upgrade(ref self: ContractState, class_hash: ClassHash) { - assert( - starknet::get_caller_address() == self.admin_address.read(), - 'Unauthorized replace class' - ); + _ensure_admin(@self); match starknet::replace_class_syscall(class_hash) { Result::Ok(_) => (), // emit event @@ -488,6 +479,19 @@ mod executor { } } + #[abi(embed_v0)] + impl ExecutorMaintenanceImpl of IMaintenance { + fn is_enabled(self: @ContractState) -> bool { + self.enabled.read() + } + + fn enable(ref self: ContractState, enable: bool) { + _ensure_admin(@self); + self.enabled.write(enable); + self.emit(ExecutorEnabled { enable }) + } + } + fn _verify_create_order(self: @ContractState, vinfo: @CreateOrderInfo) { let order = vinfo.order; let caller = starknet::get_caller_address(); @@ -772,4 +776,14 @@ mod executor { let amount = fees_ratio.compute_amount(payment_amount); (receiver, amount) } + + fn _ensure_admin(self: @ContractState) { + assert( + starknet::get_caller_address() == self.admin_address.read(), Errors::UNAUTHORIZED_ADMIN + ); + } + + fn _ensure_is_enabled(self: @ContractState) { + assert(self.enabled.read(), Errors::NOT_ENABLED) + } } diff --git a/contracts/ark_starknet/src/interfaces.cairo b/contracts/ark_starknet/src/interfaces.cairo index f12c3ee80..71a743b46 100644 --- a/contracts/ark_starknet/src/interfaces.cairo +++ b/contracts/ark_starknet/src/interfaces.cairo @@ -38,3 +38,9 @@ trait IExecutor { trait IUpgradable { fn upgrade(ref self: T, class_hash: ClassHash); } + +#[starknet::interface] +trait IMaintenance { + fn is_enabled(self: @T) -> bool; + fn enable(ref self: T, enable: bool); +} diff --git a/contracts/ark_starknet/tests/integration/create_order.cairo b/contracts/ark_starknet/tests/integration/create_order.cairo index c43c8b215..1cac600ef 100644 --- a/contracts/ark_starknet/tests/integration/create_order.cairo +++ b/contracts/ark_starknet/tests/integration/create_order.cairo @@ -4,7 +4,10 @@ use ark_common::protocol::order_v1::OrderV1; use ark_common::protocol::order_types::RouteType; -use ark_starknet::interfaces::{IExecutorDispatcher, IExecutorDispatcherTrait,}; +use ark_starknet::interfaces::{ + IExecutorDispatcher, IExecutorDispatcherTrait, IMaintenanceDispatcher, + IMaintenanceDispatcherTrait +}; use ark_tokens::erc20::IFreeMintDispatcher as Erc20Dispatcher; use ark_tokens::erc20::IFreeMintDispatcherTrait as Erc20DispatcherTrait; @@ -110,3 +113,52 @@ fn test_create_order_offerer_not_own_ec721_token() { IExecutorDispatcher { contract_address: executor_address }.create_order(order); snf::stop_prank(CheatTarget::One(executor_address)); } + +#[test] +#[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'>(); + let offerer = contract_address_const::<'offerer'>(); + let start_amount = 10_000_000; + + Erc20Dispatcher { contract_address: erc20_address }.mint(offerer, start_amount); + + let mut order = setup_order(erc20_address, nft_address); + order.offerer = offerer; + order.start_amount = start_amount; + + snf::start_prank(CheatTarget::One(executor_address), admin); + IMaintenanceDispatcher { contract_address: executor_address }.enable(false); + snf::stop_prank(CheatTarget::One(executor_address)); + + snf::start_prank(CheatTarget::One(executor_address), offerer); + IExecutorDispatcher { contract_address: executor_address }.create_order(order); + snf::stop_prank(CheatTarget::One(executor_address)); +} + +#[test] +#[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'>(); + let offerer = contract_address_const::<'offerer'>(); + + let token_id: u256 = Erc721Dispatcher { contract_address: nft_address } + .get_current_token_id() + .into(); + Erc721Dispatcher { contract_address: nft_address }.mint(offerer, 'base_uri'); + + let mut order = setup_order(erc20_address, nft_address); + order.route = RouteType::Erc721ToErc20.into(); + order.offerer = offerer; + order.token_id = Option::Some(token_id); + + snf::start_prank(CheatTarget::One(executor_address), admin); + IMaintenanceDispatcher { contract_address: executor_address }.enable(false); + snf::stop_prank(CheatTarget::One(executor_address)); + + snf::start_prank(CheatTarget::One(executor_address), offerer); + 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 2d8f9e22f..0935f8810 100644 --- a/contracts/ark_starknet/tests/integration/execute_order.cairo +++ b/contracts/ark_starknet/tests/integration/execute_order.cairo @@ -7,7 +7,10 @@ use ark_common::protocol::order_types::{FulfillInfo, ExecutionInfo, OrderTrait, use ark_oz::erc2981::{IERC2981SetupDispatcher, IERC2981SetupDispatcherTrait}; -use ark_starknet::interfaces::{IExecutorDispatcher, IExecutorDispatcherTrait, FeesRatio}; +use ark_starknet::interfaces::{ + IExecutorDispatcher, IExecutorDispatcherTrait, FeesRatio, IMaintenanceDispatcher, + IMaintenanceDispatcherTrait +}; use ark_tokens::erc20::{IFreeMintDispatcher, IFreeMintDispatcherTrait}; use ark_tokens::erc721::IFreeMintDispatcher as Erc721Dispatcher; @@ -654,3 +657,24 @@ fn test_execute_order_check_fee_too_much_fees() { assert_eq!(erc20.balance_of(fulfill_broker), 1_000_000, "Fulfill broker balance not correct"); assert_eq!(erc20.balance_of(listing_broker), 500_000, "Listing broker balance not correct"); } + +#[test] +#[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'>(); + let fulfill_broker = contract_address_const::<'fulfill_broker'>(); + let admin_address = contract_address_const::<'admin'>(); + let offerer = contract_address_const::<'offerer'>(); + + let start_amount = 10_000_000; + let (executor_address, _erc20_address, _, execution_info) = setup_execute_order( + admin_address, offerer, fulfiller, listing_broker, fulfill_broker, start_amount, false + ); + + snf::start_prank(CheatTarget::One(executor_address), admin_address); + IMaintenanceDispatcher { contract_address: executor_address }.enable(false); + snf::stop_prank(CheatTarget::One(executor_address)); + + IExecutorDispatcher { contract_address: executor_address }.execute_order(execution_info); +} diff --git a/contracts/ark_starknet/tests/integration/fulfill_order.cairo b/contracts/ark_starknet/tests/integration/fulfill_order.cairo index 42e146dbb..afd55d082 100644 --- a/contracts/ark_starknet/tests/integration/fulfill_order.cairo +++ b/contracts/ark_starknet/tests/integration/fulfill_order.cairo @@ -6,7 +6,10 @@ use ark_common::protocol::order_v1::OrderV1; use ark_common::protocol::order_types::{FulfillInfo, OrderTrait, RouteType}; -use ark_starknet::interfaces::{IExecutorDispatcher, IExecutorDispatcherTrait,}; +use ark_starknet::interfaces::{ + IExecutorDispatcher, IExecutorDispatcherTrait, IMaintenanceDispatcher, + IMaintenanceDispatcherTrait +}; use ark_tokens::erc20::{IFreeMintDispatcher, IFreeMintDispatcherTrait}; use ark_tokens::erc721::IFreeMintDispatcher as Erc721Dispatcher; @@ -503,3 +506,38 @@ fn test_fulfill_auction_order_fulfiller_same_as_offerer() { IExecutorDispatcher { contract_address: executor_address }.fulfill_order(fulfill_info); snf::stop_prank(CheatTarget::One(executor_address)); } + +#[test] +#[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'>(); + let fulfiller = contract_address_const::<'fulfiller'>(); + + let token_id: u256 = Erc721Dispatcher { contract_address: nft_address } + .get_current_token_id() + .into(); + Erc721Dispatcher { contract_address: nft_address }.mint(fulfiller, 'base_uri'); + let (order_hash, offerer, start_amount) = create_offer_order( + executor_address, erc20_address, nft_address, token_id + ); + + snf::start_prank(CheatTarget::One(erc20_address), offerer); + 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); + 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); + IMaintenanceDispatcher { contract_address: executor_address }.enable(false); + snf::stop_prank(CheatTarget::One(executor_address)); + + snf::start_prank(CheatTarget::One(executor_address), fulfiller); + 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 new file mode 100644 index 000000000..9075e10fe --- /dev/null +++ b/contracts/ark_starknet/tests/integration/maintenance.cairo @@ -0,0 +1,65 @@ +use starknet::{ContractAddress, contract_address_const}; +use ark_starknet::interfaces::{IMaintenanceDispatcher, IMaintenanceDispatcherTrait}; +use ark_starknet::executor::executor; + +use snforge_std as snf; +use snf::cheatcodes::events::{EventFetcher, EventAssertions}; +use snf::{ContractClass, ContractClassTrait, CheatTarget, spy_events, SpyOn}; + +use super::super::common::setup::deploy_executor; + +#[test] +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); + executor.enable(false); + assert!(!executor.is_enabled(), "Executor should be disabled"); + spy + .assert_emitted( + @array![ + ( + executor_address, + executor::Event::ExecutorEnabled(executor::ExecutorEnabled { enable: false, }) + ) + ] + ); + + executor.enable(true); + assert!(executor.is_enabled(), "Executor should be enabled"); + spy + .assert_emitted( + @array![ + ( + executor_address, + executor::Event::ExecutorEnabled(executor::ExecutorEnabled { enable: true, }) + ) + ] + ); + + snf::stop_prank(snf::CheatTarget::One(executor_address)); +} + +#[test] +#[should_panic(expected: ('Unauthorized admin address',))] +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); + IMaintenanceDispatcher { contract_address: executor_address }.enable(false); + snf::stop_prank(snf::CheatTarget::One(executor_address)); +} + +#[test] +#[should_panic(expected: ('Unauthorized admin address',))] +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); + IMaintenanceDispatcher { contract_address: executor_address }.enable(true); + 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 a70646ef0..771d7eded 100644 --- a/contracts/ark_starknet/tests/lib.cairo +++ b/contracts/ark_starknet/tests/lib.cairo @@ -7,6 +7,7 @@ mod unit { } mod integration { mod create_order; - mod fulfill_order; mod execute_order; + mod fulfill_order; + mod maintenance; }