diff --git a/src/contracts.cairo b/src/contracts.cairo index b2f1d13..e53bd90 100644 --- a/src/contracts.cairo +++ b/src/contracts.cairo @@ -1,2 +1,3 @@ mod zklink; mod verifier; +mod gatekeeper; diff --git a/src/contracts/gatekeeper.cairo b/src/contracts/gatekeeper.cairo new file mode 100644 index 0000000..a09baf3 --- /dev/null +++ b/src/contracts/gatekeeper.cairo @@ -0,0 +1,271 @@ +use starknet::{ContractAddress, ClassHash}; + +#[starknet::interface] +trait IUpgradeGateKeeper { + fn getMaster(self: @TContractState) -> ContractAddress; + fn transferMastership(ref self: TContractState, _newMaster: ContractAddress); + fn addUpgradeable(ref self: TContractState, _address: ContractAddress); + fn startUpgrade(ref self: TContractState, _newTargets: Array); + fn finishUpgrade(ref self: TContractState) -> bool; + fn cancelUpgrade(ref self: TContractState); +} + +#[starknet::contract] +mod UpgradeGateKeeper { + use starknet::{ContractAddress, ClassHash, get_caller_address, get_block_timestamp}; + use zeroable::Zeroable; + use openzeppelin::upgrades::interface::IUpgradeableDispatcher; + use openzeppelin::upgrades::interface::IUpgradeableDispatcherTrait; + use zklink::utils::data_structures::DataStructures::UpgradeStatus; + use zklink::contracts::zklink::IZklinkDispatcher; + use zklink::contracts::zklink::IZklinkDispatcherTrait; + + #[storage] + struct Storage { + // master address, which can call upgrade functions + master: ContractAddress, + // public, Contract which defines notice period duration and allows finish upgrade during preparation of it + mainContract: ContractAddress, + // public, Array of addresses of upgradeable contracts managed by the gatekeeper + managedContracts: LegacyMap::, + // internal, managedContracts length + managedContractsLength: usize, + // public, upgrade status + upgradeStatus: UpgradeStatus, + // public, Notice period finish timestamp (as seconds since unix epoch) + // Will be equal to zero in case of not active upgrade mode + noticePeriodFinishTimestamp: u256, + // public, Addresses of the next versions of the contracts to be upgraded (if element of this array is equal to zero address it means that appropriate upgradeable contract wouldn't be upgraded this time) + // Will be empty in case of not active upgrade mode + nextTargets: LegacyMap::, + // internal, nextTargets length + nextTargetsLength: usize, + // public, Version id of contracts + versionId: u256 + } + + /// Events + // Event emitted when new upgradeable contract is added to upgrade gatekeeper's list of managed contracts + #[derive(Drop, PartialEq, starknet::Event)] + struct NewUpgradable { + #[key] + versionId: u256, + #[key] + upgradeable: ContractAddress + } + + // Upgrade mode enter event + #[derive(Drop, PartialEq, starknet::Event)] + struct NoticePeriodStart { + #[key] + versionId: u256, + newTargets: Array, + noticePeriod: u256 // notice period (in seconds) + } + + // Upgrade mode cancel event + #[derive(Drop, PartialEq, starknet::Event)] + struct UpgradeCancel { + #[key] + versionId: u256 + } + + // Upgrade mode complete event + #[derive(Drop, PartialEq, starknet::Event)] + struct UpgradeComplete { + #[key] + versionId: u256, + newTargets: Array + } + + #[event] + #[derive(Drop, PartialEq, starknet::Event)] + enum Event { + NewUpgradable: NewUpgradable, + NoticePeriodStart: NoticePeriodStart, + UpgradeCancel: UpgradeCancel, + UpgradeComplete: UpgradeComplete + } + + #[constructor] + fn constructor(ref self: ContractState, _mainContract: ContractAddress) { + self.mainContract.write(_mainContract); + self.versionId.write(0); + self.master.write(get_caller_address()); + self.upgradeStatus.write(UpgradeStatus::Idle(())); + } + + #[external(v0)] + impl UpgradeGateKeeperImpl of super::IUpgradeGateKeeper { + fn getMaster(self: @ContractState) -> ContractAddress { + self.master.read() + } + + fn transferMastership(ref self: ContractState, _newMaster: ContractAddress) { + self.requireMaster(get_caller_address()); + assert( + _newMaster != Zeroable::zero(), '1d' + ); // otp11 - new masters address can't be zero address + self.setMaster(_newMaster); + } + + // Adds a new upgradeable contract to the list of contracts managed by the gatekeeper + // _address: addr Address of upgradeable contract to add + fn addUpgradeable(ref self: ContractState, _address: ContractAddress) { + self.requireMaster(get_caller_address()); + assert( + self.upgradeStatus.read() == UpgradeStatus::Idle(()), 'apc11' + ); // apc11 - upgradeable contract can't be added during upgrade + + let index = self.managedContractsLength.read(); + self.managedContracts.write(index, _address); + self.managedContractsLength.write(index + 1); + + self + .emit( + Event::NewUpgradable( + NewUpgradable { versionId: self.versionId.read(), upgradeable: _address } + ) + ); + } + + // Starts upgrade (activates notice period) + // _newTargets: New managed contracts class hash (if element of this array is equal to zero it means that appropriate upgradeable contract wouldn't be upgraded this time) + fn startUpgrade(ref self: ContractState, _newTargets: Array) { + self.requireMaster(get_caller_address()); + assert( + self.upgradeStatus.read() == UpgradeStatus::Idle(()), 'spu11' + ); // spu11 - unable to activate active upgrade mode + assert( + _newTargets.len() == self.managedContractsLength.read(), 'spu12' + ); // spu12 - number of new targets must be equal to the number of managed contracts + + let zklink_dispatcher = IZklinkDispatcher { + contract_address: self.mainContract.read() + }; + let notice_period = zklink_dispatcher.getNoticePeriod(); + self.upgradeStatus.write(UpgradeStatus::NoticePeriod(())); + self.noticePeriodFinishTimestamp.write(get_block_timestamp().into() + notice_period); + + let newTargets: Span = _newTargets.span(); + let mut newTargetsIndex = 0; + loop { + if newTargetsIndex >= newTargets.len() { + break (); + } + + let newTarget: ClassHash = *newTargets[newTargetsIndex]; + self.nextTargets.write(newTargetsIndex, newTarget); + newTargetsIndex += 1; + }; + self.nextTargetsLength.write(newTargetsIndex); + + self + .emit( + Event::NoticePeriodStart( + NoticePeriodStart { + versionId: self.versionId.read(), + newTargets: _newTargets, + noticePeriod: notice_period + } + ) + ); + } + + fn finishUpgrade(ref self: ContractState) -> bool { + self.requireMaster(get_caller_address()); + assert( + self.upgradeStatus.read() == UpgradeStatus::NoticePeriod(()), 'fpu11' + ); // ugp11 - unable to finish upgrade in case of not active notice period status + + if (get_block_timestamp().into() < self.noticePeriodFinishTimestamp.read()) { + return false; + } + assert( + self.managedContractsLength.read() == self.nextTargetsLength.read(), 'fpu12' + ); // fpu12 - number of new targets class hash must be equal to the number of managed contracts + let zklink_dispatcher = IZklinkDispatcher { + contract_address: self.mainContract.read() + }; + assert( + zklink_dispatcher.isReadyForUpgrade(), 'fpu13' + ); // main contract is not ready for upgrade + + let mut i = 0; + loop { + if i >= self.managedContractsLength.read() { + break (); + } + + let nextTarget: ClassHash = self.nextTargets.read(i); + if nextTarget != Zeroable::zero() { + let managedContract = self.managedContracts.read(i); + let upgrade_dispatcher = IUpgradeableDispatcher { + contract_address: managedContract + }; + upgrade_dispatcher.upgrade(nextTarget); + } + i += 1; + }; + + self.versionId.write(self.versionId.read() + 1); + + let mut newTargets: Array = array![]; + // delete nextTargets + let mut i = 0; + loop { + if i >= self.nextTargetsLength.read() { + break (); + } + newTargets.append(self.nextTargets.read(i)); + self.nextTargets.write(i, Zeroable::zero()); + i += 1; + }; + self.nextTargetsLength.write(0); + self + .emit( + Event::UpgradeComplete( + UpgradeComplete { versionId: self.versionId.read(), newTargets: newTargets } + ) + ); + + self.upgradeStatus.write(UpgradeStatus::Idle(())); + self.noticePeriodFinishTimestamp.write(0); + + return true; + } + + fn cancelUpgrade(ref self: ContractState) { + self.requireMaster(get_caller_address()); + assert( + self.upgradeStatus.read() != UpgradeStatus::Idle(()), 'cpu11' + ); // cpu11 - unable to cancel not active upgrade mode + + self.upgradeStatus.write(UpgradeStatus::Idle(())); + self.noticePeriodFinishTimestamp.write(0); + // delete nextTargets + let mut i = 0; + loop { + if i >= self.nextTargetsLength.read() { + break (); + } + self.nextTargets.write(i, Zeroable::zero()); + i += 1; + }; + self.nextTargetsLength.write(0); + + self.emit(Event::UpgradeCancel(UpgradeCancel { versionId: self.versionId.read() })) + } + } + + #[generate_trait] + impl InternalOwnableImpl of InternalOwnableTrait { + fn setMaster(ref self: ContractState, _newMaster: ContractAddress) { + self.master.write(_newMaster); + } + + fn requireMaster(self: @ContractState, _address: ContractAddress) { + assert(self.master.read() == _address, '1c'); // oro11 - only by master + } + } +} diff --git a/src/contracts/verifier.cairo b/src/contracts/verifier.cairo index 06d0eb9..ee62554 100644 --- a/src/contracts/verifier.cairo +++ b/src/contracts/verifier.cairo @@ -1,4 +1,4 @@ -use starknet::ContractAddress; +use starknet::{ContractAddress, ClassHash}; #[starknet::interface] trait IVerifier { @@ -10,7 +10,6 @@ trait IVerifier { _individualVksInputs: Array, _subProofsLimbs: Array ) -> bool; - fn verifyExitProof( ref self: TContractState, _rootHash: u256, @@ -23,12 +22,26 @@ trait IVerifier { _amount: u128, _proof: Array ) -> bool; + fn getMaster(self: @TContractState) -> ContractAddress; + fn transferMastership(ref self: TContractState, _newMaster: ContractAddress); + fn upgrade(ref self: TContractState, impl_hash: ClassHash); } #[starknet::contract] mod Verifier { + use starknet::{ContractAddress, ClassHash, get_caller_address}; + use openzeppelin::upgrades::interface::IUpgradeable; + #[storage] - struct Storage {} + struct Storage { + // public, master address, which can call upgrade functions + master: ContractAddress, + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.master.write(get_caller_address()); + } #[external(v0)] impl VerifierImpl of super::IVerifier { @@ -57,5 +70,34 @@ mod Verifier { ) -> bool { false } + + fn getMaster(self: @ContractState) -> ContractAddress { + self.master.read() + } + + fn transferMastership(ref self: ContractState, _newMaster: ContractAddress) { + self.requireMaster(get_caller_address()); + assert( + _newMaster != Zeroable::zero(), '1d' + ); // otp11 - new masters address can't be zero address + self.setMaster(_newMaster); + } + + fn upgrade(ref self: ContractState, impl_hash: ClassHash) { + self.requireMaster(get_caller_address()); + assert(!impl_hash.is_zero(), 'upg11'); + starknet::replace_class_syscall(impl_hash).unwrap(); + } + } + + #[generate_trait] + impl InternalOwnableImpl of InternalOwnableTrait { + fn setMaster(ref self: ContractState, _newMaster: ContractAddress) { + self.master.write(_newMaster); + } + + fn requireMaster(self: @ContractState, _address: ContractAddress) { + assert(self.master.read() == _address, '1c'); // oro11 - only by master + } } } diff --git a/src/contracts/zklink.cairo b/src/contracts/zklink.cairo index abd042c..d0f024f 100644 --- a/src/contracts/zklink.cairo +++ b/src/contracts/zklink.cairo @@ -1,4 +1,4 @@ -use starknet::ContractAddress; +use starknet::{ContractAddress, ClassHash}; use zklink::utils::data_structures::DataStructures::{ StoredBlockInfo, CommitBlockInfo, ProofInput, Token, CompressedBlockExtraInfo, ExecuteBlockInfo, RegisteredToken, BridgeInfo @@ -125,6 +125,11 @@ trait IZklink { fn tokenIds(self: @TContractState, _tokenAddress: ContractAddress) -> u16; fn bridges(self: @TContractState, _index: usize) -> BridgeInfo; fn bridgeIndex(self: @TContractState, _bridge: ContractAddress) -> usize; + fn getNoticePeriod(self: @TContractState) -> u256; + fn isReadyForUpgrade(self: @TContractState) -> bool; + fn getMaster(self: @TContractState) -> ContractAddress; + fn transferMastership(ref self: TContractState, _newMaster: ContractAddress); + fn upgrade(ref self: TContractState, impl_hash: ClassHash); } #[starknet::contract] @@ -138,7 +143,7 @@ mod Zklink { use box::BoxTrait; use clone::Clone; use starknet::{ - ContractAddress, contract_address_const, Felt252TryIntoContractAddress, + ContractAddress, ClassHash, contract_address_const, Felt252TryIntoContractAddress, get_contract_address, get_caller_address, get_block_info, get_block_timestamp }; use core::starknet::info::get_block_number; @@ -149,6 +154,7 @@ mod Zklink { use zklink::contracts::verifier::IVerifierDispatcherTrait; use openzeppelin::token::erc20::interface::IERC20Dispatcher; use openzeppelin::token::erc20::interface::IERC20DispatcherTrait; + use openzeppelin::upgrades::interface::IUpgradeable; use zklink::utils::bytes::{Bytes, BytesTrait, ReadBytes}; use zklink::utils::operations::Operations::{ @@ -166,16 +172,19 @@ mod Zklink { use zklink::utils::constants::{ EMPTY_STRING_KECCAK, MAX_AMOUNT_OF_REGISTERED_TOKENS, MAX_ACCOUNT_ID, MAX_SUB_ACCOUNT_ID, CHUNK_BYTES, DEPOSIT_BYTES, CHANGE_PUBKEY_BYTES, WITHDRAW_BYTES, FORCED_EXIT_BYTES, - FULL_EXIT_BYTES, PRIORITY_EXPIRATION, MAX_DEPOSIT_AMOUNT, MAX_PROOF_COMMITMENT, INPUT_MASK, - AUTH_FACT_RESET_TIMELOCK, CHAIN_ID, MIN_CHAIN_ID, MAX_CHAIN_ID, ALL_CHAINS, CHAIN_INDEX, - ENABLE_COMMIT_COMPRESSED_BLOCK, MAX_ACCEPT_FEE_RATE, TOKEN_DECIMALS_OF_LAYER2, - GLOBAL_ASSET_ACCOUNT_ID, GLOBAL_ASSET_ACCOUNT_ADDRESS, USD_TOKEN_ID, - MIN_USD_STABLE_TOKEN_ID, MAX_USD_STABLE_TOKEN_ID + FULL_EXIT_BYTES, PRIORITY_EXPIRATION, UPGRADE_NOTICE_PERIOD, MAX_DEPOSIT_AMOUNT, + MAX_PROOF_COMMITMENT, INPUT_MASK, AUTH_FACT_RESET_TIMELOCK, CHAIN_ID, MIN_CHAIN_ID, + MAX_CHAIN_ID, ALL_CHAINS, CHAIN_INDEX, ENABLE_COMMIT_COMPRESSED_BLOCK, MAX_ACCEPT_FEE_RATE, + TOKEN_DECIMALS_OF_LAYER2, GLOBAL_ASSET_ACCOUNT_ID, GLOBAL_ASSET_ACCOUNT_ADDRESS, + USD_TOKEN_ID, MIN_USD_STABLE_TOKEN_ID, MAX_USD_STABLE_TOKEN_ID }; /// Storage #[storage] struct Storage { + // public + // master address, which can call upgrade functions + master: ContractAddress, // internal // ReentrancyGuard flag entered: bool, @@ -471,6 +480,7 @@ mod Zklink { assert(_verifierAddress.is_non_zero(), 'i0'); assert(_networkGovernor.is_non_zero(), 'i2'); + self.master.write(get_caller_address()); self.verifier.write(_verifierAddress); self.networkGovernor.write(_networkGovernor); @@ -1468,11 +1478,50 @@ mod Zklink { fn bridgeIndex(self: @ContractState, _bridge: ContractAddress) -> usize { self.bridgeIndex.read(_bridge) } + + // Notice period before activation preparation status of upgrade mode + fn getNoticePeriod(self: @ContractState) -> u256 { + return UPGRADE_NOTICE_PERIOD.into(); + } + + // Checks that contract is ready for upgrade + // Returns: bool flag indicating that contract is ready for upgrade + fn isReadyForUpgrade(self: @ContractState) -> bool { + !self.exodusMode.read() + } + + fn getMaster(self: @ContractState) -> ContractAddress { + self.master.read() + } + + fn transferMastership(ref self: ContractState, _newMaster: ContractAddress) { + self.requireMaster(get_caller_address()); + assert( + _newMaster != Zeroable::zero(), '1d' + ); // otp11 - new masters address can't be zero address + self.setMaster(_newMaster); + } + + fn upgrade(ref self: ContractState, impl_hash: ClassHash) { + self.requireMaster(get_caller_address()); + assert(!impl_hash.is_zero(), 'upg11'); + starknet::replace_class_syscall(impl_hash).unwrap(); + } } #[generate_trait] - impl InternalFunctions of InternalFunctionsTrait { - // =================modifier functions================= + impl InternalOwnableImpl of InternalOwnableTrait { + fn setMaster(ref self: ContractState, _newMaster: ContractAddress) { + self.master.write(_newMaster); + } + + fn requireMaster(self: @ContractState, _address: ContractAddress) { + assert(self.master.read() == _address, '1c'); // oro11 - only by master + } + } + + #[generate_trait] + impl ModifierImpl of ModifierTrait { // Checks that current state not is exodus mode #[inline(always)] fn active(self: @ContractState) { @@ -1505,8 +1554,10 @@ mod Zklink { fn onlyValidator(self: @ContractState) { assert(self.validators.read(get_caller_address()), '4'); } + } - // =================Internal functions================= + #[generate_trait] + impl InternalFunctions of InternalFunctionsTrait { // Deposit ERC20 token internal function // Parameters: // _token Token address diff --git a/src/tests.cairo b/src/tests.cairo index 708c0d9..1e5122c 100644 --- a/src/tests.cairo +++ b/src/tests.cairo @@ -9,5 +9,7 @@ mod test_accept; mod test_exodus; mod test_pending_withdraw; mod test_fast_withdraw; +mod test_ownable; +mod test_gatekeeper; mod mocks; mod utils; diff --git a/src/tests/mocks.cairo b/src/tests/mocks.cairo index 24ffdde..460b80e 100644 --- a/src/tests/mocks.cairo +++ b/src/tests/mocks.cairo @@ -4,3 +4,5 @@ mod verifier_test; mod standard_token; mod standard_decimals_token; mod non_standard_token; +mod zklink_upgrade_v1; +mod zklink_upgrade_v2; diff --git a/src/tests/mocks/verifier_test.cairo b/src/tests/mocks/verifier_test.cairo index f1979e2..e5f7d4d 100644 --- a/src/tests/mocks/verifier_test.cairo +++ b/src/tests/mocks/verifier_test.cairo @@ -5,6 +5,7 @@ trait IVerifierMock { #[starknet::contract] mod VerifierMock { + use starknet::{ContractAddress, ClassHash}; use zklink::contracts::verifier::IVerifier; #[storage] @@ -51,5 +52,13 @@ mod VerifierMock { ) -> bool { self.verifyResult.read() } + + fn getMaster(self: @ContractState) -> ContractAddress { + starknet::get_caller_address() + } + + fn transferMastership(ref self: ContractState, _newMaster: ContractAddress) {} + + fn upgrade(ref self: ContractState, impl_hash: ClassHash) {} } } diff --git a/src/tests/mocks/zklink_upgrade_v1.cairo b/src/tests/mocks/zklink_upgrade_v1.cairo new file mode 100644 index 0000000..034862a --- /dev/null +++ b/src/tests/mocks/zklink_upgrade_v1.cairo @@ -0,0 +1,94 @@ +use starknet::{ContractAddress, ClassHash}; + +#[starknet::interface] +trait IZklinkUpgradeV1 { + fn getMaster(self: @TContractState) -> ContractAddress; + fn transferMastership(self: @TContractState, _newMaster: ContractAddress); + fn upgrade(self: @TContractState, impl_hash: ClassHash); + fn getNoticePeriod(self: @TContractState) -> u256; + fn isReadyForUpgrade(self: @TContractState) -> bool; + fn setExodus(self: @TContractState, _exodusMode: bool); + fn set_value1(ref self: TContractState, value: felt252); + fn get_value1(self: @TContractState) -> felt252; +} + +#[starknet::contract] +mod ZklinkUpgradeV1 { + use starknet::{ContractAddress, ClassHash}; + use zklink::contracts::zklink::Zklink; + use zklink::contracts::zklink::Zklink::{ + masterContractMemberStateTrait, exodusModeContractMemberStateTrait + }; + + #[storage] + struct Storage { + _governor: ContractAddress, + value1: felt252 + } + + #[constructor] + fn constructor( + ref self: ContractState, + _verifierAddress: ContractAddress, + _networkGovernor: ContractAddress, + _blockNumber: u64, + _timestamp: u64, + _stateHash: u256, + _commitment: u256, + _syncHash: u256 + ) { + self._governor.write(_networkGovernor); + let mut state: Zklink::ContractState = Zklink::contract_state_for_testing(); + Zklink::constructor( + ref state, + _verifierAddress, + _networkGovernor, + _blockNumber, + _timestamp, + _stateHash, + _commitment, + _syncHash + ); + } + + #[external(v0)] + impl ZklinkUpgradeV1Impl of super::IZklinkUpgradeV1 { + fn getMaster(self: @ContractState) -> ContractAddress { + let mut state: Zklink::ContractState = Zklink::contract_state_for_testing(); + state.master.read() + } + + fn transferMastership(self: @ContractState, _newMaster: ContractAddress) { + let mut state: Zklink::ContractState = Zklink::contract_state_for_testing(); + Zklink::Zklink::transferMastership(ref state, _newMaster); + } + + fn upgrade(self: @ContractState, impl_hash: ClassHash) { + let mut state: Zklink::ContractState = Zklink::contract_state_for_testing(); + Zklink::Zklink::upgrade(ref state, impl_hash); + } + + fn getNoticePeriod(self: @ContractState) -> u256 { + let mut state: Zklink::ContractState = Zklink::contract_state_for_testing(); + Zklink::Zklink::getNoticePeriod(@state) + } + + fn setExodus(self: @ContractState, _exodusMode: bool) { + let mut state: Zklink::ContractState = Zklink::contract_state_for_testing(); + state.exodusMode.write(_exodusMode); + } + + fn isReadyForUpgrade(self: @ContractState) -> bool { + let mut state: Zklink::ContractState = Zklink::contract_state_for_testing(); + Zklink::Zklink::isReadyForUpgrade(@state) + } + + fn set_value1(ref self: ContractState, value: felt252) { + self.value1.write(value); + } + + fn get_value1(self: @ContractState) -> felt252 { + self.value1.read() + } + } +} diff --git a/src/tests/mocks/zklink_upgrade_v2.cairo b/src/tests/mocks/zklink_upgrade_v2.cairo new file mode 100644 index 0000000..1bc08ef --- /dev/null +++ b/src/tests/mocks/zklink_upgrade_v2.cairo @@ -0,0 +1,105 @@ +use starknet::{ContractAddress, ClassHash}; + +#[starknet::interface] +trait IZklinkUpgradeV2 { + fn getMaster(self: @TContractState) -> ContractAddress; + fn transferMastership(self: @TContractState, _newMaster: ContractAddress); + fn upgrade(self: @TContractState, impl_hash: ClassHash); + fn getNoticePeriod(self: @TContractState) -> u256; + fn isReadyForUpgrade(self: @TContractState) -> bool; + fn setExodus(self: @TContractState, _exodusMode: bool); + fn set_value1(ref self: TContractState, value: felt252); + fn get_value1(self: @TContractState) -> felt252; + fn set_value2(ref self: TContractState, value: felt252); + fn get_value2(self: @TContractState) -> felt252; +} + +#[starknet::contract] +mod ZklinkUpgradeV2 { + use starknet::{ContractAddress, ClassHash}; + use zklink::contracts::zklink::Zklink; + use zklink::contracts::zklink::Zklink::{ + masterContractMemberStateTrait, exodusModeContractMemberStateTrait + }; + + #[storage] + struct Storage { + _governor: ContractAddress, + value1: felt252, + value2: felt252 + } + + #[constructor] + fn constructor( + ref self: ContractState, + _verifierAddress: ContractAddress, + _networkGovernor: ContractAddress, + _blockNumber: u64, + _timestamp: u64, + _stateHash: u256, + _commitment: u256, + _syncHash: u256 + ) { + self._governor.write(_networkGovernor); + let mut state: Zklink::ContractState = Zklink::contract_state_for_testing(); + Zklink::constructor( + ref state, + _verifierAddress, + _networkGovernor, + _blockNumber, + _timestamp, + _stateHash, + _commitment, + _syncHash + ); + } + + #[external(v0)] + impl ZklinkUpgradeV1Impl of super::IZklinkUpgradeV2 { + fn getMaster(self: @ContractState) -> ContractAddress { + let mut state: Zklink::ContractState = Zklink::contract_state_for_testing(); + state.master.read() + } + + fn transferMastership(self: @ContractState, _newMaster: ContractAddress) { + let mut state: Zklink::ContractState = Zklink::contract_state_for_testing(); + Zklink::Zklink::transferMastership(ref state, _newMaster); + } + + fn upgrade(self: @ContractState, impl_hash: ClassHash) { + let mut state: Zklink::ContractState = Zklink::contract_state_for_testing(); + Zklink::Zklink::upgrade(ref state, impl_hash); + } + + fn setExodus(self: @ContractState, _exodusMode: bool) { + let mut state: Zklink::ContractState = Zklink::contract_state_for_testing(); + state.exodusMode.write(_exodusMode); + } + + fn getNoticePeriod(self: @ContractState) -> u256 { + let mut state: Zklink::ContractState = Zklink::contract_state_for_testing(); + Zklink::Zklink::getNoticePeriod(@state) + } + + fn isReadyForUpgrade(self: @ContractState) -> bool { + let mut state: Zklink::ContractState = Zklink::contract_state_for_testing(); + Zklink::Zklink::isReadyForUpgrade(@state) + } + + fn set_value1(ref self: ContractState, value: felt252) { + self.value1.write(value); + } + + fn get_value1(self: @ContractState) -> felt252 { + self.value1.read() + } + + fn set_value2(ref self: ContractState, value: felt252) { + self.value2.write(value); + } + + fn get_value2(self: @ContractState) -> felt252 { + self.value2.read() + } + } +} diff --git a/src/tests/test_gatekeeper.cairo b/src/tests/test_gatekeeper.cairo new file mode 100644 index 0000000..9a19ee9 --- /dev/null +++ b/src/tests/test_gatekeeper.cairo @@ -0,0 +1,299 @@ +use starknet::{ContractAddress, ClassHash, contract_address_const, class_hash_const}; +use starknet::testing::{set_contract_address, set_block_timestamp}; +use starknet::testing; +use test::test_utils::assert_eq; +use clone::Clone; +use traits::TryInto; +use debug::PrintTrait; +use zklink::contracts::gatekeeper::UpgradeGateKeeper; +use zklink::contracts::gatekeeper::IUpgradeGateKeeperDispatcher; +use zklink::contracts::gatekeeper::IUpgradeGateKeeperDispatcherTrait; +use zklink::tests::mocks::zklink_upgrade_v1::ZklinkUpgradeV1; +use zklink::tests::mocks::zklink_upgrade_v1::IZklinkUpgradeV1Dispatcher; +use zklink::tests::mocks::zklink_upgrade_v1::IZklinkUpgradeV1DispatcherTrait; +use zklink::tests::mocks::zklink_upgrade_v2::ZklinkUpgradeV2; +use zklink::tests::mocks::zklink_upgrade_v2::IZklinkUpgradeV2Dispatcher; +use zklink::tests::mocks::zklink_upgrade_v2::IZklinkUpgradeV2DispatcherTrait; +use zklink::tests::utils; + +fn deploy_zklink() -> (ContractAddress, ContractAddress, ContractAddress) { + let deployer: ContractAddress = + contract_address_const::<0x74a0c0f8e8756218a96c2d9aae21152d786a0704202b10fb30496e46222b72d>(); + set_contract_address(deployer); + + let calldata = array![ + 0x123, // verifier + 2, // governor + 0, // blockNumber + 0, // timestamp + utils::GENESIS_ROOT.low.into(), // stateHash low + utils::GENESIS_ROOT.high.into(), // stateHash high + 0, // commitment low + 0, // commitment high + utils::EMPTY_STRING_KECCAK.low.into(), // syncHash low + utils::EMPTY_STRING_KECCAK.high.into(), // syncHash high + ]; + + let v1: ContractAddress = utils::deploy(ZklinkUpgradeV1::TEST_CLASS_HASH, calldata.clone()); + let v2: ContractAddress = utils::deploy(ZklinkUpgradeV2::TEST_CLASS_HASH, calldata); + let gatekeeper: ContractAddress = utils::deploy( + UpgradeGateKeeper::TEST_CLASS_HASH, array![v1.into()] + ); + (v1, v2, gatekeeper) +} + +fn assert_event_NewUpgradable( + gatekeeper: ContractAddress, versionId: u256, upgradeable: ContractAddress +) { + assert_eq( + @testing::pop_log(gatekeeper).unwrap(), + @UpgradeGateKeeper::Event::NewUpgradable( + UpgradeGateKeeper::NewUpgradable { versionId: versionId, upgradeable: upgradeable, } + ), + 'NewUpgradable Emit' + ) +} + +fn assert_event_NoticePeriodStart( + gatekeeper: ContractAddress, versionId: u256, newTargets: Array, noticePeriod: u256 +) { + assert_eq( + @testing::pop_log(gatekeeper).unwrap(), + @UpgradeGateKeeper::Event::NoticePeriodStart( + UpgradeGateKeeper::NoticePeriodStart { + versionId: versionId, newTargets: newTargets, noticePeriod: noticePeriod + } + ), + 'NoticePeriodStart Emit' + ) +} + +fn assert_event_UpgradeCancel(gatekeeper: ContractAddress, versionId: u256) { + assert_eq( + @testing::pop_log(gatekeeper).unwrap(), + @UpgradeGateKeeper::Event::UpgradeCancel( + UpgradeGateKeeper::UpgradeCancel { versionId: versionId } + ), + 'UpgradeCancel Emit' + ) +} + +#[test] +#[available_gas(20000000000)] +fn test_zklink_gatekeeper_add_upgradeable() { + let (v1, v2, gatekeeper) = deploy_zklink(); + let v1_dispatcher = IZklinkUpgradeV1Dispatcher { contract_address: v1 }; + let gatekeeper_dispatcher = IUpgradeGateKeeperDispatcher { contract_address: gatekeeper }; + v1_dispatcher.transferMastership(gatekeeper); + + gatekeeper_dispatcher.addUpgradeable(v1); + assert_event_NewUpgradable(gatekeeper, 0, v1); +} + +#[test] +#[available_gas(20000000000)] +#[should_panic(expected: ('1c', 'ENTRYPOINT_FAILED'))] +fn test_zklink_gatekeeper_add_upgradeable_invalid_master1() { + let (v1, v2, gatekeeper) = deploy_zklink(); + let v1_dispatcher = IZklinkUpgradeV1Dispatcher { contract_address: v1 }; + let gatekeeper_dispatcher = IUpgradeGateKeeperDispatcher { contract_address: gatekeeper }; + v1_dispatcher.transferMastership(gatekeeper); + gatekeeper_dispatcher.addUpgradeable(v1); + let newMaster: ContractAddress = contract_address_const::<0x64656661756c7453656e646572>(); + set_contract_address(newMaster); + gatekeeper_dispatcher.addUpgradeable(contract_address_const::<0>()); +} + +#[test] +#[available_gas(20000000000)] +#[should_panic(expected: ('1c', 'ENTRYPOINT_FAILED'))] +fn test_zklink_gatekeeper_add_upgradeable_invalid_master2() { + let (v1, v2, gatekeeper) = deploy_zklink(); + let v1_dispatcher = IZklinkUpgradeV1Dispatcher { contract_address: v1 }; + let gatekeeper_dispatcher = IUpgradeGateKeeperDispatcher { contract_address: gatekeeper }; + v1_dispatcher.transferMastership(gatekeeper); + gatekeeper_dispatcher.addUpgradeable(v1); + let newMaster: ContractAddress = contract_address_const::<0x64656661756c7453656e646572>(); + set_contract_address(newMaster); + gatekeeper_dispatcher.startUpgrade(array![]); +} + +#[test] +#[available_gas(20000000000)] +#[should_panic(expected: ('1c', 'ENTRYPOINT_FAILED'))] +fn test_zklink_gatekeeper_add_upgradeable_invalid_master3() { + let (v1, v2, gatekeeper) = deploy_zklink(); + let v1_dispatcher = IZklinkUpgradeV1Dispatcher { contract_address: v1 }; + let gatekeeper_dispatcher = IUpgradeGateKeeperDispatcher { contract_address: gatekeeper }; + v1_dispatcher.transferMastership(gatekeeper); + gatekeeper_dispatcher.addUpgradeable(v1); + let newMaster: ContractAddress = contract_address_const::<0x64656661756c7453656e646572>(); + set_contract_address(newMaster); + gatekeeper_dispatcher.cancelUpgrade(); +} + +#[test] +#[available_gas(20000000000)] +#[should_panic(expected: ('1c', 'ENTRYPOINT_FAILED'))] +fn test_zklink_gatekeeper_add_upgradeable_invalid_master4() { + let (v1, v2, gatekeeper) = deploy_zklink(); + let v1_dispatcher = IZklinkUpgradeV1Dispatcher { contract_address: v1 }; + let gatekeeper_dispatcher = IUpgradeGateKeeperDispatcher { contract_address: gatekeeper }; + v1_dispatcher.transferMastership(gatekeeper); + gatekeeper_dispatcher.addUpgradeable(v1); + let newMaster: ContractAddress = contract_address_const::<0x64656661756c7453656e646572>(); + set_contract_address(newMaster); + gatekeeper_dispatcher.finishUpgrade(); +} + +#[test] +#[available_gas(20000000000)] +#[should_panic(expected: ('cpu11', 'ENTRYPOINT_FAILED'))] +fn test_zklink_gatekeeper_add_upgradeable_invalid_status1() { + let (v1, v2, gatekeeper) = deploy_zklink(); + let v1_dispatcher = IZklinkUpgradeV1Dispatcher { contract_address: v1 }; + let gatekeeper_dispatcher = IUpgradeGateKeeperDispatcher { contract_address: gatekeeper }; + v1_dispatcher.transferMastership(gatekeeper); + gatekeeper_dispatcher.addUpgradeable(v1); + gatekeeper_dispatcher.cancelUpgrade(); +} + +#[test] +#[available_gas(20000000000)] +#[should_panic(expected: ('fpu11', 'ENTRYPOINT_FAILED'))] +fn test_zklink_gatekeeper_add_upgradeable_invalid_status2() { + let (v1, v2, gatekeeper) = deploy_zklink(); + let v1_dispatcher = IZklinkUpgradeV1Dispatcher { contract_address: v1 }; + let gatekeeper_dispatcher = IUpgradeGateKeeperDispatcher { contract_address: gatekeeper }; + v1_dispatcher.transferMastership(gatekeeper); + gatekeeper_dispatcher.addUpgradeable(v1); + gatekeeper_dispatcher.finishUpgrade(); +} + +#[test] +#[available_gas(20000000000)] +#[should_panic(expected: ('spu12', 'ENTRYPOINT_FAILED'))] +fn test_zklink_gatekeeper_add_upgradeable_invalid_status3() { + let (v1, v2, gatekeeper) = deploy_zklink(); + let v1_dispatcher = IZklinkUpgradeV1Dispatcher { contract_address: v1 }; + let gatekeeper_dispatcher = IUpgradeGateKeeperDispatcher { contract_address: gatekeeper }; + v1_dispatcher.transferMastership(gatekeeper); + gatekeeper_dispatcher.addUpgradeable(v1); + gatekeeper_dispatcher.startUpgrade(array![]); +} + +#[test] +#[available_gas(20000000000)] +#[should_panic(expected: ('spu11', 'ENTRYPOINT_FAILED'))] +fn test_zklink_gatekeeper_add_upgradeable_invalid_status4() { + let (v1, v2, gatekeeper) = deploy_zklink(); + let v1_dispatcher = IZklinkUpgradeV1Dispatcher { contract_address: v1 }; + let gatekeeper_dispatcher = IUpgradeGateKeeperDispatcher { contract_address: gatekeeper }; + v1_dispatcher.transferMastership(gatekeeper); + gatekeeper_dispatcher.addUpgradeable(v1); + let v2_class_hash: ClassHash = ZklinkUpgradeV2::TEST_CLASS_HASH.try_into().unwrap(); + gatekeeper_dispatcher.startUpgrade(array![v2_class_hash]); + + gatekeeper_dispatcher.startUpgrade(array![]); +} + +#[test] +#[available_gas(20000000000)] +fn test_zklink_gatekeeper_startUpgradeable_success() { + let (v1, v2, gatekeeper) = deploy_zklink(); + let v1_dispatcher = IZklinkUpgradeV1Dispatcher { contract_address: v1 }; + let gatekeeper_dispatcher = IUpgradeGateKeeperDispatcher { contract_address: gatekeeper }; + v1_dispatcher.transferMastership(gatekeeper); + + gatekeeper_dispatcher.addUpgradeable(v1); + utils::drop_event(gatekeeper); + let v2_class_hash: ClassHash = ZklinkUpgradeV2::TEST_CLASS_HASH.try_into().unwrap(); + gatekeeper_dispatcher.startUpgrade(array![v2_class_hash]); + assert_event_NoticePeriodStart(gatekeeper, 0, array![v2_class_hash], 0); +} + +#[test] +#[available_gas(20000000000)] +fn test_zklink_gatekeeper_cancelUpgrade_success() { + let (v1, v2, gatekeeper) = deploy_zklink(); + let v1_dispatcher = IZklinkUpgradeV1Dispatcher { contract_address: v1 }; + let gatekeeper_dispatcher = IUpgradeGateKeeperDispatcher { contract_address: gatekeeper }; + v1_dispatcher.transferMastership(gatekeeper); + + gatekeeper_dispatcher.addUpgradeable(v1); + utils::drop_event(gatekeeper); + let v2_class_hash: ClassHash = ZklinkUpgradeV2::TEST_CLASS_HASH.try_into().unwrap(); + gatekeeper_dispatcher.startUpgrade(array![v2_class_hash]); + utils::drop_event(gatekeeper); + + gatekeeper_dispatcher.cancelUpgrade(); + assert_event_UpgradeCancel(gatekeeper, 0); +} + +#[test] +#[available_gas(20000000000)] +#[should_panic(expected: ('fpu13', 'ENTRYPOINT_FAILED'))] +fn test_zklink_gatekeeper_finishUpgrade_not_ready() { + let (v1, v2, gatekeeper) = deploy_zklink(); + let v1_dispatcher = IZklinkUpgradeV1Dispatcher { contract_address: v1 }; + let gatekeeper_dispatcher = IUpgradeGateKeeperDispatcher { contract_address: gatekeeper }; + v1_dispatcher.transferMastership(gatekeeper); + + gatekeeper_dispatcher.addUpgradeable(v1); + utils::drop_event(gatekeeper); + let v2_class_hash: ClassHash = ZklinkUpgradeV2::TEST_CLASS_HASH.try_into().unwrap(); + gatekeeper_dispatcher.startUpgrade(array![v2_class_hash]); + utils::drop_event(gatekeeper); + v1_dispatcher.setExodus(true); + gatekeeper_dispatcher.finishUpgrade(); + assert_event_UpgradeCancel(gatekeeper, 0); +} + +#[test] +#[available_gas(20000000000)] +fn test_zklink_gatekeeper_finishUpgrade_not_reach_noticePeriod() { + let (v1, v2, gatekeeper) = deploy_zklink(); + let v1_dispatcher = IZklinkUpgradeV1Dispatcher { contract_address: v1 }; + let gatekeeper_dispatcher = IUpgradeGateKeeperDispatcher { contract_address: gatekeeper }; + v1_dispatcher.transferMastership(gatekeeper); + + gatekeeper_dispatcher.addUpgradeable(v1); + utils::drop_event(gatekeeper); + // set block timestamp to 10 + set_block_timestamp(10); + let v2_class_hash: ClassHash = ZklinkUpgradeV2::TEST_CLASS_HASH.try_into().unwrap(); + gatekeeper_dispatcher.startUpgrade(array![v2_class_hash]); + utils::drop_event(gatekeeper); + + // set block timestamp to 0 + set_block_timestamp(0); + let success = gatekeeper_dispatcher.finishUpgrade(); + assert(!success, 'finishUpgrade should fail'); +} + +#[test] +#[available_gas(20000000000)] +fn test_zklink_gatekeeper_upgrade_success() { + let (v1, v2, gatekeeper) = deploy_zklink(); + let v1_dispatcher = IZklinkUpgradeV1Dispatcher { contract_address: v1 }; + let gatekeeper_dispatcher = IUpgradeGateKeeperDispatcher { contract_address: gatekeeper }; + v1_dispatcher.transferMastership(gatekeeper); + + v1_dispatcher.set_value1(1); + assert(v1_dispatcher.get_value1() == 1, 'v1 value'); + + gatekeeper_dispatcher.addUpgradeable(v1); + utils::drop_event(gatekeeper); + + let v2_class_hash: ClassHash = ZklinkUpgradeV2::TEST_CLASS_HASH.try_into().unwrap(); + gatekeeper_dispatcher.startUpgrade(array![v2_class_hash]); + utils::drop_event(gatekeeper); + + let success = gatekeeper_dispatcher.finishUpgrade(); + assert(success, 'finishUpgrade should success'); + + let v2_dispatcher = IZklinkUpgradeV2Dispatcher { contract_address: v1 }; + v2_dispatcher.set_value2(2); + assert(v2_dispatcher.get_value2() == 2, 'v2 should be upgraded'); + assert(v2_dispatcher.get_value1() == 1, 'v1 should be kept'); +} diff --git a/src/tests/test_ownable.cairo b/src/tests/test_ownable.cairo new file mode 100644 index 0000000..e97466c --- /dev/null +++ b/src/tests/test_ownable.cairo @@ -0,0 +1,135 @@ +use starknet::{ContractAddress, contract_address_const}; +use starknet::testing::set_contract_address; +use debug::PrintTrait; +use zklink::contracts::zklink::Zklink; +use zklink::contracts::zklink::IZklinkDispatcher; +use zklink::contracts::zklink::IZklinkDispatcherTrait; +use zklink::contracts::verifier::Verifier; +use zklink::contracts::verifier::IVerifierDispatcher; +use zklink::contracts::verifier::IVerifierDispatcherTrait; +use zklink::tests::utils; + +fn deploy_zklink() -> (ContractAddress, ContractAddress) { + let deployer: ContractAddress = + contract_address_const::<0x74a0c0f8e8756218a96c2d9aae21152d786a0704202b10fb30496e46222b72d>(); + set_contract_address(deployer); + + let verifier: ContractAddress = utils::deploy(Verifier::TEST_CLASS_HASH, array![]); + + let calldata = array![ + verifier.into(), // verifier + 2, // governor + 0, // blockNumber + 0, // timestamp + utils::GENESIS_ROOT.low.into(), // stateHash low + utils::GENESIS_ROOT.high.into(), // stateHash high + 0, // commitment low + 0, // commitment high + utils::EMPTY_STRING_KECCAK.low.into(), // syncHash low + utils::EMPTY_STRING_KECCAK.high.into(), // syncHash high + ]; + let zklink = utils::deploy(Zklink::TEST_CLASS_HASH, calldata); + (verifier, zklink) +} + +#[test] +#[available_gas(20000000000)] +fn test_zklink_ownable_check_mastership_in_constructor() { + let (verifier, zklink) = deploy_zklink(); + let verifier_dispatcher = IVerifierDispatcher { contract_address: verifier }; + let zklink_dispatcher = IZklinkDispatcher { contract_address: zklink, }; + let deployer: ContractAddress = + contract_address_const::<0x74a0c0f8e8756218a96c2d9aae21152d786a0704202b10fb30496e46222b72d>(); + + assert(verifier_dispatcher.getMaster() == deployer, 'mastership1'); + assert(zklink_dispatcher.getMaster() == deployer, 'mastership2'); +} + +#[test] +#[available_gas(20000000000)] +#[should_panic(expected: ('1d', 'ENTRYPOINT_FAILED'))] +fn test_zklink_ownable_zklink_transfer_mastership_zero() { + let (verifier, zklink) = deploy_zklink(); + let verifier_dispatcher = IVerifierDispatcher { contract_address: verifier }; + let zklink_dispatcher = IZklinkDispatcher { contract_address: zklink, }; + let deployer: ContractAddress = + contract_address_const::<0x74a0c0f8e8756218a96c2d9aae21152d786a0704202b10fb30496e46222b72d>(); + + zklink_dispatcher.transferMastership(contract_address_const::<0>()); +} + +#[test] +#[available_gas(20000000000)] +#[should_panic(expected: ('1d', 'ENTRYPOINT_FAILED'))] +fn test_zklink_ownable_verifier_transfer_mastership_zero() { + let (verifier, zklink) = deploy_zklink(); + let verifier_dispatcher = IVerifierDispatcher { contract_address: verifier }; + let zklink_dispatcher = IZklinkDispatcher { contract_address: zklink, }; + let deployer: ContractAddress = + contract_address_const::<0x74a0c0f8e8756218a96c2d9aae21152d786a0704202b10fb30496e46222b72d>(); + + verifier_dispatcher.transferMastership(contract_address_const::<0>()); +} + +#[test] +#[available_gas(20000000000)] +#[should_panic(expected: ('1c', 'ENTRYPOINT_FAILED'))] +fn test_zklink_ownable_zklink_transfer_mastership_invalid_sender() { + let (verifier, zklink) = deploy_zklink(); + let verifier_dispatcher = IVerifierDispatcher { contract_address: verifier }; + let zklink_dispatcher = IZklinkDispatcher { contract_address: zklink, }; + let deployer: ContractAddress = + contract_address_const::<0x74a0c0f8e8756218a96c2d9aae21152d786a0704202b10fb30496e46222b72d>(); + let newMaster: ContractAddress = contract_address_const::<0x64656661756c7453656e646572>(); + + zklink_dispatcher.transferMastership(newMaster); + assert(zklink_dispatcher.getMaster() == newMaster, 'mastership1'); + + zklink_dispatcher.transferMastership(deployer); +} + +#[test] +#[available_gas(20000000000)] +#[should_panic(expected: ('1c', 'ENTRYPOINT_FAILED'))] +fn test_zklink_ownable_verifier_transfer_mastership_invalid_sender() { + let (verifier, zklink) = deploy_zklink(); + let verifier_dispatcher = IVerifierDispatcher { contract_address: verifier }; + let zklink_dispatcher = IZklinkDispatcher { contract_address: zklink, }; + let deployer: ContractAddress = + contract_address_const::<0x74a0c0f8e8756218a96c2d9aae21152d786a0704202b10fb30496e46222b72d>(); + let newMaster: ContractAddress = contract_address_const::<0x64656661756c7453656e646572>(); + + verifier_dispatcher.transferMastership(newMaster); + assert(verifier_dispatcher.getMaster() == newMaster, 'mastership1'); + + verifier_dispatcher.transferMastership(deployer); +} + +#[test] +#[available_gas(20000000000)] +fn test_zklink_ownable_transfer_mastership_back_success() { + let (verifier, zklink) = deploy_zklink(); + let verifier_dispatcher = IVerifierDispatcher { contract_address: verifier }; + let zklink_dispatcher = IZklinkDispatcher { contract_address: zklink, }; + let deployer: ContractAddress = + contract_address_const::<0x74a0c0f8e8756218a96c2d9aae21152d786a0704202b10fb30496e46222b72d>(); + let newMaster: ContractAddress = contract_address_const::<0x64656661756c7453656e646572>(); + + // zklink + zklink_dispatcher.transferMastership(newMaster); + assert(zklink_dispatcher.getMaster() == newMaster, 'mastership1'); + + // verifier + verifier_dispatcher.transferMastership(newMaster); + assert(verifier_dispatcher.getMaster() == newMaster, 'mastership2'); + + set_contract_address(newMaster); + + // zklink + zklink_dispatcher.transferMastership(deployer); + assert(zklink_dispatcher.getMaster() == deployer, 'mastership3'); + + // verifier + verifier_dispatcher.transferMastership(deployer); + assert(verifier_dispatcher.getMaster() == deployer, 'mastership4'); +} diff --git a/src/utils/data_structures.cairo b/src/utils/data_structures.cairo index fac9ddd..21cc74c 100644 --- a/src/utils/data_structures.cairo +++ b/src/utils/data_structures.cairo @@ -122,4 +122,11 @@ mod DataStructures { vkIndexes: Array, subproofsLimbs: Array } + + #[derive(Copy, Drop, PartialEq, Serde, starknet::Store)] + // Upgrade mode statuses + enum UpgradeStatus { + Idle: (), + NoticePeriod: () + } }