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;
}