From c39a881d267aa539cb4fe822042b76f1f319fc1c Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 22 May 2024 11:37:59 -0400 Subject: [PATCH 001/103] start timelock comp draft --- src/governance.cairo | 1 + src/governance/timelock_controller.cairo | 233 +++++++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 src/governance/timelock_controller.cairo diff --git a/src/governance.cairo b/src/governance.cairo index 95adf51c8..e75a5a9d7 100644 --- a/src/governance.cairo +++ b/src/governance.cairo @@ -1 +1,2 @@ +mod timelock_controller; mod utils; diff --git a/src/governance/timelock_controller.cairo b/src/governance/timelock_controller.cairo new file mode 100644 index 000000000..fbafae492 --- /dev/null +++ b/src/governance/timelock_controller.cairo @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.13.0 (governance/timelock_controller.cairo) + +/// # TimelockController Component +/// +/// +#[starknet::component] +mod TimelockControllerComponent { + use openzeppelin::access::accesscontrol::AccessControlComponent; + use openzeppelin::access::accesscontrol::AccessControlComponent::InternalTrait as AccessControlInternalTrait; + use openzeppelin::access::accesscontrol::AccessControlComponent::AccessControlImpl; + use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; + use openzeppelin::introspection::src5::SRC5Component::SRC5; + use openzeppelin::introspection::src5::SRC5Component; + use starknet::ContractAddress; + + use poseidon::PoseidonTrait; + use hash::{HashStateTrait, HashStateExTrait}; + use starknet::SyscallResultTrait; + + + // Constants + const PROPOSER_ROLE: felt252 = selector!("PROPOSER_ROLE"); + const EXECUTOR_ROLE: felt252 = selector!("EXECUTOR_ROLE"); + const CANCELLER_ROLE: felt252 = selector!("CANCELLER_ROLE"); + const DONE_TIMESTAMP: u64 = 1; + + #[storage] + struct Storage { + TimelockController_timestamps: LegacyMap, + TimelockController_min_delay: u64 + } + + #[event] + #[derive(Drop, PartialEq, starknet::Event)] + enum Event { + CallScheduled: CallScheduled, + CallExecuted: CallExecuted, + CallSalt: CallSalt, + Cancelled: Cancelled, + MinDelayChange: MinDelayChange + } + + /// Emitted when... + #[derive(Drop, PartialEq, starknet::Event)] + struct CallScheduled { + #[key] + id: u32, + #[key] + index: felt252, + target: ContractAddress, + value: u256, + _data: Span, + predecessor: u32, + delay: u64 + } + + /// Emitted when... + #[derive(Drop, PartialEq, starknet::Event)] + struct CallExecuted { + #[key] + id: u32, + #[key] + index: felt252, + target: ContractAddress, + value: u256, + _data: Span + } + + /// Emitted when... + #[derive(Drop, PartialEq, starknet::Event)] + struct CallSalt { + #[key] + id: u32, + salt: u32 + } + + /// Emitted when... + #[derive(Drop, PartialEq, starknet::Event)] + struct Cancelled { + #[key] + id: u32 + } + + /// Emitted when... + #[derive(Drop, PartialEq, starknet::Event)] + struct MinDelayChange { + old_duration: u64, + new_duration: u64 + } + + mod Errors { + const INVALID_CLASS: felt252 = 'Class hash cannot be zero'; + const INVALID_OPERATION_LEN: felt252 = 'Timelock: invalid operation len'; + const INSUFFICIENT_DELAY: felt252 = 'Timelock: insufficient delay'; + const UNEXPECTED_OPERATION_STATE: felt252 = 'Timelock: unexpected op state'; + const UNEXPECTED_PREDECESSOR: felt252 = 'Timelock: unexpected predessor'; + const UNAUTHORIZED_CALLER: felt252 = 'Timelock: unauthorized caller'; + } + + #[generate_trait] + impl ExternalImpl< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + impl AccessControl: AccessControlComponent::HasComponent, + > of ExternalTrait{ + fn is_operation(self: @ComponentState, id: u32) -> bool { + true + } + + fn is_operation_pending(self: @ComponentState, id: u32) -> bool { + true + } + + fn is_operation_ready(self: @ComponentState, id: u32) -> bool { + true + } + + fn is_operation_done(self: @ComponentState, id: u32) -> bool { + true + } + + fn get_timestamp(self: @ComponentState, id: u32) -> u64 { + self.TimelockController_timestamps.read(id) + } + + fn get_operation_state(self: @ComponentState, id: u32) -> OperationState { + let timestamp = self.get_timestamp(id); + if (timestamp == 0) { + return OperationState::Unset; + } else if (timestamp == DONE_TIMESTAMP) { + return OperationState::Done; + } else if (timestamp > starknet::get_block_timestamp()) { + return OperationState::Waiting; + } else { + return OperationState::Ready; + } + } + + fn get_min_delay(self: @ComponentState) -> u64 { + self.TimelockController_min_delay.read() + } + + fn hash_operation( + ref self: ComponentState, + target: ContractAddress, + value: u256, + data: Span, + predecessor: u32, + salt: u32 + ) -> felt252 { + self.hash_operations(array![target].span(), array![value].span(), data, predecessor, salt) + } + + fn hash_operations( + ref self: ComponentState, + targets: Span, + values: Span, + payloads: Span, + predecessor: u32, + salt: u32 + ) -> felt252 { + // todo + 1 + } + + fn schedule( + ref self: ComponentState, + target: ContractAddress, + value: u256, + data: Span, + predecessor: u32, + salt: u32, + delay: u64 + ) { // onlyRole(PROPOSER_ROLE) + //bytes32 id = hashOperation(target, value, data, predecessor, salt); + //_schedule(id, delay); + //emit CallScheduled(id, 0, target, value, data, predecessor, delay); + //if (salt != bytes32(0)) { + // emit CallSalt(id, salt); + //} + } + + fn schedule_batch( + ref self: ComponentState, + targets: Span, + values: Span, + payloads: Span, + predecessor: u32, + salt: u32, + delay: u64 + ) { // onlyRole(PROPOSER_ROLE) + //if (targets.length != values.length || targets.length != payloads.length) { + // revert TimelockInvalidOperationLength(targets.length, payloads.length, values.length); + //} + + //bytes32 id = hashOperationBatch(targets, values, payloads, predecessor, salt); + //_schedule(id, delay); + //for (uint256 i = 0; i < targets.length; ++i) { + // emit CallScheduled(id, i, targets[i], values[i], payloads[i], predecessor, delay); + //} + //if (salt != bytes32(0)) { + // emit CallSalt(id, salt); + //} + } + + } + + #[derive(Drop)] + enum OperationState { + Unset, + Waiting, + Ready, + Done + } + + #[generate_trait] + impl InternalImpl< + TContractState, +HasComponent + > of InternalTrait { + /// Document me... + /// + /// + fn initializer( + ref self: TContractState, + min_delay: felt252, + proposers: Span, + executors: Span, + admin: ContractAddress + ) {} + } +} From 2caf2776a2e38afc86a383a5c95fb44fed6a0aa3 Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 25 May 2024 19:17:25 -0400 Subject: [PATCH 002/103] move timelock to own dir, finish drafting component --- src/governance.cairo | 2 +- src/governance/timelock.cairo | 4 + src/governance/timelock/interface.cairo | 37 ++ .../timelock/timelock_controller.cairo | 322 ++++++++++++++++++ src/governance/timelock_controller.cairo | 233 ------------- 5 files changed, 364 insertions(+), 234 deletions(-) create mode 100644 src/governance/timelock.cairo create mode 100644 src/governance/timelock/interface.cairo create mode 100644 src/governance/timelock/timelock_controller.cairo delete mode 100644 src/governance/timelock_controller.cairo diff --git a/src/governance.cairo b/src/governance.cairo index e75a5a9d7..fab2c9d92 100644 --- a/src/governance.cairo +++ b/src/governance.cairo @@ -1,2 +1,2 @@ -mod timelock_controller; +mod timelock; mod utils; diff --git a/src/governance/timelock.cairo b/src/governance/timelock.cairo new file mode 100644 index 000000000..36c320c5c --- /dev/null +++ b/src/governance/timelock.cairo @@ -0,0 +1,4 @@ +mod interface; +mod timelock_controller; + +use timelock_controller::TimelockControllerComponent; diff --git a/src/governance/timelock/interface.cairo b/src/governance/timelock/interface.cairo new file mode 100644 index 000000000..f60691785 --- /dev/null +++ b/src/governance/timelock/interface.cairo @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.13.0 (governance/timelock/interface.cairo) + +/// # TimelockController Component +/// +/// + +use starknet::ContractAddress; +use starknet::account::Call; + +#[derive(Drop, Copy, Serde, PartialEq)] +enum OperationState { + Unset, + Waiting, + Ready, + Done +} + +#[starknet::interface] +trait ITimelock { + fn is_operation(self: @TState, id: felt252) -> bool; + fn is_operation_pending(self: @TState, id: felt252) -> bool; + fn is_operation_ready(self: @TState, id: felt252) -> bool; + fn is_operation_done(self: @TState, id: felt252) -> bool; + fn get_timestamp(self: @TState, id: felt252) -> u64; + fn get_operation_state(self: @TState, id: felt252) -> OperationState; + fn get_min_delay(self: @TState) -> u64; + fn hash_operation( + self: @TState, calls: Span, predecessor: felt252, salt: felt252 + ) -> felt252; + fn schedule( + ref self: TState, calls: Span, predecessor: felt252, salt: felt252, delay: u64 + ); + fn cancel(ref self: TState, id: felt252); + fn execute(ref self: TState, calls: Span, predecessor: felt252, salt: felt252); + fn update_delay(ref self: TState, new_delay: u64); +} diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo new file mode 100644 index 000000000..1d345c736 --- /dev/null +++ b/src/governance/timelock/timelock_controller.cairo @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.13.0 (governance/timelock/timelock_controller.cairo) + +/// # TimelockController Component +/// +/// +#[starknet::component] +mod TimelockControllerComponent { + use hash::{HashStateTrait, HashStateExTrait}; + use openzeppelin::access::accesscontrol::AccessControlComponent::AccessControlImpl; + use openzeppelin::access::accesscontrol::AccessControlComponent::InternalTrait as AccessControlInternalTrait; + use openzeppelin::access::accesscontrol::AccessControlComponent; + use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; + use openzeppelin::account::utils::execute_single_call; + use openzeppelin::governance::timelock::interface::{ITimelock, OperationState}; + use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; + use openzeppelin::introspection::src5::SRC5Component::SRC5; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent::InternalImpl as ERC1155InternalImpl; + use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent; + use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent::InternalImpl as ERC721InternalImpl; + use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent; + use openzeppelin::utils::serde::{CallPartialEq, HashCallImpl}; + use poseidon::PoseidonTrait; + use starknet::ContractAddress; + use starknet::SyscallResultTrait; + use starknet::account::Call; + use zeroable::Zeroable; + + // Constants + const PROPOSER_ROLE: felt252 = selector!("PROPOSER_ROLE"); + const EXECUTOR_ROLE: felt252 = selector!("EXECUTOR_ROLE"); + const CANCELLER_ROLE: felt252 = selector!("CANCELLER_ROLE"); + const DONE_TIMESTAMP: u64 = 1; + + #[storage] + struct Storage { + TimelockController_timestamps: LegacyMap, + TimelockController_min_delay: u64 + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + CallScheduled: CallScheduled, + CallExecuted: CallExecuted, + CallSalt: CallSalt, + Cancelled: Cancelled, + MinDelayChange: MinDelayChange + } + + /// Emitted when... + #[derive(Drop, PartialEq, starknet::Event)] + struct CallScheduled { + #[key] + calls: Span, + predecessor: felt252, + delay: u64 + } + + /// Emitted when... + #[derive(Drop, PartialEq, starknet::Event)] + struct CallExecuted { + #[key] + id: felt252, + #[key] + calls: Span + } + + /// Emitted when... + #[derive(Drop, PartialEq, starknet::Event)] + struct CallSalt { + #[key] + id: felt252, + salt: felt252 + } + + /// Emitted when... + #[derive(Drop, PartialEq, starknet::Event)] + struct Cancelled { + #[key] + id: felt252 + } + + /// Emitted when... + #[derive(Drop, PartialEq, starknet::Event)] + struct MinDelayChange { + old_duration: u64, + new_duration: u64 + } + + mod Errors { + const INVALID_CLASS: felt252 = 'Class hash cannot be zero'; + const INVALID_OPERATION_LEN: felt252 = 'Timelock: invalid operation len'; + const INSUFFICIENT_DELAY: felt252 = 'Timelock: insufficient delay'; + const UNEXPECTED_OPERATION_STATE: felt252 = 'Timelock: unexpected op state'; + const UNEXECUTED_PREDECESSOR: felt252 = 'Timelock: unexecuted predessor'; + const UNAUTHORIZED_CALLER: felt252 = 'Timelock: unauthorized caller'; + } + + #[embeddable_as(TimelockImpl)] + impl Timelock< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + +AccessControlComponent::HasComponent, + +ERC721ReceiverComponent::HasComponent, + +ERC1155ReceiverComponent::HasComponent, + +Drop + > of ITimelock> { + fn is_operation(self: @ComponentState, id: felt252) -> bool { + self.get_operation_state(id) != OperationState::Unset + } + + fn is_operation_pending(self: @ComponentState, id: felt252) -> bool { + let state = self.get_operation_state(id); + state == OperationState::Waiting || state == OperationState::Ready + } + + fn is_operation_ready(self: @ComponentState, id: felt252) -> bool { + self.get_operation_state(id) == OperationState::Ready + } + + fn is_operation_done(self: @ComponentState, id: felt252) -> bool { + self.get_operation_state(id) == OperationState::Done + } + + fn get_timestamp(self: @ComponentState, id: felt252) -> u64 { + self.TimelockController_timestamps.read(id) + } + + fn get_operation_state( + self: @ComponentState, id: felt252 + ) -> OperationState { + let timestamp = self.get_timestamp(id); + if (timestamp == 0) { + return OperationState::Unset; + } else if (timestamp == DONE_TIMESTAMP) { + return OperationState::Done; + } else if (timestamp > starknet::get_block_timestamp()) { + return OperationState::Waiting; + } else { + return OperationState::Ready; + } + } + + fn get_min_delay(self: @ComponentState) -> u64 { + self.TimelockController_min_delay.read() + } + + fn hash_operation( + self: @ComponentState, + calls: Span, + predecessor: felt252, + salt: felt252 + ) -> felt252 { + PoseidonTrait::new() + .update_with(@calls) + .update_with(predecessor) + .update_with(salt) + .finalize() + } + + fn schedule( + ref self: ComponentState, + calls: Span, + predecessor: felt252, + salt: felt252, + delay: u64 + ) { + self.assert_only_role(PROPOSER_ROLE); + + let id = self.hash_operation(calls, predecessor, salt); + self._schedule(id, delay); + self.emit(CallScheduled { calls, predecessor, delay }); + + if salt != 0 { + self.emit(CallSalt { id, salt }); + } + } + + fn cancel(ref self: ComponentState, id: felt252) { + self.assert_only_role(CANCELLER_ROLE); + assert(self.is_operation_pending(id), Errors::UNEXPECTED_OPERATION_STATE); + + self.TimelockController_timestamps.write(id, 0); + self.emit(Cancelled { id }); + } + + fn execute( + ref self: ComponentState, + calls: Span, + predecessor: felt252, + salt: felt252 + ) { + self.assert_only_role(EXECUTOR_ROLE); + + let id = self.hash_operation(calls, predecessor, salt); + self.before_call(id, predecessor); + self._execute(calls); + self.emit(CallExecuted { id, calls }); + self.after_call(id); + } + + fn update_delay(ref self: ComponentState, new_delay: u64) { + let this = starknet::get_contract_address(); + let caller = starknet::get_caller_address(); + assert(caller == this, Errors::UNAUTHORIZED_CALLER); + + let min_delay = self.TimelockController_min_delay.read(); + self.emit(MinDelayChange { old_duration: min_delay, new_duration: new_delay }); + + self.TimelockController_min_delay.write(new_delay); + } + } + + + #[generate_trait] + impl InternalImpl< + TContractState, + +HasComponent, + impl SRC5: SRC5Component::HasComponent, + impl AccessControl: AccessControlComponent::HasComponent, + impl ERC721Receiver: ERC721ReceiverComponent::HasComponent, + impl ERC1155Receiver: ERC1155ReceiverComponent::HasComponent, + +Drop + > of InternalTrait { + /// Document me... + /// + /// + fn initializer( + ref self: ComponentState, + min_delay: u64, + proposers: Span, + executors: Span, + admin: ContractAddress + ) { + // Register as token receivers + let mut erc721_receiver = get_dep_component_mut!(ref self, ERC721Receiver); + erc721_receiver.initializer(); + + let mut erc1155_receiver = get_dep_component_mut!(ref self, ERC1155Receiver); + erc1155_receiver.initializer(); + + // Self administration + let mut access_component = get_dep_component_mut!(ref self, AccessControl); + access_component._grant_role(DEFAULT_ADMIN_ROLE, starknet::get_contract_address()); + + // Optional admin + if admin != Zeroable::zero() { + access_component._grant_role(DEFAULT_ADMIN_ROLE, admin) + }; + + // Register proposers and cancellers + let mut i = 0; + loop { + if i == proposers.len() { + break; + } + + let mut proposer = proposers.at(i); + access_component._grant_role(PROPOSER_ROLE, *proposer); + access_component._grant_role(CANCELLER_ROLE, *proposer); + i = i + 1; + }; + + // Register executors + let mut i = 0; + loop { + if i == executors.len() { + break; + } + + let mut executor = executors.at(i); + access_component._grant_role(EXECUTOR_ROLE, *executor); + i = i + 1; + }; + + self.emit(MinDelayChange { old_duration: 0, new_duration: min_delay }) + } + + fn assert_only_role(self: @ComponentState, role: felt252) { + let access_component = get_dep_component!(self, AccessControl); + access_component.assert_only_role(role); + } + + fn before_call(self: @ComponentState, id: felt252, predecessor: felt252) { + assert(self.is_operation_ready(id), Errors::UNEXPECTED_OPERATION_STATE); + assert( + predecessor != 0 && !self.is_operation_done(predecessor), + Errors::UNEXECUTED_PREDECESSOR + ); + } + + fn after_call(ref self: ComponentState, id: felt252) { + assert(!self.is_operation_ready(id), Errors::UNEXPECTED_OPERATION_STATE); + self.TimelockController_timestamps.write(id, DONE_TIMESTAMP); + } + + fn _schedule(ref self: ComponentState, id: felt252, delay: u64) { + assert(self.is_operation(id), Errors::UNEXPECTED_OPERATION_STATE); + assert(self.get_min_delay() < delay, Errors::INSUFFICIENT_DELAY); + self.TimelockController_timestamps.write(id, starknet::get_block_timestamp() + delay); + } + + fn _execute(ref self: ComponentState, mut calls: Span) { + let mut index = 0; + loop { + if index == calls.len() { + break; + } + + let mut call = Call { + to: *calls.at(index).to, + selector: *calls.at(index).selector, + calldata: *calls.at(index).calldata + }; + execute_single_call(call); + } + } + } +} diff --git a/src/governance/timelock_controller.cairo b/src/governance/timelock_controller.cairo deleted file mode 100644 index fbafae492..000000000 --- a/src/governance/timelock_controller.cairo +++ /dev/null @@ -1,233 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.13.0 (governance/timelock_controller.cairo) - -/// # TimelockController Component -/// -/// -#[starknet::component] -mod TimelockControllerComponent { - use openzeppelin::access::accesscontrol::AccessControlComponent; - use openzeppelin::access::accesscontrol::AccessControlComponent::InternalTrait as AccessControlInternalTrait; - use openzeppelin::access::accesscontrol::AccessControlComponent::AccessControlImpl; - use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; - use openzeppelin::introspection::src5::SRC5Component::SRC5; - use openzeppelin::introspection::src5::SRC5Component; - use starknet::ContractAddress; - - use poseidon::PoseidonTrait; - use hash::{HashStateTrait, HashStateExTrait}; - use starknet::SyscallResultTrait; - - - // Constants - const PROPOSER_ROLE: felt252 = selector!("PROPOSER_ROLE"); - const EXECUTOR_ROLE: felt252 = selector!("EXECUTOR_ROLE"); - const CANCELLER_ROLE: felt252 = selector!("CANCELLER_ROLE"); - const DONE_TIMESTAMP: u64 = 1; - - #[storage] - struct Storage { - TimelockController_timestamps: LegacyMap, - TimelockController_min_delay: u64 - } - - #[event] - #[derive(Drop, PartialEq, starknet::Event)] - enum Event { - CallScheduled: CallScheduled, - CallExecuted: CallExecuted, - CallSalt: CallSalt, - Cancelled: Cancelled, - MinDelayChange: MinDelayChange - } - - /// Emitted when... - #[derive(Drop, PartialEq, starknet::Event)] - struct CallScheduled { - #[key] - id: u32, - #[key] - index: felt252, - target: ContractAddress, - value: u256, - _data: Span, - predecessor: u32, - delay: u64 - } - - /// Emitted when... - #[derive(Drop, PartialEq, starknet::Event)] - struct CallExecuted { - #[key] - id: u32, - #[key] - index: felt252, - target: ContractAddress, - value: u256, - _data: Span - } - - /// Emitted when... - #[derive(Drop, PartialEq, starknet::Event)] - struct CallSalt { - #[key] - id: u32, - salt: u32 - } - - /// Emitted when... - #[derive(Drop, PartialEq, starknet::Event)] - struct Cancelled { - #[key] - id: u32 - } - - /// Emitted when... - #[derive(Drop, PartialEq, starknet::Event)] - struct MinDelayChange { - old_duration: u64, - new_duration: u64 - } - - mod Errors { - const INVALID_CLASS: felt252 = 'Class hash cannot be zero'; - const INVALID_OPERATION_LEN: felt252 = 'Timelock: invalid operation len'; - const INSUFFICIENT_DELAY: felt252 = 'Timelock: insufficient delay'; - const UNEXPECTED_OPERATION_STATE: felt252 = 'Timelock: unexpected op state'; - const UNEXPECTED_PREDECESSOR: felt252 = 'Timelock: unexpected predessor'; - const UNAUTHORIZED_CALLER: felt252 = 'Timelock: unauthorized caller'; - } - - #[generate_trait] - impl ExternalImpl< - TContractState, - +HasComponent, - +SRC5Component::HasComponent, - impl AccessControl: AccessControlComponent::HasComponent, - > of ExternalTrait{ - fn is_operation(self: @ComponentState, id: u32) -> bool { - true - } - - fn is_operation_pending(self: @ComponentState, id: u32) -> bool { - true - } - - fn is_operation_ready(self: @ComponentState, id: u32) -> bool { - true - } - - fn is_operation_done(self: @ComponentState, id: u32) -> bool { - true - } - - fn get_timestamp(self: @ComponentState, id: u32) -> u64 { - self.TimelockController_timestamps.read(id) - } - - fn get_operation_state(self: @ComponentState, id: u32) -> OperationState { - let timestamp = self.get_timestamp(id); - if (timestamp == 0) { - return OperationState::Unset; - } else if (timestamp == DONE_TIMESTAMP) { - return OperationState::Done; - } else if (timestamp > starknet::get_block_timestamp()) { - return OperationState::Waiting; - } else { - return OperationState::Ready; - } - } - - fn get_min_delay(self: @ComponentState) -> u64 { - self.TimelockController_min_delay.read() - } - - fn hash_operation( - ref self: ComponentState, - target: ContractAddress, - value: u256, - data: Span, - predecessor: u32, - salt: u32 - ) -> felt252 { - self.hash_operations(array![target].span(), array![value].span(), data, predecessor, salt) - } - - fn hash_operations( - ref self: ComponentState, - targets: Span, - values: Span, - payloads: Span, - predecessor: u32, - salt: u32 - ) -> felt252 { - // todo - 1 - } - - fn schedule( - ref self: ComponentState, - target: ContractAddress, - value: u256, - data: Span, - predecessor: u32, - salt: u32, - delay: u64 - ) { // onlyRole(PROPOSER_ROLE) - //bytes32 id = hashOperation(target, value, data, predecessor, salt); - //_schedule(id, delay); - //emit CallScheduled(id, 0, target, value, data, predecessor, delay); - //if (salt != bytes32(0)) { - // emit CallSalt(id, salt); - //} - } - - fn schedule_batch( - ref self: ComponentState, - targets: Span, - values: Span, - payloads: Span, - predecessor: u32, - salt: u32, - delay: u64 - ) { // onlyRole(PROPOSER_ROLE) - //if (targets.length != values.length || targets.length != payloads.length) { - // revert TimelockInvalidOperationLength(targets.length, payloads.length, values.length); - //} - - //bytes32 id = hashOperationBatch(targets, values, payloads, predecessor, salt); - //_schedule(id, delay); - //for (uint256 i = 0; i < targets.length; ++i) { - // emit CallScheduled(id, i, targets[i], values[i], payloads[i], predecessor, delay); - //} - //if (salt != bytes32(0)) { - // emit CallSalt(id, salt); - //} - } - - } - - #[derive(Drop)] - enum OperationState { - Unset, - Waiting, - Ready, - Done - } - - #[generate_trait] - impl InternalImpl< - TContractState, +HasComponent - > of InternalTrait { - /// Document me... - /// - /// - fn initializer( - ref self: TContractState, - min_delay: felt252, - proposers: Span, - executors: Span, - admin: ContractAddress - ) {} - } -} From 54db1f776dcb7fb9531ea27461ed7f2a976d14eb Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 25 May 2024 19:20:39 -0400 Subject: [PATCH 003/103] tmp: add utility impls --- .../timelock/timelock_controller.cairo | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 1d345c736..2b1e6e5ef 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.13.0 (governance/timelock/timelock_controller.cairo) +use core::hash::{HashStateTrait, HashStateExTrait, Hash}; +use starknet::account::Call; + /// # TimelockController Component /// /// @@ -20,7 +23,7 @@ mod TimelockControllerComponent { use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent; use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent::InternalImpl as ERC721InternalImpl; use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent; - use openzeppelin::utils::serde::{CallPartialEq, HashCallImpl}; + use super::{CallPartialEq, HashCallImpl}; use poseidon::PoseidonTrait; use starknet::ContractAddress; use starknet::SyscallResultTrait; @@ -320,3 +323,35 @@ mod TimelockControllerComponent { } } } + + +impl HashCallImpl, +HashStateTrait, +Drop> of Hash<@Call, S> { + fn update_state(mut state: S, value: @Call) -> S { + let mut arr = array![]; + Serde::serialize(value, ref arr); + state = state.update(arr.len().into()); + while let Option::Some(elem) = arr.pop_front() { + state = state.update(elem) + }; + state + } +} + +impl CallPartialEq of PartialEq { + #[inline(always)] + fn eq(lhs: @Call, rhs: @Call) -> bool { + let mut lhs_arr = array![]; + Serde::serialize(lhs, ref lhs_arr); + let mut rhs_arr = array![]; + Serde::serialize(lhs, ref rhs_arr); + lhs_arr == rhs_arr + } + + fn ne(lhs: @Call, rhs: @Call) -> bool { + let mut lhs_arr = array![]; + Serde::serialize(lhs, ref lhs_arr); + let mut rhs_arr = array![]; + Serde::serialize(lhs, ref rhs_arr); + !(lhs_arr == rhs_arr) + } +} \ No newline at end of file From c48521cfe1a5dc356a20b671c69ad0f778272375 Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 25 May 2024 19:22:16 -0400 Subject: [PATCH 004/103] add timelock mock --- src/tests/mocks.cairo | 1 + src/tests/mocks/timelock_mocks.cairo | 65 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/tests/mocks/timelock_mocks.cairo diff --git a/src/tests/mocks.cairo b/src/tests/mocks.cairo index 143f1c6e3..5c4b2df34 100644 --- a/src/tests/mocks.cairo +++ b/src/tests/mocks.cairo @@ -14,4 +14,5 @@ mod ownable_mocks; mod pausable_mocks; mod reentrancy_mocks; mod src5_mocks; +mod timelock_mocks; mod upgrades_mocks; diff --git a/src/tests/mocks/timelock_mocks.cairo b/src/tests/mocks/timelock_mocks.cairo new file mode 100644 index 000000000..095b4b906 --- /dev/null +++ b/src/tests/mocks/timelock_mocks.cairo @@ -0,0 +1,65 @@ +#[starknet::contract] +mod TimelockControllerMock { + use openzeppelin::access::accesscontrol::AccessControlComponent; + use openzeppelin::governance::timelock::TimelockControllerComponent; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc1155::ERC1155ReceiverComponent; + use openzeppelin::token::erc721::ERC721ReceiverComponent; + + component!(path: AccessControlComponent, storage: access_control, event: AccessControlEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: TimelockControllerComponent, storage: timelock, event: TimelockEvent); + component!(path: ERC721ReceiverComponent, storage: erc721_receiver, event: ERC721ReceiverEvent); + component!( + path: ERC1155ReceiverComponent, storage: erc1155_receiver, event: ERC1155ReceiverEvent + ); + + #[abi(embed_v0)] + impl AccessControlImpl = + AccessControlComponent::AccessControlImpl; + impl AccessControlInternalImpl = AccessControlComponent::InternalImpl; + + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[abi(embed_v0)] + impl TimelockImpl = TimelockControllerComponent::TimelockImpl; + impl TimelockInternalImpl = TimelockControllerComponent::InternalImpl; + + // ERC721Receiver + impl ERC721ReceiverImpl = ERC721ReceiverComponent::ERC721ReceiverImpl; + impl ERC721ReceiverInternalImpl = ERC721ReceiverComponent::InternalImpl; + + // ERC721Receiver + impl ERC1155ReceiverImpl = ERC1155ReceiverComponent::ERC1155ReceiverImpl; + impl ERC1155ReceiverInternalImpl = ERC1155ReceiverComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + access_control: AccessControlComponent::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + timelock: TimelockControllerComponent::Storage, + #[substorage(v0)] + erc721_receiver: ERC721ReceiverComponent::Storage, + #[substorage(v0)] + erc1155_receiver: ERC1155ReceiverComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + AccessControlEvent: AccessControlComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] + TimelockEvent: TimelockControllerComponent::Event, + #[flat] + ERC721ReceiverEvent: ERC721ReceiverComponent::Event, + #[flat] + ERC1155ReceiverEvent: ERC1155ReceiverComponent::Event, + } +} From 61a92583998733d8f55311b5a05ce1d740301a45 Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 25 May 2024 19:22:28 -0400 Subject: [PATCH 005/103] add test mod for timelock --- src/tests.cairo | 2 ++ src/tests/governance.cairo | 1 + src/tests/governance/test_timelock.cairo | 0 3 files changed, 3 insertions(+) create mode 100644 src/tests/governance.cairo create mode 100644 src/tests/governance/test_timelock.cairo diff --git a/src/tests.cairo b/src/tests.cairo index fd6ee25fd..c7a7ef29f 100644 --- a/src/tests.cairo +++ b/src/tests.cairo @@ -5,6 +5,8 @@ mod account; #[cfg(test)] mod cryptography; #[cfg(test)] +mod governance; +#[cfg(test)] mod introspection; #[cfg(test)] mod mocks; diff --git a/src/tests/governance.cairo b/src/tests/governance.cairo new file mode 100644 index 000000000..c983e42d7 --- /dev/null +++ b/src/tests/governance.cairo @@ -0,0 +1 @@ +mod test_timelock; diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo new file mode 100644 index 000000000..e69de29bb From df1d88634671620edce10697d56b20b6f43978eb Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 25 May 2024 19:23:12 -0400 Subject: [PATCH 006/103] fix commnet --- src/tests/mocks/timelock_mocks.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/mocks/timelock_mocks.cairo b/src/tests/mocks/timelock_mocks.cairo index 095b4b906..672671bf2 100644 --- a/src/tests/mocks/timelock_mocks.cairo +++ b/src/tests/mocks/timelock_mocks.cairo @@ -30,7 +30,7 @@ mod TimelockControllerMock { impl ERC721ReceiverImpl = ERC721ReceiverComponent::ERC721ReceiverImpl; impl ERC721ReceiverInternalImpl = ERC721ReceiverComponent::InternalImpl; - // ERC721Receiver + // ERC1155Receiver impl ERC1155ReceiverImpl = ERC1155ReceiverComponent::ERC1155ReceiverImpl; impl ERC1155ReceiverInternalImpl = ERC1155ReceiverComponent::InternalImpl; From 40ad55430ad97456018ff4a94b7d920e5f65d2ab Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 26 May 2024 03:44:08 -0400 Subject: [PATCH 007/103] add constructor to timelock mock --- src/tests/mocks/timelock_mocks.cairo | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/tests/mocks/timelock_mocks.cairo b/src/tests/mocks/timelock_mocks.cairo index 672671bf2..75c802a47 100644 --- a/src/tests/mocks/timelock_mocks.cairo +++ b/src/tests/mocks/timelock_mocks.cairo @@ -5,6 +5,7 @@ mod TimelockControllerMock { use openzeppelin::introspection::src5::SRC5Component; use openzeppelin::token::erc1155::ERC1155ReceiverComponent; use openzeppelin::token::erc721::ERC721ReceiverComponent; + use starknet::ContractAddress; component!(path: AccessControlComponent, storage: access_control, event: AccessControlEvent); component!(path: SRC5Component, storage: src5, event: SRC5Event); @@ -62,4 +63,15 @@ mod TimelockControllerMock { #[flat] ERC1155ReceiverEvent: ERC1155ReceiverComponent::Event, } + + #[constructor] + fn constructor( + ref self: ContractState, + min_delay: u64, + proposers: Span, + executors: Span, + admin: ContractAddress + ) { + self.timelock.initializer(min_delay, proposers, executors, admin); + } } From f009bd93c7be3e70f427460e947c3275d62ca65d Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 26 May 2024 03:44:40 -0400 Subject: [PATCH 008/103] set min_delay in initializer --- src/governance/timelock/timelock_controller.cairo | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 2b1e6e5ef..440ebfc84 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -43,7 +43,7 @@ mod TimelockControllerComponent { } #[event] - #[derive(Drop, starknet::Event)] + #[derive(Drop, PartialEq, starknet::Event)] enum Event { CallScheduled: CallScheduled, CallExecuted: CallExecuted, @@ -279,6 +279,7 @@ mod TimelockControllerComponent { i = i + 1; }; + self.TimelockController_min_delay.write(min_delay); self.emit(MinDelayChange { old_duration: 0, new_duration: min_delay }) } From 43bf7948bb4c1d5f0cec6bf550871dd2f1a4cebc Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 26 May 2024 03:44:49 -0400 Subject: [PATCH 009/103] start tests --- src/tests/governance/test_timelock.cairo | 228 +++++++++++++++++++++++ 1 file changed, 228 insertions(+) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index e69de29bb..91fc4599c 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -0,0 +1,228 @@ +use openzeppelin::governance::timelock::interface::ITimelock; +use openzeppelin::introspection::interface::ISRC5_ID; +use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; +use openzeppelin::governance::timelock::interface::{ITimelockDispatcher, ITimelockDispatcherTrait}; +use openzeppelin::governance::timelock::TimelockControllerComponent; +use openzeppelin::governance::timelock::TimelockControllerComponent::{CallScheduled, CallExecuted, CallSalt, Cancelled, MinDelayChange}; +use openzeppelin::governance::timelock::TimelockControllerComponent::{PROPOSER_ROLE, EXECUTOR_ROLE, CANCELLER_ROLE}; +use openzeppelin::governance::timelock::TimelockControllerComponent::{TimelockImpl, InternalImpl as TimelockInternalImpl}; +use openzeppelin::access::accesscontrol::AccessControlComponent::{AccessControlImpl, InternalImpl as AccessControlInternalImpl}; +use openzeppelin::tests::mocks::timelock_mocks::TimelockControllerMock; +use openzeppelin::token::erc721::interface::IERC721_RECEIVER_ID; +use openzeppelin::token::erc1155::interface::IERC1155_RECEIVER_ID; +use openzeppelin::tests::utils; +use openzeppelin::utils::serde::SerializedAppend; +use starknet::ContractAddress; +use openzeppelin::tests::utils::constants::{ADMIN, ZERO, NAME, SYMBOL, BASE_URI, RECIPIENT, TOKEN_ID}; +use starknet::contract_address_const; +use openzeppelin::tests::mocks::erc721_mocks::DualCaseERC721Mock; +use openzeppelin::token::erc721::interface::{IERC721DispatcherTrait, IERC721Dispatcher}; +use openzeppelin::governance::timelock::TimelockControllerComponent::Call; + +type ComponentState = TimelockControllerComponent::ComponentState; + +fn CONTRACT_STATE() -> TimelockControllerMock::ContractState { + TimelockControllerMock::contract_state_for_testing() +} + +fn COMPONENT_STATE() -> ComponentState { + TimelockControllerComponent::component_state_for_testing() +} + +const MIN_DELAY: u64 = 2000; + +fn get_proposers() -> (ContractAddress, ContractAddress, ContractAddress) { + let p1 = contract_address_const::<'PROPOSER_1'>(); + let p2 = contract_address_const::<'PROPOSER_2'>(); + let p3 = contract_address_const::<'PROPOSER_3'>(); + (p1, p2, p3) +} + +fn get_executors() -> (ContractAddress, ContractAddress, ContractAddress) { + let e1 = contract_address_const::<'EXECUTOR_1'>(); + let e2 = contract_address_const::<'EXECUTOR_2'>(); + let e3 = contract_address_const::<'EXECUTOR_3'>(); + (e1, e2, e3) +} + +fn setup() -> ComponentState { + let mut state = COMPONENT_STATE(); + let min_delay = MIN_DELAY; + + let (p1, p2, p3) = get_proposers(); + let mut proposers = array![p1, p2, p3].span(); + + let (e1, e2, e3) = get_executors(); + let mut executors = array![e1, e2, e3].span(); + + let admin = ADMIN(); + + state.initializer(min_delay, proposers, executors, admin); + // The initializer has emits 11 `RoleGranted` events: + // - Self administration + // - Optional admin + // - 3 proposers + // - 3 cancellers + // - 3 executors + utils::drop_events(ZERO(), 11); + + state +} + +fn deploy_timelock() -> ITimelockDispatcher { + let mut calldata = array![]; + + let (p1, p2, p3) = get_proposers(); + let mut proposers = array![p1, p2, p3].span(); + + let (e1, e2, e3) = get_executors(); + let mut executors = array![e1, e2, e3].span(); + + calldata.append_serde(MIN_DELAY); + calldata.append_serde(proposers); + calldata.append_serde(executors); + calldata.append_serde(ADMIN()); + + let address = utils::deploy(TimelockControllerMock::TEST_CLASS_HASH, calldata); + ITimelockDispatcher { contract_address: address } +} + +fn deploy_erc721(recipient: ContractAddress) -> IERC721Dispatcher { + let mut calldata = array![]; + + calldata.append_serde(NAME()); + calldata.append_serde(SYMBOL()); + calldata.append_serde(BASE_URI()); + calldata.append_serde(recipient); + calldata.append_serde(TOKEN_ID); + + let address = utils::deploy(DualCaseERC721Mock::TEST_CLASS_HASH, calldata); + IERC721Dispatcher { contract_address: address } +} + +// initializer + +#[test] +fn test_initializer_roles() { + let mut state = COMPONENT_STATE(); + let contract_state = CONTRACT_STATE(); + let min_delay = MIN_DELAY; + + let (p1, p2, p3) = get_proposers(); + let mut proposers = array![p1, p2, p3].span(); + + let (e1, e2, e3) = get_executors(); + let mut executors = array![e1, e2, e3].span(); + + let admin = ADMIN(); + + state.initializer(min_delay, proposers, executors, admin); + + // Check assigned roles + let mut index = 0; + loop { + if index == proposers.len() { + break; + } + + assert!(contract_state.has_role(PROPOSER_ROLE, *proposers.at(index))); + assert!(contract_state.has_role(CANCELLER_ROLE, *proposers.at(index))); + assert!(contract_state.has_role(EXECUTOR_ROLE, *executors.at(index))); + index += 1; + }; +} + +#[test] +fn test_initializer_supported_interfaces() { + let mut state = COMPONENT_STATE(); + let contract_state = CONTRACT_STATE(); + let min_delay = MIN_DELAY; + + let (p1, p2, p3) = get_proposers(); + let mut proposers = array![p1, p2, p3].span(); + + let (e1, e2, e3) = get_executors(); + let mut executors = array![e1, e2, e3].span(); + + let admin = ADMIN(); + + state.initializer(min_delay, proposers, executors, admin); + + // Check interface support + let supports_isrc5 = contract_state.src5.supports_interface(ISRC5_ID); + assert!(supports_isrc5); + + let supports_ierc1155_receiver = contract_state.src5.supports_interface(IERC1155_RECEIVER_ID); + assert!(supports_ierc1155_receiver); + + let supports_ierc721_receiver = contract_state.src5.supports_interface(IERC721_RECEIVER_ID); + assert!(supports_ierc721_receiver); +} + +#[test] +fn test_initializer_min_delay() { + let mut state = COMPONENT_STATE(); + let min_delay = MIN_DELAY; + + let (p1, p2, p3) = get_proposers(); + let mut proposers = array![p1, p2, p3].span(); + + let (e1, e2, e3) = get_executors(); + let mut executors = array![e1, e2, e3].span(); + + let admin = ADMIN(); + + state.initializer(min_delay, proposers, executors, admin); + + // Check minimum delay is set + let delay = state.get_min_delay(); + assert_eq!(delay, MIN_DELAY); + + // The initializer has emits 11 `RoleGranted` events prior to `MinDelayChange`: + // - Self administration + // - Optional admin + // - 3 proposers + // - 3 cancellers + // - 3 executors + utils::drop_events(ZERO(), 11); + assert_only_event_delay_change(ZERO(), 0, MIN_DELAY); +} + +// hash_operation + +#[test] +fn test_hash_operation() { + let mut timelock = deploy_timelock(); + let mut erc721 = deploy_erc721(timelock.contract_address); + let erc721_address = erc721.contract_address; + + // Call + let mut calldata = array![]; + calldata.append_serde(timelock.contract_address); + calldata.append_serde(RECIPIENT()); + calldata.append_serde(TOKEN_ID); + + let mut call = Call { to: erc721_address, selector: 'transfer_from', calldata: calldata.span() }; + let call_span = array![call].span(); + + let _hash = timelock.hash_operation(call_span, 0, 0); +} + +// +// Helpers +// + +fn assert_event_delay_change( + contract: ContractAddress, old_duration: u64, new_duration: u64 +) { + let event = utils::pop_log::(contract).unwrap(); + let expected = TimelockControllerComponent::Event::MinDelayChange(MinDelayChange { old_duration, new_duration }); + assert!(event == expected); +} + +fn assert_only_event_delay_change( + contract: ContractAddress, old_duration: u64, new_duration: u64 +) { + assert_event_delay_change(contract, old_duration, new_duration); + utils::drop_event(ZERO()); +} From 83525ba5eb0bad45f4b4a099922bb2efb250a604 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 26 May 2024 03:47:08 -0400 Subject: [PATCH 010/103] fix fmt --- .../timelock/timelock_controller.cairo | 4 +- src/tests/governance/test_timelock.cairo | 53 +++++++++++-------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 440ebfc84..deb9a59fc 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -23,11 +23,11 @@ mod TimelockControllerComponent { use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent; use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent::InternalImpl as ERC721InternalImpl; use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent; - use super::{CallPartialEq, HashCallImpl}; use poseidon::PoseidonTrait; use starknet::ContractAddress; use starknet::SyscallResultTrait; use starknet::account::Call; + use super::{CallPartialEq, HashCallImpl}; use zeroable::Zeroable; // Constants @@ -355,4 +355,4 @@ impl CallPartialEq of PartialEq { Serde::serialize(lhs, ref rhs_arr); !(lhs_arr == rhs_arr) } -} \ No newline at end of file +} diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 91fc4599c..5fd6aa7de 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -1,25 +1,36 @@ +use openzeppelin::access::accesscontrol::AccessControlComponent::{ + AccessControlImpl, InternalImpl as AccessControlInternalImpl +}; +use openzeppelin::governance::timelock::TimelockControllerComponent::Call; +use openzeppelin::governance::timelock::TimelockControllerComponent::{ + CallScheduled, CallExecuted, CallSalt, Cancelled, MinDelayChange +}; +use openzeppelin::governance::timelock::TimelockControllerComponent::{ + PROPOSER_ROLE, EXECUTOR_ROLE, CANCELLER_ROLE +}; +use openzeppelin::governance::timelock::TimelockControllerComponent::{ + TimelockImpl, InternalImpl as TimelockInternalImpl +}; +use openzeppelin::governance::timelock::TimelockControllerComponent; use openzeppelin::governance::timelock::interface::ITimelock; +use openzeppelin::governance::timelock::interface::{ITimelockDispatcher, ITimelockDispatcherTrait}; use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; -use openzeppelin::governance::timelock::interface::{ITimelockDispatcher, ITimelockDispatcherTrait}; -use openzeppelin::governance::timelock::TimelockControllerComponent; -use openzeppelin::governance::timelock::TimelockControllerComponent::{CallScheduled, CallExecuted, CallSalt, Cancelled, MinDelayChange}; -use openzeppelin::governance::timelock::TimelockControllerComponent::{PROPOSER_ROLE, EXECUTOR_ROLE, CANCELLER_ROLE}; -use openzeppelin::governance::timelock::TimelockControllerComponent::{TimelockImpl, InternalImpl as TimelockInternalImpl}; -use openzeppelin::access::accesscontrol::AccessControlComponent::{AccessControlImpl, InternalImpl as AccessControlInternalImpl}; +use openzeppelin::tests::mocks::erc721_mocks::DualCaseERC721Mock; use openzeppelin::tests::mocks::timelock_mocks::TimelockControllerMock; -use openzeppelin::token::erc721::interface::IERC721_RECEIVER_ID; -use openzeppelin::token::erc1155::interface::IERC1155_RECEIVER_ID; +use openzeppelin::tests::utils::constants::{ + ADMIN, ZERO, NAME, SYMBOL, BASE_URI, RECIPIENT, TOKEN_ID +}; use openzeppelin::tests::utils; +use openzeppelin::token::erc1155::interface::IERC1155_RECEIVER_ID; +use openzeppelin::token::erc721::interface::IERC721_RECEIVER_ID; +use openzeppelin::token::erc721::interface::{IERC721DispatcherTrait, IERC721Dispatcher}; use openzeppelin::utils::serde::SerializedAppend; use starknet::ContractAddress; -use openzeppelin::tests::utils::constants::{ADMIN, ZERO, NAME, SYMBOL, BASE_URI, RECIPIENT, TOKEN_ID}; use starknet::contract_address_const; -use openzeppelin::tests::mocks::erc721_mocks::DualCaseERC721Mock; -use openzeppelin::token::erc721::interface::{IERC721DispatcherTrait, IERC721Dispatcher}; -use openzeppelin::governance::timelock::TimelockControllerComponent::Call; -type ComponentState = TimelockControllerComponent::ComponentState; +type ComponentState = + TimelockControllerComponent::ComponentState; fn CONTRACT_STATE() -> TimelockControllerMock::ContractState { TimelockControllerMock::contract_state_for_testing() @@ -202,7 +213,9 @@ fn test_hash_operation() { calldata.append_serde(RECIPIENT()); calldata.append_serde(TOKEN_ID); - let mut call = Call { to: erc721_address, selector: 'transfer_from', calldata: calldata.span() }; + let mut call = Call { + to: erc721_address, selector: 'transfer_from', calldata: calldata.span() + }; let call_span = array![call].span(); let _hash = timelock.hash_operation(call_span, 0, 0); @@ -212,17 +225,15 @@ fn test_hash_operation() { // Helpers // -fn assert_event_delay_change( - contract: ContractAddress, old_duration: u64, new_duration: u64 -) { +fn assert_event_delay_change(contract: ContractAddress, old_duration: u64, new_duration: u64) { let event = utils::pop_log::(contract).unwrap(); - let expected = TimelockControllerComponent::Event::MinDelayChange(MinDelayChange { old_duration, new_duration }); + let expected = TimelockControllerComponent::Event::MinDelayChange( + MinDelayChange { old_duration, new_duration } + ); assert!(event == expected); } -fn assert_only_event_delay_change( - contract: ContractAddress, old_duration: u64, new_duration: u64 -) { +fn assert_only_event_delay_change(contract: ContractAddress, old_duration: u64, new_duration: u64) { assert_event_delay_change(contract, old_duration, new_duration); utils::drop_event(ZERO()); } From 587a29fb6e8193ad3a4df7a66d886d9ee20a27cd Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 27 May 2024 01:55:10 -0400 Subject: [PATCH 011/103] fix schedule assertion --- src/governance/timelock/timelock_controller.cairo | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index deb9a59fc..c085cbfad 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -264,7 +264,7 @@ mod TimelockControllerComponent { let mut proposer = proposers.at(i); access_component._grant_role(PROPOSER_ROLE, *proposer); access_component._grant_role(CANCELLER_ROLE, *proposer); - i = i + 1; + i += 1; }; // Register executors @@ -276,7 +276,7 @@ mod TimelockControllerComponent { let mut executor = executors.at(i); access_component._grant_role(EXECUTOR_ROLE, *executor); - i = i + 1; + i += 1; }; self.TimelockController_min_delay.write(min_delay); @@ -302,8 +302,8 @@ mod TimelockControllerComponent { } fn _schedule(ref self: ComponentState, id: felt252, delay: u64) { - assert(self.is_operation(id), Errors::UNEXPECTED_OPERATION_STATE); - assert(self.get_min_delay() < delay, Errors::INSUFFICIENT_DELAY); + assert(!self.is_operation(id), Errors::UNEXPECTED_OPERATION_STATE); + assert(self.get_min_delay() <= delay, Errors::INSUFFICIENT_DELAY); self.TimelockController_timestamps.write(id, starknet::get_block_timestamp() + delay); } From e50b1dbf5e7bddeb9dd5b9be016eeb906aae0a5c Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 27 May 2024 01:55:40 -0400 Subject: [PATCH 012/103] add schedule tests --- src/tests/governance/test_timelock.cairo | 244 +++++++++++++++++++++-- 1 file changed, 231 insertions(+), 13 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 5fd6aa7de..77226339d 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -19,7 +19,7 @@ use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; use openzeppelin::tests::mocks::erc721_mocks::DualCaseERC721Mock; use openzeppelin::tests::mocks::timelock_mocks::TimelockControllerMock; use openzeppelin::tests::utils::constants::{ - ADMIN, ZERO, NAME, SYMBOL, BASE_URI, RECIPIENT, TOKEN_ID + ADMIN, ZERO, NAME, SYMBOL, BASE_URI, RECIPIENT, SPENDER, OTHER, SALT, TOKEN_ID }; use openzeppelin::tests::utils; use openzeppelin::token::erc1155::interface::IERC1155_RECEIVER_ID; @@ -28,6 +28,9 @@ use openzeppelin::token::erc721::interface::{IERC721DispatcherTrait, IERC721Disp use openzeppelin::utils::serde::SerializedAppend; use starknet::ContractAddress; use starknet::contract_address_const; +use openzeppelin::governance::timelock::timelock_controller::{CallPartialEq, HashCallImpl}; +use hash::{HashStateTrait, HashStateExTrait}; +use poseidon::PoseidonTrait; type ComponentState = TimelockControllerComponent::ComponentState; @@ -40,7 +43,7 @@ fn COMPONENT_STATE() -> ComponentState { TimelockControllerComponent::component_state_for_testing() } -const MIN_DELAY: u64 = 2000; +const MIN_DELAY: u64 = 1000; fn get_proposers() -> (ContractAddress, ContractAddress, ContractAddress) { let p1 = contract_address_const::<'PROPOSER_1'>(); @@ -49,6 +52,11 @@ fn get_proposers() -> (ContractAddress, ContractAddress, ContractAddress) { (p1, p2, p3) } +fn get_proposer() -> ContractAddress { + let (p1, _, _) = get_proposers(); + p1 +} + fn get_executors() -> (ContractAddress, ContractAddress, ContractAddress) { let e1 = contract_address_const::<'EXECUTOR_1'>(); let e2 = contract_address_const::<'EXECUTOR_2'>(); @@ -56,6 +64,42 @@ fn get_executors() -> (ContractAddress, ContractAddress, ContractAddress) { (e1, e2, e3) } +fn get_executor() -> ContractAddress { + let (e1, _, _) = get_executors(); + e1 +} + +fn single_operation(erc721_addr: ContractAddress) -> Call { + // Call: approve + let mut calldata = array![]; + calldata.append_serde(SPENDER()); + calldata.append_serde(TOKEN_ID); + + Call { + to: erc721_addr, selector: 'approve', calldata: calldata.span() + } +} + +fn batched_operations(erc721_addr: ContractAddress, timelock_addr: ContractAddress) -> Span { + // Call 1: approve + let mut calldata1 = array![]; + calldata1.append_serde(SPENDER()); + calldata1.append_serde(TOKEN_ID); + + let call1 = Call{ to: erc721_addr, selector: 'approve', calldata: calldata1.span() }; + + // Call 2: transfer_from + let mut calldata2 = array![]; + calldata2.append_serde(timelock_addr); + calldata2.append_serde(RECIPIENT()); + calldata2.append_serde(TOKEN_ID); + let call2 = Call { + to: erc721_addr, selector: 'transfer_from', calldata: calldata2.span() + }; + + array![call1, call2].span() +} + fn setup() -> ComponentState { let mut state = COMPONENT_STATE(); let min_delay = MIN_DELAY; @@ -189,7 +233,7 @@ fn test_initializer_min_delay() { let delay = state.get_min_delay(); assert_eq!(delay, MIN_DELAY); - // The initializer has emits 11 `RoleGranted` events prior to `MinDelayChange`: + // The initializer emits 11 `RoleGranted` events prior to `MinDelayChange`: // - Self administration // - Optional admin // - 3 proposers @@ -199,6 +243,114 @@ fn test_initializer_min_delay() { assert_only_event_delay_change(ZERO(), 0, MIN_DELAY); } +// schedule + +#[test] +fn test_schedule_from_proposer() { + let mut timelock = deploy_timelock(); + let mut erc721 = deploy_erc721(timelock.contract_address); + utils::drop_events(timelock.contract_address, 12); + + let (proposer, _, _) = get_proposers(); + starknet::testing::set_contract_address(proposer); + + let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); + let predecessor = 0; + let salt = SALT; + let delay = MIN_DELAY; + + timelock.schedule(batched_operations, predecessor, salt, delay); + assert_event_schedule(timelock.contract_address, batched_operations, predecessor, delay); + + // Check timestamp + let hash_id = timelock.hash_operation(batched_operations, predecessor, salt); + let operation_ts = timelock.get_timestamp(hash_id); + let expected_ts = starknet::get_block_timestamp() + delay; + assert_eq!(operation_ts, expected_ts); + + assert_only_event_call_salt(timelock.contract_address, hash_id, salt); +} + +#[test] +#[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] +fn test_schedule_overwrite() { + let mut timelock = deploy_timelock(); + let mut erc721 = deploy_erc721(timelock.contract_address); + utils::drop_events(timelock.contract_address, 12); + + let (proposer, _, _) = get_proposers(); + starknet::testing::set_contract_address(proposer); + + let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); + let predecessor = 0; + let salt = SALT; + let delay = MIN_DELAY; + + timelock.schedule(batched_operations, predecessor, salt, delay); + timelock.schedule(batched_operations, predecessor, salt, delay); +} + +#[test] +#[should_panic(expected: ('Caller is missing role', 'ENTRYPOINT_FAILED'))] +fn test_schedule_unauthorized() { + let mut timelock = deploy_timelock(); + let mut erc721 = deploy_erc721(timelock.contract_address); + utils::drop_events(timelock.contract_address, 12); + + starknet::testing::set_contract_address(OTHER()); + let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); + let predecessor = 0; + let salt = SALT; + let delay = MIN_DELAY; + + timelock.schedule(batched_operations, predecessor, salt, delay); +} + +#[test] +#[should_panic(expected: ('Timelock: insufficient delay', 'ENTRYPOINT_FAILED'))] +fn test_schedule_bad_min_delay() { + let mut timelock = deploy_timelock(); + let mut erc721 = deploy_erc721(timelock.contract_address); + utils::drop_events(timelock.contract_address, 12); + + let (proposer, _, _) = get_proposers(); + starknet::testing::set_contract_address(proposer); + + let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); + let predecessor = 0; + let salt = SALT; + let bad_delay = MIN_DELAY - 1; + + timelock.schedule(batched_operations, predecessor, salt, bad_delay); +} + +#[test] +fn test_schedule_with_salt_zero() { + let mut timelock = deploy_timelock(); + let mut erc721 = deploy_erc721(timelock.contract_address); + utils::drop_events(timelock.contract_address, 12); + + let proposer = get_proposer(); + starknet::testing::set_contract_address(proposer); + + // Schedule + let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); + let predecessor = 0; + let salt = 0; + let delay = MIN_DELAY; + + timelock.schedule(batched_operations, predecessor, salt, delay); + assert_only_event_schedule(timelock.contract_address, batched_operations, predecessor, delay); + + // Check timestamp + let hash_id = timelock.hash_operation(batched_operations, predecessor, salt); + let operation_ts = timelock.get_timestamp(hash_id); + let expected_ts = starknet::get_block_timestamp() + delay; + assert_eq!(operation_ts, expected_ts) +} + +// execute + // hash_operation #[test] @@ -207,18 +359,52 @@ fn test_hash_operation() { let mut erc721 = deploy_erc721(timelock.contract_address); let erc721_address = erc721.contract_address; - // Call - let mut calldata = array![]; - calldata.append_serde(timelock.contract_address); - calldata.append_serde(RECIPIENT()); - calldata.append_serde(TOKEN_ID); + // Call 1 + let mut calldata1 = array![]; + calldata1.append_serde(SPENDER()); + calldata1.append_serde(TOKEN_ID); - let mut call = Call { - to: erc721_address, selector: 'transfer_from', calldata: calldata.span() + let mut call1 = Call { + to: erc721_address, selector: 'approve', calldata: calldata1.span() }; - let call_span = array![call].span(); - let _hash = timelock.hash_operation(call_span, 0, 0); + // Call 2 + let mut calldata2 = array![]; + calldata2.append_serde(timelock.contract_address); + calldata2.append_serde(RECIPIENT()); + calldata2.append_serde(TOKEN_ID); + let mut call2 = Call { + to: erc721_address, selector: 'transfer_from', calldata: calldata2.span() + }; + + // Hash operation + let predecessor = 123; + let salt = SALT; + let call_span = array![call1, call2].span(); + let hashed_operation = timelock.hash_operation(call_span, predecessor, salt); + + // Manually set hash elements + let mut expected_hash = PoseidonTrait::new() + .update_with(14) // total elements of Call span + .update_with(2) // total number of Calls + .update_with(erc721_address) // call1::to + .update_with('approve') // call1::selector + .update_with(3) // call1::calldata.len + .update_with(SPENDER()) // call1::calldata::to + .update_with(TOKEN_ID.low) // call1::calldata::token_id.low + .update_with(TOKEN_ID.high) // call1::calldata::token_id.high + .update_with(erc721_address) // call2::to + .update_with('transfer_from') // call2::selector + .update_with(4) // call2::calldata.len + .update_with(timelock.contract_address) // call2::calldata::from + .update_with(RECIPIENT()) // call2::calldata::to + .update_with(TOKEN_ID.low) // call2::calldata::token_id.low + .update_with(TOKEN_ID.high) // call2::calldata::token_id.high + .update_with(predecessor) // predecessor + .update_with(salt) // salt + .finalize(); + + assert_eq!(hashed_operation, expected_hash); } // @@ -235,5 +421,37 @@ fn assert_event_delay_change(contract: ContractAddress, old_duration: u64, new_d fn assert_only_event_delay_change(contract: ContractAddress, old_duration: u64, new_duration: u64) { assert_event_delay_change(contract, old_duration, new_duration); - utils::drop_event(ZERO()); + utils::drop_event(contract); +} + +fn assert_event_schedule(contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64) { + let event = utils::pop_log::(contract).unwrap(); + let expected = TimelockControllerComponent::Event::CallScheduled( + CallScheduled { calls, predecessor, delay } + ); + assert!(event == expected); + + // Check indexed keys + let mut indexed_keys = array![]; + indexed_keys.append_serde(selector!("CallScheduled")); + indexed_keys.append_serde(calls); + utils::assert_indexed_keys(event, indexed_keys.span()); +} + +fn assert_only_event_schedule(contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64) { + assert_event_schedule(contract, calls, predecessor, delay); + utils::drop_event(contract); +} + +fn assert_event_call_salt(contract: ContractAddress, id: felt252, salt: felt252) { + let event = utils::pop_log::(contract).unwrap(); + let expected = TimelockControllerComponent::Event::CallSalt( + CallSalt { id, salt } + ); + assert!(event == expected); +} + +fn assert_only_event_call_salt(contract: ContractAddress, id: felt252, salt: felt252) { + assert_event_call_salt(contract, id, salt); + utils::drop_event(contract); } From 4b3a44cc1922385c05e7ecba9ae90e2bd1093412 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 27 May 2024 01:56:55 -0400 Subject: [PATCH 013/103] fix fmt --- src/tests/governance/test_timelock.cairo | 68 +++++++++++------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 77226339d..664e39fe9 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -1,3 +1,4 @@ +use hash::{HashStateTrait, HashStateExTrait}; use openzeppelin::access::accesscontrol::AccessControlComponent::{ AccessControlImpl, InternalImpl as AccessControlInternalImpl }; @@ -14,6 +15,7 @@ use openzeppelin::governance::timelock::TimelockControllerComponent::{ use openzeppelin::governance::timelock::TimelockControllerComponent; use openzeppelin::governance::timelock::interface::ITimelock; use openzeppelin::governance::timelock::interface::{ITimelockDispatcher, ITimelockDispatcherTrait}; +use openzeppelin::governance::timelock::timelock_controller::{CallPartialEq, HashCallImpl}; use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; use openzeppelin::tests::mocks::erc721_mocks::DualCaseERC721Mock; @@ -26,11 +28,9 @@ use openzeppelin::token::erc1155::interface::IERC1155_RECEIVER_ID; use openzeppelin::token::erc721::interface::IERC721_RECEIVER_ID; use openzeppelin::token::erc721::interface::{IERC721DispatcherTrait, IERC721Dispatcher}; use openzeppelin::utils::serde::SerializedAppend; +use poseidon::PoseidonTrait; use starknet::ContractAddress; use starknet::contract_address_const; -use openzeppelin::governance::timelock::timelock_controller::{CallPartialEq, HashCallImpl}; -use hash::{HashStateTrait, HashStateExTrait}; -use poseidon::PoseidonTrait; type ComponentState = TimelockControllerComponent::ComponentState; @@ -75,9 +75,7 @@ fn single_operation(erc721_addr: ContractAddress) -> Call { calldata.append_serde(SPENDER()); calldata.append_serde(TOKEN_ID); - Call { - to: erc721_addr, selector: 'approve', calldata: calldata.span() - } + Call { to: erc721_addr, selector: 'approve', calldata: calldata.span() } } fn batched_operations(erc721_addr: ContractAddress, timelock_addr: ContractAddress) -> Span { @@ -86,16 +84,14 @@ fn batched_operations(erc721_addr: ContractAddress, timelock_addr: ContractAddre calldata1.append_serde(SPENDER()); calldata1.append_serde(TOKEN_ID); - let call1 = Call{ to: erc721_addr, selector: 'approve', calldata: calldata1.span() }; + let call1 = Call { to: erc721_addr, selector: 'approve', calldata: calldata1.span() }; // Call 2: transfer_from let mut calldata2 = array![]; calldata2.append_serde(timelock_addr); calldata2.append_serde(RECIPIENT()); calldata2.append_serde(TOKEN_ID); - let call2 = Call { - to: erc721_addr, selector: 'transfer_from', calldata: calldata2.span() - }; + let call2 = Call { to: erc721_addr, selector: 'transfer_from', calldata: calldata2.span() }; array![call1, call2].span() } @@ -364,9 +360,7 @@ fn test_hash_operation() { calldata1.append_serde(SPENDER()); calldata1.append_serde(TOKEN_ID); - let mut call1 = Call { - to: erc721_address, selector: 'approve', calldata: calldata1.span() - }; + let mut call1 = Call { to: erc721_address, selector: 'approve', calldata: calldata1.span() }; // Call 2 let mut calldata2 = array![]; @@ -385,24 +379,24 @@ fn test_hash_operation() { // Manually set hash elements let mut expected_hash = PoseidonTrait::new() - .update_with(14) // total elements of Call span - .update_with(2) // total number of Calls - .update_with(erc721_address) // call1::to - .update_with('approve') // call1::selector - .update_with(3) // call1::calldata.len - .update_with(SPENDER()) // call1::calldata::to - .update_with(TOKEN_ID.low) // call1::calldata::token_id.low - .update_with(TOKEN_ID.high) // call1::calldata::token_id.high - .update_with(erc721_address) // call2::to - .update_with('transfer_from') // call2::selector - .update_with(4) // call2::calldata.len - .update_with(timelock.contract_address) // call2::calldata::from - .update_with(RECIPIENT()) // call2::calldata::to - .update_with(TOKEN_ID.low) // call2::calldata::token_id.low - .update_with(TOKEN_ID.high) // call2::calldata::token_id.high - .update_with(predecessor) // predecessor - .update_with(salt) // salt - .finalize(); + .update_with(14) // total elements of Call span + .update_with(2) // total number of Calls + .update_with(erc721_address) // call1::to + .update_with('approve') // call1::selector + .update_with(3) // call1::calldata.len + .update_with(SPENDER()) // call1::calldata::to + .update_with(TOKEN_ID.low) // call1::calldata::token_id.low + .update_with(TOKEN_ID.high) // call1::calldata::token_id.high + .update_with(erc721_address) // call2::to + .update_with('transfer_from') // call2::selector + .update_with(4) // call2::calldata.len + .update_with(timelock.contract_address) // call2::calldata::from + .update_with(RECIPIENT()) // call2::calldata::to + .update_with(TOKEN_ID.low) // call2::calldata::token_id.low + .update_with(TOKEN_ID.high) // call2::calldata::token_id.high + .update_with(predecessor) // predecessor + .update_with(salt) // salt + .finalize(); assert_eq!(hashed_operation, expected_hash); } @@ -424,7 +418,9 @@ fn assert_only_event_delay_change(contract: ContractAddress, old_duration: u64, utils::drop_event(contract); } -fn assert_event_schedule(contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64) { +fn assert_event_schedule( + contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64 +) { let event = utils::pop_log::(contract).unwrap(); let expected = TimelockControllerComponent::Event::CallScheduled( CallScheduled { calls, predecessor, delay } @@ -438,16 +434,16 @@ fn assert_event_schedule(contract: ContractAddress, calls: Span, predecess utils::assert_indexed_keys(event, indexed_keys.span()); } -fn assert_only_event_schedule(contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64) { +fn assert_only_event_schedule( + contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64 +) { assert_event_schedule(contract, calls, predecessor, delay); utils::drop_event(contract); } fn assert_event_call_salt(contract: ContractAddress, id: felt252, salt: felt252) { let event = utils::pop_log::(contract).unwrap(); - let expected = TimelockControllerComponent::Event::CallSalt( - CallSalt { id, salt } - ); + let expected = TimelockControllerComponent::Event::CallSalt(CallSalt { id, salt }); assert!(event == expected); } From 169397cad4103566852eb711c378db60bddd3b5f Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 27 May 2024 01:58:02 -0400 Subject: [PATCH 014/103] remove unused import --- src/tests/governance/test_timelock.cairo | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 664e39fe9..471bd2d43 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -13,7 +13,6 @@ use openzeppelin::governance::timelock::TimelockControllerComponent::{ TimelockImpl, InternalImpl as TimelockInternalImpl }; use openzeppelin::governance::timelock::TimelockControllerComponent; -use openzeppelin::governance::timelock::interface::ITimelock; use openzeppelin::governance::timelock::interface::{ITimelockDispatcher, ITimelockDispatcherTrait}; use openzeppelin::governance::timelock::timelock_controller::{CallPartialEq, HashCallImpl}; use openzeppelin::introspection::interface::ISRC5_ID; From 07bb530e8d26ad973b06bb9eabe153529198ab33 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 28 May 2024 03:16:19 -0400 Subject: [PATCH 015/103] fix errs, _before_call --- src/governance/timelock/timelock_controller.cairo | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index c085cbfad..ade57357c 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -93,11 +93,10 @@ mod TimelockControllerComponent { } mod Errors { - const INVALID_CLASS: felt252 = 'Class hash cannot be zero'; const INVALID_OPERATION_LEN: felt252 = 'Timelock: invalid operation len'; const INSUFFICIENT_DELAY: felt252 = 'Timelock: insufficient delay'; const UNEXPECTED_OPERATION_STATE: felt252 = 'Timelock: unexpected op state'; - const UNEXECUTED_PREDECESSOR: felt252 = 'Timelock: unexecuted predessor'; + const UNEXECUTED_PREDECESSOR: felt252 = 'Timelock: awaiting predecessor'; const UNAUTHORIZED_CALLER: felt252 = 'Timelock: unauthorized caller'; } @@ -290,10 +289,7 @@ mod TimelockControllerComponent { fn before_call(self: @ComponentState, id: felt252, predecessor: felt252) { assert(self.is_operation_ready(id), Errors::UNEXPECTED_OPERATION_STATE); - assert( - predecessor != 0 && !self.is_operation_done(predecessor), - Errors::UNEXECUTED_PREDECESSOR - ); + assert(predecessor == 0 || !self.is_operation_done(predecessor), Errors::UNEXECUTED_PREDECESSOR); } fn after_call(ref self: ComponentState, id: felt252) { @@ -320,6 +316,8 @@ mod TimelockControllerComponent { calldata: *calls.at(index).calldata }; execute_single_call(call); + + index += 1; } } } From 65d2be67bfb2f0ac06c8b861710a66c0915a199c Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 28 May 2024 03:16:47 -0400 Subject: [PATCH 016/103] start execute tests --- src/tests/governance/test_timelock.cairo | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 471bd2d43..e92240ecc 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -346,6 +346,24 @@ fn test_schedule_with_salt_zero() { // execute +#[test] +#[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] +fn test_execute_when_not_scheduled() { + let mut timelock = deploy_timelock(); + let mut erc721 = deploy_erc721(timelock.contract_address); + utils::drop_events(timelock.contract_address, 12); + + let executor = get_executor(); + starknet::testing::set_contract_address(executor); + + let predecessor = 0; + let salt = SALT; + let call = single_operation(erc721.contract_address); + let call_arr = array![call]; + + timelock.execute(call_arr.span(), predecessor, salt); +} + // hash_operation #[test] From 6f3a47f1611d0afa4dbeee5ab5994bff447a4198 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 28 May 2024 03:17:03 -0400 Subject: [PATCH 017/103] fix fmt --- src/governance/timelock/timelock_controller.cairo | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index ade57357c..315519f61 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -289,7 +289,10 @@ mod TimelockControllerComponent { fn before_call(self: @ComponentState, id: felt252, predecessor: felt252) { assert(self.is_operation_ready(id), Errors::UNEXPECTED_OPERATION_STATE); - assert(predecessor == 0 || !self.is_operation_done(predecessor), Errors::UNEXECUTED_PREDECESSOR); + assert( + predecessor == 0 || !self.is_operation_done(predecessor), + Errors::UNEXECUTED_PREDECESSOR + ); } fn after_call(ref self: ComponentState, id: felt252) { From 0e3b35bfb5a8c56915595586946fc1b5e9ec4a70 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 28 May 2024 13:06:31 -0400 Subject: [PATCH 018/103] fix after_call --- src/governance/timelock/timelock_controller.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 315519f61..435cc106a 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -296,7 +296,7 @@ mod TimelockControllerComponent { } fn after_call(ref self: ComponentState, id: felt252) { - assert(!self.is_operation_ready(id), Errors::UNEXPECTED_OPERATION_STATE); + assert(self.is_operation_ready(id), Errors::UNEXPECTED_OPERATION_STATE); self.TimelockController_timestamps.write(id, DONE_TIMESTAMP); } From 2c1186e14689b5be66322d5a6a96cb4c9a209a01 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 29 May 2024 20:33:00 -0400 Subject: [PATCH 019/103] add execute tests --- src/tests/governance/test_timelock.cairo | 401 +++++++++++++++-------- 1 file changed, 258 insertions(+), 143 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index e92240ecc..282eb406a 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -2,23 +2,30 @@ use hash::{HashStateTrait, HashStateExTrait}; use openzeppelin::access::accesscontrol::AccessControlComponent::{ AccessControlImpl, InternalImpl as AccessControlInternalImpl }; +use openzeppelin::access::accesscontrol::accesscontrol::AccessControlComponent::InternalTrait; +use openzeppelin::access::accesscontrol::interface::IAccessControl; use openzeppelin::governance::timelock::TimelockControllerComponent::Call; use openzeppelin::governance::timelock::TimelockControllerComponent::{ CallScheduled, CallExecuted, CallSalt, Cancelled, MinDelayChange }; use openzeppelin::governance::timelock::TimelockControllerComponent::{ - PROPOSER_ROLE, EXECUTOR_ROLE, CANCELLER_ROLE + PROPOSER_ROLE, EXECUTOR_ROLE, CANCELLER_ROLE, DEFAULT_ADMIN_ROLE }; use openzeppelin::governance::timelock::TimelockControllerComponent::{ TimelockImpl, InternalImpl as TimelockInternalImpl }; use openzeppelin::governance::timelock::TimelockControllerComponent; -use openzeppelin::governance::timelock::interface::{ITimelockDispatcher, ITimelockDispatcherTrait}; +use openzeppelin::governance::timelock::interface::{ + ITimelockABIDispatcher, ITimelockABIDispatcherTrait +}; use openzeppelin::governance::timelock::timelock_controller::{CallPartialEq, HashCallImpl}; use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; use openzeppelin::tests::mocks::erc721_mocks::DualCaseERC721Mock; -use openzeppelin::tests::mocks::timelock_mocks::TimelockControllerMock; +use openzeppelin::tests::mocks::timelock_mocks::{ + ITimelockAttackerDispatcher, ITimelockAttackerDispatcherTrait +}; +use openzeppelin::tests::mocks::timelock_mocks::{TimelockControllerMock, TimelockAttackerMock}; use openzeppelin::tests::utils::constants::{ ADMIN, ZERO, NAME, SYMBOL, BASE_URI, RECIPIENT, SPENDER, OTHER, SALT, TOKEN_ID }; @@ -26,10 +33,12 @@ use openzeppelin::tests::utils; use openzeppelin::token::erc1155::interface::IERC1155_RECEIVER_ID; use openzeppelin::token::erc721::interface::IERC721_RECEIVER_ID; use openzeppelin::token::erc721::interface::{IERC721DispatcherTrait, IERC721Dispatcher}; +use openzeppelin::utils::selectors; use openzeppelin::utils::serde::SerializedAppend; use poseidon::PoseidonTrait; use starknet::ContractAddress; use starknet::contract_address_const; +use starknet::testing; type ComponentState = TimelockControllerComponent::ComponentState; @@ -43,6 +52,15 @@ fn COMPONENT_STATE() -> ComponentState { } const MIN_DELAY: u64 = 1000; +const NO_PREDECESSOR: felt252 = 0; + +fn PROPOSER() -> ContractAddress { + contract_address_const::<'PROPOSER'>() +} + +fn EXECUTOR() -> ContractAddress { + contract_address_const::<'EXECUTOR'>() +} fn get_proposers() -> (ContractAddress, ContractAddress, ContractAddress) { let p1 = contract_address_const::<'PROPOSER_1'>(); @@ -51,11 +69,6 @@ fn get_proposers() -> (ContractAddress, ContractAddress, ContractAddress) { (p1, p2, p3) } -fn get_proposer() -> ContractAddress { - let (p1, _, _) = get_proposers(); - p1 -} - fn get_executors() -> (ContractAddress, ContractAddress, ContractAddress) { let e1 = contract_address_const::<'EXECUTOR_1'>(); let e2 = contract_address_const::<'EXECUTOR_2'>(); @@ -63,18 +76,13 @@ fn get_executors() -> (ContractAddress, ContractAddress, ContractAddress) { (e1, e2, e3) } -fn get_executor() -> ContractAddress { - let (e1, _, _) = get_executors(); - e1 -} - fn single_operation(erc721_addr: ContractAddress) -> Call { // Call: approve let mut calldata = array![]; calldata.append_serde(SPENDER()); calldata.append_serde(TOKEN_ID); - Call { to: erc721_addr, selector: 'approve', calldata: calldata.span() } + Call { to: erc721_addr, selector: selectors::approve, calldata: calldata.span() } } fn batched_operations(erc721_addr: ContractAddress, timelock_addr: ContractAddress) -> Span { @@ -83,58 +91,46 @@ fn batched_operations(erc721_addr: ContractAddress, timelock_addr: ContractAddre calldata1.append_serde(SPENDER()); calldata1.append_serde(TOKEN_ID); - let call1 = Call { to: erc721_addr, selector: 'approve', calldata: calldata1.span() }; + let call1 = Call { to: erc721_addr, selector: selectors::approve, calldata: calldata1.span() }; // Call 2: transfer_from let mut calldata2 = array![]; calldata2.append_serde(timelock_addr); calldata2.append_serde(RECIPIENT()); calldata2.append_serde(TOKEN_ID); - let call2 = Call { to: erc721_addr, selector: 'transfer_from', calldata: calldata2.span() }; + let call2 = Call { + to: erc721_addr, selector: selectors::transfer_from, calldata: calldata2.span() + }; array![call1, call2].span() } -fn setup() -> ComponentState { - let mut state = COMPONENT_STATE(); - let min_delay = MIN_DELAY; - - let (p1, p2, p3) = get_proposers(); - let mut proposers = array![p1, p2, p3].span(); - - let (e1, e2, e3) = get_executors(); - let mut executors = array![e1, e2, e3].span(); +fn setup_dispatchers() -> (ITimelockABIDispatcher, IERC721Dispatcher) { + let timelock = deploy_timelock(); + let token_recipient = timelock.contract_address; + let erc721 = deploy_erc721(token_recipient); - let admin = ADMIN(); - - state.initializer(min_delay, proposers, executors, admin); - // The initializer has emits 11 `RoleGranted` events: - // - Self administration - // - Optional admin - // - 3 proposers - // - 3 cancellers - // - 3 executors - utils::drop_events(ZERO(), 11); - - state + (timelock, erc721) } -fn deploy_timelock() -> ITimelockDispatcher { +fn deploy_timelock() -> ITimelockABIDispatcher { let mut calldata = array![]; - let (p1, p2, p3) = get_proposers(); - let mut proposers = array![p1, p2, p3].span(); - - let (e1, e2, e3) = get_executors(); - let mut executors = array![e1, e2, e3].span(); + let proposers = array![PROPOSER()].span(); + let executors = array![EXECUTOR()].span(); + let admin = ADMIN(); calldata.append_serde(MIN_DELAY); calldata.append_serde(proposers); calldata.append_serde(executors); - calldata.append_serde(ADMIN()); + calldata.append_serde(admin); let address = utils::deploy(TimelockControllerMock::TEST_CLASS_HASH, calldata); - ITimelockDispatcher { contract_address: address } + // Events dropped: + // - 5 RoleGranted: self, proposer, canceller, executor, admin + // - MinDelayChange + utils::drop_events(address, 6); + ITimelockABIDispatcher { contract_address: address } } fn deploy_erc721(recipient: ContractAddress) -> IERC721Dispatcher { @@ -147,13 +143,37 @@ fn deploy_erc721(recipient: ContractAddress) -> IERC721Dispatcher { calldata.append_serde(TOKEN_ID); let address = utils::deploy(DualCaseERC721Mock::TEST_CLASS_HASH, calldata); + // Event dropped: + // - Transfer + utils::drop_event(address); IERC721Dispatcher { contract_address: address } } +fn deploy_attacker() -> ITimelockAttackerDispatcher { + let mut calldata = array![]; + + let address = utils::deploy(TimelockAttackerMock::TEST_CLASS_HASH, calldata); + ITimelockAttackerDispatcher { contract_address: address } +} + // initializer #[test] -fn test_initializer_roles() { +fn test_initializer_single_role_and_no_admin() { + let mut state = COMPONENT_STATE(); + let contract_state = CONTRACT_STATE(); + let min_delay = MIN_DELAY; + + let proposers = array![PROPOSER()].span(); + let executors = array![EXECUTOR()].span(); + let admin_zero = ZERO(); + + state.initializer(min_delay, proposers, executors, admin_zero); + assert!(contract_state.has_role(DEFAULT_ADMIN_ROLE, admin_zero)); +} + +#[test] +fn test_initializer_multiple_roles_and_admin() { let mut state = COMPONENT_STATE(); let contract_state = CONTRACT_STATE(); let min_delay = MIN_DELAY; @@ -169,6 +189,8 @@ fn test_initializer_roles() { state.initializer(min_delay, proposers, executors, admin); // Check assigned roles + assert!(contract_state.has_role(DEFAULT_ADMIN_ROLE, admin)); + let mut index = 0; loop { if index == proposers.len() { @@ -188,12 +210,8 @@ fn test_initializer_supported_interfaces() { let contract_state = CONTRACT_STATE(); let min_delay = MIN_DELAY; - let (p1, p2, p3) = get_proposers(); - let mut proposers = array![p1, p2, p3].span(); - - let (e1, e2, e3) = get_executors(); - let mut executors = array![e1, e2, e3].span(); - + let proposers = array![PROPOSER()].span(); + let executors = array![EXECUTOR()].span(); let admin = ADMIN(); state.initializer(min_delay, proposers, executors, admin); @@ -214,133 +232,99 @@ fn test_initializer_min_delay() { let mut state = COMPONENT_STATE(); let min_delay = MIN_DELAY; - let (p1, p2, p3) = get_proposers(); - let mut proposers = array![p1, p2, p3].span(); - - let (e1, e2, e3) = get_executors(); - let mut executors = array![e1, e2, e3].span(); - - let admin = ADMIN(); + let proposers = array![PROPOSER()].span(); + let executors = array![EXECUTOR()].span(); + let admin_zero = ZERO(); - state.initializer(min_delay, proposers, executors, admin); + state.initializer(min_delay, proposers, executors, admin_zero); // Check minimum delay is set let delay = state.get_min_delay(); assert_eq!(delay, MIN_DELAY); - // The initializer emits 11 `RoleGranted` events prior to `MinDelayChange`: + // The initializer emits 4 `RoleGranted` events prior to `MinDelayChange`: // - Self administration - // - Optional admin - // - 3 proposers - // - 3 cancellers - // - 3 executors - utils::drop_events(ZERO(), 11); + // - 1 proposers + // - 1 cancellers + // - 1 executors + utils::drop_events(ZERO(), 4); assert_only_event_delay_change(ZERO(), 0, MIN_DELAY); } // schedule #[test] -fn test_schedule_from_proposer() { - let mut timelock = deploy_timelock(); - let mut erc721 = deploy_erc721(timelock.contract_address); - utils::drop_events(timelock.contract_address, 12); - - let (proposer, _, _) = get_proposers(); - starknet::testing::set_contract_address(proposer); - +fn test_schedule_from_proposer_with_salt() { + let (mut timelock, mut erc721) = setup_dispatchers(); let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); - let predecessor = 0; - let salt = SALT; - let delay = MIN_DELAY; - timelock.schedule(batched_operations, predecessor, salt, delay); - assert_event_schedule(timelock.contract_address, batched_operations, predecessor, delay); + testing::set_contract_address(PROPOSER()); + + timelock.schedule(batched_operations, NO_PREDECESSOR, SALT, MIN_DELAY); + assert_event_schedule(timelock.contract_address, batched_operations, NO_PREDECESSOR, MIN_DELAY); // Check timestamp - let hash_id = timelock.hash_operation(batched_operations, predecessor, salt); + let hash_id = timelock.hash_operation(batched_operations, NO_PREDECESSOR, SALT); let operation_ts = timelock.get_timestamp(hash_id); - let expected_ts = starknet::get_block_timestamp() + delay; + let expected_ts = starknet::get_block_timestamp() + MIN_DELAY; assert_eq!(operation_ts, expected_ts); - assert_only_event_call_salt(timelock.contract_address, hash_id, salt); + assert_only_event_call_salt(timelock.contract_address, hash_id, SALT); } #[test] #[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] fn test_schedule_overwrite() { - let mut timelock = deploy_timelock(); - let mut erc721 = deploy_erc721(timelock.contract_address); - utils::drop_events(timelock.contract_address, 12); - - let (proposer, _, _) = get_proposers(); - starknet::testing::set_contract_address(proposer); + let (mut timelock, mut erc721) = setup_dispatchers(); let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); - let predecessor = 0; - let salt = SALT; - let delay = MIN_DELAY; - timelock.schedule(batched_operations, predecessor, salt, delay); - timelock.schedule(batched_operations, predecessor, salt, delay); + testing::set_contract_address(PROPOSER()); + timelock.schedule(batched_operations, NO_PREDECESSOR, SALT, MIN_DELAY); + timelock.schedule(batched_operations, NO_PREDECESSOR, SALT, MIN_DELAY); } #[test] #[should_panic(expected: ('Caller is missing role', 'ENTRYPOINT_FAILED'))] fn test_schedule_unauthorized() { - let mut timelock = deploy_timelock(); - let mut erc721 = deploy_erc721(timelock.contract_address); - utils::drop_events(timelock.contract_address, 12); + let (mut timelock, mut erc721) = setup_dispatchers(); - starknet::testing::set_contract_address(OTHER()); let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); - let predecessor = 0; - let salt = SALT; - let delay = MIN_DELAY; - timelock.schedule(batched_operations, predecessor, salt, delay); + testing::set_contract_address(OTHER()); + timelock.schedule(batched_operations, NO_PREDECESSOR, SALT, MIN_DELAY); } #[test] #[should_panic(expected: ('Timelock: insufficient delay', 'ENTRYPOINT_FAILED'))] fn test_schedule_bad_min_delay() { - let mut timelock = deploy_timelock(); - let mut erc721 = deploy_erc721(timelock.contract_address); - utils::drop_events(timelock.contract_address, 12); + let (mut timelock, mut erc721) = setup_dispatchers(); - let (proposer, _, _) = get_proposers(); - starknet::testing::set_contract_address(proposer); - - let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); - let predecessor = 0; - let salt = SALT; let bad_delay = MIN_DELAY - 1; + let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); - timelock.schedule(batched_operations, predecessor, salt, bad_delay); + testing::set_contract_address(PROPOSER()); + timelock.schedule(batched_operations, NO_PREDECESSOR, SALT, bad_delay); } #[test] fn test_schedule_with_salt_zero() { - let mut timelock = deploy_timelock(); - let mut erc721 = deploy_erc721(timelock.contract_address); - utils::drop_events(timelock.contract_address, 12); + let (mut timelock, mut erc721) = setup_dispatchers(); - let proposer = get_proposer(); - starknet::testing::set_contract_address(proposer); - - // Schedule + let zero_salt = 0; let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); - let predecessor = 0; - let salt = 0; - let delay = MIN_DELAY; + let hash_id = timelock.hash_operation(batched_operations, NO_PREDECESSOR, zero_salt); - timelock.schedule(batched_operations, predecessor, salt, delay); - assert_only_event_schedule(timelock.contract_address, batched_operations, predecessor, delay); + // Schedule + testing::set_contract_address(PROPOSER()); + timelock.schedule(batched_operations, NO_PREDECESSOR, zero_salt, MIN_DELAY); + assert_only_event_schedule( + timelock.contract_address, batched_operations, NO_PREDECESSOR, MIN_DELAY + ); // Check timestamp - let hash_id = timelock.hash_operation(batched_operations, predecessor, salt); let operation_ts = timelock.get_timestamp(hash_id); - let expected_ts = starknet::get_block_timestamp() + delay; + let expected_ts = starknet::get_block_timestamp() + MIN_DELAY; assert_eq!(operation_ts, expected_ts) } @@ -349,35 +333,148 @@ fn test_schedule_with_salt_zero() { #[test] #[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] fn test_execute_when_not_scheduled() { - let mut timelock = deploy_timelock(); - let mut erc721 = deploy_erc721(timelock.contract_address); - utils::drop_events(timelock.contract_address, 12); - - let executor = get_executor(); - starknet::testing::set_contract_address(executor); + let (mut timelock, mut erc721) = setup_dispatchers(); - let predecessor = 0; let salt = SALT; let call = single_operation(erc721.contract_address); let call_arr = array![call]; - timelock.execute(call_arr.span(), predecessor, salt); + testing::set_contract_address(EXECUTOR()); + timelock.execute(call_arr.span(), NO_PREDECESSOR, salt); +} + +#[test] +fn test_execute_when_scheduled() { + let (mut timelock, mut erc721) = setup_dispatchers(); + + let call = single_operation(erc721.contract_address); + let call_span = array![call].span(); + let salt = SALT; + let delay = MIN_DELAY; + + let hash_id = timelock.hash_operation(call_span, NO_PREDECESSOR, salt); + + // schedule + testing::set_contract_address(PROPOSER()); + + timelock.schedule(call_span, NO_PREDECESSOR, salt, delay); + utils::drop_events(timelock.contract_address, 2); + + // fast-forward + testing::set_block_timestamp(delay); + + // Check initial target state + let check_approved_is_zero = erc721.get_approved(TOKEN_ID); + assert_eq!(check_approved_is_zero, ZERO()); + + // execute + testing::set_contract_address(EXECUTOR()); + + timelock.execute(call_span, NO_PREDECESSOR, salt); + assert_only_event_execute(timelock.contract_address, hash_id, call_span); + + // Check target state updates + let check_approved_is_spender = erc721.get_approved(TOKEN_ID); + assert_eq!(check_approved_is_spender, SPENDER()); +} + +#[test] +#[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] +fn test_execute_early() { + let (mut timelock, mut erc721) = setup_dispatchers(); + + let call = single_operation(erc721.contract_address); + let call_span = array![call].span(); + let salt = SALT; + let delay = MIN_DELAY; + + // schedule + testing::set_contract_address(PROPOSER()); + + timelock.schedule(call_span, NO_PREDECESSOR, salt, delay); + utils::drop_events(timelock.contract_address, 2); + + // fast-forward + let early_time = delay - 1; + testing::set_block_timestamp(early_time); + + // execute + testing::set_contract_address(EXECUTOR()); + timelock.execute(call_span, NO_PREDECESSOR, salt); +} + +#[test] +#[should_panic(expected: ('Caller is missing role', 'ENTRYPOINT_FAILED'))] +fn test_execute_unauthorized() { + let (mut timelock, mut erc721) = setup_dispatchers(); + + let call = single_operation(erc721.contract_address); + let call_span = array![call].span(); + let salt = SALT; + let delay = MIN_DELAY; + + // schedule + testing::set_contract_address(PROPOSER()); + timelock.schedule(call_span, NO_PREDECESSOR, salt, delay); + + // fast-forward + testing::set_block_timestamp(delay); + + // execute + testing::set_contract_address(OTHER()); + timelock.execute(call_span, NO_PREDECESSOR, salt); +} + +#[test] +#[should_panic( + expected: ( + 'Timelock: unexpected op state', + 'ENTRYPOINT_FAILED', + 'ENTRYPOINT_FAILED', + 'ENTRYPOINT_FAILED' + ) +)] +fn test_execute_reentrant_call() { + let mut timelock = deploy_timelock(); + let mut attacker = deploy_attacker(); + + let reentrant_call = Call { + to: attacker.contract_address, selector: selector!("reenter"), calldata: array![].span() + }; + + let reentrant_call_span = array![reentrant_call].span(); + let delay = MIN_DELAY; + + // schedule + testing::set_contract_address(PROPOSER()); + timelock.schedule(reentrant_call_span, NO_PREDECESSOR, SALT, delay); + + // fast-forward + testing::set_block_timestamp(delay); + + // Grant executor role to attacker + testing::set_contract_address(ADMIN()); + timelock.grant_role(EXECUTOR_ROLE, attacker.contract_address); + + // Attempt reentrant call + testing::set_contract_address(EXECUTOR()); + timelock.execute(reentrant_call_span, NO_PREDECESSOR, SALT); } // hash_operation #[test] fn test_hash_operation() { - let mut timelock = deploy_timelock(); - let mut erc721 = deploy_erc721(timelock.contract_address); - let erc721_address = erc721.contract_address; + let (mut timelock, mut erc721) = setup_dispatchers(); // Call 1 let mut calldata1 = array![]; calldata1.append_serde(SPENDER()); calldata1.append_serde(TOKEN_ID); - let mut call1 = Call { to: erc721_address, selector: 'approve', calldata: calldata1.span() }; + let mut call1 = Call { + to: erc721.contract_address, selector: 'approve', calldata: calldata1.span() + }; // Call 2 let mut calldata2 = array![]; @@ -385,7 +482,7 @@ fn test_hash_operation() { calldata2.append_serde(RECIPIENT()); calldata2.append_serde(TOKEN_ID); let mut call2 = Call { - to: erc721_address, selector: 'transfer_from', calldata: calldata2.span() + to: erc721.contract_address, selector: 'transfer_from', calldata: calldata2.span() }; // Hash operation @@ -398,13 +495,13 @@ fn test_hash_operation() { let mut expected_hash = PoseidonTrait::new() .update_with(14) // total elements of Call span .update_with(2) // total number of Calls - .update_with(erc721_address) // call1::to + .update_with(erc721.contract_address) // call1::to .update_with('approve') // call1::selector .update_with(3) // call1::calldata.len .update_with(SPENDER()) // call1::calldata::to .update_with(TOKEN_ID.low) // call1::calldata::token_id.low .update_with(TOKEN_ID.high) // call1::calldata::token_id.high - .update_with(erc721_address) // call2::to + .update_with(erc721.contract_address) // call2::to .update_with('transfer_from') // call2::selector .update_with(4) // call2::calldata.len .update_with(timelock.contract_address) // call2::calldata::from @@ -432,7 +529,7 @@ fn assert_event_delay_change(contract: ContractAddress, old_duration: u64, new_d fn assert_only_event_delay_change(contract: ContractAddress, old_duration: u64, new_duration: u64) { assert_event_delay_change(contract, old_duration, new_duration); - utils::drop_event(contract); + utils::assert_no_events_left(contract); } fn assert_event_schedule( @@ -455,7 +552,7 @@ fn assert_only_event_schedule( contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64 ) { assert_event_schedule(contract, calls, predecessor, delay); - utils::drop_event(contract); + utils::assert_no_events_left(contract); } fn assert_event_call_salt(contract: ContractAddress, id: felt252, salt: felt252) { @@ -466,5 +563,23 @@ fn assert_event_call_salt(contract: ContractAddress, id: felt252, salt: felt252) fn assert_only_event_call_salt(contract: ContractAddress, id: felt252, salt: felt252) { assert_event_call_salt(contract, id, salt); - utils::drop_event(contract); + utils::assert_no_events_left(contract); +} + +fn assert_event_execute(contract: ContractAddress, id: felt252, calls: Span) { + let event = utils::pop_log::(contract).unwrap(); + let expected = TimelockControllerComponent::Event::CallExecuted(CallExecuted { id, calls }); + assert!(event == expected); + + // Check indexed keys + let mut indexed_keys = array![]; + indexed_keys.append_serde(selector!("CallExecuted")); + indexed_keys.append_serde(id); + indexed_keys.append_serde(calls); + utils::assert_indexed_keys(event, indexed_keys.span()); +} + +fn assert_only_event_execute(contract: ContractAddress, id: felt252, calls: Span) { + assert_event_execute(contract, id, calls); + utils::assert_no_events_left(contract); } From fcd0331d3b2fa86d9d3c63708d99309c7efaafc8 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 29 May 2024 20:35:19 -0400 Subject: [PATCH 020/103] add abi interface --- src/governance/timelock/interface.cairo | 91 +++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/governance/timelock/interface.cairo b/src/governance/timelock/interface.cairo index f60691785..d023ce988 100644 --- a/src/governance/timelock/interface.cairo +++ b/src/governance/timelock/interface.cairo @@ -35,3 +35,94 @@ trait ITimelock { fn execute(ref self: TState, calls: Span, predecessor: felt252, salt: felt252); fn update_delay(ref self: TState, new_delay: u64); } + +#[starknet::interface] +trait ITimelockABI { + fn is_operation(self: @TState, id: felt252) -> bool; + fn is_operation_pending(self: @TState, id: felt252) -> bool; + fn is_operation_ready(self: @TState, id: felt252) -> bool; + fn is_operation_done(self: @TState, id: felt252) -> bool; + fn get_timestamp(self: @TState, id: felt252) -> u64; + fn get_operation_state(self: @TState, id: felt252) -> OperationState; + fn get_min_delay(self: @TState) -> u64; + fn hash_operation( + self: @TState, calls: Span, predecessor: felt252, salt: felt252 + ) -> felt252; + fn schedule( + ref self: TState, calls: Span, predecessor: felt252, salt: felt252, delay: u64 + ); + fn cancel(ref self: TState, id: felt252); + fn execute(ref self: TState, calls: Span, predecessor: felt252, salt: felt252); + fn update_delay(ref self: TState, new_delay: u64); + + // ISRC5 + fn supports_interface(self: @TState, interface_id: felt252) -> bool; + + // IAccessControl + fn has_role(self: @TState, role: felt252, account: ContractAddress) -> bool; + fn get_role_admin(self: @TState, role: felt252) -> felt252; + fn grant_role(ref self: TState, role: felt252, account: ContractAddress); + fn revoke_role(ref self: TState, role: felt252, account: ContractAddress); + fn renounce_role(ref self: TState, role: felt252, account: ContractAddress); + + // IAccessControlCamel + fn hasRole(self: @TState, role: felt252, account: ContractAddress) -> bool; + fn getRoleAdmin(self: @TState, role: felt252) -> felt252; + fn grantRole(ref self: TState, role: felt252, account: ContractAddress); + fn revokeRole(ref self: TState, role: felt252, account: ContractAddress); + fn renounceRole(ref self: TState, role: felt252, account: ContractAddress); + + // IERC721Receiver + fn on_erc721_received( + self: @TState, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + data: Span + ) -> felt252; + + // IERC721ReceiverCamel + fn onERC721Received( + self: @TState, + operator: ContractAddress, + from: ContractAddress, + tokenId: u256, + data: Span + ) -> felt252; + + // IERC1155Receiver + fn on_erc1155_received( + self: @TState, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + value: u256, + data: Span + ) -> felt252; + fn on_erc1155_batch_received( + self: @TState, + operator: ContractAddress, + from: ContractAddress, + token_ids: Span, + values: Span, + data: Span + ) -> felt252; + + // IERC1155ReceiverCamel + fn onERC1155Received( + self: @TState, + operator: ContractAddress, + from: ContractAddress, + tokenId: u256, + value: u256, + data: Span + ) -> felt252; + fn onERC1155BatchReceived( + self: @TState, + operator: ContractAddress, + from: ContractAddress, + tokenIds: Span, + values: Span, + data: Span + ) -> felt252; +} From 94505300ac7d2a2adc70be5f19b0a491083e223c Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 29 May 2024 20:35:39 -0400 Subject: [PATCH 021/103] add reentrancy mock for timelock --- src/tests/mocks/timelock_mocks.cairo | 52 ++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/tests/mocks/timelock_mocks.cairo b/src/tests/mocks/timelock_mocks.cairo index 75c802a47..56c41d32d 100644 --- a/src/tests/mocks/timelock_mocks.cairo +++ b/src/tests/mocks/timelock_mocks.cairo @@ -75,3 +75,55 @@ mod TimelockControllerMock { self.timelock.initializer(min_delay, proposers, executors, admin); } } + +#[starknet::interface] +trait ITimelockAttacker { + fn reenter(ref self: TState); +} + +#[starknet::contract] +mod TimelockAttackerMock { + use openzeppelin::governance::timelock::interface::{ + ITimelockDispatcher, ITimelockDispatcherTrait + }; + use openzeppelin::tests::utils::constants::SALT; + use starknet::ContractAddress; + use starknet::account::Call; + use super::ITimelockAttacker; + + const PREDECESSOR: felt252 = 0; + + #[storage] + struct Storage { + balance: felt252, + count: felt252 + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[abi(embed_v0)] + impl TimelockAttackerImpl of ITimelockAttacker { + fn reenter(ref self: ContractState) { + let new_balance = self.balance.read() + 1; + self.balance.write(new_balance); + + let sender = starknet::get_caller_address(); + let this = starknet::get_contract_address(); + + let current_count = self.count.read(); + if current_count != 2 { + self.count.write(current_count + 1); + + let reentrant_call = Call { + to: this, selector: selector!("reenter"), calldata: array![].span() + }; + let reentrant_call_span = array![reentrant_call].span(); + + let timelock = ITimelockDispatcher { contract_address: sender }; + timelock.execute(reentrant_call_span, PREDECESSOR, SALT); + } + } + } +} From 290ecbcd13590f3f7452f0b82c878d8ed1a88ea1 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 30 May 2024 00:30:00 -0400 Subject: [PATCH 022/103] add tests for cancel and update_delay --- src/tests/governance/test_timelock.cairo | 156 +++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 282eb406a..9a5b1cb30 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -52,6 +52,7 @@ fn COMPONENT_STATE() -> ComponentState { } const MIN_DELAY: u64 = 1000; +const NEW_DELAY: u64 = 2000; const NO_PREDECESSOR: felt252 = 0; fn PROPOSER() -> ContractAddress { @@ -85,6 +86,16 @@ fn single_operation(erc721_addr: ContractAddress) -> Call { Call { to: erc721_addr, selector: selectors::approve, calldata: calldata.span() } } +fn failing_operation(erc721_addr: ContractAddress) -> Call { + let nonexistent_token = 999_u256; + // Call: approve + let mut calldata = array![]; + calldata.append_serde(SPENDER()); + calldata.append_serde(nonexistent_token); + + Call { to: erc721_addr, selector: selectors::approve, calldata: calldata.span() } +} + fn batched_operations(erc721_addr: ContractAddress, timelock_addr: ContractAddress) -> Span { // Call 1: approve let mut calldata1 = array![]; @@ -461,6 +472,123 @@ fn test_execute_reentrant_call() { timelock.execute(reentrant_call_span, NO_PREDECESSOR, SALT); } +#[test] +#[should_panic(expected: ('ERC721: invalid token ID', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED'))] +fn test_execute_partial_execution() { + let (mut timelock, mut erc721) = setup_dispatchers(); + + let good_call = single_operation(erc721.contract_address); + let bad_call = failing_operation(erc721.contract_address); + let call_span = array![good_call, bad_call].span(); + let salt = SALT; + let delay = MIN_DELAY; + + // schedule + testing::set_contract_address(PROPOSER()); + + timelock.schedule(call_span, NO_PREDECESSOR, salt, delay); + utils::drop_events(timelock.contract_address, 2); + + // fast-forward + testing::set_block_timestamp(delay); + + // execute + testing::set_contract_address(EXECUTOR()); + timelock.execute(call_span, NO_PREDECESSOR, salt); +} + +// cancel + +#[test] +fn test_cancel() { + let (mut timelock, mut erc721) = setup_dispatchers(); + + let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); + let hash_id = timelock.hash_operation(batched_operations, NO_PREDECESSOR, SALT); + + // Schedule + testing::set_contract_address(PROPOSER()); // PROPOSER is also CANCELLER + timelock.schedule(batched_operations, NO_PREDECESSOR, SALT, MIN_DELAY); + utils::drop_events(timelock.contract_address, 2); + + // Cancel + timelock.cancel(hash_id); + assert_only_event_cancel(timelock.contract_address, hash_id); +} + +#[test] +#[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] +fn test_cancel_invalid_operation() { + let (mut timelock, mut erc721) = setup_dispatchers(); + + let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); + let hash_id = timelock.hash_operation(batched_operations, NO_PREDECESSOR, SALT); + + // PROPOSER is also CANCELLER + testing::set_contract_address(PROPOSER()); + + timelock.cancel(hash_id); +} + +#[test] +#[should_panic(expected: ('Caller is missing role', 'ENTRYPOINT_FAILED'))] +fn test_cancel_unauthorized() { + let (mut timelock, mut erc721) = setup_dispatchers(); + + let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); + let hash_id = timelock.hash_operation(batched_operations, NO_PREDECESSOR, SALT); + + // Schedule + testing::set_contract_address(PROPOSER()); + timelock.schedule(batched_operations, NO_PREDECESSOR, SALT, MIN_DELAY); + utils::drop_events(timelock.contract_address, 2); + + // Cancel + testing::set_contract_address(OTHER()); + timelock.cancel(hash_id); +} + +// update_delay + +#[test] +#[should_panic(expected: ('Timelock: unauthorized caller', 'ENTRYPOINT_FAILED'))] +fn test_update_delay_unauthorized() { + let mut timelock = deploy_timelock(); + + timelock.update_delay(NEW_DELAY); +} + +#[test] +fn test_update_delay_scheduled() { + let mut timelock = deploy_timelock(); + + let update_delay_call = Call { + to: timelock.contract_address, + selector: selector!("update_delay"), + calldata: array![NEW_DELAY.into()].span() + }; + let call_span = array![update_delay_call].span(); + let hash_id = timelock.hash_operation(call_span, NO_PREDECESSOR, SALT); + + // Schedule + testing::set_contract_address(PROPOSER()); + timelock.schedule(call_span, NO_PREDECESSOR, SALT, MIN_DELAY); + utils::drop_events(timelock.contract_address, 2); + + // fast-forward + testing::set_block_timestamp(MIN_DELAY); + + // execute + testing::set_contract_address(EXECUTOR()); + timelock.execute(call_span, NO_PREDECESSOR, SALT); + assert_event_delay(timelock.contract_address, MIN_DELAY, NEW_DELAY); + assert_only_event_execute(timelock.contract_address, hash_id, call_span); + + // Check new minimum delay + let get_new_delay = timelock.get_min_delay(); + assert_eq!(get_new_delay, NEW_DELAY); +} + // hash_operation #[test] @@ -583,3 +711,31 @@ fn assert_only_event_execute(contract: ContractAddress, id: felt252, calls: Span assert_event_execute(contract, id, calls); utils::assert_no_events_left(contract); } + +fn assert_event_cancel(contract: ContractAddress, id: felt252) { + let event = utils::pop_log::(contract).unwrap(); + let expected = TimelockControllerComponent::Event::Cancelled(Cancelled { id }); + assert!(event == expected); + + // Check indexed keys + let mut indexed_keys = array![]; + indexed_keys.append_serde(selector!("Cancelled")); + indexed_keys.append_serde(id); + utils::assert_indexed_keys(event, indexed_keys.span()); +} + +fn assert_only_event_cancel(contract: ContractAddress, id: felt252) { + assert_event_cancel(contract, id); + utils::assert_no_events_left(contract); +} + +fn assert_event_delay(contract: ContractAddress, old_duration: u64, new_duration: u64) { + let event = utils::pop_log::(contract).unwrap(); + let expected = TimelockControllerComponent::Event::MinDelayChange(MinDelayChange { old_duration, new_duration }); + assert!(event == expected); +} + +fn assert_only_event_delay(contract: ContractAddress, old_duration: u64, new_duration: u64) { + assert_event_delay(contract, old_duration, new_duration); + utils::assert_no_events_left(contract); +} From c5d4102cdceb7a276794df797dcdc7a78e95c5a5 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 30 May 2024 00:32:44 -0400 Subject: [PATCH 023/103] fix hash_op test --- src/tests/governance/test_timelock.cairo | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 9a5b1cb30..f397beddb 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -601,7 +601,7 @@ fn test_hash_operation() { calldata1.append_serde(TOKEN_ID); let mut call1 = Call { - to: erc721.contract_address, selector: 'approve', calldata: calldata1.span() + to: erc721.contract_address, selector: selectors::approve, calldata: calldata1.span() }; // Call 2 @@ -610,7 +610,7 @@ fn test_hash_operation() { calldata2.append_serde(RECIPIENT()); calldata2.append_serde(TOKEN_ID); let mut call2 = Call { - to: erc721.contract_address, selector: 'transfer_from', calldata: calldata2.span() + to: erc721.contract_address, selector: selectors::transfer_from, calldata: calldata2.span() }; // Hash operation @@ -624,13 +624,13 @@ fn test_hash_operation() { .update_with(14) // total elements of Call span .update_with(2) // total number of Calls .update_with(erc721.contract_address) // call1::to - .update_with('approve') // call1::selector + .update_with(selector!("approve")) // call1::selector .update_with(3) // call1::calldata.len .update_with(SPENDER()) // call1::calldata::to .update_with(TOKEN_ID.low) // call1::calldata::token_id.low .update_with(TOKEN_ID.high) // call1::calldata::token_id.high .update_with(erc721.contract_address) // call2::to - .update_with('transfer_from') // call2::selector + .update_with(selector!("transfer_from")) // call2::selector .update_with(4) // call2::calldata.len .update_with(timelock.contract_address) // call2::calldata::from .update_with(RECIPIENT()) // call2::calldata::to From ba282887910db2e653acbfaa972bf3bba7378432 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 30 May 2024 10:44:22 -0400 Subject: [PATCH 024/103] add execute with predecessor test --- src/tests/governance/test_timelock.cairo | 103 ++++++++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index f397beddb..45a49964b 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -15,10 +15,11 @@ use openzeppelin::governance::timelock::TimelockControllerComponent::{ TimelockImpl, InternalImpl as TimelockInternalImpl }; use openzeppelin::governance::timelock::TimelockControllerComponent; +use openzeppelin::governance::timelock::TimelockControllerComponent::OperationState; use openzeppelin::governance::timelock::interface::{ ITimelockABIDispatcher, ITimelockABIDispatcherTrait }; -use openzeppelin::governance::timelock::timelock_controller::{CallPartialEq, HashCallImpl}; +use openzeppelin::governance::timelock::utils::{CallPartialEq, HashCallImpl}; use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; use openzeppelin::tests::mocks::erc721_mocks::DualCaseERC721Mock; @@ -268,14 +269,16 @@ fn test_initializer_min_delay() { fn test_schedule_from_proposer_with_salt() { let (mut timelock, mut erc721) = setup_dispatchers(); let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); + let hash_id = timelock.hash_operation(batched_operations, NO_PREDECESSOR, SALT); + assert_operation_state(timelock, OperationState::Unset, hash_id); testing::set_contract_address(PROPOSER()); timelock.schedule(batched_operations, NO_PREDECESSOR, SALT, MIN_DELAY); + assert_operation_state(timelock, OperationState::Waiting, hash_id); assert_event_schedule(timelock.contract_address, batched_operations, NO_PREDECESSOR, MIN_DELAY); // Check timestamp - let hash_id = timelock.hash_operation(batched_operations, NO_PREDECESSOR, SALT); let operation_ts = timelock.get_timestamp(hash_id); let expected_ts = starknet::get_block_timestamp() + MIN_DELAY; assert_eq!(operation_ts, expected_ts); @@ -497,6 +500,61 @@ fn test_execute_partial_execution() { timelock.execute(call_span, NO_PREDECESSOR, salt); } +#[test] +fn test_execute_with_predecessor() { + let (mut timelock, mut erc721) = setup_dispatchers(); + + // Call 1 + let approve_call = single_operation(erc721.contract_address); + let call_1_span = array![approve_call].span(); + let call_1_id = timelock.hash_operation(call_1_span, NO_PREDECESSOR, SALT); + + // Schedule call 1 + testing::set_contract_address(PROPOSER()); + timelock.schedule(call_1_span, NO_PREDECESSOR, SALT, MIN_DELAY); + + assert_event_schedule(timelock.contract_address, call_1_span, NO_PREDECESSOR, MIN_DELAY); + assert_only_event_call_salt(timelock.contract_address, call_1_id, SALT); + + // Call 2 + let mut calldata_2 = array![]; + calldata_2.append_serde(timelock.contract_address); + calldata_2.append_serde(RECIPIENT()); + calldata_2.append_serde(TOKEN_ID); + + let transfer_call = Call { + to: erc721.contract_address, selector: selectors::transfer_from, calldata: calldata_2.span() + }; + let call_2_span = array![transfer_call].span(); + let call_2_id = timelock.hash_operation(call_2_span, call_1_id, SALT); + + // Schedule call 2 + testing::set_contract_address(PROPOSER()); + timelock.schedule(call_2_span, call_1_id, SALT, MIN_DELAY); + assert_event_schedule(timelock.contract_address, call_2_span, call_1_id, MIN_DELAY); + assert_only_event_call_salt(timelock.contract_address, call_2_id, SALT); + + // Check initial owner + let token_owner = erc721.owner_of(TOKEN_ID); + assert_eq!(token_owner, timelock.contract_address); + + // fast-forward + testing::set_block_timestamp(MIN_DELAY); + + // Execute call 1 + testing::set_contract_address(EXECUTOR()); + timelock.execute(call_1_span, NO_PREDECESSOR, SALT); + assert_only_event_execute(timelock.contract_address, call_1_id, call_1_span); + + // Execute call 2 + timelock.execute(call_2_span, call_1_id, SALT); + assert_event_execute(timelock.contract_address, call_2_id, call_2_span); + + // Check new owner + let token_owner = erc721.owner_of(TOKEN_ID); + assert_eq!(token_owner, RECIPIENT()); +} + // cancel #[test] @@ -647,6 +705,47 @@ fn test_hash_operation() { // Helpers // +fn assert_operation_state(timelock: ITimelockABIDispatcher, exp_state: OperationState, id: felt252) { + let operation_state = timelock.get_operation_state(id); + assert_eq!(operation_state, exp_state); + + let is_operation = timelock.is_operation(id); + let is_pending = timelock.is_operation_pending(id); + let is_ready = timelock.is_operation_ready(id); + let is_done = timelock.is_operation_done(id); + + match exp_state { + OperationState::Unset => { + assert!(!is_operation); + assert!(!is_pending); + assert!(!is_ready); + assert!(!is_done); + }, + OperationState::Waiting => { + assert!(is_operation); + assert!(is_pending); + assert!(!is_ready); + assert!(!is_done); + }, + OperationState::Ready => { + assert!(is_operation); + assert!(!is_pending); + assert!(is_ready); + assert!(!is_done); + }, + OperationState::Done => { + assert!(is_operation); + assert!(!is_pending); + assert!(!is_ready); + assert!(is_done); + } + }; +} + +// +// Event helpers +// + fn assert_event_delay_change(contract: ContractAddress, old_duration: u64, new_duration: u64) { let event = utils::pop_log::(contract).unwrap(); let expected = TimelockControllerComponent::Event::MinDelayChange( From 4540db2fa932f32c691a367d8ea449cd604b5c32 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 30 May 2024 10:45:24 -0400 Subject: [PATCH 025/103] add timelock utils, add operation_state debug impl --- src/governance/timelock.cairo | 1 + src/governance/timelock/interface.cairo | 9 +--- .../timelock/timelock_controller.cairo | 42 ++----------------- src/governance/timelock/utils.cairo | 5 +++ .../timelock/utils/call_impls.cairo | 36 ++++++++++++++++ .../timelock/utils/operation_state.cairo | 23 ++++++++++ 6 files changed, 70 insertions(+), 46 deletions(-) create mode 100644 src/governance/timelock/utils.cairo create mode 100644 src/governance/timelock/utils/call_impls.cairo create mode 100644 src/governance/timelock/utils/operation_state.cairo diff --git a/src/governance/timelock.cairo b/src/governance/timelock.cairo index 36c320c5c..e0e923bdf 100644 --- a/src/governance/timelock.cairo +++ b/src/governance/timelock.cairo @@ -1,4 +1,5 @@ mod interface; mod timelock_controller; +mod utils; use timelock_controller::TimelockControllerComponent; diff --git a/src/governance/timelock/interface.cairo b/src/governance/timelock/interface.cairo index d023ce988..b9038a1c0 100644 --- a/src/governance/timelock/interface.cairo +++ b/src/governance/timelock/interface.cairo @@ -7,14 +7,7 @@ use starknet::ContractAddress; use starknet::account::Call; - -#[derive(Drop, Copy, Serde, PartialEq)] -enum OperationState { - Unset, - Waiting, - Ready, - Done -} +use openzeppelin::governance::timelock::utils::OperationState; #[starknet::interface] trait ITimelock { diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 435cc106a..72e7ea7d3 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -1,9 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.13.0 (governance/timelock/timelock_controller.cairo) -use core::hash::{HashStateTrait, HashStateExTrait, Hash}; -use starknet::account::Call; - /// # TimelockController Component /// /// @@ -15,7 +12,9 @@ mod TimelockControllerComponent { use openzeppelin::access::accesscontrol::AccessControlComponent; use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin::account::utils::execute_single_call; - use openzeppelin::governance::timelock::interface::{ITimelock, OperationState}; + use openzeppelin::governance::timelock::interface::ITimelock; + use openzeppelin::governance::timelock::utils::OperationState; + use openzeppelin::governance::timelock::utils::{CallPartialEq, HashCallImpl}; use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; use openzeppelin::introspection::src5::SRC5Component::SRC5; use openzeppelin::introspection::src5::SRC5Component; @@ -27,7 +26,6 @@ mod TimelockControllerComponent { use starknet::ContractAddress; use starknet::SyscallResultTrait; use starknet::account::Call; - use super::{CallPartialEq, HashCallImpl}; use zeroable::Zeroable; // Constants @@ -290,7 +288,7 @@ mod TimelockControllerComponent { fn before_call(self: @ComponentState, id: felt252, predecessor: felt252) { assert(self.is_operation_ready(id), Errors::UNEXPECTED_OPERATION_STATE); assert( - predecessor == 0 || !self.is_operation_done(predecessor), + predecessor == 0 || self.is_operation_done(predecessor), Errors::UNEXECUTED_PREDECESSOR ); } @@ -325,35 +323,3 @@ mod TimelockControllerComponent { } } } - - -impl HashCallImpl, +HashStateTrait, +Drop> of Hash<@Call, S> { - fn update_state(mut state: S, value: @Call) -> S { - let mut arr = array![]; - Serde::serialize(value, ref arr); - state = state.update(arr.len().into()); - while let Option::Some(elem) = arr.pop_front() { - state = state.update(elem) - }; - state - } -} - -impl CallPartialEq of PartialEq { - #[inline(always)] - fn eq(lhs: @Call, rhs: @Call) -> bool { - let mut lhs_arr = array![]; - Serde::serialize(lhs, ref lhs_arr); - let mut rhs_arr = array![]; - Serde::serialize(lhs, ref rhs_arr); - lhs_arr == rhs_arr - } - - fn ne(lhs: @Call, rhs: @Call) -> bool { - let mut lhs_arr = array![]; - Serde::serialize(lhs, ref lhs_arr); - let mut rhs_arr = array![]; - Serde::serialize(lhs, ref rhs_arr); - !(lhs_arr == rhs_arr) - } -} diff --git a/src/governance/timelock/utils.cairo b/src/governance/timelock/utils.cairo new file mode 100644 index 000000000..c6a2b8ffe --- /dev/null +++ b/src/governance/timelock/utils.cairo @@ -0,0 +1,5 @@ +mod call_impls; +mod operation_state; + +use call_impls::{CallPartialEq, HashCallImpl}; +use operation_state::OperationState; diff --git a/src/governance/timelock/utils/call_impls.cairo b/src/governance/timelock/utils/call_impls.cairo new file mode 100644 index 000000000..1df35b857 --- /dev/null +++ b/src/governance/timelock/utils/call_impls.cairo @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.13.0 (governance/timelock/utils/call_impls.cairo) + +use core::hash::{HashStateTrait, HashStateExTrait, Hash}; +use starknet::account::Call; + +impl HashCallImpl, +HashStateTrait, +Drop> of Hash<@Call, S> { + fn update_state(mut state: S, value: @Call) -> S { + let mut arr = array![]; + Serde::serialize(value, ref arr); + state = state.update(arr.len().into()); + while let Option::Some(elem) = arr.pop_front() { + state = state.update(elem) + }; + state + } +} + +impl CallPartialEq of PartialEq { + #[inline(always)] + fn eq(lhs: @Call, rhs: @Call) -> bool { + let mut lhs_arr = array![]; + Serde::serialize(lhs, ref lhs_arr); + let mut rhs_arr = array![]; + Serde::serialize(lhs, ref rhs_arr); + lhs_arr == rhs_arr + } + + fn ne(lhs: @Call, rhs: @Call) -> bool { + let mut lhs_arr = array![]; + Serde::serialize(lhs, ref lhs_arr); + let mut rhs_arr = array![]; + Serde::serialize(lhs, ref rhs_arr); + !(lhs_arr == rhs_arr) + } +} diff --git a/src/governance/timelock/utils/operation_state.cairo b/src/governance/timelock/utils/operation_state.cairo new file mode 100644 index 000000000..3e1c7b799 --- /dev/null +++ b/src/governance/timelock/utils/operation_state.cairo @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.13.0 (governance/timelock/utils/operation_state.cairo) + +use core::fmt::{Debug, Formatter, Error}; + +#[derive(Drop, Copy, Serde, PartialEq)] +enum OperationState { + Unset, + Waiting, + Ready, + Done +} + +impl DebugOperationState of core::fmt::Debug { + fn fmt(self: @OperationState, ref f: Formatter) -> Result<(), Error> { + match self { + OperationState::Unset => write!(f, "Unset"), + OperationState::Waiting => write!(f, "Waiting"), + OperationState::Ready => write!(f, "Ready"), + OperationState::Done => write!(f, "Done"), + } + } +} From cc70677a16cdf0fc33fa098320abb72e93550f27 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 30 May 2024 10:46:09 -0400 Subject: [PATCH 026/103] fix fmt --- src/governance/timelock/interface.cairo | 2 +- src/tests/governance/test_timelock.cairo | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/governance/timelock/interface.cairo b/src/governance/timelock/interface.cairo index b9038a1c0..ee749856e 100644 --- a/src/governance/timelock/interface.cairo +++ b/src/governance/timelock/interface.cairo @@ -5,9 +5,9 @@ /// /// +use openzeppelin::governance::timelock::utils::OperationState; use starknet::ContractAddress; use starknet::account::Call; -use openzeppelin::governance::timelock::utils::OperationState; #[starknet::interface] trait ITimelock { diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 45a49964b..162a953ba 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -5,6 +5,7 @@ use openzeppelin::access::accesscontrol::AccessControlComponent::{ use openzeppelin::access::accesscontrol::accesscontrol::AccessControlComponent::InternalTrait; use openzeppelin::access::accesscontrol::interface::IAccessControl; use openzeppelin::governance::timelock::TimelockControllerComponent::Call; +use openzeppelin::governance::timelock::TimelockControllerComponent::OperationState; use openzeppelin::governance::timelock::TimelockControllerComponent::{ CallScheduled, CallExecuted, CallSalt, Cancelled, MinDelayChange }; @@ -15,7 +16,6 @@ use openzeppelin::governance::timelock::TimelockControllerComponent::{ TimelockImpl, InternalImpl as TimelockInternalImpl }; use openzeppelin::governance::timelock::TimelockControllerComponent; -use openzeppelin::governance::timelock::TimelockControllerComponent::OperationState; use openzeppelin::governance::timelock::interface::{ ITimelockABIDispatcher, ITimelockABIDispatcherTrait }; @@ -705,7 +705,9 @@ fn test_hash_operation() { // Helpers // -fn assert_operation_state(timelock: ITimelockABIDispatcher, exp_state: OperationState, id: felt252) { +fn assert_operation_state( + timelock: ITimelockABIDispatcher, exp_state: OperationState, id: felt252 +) { let operation_state = timelock.get_operation_state(id); assert_eq!(operation_state, exp_state); @@ -830,7 +832,9 @@ fn assert_only_event_cancel(contract: ContractAddress, id: felt252) { fn assert_event_delay(contract: ContractAddress, old_duration: u64, new_duration: u64) { let event = utils::pop_log::(contract).unwrap(); - let expected = TimelockControllerComponent::Event::MinDelayChange(MinDelayChange { old_duration, new_duration }); + let expected = TimelockControllerComponent::Event::MinDelayChange( + MinDelayChange { old_duration, new_duration } + ); assert!(event == expected); } From aab92451dbf37bc3bf67038fa220fed84957402a Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 30 May 2024 12:35:52 -0400 Subject: [PATCH 027/103] improve imports --- src/governance/timelock/utils.cairo | 1 - src/tests/governance/test_timelock.cairo | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/governance/timelock/utils.cairo b/src/governance/timelock/utils.cairo index c6a2b8ffe..7b12cd975 100644 --- a/src/governance/timelock/utils.cairo +++ b/src/governance/timelock/utils.cairo @@ -1,5 +1,4 @@ mod call_impls; mod operation_state; -use call_impls::{CallPartialEq, HashCallImpl}; use operation_state::OperationState; diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 162a953ba..4642e0813 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -19,7 +19,7 @@ use openzeppelin::governance::timelock::TimelockControllerComponent; use openzeppelin::governance::timelock::interface::{ ITimelockABIDispatcher, ITimelockABIDispatcherTrait }; -use openzeppelin::governance::timelock::utils::{CallPartialEq, HashCallImpl}; +use openzeppelin::governance::timelock::utils::call_impls::{CallPartialEq, HashCallImpl}; use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; use openzeppelin::tests::mocks::erc721_mocks::DualCaseERC721Mock; From b19c1937de3868e310094bb9aed6132b235e7648 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 30 May 2024 12:36:07 -0400 Subject: [PATCH 028/103] improve _execute --- src/governance/timelock/timelock_controller.cairo | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 72e7ea7d3..1d9d7da15 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -11,10 +11,9 @@ mod TimelockControllerComponent { use openzeppelin::access::accesscontrol::AccessControlComponent::InternalTrait as AccessControlInternalTrait; use openzeppelin::access::accesscontrol::AccessControlComponent; use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; - use openzeppelin::account::utils::execute_single_call; use openzeppelin::governance::timelock::interface::ITimelock; use openzeppelin::governance::timelock::utils::OperationState; - use openzeppelin::governance::timelock::utils::{CallPartialEq, HashCallImpl}; + use openzeppelin::governance::timelock::utils::call_impls::{CallPartialEq, HashCallImpl}; use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; use openzeppelin::introspection::src5::SRC5Component::SRC5; use openzeppelin::introspection::src5::SRC5Component; @@ -311,12 +310,8 @@ mod TimelockControllerComponent { break; } - let mut call = Call { - to: *calls.at(index).to, - selector: *calls.at(index).selector, - calldata: *calls.at(index).calldata - }; - execute_single_call(call); + let Call { to, selector, calldata } = calls.at(index); + starknet::call_contract_syscall(*to, *selector, *calldata).unwrap_syscall(); index += 1; } From b4702781944a0794d75bb5e47ebc94be65239812 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 03:02:44 -0400 Subject: [PATCH 029/103] add basic mock for tests --- src/tests/mocks/timelock_mocks.cairo | 42 ++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/tests/mocks/timelock_mocks.cairo b/src/tests/mocks/timelock_mocks.cairo index 56c41d32d..d871b4301 100644 --- a/src/tests/mocks/timelock_mocks.cairo +++ b/src/tests/mocks/timelock_mocks.cairo @@ -76,6 +76,42 @@ mod TimelockControllerMock { } } +#[starknet::interface] +trait IMockContract { + fn set_number(ref self: TState, new_number: felt252); + fn get_number(self: @TState) -> felt252; + fn failing_function(self: @TState); +} + +#[starknet::contract] +mod MockContract { + use super::IMockContract; + + #[storage] + struct Storage { + number: felt252, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[abi(embed_v0)] + impl MockContractImpl of IMockContract { + fn set_number(ref self: ContractState, new_number: felt252) { + self.number.write(new_number); + } + + fn get_number(self: @ContractState) -> felt252 { + self.number.read() + } + + fn failing_function(self: @ContractState) { + core::panic_with_felt252('Expected failure'); + } + } +} + #[starknet::interface] trait ITimelockAttacker { fn reenter(ref self: TState); @@ -86,9 +122,9 @@ mod TimelockAttackerMock { use openzeppelin::governance::timelock::interface::{ ITimelockDispatcher, ITimelockDispatcherTrait }; + use openzeppelin::governance::timelock::utils::call_impls::Call; use openzeppelin::tests::utils::constants::SALT; use starknet::ContractAddress; - use starknet::account::Call; use super::ITimelockAttacker; const PREDECESSOR: felt252 = 0; @@ -119,10 +155,10 @@ mod TimelockAttackerMock { let reentrant_call = Call { to: this, selector: selector!("reenter"), calldata: array![].span() }; - let reentrant_call_span = array![reentrant_call].span(); + //let reentrant_call_span = array![reentrant_call].span(); let timelock = ITimelockDispatcher { contract_address: sender }; - timelock.execute(reentrant_call_span, PREDECESSOR, SALT); + timelock.execute(reentrant_call, PREDECESSOR, SALT); } } } From 269ea6ef88b15e7f9fbe19647e9967f65ad8f7cb Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 03:03:42 -0400 Subject: [PATCH 030/103] add tmp call struct --- src/governance/timelock/utils/call_impls.cairo | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/governance/timelock/utils/call_impls.cairo b/src/governance/timelock/utils/call_impls.cairo index 1df35b857..7bf6c8117 100644 --- a/src/governance/timelock/utils/call_impls.cairo +++ b/src/governance/timelock/utils/call_impls.cairo @@ -2,7 +2,16 @@ // OpenZeppelin Contracts for Cairo v0.13.0 (governance/timelock/utils/call_impls.cairo) use core::hash::{HashStateTrait, HashStateExTrait, Hash}; -use starknet::account::Call; +use starknet::ContractAddress; + +// TMP until cairo v2.7 release, then use SN `Call` struct +// `Call` from v2.6 does not derive Copy trait +#[derive(Drop, Copy, Serde, Debug)] +pub struct Call { + pub to: ContractAddress, + pub selector: felt252, + pub calldata: Span +} impl HashCallImpl, +HashStateTrait, +Drop> of Hash<@Call, S> { fn update_state(mut state: S, value: @Call) -> S { From 08908b56e7283d5b7eebb23cc84ef47c61702d5d Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 03:04:17 -0400 Subject: [PATCH 031/103] add batch fns to interface --- src/governance/timelock/interface.cairo | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/governance/timelock/interface.cairo b/src/governance/timelock/interface.cairo index ee749856e..16e1d1e02 100644 --- a/src/governance/timelock/interface.cairo +++ b/src/governance/timelock/interface.cairo @@ -7,7 +7,7 @@ use openzeppelin::governance::timelock::utils::OperationState; use starknet::ContractAddress; -use starknet::account::Call; +use openzeppelin::governance::timelock::utils::call_impls::Call; #[starknet::interface] trait ITimelock { @@ -19,13 +19,20 @@ trait ITimelock { fn get_operation_state(self: @TState, id: felt252) -> OperationState; fn get_min_delay(self: @TState) -> u64; fn hash_operation( + self: @TState, call: Call, predecessor: felt252, salt: felt252 + ) -> felt252; + fn hash_operation_batch( self: @TState, calls: Span, predecessor: felt252, salt: felt252 ) -> felt252; fn schedule( + ref self: TState, call: Call, predecessor: felt252, salt: felt252, delay: u64 + ); + fn schedule_batch( ref self: TState, calls: Span, predecessor: felt252, salt: felt252, delay: u64 ); fn cancel(ref self: TState, id: felt252); - fn execute(ref self: TState, calls: Span, predecessor: felt252, salt: felt252); + fn execute(ref self: TState, call: Call, predecessor: felt252, salt: felt252); + fn execute_batch(ref self: TState, calls: Span, predecessor: felt252, salt: felt252); fn update_delay(ref self: TState, new_delay: u64); } @@ -39,13 +46,20 @@ trait ITimelockABI { fn get_operation_state(self: @TState, id: felt252) -> OperationState; fn get_min_delay(self: @TState) -> u64; fn hash_operation( + self: @TState, call: Call, predecessor: felt252, salt: felt252 + ) -> felt252; + fn hash_operation_batch( self: @TState, calls: Span, predecessor: felt252, salt: felt252 ) -> felt252; fn schedule( + ref self: TState, call: Call, predecessor: felt252, salt: felt252, delay: u64 + ); + fn schedule_batch( ref self: TState, calls: Span, predecessor: felt252, salt: felt252, delay: u64 ); fn cancel(ref self: TState, id: felt252); - fn execute(ref self: TState, calls: Span, predecessor: felt252, salt: felt252); + fn execute(ref self: TState, call: Call, predecessor: felt252, salt: felt252); + fn execute_batch(ref self: TState, calls: Span, predecessor: felt252, salt: felt252); fn update_delay(ref self: TState, new_delay: u64); // ISRC5 From 090d1a33bbe994a24b41cfd7693127c166b7670e Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 03:04:52 -0400 Subject: [PATCH 032/103] add batch fns --- .../timelock/timelock_controller.cairo | 108 ++++++++++++++---- 1 file changed, 83 insertions(+), 25 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 1d9d7da15..d88862ace 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -13,7 +13,7 @@ mod TimelockControllerComponent { use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin::governance::timelock::interface::ITimelock; use openzeppelin::governance::timelock::utils::OperationState; - use openzeppelin::governance::timelock::utils::call_impls::{CallPartialEq, HashCallImpl}; + use openzeppelin::governance::timelock::utils::call_impls::{CallPartialEq, HashCallImpl, Call}; use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; use openzeppelin::introspection::src5::SRC5Component::SRC5; use openzeppelin::introspection::src5::SRC5Component; @@ -24,7 +24,7 @@ mod TimelockControllerComponent { use poseidon::PoseidonTrait; use starknet::ContractAddress; use starknet::SyscallResultTrait; - use starknet::account::Call; + //use starknet::account::Call; use zeroable::Zeroable; // Constants @@ -53,7 +53,7 @@ mod TimelockControllerComponent { #[derive(Drop, PartialEq, starknet::Event)] struct CallScheduled { #[key] - calls: Span, + call: Call, predecessor: felt252, delay: u64 } @@ -64,7 +64,7 @@ mod TimelockControllerComponent { #[key] id: felt252, #[key] - calls: Span + call: Call } /// Emitted when... @@ -148,6 +148,19 @@ mod TimelockControllerComponent { } fn hash_operation( + self: @ComponentState, + call: Call, + predecessor: felt252, + salt: felt252 + ) -> felt252 { + PoseidonTrait::new() + .update_with(@call) + .update_with(predecessor) + .update_with(salt) + .finalize() + } + + fn hash_operation_batch( self: @ComponentState, calls: Span, predecessor: felt252, @@ -161,6 +174,24 @@ mod TimelockControllerComponent { } fn schedule( + ref self: ComponentState, + call: Call, + predecessor: felt252, + salt: felt252, + delay: u64 + ) { + self.assert_only_role(PROPOSER_ROLE); + + let id = self.hash_operation(call, predecessor, salt); + self._schedule(id, delay); + self.emit(CallScheduled { call, predecessor, delay }); + + if salt != 0 { + self.emit(CallSalt { id, salt }); + } + } + + fn schedule_batch( ref self: ComponentState, calls: Span, predecessor: felt252, @@ -169,9 +200,19 @@ mod TimelockControllerComponent { ) { self.assert_only_role(PROPOSER_ROLE); - let id = self.hash_operation(calls, predecessor, salt); + let id = self.hash_operation_batch(calls, predecessor, salt); self._schedule(id, delay); - self.emit(CallScheduled { calls, predecessor, delay }); + + let mut index = 0; + loop { + if index == calls.len() { + break; + } + + let call = *calls.at(index); + self.emit(CallScheduled { call, predecessor, delay }); + index += 1; + }; if salt != 0 { self.emit(CallSalt { id, salt }); @@ -187,6 +228,21 @@ mod TimelockControllerComponent { } fn execute( + ref self: ComponentState, + call: Call, + predecessor: felt252, + salt: felt252 + ) { + self.assert_only_role(EXECUTOR_ROLE); + + let id = self.hash_operation(call, predecessor, salt); + self._before_call(id, predecessor); + self._execute(call); + self.emit(CallExecuted { id, call }); + self._after_call(id); + } + + fn execute_batch( ref self: ComponentState, calls: Span, predecessor: felt252, @@ -194,11 +250,22 @@ mod TimelockControllerComponent { ) { self.assert_only_role(EXECUTOR_ROLE); - let id = self.hash_operation(calls, predecessor, salt); - self.before_call(id, predecessor); - self._execute(calls); - self.emit(CallExecuted { id, calls }); - self.after_call(id); + let id = self.hash_operation_batch(calls, predecessor, salt); + self._before_call(id, predecessor); + + let mut index = 0; + loop { + if index == calls.len() { + break; + } + + let call = *calls.at(index); + self._execute(call); + self.emit(CallExecuted { id, call }); + index += 1; + }; + + self._after_call(id); } fn update_delay(ref self: ComponentState, new_delay: u64) { @@ -284,7 +351,7 @@ mod TimelockControllerComponent { access_component.assert_only_role(role); } - fn before_call(self: @ComponentState, id: felt252, predecessor: felt252) { + fn _before_call(self: @ComponentState, id: felt252, predecessor: felt252) { assert(self.is_operation_ready(id), Errors::UNEXPECTED_OPERATION_STATE); assert( predecessor == 0 || self.is_operation_done(predecessor), @@ -292,7 +359,7 @@ mod TimelockControllerComponent { ); } - fn after_call(ref self: ComponentState, id: felt252) { + fn _after_call(ref self: ComponentState, id: felt252) { assert(self.is_operation_ready(id), Errors::UNEXPECTED_OPERATION_STATE); self.TimelockController_timestamps.write(id, DONE_TIMESTAMP); } @@ -303,18 +370,9 @@ mod TimelockControllerComponent { self.TimelockController_timestamps.write(id, starknet::get_block_timestamp() + delay); } - fn _execute(ref self: ComponentState, mut calls: Span) { - let mut index = 0; - loop { - if index == calls.len() { - break; - } - - let Call { to, selector, calldata } = calls.at(index); - starknet::call_contract_syscall(*to, *selector, *calldata).unwrap_syscall(); - - index += 1; - } + fn _execute(ref self: ComponentState, call: Call) { + let Call { to, selector, calldata } = call; + starknet::call_contract_syscall(to, selector, calldata).unwrap_syscall(); } } } From 2b11d3e1fc11b3fc3a6c17ece174f8bcc5a96dba Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 03:05:35 -0400 Subject: [PATCH 033/103] refactor tests to use dedicated mock, add batch tests --- src/tests/governance/test_timelock.cairo | 945 ++++++++++++++++------- 1 file changed, 681 insertions(+), 264 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 4642e0813..8d8ee1183 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -23,6 +23,7 @@ use openzeppelin::governance::timelock::utils::call_impls::{CallPartialEq, HashC use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; use openzeppelin::tests::mocks::erc721_mocks::DualCaseERC721Mock; +use openzeppelin::tests::mocks::timelock_mocks::{MockContract, IMockContractDispatcher, IMockContractDispatcherTrait}; use openzeppelin::tests::mocks::timelock_mocks::{ ITimelockAttackerDispatcher, ITimelockAttackerDispatcherTrait }; @@ -54,6 +55,7 @@ fn COMPONENT_STATE() -> ComponentState { const MIN_DELAY: u64 = 1000; const NEW_DELAY: u64 = 2000; +const VALUE: felt252 = 'VALUE'; const NO_PREDECESSOR: felt252 = 0; fn PROPOSER() -> ContractAddress { @@ -78,51 +80,35 @@ fn get_executors() -> (ContractAddress, ContractAddress, ContractAddress) { (e1, e2, e3) } -fn single_operation(erc721_addr: ContractAddress) -> Call { +fn single_operation(target: ContractAddress) -> Call { // Call: approve let mut calldata = array![]; - calldata.append_serde(SPENDER()); - calldata.append_serde(TOKEN_ID); + calldata.append_serde(VALUE); - Call { to: erc721_addr, selector: selectors::approve, calldata: calldata.span() } + Call { to: target, selector: selector!("set_number"), calldata: calldata.span() } } -fn failing_operation(erc721_addr: ContractAddress) -> Call { - let nonexistent_token = 999_u256; - // Call: approve - let mut calldata = array![]; - calldata.append_serde(SPENDER()); - calldata.append_serde(nonexistent_token); +fn batched_operations(target: ContractAddress) -> Span { + let mut calls = array![]; + let call = single_operation(target); + calls.append(call); + calls.append(call); + calls.append(call); - Call { to: erc721_addr, selector: selectors::approve, calldata: calldata.span() } + calls.span() } -fn batched_operations(erc721_addr: ContractAddress, timelock_addr: ContractAddress) -> Span { - // Call 1: approve - let mut calldata1 = array![]; - calldata1.append_serde(SPENDER()); - calldata1.append_serde(TOKEN_ID); - - let call1 = Call { to: erc721_addr, selector: selectors::approve, calldata: calldata1.span() }; - - // Call 2: transfer_from - let mut calldata2 = array![]; - calldata2.append_serde(timelock_addr); - calldata2.append_serde(RECIPIENT()); - calldata2.append_serde(TOKEN_ID); - let call2 = Call { - to: erc721_addr, selector: selectors::transfer_from, calldata: calldata2.span() - }; +fn failing_operation(target: ContractAddress) -> Call { + let mut calldata = array![]; - array![call1, call2].span() + Call { to: target, selector: selector!("failing_function"), calldata: calldata.span() } } -fn setup_dispatchers() -> (ITimelockABIDispatcher, IERC721Dispatcher) { +fn setup_dispatchers() -> (ITimelockABIDispatcher, IMockContractDispatcher) { let timelock = deploy_timelock(); - let token_recipient = timelock.contract_address; - let erc721 = deploy_erc721(token_recipient); + let target = deploy_mock_target(); - (timelock, erc721) + (timelock, target) } fn deploy_timelock() -> ITimelockABIDispatcher { @@ -161,6 +147,13 @@ fn deploy_erc721(recipient: ContractAddress) -> IERC721Dispatcher { IERC721Dispatcher { contract_address: address } } +fn deploy_mock_target() -> IMockContractDispatcher { + let mut calldata = array![]; + + let address = utils::deploy(MockContract::TEST_CLASS_HASH, calldata); + IMockContractDispatcher { contract_address: address } +} + fn deploy_attacker() -> ITimelockAttackerDispatcher { let mut calldata = array![]; @@ -263,83 +256,255 @@ fn test_initializer_min_delay() { assert_only_event_delay_change(ZERO(), 0, MIN_DELAY); } -// schedule +// hash_operation #[test] -fn test_schedule_from_proposer_with_salt() { - let (mut timelock, mut erc721) = setup_dispatchers(); - let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); - let hash_id = timelock.hash_operation(batched_operations, NO_PREDECESSOR, SALT); - assert_operation_state(timelock, OperationState::Unset, hash_id); +fn test_hash_operation() { + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = 123; + let salt = SALT; + + // Setup call + let mut calldata = array![]; + calldata.append_serde(VALUE); + + let mut call = Call { + to: target.contract_address, selector: selector!("set_number"), calldata: calldata.span() + }; + + let hashed_operation = timelock.hash_operation(call, predecessor, salt); + + let mut expected_hash = PoseidonTrait::new() + .update_with(4) // total elements of call + .update_with(target.contract_address) // call::to + .update_with(selector!("set_number")) // call::selector + .update_with(1) // call::calldata.len + .update_with(VALUE) // call::calldata::number + .update_with(predecessor) // predecessor + .update_with(salt) // salt + .finalize(); + assert_eq!(hashed_operation, expected_hash); +} + +#[test] +fn test_hash_operation_batch() { + let (mut timelock, mut target) = setup_dispatchers(); + + // Setup call + let mut calldata = array![]; + calldata.append_serde(VALUE); + + let mut call = Call { + to: target.contract_address, selector: selector!("set_number"), calldata: calldata.span() + }; + + // Hash operation + let predecessor = 123; + let salt = SALT; + let calls = array![call, call, call].span(); + let hashed_operation = timelock.hash_operation_batch(calls, predecessor, salt); + + // Manually set hash elements + let mut expected_hash = PoseidonTrait::new() + .update_with(13) // total elements of Call span + .update_with(3) // total number of Calls + + .update_with(target.contract_address) // call::to + .update_with(selector!("set_number")) // call::selector + .update_with(1) // call::calldata.len + .update_with(VALUE) // call::calldata::number + + .update_with(target.contract_address) // call::to + .update_with(selector!("set_number")) // call::selector + .update_with(1) // call::calldata.len + .update_with(VALUE) // call::calldata::number + + .update_with(target.contract_address) // call::to + .update_with(selector!("set_number")) // call::selector + .update_with(1) // call::calldata.len + .update_with(VALUE) // call::calldata::number + + .update_with(predecessor) // predecessor + .update_with(salt) // salt + .finalize(); + + assert_eq!(hashed_operation, expected_hash); +} + +// schedule + +fn schedule_from_proposer(salt: felt252) { + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let delay = MIN_DELAY; + let mut salt = salt; + + let call = single_operation(target.contract_address); + let target_id = timelock.hash_operation(call, predecessor, salt); + assert_operation_state(timelock, OperationState::Unset, target_id); testing::set_contract_address(PROPOSER()); - timelock.schedule(batched_operations, NO_PREDECESSOR, SALT, MIN_DELAY); - assert_operation_state(timelock, OperationState::Waiting, hash_id); - assert_event_schedule(timelock.contract_address, batched_operations, NO_PREDECESSOR, MIN_DELAY); + timelock.schedule(call, predecessor, salt, delay); + assert_operation_state(timelock, OperationState::Waiting, target_id); // Check timestamp - let operation_ts = timelock.get_timestamp(hash_id); - let expected_ts = starknet::get_block_timestamp() + MIN_DELAY; + let operation_ts = timelock.get_timestamp(target_id); + let expected_ts = starknet::get_block_timestamp() + delay; assert_eq!(operation_ts, expected_ts); - assert_only_event_call_salt(timelock.contract_address, hash_id, SALT); + // Check event(s) + if salt != 0 { + assert_event_schedule(timelock.contract_address, call, predecessor, delay); + assert_only_event_call_salt(timelock.contract_address, target_id, salt); + } else { + assert_only_event_schedule(timelock.contract_address, call, predecessor, delay); + } +} + +#[test] +fn test_schedule_from_proposer_with_salt() { + let salt = SALT; + schedule_from_proposer(salt); +} + +#[test] +fn test_schedule_from_proposer_no_salt() { + let salt = 0; + schedule_from_proposer(salt); } #[test] #[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] fn test_schedule_overwrite() { - let (mut timelock, mut erc721) = setup_dispatchers(); + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = SALT; + let delay = MIN_DELAY; - let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); + let call = single_operation(target.contract_address); testing::set_contract_address(PROPOSER()); - timelock.schedule(batched_operations, NO_PREDECESSOR, SALT, MIN_DELAY); - timelock.schedule(batched_operations, NO_PREDECESSOR, SALT, MIN_DELAY); + timelock.schedule(call, predecessor, salt, delay); + timelock.schedule(call, predecessor, salt, delay); } #[test] #[should_panic(expected: ('Caller is missing role', 'ENTRYPOINT_FAILED'))] fn test_schedule_unauthorized() { - let (mut timelock, mut erc721) = setup_dispatchers(); + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = SALT; + let delay = MIN_DELAY; - let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); + let call = single_operation(target.contract_address); testing::set_contract_address(OTHER()); - timelock.schedule(batched_operations, NO_PREDECESSOR, SALT, MIN_DELAY); + timelock.schedule(call, predecessor, salt, delay); } #[test] #[should_panic(expected: ('Timelock: insufficient delay', 'ENTRYPOINT_FAILED'))] fn test_schedule_bad_min_delay() { - let (mut timelock, mut erc721) = setup_dispatchers(); - + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = SALT; let bad_delay = MIN_DELAY - 1; - let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); + + let call = single_operation(target.contract_address); testing::set_contract_address(PROPOSER()); - timelock.schedule(batched_operations, NO_PREDECESSOR, SALT, bad_delay); + timelock.schedule(call, predecessor, salt, bad_delay); } -#[test] -fn test_schedule_with_salt_zero() { - let (mut timelock, mut erc721) = setup_dispatchers(); +// schedule_batch - let zero_salt = 0; - let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); - let hash_id = timelock.hash_operation(batched_operations, NO_PREDECESSOR, zero_salt); +fn schedule_batch_from_proposer(salt: felt252) { + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let delay = MIN_DELAY; + let mut salt = salt; + + let calls = batched_operations(target.contract_address); + let target_id = timelock.hash_operation_batch(calls, predecessor, salt); + assert_operation_state(timelock, OperationState::Unset, target_id); - // Schedule testing::set_contract_address(PROPOSER()); - timelock.schedule(batched_operations, NO_PREDECESSOR, zero_salt, MIN_DELAY); - assert_only_event_schedule( - timelock.contract_address, batched_operations, NO_PREDECESSOR, MIN_DELAY - ); + + timelock.schedule_batch(calls, predecessor, salt, delay); + assert_operation_state(timelock, OperationState::Waiting, target_id); // Check timestamp - let operation_ts = timelock.get_timestamp(hash_id); - let expected_ts = starknet::get_block_timestamp() + MIN_DELAY; - assert_eq!(operation_ts, expected_ts) + let operation_ts = timelock.get_timestamp(target_id); + let expected_ts = starknet::get_block_timestamp() + delay; + assert_eq!(operation_ts, expected_ts); + + // Check events + if salt != 0 { + assert_event_schedule(timelock.contract_address, *calls.at(0), predecessor, delay); + assert_event_schedule(timelock.contract_address, *calls.at(1), predecessor, delay); + assert_event_schedule(timelock.contract_address, *calls.at(2), predecessor, delay); + assert_only_event_call_salt(timelock.contract_address, target_id, salt); + } else { + assert_event_schedule(timelock.contract_address, *calls.at(0), predecessor, delay); + assert_event_schedule(timelock.contract_address, *calls.at(1), predecessor, delay); + assert_only_event_schedule(timelock.contract_address, *calls.at(2), predecessor, delay); + } +} + +#[test] +fn test_schedule_batch_from_proposer_with_salt() { + let salt = SALT; + schedule_batch_from_proposer(salt); +} + +#[test] +fn test_schedule_batch_from_proposer_no_salt() { + let no_salt = 0; + schedule_batch_from_proposer(no_salt); +} + +#[test] +#[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] +fn test_schedule_batch_overwrite() { + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = SALT; + let delay = MIN_DELAY; + + let calls = batched_operations(target.contract_address); + + testing::set_contract_address(PROPOSER()); + timelock.schedule_batch(calls, predecessor, salt, delay); + timelock.schedule_batch(calls, predecessor, salt, delay); +} + +#[test] +#[should_panic(expected: ('Caller is missing role', 'ENTRYPOINT_FAILED'))] +fn test_schedule_batch_unauthorized() { + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = SALT; + let delay = MIN_DELAY; + + let calls = batched_operations(target.contract_address); + + testing::set_contract_address(OTHER()); + timelock.schedule_batch(calls, predecessor, salt, delay); +} + +#[test] +#[should_panic(expected: ('Timelock: insufficient delay', 'ENTRYPOINT_FAILED'))] +fn test_schedule_batch_bad_min_delay() { + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = SALT; + let bad_delay = MIN_DELAY - 1; + + let calls = batched_operations(target.contract_address); + + testing::set_contract_address(PROPOSER()); + timelock.schedule_batch(calls, predecessor, salt, bad_delay); } // execute @@ -347,96 +512,97 @@ fn test_schedule_with_salt_zero() { #[test] #[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] fn test_execute_when_not_scheduled() { - let (mut timelock, mut erc721) = setup_dispatchers(); + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = 0; - let salt = SALT; - let call = single_operation(erc721.contract_address); - let call_arr = array![call]; + let call = single_operation(target.contract_address); testing::set_contract_address(EXECUTOR()); - timelock.execute(call_arr.span(), NO_PREDECESSOR, salt); + timelock.execute(call, predecessor, salt); } #[test] fn test_execute_when_scheduled() { - let (mut timelock, mut erc721) = setup_dispatchers(); - - let call = single_operation(erc721.contract_address); - let call_span = array![call].span(); - let salt = SALT; + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = 0; let delay = MIN_DELAY; - let hash_id = timelock.hash_operation(call_span, NO_PREDECESSOR, salt); + // Set up call + let call = single_operation(target.contract_address); + let target_id = timelock.hash_operation(call, predecessor, salt); + assert_operation_state(timelock, OperationState::Unset, target_id); - // schedule + // Schedule testing::set_contract_address(PROPOSER()); + timelock.schedule(call, predecessor, salt, delay); + assert_only_event_schedule(timelock.contract_address, call, predecessor, delay); + assert_operation_state(timelock, OperationState::Waiting, target_id); - timelock.schedule(call_span, NO_PREDECESSOR, salt, delay); - utils::drop_events(timelock.contract_address, 2); - - // fast-forward + // Fast-forward testing::set_block_timestamp(delay); + assert_operation_state(timelock, OperationState::Ready, target_id); // Check initial target state - let check_approved_is_zero = erc721.get_approved(TOKEN_ID); - assert_eq!(check_approved_is_zero, ZERO()); + let check_target = target.get_number(); + assert_eq!(check_target, 0); - // execute + // Execute testing::set_contract_address(EXECUTOR()); + timelock.execute(call, predecessor, salt); - timelock.execute(call_span, NO_PREDECESSOR, salt); - assert_only_event_execute(timelock.contract_address, hash_id, call_span); + assert_operation_state(timelock, OperationState::Done, target_id); + assert_only_event_execute(timelock.contract_address, target_id, call); // Check target state updates - let check_approved_is_spender = erc721.get_approved(TOKEN_ID); - assert_eq!(check_approved_is_spender, SPENDER()); + let check_target = target.get_number(); + assert_eq!(check_target, VALUE); } #[test] #[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] fn test_execute_early() { - let (mut timelock, mut erc721) = setup_dispatchers(); - - let call = single_operation(erc721.contract_address); - let call_span = array![call].span(); - let salt = SALT; + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = 0; let delay = MIN_DELAY; - // schedule - testing::set_contract_address(PROPOSER()); + let call = single_operation(target.contract_address); - timelock.schedule(call_span, NO_PREDECESSOR, salt, delay); - utils::drop_events(timelock.contract_address, 2); + // Schedule + testing::set_contract_address(PROPOSER()); + timelock.schedule(call, predecessor, salt, delay); - // fast-forward + // Fast-forward let early_time = delay - 1; testing::set_block_timestamp(early_time); - // execute + // Execute testing::set_contract_address(EXECUTOR()); - timelock.execute(call_span, NO_PREDECESSOR, salt); + timelock.execute(call, predecessor, salt); } #[test] #[should_panic(expected: ('Caller is missing role', 'ENTRYPOINT_FAILED'))] fn test_execute_unauthorized() { - let (mut timelock, mut erc721) = setup_dispatchers(); - - let call = single_operation(erc721.contract_address); - let call_span = array![call].span(); - let salt = SALT; + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = 0; let delay = MIN_DELAY; - // schedule + let call = single_operation(target.contract_address); + + // Schedule testing::set_contract_address(PROPOSER()); - timelock.schedule(call_span, NO_PREDECESSOR, salt, delay); + timelock.schedule(call, predecessor, salt, delay); - // fast-forward + // Fast-forward testing::set_block_timestamp(delay); - // execute + // Execute testing::set_contract_address(OTHER()); - timelock.execute(call_span, NO_PREDECESSOR, salt); + timelock.execute(call, predecessor, salt); } #[test] @@ -451,19 +617,19 @@ fn test_execute_unauthorized() { fn test_execute_reentrant_call() { let mut timelock = deploy_timelock(); let mut attacker = deploy_attacker(); + let predecessor = NO_PREDECESSOR; + let salt = 0; + let delay = MIN_DELAY; let reentrant_call = Call { to: attacker.contract_address, selector: selector!("reenter"), calldata: array![].span() }; - let reentrant_call_span = array![reentrant_call].span(); - let delay = MIN_DELAY; - - // schedule + // Schedule testing::set_contract_address(PROPOSER()); - timelock.schedule(reentrant_call_span, NO_PREDECESSOR, SALT, delay); + timelock.schedule(reentrant_call, predecessor, salt, delay); - // fast-forward + // Fast-forward testing::set_block_timestamp(delay); // Grant executor role to attacker @@ -472,138 +638,381 @@ fn test_execute_reentrant_call() { // Attempt reentrant call testing::set_contract_address(EXECUTOR()); - timelock.execute(reentrant_call_span, NO_PREDECESSOR, SALT); + timelock.execute(reentrant_call, predecessor, salt); } #[test] -#[should_panic(expected: ('ERC721: invalid token ID', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED'))] -fn test_execute_partial_execution() { - let (mut timelock, mut erc721) = setup_dispatchers(); - - let good_call = single_operation(erc721.contract_address); - let bad_call = failing_operation(erc721.contract_address); - let call_span = array![good_call, bad_call].span(); - let salt = SALT; +#[should_panic(expected: ('Timelock: awaiting predecessor', 'ENTRYPOINT_FAILED'))] +fn test_execute_before_dependency() { + let (mut timelock, mut target) = setup_dispatchers(); + let salt = 0; let delay = MIN_DELAY; - // schedule + // Call 1 + let call_1 = single_operation(target.contract_address); + let predecessor_1 = NO_PREDECESSOR; + let target_id_1 = timelock.hash_operation(call_1, predecessor_1, salt); + + // Call 2 + let call_2 = single_operation(target.contract_address); + let predecessor_2 = target_id_1; + let target_id_2 = timelock.hash_operation(call_2, predecessor_2, salt); + + // Schedule call 1 testing::set_contract_address(PROPOSER()); + timelock.schedule(call_1, predecessor_1, salt, delay); - timelock.schedule(call_span, NO_PREDECESSOR, salt, delay); - utils::drop_events(timelock.contract_address, 2); + // Schedule call 2 + timelock.schedule(call_2, predecessor_2, salt, delay); - // fast-forward + // Fast-forward testing::set_block_timestamp(delay); + assert_operation_state(timelock, OperationState::Ready, target_id_1); + assert_operation_state(timelock, OperationState::Ready, target_id_2); - // execute + // Execute testing::set_contract_address(EXECUTOR()); - timelock.execute(call_span, NO_PREDECESSOR, salt); + timelock.execute(call_2, predecessor_2, salt); } #[test] -fn test_execute_with_predecessor() { - let (mut timelock, mut erc721) = setup_dispatchers(); +fn test_execute_after_dependency() { + let (mut timelock, mut target) = setup_dispatchers(); + let salt = 0; + let delay = MIN_DELAY; // Call 1 - let approve_call = single_operation(erc721.contract_address); - let call_1_span = array![approve_call].span(); - let call_1_id = timelock.hash_operation(call_1_span, NO_PREDECESSOR, SALT); + let call_1 = single_operation(target.contract_address); + let predecessor_1 = NO_PREDECESSOR; + let target_id_1 = timelock.hash_operation(call_1, predecessor_1, salt); + assert_operation_state(timelock, OperationState::Unset, target_id_1); + + // Call 2 + let call_2 = single_operation(target.contract_address); + let predecessor_2 = target_id_1; + let target_id_2 = timelock.hash_operation(call_2, predecessor_2, salt); + assert_operation_state(timelock, OperationState::Unset, target_id_2); // Schedule call 1 testing::set_contract_address(PROPOSER()); - timelock.schedule(call_1_span, NO_PREDECESSOR, SALT, MIN_DELAY); + timelock.schedule(call_1, predecessor_1, salt, delay); + assert_operation_state(timelock, OperationState::Waiting, target_id_1); + assert_only_event_schedule(timelock.contract_address, call_1, predecessor_1, delay); - assert_event_schedule(timelock.contract_address, call_1_span, NO_PREDECESSOR, MIN_DELAY); - assert_only_event_call_salt(timelock.contract_address, call_1_id, SALT); + // Schedule call 2 + timelock.schedule(call_2, predecessor_2, salt, delay); + assert_operation_state(timelock, OperationState::Waiting, target_id_2); + assert_only_event_schedule(timelock.contract_address, call_2, predecessor_2, delay); - // Call 2 - let mut calldata_2 = array![]; - calldata_2.append_serde(timelock.contract_address); - calldata_2.append_serde(RECIPIENT()); - calldata_2.append_serde(TOKEN_ID); + // Fast-forward + testing::set_block_timestamp(delay); + assert_operation_state(timelock, OperationState::Ready, target_id_1); + assert_operation_state(timelock, OperationState::Ready, target_id_2); - let transfer_call = Call { - to: erc721.contract_address, selector: selectors::transfer_from, calldata: calldata_2.span() - }; - let call_2_span = array![transfer_call].span(); - let call_2_id = timelock.hash_operation(call_2_span, call_1_id, SALT); + // Execute call 1 + testing::set_contract_address(EXECUTOR()); + timelock.execute(call_1, predecessor_1, salt); + assert_operation_state(timelock, OperationState::Done, target_id_1); + assert_event_execute(timelock.contract_address, target_id_1, call_1); - // Schedule call 2 + // Execute call 2 + timelock.execute(call_2, predecessor_2, salt); + assert_operation_state(timelock, OperationState::Done, target_id_2); + assert_only_event_execute(timelock.contract_address, target_id_2, call_2); +} + +// execute_batch + +#[test] +#[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] +fn test_execute_batch_when_not_scheduled() { + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = 0; + + let calls = batched_operations(target.contract_address); + + testing::set_contract_address(EXECUTOR()); + timelock.execute_batch(calls, predecessor, salt); +} + +#[test] +fn test_execute_batch_when_scheduled() { + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = 0; + let delay = MIN_DELAY; + + // Set up call + let calls = batched_operations(target.contract_address); + let target_id = timelock.hash_operation_batch(calls, predecessor, salt); + assert_operation_state(timelock, OperationState::Unset, target_id); + + // Schedule + testing::set_contract_address(PROPOSER()); + timelock.schedule_batch(calls, predecessor, salt, delay); + assert_operation_state(timelock, OperationState::Waiting, target_id); + assert_only_events_schedule_batch(timelock.contract_address, calls, predecessor, delay); + + // Fast-forward + testing::set_block_timestamp(delay); + assert_operation_state(timelock, OperationState::Ready, target_id); + + // Check initial target state + let check_target = target.get_number(); + assert_eq!(check_target, 0); + + // Execute + testing::set_contract_address(EXECUTOR()); + timelock.execute_batch(calls, predecessor, salt); + assert_operation_state(timelock, OperationState::Done, target_id); + assert_only_events_execute_batch(timelock.contract_address, target_id, calls); + + // Check target state updates + let check_target = target.get_number(); + assert_eq!(check_target, VALUE); +} + +#[test] +#[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] +fn test_execute_batch_early() { + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = 0; + let delay = MIN_DELAY; + + let calls = batched_operations(target.contract_address); + + // Schedule + testing::set_contract_address(PROPOSER()); + timelock.schedule_batch(calls, predecessor, salt, delay); + + // Fast-forward + let early_time = delay - 1; + testing::set_block_timestamp(early_time); + + // Execute + testing::set_contract_address(EXECUTOR()); + timelock.execute_batch(calls, predecessor, salt); +} + +#[test] +#[should_panic(expected: ('Caller is missing role', 'ENTRYPOINT_FAILED'))] +fn test_execute_batch_unauthorized() { + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = 0; + let delay = MIN_DELAY; + + let calls = batched_operations(target.contract_address); + + // Schedule testing::set_contract_address(PROPOSER()); - timelock.schedule(call_2_span, call_1_id, SALT, MIN_DELAY); - assert_event_schedule(timelock.contract_address, call_2_span, call_1_id, MIN_DELAY); - assert_only_event_call_salt(timelock.contract_address, call_2_id, SALT); + timelock.schedule_batch(calls, predecessor, salt, delay); + + // Fast-forward + testing::set_block_timestamp(delay); + + // Execute + testing::set_contract_address(OTHER()); + timelock.execute_batch(calls, predecessor, salt); +} + +#[test] +#[should_panic( + expected: ( + 'Timelock: unexpected op state', + 'ENTRYPOINT_FAILED', + 'ENTRYPOINT_FAILED', + 'ENTRYPOINT_FAILED' + ) +)] +fn test_execute_batch_reentrant_call() { + let (mut timelock, mut target) = setup_dispatchers(); + let mut attacker = deploy_attacker(); + let predecessor = NO_PREDECESSOR; + let salt = 0; + let delay = MIN_DELAY; + + let call_1 = single_operation(target.contract_address); + let call_2 = single_operation(target.contract_address); + let reentrant_call = Call { + to: attacker.contract_address, selector: selector!("reenter"), calldata: array![].span() + }; + let calls = array![call_1, call_2, reentrant_call].span(); - // Check initial owner - let token_owner = erc721.owner_of(TOKEN_ID); - assert_eq!(token_owner, timelock.contract_address); + // schedule + testing::set_contract_address(PROPOSER()); + timelock.schedule_batch(calls, predecessor, salt, delay); // fast-forward - testing::set_block_timestamp(MIN_DELAY); + testing::set_block_timestamp(delay); - // Execute call 1 + // Grant executor role to attacker + testing::set_contract_address(ADMIN()); + timelock.grant_role(EXECUTOR_ROLE, attacker.contract_address); + + // Attempt reentrant call testing::set_contract_address(EXECUTOR()); - timelock.execute(call_1_span, NO_PREDECESSOR, SALT); - assert_only_event_execute(timelock.contract_address, call_1_id, call_1_span); + timelock.execute_batch(calls, predecessor, salt); +} - // Execute call 2 - timelock.execute(call_2_span, call_1_id, SALT); - assert_event_execute(timelock.contract_address, call_2_id, call_2_span); +#[test] +#[should_panic(expected: ('Expected failure', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED'))] +fn test_execute_batch_partial_execution() { + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = 0; + let delay = MIN_DELAY; + + let good_call = single_operation(target.contract_address); + let bad_call = failing_operation(target.contract_address); + let calls = array![good_call, bad_call].span(); + + // Schedule + testing::set_contract_address(PROPOSER()); + timelock.schedule_batch(calls, predecessor, salt, delay); - // Check new owner - let token_owner = erc721.owner_of(TOKEN_ID); - assert_eq!(token_owner, RECIPIENT()); + // Fast-forward + testing::set_block_timestamp(delay); + + // Execute + testing::set_contract_address(EXECUTOR()); + timelock.execute_batch(calls, predecessor, salt); +} + +#[test] +#[should_panic(expected: ('Timelock: awaiting predecessor', 'ENTRYPOINT_FAILED'))] +fn test_execute_batch_before_dependency() { + let (mut timelock, mut target) = setup_dispatchers(); + let salt = 0; + let delay = MIN_DELAY; + + // Call 1 + let calls_1 = batched_operations(target.contract_address); + let predecessor_1 = NO_PREDECESSOR; + let target_id_1 = timelock.hash_operation_batch(calls_1, predecessor_1, salt); + + // Call 2 + let calls_2 = batched_operations(target.contract_address); + let predecessor_2 = target_id_1; + + // Schedule calls 1 + testing::set_contract_address(PROPOSER()); + timelock.schedule_batch(calls_1, predecessor_1, salt, delay); + + // Schedule calls 2 + timelock.schedule_batch(calls_2, predecessor_2, salt, delay); + + // Fast-forward + testing::set_block_timestamp(delay); + + // Execute + testing::set_contract_address(EXECUTOR()); + timelock.execute_batch(calls_2, predecessor_2, salt); +} + +#[test] +fn test_execute_batch_after_dependency() { + let (mut timelock, mut target) = setup_dispatchers(); + let salt = 0; + let delay = MIN_DELAY; + + // Calls 1 + let calls_1 = batched_operations(target.contract_address); + let predecessor_1 = NO_PREDECESSOR; + let target_id_1 = timelock.hash_operation_batch(calls_1, predecessor_1, salt); + assert_operation_state(timelock, OperationState::Unset, target_id_1); + + // Calls 2 + let calls_2 = batched_operations(target.contract_address); + let predecessor_2 = target_id_1; + let target_id_2 = timelock.hash_operation_batch(calls_2, predecessor_2, salt); + assert_operation_state(timelock, OperationState::Unset, target_id_2); + + // Schedule calls 1 + testing::set_contract_address(PROPOSER()); + timelock.schedule_batch(calls_1, predecessor_1, salt, delay); + assert_operation_state(timelock, OperationState::Waiting, target_id_1); + assert_only_events_schedule_batch(timelock.contract_address, calls_1, predecessor_1, delay); + + // Schedule calls 2 + timelock.schedule_batch(calls_2, predecessor_2, salt, delay); + assert_operation_state(timelock, OperationState::Waiting, target_id_2); + assert_only_events_schedule_batch(timelock.contract_address, calls_2, predecessor_2, delay); + + // Fast-forward + testing::set_block_timestamp(delay); + assert_operation_state(timelock, OperationState::Ready, target_id_1); + assert_operation_state(timelock, OperationState::Ready, target_id_2); + + // Execute calls 1 + testing::set_contract_address(EXECUTOR()); + timelock.execute_batch(calls_1, predecessor_1, salt); + assert_only_events_execute_batch(timelock.contract_address, target_id_1, calls_1); + assert_operation_state(timelock, OperationState::Done, target_id_1); + + // Execute calls 2 + timelock.execute_batch(calls_2, predecessor_2, salt); + assert_operation_state(timelock, OperationState::Done, target_id_2); + assert_only_events_execute_batch(timelock.contract_address, target_id_2, calls_2); } // cancel #[test] -fn test_cancel() { - let (mut timelock, mut erc721) = setup_dispatchers(); +fn test_cancel_from_canceller() { + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = 0; + let delay = MIN_DELAY; - let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); - let hash_id = timelock.hash_operation(batched_operations, NO_PREDECESSOR, SALT); + let call = single_operation(target.contract_address); + let target_id = timelock.hash_operation(call, predecessor, salt); + assert_operation_state(timelock, OperationState::Unset, target_id); // Schedule testing::set_contract_address(PROPOSER()); // PROPOSER is also CANCELLER - timelock.schedule(batched_operations, NO_PREDECESSOR, SALT, MIN_DELAY); - utils::drop_events(timelock.contract_address, 2); + timelock.schedule(call, predecessor, salt, delay); + assert_operation_state(timelock, OperationState::Waiting, target_id); + assert_only_event_schedule(timelock.contract_address, call, predecessor, delay); // Cancel - timelock.cancel(hash_id); - assert_only_event_cancel(timelock.contract_address, hash_id); + timelock.cancel(target_id); + assert_only_event_cancel(timelock.contract_address, target_id); + assert_operation_state(timelock, OperationState::Unset, target_id); } #[test] #[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] fn test_cancel_invalid_operation() { - let (mut timelock, mut erc721) = setup_dispatchers(); - - let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); - let hash_id = timelock.hash_operation(batched_operations, NO_PREDECESSOR, SALT); + let (mut timelock, _) = setup_dispatchers(); + let invalid_id = 0; // PROPOSER is also CANCELLER testing::set_contract_address(PROPOSER()); - - timelock.cancel(hash_id); + timelock.cancel(invalid_id); } #[test] #[should_panic(expected: ('Caller is missing role', 'ENTRYPOINT_FAILED'))] fn test_cancel_unauthorized() { - let (mut timelock, mut erc721) = setup_dispatchers(); + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = 0; + let delay = MIN_DELAY; - let batched_operations = batched_operations(erc721.contract_address, timelock.contract_address); - let hash_id = timelock.hash_operation(batched_operations, NO_PREDECESSOR, SALT); + let call = single_operation(target.contract_address); + let target_id = timelock.hash_operation(call, predecessor, salt); // Schedule testing::set_contract_address(PROPOSER()); - timelock.schedule(batched_operations, NO_PREDECESSOR, SALT, MIN_DELAY); + timelock.schedule(call, predecessor, salt, delay); utils::drop_events(timelock.contract_address, 2); // Cancel testing::set_contract_address(OTHER()); - timelock.cancel(hash_id); + timelock.cancel(target_id); } // update_delay @@ -619,88 +1028,38 @@ fn test_update_delay_unauthorized() { #[test] fn test_update_delay_scheduled() { let mut timelock = deploy_timelock(); + let predecessor = NO_PREDECESSOR; + let salt = 0; + let delay = MIN_DELAY; - let update_delay_call = Call { + let call = Call { to: timelock.contract_address, selector: selector!("update_delay"), calldata: array![NEW_DELAY.into()].span() }; - let call_span = array![update_delay_call].span(); - let hash_id = timelock.hash_operation(call_span, NO_PREDECESSOR, SALT); + let target_id = timelock.hash_operation(call, predecessor, salt); // Schedule testing::set_contract_address(PROPOSER()); - timelock.schedule(call_span, NO_PREDECESSOR, SALT, MIN_DELAY); - utils::drop_events(timelock.contract_address, 2); + timelock.schedule(call, predecessor, salt, delay); + assert_operation_state(timelock, OperationState::Waiting, target_id); + assert_only_event_schedule(timelock.contract_address, call, predecessor, delay); - // fast-forward - testing::set_block_timestamp(MIN_DELAY); + // Fast-forward + testing::set_block_timestamp(delay); - // execute + // Execute testing::set_contract_address(EXECUTOR()); - timelock.execute(call_span, NO_PREDECESSOR, SALT); + timelock.execute(call, predecessor, salt); + assert_operation_state(timelock, OperationState::Done, target_id); assert_event_delay(timelock.contract_address, MIN_DELAY, NEW_DELAY); - assert_only_event_execute(timelock.contract_address, hash_id, call_span); + assert_only_event_execute(timelock.contract_address, target_id, call); // Check new minimum delay let get_new_delay = timelock.get_min_delay(); assert_eq!(get_new_delay, NEW_DELAY); } -// hash_operation - -#[test] -fn test_hash_operation() { - let (mut timelock, mut erc721) = setup_dispatchers(); - - // Call 1 - let mut calldata1 = array![]; - calldata1.append_serde(SPENDER()); - calldata1.append_serde(TOKEN_ID); - - let mut call1 = Call { - to: erc721.contract_address, selector: selectors::approve, calldata: calldata1.span() - }; - - // Call 2 - let mut calldata2 = array![]; - calldata2.append_serde(timelock.contract_address); - calldata2.append_serde(RECIPIENT()); - calldata2.append_serde(TOKEN_ID); - let mut call2 = Call { - to: erc721.contract_address, selector: selectors::transfer_from, calldata: calldata2.span() - }; - - // Hash operation - let predecessor = 123; - let salt = SALT; - let call_span = array![call1, call2].span(); - let hashed_operation = timelock.hash_operation(call_span, predecessor, salt); - - // Manually set hash elements - let mut expected_hash = PoseidonTrait::new() - .update_with(14) // total elements of Call span - .update_with(2) // total number of Calls - .update_with(erc721.contract_address) // call1::to - .update_with(selector!("approve")) // call1::selector - .update_with(3) // call1::calldata.len - .update_with(SPENDER()) // call1::calldata::to - .update_with(TOKEN_ID.low) // call1::calldata::token_id.low - .update_with(TOKEN_ID.high) // call1::calldata::token_id.high - .update_with(erc721.contract_address) // call2::to - .update_with(selector!("transfer_from")) // call2::selector - .update_with(4) // call2::calldata.len - .update_with(timelock.contract_address) // call2::calldata::from - .update_with(RECIPIENT()) // call2::calldata::to - .update_with(TOKEN_ID.low) // call2::calldata::token_id.low - .update_with(TOKEN_ID.high) // call2::calldata::token_id.high - .update_with(predecessor) // predecessor - .update_with(salt) // salt - .finalize(); - - assert_eq!(hashed_operation, expected_hash); -} - // // Helpers // @@ -731,7 +1090,7 @@ fn assert_operation_state( }, OperationState::Ready => { assert!(is_operation); - assert!(!is_pending); + assert!(is_pending); assert!(is_ready); assert!(!is_done); }, @@ -748,6 +1107,8 @@ fn assert_operation_state( // Event helpers // +// MinDelayChange + fn assert_event_delay_change(contract: ContractAddress, old_duration: u64, new_duration: u64) { let event = utils::pop_log::(contract).unwrap(); let expected = TimelockControllerComponent::Event::MinDelayChange( @@ -761,29 +1122,56 @@ fn assert_only_event_delay_change(contract: ContractAddress, old_duration: u64, utils::assert_no_events_left(contract); } +// CallScheduled + fn assert_event_schedule( - contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64 + contract: ContractAddress, call: Call, predecessor: felt252, delay: u64 ) { let event = utils::pop_log::(contract).unwrap(); let expected = TimelockControllerComponent::Event::CallScheduled( - CallScheduled { calls, predecessor, delay } + CallScheduled { call, predecessor, delay } ); assert!(event == expected); // Check indexed keys let mut indexed_keys = array![]; indexed_keys.append_serde(selector!("CallScheduled")); - indexed_keys.append_serde(calls); + indexed_keys.append_serde(call); utils::assert_indexed_keys(event, indexed_keys.span()); } fn assert_only_event_schedule( - contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64 + contract: ContractAddress, call: Call, predecessor: felt252, delay: u64 ) { - assert_event_schedule(contract, calls, predecessor, delay); + assert_event_schedule(contract, call, predecessor, delay); utils::assert_no_events_left(contract); } +fn assert_events_schedule_batch(contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64) { + let mut index = 0; + loop { + if index == calls.len() { + break; + } + assert_event_schedule(contract, *calls.at(index), predecessor, delay); + index += 1; + } +} + +fn assert_only_events_schedule_batch(contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64) { + let mut index = 0; + loop { + if index == calls.len() - 1 { + break; + } + assert_event_schedule(contract, *calls.at(index), predecessor, delay); + index += 1; + }; + assert_only_event_schedule(contract, *calls.at(index), predecessor, delay); +} + +// CallSalt + fn assert_event_call_salt(contract: ContractAddress, id: felt252, salt: felt252) { let event = utils::pop_log::(contract).unwrap(); let expected = TimelockControllerComponent::Event::CallSalt(CallSalt { id, salt }); @@ -795,24 +1183,51 @@ fn assert_only_event_call_salt(contract: ContractAddress, id: felt252, salt: fel utils::assert_no_events_left(contract); } -fn assert_event_execute(contract: ContractAddress, id: felt252, calls: Span) { +// CallExecuted + +fn assert_event_execute(contract: ContractAddress, id: felt252, call: Call) { let event = utils::pop_log::(contract).unwrap(); - let expected = TimelockControllerComponent::Event::CallExecuted(CallExecuted { id, calls }); + let expected = TimelockControllerComponent::Event::CallExecuted(CallExecuted { id, call }); assert!(event == expected); // Check indexed keys let mut indexed_keys = array![]; indexed_keys.append_serde(selector!("CallExecuted")); indexed_keys.append_serde(id); - indexed_keys.append_serde(calls); + indexed_keys.append_serde(call); utils::assert_indexed_keys(event, indexed_keys.span()); } -fn assert_only_event_execute(contract: ContractAddress, id: felt252, calls: Span) { - assert_event_execute(contract, id, calls); +fn assert_only_event_execute(contract: ContractAddress, id: felt252, call: Call) { + assert_event_execute(contract, id, call); utils::assert_no_events_left(contract); } +fn assert_events_execute_batch(contract: ContractAddress, id: felt252, calls: Span) { + let mut index = 0; + loop { + if index == calls.len() { + break; + } + assert_event_execute(contract, id, *calls.at(index)); + index += 1; + } +} + +fn assert_only_events_execute_batch(contract: ContractAddress, id: felt252, calls: Span) { + let mut index = 0; + loop { + if index == calls.len() - 1 { + break; + } + assert_event_execute(contract, id, *calls.at(index)); + index += 1; + }; + assert_only_event_execute(contract, id, *calls.at(index)); +} + +// Cancelled + fn assert_event_cancel(contract: ContractAddress, id: felt252) { let event = utils::pop_log::(contract).unwrap(); let expected = TimelockControllerComponent::Event::Cancelled(Cancelled { id }); @@ -830,6 +1245,8 @@ fn assert_only_event_cancel(contract: ContractAddress, id: felt252) { utils::assert_no_events_left(contract); } +// MinDelayChange + fn assert_event_delay(contract: ContractAddress, old_duration: u64, new_duration: u64) { let event = utils::pop_log::(contract).unwrap(); let expected = TimelockControllerComponent::Event::MinDelayChange( From 7f5a848563c47ceebd5ce3578b66accac5519e1f Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 03:06:41 -0400 Subject: [PATCH 034/103] fix fmt --- src/governance/timelock/interface.cairo | 18 +++++------------ .../timelock/timelock_controller.cairo | 5 +---- src/tests/governance/test_timelock.cairo | 20 +++++++++---------- 3 files changed, 16 insertions(+), 27 deletions(-) diff --git a/src/governance/timelock/interface.cairo b/src/governance/timelock/interface.cairo index 16e1d1e02..d8832d364 100644 --- a/src/governance/timelock/interface.cairo +++ b/src/governance/timelock/interface.cairo @@ -6,8 +6,8 @@ /// use openzeppelin::governance::timelock::utils::OperationState; -use starknet::ContractAddress; use openzeppelin::governance::timelock::utils::call_impls::Call; +use starknet::ContractAddress; #[starknet::interface] trait ITimelock { @@ -18,15 +18,11 @@ trait ITimelock { fn get_timestamp(self: @TState, id: felt252) -> u64; fn get_operation_state(self: @TState, id: felt252) -> OperationState; fn get_min_delay(self: @TState) -> u64; - fn hash_operation( - self: @TState, call: Call, predecessor: felt252, salt: felt252 - ) -> felt252; + fn hash_operation(self: @TState, call: Call, predecessor: felt252, salt: felt252) -> felt252; fn hash_operation_batch( self: @TState, calls: Span, predecessor: felt252, salt: felt252 ) -> felt252; - fn schedule( - ref self: TState, call: Call, predecessor: felt252, salt: felt252, delay: u64 - ); + fn schedule(ref self: TState, call: Call, predecessor: felt252, salt: felt252, delay: u64); fn schedule_batch( ref self: TState, calls: Span, predecessor: felt252, salt: felt252, delay: u64 ); @@ -45,15 +41,11 @@ trait ITimelockABI { fn get_timestamp(self: @TState, id: felt252) -> u64; fn get_operation_state(self: @TState, id: felt252) -> OperationState; fn get_min_delay(self: @TState) -> u64; - fn hash_operation( - self: @TState, call: Call, predecessor: felt252, salt: felt252 - ) -> felt252; + fn hash_operation(self: @TState, call: Call, predecessor: felt252, salt: felt252) -> felt252; fn hash_operation_batch( self: @TState, calls: Span, predecessor: felt252, salt: felt252 ) -> felt252; - fn schedule( - ref self: TState, call: Call, predecessor: felt252, salt: felt252, delay: u64 - ); + fn schedule(ref self: TState, call: Call, predecessor: felt252, salt: felt252, delay: u64); fn schedule_batch( ref self: TState, calls: Span, predecessor: felt252, salt: felt252, delay: u64 ); diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index d88862ace..48968314b 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -148,10 +148,7 @@ mod TimelockControllerComponent { } fn hash_operation( - self: @ComponentState, - call: Call, - predecessor: felt252, - salt: felt252 + self: @ComponentState, call: Call, predecessor: felt252, salt: felt252 ) -> felt252 { PoseidonTrait::new() .update_with(@call) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 8d8ee1183..f673c559f 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -23,10 +23,12 @@ use openzeppelin::governance::timelock::utils::call_impls::{CallPartialEq, HashC use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; use openzeppelin::tests::mocks::erc721_mocks::DualCaseERC721Mock; -use openzeppelin::tests::mocks::timelock_mocks::{MockContract, IMockContractDispatcher, IMockContractDispatcherTrait}; use openzeppelin::tests::mocks::timelock_mocks::{ ITimelockAttackerDispatcher, ITimelockAttackerDispatcherTrait }; +use openzeppelin::tests::mocks::timelock_mocks::{ + MockContract, IMockContractDispatcher, IMockContractDispatcherTrait +}; use openzeppelin::tests::mocks::timelock_mocks::{TimelockControllerMock, TimelockAttackerMock}; use openzeppelin::tests::utils::constants::{ ADMIN, ZERO, NAME, SYMBOL, BASE_URI, RECIPIENT, SPENDER, OTHER, SALT, TOKEN_ID @@ -308,22 +310,18 @@ fn test_hash_operation_batch() { let mut expected_hash = PoseidonTrait::new() .update_with(13) // total elements of Call span .update_with(3) // total number of Calls - .update_with(target.contract_address) // call::to .update_with(selector!("set_number")) // call::selector .update_with(1) // call::calldata.len .update_with(VALUE) // call::calldata::number - .update_with(target.contract_address) // call::to .update_with(selector!("set_number")) // call::selector .update_with(1) // call::calldata.len .update_with(VALUE) // call::calldata::number - .update_with(target.contract_address) // call::to .update_with(selector!("set_number")) // call::selector .update_with(1) // call::calldata.len .update_with(VALUE) // call::calldata::number - .update_with(predecessor) // predecessor .update_with(salt) // salt .finalize(); @@ -1124,9 +1122,7 @@ fn assert_only_event_delay_change(contract: ContractAddress, old_duration: u64, // CallScheduled -fn assert_event_schedule( - contract: ContractAddress, call: Call, predecessor: felt252, delay: u64 -) { +fn assert_event_schedule(contract: ContractAddress, call: Call, predecessor: felt252, delay: u64) { let event = utils::pop_log::(contract).unwrap(); let expected = TimelockControllerComponent::Event::CallScheduled( CallScheduled { call, predecessor, delay } @@ -1147,7 +1143,9 @@ fn assert_only_event_schedule( utils::assert_no_events_left(contract); } -fn assert_events_schedule_batch(contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64) { +fn assert_events_schedule_batch( + contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64 +) { let mut index = 0; loop { if index == calls.len() { @@ -1158,7 +1156,9 @@ fn assert_events_schedule_batch(contract: ContractAddress, calls: Span, pr } } -fn assert_only_events_schedule_batch(contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64) { +fn assert_only_events_schedule_batch( + contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64 +) { let mut index = 0; loop { if index == calls.len() - 1 { From 2b42f6d0e731f425d2c0d102c1e605b1c440b6f1 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 03:07:21 -0400 Subject: [PATCH 035/103] remove use clause --- src/governance/timelock/timelock_controller.cairo | 1 - 1 file changed, 1 deletion(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 48968314b..4eaf9f92b 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -24,7 +24,6 @@ mod TimelockControllerComponent { use poseidon::PoseidonTrait; use starknet::ContractAddress; use starknet::SyscallResultTrait; - //use starknet::account::Call; use zeroable::Zeroable; // Constants From ae31e78cc124c52d99f3b8572299888314ec5d8d Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 11:03:33 -0400 Subject: [PATCH 036/103] add timelock mixin --- .../timelock/timelock_controller.cairo | 262 ++++++++++++++++-- src/tests/mocks/timelock_mocks.cairo | 19 +- 2 files changed, 239 insertions(+), 42 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 4eaf9f92b..a63eeb9b5 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -7,18 +7,20 @@ #[starknet::component] mod TimelockControllerComponent { use hash::{HashStateTrait, HashStateExTrait}; - use openzeppelin::access::accesscontrol::AccessControlComponent::AccessControlImpl; + use openzeppelin::access::accesscontrol::AccessControlComponent::{AccessControlImpl, AccessControlCamelImpl}; use openzeppelin::access::accesscontrol::AccessControlComponent::InternalTrait as AccessControlInternalTrait; use openzeppelin::access::accesscontrol::AccessControlComponent; use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; - use openzeppelin::governance::timelock::interface::ITimelock; + use openzeppelin::governance::timelock::interface::{ITimelock, TimelockABI}; use openzeppelin::governance::timelock::utils::OperationState; use openzeppelin::governance::timelock::utils::call_impls::{CallPartialEq, HashCallImpl, Call}; use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; use openzeppelin::introspection::src5::SRC5Component::SRC5; use openzeppelin::introspection::src5::SRC5Component; - use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent::InternalImpl as ERC1155InternalImpl; + use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent::{ERC1155ReceiverImpl, ERC1155ReceiverCamelImpl}; + use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent::{InternalImpl as ERC1155InternalImpl}; use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent; + use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent::{ERC721ReceiverImpl, ERC721ReceiverCamelImpl}; use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent::InternalImpl as ERC721InternalImpl; use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent; use poseidon::PoseidonTrait; @@ -107,20 +109,20 @@ mod TimelockControllerComponent { +Drop > of ITimelock> { fn is_operation(self: @ComponentState, id: felt252) -> bool { - self.get_operation_state(id) != OperationState::Unset + Timelock::get_operation_state(self, id) != OperationState::Unset } fn is_operation_pending(self: @ComponentState, id: felt252) -> bool { - let state = self.get_operation_state(id); + let state = Timelock::get_operation_state(self, id); state == OperationState::Waiting || state == OperationState::Ready } fn is_operation_ready(self: @ComponentState, id: felt252) -> bool { - self.get_operation_state(id) == OperationState::Ready + Timelock::get_operation_state(self, id) == OperationState::Ready } fn is_operation_done(self: @ComponentState, id: felt252) -> bool { - self.get_operation_state(id) == OperationState::Done + Timelock::get_operation_state(self, id) == OperationState::Done } fn get_timestamp(self: @ComponentState, id: felt252) -> u64 { @@ -130,7 +132,7 @@ mod TimelockControllerComponent { fn get_operation_state( self: @ComponentState, id: felt252 ) -> OperationState { - let timestamp = self.get_timestamp(id); + let timestamp = Timelock::get_timestamp(self, id); if (timestamp == 0) { return OperationState::Unset; } else if (timestamp == DONE_TIMESTAMP) { @@ -176,9 +178,9 @@ mod TimelockControllerComponent { salt: felt252, delay: u64 ) { - self.assert_only_role(PROPOSER_ROLE); + self.assert_only_role_or_open_role(PROPOSER_ROLE); - let id = self.hash_operation(call, predecessor, salt); + let id = Timelock::hash_operation(@self, call, predecessor, salt); self._schedule(id, delay); self.emit(CallScheduled { call, predecessor, delay }); @@ -194,9 +196,9 @@ mod TimelockControllerComponent { salt: felt252, delay: u64 ) { - self.assert_only_role(PROPOSER_ROLE); + self.assert_only_role_or_open_role(PROPOSER_ROLE); - let id = self.hash_operation_batch(calls, predecessor, salt); + let id = Timelock::hash_operation_batch(@self, calls, predecessor, salt); self._schedule(id, delay); let mut index = 0; @@ -216,8 +218,8 @@ mod TimelockControllerComponent { } fn cancel(ref self: ComponentState, id: felt252) { - self.assert_only_role(CANCELLER_ROLE); - assert(self.is_operation_pending(id), Errors::UNEXPECTED_OPERATION_STATE); + self.assert_only_role_or_open_role(CANCELLER_ROLE); + assert(Timelock::is_operation_pending(@self, id), Errors::UNEXPECTED_OPERATION_STATE); self.TimelockController_timestamps.write(id, 0); self.emit(Cancelled { id }); @@ -229,9 +231,9 @@ mod TimelockControllerComponent { predecessor: felt252, salt: felt252 ) { - self.assert_only_role(EXECUTOR_ROLE); + self.assert_only_role_or_open_role(EXECUTOR_ROLE); - let id = self.hash_operation(call, predecessor, salt); + let id = Timelock::hash_operation(@self, call, predecessor, salt); self._before_call(id, predecessor); self._execute(call); self.emit(CallExecuted { id, call }); @@ -244,9 +246,9 @@ mod TimelockControllerComponent { predecessor: felt252, salt: felt252 ) { - self.assert_only_role(EXECUTOR_ROLE); + self.assert_only_role_or_open_role(EXECUTOR_ROLE); - let id = self.hash_operation_batch(calls, predecessor, salt); + let id = Timelock::hash_operation_batch(@self, calls, predecessor, salt); self._before_call(id, predecessor); let mut index = 0; @@ -276,6 +278,213 @@ mod TimelockControllerComponent { } } + #[embeddable_as(TimelockMixinImpl)] + impl TimelockMixin< + TContractState, + +HasComponent, + impl SRC5: SRC5Component::HasComponent, + impl AccessControl: AccessControlComponent::HasComponent, + impl ERC721Receiver: ERC721ReceiverComponent::HasComponent, + impl ERC1155Receiver: ERC1155ReceiverComponent::HasComponent, + +Drop + > of TimelockABI> { + fn is_operation(self: @ComponentState, id: felt252) -> bool { + Timelock::is_operation(self, id) + } + + fn is_operation_pending(self: @ComponentState, id: felt252) -> bool { + Timelock::is_operation_pending(self, id) + } + + fn is_operation_ready(self: @ComponentState, id: felt252) -> bool { + Timelock::is_operation_ready(self, id) + } + + fn is_operation_done(self: @ComponentState, id: felt252) -> bool { + Timelock::is_operation_done(self, id) + } + + fn get_timestamp(self: @ComponentState, id: felt252) -> u64 { + Timelock::get_timestamp(self, id) + } + + fn get_operation_state(self: @ComponentState, id: felt252) -> OperationState { + Timelock::get_operation_state(self, id) + } + + fn get_min_delay(self: @ComponentState) -> u64 { + Timelock::get_min_delay(self) + } + + fn hash_operation(self: @ComponentState, call: Call, predecessor: felt252, salt: felt252) -> felt252 { + Timelock::hash_operation(self, call, predecessor, salt) + } + + fn hash_operation_batch( + self: @ComponentState, calls: Span, predecessor: felt252, salt: felt252 + ) -> felt252 { + Timelock::hash_operation_batch(self, calls, predecessor, salt) + } + + fn schedule(ref self: ComponentState, call: Call, predecessor: felt252, salt: felt252, delay: u64) { + Timelock::schedule(ref self, call, predecessor, salt, delay); + } + + fn schedule_batch( + ref self: ComponentState, calls: Span, predecessor: felt252, salt: felt252, delay: u64 + ) { + Timelock::schedule_batch(ref self, calls, predecessor, salt, delay); + } + + fn cancel(ref self: ComponentState, id: felt252) { + Timelock::cancel(ref self, id); + } + + fn execute(ref self: ComponentState, call: Call, predecessor: felt252, salt: felt252) { + Timelock::execute(ref self, call, predecessor, salt); + } + + fn execute_batch(ref self: ComponentState, calls: Span, predecessor: felt252, salt: felt252) { + Timelock::execute_batch(ref self, calls, predecessor, salt); + } + + fn update_delay(ref self: ComponentState, new_delay: u64) { + Timelock::update_delay(ref self, new_delay); + } + + // ISRC5 + fn supports_interface( + self: @ComponentState, interface_id: felt252 + ) -> bool { + let src5 = get_dep_component!(self, SRC5); + src5.supports_interface(interface_id) + } + + // IAccessControl + fn has_role(self: @ComponentState, role: felt252, account: ContractAddress) -> bool { + let access_control = get_dep_component!(self, AccessControl); + access_control.has_role(role, account) + } + + fn get_role_admin(self: @ComponentState, role: felt252) -> felt252 { + let access_control = get_dep_component!(self, AccessControl); + access_control.get_role_admin(role) + } + + fn grant_role(ref self: ComponentState, role: felt252, account: ContractAddress) { + let mut access_control = get_dep_component_mut!(ref self, AccessControl); + access_control.grant_role(role, account); + } + + fn revoke_role(ref self: ComponentState, role: felt252, account: ContractAddress) { + let mut access_control = get_dep_component_mut!(ref self, AccessControl); + access_control.revoke_role(role, account); + } + fn renounce_role(ref self: ComponentState, role: felt252, account: ContractAddress) { + let mut access_control = get_dep_component_mut!(ref self, AccessControl); + access_control.renounce_role(role, account); + } + + // IAccessControlCamel + fn hasRole(self: @ComponentState, role: felt252, account: ContractAddress) -> bool { + let access_control = get_dep_component!(self, AccessControl); + access_control.hasRole(role, account) + } + + fn getRoleAdmin(self: @ComponentState, role: felt252) -> felt252 { + let access_control = get_dep_component!(self, AccessControl); + access_control.getRoleAdmin(role) + } + + fn grantRole(ref self: ComponentState, role: felt252, account: ContractAddress) { + let mut access_control = get_dep_component_mut!(ref self, AccessControl); + access_control.grantRole(role, account); + } + + fn revokeRole(ref self: ComponentState, role: felt252, account: ContractAddress) { + let mut access_control = get_dep_component_mut!(ref self, AccessControl); + access_control.revokeRole(role, account); + } + + fn renounceRole(ref self: ComponentState, role: felt252, account: ContractAddress) { + let mut access_control = get_dep_component_mut!(ref self, AccessControl); + access_control.renounceRole(role, account); + } + + // IERC721Receiver + fn on_erc721_received( + self: @ComponentState, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + data: Span + ) -> felt252 { + let erc721_receiver = get_dep_component!(self, ERC721Receiver); + erc721_receiver.on_erc721_received(operator, from, token_id, data) + } + + // IERC721ReceiverCamel + fn onERC721Received( + self: @ComponentState, + operator: ContractAddress, + from: ContractAddress, + tokenId: u256, + data: Span + ) -> felt252 { + let erc721_receiver = get_dep_component!(self, ERC721Receiver); + erc721_receiver.onERC721Received(operator, from, tokenId, data) + } + + // IERC1155Receiver + fn on_erc1155_received( + self: @ComponentState, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + value: u256, + data: Span + ) -> felt252 { + let erc1155_receiver = get_dep_component!(self, ERC1155Receiver); + erc1155_receiver.on_erc1155_received(operator, from, token_id, value, data) + } + + fn on_erc1155_batch_received( + self: @ComponentState, + operator: ContractAddress, + from: ContractAddress, + token_ids: Span, + values: Span, + data: Span + ) -> felt252 { + let erc1155_receiver = get_dep_component!(self, ERC1155Receiver); + erc1155_receiver.on_erc1155_batch_received(operator, from, token_ids, values, data) + } + + // IERC1155ReceiverCamel + fn onERC1155Received( + self: @ComponentState, + operator: ContractAddress, + from: ContractAddress, + tokenId: u256, + value: u256, + data: Span + ) -> felt252 { + let erc1155_receiver = get_dep_component!(self, ERC1155Receiver); + erc1155_receiver.onERC1155Received(operator, from, tokenId, value, data) + } + + fn onERC1155BatchReceived( + self: @ComponentState, + operator: ContractAddress, + from: ContractAddress, + tokenIds: Span, + values: Span, + data: Span + ) -> felt252 { + let erc1155_receiver = get_dep_component!(self, ERC1155Receiver); + erc1155_receiver.onERC1155BatchReceived(operator, from, tokenIds, values, data) + } + } #[generate_trait] impl InternalImpl< @@ -342,27 +551,30 @@ mod TimelockControllerComponent { self.emit(MinDelayChange { old_duration: 0, new_duration: min_delay }) } - fn assert_only_role(self: @ComponentState, role: felt252) { + fn assert_only_role_or_open_role(self: @ComponentState, role: felt252) { let access_component = get_dep_component!(self, AccessControl); - access_component.assert_only_role(role); + let is_role_open = access_component.has_role(role, Zeroable::zero()); + if !is_role_open { + access_component.assert_only_role(role); + } } fn _before_call(self: @ComponentState, id: felt252, predecessor: felt252) { - assert(self.is_operation_ready(id), Errors::UNEXPECTED_OPERATION_STATE); + assert(Timelock::is_operation_ready(self, id), Errors::UNEXPECTED_OPERATION_STATE); assert( - predecessor == 0 || self.is_operation_done(predecessor), + predecessor == 0 || Timelock::is_operation_done(self, predecessor), Errors::UNEXECUTED_PREDECESSOR ); } fn _after_call(ref self: ComponentState, id: felt252) { - assert(self.is_operation_ready(id), Errors::UNEXPECTED_OPERATION_STATE); + assert(Timelock::is_operation_ready(@self, id), Errors::UNEXPECTED_OPERATION_STATE); self.TimelockController_timestamps.write(id, DONE_TIMESTAMP); } fn _schedule(ref self: ComponentState, id: felt252, delay: u64) { - assert(!self.is_operation(id), Errors::UNEXPECTED_OPERATION_STATE); - assert(self.get_min_delay() <= delay, Errors::INSUFFICIENT_DELAY); + assert(!Timelock::is_operation(@self, id), Errors::UNEXPECTED_OPERATION_STATE); + assert(Timelock::get_min_delay(@self) <= delay, Errors::INSUFFICIENT_DELAY); self.TimelockController_timestamps.write(id, starknet::get_block_timestamp() + delay); } diff --git a/src/tests/mocks/timelock_mocks.cairo b/src/tests/mocks/timelock_mocks.cairo index d871b4301..327cb3f9f 100644 --- a/src/tests/mocks/timelock_mocks.cairo +++ b/src/tests/mocks/timelock_mocks.cairo @@ -15,26 +15,11 @@ mod TimelockControllerMock { path: ERC1155ReceiverComponent, storage: erc1155_receiver, event: ERC1155ReceiverEvent ); + // Timelock Mixin #[abi(embed_v0)] - impl AccessControlImpl = - AccessControlComponent::AccessControlImpl; - impl AccessControlInternalImpl = AccessControlComponent::InternalImpl; - - #[abi(embed_v0)] - impl SRC5Impl = SRC5Component::SRC5Impl; - - #[abi(embed_v0)] - impl TimelockImpl = TimelockControllerComponent::TimelockImpl; + impl TimelockMixinImpl = TimelockControllerComponent::TimelockMixinImpl; impl TimelockInternalImpl = TimelockControllerComponent::InternalImpl; - // ERC721Receiver - impl ERC721ReceiverImpl = ERC721ReceiverComponent::ERC721ReceiverImpl; - impl ERC721ReceiverInternalImpl = ERC721ReceiverComponent::InternalImpl; - - // ERC1155Receiver - impl ERC1155ReceiverImpl = ERC1155ReceiverComponent::ERC1155ReceiverImpl; - impl ERC1155ReceiverInternalImpl = ERC1155ReceiverComponent::InternalImpl; - #[storage] struct Storage { #[substorage(v0)] From ae1b474ddd62432529aff87900219b538da90519 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 11:03:50 -0400 Subject: [PATCH 037/103] fix interface name --- src/governance/timelock/interface.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/governance/timelock/interface.cairo b/src/governance/timelock/interface.cairo index d8832d364..8b03b35d2 100644 --- a/src/governance/timelock/interface.cairo +++ b/src/governance/timelock/interface.cairo @@ -33,7 +33,7 @@ trait ITimelock { } #[starknet::interface] -trait ITimelockABI { +trait TimelockABI { fn is_operation(self: @TState, id: felt252) -> bool; fn is_operation_pending(self: @TState, id: felt252) -> bool; fn is_operation_ready(self: @TState, id: felt252) -> bool; From d12be8e1215d6d7014c926e1be9f24e25ac5a296 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 11:04:22 -0400 Subject: [PATCH 038/103] improve event assertions --- src/tests/governance/test_timelock.cairo | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index f673c559f..f9d2c285d 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -17,7 +17,7 @@ use openzeppelin::governance::timelock::TimelockControllerComponent::{ }; use openzeppelin::governance::timelock::TimelockControllerComponent; use openzeppelin::governance::timelock::interface::{ - ITimelockABIDispatcher, ITimelockABIDispatcherTrait + TimelockABIDispatcher, TimelockABIDispatcherTrait }; use openzeppelin::governance::timelock::utils::call_impls::{CallPartialEq, HashCallImpl}; use openzeppelin::introspection::interface::ISRC5_ID; @@ -106,14 +106,14 @@ fn failing_operation(target: ContractAddress) -> Call { Call { to: target, selector: selector!("failing_function"), calldata: calldata.span() } } -fn setup_dispatchers() -> (ITimelockABIDispatcher, IMockContractDispatcher) { +fn setup_dispatchers() -> (TimelockABIDispatcher, IMockContractDispatcher) { let timelock = deploy_timelock(); let target = deploy_mock_target(); (timelock, target) } -fn deploy_timelock() -> ITimelockABIDispatcher { +fn deploy_timelock() -> TimelockABIDispatcher { let mut calldata = array![]; let proposers = array![PROPOSER()].span(); @@ -130,7 +130,7 @@ fn deploy_timelock() -> ITimelockABIDispatcher { // - 5 RoleGranted: self, proposer, canceller, executor, admin // - MinDelayChange utils::drop_events(address, 6); - ITimelockABIDispatcher { contract_address: address } + TimelockABIDispatcher { contract_address: address } } fn deploy_erc721(recipient: ContractAddress) -> IERC721Dispatcher { @@ -292,18 +292,18 @@ fn test_hash_operation() { fn test_hash_operation_batch() { let (mut timelock, mut target) = setup_dispatchers(); - // Setup call + // Setup calls let mut calldata = array![]; calldata.append_serde(VALUE); let mut call = Call { to: target.contract_address, selector: selector!("set_number"), calldata: calldata.span() }; + let calls = array![call, call, call].span(); // Hash operation let predecessor = 123; let salt = SALT; - let calls = array![call, call, call].span(); let hashed_operation = timelock.hash_operation_batch(calls, predecessor, salt); // Manually set hash elements @@ -439,14 +439,10 @@ fn schedule_batch_from_proposer(salt: felt252) { // Check events if salt != 0 { - assert_event_schedule(timelock.contract_address, *calls.at(0), predecessor, delay); - assert_event_schedule(timelock.contract_address, *calls.at(1), predecessor, delay); - assert_event_schedule(timelock.contract_address, *calls.at(2), predecessor, delay); + assert_events_schedule_batch(timelock.contract_address, calls, predecessor, delay); assert_only_event_call_salt(timelock.contract_address, target_id, salt); } else { - assert_event_schedule(timelock.contract_address, *calls.at(0), predecessor, delay); - assert_event_schedule(timelock.contract_address, *calls.at(1), predecessor, delay); - assert_only_event_schedule(timelock.contract_address, *calls.at(2), predecessor, delay); + assert_only_events_schedule_batch(timelock.contract_address, calls, predecessor, delay); } } @@ -1063,7 +1059,7 @@ fn test_update_delay_scheduled() { // fn assert_operation_state( - timelock: ITimelockABIDispatcher, exp_state: OperationState, id: felt252 + timelock: TimelockABIDispatcher, exp_state: OperationState, id: felt252 ) { let operation_state = timelock.get_operation_state(id); assert_eq!(operation_state, exp_state); From 98fb423d4bd67745d1a014443d87861a4421ed1f Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 14:06:16 -0400 Subject: [PATCH 039/103] fix execute and schedule events --- .../timelock/timelock_controller.cairo | 12 +- src/tests/governance/test_timelock.cairo | 355 ++++++++++-------- 2 files changed, 213 insertions(+), 154 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index a63eeb9b5..ce1605571 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -54,6 +54,9 @@ mod TimelockControllerComponent { #[derive(Drop, PartialEq, starknet::Event)] struct CallScheduled { #[key] + id: felt252, + #[key] + index: felt252, call: Call, predecessor: felt252, delay: u64 @@ -65,6 +68,7 @@ mod TimelockControllerComponent { #[key] id: felt252, #[key] + index: felt252, call: Call } @@ -182,7 +186,7 @@ mod TimelockControllerComponent { let id = Timelock::hash_operation(@self, call, predecessor, salt); self._schedule(id, delay); - self.emit(CallScheduled { call, predecessor, delay }); + self.emit(CallScheduled { id, index: 0, call, predecessor, delay }); if salt != 0 { self.emit(CallSalt { id, salt }); @@ -208,7 +212,7 @@ mod TimelockControllerComponent { } let call = *calls.at(index); - self.emit(CallScheduled { call, predecessor, delay }); + self.emit(CallScheduled { id, index: index.into(), call, predecessor, delay }); index += 1; }; @@ -236,7 +240,7 @@ mod TimelockControllerComponent { let id = Timelock::hash_operation(@self, call, predecessor, salt); self._before_call(id, predecessor); self._execute(call); - self.emit(CallExecuted { id, call }); + self.emit(CallExecuted { id, index: 0, call }); self._after_call(id); } @@ -259,7 +263,7 @@ mod TimelockControllerComponent { let call = *calls.at(index); self._execute(call); - self.emit(CallExecuted { id, call }); + self.emit(CallExecuted { id, index: index.into(), call }); index += 1; }; diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index f9d2c285d..74b17889a 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -2,7 +2,6 @@ use hash::{HashStateTrait, HashStateExTrait}; use openzeppelin::access::accesscontrol::AccessControlComponent::{ AccessControlImpl, InternalImpl as AccessControlInternalImpl }; -use openzeppelin::access::accesscontrol::accesscontrol::AccessControlComponent::InternalTrait; use openzeppelin::access::accesscontrol::interface::IAccessControl; use openzeppelin::governance::timelock::TimelockControllerComponent::Call; use openzeppelin::governance::timelock::TimelockControllerComponent::OperationState; @@ -163,101 +162,6 @@ fn deploy_attacker() -> ITimelockAttackerDispatcher { ITimelockAttackerDispatcher { contract_address: address } } -// initializer - -#[test] -fn test_initializer_single_role_and_no_admin() { - let mut state = COMPONENT_STATE(); - let contract_state = CONTRACT_STATE(); - let min_delay = MIN_DELAY; - - let proposers = array![PROPOSER()].span(); - let executors = array![EXECUTOR()].span(); - let admin_zero = ZERO(); - - state.initializer(min_delay, proposers, executors, admin_zero); - assert!(contract_state.has_role(DEFAULT_ADMIN_ROLE, admin_zero)); -} - -#[test] -fn test_initializer_multiple_roles_and_admin() { - let mut state = COMPONENT_STATE(); - let contract_state = CONTRACT_STATE(); - let min_delay = MIN_DELAY; - - let (p1, p2, p3) = get_proposers(); - let mut proposers = array![p1, p2, p3].span(); - - let (e1, e2, e3) = get_executors(); - let mut executors = array![e1, e2, e3].span(); - - let admin = ADMIN(); - - state.initializer(min_delay, proposers, executors, admin); - - // Check assigned roles - assert!(contract_state.has_role(DEFAULT_ADMIN_ROLE, admin)); - - let mut index = 0; - loop { - if index == proposers.len() { - break; - } - - assert!(contract_state.has_role(PROPOSER_ROLE, *proposers.at(index))); - assert!(contract_state.has_role(CANCELLER_ROLE, *proposers.at(index))); - assert!(contract_state.has_role(EXECUTOR_ROLE, *executors.at(index))); - index += 1; - }; -} - -#[test] -fn test_initializer_supported_interfaces() { - let mut state = COMPONENT_STATE(); - let contract_state = CONTRACT_STATE(); - let min_delay = MIN_DELAY; - - let proposers = array![PROPOSER()].span(); - let executors = array![EXECUTOR()].span(); - let admin = ADMIN(); - - state.initializer(min_delay, proposers, executors, admin); - - // Check interface support - let supports_isrc5 = contract_state.src5.supports_interface(ISRC5_ID); - assert!(supports_isrc5); - - let supports_ierc1155_receiver = contract_state.src5.supports_interface(IERC1155_RECEIVER_ID); - assert!(supports_ierc1155_receiver); - - let supports_ierc721_receiver = contract_state.src5.supports_interface(IERC721_RECEIVER_ID); - assert!(supports_ierc721_receiver); -} - -#[test] -fn test_initializer_min_delay() { - let mut state = COMPONENT_STATE(); - let min_delay = MIN_DELAY; - - let proposers = array![PROPOSER()].span(); - let executors = array![EXECUTOR()].span(); - let admin_zero = ZERO(); - - state.initializer(min_delay, proposers, executors, admin_zero); - - // Check minimum delay is set - let delay = state.get_min_delay(); - assert_eq!(delay, MIN_DELAY); - - // The initializer emits 4 `RoleGranted` events prior to `MinDelayChange`: - // - Self administration - // - 1 proposers - // - 1 cancellers - // - 1 executors - utils::drop_events(ZERO(), 4); - assert_only_event_delay_change(ZERO(), 0, MIN_DELAY); -} - // hash_operation #[test] @@ -352,11 +256,12 @@ fn schedule_from_proposer(salt: felt252) { assert_eq!(operation_ts, expected_ts); // Check event(s) + let event_index = 0; if salt != 0 { - assert_event_schedule(timelock.contract_address, call, predecessor, delay); + assert_event_schedule(timelock.contract_address, target_id, event_index, call, predecessor, delay); assert_only_event_call_salt(timelock.contract_address, target_id, salt); } else { - assert_only_event_schedule(timelock.contract_address, call, predecessor, delay); + assert_only_event_schedule(timelock.contract_address, target_id, event_index, call, predecessor, delay); } } @@ -439,10 +344,10 @@ fn schedule_batch_from_proposer(salt: felt252) { // Check events if salt != 0 { - assert_events_schedule_batch(timelock.contract_address, calls, predecessor, delay); + assert_events_schedule_batch(timelock.contract_address, target_id, calls, predecessor, delay); assert_only_event_call_salt(timelock.contract_address, target_id, salt); } else { - assert_only_events_schedule_batch(timelock.contract_address, calls, predecessor, delay); + assert_only_events_schedule_batch(timelock.contract_address, target_id, calls, predecessor, delay); } } @@ -522,6 +427,7 @@ fn test_execute_when_scheduled() { let predecessor = NO_PREDECESSOR; let salt = 0; let delay = MIN_DELAY; + let event_index = 0; // Set up call let call = single_operation(target.contract_address); @@ -531,7 +437,7 @@ fn test_execute_when_scheduled() { // Schedule testing::set_contract_address(PROPOSER()); timelock.schedule(call, predecessor, salt, delay); - assert_only_event_schedule(timelock.contract_address, call, predecessor, delay); + assert_only_event_schedule(timelock.contract_address, target_id, event_index, call, predecessor, delay); assert_operation_state(timelock, OperationState::Waiting, target_id); // Fast-forward @@ -547,7 +453,7 @@ fn test_execute_when_scheduled() { timelock.execute(call, predecessor, salt); assert_operation_state(timelock, OperationState::Done, target_id); - assert_only_event_execute(timelock.contract_address, target_id, call); + assert_only_event_execute(timelock.contract_address, target_id, event_index, call); // Check target state updates let check_target = target.get_number(); @@ -674,6 +580,7 @@ fn test_execute_after_dependency() { let (mut timelock, mut target) = setup_dispatchers(); let salt = 0; let delay = MIN_DELAY; + let event_index = 0; // Call 1 let call_1 = single_operation(target.contract_address); @@ -691,12 +598,12 @@ fn test_execute_after_dependency() { testing::set_contract_address(PROPOSER()); timelock.schedule(call_1, predecessor_1, salt, delay); assert_operation_state(timelock, OperationState::Waiting, target_id_1); - assert_only_event_schedule(timelock.contract_address, call_1, predecessor_1, delay); + assert_only_event_schedule(timelock.contract_address, target_id_1, event_index, call_1, predecessor_1, delay); // Schedule call 2 timelock.schedule(call_2, predecessor_2, salt, delay); assert_operation_state(timelock, OperationState::Waiting, target_id_2); - assert_only_event_schedule(timelock.contract_address, call_2, predecessor_2, delay); + assert_only_event_schedule(timelock.contract_address, target_id_2, event_index, call_2, predecessor_2, delay); // Fast-forward testing::set_block_timestamp(delay); @@ -707,12 +614,12 @@ fn test_execute_after_dependency() { testing::set_contract_address(EXECUTOR()); timelock.execute(call_1, predecessor_1, salt); assert_operation_state(timelock, OperationState::Done, target_id_1); - assert_event_execute(timelock.contract_address, target_id_1, call_1); + assert_event_execute(timelock.contract_address, target_id_1,event_index, call_1); // Execute call 2 timelock.execute(call_2, predecessor_2, salt); assert_operation_state(timelock, OperationState::Done, target_id_2); - assert_only_event_execute(timelock.contract_address, target_id_2, call_2); + assert_only_event_execute(timelock.contract_address, target_id_2, event_index, call_2); } // execute_batch @@ -746,7 +653,7 @@ fn test_execute_batch_when_scheduled() { testing::set_contract_address(PROPOSER()); timelock.schedule_batch(calls, predecessor, salt, delay); assert_operation_state(timelock, OperationState::Waiting, target_id); - assert_only_events_schedule_batch(timelock.contract_address, calls, predecessor, delay); + assert_only_events_schedule_batch(timelock.contract_address, target_id, calls, predecessor, delay); // Fast-forward testing::set_block_timestamp(delay); @@ -928,12 +835,12 @@ fn test_execute_batch_after_dependency() { testing::set_contract_address(PROPOSER()); timelock.schedule_batch(calls_1, predecessor_1, salt, delay); assert_operation_state(timelock, OperationState::Waiting, target_id_1); - assert_only_events_schedule_batch(timelock.contract_address, calls_1, predecessor_1, delay); + assert_only_events_schedule_batch(timelock.contract_address, target_id_1, calls_1, predecessor_1, delay); // Schedule calls 2 timelock.schedule_batch(calls_2, predecessor_2, salt, delay); assert_operation_state(timelock, OperationState::Waiting, target_id_2); - assert_only_events_schedule_batch(timelock.contract_address, calls_2, predecessor_2, delay); + assert_only_events_schedule_batch(timelock.contract_address, target_id_2, calls_2, predecessor_2, delay); // Fast-forward testing::set_block_timestamp(delay); @@ -960,6 +867,7 @@ fn test_cancel_from_canceller() { let predecessor = NO_PREDECESSOR; let salt = 0; let delay = MIN_DELAY; + let event_index = 0; let call = single_operation(target.contract_address); let target_id = timelock.hash_operation(call, predecessor, salt); @@ -969,7 +877,7 @@ fn test_cancel_from_canceller() { testing::set_contract_address(PROPOSER()); // PROPOSER is also CANCELLER timelock.schedule(call, predecessor, salt, delay); assert_operation_state(timelock, OperationState::Waiting, target_id); - assert_only_event_schedule(timelock.contract_address, call, predecessor, delay); + assert_only_event_schedule(timelock.contract_address, target_id, event_index, call, predecessor, delay); // Cancel timelock.cancel(target_id); @@ -1025,6 +933,7 @@ fn test_update_delay_scheduled() { let predecessor = NO_PREDECESSOR; let salt = 0; let delay = MIN_DELAY; + let event_index = 0; let call = Call { to: timelock.contract_address, @@ -1037,7 +946,7 @@ fn test_update_delay_scheduled() { testing::set_contract_address(PROPOSER()); timelock.schedule(call, predecessor, salt, delay); assert_operation_state(timelock, OperationState::Waiting, target_id); - assert_only_event_schedule(timelock.contract_address, call, predecessor, delay); + assert_only_event_schedule(timelock.contract_address, target_id, event_index, call, predecessor, delay); // Fast-forward testing::set_block_timestamp(delay); @@ -1047,13 +956,172 @@ fn test_update_delay_scheduled() { timelock.execute(call, predecessor, salt); assert_operation_state(timelock, OperationState::Done, target_id); assert_event_delay(timelock.contract_address, MIN_DELAY, NEW_DELAY); - assert_only_event_execute(timelock.contract_address, target_id, call); + assert_only_event_execute(timelock.contract_address, target_id, event_index, call); // Check new minimum delay let get_new_delay = timelock.get_min_delay(); assert_eq!(get_new_delay, NEW_DELAY); } +// +// Internal +// + +// initializer + +#[test] +fn test_initializer_single_role_and_no_admin() { + let mut state = COMPONENT_STATE(); + let contract_state = CONTRACT_STATE(); + let min_delay = MIN_DELAY; + + let proposers = array![PROPOSER()].span(); + let executors = array![EXECUTOR()].span(); + let admin_zero = ZERO(); + + state.initializer(min_delay, proposers, executors, admin_zero); + assert!(contract_state.has_role(DEFAULT_ADMIN_ROLE, admin_zero)); +} + +#[test] +fn test_initializer_multiple_roles_and_admin() { + let mut state = COMPONENT_STATE(); + let contract_state = CONTRACT_STATE(); + let min_delay = MIN_DELAY; + + let (p1, p2, p3) = get_proposers(); + let mut proposers = array![p1, p2, p3].span(); + + let (e1, e2, e3) = get_executors(); + let mut executors = array![e1, e2, e3].span(); + + let admin = ADMIN(); + + state.initializer(min_delay, proposers, executors, admin); + + // Check assigned roles + assert!(contract_state.has_role(DEFAULT_ADMIN_ROLE, admin)); + + let mut index = 0; + loop { + if index == proposers.len() { + break; + } + + assert!(contract_state.has_role(PROPOSER_ROLE, *proposers.at(index))); + assert!(contract_state.has_role(CANCELLER_ROLE, *proposers.at(index))); + assert!(contract_state.has_role(EXECUTOR_ROLE, *executors.at(index))); + index += 1; + }; +} + +#[test] +fn test_initializer_supported_interfaces() { + let mut state = COMPONENT_STATE(); + let contract_state = CONTRACT_STATE(); + let min_delay = MIN_DELAY; + + let proposers = array![PROPOSER()].span(); + let executors = array![EXECUTOR()].span(); + let admin = ADMIN(); + + state.initializer(min_delay, proposers, executors, admin); + + // Check interface support + let supports_isrc5 = contract_state.src5.supports_interface(ISRC5_ID); + assert!(supports_isrc5); + + let supports_ierc1155_receiver = contract_state.src5.supports_interface(IERC1155_RECEIVER_ID); + assert!(supports_ierc1155_receiver); + + let supports_ierc721_receiver = contract_state.src5.supports_interface(IERC721_RECEIVER_ID); + assert!(supports_ierc721_receiver); +} + +#[test] +fn test_initializer_min_delay() { + let mut state = COMPONENT_STATE(); + let min_delay = MIN_DELAY; + + let proposers = array![PROPOSER()].span(); + let executors = array![EXECUTOR()].span(); + let admin_zero = ZERO(); + + state.initializer(min_delay, proposers, executors, admin_zero); + + // Check minimum delay is set + let delay = state.get_min_delay(); + assert_eq!(delay, MIN_DELAY); + + // The initializer emits 4 `RoleGranted` events prior to `MinDelayChange`: + // - Self administration + // - 1 proposers + // - 1 cancellers + // - 1 executors + utils::drop_events(ZERO(), 4); + assert_only_event_delay_change(ZERO(), 0, MIN_DELAY); +} + +// assert_only_role_or_open_role + +#[test] +fn test_assert_only_role_or_open_role_when_has_role() { + let mut state = COMPONENT_STATE(); + let min_delay = MIN_DELAY; + + let proposers = array![PROPOSER()].span(); + let executors = array![EXECUTOR()].span(); + let admin = ADMIN(); + + state.initializer(min_delay, proposers, executors, admin); + + testing::set_caller_address(PROPOSER()); + state.assert_only_role_or_open_role(PROPOSER_ROLE); + + // PROPOSER == CANCELLER + testing::set_caller_address(PROPOSER()); + state.assert_only_role_or_open_role(CANCELLER_ROLE); + + testing::set_caller_address(EXECUTOR()); + state.assert_only_role_or_open_role(EXECUTOR_ROLE); +} + +#[test] +#[should_panic(expected: ('Caller is missing role',))] +fn test_assert_only_role_or_open_role_unauthorized() { + let mut state = COMPONENT_STATE(); + let min_delay = MIN_DELAY; + + let proposers = array![PROPOSER()].span(); + let executors = array![EXECUTOR()].span(); + let admin = ADMIN(); + + state.initializer(min_delay, proposers, executors, admin); + + testing::set_caller_address(OTHER()); + state.assert_only_role_or_open_role(PROPOSER_ROLE); +} + +#[test] +fn test_assert_only_role_or_open_role_with_open_role() { + let mut state = COMPONENT_STATE(); + let contract_state = CONTRACT_STATE(); + let min_delay = MIN_DELAY; + let open_role = ZERO(); + + let proposers = array![open_role].span(); + let executors = array![EXECUTOR()].span(); + let admin = ADMIN(); + + state.initializer(min_delay, proposers, executors, admin); + + let is_open_role = contract_state.has_role(PROPOSER_ROLE, open_role); + assert!(is_open_role); + + testing::set_caller_address(OTHER()); + state.assert_only_role_or_open_role(PROPOSER_ROLE); +} + // // Helpers // @@ -1118,52 +1186,46 @@ fn assert_only_event_delay_change(contract: ContractAddress, old_duration: u64, // CallScheduled -fn assert_event_schedule(contract: ContractAddress, call: Call, predecessor: felt252, delay: u64) { +fn assert_event_schedule(contract: ContractAddress, id: felt252, index: felt252, call: Call, predecessor: felt252, delay: u64) { let event = utils::pop_log::(contract).unwrap(); let expected = TimelockControllerComponent::Event::CallScheduled( - CallScheduled { call, predecessor, delay } + CallScheduled { id, index, call, predecessor, delay } ); assert!(event == expected); // Check indexed keys let mut indexed_keys = array![]; indexed_keys.append_serde(selector!("CallScheduled")); - indexed_keys.append_serde(call); + indexed_keys.append_serde(id); + indexed_keys.append_serde(index); utils::assert_indexed_keys(event, indexed_keys.span()); } fn assert_only_event_schedule( - contract: ContractAddress, call: Call, predecessor: felt252, delay: u64 + contract: ContractAddress, id: felt252, index: felt252, call: Call, predecessor: felt252, delay: u64 ) { - assert_event_schedule(contract, call, predecessor, delay); + assert_event_schedule(contract, id, index, call, predecessor, delay); utils::assert_no_events_left(contract); } fn assert_events_schedule_batch( - contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64 + contract: ContractAddress, id: felt252, calls: Span, predecessor: felt252, delay: u64 ) { - let mut index = 0; + let mut i = 0; loop { - if index == calls.len() { + if i == calls.len() { break; } - assert_event_schedule(contract, *calls.at(index), predecessor, delay); - index += 1; + assert_event_schedule(contract, id, i.into(), *calls.at(i), predecessor, delay); + i += 1; } } fn assert_only_events_schedule_batch( - contract: ContractAddress, calls: Span, predecessor: felt252, delay: u64 + contract: ContractAddress, id: felt252, calls: Span, predecessor: felt252, delay: u64 ) { - let mut index = 0; - loop { - if index == calls.len() - 1 { - break; - } - assert_event_schedule(contract, *calls.at(index), predecessor, delay); - index += 1; - }; - assert_only_event_schedule(contract, *calls.at(index), predecessor, delay); + assert_events_schedule_batch(contract, id, calls, predecessor, delay); + utils::assert_no_events_left(contract); } // CallSalt @@ -1181,45 +1243,38 @@ fn assert_only_event_call_salt(contract: ContractAddress, id: felt252, salt: fel // CallExecuted -fn assert_event_execute(contract: ContractAddress, id: felt252, call: Call) { +fn assert_event_execute(contract: ContractAddress, id: felt252, index: felt252, call: Call) { let event = utils::pop_log::(contract).unwrap(); - let expected = TimelockControllerComponent::Event::CallExecuted(CallExecuted { id, call }); + let expected = TimelockControllerComponent::Event::CallExecuted(CallExecuted { id, index, call }); assert!(event == expected); // Check indexed keys let mut indexed_keys = array![]; indexed_keys.append_serde(selector!("CallExecuted")); indexed_keys.append_serde(id); - indexed_keys.append_serde(call); + indexed_keys.append_serde(index); utils::assert_indexed_keys(event, indexed_keys.span()); } -fn assert_only_event_execute(contract: ContractAddress, id: felt252, call: Call) { - assert_event_execute(contract, id, call); +fn assert_only_event_execute(contract: ContractAddress, id: felt252, index: felt252, call: Call) { + assert_event_execute(contract, id, index, call); utils::assert_no_events_left(contract); } fn assert_events_execute_batch(contract: ContractAddress, id: felt252, calls: Span) { - let mut index = 0; + let mut i = 0; loop { - if index == calls.len() { + if i == calls.len() { break; } - assert_event_execute(contract, id, *calls.at(index)); - index += 1; + assert_event_execute(contract, id, i.into(), *calls.at(i)); + i += 1; } } fn assert_only_events_execute_batch(contract: ContractAddress, id: felt252, calls: Span) { - let mut index = 0; - loop { - if index == calls.len() - 1 { - break; - } - assert_event_execute(contract, id, *calls.at(index)); - index += 1; - }; - assert_only_event_execute(contract, id, *calls.at(index)); + assert_events_execute_batch(contract, id, calls); + utils::assert_no_events_left(contract); } // Cancelled From c8efa81350276e1a2452cd5db8bc6693bafc5194 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 14:06:41 -0400 Subject: [PATCH 040/103] fix fmt --- .../timelock/timelock_controller.cairo | 89 +++++++++++++++---- src/tests/governance/test_timelock.cairo | 74 +++++++++++---- src/tests/mocks/timelock_mocks.cairo | 3 +- 3 files changed, 127 insertions(+), 39 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index ce1605571..31608d9c5 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -7,8 +7,10 @@ #[starknet::component] mod TimelockControllerComponent { use hash::{HashStateTrait, HashStateExTrait}; - use openzeppelin::access::accesscontrol::AccessControlComponent::{AccessControlImpl, AccessControlCamelImpl}; use openzeppelin::access::accesscontrol::AccessControlComponent::InternalTrait as AccessControlInternalTrait; + use openzeppelin::access::accesscontrol::AccessControlComponent::{ + AccessControlImpl, AccessControlCamelImpl + }; use openzeppelin::access::accesscontrol::AccessControlComponent; use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin::governance::timelock::interface::{ITimelock, TimelockABI}; @@ -17,11 +19,17 @@ mod TimelockControllerComponent { use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; use openzeppelin::introspection::src5::SRC5Component::SRC5; use openzeppelin::introspection::src5::SRC5Component; - use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent::{ERC1155ReceiverImpl, ERC1155ReceiverCamelImpl}; - use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent::{InternalImpl as ERC1155InternalImpl}; + use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent::{ + ERC1155ReceiverImpl, ERC1155ReceiverCamelImpl + }; + use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent::{ + InternalImpl as ERC1155InternalImpl + }; use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent; - use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent::{ERC721ReceiverImpl, ERC721ReceiverCamelImpl}; use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent::InternalImpl as ERC721InternalImpl; + use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent::{ + ERC721ReceiverImpl, ERC721ReceiverCamelImpl + }; use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent; use poseidon::PoseidonTrait; use starknet::ContractAddress; @@ -312,7 +320,9 @@ mod TimelockControllerComponent { Timelock::get_timestamp(self, id) } - fn get_operation_state(self: @ComponentState, id: felt252) -> OperationState { + fn get_operation_state( + self: @ComponentState, id: felt252 + ) -> OperationState { Timelock::get_operation_state(self, id) } @@ -320,22 +330,37 @@ mod TimelockControllerComponent { Timelock::get_min_delay(self) } - fn hash_operation(self: @ComponentState, call: Call, predecessor: felt252, salt: felt252) -> felt252 { + fn hash_operation( + self: @ComponentState, call: Call, predecessor: felt252, salt: felt252 + ) -> felt252 { Timelock::hash_operation(self, call, predecessor, salt) } fn hash_operation_batch( - self: @ComponentState, calls: Span, predecessor: felt252, salt: felt252 + self: @ComponentState, + calls: Span, + predecessor: felt252, + salt: felt252 ) -> felt252 { Timelock::hash_operation_batch(self, calls, predecessor, salt) } - fn schedule(ref self: ComponentState, call: Call, predecessor: felt252, salt: felt252, delay: u64) { + fn schedule( + ref self: ComponentState, + call: Call, + predecessor: felt252, + salt: felt252, + delay: u64 + ) { Timelock::schedule(ref self, call, predecessor, salt, delay); } fn schedule_batch( - ref self: ComponentState, calls: Span, predecessor: felt252, salt: felt252, delay: u64 + ref self: ComponentState, + calls: Span, + predecessor: felt252, + salt: felt252, + delay: u64 ) { Timelock::schedule_batch(ref self, calls, predecessor, salt, delay); } @@ -344,11 +369,21 @@ mod TimelockControllerComponent { Timelock::cancel(ref self, id); } - fn execute(ref self: ComponentState, call: Call, predecessor: felt252, salt: felt252) { + fn execute( + ref self: ComponentState, + call: Call, + predecessor: felt252, + salt: felt252 + ) { Timelock::execute(ref self, call, predecessor, salt); } - fn execute_batch(ref self: ComponentState, calls: Span, predecessor: felt252, salt: felt252) { + fn execute_batch( + ref self: ComponentState, + calls: Span, + predecessor: felt252, + salt: felt252 + ) { Timelock::execute_batch(ref self, calls, predecessor, salt); } @@ -365,7 +400,9 @@ mod TimelockControllerComponent { } // IAccessControl - fn has_role(self: @ComponentState, role: felt252, account: ContractAddress) -> bool { + fn has_role( + self: @ComponentState, role: felt252, account: ContractAddress + ) -> bool { let access_control = get_dep_component!(self, AccessControl); access_control.has_role(role, account) } @@ -375,22 +412,30 @@ mod TimelockControllerComponent { access_control.get_role_admin(role) } - fn grant_role(ref self: ComponentState, role: felt252, account: ContractAddress) { + fn grant_role( + ref self: ComponentState, role: felt252, account: ContractAddress + ) { let mut access_control = get_dep_component_mut!(ref self, AccessControl); access_control.grant_role(role, account); } - fn revoke_role(ref self: ComponentState, role: felt252, account: ContractAddress) { + fn revoke_role( + ref self: ComponentState, role: felt252, account: ContractAddress + ) { let mut access_control = get_dep_component_mut!(ref self, AccessControl); access_control.revoke_role(role, account); } - fn renounce_role(ref self: ComponentState, role: felt252, account: ContractAddress) { + fn renounce_role( + ref self: ComponentState, role: felt252, account: ContractAddress + ) { let mut access_control = get_dep_component_mut!(ref self, AccessControl); access_control.renounce_role(role, account); } // IAccessControlCamel - fn hasRole(self: @ComponentState, role: felt252, account: ContractAddress) -> bool { + fn hasRole( + self: @ComponentState, role: felt252, account: ContractAddress + ) -> bool { let access_control = get_dep_component!(self, AccessControl); access_control.hasRole(role, account) } @@ -400,17 +445,23 @@ mod TimelockControllerComponent { access_control.getRoleAdmin(role) } - fn grantRole(ref self: ComponentState, role: felt252, account: ContractAddress) { + fn grantRole( + ref self: ComponentState, role: felt252, account: ContractAddress + ) { let mut access_control = get_dep_component_mut!(ref self, AccessControl); access_control.grantRole(role, account); } - fn revokeRole(ref self: ComponentState, role: felt252, account: ContractAddress) { + fn revokeRole( + ref self: ComponentState, role: felt252, account: ContractAddress + ) { let mut access_control = get_dep_component_mut!(ref self, AccessControl); access_control.revokeRole(role, account); } - fn renounceRole(ref self: ComponentState, role: felt252, account: ContractAddress) { + fn renounceRole( + ref self: ComponentState, role: felt252, account: ContractAddress + ) { let mut access_control = get_dep_component_mut!(ref self, AccessControl); access_control.renounceRole(role, account); } diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 74b17889a..1c20552c8 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -258,10 +258,14 @@ fn schedule_from_proposer(salt: felt252) { // Check event(s) let event_index = 0; if salt != 0 { - assert_event_schedule(timelock.contract_address, target_id, event_index, call, predecessor, delay); + assert_event_schedule( + timelock.contract_address, target_id, event_index, call, predecessor, delay + ); assert_only_event_call_salt(timelock.contract_address, target_id, salt); } else { - assert_only_event_schedule(timelock.contract_address, target_id, event_index, call, predecessor, delay); + assert_only_event_schedule( + timelock.contract_address, target_id, event_index, call, predecessor, delay + ); } } @@ -344,10 +348,14 @@ fn schedule_batch_from_proposer(salt: felt252) { // Check events if salt != 0 { - assert_events_schedule_batch(timelock.contract_address, target_id, calls, predecessor, delay); + assert_events_schedule_batch( + timelock.contract_address, target_id, calls, predecessor, delay + ); assert_only_event_call_salt(timelock.contract_address, target_id, salt); } else { - assert_only_events_schedule_batch(timelock.contract_address, target_id, calls, predecessor, delay); + assert_only_events_schedule_batch( + timelock.contract_address, target_id, calls, predecessor, delay + ); } } @@ -437,7 +445,9 @@ fn test_execute_when_scheduled() { // Schedule testing::set_contract_address(PROPOSER()); timelock.schedule(call, predecessor, salt, delay); - assert_only_event_schedule(timelock.contract_address, target_id, event_index, call, predecessor, delay); + assert_only_event_schedule( + timelock.contract_address, target_id, event_index, call, predecessor, delay + ); assert_operation_state(timelock, OperationState::Waiting, target_id); // Fast-forward @@ -598,12 +608,16 @@ fn test_execute_after_dependency() { testing::set_contract_address(PROPOSER()); timelock.schedule(call_1, predecessor_1, salt, delay); assert_operation_state(timelock, OperationState::Waiting, target_id_1); - assert_only_event_schedule(timelock.contract_address, target_id_1, event_index, call_1, predecessor_1, delay); + assert_only_event_schedule( + timelock.contract_address, target_id_1, event_index, call_1, predecessor_1, delay + ); // Schedule call 2 timelock.schedule(call_2, predecessor_2, salt, delay); assert_operation_state(timelock, OperationState::Waiting, target_id_2); - assert_only_event_schedule(timelock.contract_address, target_id_2, event_index, call_2, predecessor_2, delay); + assert_only_event_schedule( + timelock.contract_address, target_id_2, event_index, call_2, predecessor_2, delay + ); // Fast-forward testing::set_block_timestamp(delay); @@ -614,7 +628,7 @@ fn test_execute_after_dependency() { testing::set_contract_address(EXECUTOR()); timelock.execute(call_1, predecessor_1, salt); assert_operation_state(timelock, OperationState::Done, target_id_1); - assert_event_execute(timelock.contract_address, target_id_1,event_index, call_1); + assert_event_execute(timelock.contract_address, target_id_1, event_index, call_1); // Execute call 2 timelock.execute(call_2, predecessor_2, salt); @@ -653,7 +667,9 @@ fn test_execute_batch_when_scheduled() { testing::set_contract_address(PROPOSER()); timelock.schedule_batch(calls, predecessor, salt, delay); assert_operation_state(timelock, OperationState::Waiting, target_id); - assert_only_events_schedule_batch(timelock.contract_address, target_id, calls, predecessor, delay); + assert_only_events_schedule_batch( + timelock.contract_address, target_id, calls, predecessor, delay + ); // Fast-forward testing::set_block_timestamp(delay); @@ -835,12 +851,16 @@ fn test_execute_batch_after_dependency() { testing::set_contract_address(PROPOSER()); timelock.schedule_batch(calls_1, predecessor_1, salt, delay); assert_operation_state(timelock, OperationState::Waiting, target_id_1); - assert_only_events_schedule_batch(timelock.contract_address, target_id_1, calls_1, predecessor_1, delay); + assert_only_events_schedule_batch( + timelock.contract_address, target_id_1, calls_1, predecessor_1, delay + ); // Schedule calls 2 timelock.schedule_batch(calls_2, predecessor_2, salt, delay); assert_operation_state(timelock, OperationState::Waiting, target_id_2); - assert_only_events_schedule_batch(timelock.contract_address, target_id_2, calls_2, predecessor_2, delay); + assert_only_events_schedule_batch( + timelock.contract_address, target_id_2, calls_2, predecessor_2, delay + ); // Fast-forward testing::set_block_timestamp(delay); @@ -877,7 +897,9 @@ fn test_cancel_from_canceller() { testing::set_contract_address(PROPOSER()); // PROPOSER is also CANCELLER timelock.schedule(call, predecessor, salt, delay); assert_operation_state(timelock, OperationState::Waiting, target_id); - assert_only_event_schedule(timelock.contract_address, target_id, event_index, call, predecessor, delay); + assert_only_event_schedule( + timelock.contract_address, target_id, event_index, call, predecessor, delay + ); // Cancel timelock.cancel(target_id); @@ -946,7 +968,9 @@ fn test_update_delay_scheduled() { testing::set_contract_address(PROPOSER()); timelock.schedule(call, predecessor, salt, delay); assert_operation_state(timelock, OperationState::Waiting, target_id); - assert_only_event_schedule(timelock.contract_address, target_id, event_index, call, predecessor, delay); + assert_only_event_schedule( + timelock.contract_address, target_id, event_index, call, predecessor, delay + ); // Fast-forward testing::set_block_timestamp(delay); @@ -1126,9 +1150,7 @@ fn test_assert_only_role_or_open_role_with_open_role() { // Helpers // -fn assert_operation_state( - timelock: TimelockABIDispatcher, exp_state: OperationState, id: felt252 -) { +fn assert_operation_state(timelock: TimelockABIDispatcher, exp_state: OperationState, id: felt252) { let operation_state = timelock.get_operation_state(id); assert_eq!(operation_state, exp_state); @@ -1186,7 +1208,14 @@ fn assert_only_event_delay_change(contract: ContractAddress, old_duration: u64, // CallScheduled -fn assert_event_schedule(contract: ContractAddress, id: felt252, index: felt252, call: Call, predecessor: felt252, delay: u64) { +fn assert_event_schedule( + contract: ContractAddress, + id: felt252, + index: felt252, + call: Call, + predecessor: felt252, + delay: u64 +) { let event = utils::pop_log::(contract).unwrap(); let expected = TimelockControllerComponent::Event::CallScheduled( CallScheduled { id, index, call, predecessor, delay } @@ -1202,7 +1231,12 @@ fn assert_event_schedule(contract: ContractAddress, id: felt252, index: felt252, } fn assert_only_event_schedule( - contract: ContractAddress, id: felt252, index: felt252, call: Call, predecessor: felt252, delay: u64 + contract: ContractAddress, + id: felt252, + index: felt252, + call: Call, + predecessor: felt252, + delay: u64 ) { assert_event_schedule(contract, id, index, call, predecessor, delay); utils::assert_no_events_left(contract); @@ -1245,7 +1279,9 @@ fn assert_only_event_call_salt(contract: ContractAddress, id: felt252, salt: fel fn assert_event_execute(contract: ContractAddress, id: felt252, index: felt252, call: Call) { let event = utils::pop_log::(contract).unwrap(); - let expected = TimelockControllerComponent::Event::CallExecuted(CallExecuted { id, index, call }); + let expected = TimelockControllerComponent::Event::CallExecuted( + CallExecuted { id, index, call } + ); assert!(event == expected); // Check indexed keys diff --git a/src/tests/mocks/timelock_mocks.cairo b/src/tests/mocks/timelock_mocks.cairo index 327cb3f9f..aa8166700 100644 --- a/src/tests/mocks/timelock_mocks.cairo +++ b/src/tests/mocks/timelock_mocks.cairo @@ -17,7 +17,8 @@ mod TimelockControllerMock { // Timelock Mixin #[abi(embed_v0)] - impl TimelockMixinImpl = TimelockControllerComponent::TimelockMixinImpl; + impl TimelockMixinImpl = + TimelockControllerComponent::TimelockMixinImpl; impl TimelockInternalImpl = TimelockControllerComponent::InternalImpl; #[storage] From 7054fcdb2a94246f1897459d8224f6c20832544a Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 16:32:10 -0400 Subject: [PATCH 041/103] add safe token transfer tests --- src/tests/governance/test_timelock.cairo | 139 ++++++++++++++++++++++- 1 file changed, 133 insertions(+), 6 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 1c20552c8..896989f26 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -3,6 +3,7 @@ use openzeppelin::access::accesscontrol::AccessControlComponent::{ AccessControlImpl, InternalImpl as AccessControlInternalImpl }; use openzeppelin::access::accesscontrol::interface::IAccessControl; +use openzeppelin::tests::mocks::account_mocks::SnakeAccountMock; use openzeppelin::governance::timelock::TimelockControllerComponent::Call; use openzeppelin::governance::timelock::TimelockControllerComponent::OperationState; use openzeppelin::governance::timelock::TimelockControllerComponent::{ @@ -21,6 +22,7 @@ use openzeppelin::governance::timelock::interface::{ use openzeppelin::governance::timelock::utils::call_impls::{CallPartialEq, HashCallImpl}; use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; +use openzeppelin::tests::mocks::erc1155_mocks::DualCaseERC1155Mock; use openzeppelin::tests::mocks::erc721_mocks::DualCaseERC721Mock; use openzeppelin::tests::mocks::timelock_mocks::{ ITimelockAttackerDispatcher, ITimelockAttackerDispatcherTrait @@ -30,10 +32,11 @@ use openzeppelin::tests::mocks::timelock_mocks::{ }; use openzeppelin::tests::mocks::timelock_mocks::{TimelockControllerMock, TimelockAttackerMock}; use openzeppelin::tests::utils::constants::{ - ADMIN, ZERO, NAME, SYMBOL, BASE_URI, RECIPIENT, SPENDER, OTHER, SALT, TOKEN_ID + ADMIN, ZERO, NAME, SYMBOL, BASE_URI, OWNER, RECIPIENT, SPENDER, OTHER, PUBKEY, SALT, TOKEN_ID, TOKEN_ID_2, TOKEN_VALUE, TOKEN_VALUE_2 }; use openzeppelin::tests::utils; use openzeppelin::token::erc1155::interface::IERC1155_RECEIVER_ID; +use openzeppelin::token::erc1155::interface::{IERC1155DispatcherTrait, IERC1155Dispatcher}; use openzeppelin::token::erc721::interface::IERC721_RECEIVER_ID; use openzeppelin::token::erc721::interface::{IERC721DispatcherTrait, IERC721Dispatcher}; use openzeppelin::utils::selectors; @@ -54,11 +57,13 @@ fn COMPONENT_STATE() -> ComponentState { TimelockControllerComponent::component_state_for_testing() } +// Constants const MIN_DELAY: u64 = 1000; const NEW_DELAY: u64 = 2000; const VALUE: felt252 = 'VALUE'; const NO_PREDECESSOR: felt252 = 0; +// Addresses fn PROPOSER() -> ContractAddress { contract_address_const::<'PROPOSER'>() } @@ -81,6 +86,7 @@ fn get_executors() -> (ContractAddress, ContractAddress, ContractAddress) { (e1, e2, e3) } +// Operations fn single_operation(target: ContractAddress) -> Call { // Call: approve let mut calldata = array![]; @@ -105,6 +111,10 @@ fn failing_operation(target: ContractAddress) -> Call { Call { to: target, selector: selector!("failing_function"), calldata: calldata.span() } } +// +// Dispatchers +// + fn setup_dispatchers() -> (TimelockABIDispatcher, IMockContractDispatcher) { let timelock = deploy_timelock(); let target = deploy_mock_target(); @@ -132,22 +142,42 @@ fn deploy_timelock() -> TimelockABIDispatcher { TimelockABIDispatcher { contract_address: address } } -fn deploy_erc721(recipient: ContractAddress) -> IERC721Dispatcher { +fn deploy_erc721() -> IERC721Dispatcher { let mut calldata = array![]; calldata.append_serde(NAME()); calldata.append_serde(SYMBOL()); calldata.append_serde(BASE_URI()); - calldata.append_serde(recipient); + calldata.append_serde(OWNER()); calldata.append_serde(TOKEN_ID); let address = utils::deploy(DualCaseERC721Mock::TEST_CLASS_HASH, calldata); - // Event dropped: - // - Transfer - utils::drop_event(address); IERC721Dispatcher { contract_address: address } } +fn deploy_erc1155() -> (IERC1155Dispatcher, ContractAddress) { + let uri: ByteArray = "URI"; + let mut calldata = array![]; + let mut token_id = TOKEN_ID; + let mut value = TOKEN_VALUE; + + let owner = setup_account(); + testing::set_contract_address(owner); + + calldata.append_serde(uri); + calldata.append_serde(owner); + calldata.append_serde(token_id); + calldata.append_serde(value); + + let address = utils::deploy(DualCaseERC1155Mock::TEST_CLASS_HASH, calldata); + (IERC1155Dispatcher { contract_address: address }, owner) +} + +fn setup_account() -> ContractAddress { + let mut calldata = array![PUBKEY]; + utils::deploy(SnakeAccountMock::TEST_CLASS_HASH, calldata) +} + fn deploy_mock_target() -> IMockContractDispatcher { let mut calldata = array![]; @@ -987,6 +1017,103 @@ fn test_update_delay_scheduled() { assert_eq!(get_new_delay, NEW_DELAY); } +// +// Safe receive +// + +#[test] +fn test_receive_erc721_safe_transfer() { + let mut timelock = deploy_timelock(); + let mut erc721 = deploy_erc721(); + + let owner = OWNER(); + let timelock_addr = timelock.contract_address; + let token = TOKEN_ID; + let data = array![].span(); + + // Check original holder + let original_owner = erc721.owner_of(token); + assert_eq!(original_owner, owner); + + // Safe transfer + testing::set_contract_address(OWNER()); + erc721.safe_transfer_from(owner, timelock_addr, token, data); + + // Check that timelock accepted safe transfer + let new_owner = erc721.owner_of(token); + assert_eq!(new_owner, timelock_addr); +} + +#[test] +fn test_receive_erc1155_safe_transfer() { + let mut timelock = deploy_timelock(); + let (mut erc1155, owner) = deploy_erc1155(); + + let token_id = TOKEN_ID; + let token_value = TOKEN_VALUE; + //let data = array![]; + + // Check initial balances + let owner_balance = erc1155.balance_of(owner, token_id); + let expected_balance = token_value; + assert_eq!(owner_balance, expected_balance); + + let timelock_balance = erc1155.balance_of(timelock.contract_address, token_id); + let expected_balance = 0; + assert_eq!(timelock_balance, expected_balance); + + // Safe transfer + testing::set_contract_address(owner); + let transfer_amt = 1; + let data = array![].span(); + erc1155.safe_transfer_from(owner, timelock.contract_address, token_id, transfer_amt, data); + + // Check new balances + let owner_balance = erc1155.balance_of(owner, token_id); + let expected_balance = token_value - transfer_amt; + assert_eq!(owner_balance, expected_balance); + + let timelock_balance = erc1155.balance_of(timelock.contract_address, token_id); + let expected_balance = transfer_amt; + assert_eq!(timelock_balance, expected_balance); +} + +#[test] +fn test_receive_erc1155_safe_batch_transfer() { + let mut timelock = deploy_timelock(); + let (mut erc1155, owner) = deploy_erc1155(); + + let token_id = TOKEN_ID; + let token_value = TOKEN_VALUE; + + // Check initial balances + let owner_balance = erc1155.balance_of(owner, token_id); + let expected_balance = token_value; + assert_eq!(owner_balance, expected_balance); + + let timelock_balance = erc1155.balance_of(timelock.contract_address, token_id); + let expected_balance = 0; + assert_eq!(timelock_balance, expected_balance); + + // Safe batch transfer + testing::set_contract_address(owner); + let transfer_ids = array![token_id, token_id].span(); + let transfer_amts = array![1, 1].span(); + let data = array![].span(); + erc1155.safe_batch_transfer_from(owner, timelock.contract_address, transfer_ids, transfer_amts, data); + + // Check new balances + let total_transfer_amt = 2; + + let owner_balance = erc1155.balance_of(owner, token_id); + let expected_balance = token_value - total_transfer_amt; + assert_eq!(owner_balance, expected_balance); + + let timelock_balance = erc1155.balance_of(timelock.contract_address, token_id); + let expected_balance = total_transfer_amt; + assert_eq!(timelock_balance, expected_balance); +} + // // Internal // From 41681d951cfcdcda3de203bee4ef211916c6e172 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 16:32:27 -0400 Subject: [PATCH 042/103] fix fmt --- src/tests/governance/test_timelock.cairo | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 896989f26..ef30d09fc 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -3,7 +3,6 @@ use openzeppelin::access::accesscontrol::AccessControlComponent::{ AccessControlImpl, InternalImpl as AccessControlInternalImpl }; use openzeppelin::access::accesscontrol::interface::IAccessControl; -use openzeppelin::tests::mocks::account_mocks::SnakeAccountMock; use openzeppelin::governance::timelock::TimelockControllerComponent::Call; use openzeppelin::governance::timelock::TimelockControllerComponent::OperationState; use openzeppelin::governance::timelock::TimelockControllerComponent::{ @@ -22,6 +21,7 @@ use openzeppelin::governance::timelock::interface::{ use openzeppelin::governance::timelock::utils::call_impls::{CallPartialEq, HashCallImpl}; use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; +use openzeppelin::tests::mocks::account_mocks::SnakeAccountMock; use openzeppelin::tests::mocks::erc1155_mocks::DualCaseERC1155Mock; use openzeppelin::tests::mocks::erc721_mocks::DualCaseERC721Mock; use openzeppelin::tests::mocks::timelock_mocks::{ @@ -32,7 +32,8 @@ use openzeppelin::tests::mocks::timelock_mocks::{ }; use openzeppelin::tests::mocks::timelock_mocks::{TimelockControllerMock, TimelockAttackerMock}; use openzeppelin::tests::utils::constants::{ - ADMIN, ZERO, NAME, SYMBOL, BASE_URI, OWNER, RECIPIENT, SPENDER, OTHER, PUBKEY, SALT, TOKEN_ID, TOKEN_ID_2, TOKEN_VALUE, TOKEN_VALUE_2 + ADMIN, ZERO, NAME, SYMBOL, BASE_URI, OWNER, RECIPIENT, SPENDER, OTHER, PUBKEY, SALT, TOKEN_ID, + TOKEN_ID_2, TOKEN_VALUE, TOKEN_VALUE_2 }; use openzeppelin::tests::utils; use openzeppelin::token::erc1155::interface::IERC1155_RECEIVER_ID; @@ -1100,7 +1101,10 @@ fn test_receive_erc1155_safe_batch_transfer() { let transfer_ids = array![token_id, token_id].span(); let transfer_amts = array![1, 1].span(); let data = array![].span(); - erc1155.safe_batch_transfer_from(owner, timelock.contract_address, transfer_ids, transfer_amts, data); + erc1155 + .safe_batch_transfer_from( + owner, timelock.contract_address, transfer_ids, transfer_amts, data + ); // Check new balances let total_transfer_amt = 2; From 7144eeca4b8bfa7eede891d365a64307568313ff Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 17:25:31 -0400 Subject: [PATCH 043/103] tidy up code --- src/governance/timelock.cairo | 1 + .../timelock/timelock_controller.cairo | 2 +- src/tests/governance/test_timelock.cairo | 59 +++++++++++++++---- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/governance/timelock.cairo b/src/governance/timelock.cairo index e0e923bdf..ede6ac3e0 100644 --- a/src/governance/timelock.cairo +++ b/src/governance/timelock.cairo @@ -3,3 +3,4 @@ mod timelock_controller; mod utils; use timelock_controller::TimelockControllerComponent; +use timelock_controller::TimelockControllerComponent::{PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE}; diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 31608d9c5..bd625c202 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -15,7 +15,7 @@ mod TimelockControllerComponent { use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin::governance::timelock::interface::{ITimelock, TimelockABI}; use openzeppelin::governance::timelock::utils::OperationState; - use openzeppelin::governance::timelock::utils::call_impls::{CallPartialEq, HashCallImpl, Call}; + use openzeppelin::governance::timelock::utils::call_impls::{HashCallImpl, Call}; use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; use openzeppelin::introspection::src5::SRC5Component::SRC5; use openzeppelin::introspection::src5::SRC5Component; diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index ef30d09fc..5a5dace29 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -1,4 +1,5 @@ use hash::{HashStateTrait, HashStateExTrait}; +use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin::access::accesscontrol::AccessControlComponent::{ AccessControlImpl, InternalImpl as AccessControlInternalImpl }; @@ -8,8 +9,8 @@ use openzeppelin::governance::timelock::TimelockControllerComponent::OperationSt use openzeppelin::governance::timelock::TimelockControllerComponent::{ CallScheduled, CallExecuted, CallSalt, Cancelled, MinDelayChange }; -use openzeppelin::governance::timelock::TimelockControllerComponent::{ - PROPOSER_ROLE, EXECUTOR_ROLE, CANCELLER_ROLE, DEFAULT_ADMIN_ROLE +use openzeppelin::governance::timelock::{ + PROPOSER_ROLE, EXECUTOR_ROLE, CANCELLER_ROLE }; use openzeppelin::governance::timelock::TimelockControllerComponent::{ TimelockImpl, InternalImpl as TimelockInternalImpl @@ -18,7 +19,6 @@ use openzeppelin::governance::timelock::TimelockControllerComponent; use openzeppelin::governance::timelock::interface::{ TimelockABIDispatcher, TimelockABIDispatcherTrait }; -use openzeppelin::governance::timelock::utils::call_impls::{CallPartialEq, HashCallImpl}; use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; use openzeppelin::tests::mocks::account_mocks::SnakeAccountMock; @@ -27,9 +27,8 @@ use openzeppelin::tests::mocks::erc721_mocks::DualCaseERC721Mock; use openzeppelin::tests::mocks::timelock_mocks::{ ITimelockAttackerDispatcher, ITimelockAttackerDispatcherTrait }; -use openzeppelin::tests::mocks::timelock_mocks::{ - MockContract, IMockContractDispatcher, IMockContractDispatcherTrait -}; +use openzeppelin::tests::mocks::timelock_mocks::MockContract; +use openzeppelin::tests::mocks::timelock_mocks::{IMockContractDispatcher, IMockContractDispatcherTrait}; use openzeppelin::tests::mocks::timelock_mocks::{TimelockControllerMock, TimelockAttackerMock}; use openzeppelin::tests::utils::constants::{ ADMIN, ZERO, NAME, SYMBOL, BASE_URI, OWNER, RECIPIENT, SPENDER, OTHER, PUBKEY, SALT, TOKEN_ID, @@ -58,13 +57,19 @@ fn COMPONENT_STATE() -> ComponentState { TimelockControllerComponent::component_state_for_testing() } +// // Constants +// + const MIN_DELAY: u64 = 1000; const NEW_DELAY: u64 = 2000; const VALUE: felt252 = 'VALUE'; const NO_PREDECESSOR: felt252 = 0; +// // Addresses +// + fn PROPOSER() -> ContractAddress { contract_address_const::<'PROPOSER'>() } @@ -87,9 +92,11 @@ fn get_executors() -> (ContractAddress, ContractAddress, ContractAddress) { (e1, e2, e3) } +// // Operations +// + fn single_operation(target: ContractAddress) -> Call { - // Call: approve let mut calldata = array![]; calldata.append_serde(VALUE); @@ -193,7 +200,9 @@ fn deploy_attacker() -> ITimelockAttackerDispatcher { ITimelockAttackerDispatcher { contract_address: address } } +// // hash_operation +// #[test] fn test_hash_operation() { @@ -204,13 +213,14 @@ fn test_hash_operation() { // Setup call let mut calldata = array![]; calldata.append_serde(VALUE); - let mut call = Call { to: target.contract_address, selector: selector!("set_number"), calldata: calldata.span() }; + // Hash operation let hashed_operation = timelock.hash_operation(call, predecessor, salt); + // Manually set hash elements let mut expected_hash = PoseidonTrait::new() .update_with(4) // total elements of call .update_with(target.contract_address) // call::to @@ -226,19 +236,18 @@ fn test_hash_operation() { #[test] fn test_hash_operation_batch() { let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = 123; + let salt = SALT; // Setup calls let mut calldata = array![]; calldata.append_serde(VALUE); - let mut call = Call { to: target.contract_address, selector: selector!("set_number"), calldata: calldata.span() }; let calls = array![call, call, call].span(); // Hash operation - let predecessor = 123; - let salt = SALT; let hashed_operation = timelock.hash_operation_batch(calls, predecessor, salt); // Manually set hash elements @@ -264,7 +273,9 @@ fn test_hash_operation_batch() { assert_eq!(hashed_operation, expected_hash); } +// // schedule +// fn schedule_from_proposer(salt: felt252) { let (mut timelock, mut target) = setup_dispatchers(); @@ -355,7 +366,9 @@ fn test_schedule_bad_min_delay() { timelock.schedule(call, predecessor, salt, bad_delay); } +// // schedule_batch +// fn schedule_batch_from_proposer(salt: felt252) { let (mut timelock, mut target) = setup_dispatchers(); @@ -445,7 +458,9 @@ fn test_schedule_batch_bad_min_delay() { timelock.schedule_batch(calls, predecessor, salt, bad_delay); } +// // execute +// #[test] #[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] @@ -667,7 +682,9 @@ fn test_execute_after_dependency() { assert_only_event_execute(timelock.contract_address, target_id_2, event_index, call_2); } +// // execute_batch +// #[test] #[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] @@ -910,7 +927,9 @@ fn test_execute_batch_after_dependency() { assert_only_events_execute_batch(timelock.contract_address, target_id_2, calls_2); } +// // cancel +// #[test] fn test_cancel_from_canceller() { @@ -970,7 +989,9 @@ fn test_cancel_unauthorized() { timelock.cancel(target_id); } +// // update_delay +// #[test] #[should_panic(expected: ('Timelock: unauthorized caller', 'ENTRYPOINT_FAILED'))] @@ -1122,7 +1143,9 @@ fn test_receive_erc1155_safe_batch_transfer() { // Internal // +// // initializer +// #[test] fn test_initializer_single_role_and_no_admin() { @@ -1217,7 +1240,9 @@ fn test_initializer_min_delay() { assert_only_event_delay_change(ZERO(), 0, MIN_DELAY); } +// // assert_only_role_or_open_role +// #[test] fn test_assert_only_role_or_open_role_when_has_role() { @@ -1322,7 +1347,9 @@ fn assert_operation_state(timelock: TimelockABIDispatcher, exp_state: OperationS // Event helpers // +// // MinDelayChange +// fn assert_event_delay_change(contract: ContractAddress, old_duration: u64, new_duration: u64) { let event = utils::pop_log::(contract).unwrap(); @@ -1337,7 +1364,9 @@ fn assert_only_event_delay_change(contract: ContractAddress, old_duration: u64, utils::assert_no_events_left(contract); } +// // CallScheduled +// fn assert_event_schedule( contract: ContractAddress, @@ -1393,7 +1422,9 @@ fn assert_only_events_schedule_batch( utils::assert_no_events_left(contract); } +// // CallSalt +// fn assert_event_call_salt(contract: ContractAddress, id: felt252, salt: felt252) { let event = utils::pop_log::(contract).unwrap(); @@ -1406,7 +1437,9 @@ fn assert_only_event_call_salt(contract: ContractAddress, id: felt252, salt: fel utils::assert_no_events_left(contract); } +// // CallExecuted +// fn assert_event_execute(contract: ContractAddress, id: felt252, index: felt252, call: Call) { let event = utils::pop_log::(contract).unwrap(); @@ -1444,7 +1477,9 @@ fn assert_only_events_execute_batch(contract: ContractAddress, id: felt252, call utils::assert_no_events_left(contract); } +// // Cancelled +// fn assert_event_cancel(contract: ContractAddress, id: felt252) { let event = utils::pop_log::(contract).unwrap(); @@ -1463,7 +1498,9 @@ fn assert_only_event_cancel(contract: ContractAddress, id: felt252) { utils::assert_no_events_left(contract); } +// // MinDelayChange +// fn assert_event_delay(contract: ContractAddress, old_duration: u64, new_duration: u64) { let event = utils::pop_log::(contract).unwrap(); From 00706075f61158421093f3247310f159542290ab Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 18:58:35 -0400 Subject: [PATCH 044/103] add descriptions to events --- src/governance/timelock/timelock_controller.cairo | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index bd625c202..991af72ac 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -58,7 +58,7 @@ mod TimelockControllerComponent { MinDelayChange: MinDelayChange } - /// Emitted when... + /// Emitted when `call` is scheduled as part of operation `id`. #[derive(Drop, PartialEq, starknet::Event)] struct CallScheduled { #[key] @@ -70,7 +70,7 @@ mod TimelockControllerComponent { delay: u64 } - /// Emitted when... + /// Emitted when `call` is performed as part of operation `id`. #[derive(Drop, PartialEq, starknet::Event)] struct CallExecuted { #[key] @@ -80,7 +80,7 @@ mod TimelockControllerComponent { call: Call } - /// Emitted when... + /// Emitted when a new proposal is scheduled with non-zero salt. #[derive(Drop, PartialEq, starknet::Event)] struct CallSalt { #[key] @@ -88,14 +88,14 @@ mod TimelockControllerComponent { salt: felt252 } - /// Emitted when... + /// Emitted when operation `id` is cancelled. #[derive(Drop, PartialEq, starknet::Event)] struct Cancelled { #[key] id: felt252 } - /// Emitted when... + /// Emitted when the minimum delay for future operations is modified. #[derive(Drop, PartialEq, starknet::Event)] struct MinDelayChange { old_duration: u64, From 4a4d495db7601dd50a1c65b86971944b98f996e3 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 19:03:24 -0400 Subject: [PATCH 045/103] clean up code --- src/governance/timelock.cairo | 4 +++- src/governance/timelock/timelock_controller.cairo | 6 ++---- src/tests/governance/test_timelock.cairo | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/governance/timelock.cairo b/src/governance/timelock.cairo index ede6ac3e0..34d2163ba 100644 --- a/src/governance/timelock.cairo +++ b/src/governance/timelock.cairo @@ -1,6 +1,8 @@ mod interface; mod timelock_controller; mod utils; +use timelock_controller::TimelockControllerComponent::{ + PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE +}; use timelock_controller::TimelockControllerComponent; -use timelock_controller::TimelockControllerComponent::{PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE}; diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 991af72ac..5588876cb 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.13.0 (governance/timelock/timelock_controller.cairo) -/// # TimelockController Component +/// # Timelock Controller Component /// /// #[starknet::component] @@ -16,7 +16,6 @@ mod TimelockControllerComponent { use openzeppelin::governance::timelock::interface::{ITimelock, TimelockABI}; use openzeppelin::governance::timelock::utils::OperationState; use openzeppelin::governance::timelock::utils::call_impls::{HashCallImpl, Call}; - use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; use openzeppelin::introspection::src5::SRC5Component::SRC5; use openzeppelin::introspection::src5::SRC5Component; use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent::{ @@ -26,7 +25,7 @@ mod TimelockControllerComponent { InternalImpl as ERC1155InternalImpl }; use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent; - use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent::InternalImpl as ERC721InternalImpl; + use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent::InternalImpl as ERC721ReceiverInternalImpl; use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent::{ ERC721ReceiverImpl, ERC721ReceiverCamelImpl }; @@ -34,7 +33,6 @@ mod TimelockControllerComponent { use poseidon::PoseidonTrait; use starknet::ContractAddress; use starknet::SyscallResultTrait; - use zeroable::Zeroable; // Constants const PROPOSER_ROLE: felt252 = selector!("PROPOSER_ROLE"); diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 5a5dace29..043c3f644 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -1,17 +1,14 @@ use hash::{HashStateTrait, HashStateExTrait}; -use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin::access::accesscontrol::AccessControlComponent::{ AccessControlImpl, InternalImpl as AccessControlInternalImpl }; +use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin::access::accesscontrol::interface::IAccessControl; use openzeppelin::governance::timelock::TimelockControllerComponent::Call; use openzeppelin::governance::timelock::TimelockControllerComponent::OperationState; use openzeppelin::governance::timelock::TimelockControllerComponent::{ CallScheduled, CallExecuted, CallSalt, Cancelled, MinDelayChange }; -use openzeppelin::governance::timelock::{ - PROPOSER_ROLE, EXECUTOR_ROLE, CANCELLER_ROLE -}; use openzeppelin::governance::timelock::TimelockControllerComponent::{ TimelockImpl, InternalImpl as TimelockInternalImpl }; @@ -19,16 +16,19 @@ use openzeppelin::governance::timelock::TimelockControllerComponent; use openzeppelin::governance::timelock::interface::{ TimelockABIDispatcher, TimelockABIDispatcherTrait }; +use openzeppelin::governance::timelock::{PROPOSER_ROLE, EXECUTOR_ROLE, CANCELLER_ROLE}; use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; use openzeppelin::tests::mocks::account_mocks::SnakeAccountMock; use openzeppelin::tests::mocks::erc1155_mocks::DualCaseERC1155Mock; use openzeppelin::tests::mocks::erc721_mocks::DualCaseERC721Mock; +use openzeppelin::tests::mocks::timelock_mocks::MockContract; use openzeppelin::tests::mocks::timelock_mocks::{ ITimelockAttackerDispatcher, ITimelockAttackerDispatcherTrait }; -use openzeppelin::tests::mocks::timelock_mocks::MockContract; -use openzeppelin::tests::mocks::timelock_mocks::{IMockContractDispatcher, IMockContractDispatcherTrait}; +use openzeppelin::tests::mocks::timelock_mocks::{ + IMockContractDispatcher, IMockContractDispatcherTrait +}; use openzeppelin::tests::mocks::timelock_mocks::{TimelockControllerMock, TimelockAttackerMock}; use openzeppelin::tests::utils::constants::{ ADMIN, ZERO, NAME, SYMBOL, BASE_URI, OWNER, RECIPIENT, SPENDER, OTHER, PUBKEY, SALT, TOKEN_ID, From 9ce8d0223a523bfc92d74bdcb985f22585a8766c Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 31 May 2024 19:18:49 -0400 Subject: [PATCH 046/103] inline CallPartialEq fns --- src/governance/timelock/utils/call_impls.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/governance/timelock/utils/call_impls.cairo b/src/governance/timelock/utils/call_impls.cairo index 7bf6c8117..99ea55e0b 100644 --- a/src/governance/timelock/utils/call_impls.cairo +++ b/src/governance/timelock/utils/call_impls.cairo @@ -34,7 +34,7 @@ impl CallPartialEq of PartialEq { Serde::serialize(lhs, ref rhs_arr); lhs_arr == rhs_arr } - + #[inline(always)] fn ne(lhs: @Call, rhs: @Call) -> bool { let mut lhs_arr = array![]; Serde::serialize(lhs, ref lhs_arr); From 6c67531a2ec7b4b1f43243668546340b4a35a62e Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 2 Jun 2024 16:24:51 -0400 Subject: [PATCH 047/103] start fn descriptions --- .../timelock/timelock_controller.cairo | 78 ++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 5588876cb..9866b2f7a 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -118,27 +118,39 @@ mod TimelockControllerComponent { +ERC1155ReceiverComponent::HasComponent, +Drop > of ITimelock> { + /// Returns whether `id` corresponds to a registered operation. + /// This includes the OperationStates: Waiting, Ready, and Done. fn is_operation(self: @ComponentState, id: felt252) -> bool { Timelock::get_operation_state(self, id) != OperationState::Unset } + /// Returns whether the `id` OperationState is Pending or not. + /// Note that a Pending operation may also be Ready. fn is_operation_pending(self: @ComponentState, id: felt252) -> bool { let state = Timelock::get_operation_state(self, id); state == OperationState::Waiting || state == OperationState::Ready } + /// Returns whether the `id` OperationState is Ready or not. + /// Note that a Pending operation may also be Ready. fn is_operation_ready(self: @ComponentState, id: felt252) -> bool { Timelock::get_operation_state(self, id) == OperationState::Ready } + /// Returns whether the `id` OperationState is Done or not. fn is_operation_done(self: @ComponentState, id: felt252) -> bool { Timelock::get_operation_state(self, id) == OperationState::Done } + /// Returns the timestamp at which `id` becomes Ready. + /// + /// NOTE: `0` means the OperationState is Unset and `1` means the OperationState + /// is Done. fn get_timestamp(self: @ComponentState, id: felt252) -> u64 { self.TimelockController_timestamps.read(id) } + /// Returns the OperationState for `id`. fn get_operation_state( self: @ComponentState, id: felt252 ) -> OperationState { @@ -154,10 +166,13 @@ mod TimelockControllerComponent { } } + /// Returns the minimum delay in seconds for an operation to become valid. + /// This value can be changed by executing an operation that calls `update_delay`. fn get_min_delay(self: @ComponentState) -> u64 { self.TimelockController_min_delay.read() } + /// Returns the identifier of an operation containing a single transaction. fn hash_operation( self: @ComponentState, call: Call, predecessor: felt252, salt: felt252 ) -> felt252 { @@ -168,6 +183,7 @@ mod TimelockControllerComponent { .finalize() } + /// Returns the identifier of an operation containing a batch of transactions. fn hash_operation_batch( self: @ComponentState, calls: Span, @@ -181,6 +197,14 @@ mod TimelockControllerComponent { .finalize() } + /// Schedule an operation containing a single transaction. + /// + /// Requirements: + /// + /// - the caller must have the `PROPOSER_ROLE` role. + /// + /// Emits `CallScheduled` event. + /// If `salt` is not zero, emits `CallSalt` event. fn schedule( ref self: ComponentState, call: Call, @@ -199,6 +223,14 @@ mod TimelockControllerComponent { } } + /// Schedule an operation containing a batch of transactions. + /// + /// Requirements: + /// + /// - the caller must have the `PROPOSER_ROLE` role. + /// + /// Emits one `CallScheduled` event for each transaction in the batch. + /// If `salt` is not zero, emits `CallSalt` event. fn schedule_batch( ref self: ComponentState, calls: Span, @@ -227,6 +259,14 @@ mod TimelockControllerComponent { } } + /// Cancel an operation. + /// + /// Requirements: + /// + /// - The caller must have the `CANCELLER_ROLE` role. + /// - `id` must be an operation. + /// + /// Emits a `Cancelled` event. fn cancel(ref self: ComponentState, id: felt252) { self.assert_only_role_or_open_role(CANCELLER_ROLE); assert(Timelock::is_operation_pending(@self, id), Errors::UNEXPECTED_OPERATION_STATE); @@ -549,9 +589,29 @@ mod TimelockControllerComponent { impl ERC1155Receiver: ERC1155ReceiverComponent::HasComponent, +Drop > of InternalTrait { - /// Document me... + /// Initializes the contract by registering support as a token receiver for + /// ERC721 and ERC1155 safe transfers. /// + /// This function also configures the contract with the following parameters: /// + /// - `min_delay`: initial minimum delay in seconds for operations. + /// - `proposers`: accounts to be granted proposer and canceller roles. + /// - `executors`: accounts to be granted executor role. + /// - `admin`: optional account to be granted admin role; disable with zero address. + /// + /// WARNING: The optional admin can aid with initial configuration of roles after deployment + /// without being subject to delay, but this role should be subsequently renounced in favor of + /// administration through timelocked proposals. + /// + /// Emits two `RoleGranted` events for each account in `proposers` with `PROPOSER_ROLE` admin + /// `CANCELLER_ROLE` roles. + /// + /// Emits a `RoleGranted` event for each account in `executors` with `EXECUTOR_ROLE` role. + /// + /// May emit a `RoleGranted` event for `admin` with `DEFAULT_ADMIN_ROLE` role (if `admin` is + /// not zero). + /// + /// Emits `MinDelayChange` event. fn initializer( ref self: ComponentState, min_delay: u64, @@ -604,6 +664,9 @@ mod TimelockControllerComponent { self.emit(MinDelayChange { old_duration: 0, new_duration: min_delay }) } + /// Validates that the caller has the given `role`. + /// If `role` is granted to the zero address, then this is considered an open role which + /// allows anyone to be the caller. fn assert_only_role_or_open_role(self: @ComponentState, role: felt252) { let access_component = get_dep_component!(self, AccessControl); let is_role_open = access_component.has_role(role, Zeroable::zero()); @@ -612,6 +675,12 @@ mod TimelockControllerComponent { } } + /// Private function that checks before execution of an operation's calls. + /// + /// Requirements: + /// + /// - `id` must be in the Ready OperationState. + /// - `predecessor` must either be zero or be in the Done OperationState. fn _before_call(self: @ComponentState, id: felt252, predecessor: felt252) { assert(Timelock::is_operation_ready(self, id), Errors::UNEXPECTED_OPERATION_STATE); assert( @@ -620,17 +689,24 @@ mod TimelockControllerComponent { ); } + /// Private functions that checks after execution of an operation's calls. + /// + /// Requirements: + /// + /// - `id` must be in the Ready OperationState. fn _after_call(ref self: ComponentState, id: felt252) { assert(Timelock::is_operation_ready(@self, id), Errors::UNEXPECTED_OPERATION_STATE); self.TimelockController_timestamps.write(id, DONE_TIMESTAMP); } + /// Private function that schedules an operation that is to become valid after a given `delay`. fn _schedule(ref self: ComponentState, id: felt252, delay: u64) { assert(!Timelock::is_operation(@self, id), Errors::UNEXPECTED_OPERATION_STATE); assert(Timelock::get_min_delay(@self) <= delay, Errors::INSUFFICIENT_DELAY); self.TimelockController_timestamps.write(id, starknet::get_block_timestamp() + delay); } + /// Private function that executes an operation's calls. fn _execute(ref self: ComponentState, call: Call) { let Call { to, selector, calldata } = call; starknet::call_contract_syscall(to, selector, calldata).unwrap_syscall(); From f5b484adb983887f8b36db25f9839374d7b344ca Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 2 Jun 2024 16:25:17 -0400 Subject: [PATCH 048/103] remove comments --- src/governance/timelock/interface.cairo | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/governance/timelock/interface.cairo b/src/governance/timelock/interface.cairo index 8b03b35d2..43033b0c2 100644 --- a/src/governance/timelock/interface.cairo +++ b/src/governance/timelock/interface.cairo @@ -1,10 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.13.0 (governance/timelock/interface.cairo) -/// # TimelockController Component -/// -/// - use openzeppelin::governance::timelock::utils::OperationState; use openzeppelin::governance::timelock::utils::call_impls::Call; use starknet::ContractAddress; From 7527d0d4c84aa0f866021d42b4caa5ead54b335e Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 2 Jun 2024 16:25:50 -0400 Subject: [PATCH 049/103] remove comment --- src/tests/mocks/timelock_mocks.cairo | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tests/mocks/timelock_mocks.cairo b/src/tests/mocks/timelock_mocks.cairo index aa8166700..4ca73903f 100644 --- a/src/tests/mocks/timelock_mocks.cairo +++ b/src/tests/mocks/timelock_mocks.cairo @@ -141,7 +141,6 @@ mod TimelockAttackerMock { let reentrant_call = Call { to: this, selector: selector!("reenter"), calldata: array![].span() }; - //let reentrant_call_span = array![reentrant_call].span(); let timelock = ITimelockDispatcher { contract_address: sender }; timelock.execute(reentrant_call, PREDECESSOR, SALT); From cd0352cdd766857414623d151f926d6a8047b545 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 2 Jun 2024 16:26:20 -0400 Subject: [PATCH 050/103] fix fmt --- src/tests/governance/test_timelock.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 043c3f644..59cec9036 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -24,10 +24,10 @@ use openzeppelin::tests::mocks::erc1155_mocks::DualCaseERC1155Mock; use openzeppelin::tests::mocks::erc721_mocks::DualCaseERC721Mock; use openzeppelin::tests::mocks::timelock_mocks::MockContract; use openzeppelin::tests::mocks::timelock_mocks::{ - ITimelockAttackerDispatcher, ITimelockAttackerDispatcherTrait + IMockContractDispatcher, IMockContractDispatcherTrait }; use openzeppelin::tests::mocks::timelock_mocks::{ - IMockContractDispatcher, IMockContractDispatcherTrait + ITimelockAttackerDispatcher, ITimelockAttackerDispatcherTrait }; use openzeppelin::tests::mocks::timelock_mocks::{TimelockControllerMock, TimelockAttackerMock}; use openzeppelin::tests::utils::constants::{ From f638c82bff1ddce2fd958fd970e97a4fa20f6dcd Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 3 Jun 2024 01:19:54 -0400 Subject: [PATCH 051/103] add changelog entries --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 939596808..661407917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- TimelockController component (#996) +- HashCall implementation (#996) +- CallPartialEq (#996) + ## 0.13.0 (2024-05-20) ### Added From a51aae3bd94b909b7fb95659b1fa803e3a880e4b Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 3 Jun 2024 01:22:14 -0400 Subject: [PATCH 052/103] improve spacing --- src/governance/timelock.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/governance/timelock.cairo b/src/governance/timelock.cairo index 34d2163ba..c353d426f 100644 --- a/src/governance/timelock.cairo +++ b/src/governance/timelock.cairo @@ -1,8 +1,8 @@ mod interface; mod timelock_controller; mod utils; + use timelock_controller::TimelockControllerComponent::{ PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE }; - use timelock_controller::TimelockControllerComponent; From a3c03d8b7720dfa528b7f7120d3d6093f3df237a Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 3 Jun 2024 01:24:49 -0400 Subject: [PATCH 053/103] add line break to hash test --- src/tests/governance/test_timelock.cairo | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 59cec9036..96a1a3f3e 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -230,6 +230,7 @@ fn test_hash_operation() { .update_with(predecessor) // predecessor .update_with(salt) // salt .finalize(); + assert_eq!(hashed_operation, expected_hash); } From 39561eead62b35c363277b1aea70509cbc910b81 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 3 Jun 2024 01:32:36 -0400 Subject: [PATCH 054/103] clean up tests --- src/tests/governance/test_timelock.cairo | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 96a1a3f3e..cb38cf193 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -284,12 +284,13 @@ fn schedule_from_proposer(salt: felt252) { let delay = MIN_DELAY; let mut salt = salt; + // Set up call let call = single_operation(target.contract_address); let target_id = timelock.hash_operation(call, predecessor, salt); assert_operation_state(timelock, OperationState::Unset, target_id); + // Schedule testing::set_contract_address(PROPOSER()); - timelock.schedule(call, predecessor, salt, delay); assert_operation_state(timelock, OperationState::Waiting, target_id); @@ -377,12 +378,13 @@ fn schedule_batch_from_proposer(salt: felt252) { let delay = MIN_DELAY; let mut salt = salt; + // Set up calls let calls = batched_operations(target.contract_address); let target_id = timelock.hash_operation_batch(calls, predecessor, salt); assert_operation_state(timelock, OperationState::Unset, target_id); + // Schedule batch testing::set_contract_address(PROPOSER()); - timelock.schedule_batch(calls, predecessor, salt, delay); assert_operation_state(timelock, OperationState::Waiting, target_id); @@ -807,11 +809,11 @@ fn test_execute_batch_reentrant_call() { }; let calls = array![call_1, call_2, reentrant_call].span(); - // schedule + // Schedule testing::set_contract_address(PROPOSER()); timelock.schedule_batch(calls, predecessor, salt, delay); - // fast-forward + // Fast-forward testing::set_block_timestamp(delay); // Grant executor role to attacker @@ -854,12 +856,12 @@ fn test_execute_batch_before_dependency() { let salt = 0; let delay = MIN_DELAY; - // Call 1 + // Calls 1 let calls_1 = batched_operations(target.contract_address); let predecessor_1 = NO_PREDECESSOR; let target_id_1 = timelock.hash_operation_batch(calls_1, predecessor_1, salt); - // Call 2 + // Calls 2 let calls_2 = batched_operations(target.contract_address); let predecessor_2 = target_id_1; @@ -983,7 +985,6 @@ fn test_cancel_unauthorized() { // Schedule testing::set_contract_address(PROPOSER()); timelock.schedule(call, predecessor, salt, delay); - utils::drop_events(timelock.contract_address, 2); // Cancel testing::set_contract_address(OTHER()); From 8764889337e865d6e5b1a380c4b3b67cba883c1c Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 3 Jun 2024 01:33:08 -0400 Subject: [PATCH 055/103] clean up tests --- src/tests/governance/test_timelock.cairo | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index cb38cf193..2dcfb9ff3 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -1075,7 +1075,6 @@ fn test_receive_erc1155_safe_transfer() { let token_id = TOKEN_ID; let token_value = TOKEN_VALUE; - //let data = array![]; // Check initial balances let owner_balance = erc1155.balance_of(owner, token_id); @@ -1235,9 +1234,9 @@ fn test_initializer_min_delay() { // The initializer emits 4 `RoleGranted` events prior to `MinDelayChange`: // - Self administration - // - 1 proposers - // - 1 cancellers - // - 1 executors + // - 1 proposer + // - 1 canceller + // - 1 executor utils::drop_events(ZERO(), 4); assert_only_event_delay_change(ZERO(), 0, MIN_DELAY); } From 6cacdc79074784c633ffc8fe02e1202bfd9509e0 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 3 Jun 2024 01:49:22 -0400 Subject: [PATCH 056/103] fix constants in attacker impl --- src/tests/mocks/timelock_mocks.cairo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tests/mocks/timelock_mocks.cairo b/src/tests/mocks/timelock_mocks.cairo index 4ca73903f..c18608c0f 100644 --- a/src/tests/mocks/timelock_mocks.cairo +++ b/src/tests/mocks/timelock_mocks.cairo @@ -109,11 +109,11 @@ mod TimelockAttackerMock { ITimelockDispatcher, ITimelockDispatcherTrait }; use openzeppelin::governance::timelock::utils::call_impls::Call; - use openzeppelin::tests::utils::constants::SALT; use starknet::ContractAddress; use super::ITimelockAttacker; - const PREDECESSOR: felt252 = 0; + const NO_PREDECESSOR: felt252 = 0; + const NO_SALT: felt252 = 0; #[storage] struct Storage { @@ -143,7 +143,7 @@ mod TimelockAttackerMock { }; let timelock = ITimelockDispatcher { contract_address: sender }; - timelock.execute(reentrant_call, PREDECESSOR, SALT); + timelock.execute(reentrant_call, NO_PREDECESSOR, NO_SALT); } } } From e4fd560f8f16bad1798021905f9ffb8212cfa88c Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 12 Jun 2024 19:29:12 -0500 Subject: [PATCH 057/103] add initializer helper, register access control support --- .../timelock/timelock_controller.cairo | 46 ++++++++++--------- src/tests/governance/test_timelock.cairo | 37 +++++++++++++++ 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 9866b2f7a..607495f67 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -626,8 +626,9 @@ mod TimelockControllerComponent { let mut erc1155_receiver = get_dep_component_mut!(ref self, ERC1155Receiver); erc1155_receiver.initializer(); - // Self administration + // Register access control ID and self as default admin let mut access_component = get_dep_component_mut!(ref self, AccessControl); + access_component.initializer(); access_component._grant_role(DEFAULT_ADMIN_ROLE, starknet::get_contract_address()); // Optional admin @@ -636,30 +637,13 @@ mod TimelockControllerComponent { }; // Register proposers and cancellers - let mut i = 0; - loop { - if i == proposers.len() { - break; - } - - let mut proposer = proposers.at(i); - access_component._grant_role(PROPOSER_ROLE, *proposer); - access_component._grant_role(CANCELLER_ROLE, *proposer); - i += 1; - }; + self._batch_grant_role(PROPOSER_ROLE, proposers); + self._batch_grant_role(CANCELLER_ROLE, proposers); // Register executors - let mut i = 0; - loop { - if i == executors.len() { - break; - } - - let mut executor = executors.at(i); - access_component._grant_role(EXECUTOR_ROLE, *executor); - i += 1; - }; + self._batch_grant_role(EXECUTOR_ROLE, executors); + // Set minimum delay self.TimelockController_min_delay.write(min_delay); self.emit(MinDelayChange { old_duration: 0, new_duration: min_delay }) } @@ -711,5 +695,23 @@ mod TimelockControllerComponent { let Call { to, selector, calldata } = call; starknet::call_contract_syscall(to, selector, calldata).unwrap_syscall(); } + + /// Grants each contract address in `accounts` with `role`. + fn _batch_grant_role( + ref self: ComponentState, role: felt252, accounts: Span + ) { + let mut access_component = get_dep_component_mut!(ref self, AccessControl); + + let mut i = 0; + loop { + if i == accounts.len() { + break; + } + + let mut account = accounts.at(i); + access_component._grant_role(role, *account); + i += 1; + }; + } } } diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 2dcfb9ff3..636dc7554 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -3,6 +3,7 @@ use openzeppelin::access::accesscontrol::AccessControlComponent::{ AccessControlImpl, InternalImpl as AccessControlInternalImpl }; use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; +use openzeppelin::access::accesscontrol::interface::IACCESSCONTROL_ID; use openzeppelin::access::accesscontrol::interface::IAccessControl; use openzeppelin::governance::timelock::TimelockControllerComponent::Call; use openzeppelin::governance::timelock::TimelockControllerComponent::OperationState; @@ -1215,6 +1216,9 @@ fn test_initializer_supported_interfaces() { let supports_ierc721_receiver = contract_state.src5.supports_interface(IERC721_RECEIVER_ID); assert!(supports_ierc721_receiver); + + let supports_access_control = contract_state.src5.supports_interface(IACCESSCONTROL_ID); + assert!(supports_access_control); } #[test] @@ -1303,6 +1307,39 @@ fn test_assert_only_role_or_open_role_with_open_role() { state.assert_only_role_or_open_role(PROPOSER_ROLE); } +// +// _batch_grant_role +// + +#[test] +fn test__batch_grant_role() { + let mut state = COMPONENT_STATE(); + let contract_state = CONTRACT_STATE(); + let (t1, t2, t3) = get_proposers(); + let target_role = 'ROLE'; + + let is_not_supported = !contract_state.access_control.has_role(target_role, t1); + assert!(is_not_supported); + + let is_not_supported = !contract_state.access_control.has_role(target_role, t2); + assert!(is_not_supported); + + let is_not_supported = !contract_state.access_control.has_role(target_role, t3); + assert!(is_not_supported); + + let target_span = array![t1, t2, t3].span(); + state._batch_grant_role(target_role, target_span); + + let is_supported = contract_state.access_control.has_role(target_role, t1); + assert!(is_supported); + + let is_supported = contract_state.access_control.has_role(target_role, t2); + assert!(is_supported); + + let is_supported = contract_state.access_control.has_role(target_role, t3); + assert!(is_supported); +} + // // Helpers // From 3583d38dac6154f7c3cbdc18d36ec4385f1a7f03 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 13 Jun 2024 11:56:52 -0500 Subject: [PATCH 058/103] add _before_call and _after_call tests --- src/tests/governance/test_timelock.cairo | 196 +++++++++++++++++++++++ 1 file changed, 196 insertions(+) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 636dc7554..088b206ea 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -1,3 +1,5 @@ +use openzeppelin::governance::timelock::interface::ITimelock; +use core::starknet::storage::{StorageMemberAccessTrait, StorageMapMemberAccessTrait}; use hash::{HashStateTrait, HashStateExTrait}; use openzeppelin::access::accesscontrol::AccessControlComponent::{ AccessControlImpl, InternalImpl as AccessControlInternalImpl @@ -1307,6 +1309,200 @@ fn test_assert_only_role_or_open_role_with_open_role() { state.assert_only_role_or_open_role(PROPOSER_ROLE); } +// +// _before_call +// + +#[test] +fn test__before_call() { + let mut state = COMPONENT_STATE(); + let predecessor = NO_PREDECESSOR; + + // Mock targets + let target_id = 'TARGET_ID'; + let target_time = MIN_DELAY + starknet::get_block_timestamp(); + + // Set targets in storage + state.TimelockController_timestamps.write(target_id, target_time); + + // Fast-forward + testing::set_block_timestamp(target_time); + + state._before_call(target_id, predecessor); +} + +#[test] +#[should_panic(expected: ('Timelock: unexpected op state',))] +fn test__before_call_nonexistent_operation() { + let mut state = COMPONENT_STATE(); + let predecessor = NO_PREDECESSOR; + + // Mock targets + let target_id = 'TARGET_ID'; + let not_scheduled = 0; + + // Set targets in storage + state.TimelockController_timestamps.write(target_id, not_scheduled); + + state._before_call(target_id, predecessor); +} + +#[test] +#[should_panic(expected: ('Timelock: unexpected op state',))] +fn test__before_call_insufficient_time() { + let mut state = COMPONENT_STATE(); + let predecessor = NO_PREDECESSOR; + + // Mock targets + let target_id = 'TARGET_ID'; + let target_time = MIN_DELAY + starknet::get_block_timestamp(); + + // Set targets in storage + state.TimelockController_timestamps.write(target_id, target_time); + + // Fast-forward + testing::set_block_timestamp(target_time - 1); + + state._before_call(target_id, predecessor); +} + +#[test] +#[should_panic(expected: ('Timelock: unexpected op state',))] +fn test__before_call_when_already_done() { + let mut state = COMPONENT_STATE(); + let predecessor = NO_PREDECESSOR; + + // Mock targets + let target_id = 'TARGET_ID'; + let done_time = 1; + + // Set targets in storage + state.TimelockController_timestamps.write(target_id, done_time); + + // Fast-forward + testing::set_block_timestamp(done_time); + + state._before_call(target_id, predecessor); +} + +#[test] +fn test__before_call_with_predecessor_done() { + let mut state = COMPONENT_STATE(); + + // Mock targets + let target_id = 'TARGET_ID'; + let predecessor_id = 'DONE'; + let done_time = 1; + let target_time = MIN_DELAY + starknet::get_block_timestamp(); + + // Set targets in storage + state.TimelockController_timestamps.write(predecessor_id, done_time); + state.TimelockController_timestamps.write(target_id, target_time); + + // Fast-forward + testing::set_block_timestamp(target_time); + + state._before_call(target_id, predecessor_id); +} + +#[test] +#[should_panic(expected: ('Timelock: awaiting predecessor',))] +fn test__before_call_with_predecessor_not_done() { + let mut state = COMPONENT_STATE(); + + // Mock targets + let target_id = 'TARGET_ID'; + let predecessor_id = 'DONE'; + let not_done_time = 2; + let target_time = MIN_DELAY + starknet::get_block_timestamp(); + + // Set targets in storage + state.TimelockController_timestamps.write(predecessor_id, not_done_time); + state.TimelockController_timestamps.write(target_id, target_time); + + // Fast-forward + testing::set_block_timestamp(target_time); + + state._before_call(target_id, predecessor_id); +} + +// +// _after_call +// + +#[test] +fn test__after_call() { + let mut state = COMPONENT_STATE(); + + // Mock targets + let target_id = 'TARGET_ID'; + let target_time = MIN_DELAY + starknet::get_block_timestamp(); + + // Set targets in storage + state.TimelockController_timestamps.write(target_id, target_time); + + // Fast-forward + testing::set_block_timestamp(target_time); + + state._after_call(target_id); + + // Check timestamp is set to done (1) + let done_ts = 1; + let is_done = state.TimelockController_timestamps.read(target_id); + assert_eq!(is_done, done_ts); +} + +#[test] +#[should_panic(expected: ('Timelock: unexpected op state',))] +fn test__after_call_nonexistent_operation() { + let mut state = COMPONENT_STATE(); + + // Mock targets + let target_id = 'TARGET_ID'; + let not_scheduled = 0; + + // Set targets in storage + state.TimelockController_timestamps.write(target_id, not_scheduled); + + state._after_call(target_id); +} + +#[test] +#[should_panic(expected: ('Timelock: unexpected op state',))] +fn test__after_call_insufficient_time() { + let mut state = COMPONENT_STATE(); + + // Mock targets + let target_id = 'TARGET_ID'; + let target_time = MIN_DELAY + starknet::get_block_timestamp(); + + // Set targets in storage + state.TimelockController_timestamps.write(target_id, target_time); + + // Fast-forward + testing::set_block_timestamp(target_time - 1); + + state._after_call(target_id); +} + +#[test] +#[should_panic(expected: ('Timelock: unexpected op state',))] +fn test__after_call_already_done() { + let mut state = COMPONENT_STATE(); + + // Mock targets + let target_id = 'TARGET_ID'; + let done_time = 1; + + // Set targets in storage + state.TimelockController_timestamps.write(target_id, done_time); + + // Fast-forward + testing::set_block_timestamp(done_time); + + state._after_call(target_id); +} + // // _batch_grant_role // From e878caee0850edb05733734093bc121efa05a3b5 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 13 Jun 2024 14:23:35 -0500 Subject: [PATCH 059/103] fix reentrant batch mock call --- src/tests/governance/test_timelock.cairo | 8 +++----- src/tests/mocks/timelock_mocks.cairo | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 088b206ea..c9a552d94 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -799,18 +799,16 @@ fn test_execute_batch_unauthorized() { ) )] fn test_execute_batch_reentrant_call() { - let (mut timelock, mut target) = setup_dispatchers(); + let mut timelock = deploy_timelock(); let mut attacker = deploy_attacker(); let predecessor = NO_PREDECESSOR; let salt = 0; let delay = MIN_DELAY; - let call_1 = single_operation(target.contract_address); - let call_2 = single_operation(target.contract_address); let reentrant_call = Call { - to: attacker.contract_address, selector: selector!("reenter"), calldata: array![].span() + to: attacker.contract_address, selector: selector!("reenter_batch"), calldata: array![].span() }; - let calls = array![call_1, call_2, reentrant_call].span(); + let calls = array![reentrant_call].span(); // Schedule testing::set_contract_address(PROPOSER()); diff --git a/src/tests/mocks/timelock_mocks.cairo b/src/tests/mocks/timelock_mocks.cairo index c18608c0f..a35328573 100644 --- a/src/tests/mocks/timelock_mocks.cairo +++ b/src/tests/mocks/timelock_mocks.cairo @@ -101,6 +101,7 @@ mod MockContract { #[starknet::interface] trait ITimelockAttacker { fn reenter(ref self: TState); + fn reenter_batch(ref self: TState); } #[starknet::contract] @@ -146,5 +147,28 @@ mod TimelockAttackerMock { timelock.execute(reentrant_call, NO_PREDECESSOR, NO_SALT); } } + + fn reenter_batch(ref self: ContractState) { + let new_balance = self.balance.read() + 1; + self.balance.write(new_balance); + + let sender = starknet::get_caller_address(); + let this = starknet::get_contract_address(); + + let current_count = self.count.read(); + if current_count != 2 { + self.count.write(current_count + 1); + + let reentrant_call = Call { + to: this, selector: selector!("reenter_batch"), calldata: array![].span() + }; + + let calls = array![reentrant_call].span(); + + let timelock = ITimelockDispatcher { contract_address: sender }; + timelock.execute_batch(calls, NO_PREDECESSOR, NO_SALT); + } + } + } } From 70f44f4c6d6f54f9389fdd57d1ab88db97cfbd43 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 13 Jun 2024 15:14:55 -0500 Subject: [PATCH 060/103] add _schedule and _execute tests --- src/tests/governance/test_timelock.cairo | 114 +++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index c9a552d94..dfb886b12 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -1501,6 +1501,120 @@ fn test__after_call_already_done() { state._after_call(target_id); } +// +// _schedule +// + +#[test] +fn test__schedule() { + let mut state = COMPONENT_STATE(); + let mut target = deploy_mock_target(); + let predecessor = NO_PREDECESSOR; + let delay = MIN_DELAY; + let mut salt = 0; + + // Set up call + let call = single_operation(target.contract_address); + let target_id = state.hash_operation(call, predecessor, salt); + + // Schedule + state._schedule(target_id, delay); + + let actual_ts = state.TimelockController_timestamps.read(target_id); + let expected_ts = starknet::get_block_timestamp() + delay; + assert_eq!(actual_ts, expected_ts); +} + +#[test] +#[should_panic(expected: ('Timelock: unexpected op state',))] +fn test__schedule_overwrite() { + let mut state = COMPONENT_STATE(); + let mut target = deploy_mock_target(); + let predecessor = NO_PREDECESSOR; + let delay = MIN_DELAY; + let mut salt = 0; + + // Set up call + let call = single_operation(target.contract_address); + let target_id = state.hash_operation(call, predecessor, salt); + + // Schedule and overwrite + state._schedule(target_id, delay); + state._schedule(target_id, delay); +} + +#[test] +#[should_panic(expected: ('Timelock: insufficient delay',))] +fn test__schedule_bad_delay() { + let mut state = COMPONENT_STATE(); + let mut target = deploy_mock_target(); + let predecessor = NO_PREDECESSOR; + let mut salt = 0; + let delay = MIN_DELAY; + + // Set up call + let call = single_operation(target.contract_address); + let target_id = state.hash_operation(call, predecessor, salt); + + // Set min delay + state.TimelockController_min_delay.write(delay); + + // Schedule with bad delay + state._schedule(target_id, delay - 1); +} + +// +// _execute +// + +#[test] +fn test__execute() { + let mut state = COMPONENT_STATE(); + let mut target = deploy_mock_target(); + + // Set up call + let call = single_operation(target.contract_address); + + let storage_num = target.get_number(); + let expected_num = 0; + assert_eq!(storage_num, expected_num); + + // Execute + state._execute(call); + + let storage_num = target.get_number(); + let expected_num = VALUE; + assert_eq!(storage_num, expected_num); +} + +#[test] +#[should_panic(expected: ('Expected failure', 'ENTRYPOINT_FAILED',))] +fn test__execute_with_failing_tx() { + let mut state = COMPONENT_STATE(); + let mut target = deploy_mock_target(); + + // Set up call + let call = failing_operation(target.contract_address); + + // Execute failing tx + state._execute(call); +} + +#[test] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test__execute_with_bad_selector() { + let mut state = COMPONENT_STATE(); + let mut target = deploy_mock_target(); + + // Set up call + let bad_selector_call = Call { + to: target.contract_address, selector: selector!("bad_selector"), calldata: array![].span() + }; + + // Execute call with bad selector + state._execute(bad_selector_call); +} + // // _batch_grant_role // From 4018bbe05d9f1741800f14b8ed778127ffbb1352 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 13 Jun 2024 17:47:35 -0500 Subject: [PATCH 061/103] add timelock description --- .../timelock/timelock_controller.cairo | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 607495f67..9b714fb5b 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -3,7 +3,14 @@ /// # Timelock Controller Component /// +/// Component that acts as a timelocked controller. When set as the owner of an `Ownable` smart contract, +/// it enforces a timelock on all `only_owner` maintenance operations. This gives time for users +/// of the controlled contract to exit before a potentially dangerous maintenance operation is applied. /// +/// By default, this component is self administered, meaning administration tasks have to go through +/// the timelock process. The proposer role is in charge of proposing operations. A common use case +/// is to position the timelock controller as the owner of a smart contract, with a multi-sig +/// or a DAO as the sole proposer. #[starknet::component] mod TimelockControllerComponent { use hash::{HashStateTrait, HashStateExTrait}; @@ -275,6 +282,19 @@ mod TimelockControllerComponent { self.emit(Cancelled { id }); } + /// Execute a (Ready) operation containing a single Call. + /// + /// Requirements: + /// + /// - Caller must have `EXECUTOR_ROLE`. + /// - `id` must be in Ready OperationState. + /// - `predecessor` must either be `0` or in Done OperationState. + /// + /// NOTE: This function can reenter, but it doesn't pose a risk because `_after_call` + /// checks that the proposal is pending, thus any modifications to the operation during + /// reentrancy should be caught. + /// + /// Emits a `CallExecuted` event. fn execute( ref self: ComponentState, call: Call, @@ -290,6 +310,19 @@ mod TimelockControllerComponent { self._after_call(id); } + /// Execute a (Ready) operation containing a batch of Calls. + /// + /// Requirements: + /// + /// - Caller must have `EXECUTOR_ROLE`. + /// - `id` must be in Ready OperationState. + /// - `predecessor` must either be `0` or in Done OperationState. + /// + /// NOTE: This function can reenter, but it doesn't pose a risk because `_after_call` + /// checks that the proposal is pending, thus any modifications to the operation during + /// reentrancy should be caught. + /// + /// Emits a `CallExecuted` event for each Call. fn execute_batch( ref self: ComponentState, calls: Span, @@ -316,6 +349,15 @@ mod TimelockControllerComponent { self._after_call(id); } + /// Changes the minimum timelock duration for future operations. + /// + /// Requirements: + /// + /// - The caller must be the timelock itself. This can only be achieved by scheduling + /// and later executing an operation where the timelock is the target and the data + /// is the ABI-encoded call to this function. + /// + /// Emits a `MinDelayChange` event. fn update_delay(ref self: ComponentState, new_delay: u64) { let this = starknet::get_contract_address(); let caller = starknet::get_caller_address(); From 29e08650b4515523e0551a86aabb3c1ca1a3d0a8 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 13 Jun 2024 18:05:19 -0500 Subject: [PATCH 062/103] fix formatting --- src/tests/governance/test_timelock.cairo | 7 ++++--- src/tests/mocks/timelock_mocks.cairo | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index dfb886b12..c36c6f2e0 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -1,5 +1,3 @@ -use openzeppelin::governance::timelock::interface::ITimelock; -use core::starknet::storage::{StorageMemberAccessTrait, StorageMapMemberAccessTrait}; use hash::{HashStateTrait, HashStateExTrait}; use openzeppelin::access::accesscontrol::AccessControlComponent::{ AccessControlImpl, InternalImpl as AccessControlInternalImpl @@ -47,6 +45,7 @@ use openzeppelin::utils::serde::SerializedAppend; use poseidon::PoseidonTrait; use starknet::ContractAddress; use starknet::contract_address_const; +use starknet::storage::{StorageMemberAccessTrait, StorageMapMemberAccessTrait}; use starknet::testing; type ComponentState = @@ -806,7 +805,9 @@ fn test_execute_batch_reentrant_call() { let delay = MIN_DELAY; let reentrant_call = Call { - to: attacker.contract_address, selector: selector!("reenter_batch"), calldata: array![].span() + to: attacker.contract_address, + selector: selector!("reenter_batch"), + calldata: array![].span() }; let calls = array![reentrant_call].span(); diff --git a/src/tests/mocks/timelock_mocks.cairo b/src/tests/mocks/timelock_mocks.cairo index a35328573..37627b8d3 100644 --- a/src/tests/mocks/timelock_mocks.cairo +++ b/src/tests/mocks/timelock_mocks.cairo @@ -169,6 +169,5 @@ mod TimelockAttackerMock { timelock.execute_batch(calls, NO_PREDECESSOR, NO_SALT); } } - } } From ca5b0dd7a0deabf8f332bd0b792119319a7e1f86 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 13 Jun 2024 18:13:29 -0500 Subject: [PATCH 063/103] fix comments --- src/tests/governance/test_timelock.cairo | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index c36c6f2e0..7883d502f 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -1388,10 +1388,12 @@ fn test__before_call_when_already_done() { fn test__before_call_with_predecessor_done() { let mut state = COMPONENT_STATE(); - // Mock targets - let target_id = 'TARGET_ID'; + // Mock `Done` predecessor let predecessor_id = 'DONE'; let done_time = 1; + + // Mock targets + let target_id = 'TARGET_ID'; let target_time = MIN_DELAY + starknet::get_block_timestamp(); // Set targets in storage @@ -1409,10 +1411,12 @@ fn test__before_call_with_predecessor_done() { fn test__before_call_with_predecessor_not_done() { let mut state = COMPONENT_STATE(); - // Mock targets - let target_id = 'TARGET_ID'; + // Mock awaiting predecessor let predecessor_id = 'DONE'; let not_done_time = 2; + + // Mock targets + let target_id = 'TARGET_ID'; let target_time = MIN_DELAY + starknet::get_block_timestamp(); // Set targets in storage From 0909f9f1730f52d373904be624da26237e82356d Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 13 Jun 2024 18:22:22 -0500 Subject: [PATCH 064/103] fix comment --- src/governance/timelock/timelock_controller.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 9b714fb5b..1638d749f 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -131,8 +131,8 @@ mod TimelockControllerComponent { Timelock::get_operation_state(self, id) != OperationState::Unset } - /// Returns whether the `id` OperationState is Pending or not. - /// Note that a Pending operation may also be Ready. + /// Returns whether the `id` OperationState is Waiting or not. + /// Note that a Waiting operation may also be Ready. fn is_operation_pending(self: @ComponentState, id: felt252) -> bool { let state = Timelock::get_operation_state(self, id); state == OperationState::Waiting || state == OperationState::Ready From ae45e5b59a046efdfdc9bd9630528277c72fae57 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 13 Jun 2024 19:19:41 -0500 Subject: [PATCH 065/103] fmt --- src/governance/timelock/timelock_controller.cairo | 2 +- src/governance/timelock/utils.cairo | 1 + src/governance/timelock/utils/call_impls.cairo | 4 +++- src/tests/governance/test_timelock.cairo | 5 ++--- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 5b868ff7b..94f52a1bb 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -13,8 +13,8 @@ /// or a DAO as the sole proposer. #[starknet::component] pub mod TimelockControllerComponent { - use core::num::traits::Zero; use core::hash::{HashStateTrait, HashStateExTrait}; + use core::num::traits::Zero; use core::poseidon::PoseidonTrait; use openzeppelin::access::accesscontrol::AccessControlComponent::InternalTrait as AccessControlInternalTrait; use openzeppelin::access::accesscontrol::AccessControlComponent::{ diff --git a/src/governance/timelock/utils.cairo b/src/governance/timelock/utils.cairo index dd3003f85..c9824f6b1 100644 --- a/src/governance/timelock/utils.cairo +++ b/src/governance/timelock/utils.cairo @@ -1,4 +1,5 @@ pub mod call_impls; pub mod operation_state; +pub use call_impls::Call; pub use operation_state::OperationState; diff --git a/src/governance/timelock/utils/call_impls.cairo b/src/governance/timelock/utils/call_impls.cairo index 6604d37be..4a2d86d32 100644 --- a/src/governance/timelock/utils/call_impls.cairo +++ b/src/governance/timelock/utils/call_impls.cairo @@ -13,7 +13,9 @@ pub struct Call { pub calldata: Span } -pub(crate) impl HashCallImpl, +HashStateTrait, +Drop> of Hash<@Call, S> { +pub(crate) impl HashCallImpl< + Call, S, +Serde, +HashStateTrait, +Drop +> of Hash<@Call, S> { fn update_state(mut state: S, value: @Call) -> S { let mut arr = array![]; Serde::serialize(value, ref arr); diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index f9148d117..971f125de 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -1,5 +1,5 @@ -use core::num::traits::Zero; use core::hash::{HashStateTrait, HashStateExTrait}; +use core::num::traits::Zero; use core::poseidon::PoseidonTrait; use openzeppelin::access::accesscontrol::AccessControlComponent::{ AccessControlImpl, InternalImpl as AccessControlInternalImpl @@ -7,8 +7,6 @@ use openzeppelin::access::accesscontrol::AccessControlComponent::{ use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin::access::accesscontrol::interface::IACCESSCONTROL_ID; use openzeppelin::access::accesscontrol::interface::IAccessControl; -use openzeppelin::governance::timelock::utils::call_impls::Call; -use openzeppelin::governance::timelock::utils::operation_state::OperationState; use openzeppelin::governance::timelock::TimelockControllerComponent::{ CallScheduled, CallExecuted, CallSalt, Cancelled, MinDelayChange }; @@ -19,6 +17,7 @@ use openzeppelin::governance::timelock::TimelockControllerComponent; use openzeppelin::governance::timelock::interface::{ TimelockABIDispatcher, TimelockABIDispatcherTrait }; +use openzeppelin::governance::timelock::utils::{Call, OperationState}; use openzeppelin::governance::timelock::{PROPOSER_ROLE, EXECUTOR_ROLE, CANCELLER_ROLE}; use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; From 3e533e13b3f4d71c0ad09d4b29da2446cc32608d Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 13 Jun 2024 20:41:55 -0500 Subject: [PATCH 066/103] tidy up tests --- src/tests/governance/test_timelock.cairo | 106 +++++++++++++++++------ 1 file changed, 80 insertions(+), 26 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 971f125de..9a7636963 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -121,17 +121,16 @@ fn failing_operation(target: ContractAddress) -> Call { Call { to: target, selector: selector!("failing_function"), calldata: calldata.span() } } +fn operation_with_bad_selector(target: ContractAddress) -> Call { + let mut calldata = array![]; + + Call { to: target, selector: selector!("bad_selector"), calldata: calldata.span() } +} + // // Dispatchers // -fn setup_dispatchers() -> (TimelockABIDispatcher, IMockContractDispatcher) { - let timelock = deploy_timelock(); - let target = deploy_mock_target(); - - (timelock, target) -} - fn deploy_timelock() -> TimelockABIDispatcher { let mut calldata = array![]; @@ -152,6 +151,27 @@ fn deploy_timelock() -> TimelockABIDispatcher { TimelockABIDispatcher { contract_address: address } } +fn deploy_mock_target() -> IMockContractDispatcher { + let mut calldata = array![]; + + let address = utils::deploy(MockContract::TEST_CLASS_HASH, calldata); + IMockContractDispatcher { contract_address: address } +} + +fn setup_dispatchers() -> (TimelockABIDispatcher, IMockContractDispatcher) { + let timelock = deploy_timelock(); + let target = deploy_mock_target(); + + (timelock, target) +} + +fn deploy_attacker() -> ITimelockAttackerDispatcher { + let mut calldata = array![]; + + let address = utils::deploy(TimelockAttackerMock::TEST_CLASS_HASH, calldata); + ITimelockAttackerDispatcher { contract_address: address } +} + fn deploy_erc721() -> IERC721Dispatcher { let mut calldata = array![]; @@ -188,20 +208,6 @@ fn setup_account() -> ContractAddress { utils::deploy(SnakeAccountMock::TEST_CLASS_HASH, calldata) } -fn deploy_mock_target() -> IMockContractDispatcher { - let mut calldata = array![]; - - let address = utils::deploy(MockContract::TEST_CLASS_HASH, calldata); - IMockContractDispatcher { contract_address: address } -} - -fn deploy_attacker() -> ITimelockAttackerDispatcher { - let mut calldata = array![]; - - let address = utils::deploy(TimelockAttackerMock::TEST_CLASS_HASH, calldata); - ITimelockAttackerDispatcher { contract_address: address } -} - // // hash_operation // @@ -212,7 +218,7 @@ fn test_hash_operation() { let predecessor = 123; let salt = SALT; - // Setup call + // Set up call let mut calldata = array![]; calldata.append_serde(VALUE); let mut call = Call { @@ -242,7 +248,7 @@ fn test_hash_operation_batch() { let predecessor = 123; let salt = SALT; - // Setup calls + // Set up calls let mut calldata = array![]; calldata.append_serde(VALUE); let mut call = Call { @@ -566,6 +572,56 @@ fn test_execute_unauthorized() { timelock.execute(call, predecessor, salt); } +#[test] +#[should_panic(expected: ('Expected failure', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED'))] +fn test_execute_failing_tx() { + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = 0; + let delay = MIN_DELAY; + + // Set up call + let call = failing_operation(target.contract_address); + let target_id = timelock.hash_operation(call, predecessor, salt); + + // Schedule + testing::set_contract_address(PROPOSER()); + timelock.schedule(call, predecessor, salt, delay); + + // Fast-forward + testing::set_block_timestamp(delay); + assert_operation_state(timelock, OperationState::Ready, target_id); + + // Execute + testing::set_contract_address(EXECUTOR()); + timelock.execute(call, predecessor, salt); +} + +#[test] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED'))] +fn test_execute_bad_selector() { + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = 0; + let delay = MIN_DELAY; + + // Set up call + let call = operation_with_bad_selector(target.contract_address); + let target_id = timelock.hash_operation(call, predecessor, salt); + + // Schedule + testing::set_contract_address(PROPOSER()); + timelock.schedule(call, predecessor, salt, delay); + + // Fast-forward + testing::set_block_timestamp(delay); + assert_operation_state(timelock, OperationState::Ready, target_id); + + // Execute + testing::set_contract_address(EXECUTOR()); + timelock.execute(call, predecessor, salt); +} + #[test] #[should_panic( expected: ( @@ -1612,9 +1668,7 @@ fn test__execute_with_bad_selector() { let mut target = deploy_mock_target(); // Set up call - let bad_selector_call = Call { - to: target.contract_address, selector: selector!("bad_selector"), calldata: array![].span() - }; + let bad_selector_call = operation_with_bad_selector(target.contract_address); // Execute call with bad selector state._execute(bad_selector_call); From e6dc70703384e1b19ccc6513d9c3adf1dda4f6a4 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 13 Jun 2024 20:49:04 -0500 Subject: [PATCH 067/103] remove batch helper fn --- .../timelock/timelock_controller.cairo | 42 +++++++++---------- src/tests/governance/test_timelock.cairo | 33 --------------- 2 files changed, 21 insertions(+), 54 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 94f52a1bb..96dee2cca 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -680,11 +680,29 @@ pub mod TimelockControllerComponent { }; // Register proposers and cancellers - self._batch_grant_role(PROPOSER_ROLE, proposers); - self._batch_grant_role(CANCELLER_ROLE, proposers); + let mut i = 0; + loop { + if i == proposers.len() { + break; + } + + let mut proposer = proposers.at(i); + access_component._grant_role(PROPOSER_ROLE, *proposer); + access_component._grant_role(CANCELLER_ROLE, *proposer); + i += 1; + }; // Register executors - self._batch_grant_role(EXECUTOR_ROLE, executors); + let mut i = 0; + loop { + if i == executors.len() { + break; + } + + let mut executor = executors.at(i); + access_component._grant_role(EXECUTOR_ROLE, *executor); + i += 1; + }; // Set minimum delay self.TimelockController_min_delay.write(min_delay); @@ -738,23 +756,5 @@ pub mod TimelockControllerComponent { let Call { to, selector, calldata } = call; starknet::syscalls::call_contract_syscall(to, selector, calldata).unwrap_syscall(); } - - /// Grants each contract address in `accounts` with `role`. - fn _batch_grant_role( - ref self: ComponentState, role: felt252, accounts: Span - ) { - let mut access_component = get_dep_component_mut!(ref self, AccessControl); - - let mut i = 0; - loop { - if i == accounts.len() { - break; - } - - let mut account = accounts.at(i); - access_component._grant_role(role, *account); - i += 1; - }; - } } } diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 9a7636963..54811953c 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -1674,39 +1674,6 @@ fn test__execute_with_bad_selector() { state._execute(bad_selector_call); } -// -// _batch_grant_role -// - -#[test] -fn test__batch_grant_role() { - let mut state = COMPONENT_STATE(); - let contract_state = CONTRACT_STATE(); - let (t1, t2, t3) = get_proposers(); - let target_role = 'ROLE'; - - let is_not_supported = !contract_state.access_control.has_role(target_role, t1); - assert!(is_not_supported); - - let is_not_supported = !contract_state.access_control.has_role(target_role, t2); - assert!(is_not_supported); - - let is_not_supported = !contract_state.access_control.has_role(target_role, t3); - assert!(is_not_supported); - - let target_span = array![t1, t2, t3].span(); - state._batch_grant_role(target_role, target_span); - - let is_supported = contract_state.access_control.has_role(target_role, t1); - assert!(is_supported); - - let is_supported = contract_state.access_control.has_role(target_role, t2); - assert!(is_supported); - - let is_supported = contract_state.access_control.has_role(target_role, t3); - assert!(is_supported); -} - // // Helpers // From b92165ae23a93aed6afe6103b4422e93d34a5345 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 13 Jun 2024 20:55:03 -0500 Subject: [PATCH 068/103] remove event from mocks --- src/tests/mocks/timelock_mocks.cairo | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/tests/mocks/timelock_mocks.cairo b/src/tests/mocks/timelock_mocks.cairo index 990cc7800..aeeea6b34 100644 --- a/src/tests/mocks/timelock_mocks.cairo +++ b/src/tests/mocks/timelock_mocks.cairo @@ -78,10 +78,6 @@ pub(crate) mod MockContract { number: felt252, } - #[event] - #[derive(Drop, starknet::Event)] - enum Event {} - #[abi(embed_v0)] impl MockContractImpl of IMockContract { fn set_number(ref self: ContractState, new_number: felt252) { @@ -122,10 +118,6 @@ pub(crate) mod TimelockAttackerMock { count: felt252 } - #[event] - #[derive(Drop, starknet::Event)] - enum Event {} - #[abi(embed_v0)] impl TimelockAttackerImpl of ITimelockAttacker { fn reenter(ref self: ContractState) { From 8b2978842d750009ba990700e0cde677404a6215 Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Thu, 20 Jun 2024 11:39:51 -0500 Subject: [PATCH 069/103] Apply suggestions from code review Co-authored-by: Eric Nordelo --- src/governance/timelock/interface.cairo | 2 +- src/governance/timelock/timelock_controller.cairo | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/governance/timelock/interface.cairo b/src/governance/timelock/interface.cairo index 23a4e85c8..cb1323a22 100644 --- a/src/governance/timelock/interface.cairo +++ b/src/governance/timelock/interface.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.13.0 (governance/timelock/interface.cairo) +// OpenZeppelin Contracts for Cairo v0.14.0 (governance/timelock/interface.cairo) use openzeppelin::governance::timelock::utils::OperationState; use openzeppelin::governance::timelock::utils::call_impls::Call; diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 96dee2cca..c8c42f278 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.13.0 (governance/timelock/timelock_controller.cairo) -/// # Timelock Controller Component +/// # TimelockController Component /// /// Component that acts as a timelocked controller. When set as the owner of an `Ownable` smart contract, /// it enforces a timelock on all `only_owner` maintenance operations. This gives time for users From 7dddac96ec90f0850f809bcec0dcab9d67c2f929 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 20 Jun 2024 11:41:04 -0500 Subject: [PATCH 070/103] update spdx --- src/governance/timelock/timelock_controller.cairo | 2 +- src/governance/timelock/utils/call_impls.cairo | 2 +- src/governance/timelock/utils/operation_state.cairo | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index c8c42f278..c3e2bb31c 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.13.0 (governance/timelock/timelock_controller.cairo) +// OpenZeppelin Contracts for Cairo v0.14.0 (governance/timelock/timelock_controller.cairo) /// # TimelockController Component /// diff --git a/src/governance/timelock/utils/call_impls.cairo b/src/governance/timelock/utils/call_impls.cairo index 4a2d86d32..63c7b62c6 100644 --- a/src/governance/timelock/utils/call_impls.cairo +++ b/src/governance/timelock/utils/call_impls.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.13.0 (governance/timelock/utils/call_impls.cairo) +// OpenZeppelin Contracts for Cairo v0.14.0 (governance/timelock/utils/call_impls.cairo) use core::hash::{HashStateTrait, HashStateExTrait, Hash}; use starknet::ContractAddress; diff --git a/src/governance/timelock/utils/operation_state.cairo b/src/governance/timelock/utils/operation_state.cairo index 0846d7a77..0fabf1db1 100644 --- a/src/governance/timelock/utils/operation_state.cairo +++ b/src/governance/timelock/utils/operation_state.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.13.0 (governance/timelock/utils/operation_state.cairo) +// OpenZeppelin Contracts for Cairo v0.14.0 (governance/timelock/utils/operation_state.cairo) use core::fmt::{Debug, Formatter, Error}; From 996123ba8d330cd7f20d319abae03039e7f5dbb0 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 21 Jun 2024 11:32:50 -0500 Subject: [PATCH 071/103] remove token receiver support --- src/governance/timelock/interface.cairo | 54 ------- .../timelock/timelock_controller.cairo | 102 +----------- src/tests/governance/test_timelock.cairo | 153 +----------------- 3 files changed, 2 insertions(+), 307 deletions(-) diff --git a/src/governance/timelock/interface.cairo b/src/governance/timelock/interface.cairo index cb1323a22..0d41ba073 100644 --- a/src/governance/timelock/interface.cairo +++ b/src/governance/timelock/interface.cairo @@ -66,58 +66,4 @@ pub trait TimelockABI { fn grantRole(ref self: TState, role: felt252, account: ContractAddress); fn revokeRole(ref self: TState, role: felt252, account: ContractAddress); fn renounceRole(ref self: TState, role: felt252, account: ContractAddress); - - // IERC721Receiver - fn on_erc721_received( - self: @TState, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - data: Span - ) -> felt252; - - // IERC721ReceiverCamel - fn onERC721Received( - self: @TState, - operator: ContractAddress, - from: ContractAddress, - tokenId: u256, - data: Span - ) -> felt252; - - // IERC1155Receiver - fn on_erc1155_received( - self: @TState, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - value: u256, - data: Span - ) -> felt252; - fn on_erc1155_batch_received( - self: @TState, - operator: ContractAddress, - from: ContractAddress, - token_ids: Span, - values: Span, - data: Span - ) -> felt252; - - // IERC1155ReceiverCamel - fn onERC1155Received( - self: @TState, - operator: ContractAddress, - from: ContractAddress, - tokenId: u256, - value: u256, - data: Span - ) -> felt252; - fn onERC1155BatchReceived( - self: @TState, - operator: ContractAddress, - from: ContractAddress, - tokenIds: Span, - values: Span, - data: Span - ) -> felt252; } diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index c3e2bb31c..8ff7b61e1 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -27,18 +27,6 @@ pub mod TimelockControllerComponent { use openzeppelin::governance::timelock::utils::call_impls::{HashCallImpl, Call}; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; use openzeppelin::introspection::src5::SRC5Component; - use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent::{ - ERC1155ReceiverImpl, ERC1155ReceiverCamelImpl - }; - use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent::{ - InternalImpl as ERC1155InternalImpl - }; - use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent; - use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent::InternalImpl as ERC721ReceiverInternalImpl; - use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent::{ - ERC721ReceiverImpl, ERC721ReceiverCamelImpl - }; - use openzeppelin::token::erc721::erc721_receiver::ERC721ReceiverComponent; use starknet::ContractAddress; use starknet::SyscallResultTrait; @@ -122,8 +110,6 @@ pub mod TimelockControllerComponent { +HasComponent, +SRC5Component::HasComponent, +AccessControlComponent::HasComponent, - +ERC721ReceiverComponent::HasComponent, - +ERC1155ReceiverComponent::HasComponent, +Drop > of ITimelock> { /// Returns whether `id` corresponds to a registered operation. @@ -377,8 +363,6 @@ pub mod TimelockControllerComponent { +HasComponent, impl SRC5: SRC5Component::HasComponent, impl AccessControl: AccessControlComponent::HasComponent, - impl ERC721Receiver: ERC721ReceiverComponent::HasComponent, - impl ERC1155Receiver: ERC1155ReceiverComponent::HasComponent, +Drop > of TimelockABI> { fn is_operation(self: @ComponentState, id: felt252) -> bool { @@ -546,80 +530,6 @@ pub mod TimelockControllerComponent { let mut access_control = get_dep_component_mut!(ref self, AccessControl); access_control.renounceRole(role, account); } - - // IERC721Receiver - fn on_erc721_received( - self: @ComponentState, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - data: Span - ) -> felt252 { - let erc721_receiver = get_dep_component!(self, ERC721Receiver); - erc721_receiver.on_erc721_received(operator, from, token_id, data) - } - - // IERC721ReceiverCamel - fn onERC721Received( - self: @ComponentState, - operator: ContractAddress, - from: ContractAddress, - tokenId: u256, - data: Span - ) -> felt252 { - let erc721_receiver = get_dep_component!(self, ERC721Receiver); - erc721_receiver.onERC721Received(operator, from, tokenId, data) - } - - // IERC1155Receiver - fn on_erc1155_received( - self: @ComponentState, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - value: u256, - data: Span - ) -> felt252 { - let erc1155_receiver = get_dep_component!(self, ERC1155Receiver); - erc1155_receiver.on_erc1155_received(operator, from, token_id, value, data) - } - - fn on_erc1155_batch_received( - self: @ComponentState, - operator: ContractAddress, - from: ContractAddress, - token_ids: Span, - values: Span, - data: Span - ) -> felt252 { - let erc1155_receiver = get_dep_component!(self, ERC1155Receiver); - erc1155_receiver.on_erc1155_batch_received(operator, from, token_ids, values, data) - } - - // IERC1155ReceiverCamel - fn onERC1155Received( - self: @ComponentState, - operator: ContractAddress, - from: ContractAddress, - tokenId: u256, - value: u256, - data: Span - ) -> felt252 { - let erc1155_receiver = get_dep_component!(self, ERC1155Receiver); - erc1155_receiver.onERC1155Received(operator, from, tokenId, value, data) - } - - fn onERC1155BatchReceived( - self: @ComponentState, - operator: ContractAddress, - from: ContractAddress, - tokenIds: Span, - values: Span, - data: Span - ) -> felt252 { - let erc1155_receiver = get_dep_component!(self, ERC1155Receiver); - erc1155_receiver.onERC1155BatchReceived(operator, from, tokenIds, values, data) - } } #[generate_trait] @@ -628,12 +538,9 @@ pub mod TimelockControllerComponent { +HasComponent, impl SRC5: SRC5Component::HasComponent, impl AccessControl: AccessControlComponent::HasComponent, - impl ERC721Receiver: ERC721ReceiverComponent::HasComponent, - impl ERC1155Receiver: ERC1155ReceiverComponent::HasComponent, +Drop > of InternalTrait { - /// Initializes the contract by registering support as a token receiver for - /// ERC721 and ERC1155 safe transfers. + /// Initializes the contract by registering support for SRC5 and AccessControl. /// /// This function also configures the contract with the following parameters: /// @@ -662,13 +569,6 @@ pub mod TimelockControllerComponent { executors: Span, admin: ContractAddress ) { - // Register as token receivers - let mut erc721_receiver = get_dep_component_mut!(ref self, ERC721Receiver); - erc721_receiver.initializer(); - - let mut erc1155_receiver = get_dep_component_mut!(ref self, ERC1155Receiver); - erc1155_receiver.initializer(); - // Register access control ID and self as default admin let mut access_component = get_dep_component_mut!(ref self, AccessControl); access_component.initializer(); diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 54811953c..5cc2cb36d 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -21,9 +21,6 @@ use openzeppelin::governance::timelock::utils::{Call, OperationState}; use openzeppelin::governance::timelock::{PROPOSER_ROLE, EXECUTOR_ROLE, CANCELLER_ROLE}; use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; -use openzeppelin::tests::mocks::account_mocks::SnakeAccountMock; -use openzeppelin::tests::mocks::erc1155_mocks::DualCaseERC1155Mock; -use openzeppelin::tests::mocks::erc721_mocks::DualCaseERC721Mock; use openzeppelin::tests::mocks::timelock_mocks::MockContract; use openzeppelin::tests::mocks::timelock_mocks::{ IMockContractDispatcher, IMockContractDispatcherTrait @@ -32,15 +29,8 @@ use openzeppelin::tests::mocks::timelock_mocks::{ ITimelockAttackerDispatcher, ITimelockAttackerDispatcherTrait }; use openzeppelin::tests::mocks::timelock_mocks::{TimelockControllerMock, TimelockAttackerMock}; -use openzeppelin::tests::utils::constants::{ - ADMIN, ZERO, NAME, SYMBOL, BASE_URI, OWNER, RECIPIENT, SPENDER, OTHER, PUBKEY, SALT, TOKEN_ID, - TOKEN_ID_2, TOKEN_VALUE, TOKEN_VALUE_2 -}; +use openzeppelin::tests::utils::constants::{ADMIN, ZERO, OTHER, SALT}; use openzeppelin::tests::utils; -use openzeppelin::token::erc1155::interface::IERC1155_RECEIVER_ID; -use openzeppelin::token::erc1155::interface::{IERC1155DispatcherTrait, IERC1155Dispatcher}; -use openzeppelin::token::erc721::interface::IERC721_RECEIVER_ID; -use openzeppelin::token::erc721::interface::{IERC721DispatcherTrait, IERC721Dispatcher}; use openzeppelin::utils::selectors; use openzeppelin::utils::serde::SerializedAppend; use starknet::ContractAddress; @@ -172,42 +162,6 @@ fn deploy_attacker() -> ITimelockAttackerDispatcher { ITimelockAttackerDispatcher { contract_address: address } } -fn deploy_erc721() -> IERC721Dispatcher { - let mut calldata = array![]; - - calldata.append_serde(NAME()); - calldata.append_serde(SYMBOL()); - calldata.append_serde(BASE_URI()); - calldata.append_serde(OWNER()); - calldata.append_serde(TOKEN_ID); - - let address = utils::deploy(DualCaseERC721Mock::TEST_CLASS_HASH, calldata); - IERC721Dispatcher { contract_address: address } -} - -fn deploy_erc1155() -> (IERC1155Dispatcher, ContractAddress) { - let uri: ByteArray = "URI"; - let mut calldata = array![]; - let mut token_id = TOKEN_ID; - let mut value = TOKEN_VALUE; - - let owner = setup_account(); - testing::set_contract_address(owner); - - calldata.append_serde(uri); - calldata.append_serde(owner); - calldata.append_serde(token_id); - calldata.append_serde(value); - - let address = utils::deploy(DualCaseERC1155Mock::TEST_CLASS_HASH, calldata); - (IERC1155Dispatcher { contract_address: address }, owner) -} - -fn setup_account() -> ContractAddress { - let mut calldata = array![PUBKEY]; - utils::deploy(SnakeAccountMock::TEST_CLASS_HASH, calldata) -} - // // hash_operation // @@ -1099,105 +1053,6 @@ fn test_update_delay_scheduled() { assert_eq!(get_new_delay, NEW_DELAY); } -// -// Safe receive -// - -#[test] -fn test_receive_erc721_safe_transfer() { - let mut timelock = deploy_timelock(); - let mut erc721 = deploy_erc721(); - - let owner = OWNER(); - let timelock_addr = timelock.contract_address; - let token = TOKEN_ID; - let data = array![].span(); - - // Check original holder - let original_owner = erc721.owner_of(token); - assert_eq!(original_owner, owner); - - // Safe transfer - testing::set_contract_address(OWNER()); - erc721.safe_transfer_from(owner, timelock_addr, token, data); - - // Check that timelock accepted safe transfer - let new_owner = erc721.owner_of(token); - assert_eq!(new_owner, timelock_addr); -} - -#[test] -fn test_receive_erc1155_safe_transfer() { - let mut timelock = deploy_timelock(); - let (mut erc1155, owner) = deploy_erc1155(); - - let token_id = TOKEN_ID; - let token_value = TOKEN_VALUE; - - // Check initial balances - let owner_balance = erc1155.balance_of(owner, token_id); - let expected_balance = token_value; - assert_eq!(owner_balance, expected_balance); - - let timelock_balance = erc1155.balance_of(timelock.contract_address, token_id); - let expected_balance = 0; - assert_eq!(timelock_balance, expected_balance); - - // Safe transfer - testing::set_contract_address(owner); - let transfer_amt = 1; - let data = array![].span(); - erc1155.safe_transfer_from(owner, timelock.contract_address, token_id, transfer_amt, data); - - // Check new balances - let owner_balance = erc1155.balance_of(owner, token_id); - let expected_balance = token_value - transfer_amt; - assert_eq!(owner_balance, expected_balance); - - let timelock_balance = erc1155.balance_of(timelock.contract_address, token_id); - let expected_balance = transfer_amt; - assert_eq!(timelock_balance, expected_balance); -} - -#[test] -fn test_receive_erc1155_safe_batch_transfer() { - let mut timelock = deploy_timelock(); - let (mut erc1155, owner) = deploy_erc1155(); - - let token_id = TOKEN_ID; - let token_value = TOKEN_VALUE; - - // Check initial balances - let owner_balance = erc1155.balance_of(owner, token_id); - let expected_balance = token_value; - assert_eq!(owner_balance, expected_balance); - - let timelock_balance = erc1155.balance_of(timelock.contract_address, token_id); - let expected_balance = 0; - assert_eq!(timelock_balance, expected_balance); - - // Safe batch transfer - testing::set_contract_address(owner); - let transfer_ids = array![token_id, token_id].span(); - let transfer_amts = array![1, 1].span(); - let data = array![].span(); - erc1155 - .safe_batch_transfer_from( - owner, timelock.contract_address, transfer_ids, transfer_amts, data - ); - - // Check new balances - let total_transfer_amt = 2; - - let owner_balance = erc1155.balance_of(owner, token_id); - let expected_balance = token_value - total_transfer_amt; - assert_eq!(owner_balance, expected_balance); - - let timelock_balance = erc1155.balance_of(timelock.contract_address, token_id); - let expected_balance = total_transfer_amt; - assert_eq!(timelock_balance, expected_balance); -} - // // Internal // @@ -1268,12 +1123,6 @@ fn test_initializer_supported_interfaces() { let supports_isrc5 = contract_state.src5.supports_interface(ISRC5_ID); assert!(supports_isrc5); - let supports_ierc1155_receiver = contract_state.src5.supports_interface(IERC1155_RECEIVER_ID); - assert!(supports_ierc1155_receiver); - - let supports_ierc721_receiver = contract_state.src5.supports_interface(IERC721_RECEIVER_ID); - assert!(supports_ierc721_receiver); - let supports_access_control = contract_state.src5.supports_interface(IACCESSCONTROL_ID); assert!(supports_access_control); } From a48b2a317570190fddad7f9106afff5979502707 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 21 Jun 2024 11:39:05 -0500 Subject: [PATCH 072/103] add additional cancel tests --- src/tests/governance/test_timelock.cairo | 54 ++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 5cc2cb36d..60692f197 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -946,8 +946,7 @@ fn test_execute_batch_after_dependency() { // cancel // -#[test] -fn test_cancel_from_canceller() { +fn cancel_from_canceller(operation_state: OperationState) { let (mut timelock, mut target) = setup_dispatchers(); let predecessor = NO_PREDECESSOR; let salt = 0; @@ -966,15 +965,64 @@ fn test_cancel_from_canceller() { timelock.contract_address, target_id, event_index, call, predecessor, delay ); + if operation_state == OperationState::Ready { + // Fast-forward + testing::set_block_timestamp(delay); + assert_operation_state(timelock, OperationState::Ready, target_id); + } + // Cancel timelock.cancel(target_id); assert_only_event_cancel(timelock.contract_address, target_id); assert_operation_state(timelock, OperationState::Unset, target_id); } +#[test] +fn test_cancel_when_waiting() { + let waiting = OperationState::Waiting; + cancel_from_canceller(waiting); +} + +#[test] +fn test_cancel_when_ready() { + let ready = OperationState::Waiting; + cancel_from_canceller(ready); +} + +#[test] +#[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] +fn test_cancel_when_done() { + let (mut timelock, mut target) = setup_dispatchers(); + let predecessor = NO_PREDECESSOR; + let salt = 0; + let delay = MIN_DELAY; + + let call = single_operation(target.contract_address); + let target_id = timelock.hash_operation(call, predecessor, salt); + assert_operation_state(timelock, OperationState::Unset, target_id); + + // Schedule + testing::set_contract_address(PROPOSER()); + timelock.schedule(call, predecessor, salt, delay); + assert_operation_state(timelock, OperationState::Waiting, target_id); + + // Fast-forward + testing::set_block_timestamp(delay); + assert_operation_state(timelock, OperationState::Ready, target_id); + + // Execute + testing::set_contract_address(EXECUTOR()); + timelock.execute(call, predecessor, salt); + assert_operation_state(timelock, OperationState::Done, target_id); + + // Attempt cancel + testing::set_contract_address(PROPOSER()); // PROPOSER is also CANCELLER + timelock.cancel(target_id); +} + #[test] #[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] -fn test_cancel_invalid_operation() { +fn test_cancel_when_unset() { let (mut timelock, _) = setup_dispatchers(); let invalid_id = 0; From 309b4d0d13bee1e55739ed221f1acc018baad2aa Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Fri, 28 Jun 2024 10:26:42 -0500 Subject: [PATCH 073/103] Apply suggestions from code review Co-authored-by: Eric Nordelo --- src/governance/timelock/timelock_controller.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 8ff7b61e1..1434250c3 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -34,7 +34,7 @@ pub mod TimelockControllerComponent { pub const PROPOSER_ROLE: felt252 = selector!("PROPOSER_ROLE"); pub const EXECUTOR_ROLE: felt252 = selector!("EXECUTOR_ROLE"); pub const CANCELLER_ROLE: felt252 = selector!("CANCELLER_ROLE"); - pub const DONE_TIMESTAMP: u64 = 1; + const DONE_TIMESTAMP: u64 = 1; #[storage] struct Storage { @@ -342,7 +342,7 @@ pub mod TimelockControllerComponent { /// /// - The caller must be the timelock itself. This can only be achieved by scheduling /// and later executing an operation where the timelock is the target and the data - /// is the ABI-encoded call to this function. + /// is the serialized call to this function. /// /// Emits a `MinDelayChange` event. fn update_delay(ref self: ComponentState, new_delay: u64) { From 0f89aa0ecbfb24cecc10df31a2a1014e37407d60 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 28 Jun 2024 10:33:03 -0500 Subject: [PATCH 074/103] initializer: remove mut, use while loop --- .../timelock/timelock_controller.cairo | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 1434250c3..db9fe93b0 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -581,12 +581,8 @@ pub mod TimelockControllerComponent { // Register proposers and cancellers let mut i = 0; - loop { - if i == proposers.len() { - break; - } - - let mut proposer = proposers.at(i); + while i < proposers.len() { + let proposer = proposers.at(i); access_component._grant_role(PROPOSER_ROLE, *proposer); access_component._grant_role(CANCELLER_ROLE, *proposer); i += 1; @@ -594,12 +590,8 @@ pub mod TimelockControllerComponent { // Register executors let mut i = 0; - loop { - if i == executors.len() { - break; - } - - let mut executor = executors.at(i); + while i < executors.len() { + let executor = executors.at(i); access_component._grant_role(EXECUTOR_ROLE, *executor); i += 1; }; From 2278f4fd8c85aa540cd6366ec8e5b63030f5b220 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 28 Jun 2024 10:38:56 -0500 Subject: [PATCH 075/103] fix fmt --- .../timelock/timelock_controller.cairo | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index db9fe93b0..a07e63fca 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -581,20 +581,22 @@ pub mod TimelockControllerComponent { // Register proposers and cancellers let mut i = 0; - while i < proposers.len() { - let proposer = proposers.at(i); - access_component._grant_role(PROPOSER_ROLE, *proposer); - access_component._grant_role(CANCELLER_ROLE, *proposer); - i += 1; - }; + while i < proposers + .len() { + let proposer = proposers.at(i); + access_component._grant_role(PROPOSER_ROLE, *proposer); + access_component._grant_role(CANCELLER_ROLE, *proposer); + i += 1; + }; // Register executors let mut i = 0; - while i < executors.len() { - let executor = executors.at(i); - access_component._grant_role(EXECUTOR_ROLE, *executor); - i += 1; - }; + while i < executors + .len() { + let executor = executors.at(i); + access_component._grant_role(EXECUTOR_ROLE, *executor); + i += 1; + }; // Set minimum delay self.TimelockController_min_delay.write(min_delay); From d366e813de4e1becacc94f2ec1d347d58c4f3628 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 28 Jun 2024 12:32:08 -0500 Subject: [PATCH 076/103] add assert_only_self fn --- src/governance/timelock/timelock_controller.cairo | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index a07e63fca..a8596ea0b 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -346,9 +346,7 @@ pub mod TimelockControllerComponent { /// /// Emits a `MinDelayChange` event. fn update_delay(ref self: ComponentState, new_delay: u64) { - let this = starknet::get_contract_address(); - let caller = starknet::get_caller_address(); - assert(caller == this, Errors::UNAUTHORIZED_CALLER); + self.assert_only_self(); let min_delay = self.TimelockController_min_delay.read(); self.emit(MinDelayChange { old_duration: min_delay, new_duration: new_delay }); @@ -614,6 +612,14 @@ pub mod TimelockControllerComponent { } } + /// Validates that the caller is the timelock contract itself. + /// Otherwise it reverts. + fn assert_only_self(self: @ComponentState) { + let this = starknet::get_contract_address(); + let caller = starknet::get_caller_address(); + assert(caller == this, Errors::UNAUTHORIZED_CALLER); + } + /// Private function that checks before execution of an operation's calls. /// /// Requirements: From 9900a8c99c623e8e9e073cf710985e3aaf9366a1 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 28 Jun 2024 13:23:12 -0500 Subject: [PATCH 077/103] fix getter comments re: pending/waiting --- src/governance/timelock/timelock_controller.cairo | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index a8596ea0b..37856358a 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -118,15 +118,14 @@ pub mod TimelockControllerComponent { Timelock::get_operation_state(self, id) != OperationState::Unset } - /// Returns whether the `id` OperationState is Waiting or not. - /// Note that a Waiting operation may also be Ready. + /// Returns whether the `id` OperationState is pending or not. + /// Note that a pending operation may be either Waiting or Ready. fn is_operation_pending(self: @ComponentState, id: felt252) -> bool { let state = Timelock::get_operation_state(self, id); state == OperationState::Waiting || state == OperationState::Ready } /// Returns whether the `id` OperationState is Ready or not. - /// Note that a Pending operation may also be Ready. fn is_operation_ready(self: @ComponentState, id: felt252) -> bool { Timelock::get_operation_state(self, id) == OperationState::Ready } From 9686d22e336f2c333004bb72689a0dcc99e1262a Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 28 Jun 2024 14:05:15 -0500 Subject: [PATCH 078/103] add assert_only_role --- src/governance/timelock/timelock_controller.cairo | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 37856358a..53d5c8a1d 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -205,7 +205,7 @@ pub mod TimelockControllerComponent { salt: felt252, delay: u64 ) { - self.assert_only_role_or_open_role(PROPOSER_ROLE); + self.assert_only_role(PROPOSER_ROLE); let id = Timelock::hash_operation(@self, call, predecessor, salt); self._schedule(id, delay); @@ -231,7 +231,7 @@ pub mod TimelockControllerComponent { salt: felt252, delay: u64 ) { - self.assert_only_role_or_open_role(PROPOSER_ROLE); + self.assert_only_role(PROPOSER_ROLE); let id = Timelock::hash_operation_batch(@self, calls, predecessor, salt); self._schedule(id, delay); @@ -261,7 +261,7 @@ pub mod TimelockControllerComponent { /// /// Emits a `Cancelled` event. fn cancel(ref self: ComponentState, id: felt252) { - self.assert_only_role_or_open_role(CANCELLER_ROLE); + self.assert_only_role(CANCELLER_ROLE); assert(Timelock::is_operation_pending(@self, id), Errors::UNEXPECTED_OPERATION_STATE); self.TimelockController_timestamps.write(id, 0); @@ -600,6 +600,13 @@ pub mod TimelockControllerComponent { self.emit(MinDelayChange { old_duration: 0, new_duration: min_delay }) } + /// Validates that the caller has the given `role`. + /// Otherwise it reverts. + fn assert_only_role(self: @ComponentState, role: felt252) { + let access_component = get_dep_component!(self, AccessControl); + access_component.assert_only_role(role); + } + /// Validates that the caller has the given `role`. /// If `role` is granted to the zero address, then this is considered an open role which /// allows anyone to be the caller. From e3e62eaa43f07c60c5a436ed48b64ee50e9a8d67 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 28 Jun 2024 14:47:50 -0500 Subject: [PATCH 079/103] add specific op errors --- .../timelock/timelock_controller.cairo | 12 ++++--- src/tests/governance/test_timelock.cairo | 34 +++++++++---------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 53d5c8a1d..929497587 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -99,7 +99,9 @@ pub mod TimelockControllerComponent { pub mod Errors { pub const INVALID_OPERATION_LEN: felt252 = 'Timelock: invalid operation len'; pub const INSUFFICIENT_DELAY: felt252 = 'Timelock: insufficient delay'; - pub const UNEXPECTED_OPERATION_STATE: felt252 = 'Timelock: unexpected op state'; + pub const EXPECTED_UNSET_OPERATION: felt252 = 'Timelock: expected Unset op'; + pub const EXPECTED_PENDING_OPERATION: felt252 = 'Timelock: expected Pending op'; + pub const EXPECTED_READY_OPERATION: felt252 = 'Timelock: expected Ready op'; pub const UNEXECUTED_PREDECESSOR: felt252 = 'Timelock: awaiting predecessor'; pub const UNAUTHORIZED_CALLER: felt252 = 'Timelock: unauthorized caller'; } @@ -262,7 +264,7 @@ pub mod TimelockControllerComponent { /// Emits a `Cancelled` event. fn cancel(ref self: ComponentState, id: felt252) { self.assert_only_role(CANCELLER_ROLE); - assert(Timelock::is_operation_pending(@self, id), Errors::UNEXPECTED_OPERATION_STATE); + assert(Timelock::is_operation_pending(@self, id), Errors::EXPECTED_PENDING_OPERATION); self.TimelockController_timestamps.write(id, 0); self.emit(Cancelled { id }); @@ -633,7 +635,7 @@ pub mod TimelockControllerComponent { /// - `id` must be in the Ready OperationState. /// - `predecessor` must either be zero or be in the Done OperationState. fn _before_call(self: @ComponentState, id: felt252, predecessor: felt252) { - assert(Timelock::is_operation_ready(self, id), Errors::UNEXPECTED_OPERATION_STATE); + assert(Timelock::is_operation_ready(self, id), Errors::EXPECTED_READY_OPERATION); assert( predecessor == 0 || Timelock::is_operation_done(self, predecessor), Errors::UNEXECUTED_PREDECESSOR @@ -646,13 +648,13 @@ pub mod TimelockControllerComponent { /// /// - `id` must be in the Ready OperationState. fn _after_call(ref self: ComponentState, id: felt252) { - assert(Timelock::is_operation_ready(@self, id), Errors::UNEXPECTED_OPERATION_STATE); + assert(Timelock::is_operation_ready(@self, id), Errors::EXPECTED_READY_OPERATION); self.TimelockController_timestamps.write(id, DONE_TIMESTAMP); } /// Private function that schedules an operation that is to become valid after a given `delay`. fn _schedule(ref self: ComponentState, id: felt252, delay: u64) { - assert(!Timelock::is_operation(@self, id), Errors::UNEXPECTED_OPERATION_STATE); + assert(!Timelock::is_operation(@self, id), Errors::EXPECTED_UNSET_OPERATION); assert(Timelock::get_min_delay(@self) <= delay, Errors::INSUFFICIENT_DELAY); self.TimelockController_timestamps.write(id, starknet::get_block_timestamp() + delay); } diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 60692f197..07d7743b3 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -288,7 +288,7 @@ fn test_schedule_from_proposer_no_salt() { } #[test] -#[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: ('Timelock: expected Unset op', 'ENTRYPOINT_FAILED'))] fn test_schedule_overwrite() { let (mut timelock, mut target) = setup_dispatchers(); let predecessor = NO_PREDECESSOR; @@ -381,7 +381,7 @@ fn test_schedule_batch_from_proposer_no_salt() { } #[test] -#[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: ('Timelock: expected Unset op', 'ENTRYPOINT_FAILED'))] fn test_schedule_batch_overwrite() { let (mut timelock, mut target) = setup_dispatchers(); let predecessor = NO_PREDECESSOR; @@ -428,7 +428,7 @@ fn test_schedule_batch_bad_min_delay() { // #[test] -#[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: ('Timelock: expected Ready op', 'ENTRYPOINT_FAILED'))] fn test_execute_when_not_scheduled() { let (mut timelock, mut target) = setup_dispatchers(); let predecessor = NO_PREDECESSOR; @@ -482,7 +482,7 @@ fn test_execute_when_scheduled() { } #[test] -#[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: ('Timelock: expected Ready op', 'ENTRYPOINT_FAILED'))] fn test_execute_early() { let (mut timelock, mut target) = setup_dispatchers(); let predecessor = NO_PREDECESSOR; @@ -579,7 +579,7 @@ fn test_execute_bad_selector() { #[test] #[should_panic( expected: ( - 'Timelock: unexpected op state', + 'Timelock: expected Ready op', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED' @@ -702,7 +702,7 @@ fn test_execute_after_dependency() { // #[test] -#[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: ('Timelock: expected Ready op', 'ENTRYPOINT_FAILED'))] fn test_execute_batch_when_not_scheduled() { let (mut timelock, mut target) = setup_dispatchers(); let predecessor = NO_PREDECESSOR; @@ -754,7 +754,7 @@ fn test_execute_batch_when_scheduled() { } #[test] -#[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: ('Timelock: expected Ready op', 'ENTRYPOINT_FAILED'))] fn test_execute_batch_early() { let (mut timelock, mut target) = setup_dispatchers(); let predecessor = NO_PREDECESSOR; @@ -801,7 +801,7 @@ fn test_execute_batch_unauthorized() { #[test] #[should_panic( expected: ( - 'Timelock: unexpected op state', + 'Timelock: expected Ready op', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED' @@ -990,7 +990,7 @@ fn test_cancel_when_ready() { } #[test] -#[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: ('Timelock: expected Pending op', 'ENTRYPOINT_FAILED'))] fn test_cancel_when_done() { let (mut timelock, mut target) = setup_dispatchers(); let predecessor = NO_PREDECESSOR; @@ -1021,7 +1021,7 @@ fn test_cancel_when_done() { } #[test] -#[should_panic(expected: ('Timelock: unexpected op state', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: ('Timelock: expected Pending op', 'ENTRYPOINT_FAILED'))] fn test_cancel_when_unset() { let (mut timelock, _) = setup_dispatchers(); let invalid_id = 0; @@ -1284,7 +1284,7 @@ fn test__before_call() { } #[test] -#[should_panic(expected: ('Timelock: unexpected op state',))] +#[should_panic(expected: ('Timelock: expected Ready op',))] fn test__before_call_nonexistent_operation() { let mut state = COMPONENT_STATE(); let predecessor = NO_PREDECESSOR; @@ -1300,7 +1300,7 @@ fn test__before_call_nonexistent_operation() { } #[test] -#[should_panic(expected: ('Timelock: unexpected op state',))] +#[should_panic(expected: ('Timelock: expected Ready op',))] fn test__before_call_insufficient_time() { let mut state = COMPONENT_STATE(); let predecessor = NO_PREDECESSOR; @@ -1319,7 +1319,7 @@ fn test__before_call_insufficient_time() { } #[test] -#[should_panic(expected: ('Timelock: unexpected op state',))] +#[should_panic(expected: ('Timelock: expected Ready op',))] fn test__before_call_when_already_done() { let mut state = COMPONENT_STATE(); let predecessor = NO_PREDECESSOR; @@ -1409,7 +1409,7 @@ fn test__after_call() { } #[test] -#[should_panic(expected: ('Timelock: unexpected op state',))] +#[should_panic(expected: ('Timelock: expected Ready op',))] fn test__after_call_nonexistent_operation() { let mut state = COMPONENT_STATE(); @@ -1424,7 +1424,7 @@ fn test__after_call_nonexistent_operation() { } #[test] -#[should_panic(expected: ('Timelock: unexpected op state',))] +#[should_panic(expected: ('Timelock: expected Ready op',))] fn test__after_call_insufficient_time() { let mut state = COMPONENT_STATE(); @@ -1442,7 +1442,7 @@ fn test__after_call_insufficient_time() { } #[test] -#[should_panic(expected: ('Timelock: unexpected op state',))] +#[should_panic(expected: ('Timelock: expected Ready op',))] fn test__after_call_already_done() { let mut state = COMPONENT_STATE(); @@ -1484,7 +1484,7 @@ fn test__schedule() { } #[test] -#[should_panic(expected: ('Timelock: unexpected op state',))] +#[should_panic(expected: ('Timelock: expected Unset op',))] fn test__schedule_overwrite() { let mut state = COMPONENT_STATE(); let mut target = deploy_mock_target(); From ac9c4b8bc1b826b1e6be56fdb73f4ae86c2b5858 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 28 Jun 2024 15:09:03 -0500 Subject: [PATCH 080/103] make event names consistent --- .../timelock/timelock_controller.cairo | 20 ++++++------ src/tests/governance/test_timelock.cairo | 32 ++++++++----------- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 929497587..28c0e4ad7 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -48,8 +48,8 @@ pub mod TimelockControllerComponent { CallScheduled: CallScheduled, CallExecuted: CallExecuted, CallSalt: CallSalt, - Cancelled: Cancelled, - MinDelayChange: MinDelayChange + CallCancelled: CallCancelled, + MinDelayChanged: MinDelayChanged } /// Emitted when `call` is scheduled as part of operation `id`. @@ -84,14 +84,14 @@ pub mod TimelockControllerComponent { /// Emitted when operation `id` is cancelled. #[derive(Drop, PartialEq, starknet::Event)] - pub struct Cancelled { + pub struct CallCancelled { #[key] pub id: felt252 } /// Emitted when the minimum delay for future operations is modified. #[derive(Drop, PartialEq, starknet::Event)] - pub struct MinDelayChange { + pub struct MinDelayChanged { pub old_duration: u64, pub new_duration: u64 } @@ -261,13 +261,13 @@ pub mod TimelockControllerComponent { /// - The caller must have the `CANCELLER_ROLE` role. /// - `id` must be an operation. /// - /// Emits a `Cancelled` event. + /// Emits a `CallCancelled` event. fn cancel(ref self: ComponentState, id: felt252) { self.assert_only_role(CANCELLER_ROLE); assert(Timelock::is_operation_pending(@self, id), Errors::EXPECTED_PENDING_OPERATION); self.TimelockController_timestamps.write(id, 0); - self.emit(Cancelled { id }); + self.emit(CallCancelled { id }); } /// Execute a (Ready) operation containing a single Call. @@ -345,12 +345,12 @@ pub mod TimelockControllerComponent { /// and later executing an operation where the timelock is the target and the data /// is the serialized call to this function. /// - /// Emits a `MinDelayChange` event. + /// Emits a `MinDelayChanged` event. fn update_delay(ref self: ComponentState, new_delay: u64) { self.assert_only_self(); let min_delay = self.TimelockController_min_delay.read(); - self.emit(MinDelayChange { old_duration: min_delay, new_duration: new_delay }); + self.emit(MinDelayChanged { old_duration: min_delay, new_duration: new_delay }); self.TimelockController_min_delay.write(new_delay); } @@ -560,7 +560,7 @@ pub mod TimelockControllerComponent { /// May emit a `RoleGranted` event for `admin` with `DEFAULT_ADMIN_ROLE` role (if `admin` is /// not zero). /// - /// Emits `MinDelayChange` event. + /// Emits `MinDelayChanged` event. fn initializer( ref self: ComponentState, min_delay: u64, @@ -599,7 +599,7 @@ pub mod TimelockControllerComponent { // Set minimum delay self.TimelockController_min_delay.write(min_delay); - self.emit(MinDelayChange { old_duration: 0, new_duration: min_delay }) + self.emit(MinDelayChanged { old_duration: 0, new_duration: min_delay }) } /// Validates that the caller has the given `role`. diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 07d7743b3..0d0d79305 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -8,7 +8,7 @@ use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin::access::accesscontrol::interface::IACCESSCONTROL_ID; use openzeppelin::access::accesscontrol::interface::IAccessControl; use openzeppelin::governance::timelock::TimelockControllerComponent::{ - CallScheduled, CallExecuted, CallSalt, Cancelled, MinDelayChange + CallScheduled, CallExecuted, CallSalt, CallCancelled, MinDelayChanged }; use openzeppelin::governance::timelock::TimelockControllerComponent::{ TimelockImpl, InternalImpl as TimelockInternalImpl @@ -136,7 +136,7 @@ fn deploy_timelock() -> TimelockABIDispatcher { let address = utils::deploy(TimelockControllerMock::TEST_CLASS_HASH, calldata); // Events dropped: // - 5 RoleGranted: self, proposer, canceller, executor, admin - // - MinDelayChange + // - MinDelayChanged utils::drop_events(address, 6); TimelockABIDispatcher { contract_address: address } } @@ -579,10 +579,7 @@ fn test_execute_bad_selector() { #[test] #[should_panic( expected: ( - 'Timelock: expected Ready op', - 'ENTRYPOINT_FAILED', - 'ENTRYPOINT_FAILED', - 'ENTRYPOINT_FAILED' + 'Timelock: expected Ready op', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED' ) )] fn test_execute_reentrant_call() { @@ -801,10 +798,7 @@ fn test_execute_batch_unauthorized() { #[test] #[should_panic( expected: ( - 'Timelock: expected Ready op', - 'ENTRYPOINT_FAILED', - 'ENTRYPOINT_FAILED', - 'ENTRYPOINT_FAILED' + 'Timelock: expected Ready op', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED' ) )] fn test_execute_batch_reentrant_call() { @@ -1190,7 +1184,7 @@ fn test_initializer_min_delay() { let delay = state.get_min_delay(); assert_eq!(delay, MIN_DELAY); - // The initializer emits 4 `RoleGranted` events prior to `MinDelayChange`: + // The initializer emits 4 `RoleGranted` events prior to `MinDelayChanged`: // - Self administration // - 1 proposer // - 1 canceller @@ -1617,13 +1611,13 @@ fn assert_operation_state(timelock: TimelockABIDispatcher, exp_state: OperationS // // -// MinDelayChange +// MinDelayChanged // fn assert_event_delay_change(contract: ContractAddress, old_duration: u64, new_duration: u64) { let event = utils::pop_log::(contract).unwrap(); - let expected = TimelockControllerComponent::Event::MinDelayChange( - MinDelayChange { old_duration, new_duration } + let expected = TimelockControllerComponent::Event::MinDelayChanged( + MinDelayChanged { old_duration, new_duration } ); assert!(event == expected); } @@ -1752,12 +1746,12 @@ fn assert_only_events_execute_batch(contract: ContractAddress, id: felt252, call fn assert_event_cancel(contract: ContractAddress, id: felt252) { let event = utils::pop_log::(contract).unwrap(); - let expected = TimelockControllerComponent::Event::Cancelled(Cancelled { id }); + let expected = TimelockControllerComponent::Event::CallCancelled(CallCancelled { id }); assert!(event == expected); // Check indexed keys let mut indexed_keys = array![]; - indexed_keys.append_serde(selector!("Cancelled")); + indexed_keys.append_serde(selector!("CallCancelled")); indexed_keys.append_serde(id); utils::assert_indexed_keys(event, indexed_keys.span()); } @@ -1768,13 +1762,13 @@ fn assert_only_event_cancel(contract: ContractAddress, id: felt252) { } // -// MinDelayChange +// MinDelayChanged // fn assert_event_delay(contract: ContractAddress, old_duration: u64, new_duration: u64) { let event = utils::pop_log::(contract).unwrap(); - let expected = TimelockControllerComponent::Event::MinDelayChange( - MinDelayChange { old_duration, new_duration } + let expected = TimelockControllerComponent::Event::MinDelayChanged( + MinDelayChanged { old_duration, new_duration } ); assert!(event == expected); } From 9350e5061cf9f1dc054de3461f74040c896f8bde Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 28 Jun 2024 15:57:30 -0500 Subject: [PATCH 081/103] fix test --- src/tests/governance/test_timelock.cairo | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 0d0d79305..71a50d9b3 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -1242,17 +1242,17 @@ fn test_assert_only_role_or_open_role_with_open_role() { let min_delay = MIN_DELAY; let open_role = ZERO(); - let proposers = array![open_role].span(); - let executors = array![EXECUTOR()].span(); + let proposers = array![PROPOSER()].span(); + let executors = array![open_role].span(); let admin = ADMIN(); state.initializer(min_delay, proposers, executors, admin); - let is_open_role = contract_state.has_role(PROPOSER_ROLE, open_role); + let is_open_role = contract_state.has_role(EXECUTOR_ROLE, open_role); assert!(is_open_role); testing::set_caller_address(OTHER()); - state.assert_only_role_or_open_role(PROPOSER_ROLE); + state.assert_only_role_or_open_role(EXECUTOR_ROLE); } // From 9a7f3bf6aaa9aa7d68ad02f1419e791b070d9eed Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 5 Jul 2024 19:02:10 -0500 Subject: [PATCH 082/103] remove serialization from HashCallImpl --- .../timelock/timelock_controller.cairo | 2 +- .../timelock/utils/call_impls.cairo | 30 +++++++++++++------ src/tests/governance/test_timelock.cairo | 2 -- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 28c0e4ad7..9f9e9f64a 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -24,7 +24,7 @@ pub mod TimelockControllerComponent { use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin::governance::timelock::interface::{ITimelock, TimelockABI}; use openzeppelin::governance::timelock::utils::OperationState; - use openzeppelin::governance::timelock::utils::call_impls::{HashCallImpl, Call}; + use openzeppelin::governance::timelock::utils::call_impls::{HashCallImpl, HashCallsImpl, Call}; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; use openzeppelin::introspection::src5::SRC5Component; use starknet::ContractAddress; diff --git a/src/governance/timelock/utils/call_impls.cairo b/src/governance/timelock/utils/call_impls.cairo index 63c7b62c6..30ba1d583 100644 --- a/src/governance/timelock/utils/call_impls.cairo +++ b/src/governance/timelock/utils/call_impls.cairo @@ -13,16 +13,28 @@ pub struct Call { pub calldata: Span } -pub(crate) impl HashCallImpl< - Call, S, +Serde, +HashStateTrait, +Drop -> of Hash<@Call, S> { +pub(crate) impl HashCallImpl, +HashStateTrait, +Drop> of Hash<@Call, S> { fn update_state(mut state: S, value: @Call) -> S { - let mut arr = array![]; - Serde::serialize(value, ref arr); - state = state.update(arr.len().into()); - while let Option::Some(elem) = arr.pop_front() { - state = state.update(elem) - }; + let Call { to, selector, mut calldata } = *value; + state = state.update_with(to).update_with(selector).update_with(calldata.len()); + while calldata + .len() > 0 { + let elem = *calldata.pop_front().unwrap(); + state = state.update_with(elem); + }; + state + } +} + +pub(crate) impl HashCallsImpl, +Drop> of Hash<@Span, S> { + fn update_state(mut state: S, value: @Span) -> S { + let mut calls = *value; + state = state.update_with(calls.len()); + while calls + .len() > 0 { + let call = calls.pop_front().unwrap(); + state = state.update_with(call); + }; state } } diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 71a50d9b3..8db7e9531 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -184,7 +184,6 @@ fn test_hash_operation() { // Manually set hash elements let mut expected_hash = PoseidonTrait::new() - .update_with(4) // total elements of call .update_with(target.contract_address) // call::to .update_with(selector!("set_number")) // call::selector .update_with(1) // call::calldata.len @@ -215,7 +214,6 @@ fn test_hash_operation_batch() { // Manually set hash elements let mut expected_hash = PoseidonTrait::new() - .update_with(13) // total elements of Call span .update_with(3) // total number of Calls .update_with(target.contract_address) // call::to .update_with(selector!("set_number")) // call::selector From a11a6e7c1d176f52b8aaa24d602b6ce29f926d28 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 5 Jul 2024 23:08:55 -0500 Subject: [PATCH 083/103] remove unused components from mock --- src/tests/mocks/timelock_mocks.cairo | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/tests/mocks/timelock_mocks.cairo b/src/tests/mocks/timelock_mocks.cairo index aeeea6b34..d137d00b1 100644 --- a/src/tests/mocks/timelock_mocks.cairo +++ b/src/tests/mocks/timelock_mocks.cairo @@ -3,17 +3,11 @@ pub(crate) mod TimelockControllerMock { use openzeppelin::access::accesscontrol::AccessControlComponent; use openzeppelin::governance::timelock::TimelockControllerComponent; use openzeppelin::introspection::src5::SRC5Component; - use openzeppelin::token::erc1155::ERC1155ReceiverComponent; - use openzeppelin::token::erc721::ERC721ReceiverComponent; use starknet::ContractAddress; component!(path: AccessControlComponent, storage: access_control, event: AccessControlEvent); component!(path: SRC5Component, storage: src5, event: SRC5Event); component!(path: TimelockControllerComponent, storage: timelock, event: TimelockEvent); - component!(path: ERC721ReceiverComponent, storage: erc721_receiver, event: ERC721ReceiverEvent); - component!( - path: ERC1155ReceiverComponent, storage: erc1155_receiver, event: ERC1155ReceiverEvent - ); // Timelock Mixin #[abi(embed_v0)] @@ -28,11 +22,7 @@ pub(crate) mod TimelockControllerMock { #[substorage(v0)] src5: SRC5Component::Storage, #[substorage(v0)] - timelock: TimelockControllerComponent::Storage, - #[substorage(v0)] - erc721_receiver: ERC721ReceiverComponent::Storage, - #[substorage(v0)] - erc1155_receiver: ERC1155ReceiverComponent::Storage, + timelock: TimelockControllerComponent::Storage } #[event] @@ -43,11 +33,7 @@ pub(crate) mod TimelockControllerMock { #[flat] SRC5Event: SRC5Component::Event, #[flat] - TimelockEvent: TimelockControllerComponent::Event, - #[flat] - ERC721ReceiverEvent: ERC721ReceiverComponent::Event, - #[flat] - ERC1155ReceiverEvent: ERC1155ReceiverComponent::Event, + TimelockEvent: TimelockControllerComponent::Event } #[constructor] From 1487b9f998e85487c51fd2aac2c60637b6c07aeb Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 5 Jul 2024 23:13:48 -0500 Subject: [PATCH 084/103] clean up code --- src/governance/timelock/utils/call_impls.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/governance/timelock/utils/call_impls.cairo b/src/governance/timelock/utils/call_impls.cairo index 30ba1d583..1859fc3f1 100644 --- a/src/governance/timelock/utils/call_impls.cairo +++ b/src/governance/timelock/utils/call_impls.cairo @@ -13,7 +13,7 @@ pub struct Call { pub calldata: Span } -pub(crate) impl HashCallImpl, +HashStateTrait, +Drop> of Hash<@Call, S> { +pub(crate) impl HashCallImpl, +Drop> of Hash<@Call, S> { fn update_state(mut state: S, value: @Call) -> S { let Call { to, selector, mut calldata } = *value; state = state.update_with(to).update_with(selector).update_with(calldata.len()); From 7f09ecc04b13afdc65c3d9c3b8c1df02a3f9c4ea Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 11 Jul 2024 11:31:44 -0500 Subject: [PATCH 085/103] update to 2.7.0-rc.1 --- .../timelock/timelock_controller.cairo | 23 ++++++++++--------- src/tests/governance/test_timelock.cairo | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 9f9e9f64a..752e28eb8 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -28,6 +28,7 @@ pub mod TimelockControllerComponent { use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; use openzeppelin::introspection::src5::SRC5Component; use starknet::ContractAddress; + use starknet::storage::Map; use starknet::SyscallResultTrait; // Constants @@ -38,7 +39,7 @@ pub mod TimelockControllerComponent { #[storage] struct Storage { - TimelockController_timestamps: LegacyMap, + TimelockController_timestamps: Map, TimelockController_min_delay: u64 } @@ -117,24 +118,24 @@ pub mod TimelockControllerComponent { /// Returns whether `id` corresponds to a registered operation. /// This includes the OperationStates: Waiting, Ready, and Done. fn is_operation(self: @ComponentState, id: felt252) -> bool { - Timelock::get_operation_state(self, id) != OperationState::Unset + Self::get_operation_state(self, id) != OperationState::Unset } /// Returns whether the `id` OperationState is pending or not. /// Note that a pending operation may be either Waiting or Ready. fn is_operation_pending(self: @ComponentState, id: felt252) -> bool { - let state = Timelock::get_operation_state(self, id); + let state = Self::get_operation_state(self, id); state == OperationState::Waiting || state == OperationState::Ready } /// Returns whether the `id` OperationState is Ready or not. fn is_operation_ready(self: @ComponentState, id: felt252) -> bool { - Timelock::get_operation_state(self, id) == OperationState::Ready + Self::get_operation_state(self, id) == OperationState::Ready } /// Returns whether the `id` OperationState is Done or not. fn is_operation_done(self: @ComponentState, id: felt252) -> bool { - Timelock::get_operation_state(self, id) == OperationState::Done + Self::get_operation_state(self, id) == OperationState::Done } /// Returns the timestamp at which `id` becomes Ready. @@ -149,7 +150,7 @@ pub mod TimelockControllerComponent { fn get_operation_state( self: @ComponentState, id: felt252 ) -> OperationState { - let timestamp = Timelock::get_timestamp(self, id); + let timestamp = Self::get_timestamp(self, id); if (timestamp == 0) { return OperationState::Unset; } else if (timestamp == DONE_TIMESTAMP) { @@ -209,7 +210,7 @@ pub mod TimelockControllerComponent { ) { self.assert_only_role(PROPOSER_ROLE); - let id = Timelock::hash_operation(@self, call, predecessor, salt); + let id = Self::hash_operation(@self, call, predecessor, salt); self._schedule(id, delay); self.emit(CallScheduled { id, index: 0, call, predecessor, delay }); @@ -235,7 +236,7 @@ pub mod TimelockControllerComponent { ) { self.assert_only_role(PROPOSER_ROLE); - let id = Timelock::hash_operation_batch(@self, calls, predecessor, salt); + let id = Self::hash_operation_batch(@self, calls, predecessor, salt); self._schedule(id, delay); let mut index = 0; @@ -264,7 +265,7 @@ pub mod TimelockControllerComponent { /// Emits a `CallCancelled` event. fn cancel(ref self: ComponentState, id: felt252) { self.assert_only_role(CANCELLER_ROLE); - assert(Timelock::is_operation_pending(@self, id), Errors::EXPECTED_PENDING_OPERATION); + assert(Self::is_operation_pending(@self, id), Errors::EXPECTED_PENDING_OPERATION); self.TimelockController_timestamps.write(id, 0); self.emit(CallCancelled { id }); @@ -291,7 +292,7 @@ pub mod TimelockControllerComponent { ) { self.assert_only_role_or_open_role(EXECUTOR_ROLE); - let id = Timelock::hash_operation(@self, call, predecessor, salt); + let id = Self::hash_operation(@self, call, predecessor, salt); self._before_call(id, predecessor); self._execute(call); self.emit(CallExecuted { id, index: 0, call }); @@ -319,7 +320,7 @@ pub mod TimelockControllerComponent { ) { self.assert_only_role_or_open_role(EXECUTOR_ROLE); - let id = Timelock::hash_operation_batch(@self, calls, predecessor, salt); + let id = Self::hash_operation_batch(@self, calls, predecessor, salt); self._before_call(id, predecessor); let mut index = 0; diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 8db7e9531..01647fc80 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -35,7 +35,7 @@ use openzeppelin::utils::selectors; use openzeppelin::utils::serde::SerializedAppend; use starknet::ContractAddress; use starknet::contract_address_const; -use starknet::storage::{StorageMemberAccessTrait, StorageMapMemberAccessTrait}; +//use starknet::storage::{StorageMemberAccessTrait, StorageMapMemberAccessTrait}; use starknet::testing; type ComponentState = From ab0025ac486b0395d5eafa27e9b3e9b81d7c244a Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 11 Jul 2024 11:32:56 -0500 Subject: [PATCH 086/103] fix fmt --- .../timelock/timelock_controller.cairo | 44 +++++++++---------- .../timelock/utils/call_impls.cairo | 18 ++++---- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 752e28eb8..725416df9 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -3,9 +3,10 @@ /// # TimelockController Component /// -/// Component that acts as a timelocked controller. When set as the owner of an `Ownable` smart contract, -/// it enforces a timelock on all `only_owner` maintenance operations. This gives time for users -/// of the controlled contract to exit before a potentially dangerous maintenance operation is applied. +/// Component that acts as a timelocked controller. When set as the owner of an `Ownable` smart +/// contract, it enforces a timelock on all `only_owner` maintenance operations. This gives time for +/// users of the controlled contract to exit before a potentially dangerous maintenance operation is +/// applied. /// /// By default, this component is self administered, meaning administration tasks have to go through /// the timelock process. The proposer role is in charge of proposing operations. A common use case @@ -28,8 +29,8 @@ pub mod TimelockControllerComponent { use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; use openzeppelin::introspection::src5::SRC5Component; use starknet::ContractAddress; - use starknet::storage::Map; use starknet::SyscallResultTrait; + use starknet::storage::Map; // Constants pub const PROPOSER_ROLE: felt252 = selector!("PROPOSER_ROLE"); @@ -550,11 +551,11 @@ pub mod TimelockControllerComponent { /// - `admin`: optional account to be granted admin role; disable with zero address. /// /// WARNING: The optional admin can aid with initial configuration of roles after deployment - /// without being subject to delay, but this role should be subsequently renounced in favor of - /// administration through timelocked proposals. + /// without being subject to delay, but this role should be subsequently renounced in favor + /// of administration through timelocked proposals. /// - /// Emits two `RoleGranted` events for each account in `proposers` with `PROPOSER_ROLE` admin - /// `CANCELLER_ROLE` roles. + /// Emits two `RoleGranted` events for each account in `proposers` with `PROPOSER_ROLE` + /// admin `CANCELLER_ROLE` roles. /// /// Emits a `RoleGranted` event for each account in `executors` with `EXECUTOR_ROLE` role. /// @@ -581,22 +582,20 @@ pub mod TimelockControllerComponent { // Register proposers and cancellers let mut i = 0; - while i < proposers - .len() { - let proposer = proposers.at(i); - access_component._grant_role(PROPOSER_ROLE, *proposer); - access_component._grant_role(CANCELLER_ROLE, *proposer); - i += 1; - }; + while i < proposers.len() { + let proposer = proposers.at(i); + access_component._grant_role(PROPOSER_ROLE, *proposer); + access_component._grant_role(CANCELLER_ROLE, *proposer); + i += 1; + }; // Register executors let mut i = 0; - while i < executors - .len() { - let executor = executors.at(i); - access_component._grant_role(EXECUTOR_ROLE, *executor); - i += 1; - }; + while i < executors.len() { + let executor = executors.at(i); + access_component._grant_role(EXECUTOR_ROLE, *executor); + i += 1; + }; // Set minimum delay self.TimelockController_min_delay.write(min_delay); @@ -653,7 +652,8 @@ pub mod TimelockControllerComponent { self.TimelockController_timestamps.write(id, DONE_TIMESTAMP); } - /// Private function that schedules an operation that is to become valid after a given `delay`. + /// Private function that schedules an operation that is to become valid after a given + /// `delay`. fn _schedule(ref self: ComponentState, id: felt252, delay: u64) { assert(!Timelock::is_operation(@self, id), Errors::EXPECTED_UNSET_OPERATION); assert(Timelock::get_min_delay(@self) <= delay, Errors::INSUFFICIENT_DELAY); diff --git a/src/governance/timelock/utils/call_impls.cairo b/src/governance/timelock/utils/call_impls.cairo index 1859fc3f1..4df463f77 100644 --- a/src/governance/timelock/utils/call_impls.cairo +++ b/src/governance/timelock/utils/call_impls.cairo @@ -17,11 +17,10 @@ pub(crate) impl HashCallImpl, +Drop> of Hash<@Call, S> fn update_state(mut state: S, value: @Call) -> S { let Call { to, selector, mut calldata } = *value; state = state.update_with(to).update_with(selector).update_with(calldata.len()); - while calldata - .len() > 0 { - let elem = *calldata.pop_front().unwrap(); - state = state.update_with(elem); - }; + while calldata.len() > 0 { + let elem = *calldata.pop_front().unwrap(); + state = state.update_with(elem); + }; state } } @@ -30,11 +29,10 @@ pub(crate) impl HashCallsImpl, +Drop> of Hash<@Span) -> S { let mut calls = *value; state = state.update_with(calls.len()); - while calls - .len() > 0 { - let call = calls.pop_front().unwrap(); - state = state.update_with(call); - }; + while calls.len() > 0 { + let call = calls.pop_front().unwrap(); + state = state.update_with(call); + }; state } } From de472701e85226b42cadc5f924826262a9756406 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 11 Jul 2024 17:14:46 -0500 Subject: [PATCH 087/103] import Call from corelib --- src/governance/timelock/interface.cairo | 2 +- src/governance/timelock/timelock_controller.cairo | 3 ++- src/governance/timelock/utils.cairo | 1 - src/governance/timelock/utils/call_impls.cairo | 12 ++---------- src/tests/governance/test_timelock.cairo | 4 ++-- src/tests/mocks/timelock_mocks.cairo | 2 +- 6 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/governance/timelock/interface.cairo b/src/governance/timelock/interface.cairo index 0d41ba073..de494fbe0 100644 --- a/src/governance/timelock/interface.cairo +++ b/src/governance/timelock/interface.cairo @@ -2,8 +2,8 @@ // OpenZeppelin Contracts for Cairo v0.14.0 (governance/timelock/interface.cairo) use openzeppelin::governance::timelock::utils::OperationState; -use openzeppelin::governance::timelock::utils::call_impls::Call; use starknet::ContractAddress; +use starknet::account::Call; #[starknet::interface] pub trait ITimelock { diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 725416df9..b47225ce6 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -25,11 +25,12 @@ pub mod TimelockControllerComponent { use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin::governance::timelock::interface::{ITimelock, TimelockABI}; use openzeppelin::governance::timelock::utils::OperationState; - use openzeppelin::governance::timelock::utils::call_impls::{HashCallImpl, HashCallsImpl, Call}; + use openzeppelin::governance::timelock::utils::call_impls::{HashCallImpl, HashCallsImpl, CallPartialEq}; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; use openzeppelin::introspection::src5::SRC5Component; use starknet::ContractAddress; use starknet::SyscallResultTrait; + use starknet::account::Call; use starknet::storage::Map; // Constants diff --git a/src/governance/timelock/utils.cairo b/src/governance/timelock/utils.cairo index c9824f6b1..dd3003f85 100644 --- a/src/governance/timelock/utils.cairo +++ b/src/governance/timelock/utils.cairo @@ -1,5 +1,4 @@ pub mod call_impls; pub mod operation_state; -pub use call_impls::Call; pub use operation_state::OperationState; diff --git a/src/governance/timelock/utils/call_impls.cairo b/src/governance/timelock/utils/call_impls.cairo index 4df463f77..5e434efdd 100644 --- a/src/governance/timelock/utils/call_impls.cairo +++ b/src/governance/timelock/utils/call_impls.cairo @@ -3,15 +3,7 @@ use core::hash::{HashStateTrait, HashStateExTrait, Hash}; use starknet::ContractAddress; - -// TMP until cairo v2.7 release, then use SN `Call` struct -// `Call` from v2.6 does not derive Copy trait -#[derive(Drop, Copy, Serde, Debug)] -pub struct Call { - pub to: ContractAddress, - pub selector: felt252, - pub calldata: Span -} +use starknet::account::Call; pub(crate) impl HashCallImpl, +Drop> of Hash<@Call, S> { fn update_state(mut state: S, value: @Call) -> S { @@ -37,7 +29,7 @@ pub(crate) impl HashCallsImpl, +Drop> of Hash<@Span { +pub(crate) impl CallPartialEq of PartialEq { #[inline(always)] fn eq(lhs: @Call, rhs: @Call) -> bool { let mut lhs_arr = array![]; diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 01647fc80..025c9bcdb 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -17,7 +17,7 @@ use openzeppelin::governance::timelock::TimelockControllerComponent; use openzeppelin::governance::timelock::interface::{ TimelockABIDispatcher, TimelockABIDispatcherTrait }; -use openzeppelin::governance::timelock::utils::{Call, OperationState}; +use openzeppelin::governance::timelock::utils::OperationState; use openzeppelin::governance::timelock::{PROPOSER_ROLE, EXECUTOR_ROLE, CANCELLER_ROLE}; use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; @@ -34,8 +34,8 @@ use openzeppelin::tests::utils; use openzeppelin::utils::selectors; use openzeppelin::utils::serde::SerializedAppend; use starknet::ContractAddress; +use starknet::account::Call; use starknet::contract_address_const; -//use starknet::storage::{StorageMemberAccessTrait, StorageMapMemberAccessTrait}; use starknet::testing; type ComponentState = diff --git a/src/tests/mocks/timelock_mocks.cairo b/src/tests/mocks/timelock_mocks.cairo index d137d00b1..a60a9da19 100644 --- a/src/tests/mocks/timelock_mocks.cairo +++ b/src/tests/mocks/timelock_mocks.cairo @@ -91,8 +91,8 @@ pub(crate) mod TimelockAttackerMock { use openzeppelin::governance::timelock::interface::{ ITimelockDispatcher, ITimelockDispatcherTrait }; - use openzeppelin::governance::timelock::utils::call_impls::Call; use starknet::ContractAddress; + use starknet::account::Call; use super::ITimelockAttacker; const NO_PREDECESSOR: felt252 = 0; From e97407e705b6e78c65fa0fb1f30ab7a28d0fe908 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 11 Jul 2024 17:16:43 -0500 Subject: [PATCH 088/103] update spdx --- src/governance/timelock/interface.cairo | 2 +- src/governance/timelock/timelock_controller.cairo | 2 +- src/governance/timelock/utils/call_impls.cairo | 2 +- src/governance/timelock/utils/operation_state.cairo | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/governance/timelock/interface.cairo b/src/governance/timelock/interface.cairo index de494fbe0..8aad47383 100644 --- a/src/governance/timelock/interface.cairo +++ b/src/governance/timelock/interface.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.14.0 (governance/timelock/interface.cairo) +// OpenZeppelin Contracts for Cairo v0.15.0-rc.0 (governance/timelock/interface.cairo) use openzeppelin::governance::timelock::utils::OperationState; use starknet::ContractAddress; diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index b47225ce6..ab315073b 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.14.0 (governance/timelock/timelock_controller.cairo) +// OpenZeppelin Contracts for Cairo v0.15.0-rc.0 (governance/timelock/timelock_controller.cairo) /// # TimelockController Component /// diff --git a/src/governance/timelock/utils/call_impls.cairo b/src/governance/timelock/utils/call_impls.cairo index 5e434efdd..9086bd4b3 100644 --- a/src/governance/timelock/utils/call_impls.cairo +++ b/src/governance/timelock/utils/call_impls.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.14.0 (governance/timelock/utils/call_impls.cairo) +// OpenZeppelin Contracts for Cairo v0.15.0-rc.0 (governance/timelock/utils/call_impls.cairo) use core::hash::{HashStateTrait, HashStateExTrait, Hash}; use starknet::ContractAddress; diff --git a/src/governance/timelock/utils/operation_state.cairo b/src/governance/timelock/utils/operation_state.cairo index 0fabf1db1..d7e82497e 100644 --- a/src/governance/timelock/utils/operation_state.cairo +++ b/src/governance/timelock/utils/operation_state.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.14.0 (governance/timelock/utils/operation_state.cairo) +// OpenZeppelin Contracts for Cairo v0.15.0-rc.0 (governance/timelock/utils/operation_state.cairo) use core::fmt::{Debug, Formatter, Error}; From f5c1e638bf870bfe026f4cafa2cafff57f795f8a Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 11 Jul 2024 17:17:17 -0500 Subject: [PATCH 089/103] fix fmt --- src/governance/timelock/timelock_controller.cairo | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index ab315073b..5dd422f36 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -25,7 +25,9 @@ pub mod TimelockControllerComponent { use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin::governance::timelock::interface::{ITimelock, TimelockABI}; use openzeppelin::governance::timelock::utils::OperationState; - use openzeppelin::governance::timelock::utils::call_impls::{HashCallImpl, HashCallsImpl, CallPartialEq}; + use openzeppelin::governance::timelock::utils::call_impls::{ + HashCallImpl, HashCallsImpl, CallPartialEq + }; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; use openzeppelin::introspection::src5::SRC5Component; use starknet::ContractAddress; From 3d96bf2a48fd08c491e42c841a719d411b68603a Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 16 Jul 2024 12:09:29 -0500 Subject: [PATCH 090/103] move OperationState, derive debug --- src/governance/timelock.cairo | 1 + src/governance/timelock/interface.cairo | 2 +- .../timelock/timelock_controller.cairo | 10 +++++++- src/governance/timelock/utils.cairo | 3 --- .../timelock/utils/operation_state.cairo | 23 ------------------- src/tests/governance/test_timelock.cairo | 2 +- 6 files changed, 12 insertions(+), 29 deletions(-) delete mode 100644 src/governance/timelock/utils/operation_state.cairo diff --git a/src/governance/timelock.cairo b/src/governance/timelock.cairo index 161ee2038..0abcf6e3b 100644 --- a/src/governance/timelock.cairo +++ b/src/governance/timelock.cairo @@ -2,6 +2,7 @@ pub mod interface; pub mod timelock_controller; pub mod utils; +pub use timelock_controller::OperationState; pub use timelock_controller::TimelockControllerComponent::{ PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE }; diff --git a/src/governance/timelock/interface.cairo b/src/governance/timelock/interface.cairo index 8aad47383..0c89d0c20 100644 --- a/src/governance/timelock/interface.cairo +++ b/src/governance/timelock/interface.cairo @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.15.0-rc.0 (governance/timelock/interface.cairo) -use openzeppelin::governance::timelock::utils::OperationState; +use openzeppelin::governance::timelock::OperationState; use starknet::ContractAddress; use starknet::account::Call; diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 5dd422f36..dd9a9c39d 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -24,7 +24,6 @@ pub mod TimelockControllerComponent { use openzeppelin::access::accesscontrol::AccessControlComponent; use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin::governance::timelock::interface::{ITimelock, TimelockABI}; - use openzeppelin::governance::timelock::utils::OperationState; use openzeppelin::governance::timelock::utils::call_impls::{ HashCallImpl, HashCallsImpl, CallPartialEq }; @@ -34,6 +33,7 @@ pub mod TimelockControllerComponent { use starknet::SyscallResultTrait; use starknet::account::Call; use starknet::storage::Map; + use super::OperationState; // Constants pub const PROPOSER_ROLE: felt252 = selector!("PROPOSER_ROLE"); @@ -670,3 +670,11 @@ pub mod TimelockControllerComponent { } } } + +#[derive(Drop, Serde, PartialEq, Debug)] +pub enum OperationState { + Unset, + Waiting, + Ready, + Done +} diff --git a/src/governance/timelock/utils.cairo b/src/governance/timelock/utils.cairo index dd3003f85..0b626634e 100644 --- a/src/governance/timelock/utils.cairo +++ b/src/governance/timelock/utils.cairo @@ -1,4 +1 @@ pub mod call_impls; -pub mod operation_state; - -pub use operation_state::OperationState; diff --git a/src/governance/timelock/utils/operation_state.cairo b/src/governance/timelock/utils/operation_state.cairo deleted file mode 100644 index d7e82497e..000000000 --- a/src/governance/timelock/utils/operation_state.cairo +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.15.0-rc.0 (governance/timelock/utils/operation_state.cairo) - -use core::fmt::{Debug, Formatter, Error}; - -#[derive(Drop, Copy, Serde, PartialEq)] -pub enum OperationState { - Unset, - Waiting, - Ready, - Done -} - -impl DebugOperationState of core::fmt::Debug { - fn fmt(self: @OperationState, ref f: Formatter) -> Result<(), Error> { - match self { - OperationState::Unset => write!(f, "Unset"), - OperationState::Waiting => write!(f, "Waiting"), - OperationState::Ready => write!(f, "Ready"), - OperationState::Done => write!(f, "Done"), - } - } -} diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 025c9bcdb..dafb14457 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -17,7 +17,7 @@ use openzeppelin::governance::timelock::TimelockControllerComponent; use openzeppelin::governance::timelock::interface::{ TimelockABIDispatcher, TimelockABIDispatcherTrait }; -use openzeppelin::governance::timelock::utils::OperationState; +use openzeppelin::governance::timelock::OperationState; use openzeppelin::governance::timelock::{PROPOSER_ROLE, EXECUTOR_ROLE, CANCELLER_ROLE}; use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; From 6352975e90fc0138445b6fda545c592d3109f134 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 16 Jul 2024 12:09:58 -0500 Subject: [PATCH 091/103] fix fmt --- src/tests/governance/test_timelock.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index dafb14457..ec78c44cd 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -7,6 +7,7 @@ use openzeppelin::access::accesscontrol::AccessControlComponent::{ use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE; use openzeppelin::access::accesscontrol::interface::IACCESSCONTROL_ID; use openzeppelin::access::accesscontrol::interface::IAccessControl; +use openzeppelin::governance::timelock::OperationState; use openzeppelin::governance::timelock::TimelockControllerComponent::{ CallScheduled, CallExecuted, CallSalt, CallCancelled, MinDelayChanged }; @@ -17,7 +18,6 @@ use openzeppelin::governance::timelock::TimelockControllerComponent; use openzeppelin::governance::timelock::interface::{ TimelockABIDispatcher, TimelockABIDispatcherTrait }; -use openzeppelin::governance::timelock::OperationState; use openzeppelin::governance::timelock::{PROPOSER_ROLE, EXECUTOR_ROLE, CANCELLER_ROLE}; use openzeppelin::introspection::interface::ISRC5_ID; use openzeppelin::introspection::src5::SRC5Component::SRC5Impl; From 858bb94b592b234060d2788eba9e134e0c31126b Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 16 Jul 2024 12:35:37 -0500 Subject: [PATCH 092/103] fix hash impls --- src/governance/timelock/timelock_controller.cairo | 4 ++-- src/governance/timelock/utils/call_impls.cairo | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index dd9a9c39d..c60b62488 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -177,7 +177,7 @@ pub mod TimelockControllerComponent { self: @ComponentState, call: Call, predecessor: felt252, salt: felt252 ) -> felt252 { PoseidonTrait::new() - .update_with(@call) + .update_with(call) .update_with(predecessor) .update_with(salt) .finalize() @@ -191,7 +191,7 @@ pub mod TimelockControllerComponent { salt: felt252 ) -> felt252 { PoseidonTrait::new() - .update_with(@calls) + .update_with(calls) .update_with(predecessor) .update_with(salt) .finalize() diff --git a/src/governance/timelock/utils/call_impls.cairo b/src/governance/timelock/utils/call_impls.cairo index 9086bd4b3..94a262d63 100644 --- a/src/governance/timelock/utils/call_impls.cairo +++ b/src/governance/timelock/utils/call_impls.cairo @@ -5,9 +5,9 @@ use core::hash::{HashStateTrait, HashStateExTrait, Hash}; use starknet::ContractAddress; use starknet::account::Call; -pub(crate) impl HashCallImpl, +Drop> of Hash<@Call, S> { - fn update_state(mut state: S, value: @Call) -> S { - let Call { to, selector, mut calldata } = *value; +pub(crate) impl HashCallImpl, +Drop> of Hash { + fn update_state(mut state: S, value: Call) -> S { + let Call { to, selector, mut calldata } = value; state = state.update_with(to).update_with(selector).update_with(calldata.len()); while calldata.len() > 0 { let elem = *calldata.pop_front().unwrap(); @@ -17,13 +17,13 @@ pub(crate) impl HashCallImpl, +Drop> of Hash<@Call, S> } } -pub(crate) impl HashCallsImpl, +Drop> of Hash<@Span, S> { - fn update_state(mut state: S, value: @Span) -> S { - let mut calls = *value; +pub(crate) impl HashCallsImpl, +Drop> of Hash, S> { + fn update_state(mut state: S, value: Span) -> S { + let mut calls = value; state = state.update_with(calls.len()); while calls.len() > 0 { let call = calls.pop_front().unwrap(); - state = state.update_with(call); + state = state.update_with(*call); }; state } From 6c37eb8ed4ad26a3c551db34ff406ff95d789a5a Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 16 Jul 2024 17:53:31 -0500 Subject: [PATCH 093/103] add for loops --- .../timelock/timelock_controller.cairo | 32 +++++-------------- .../timelock/utils/call_impls.cairo | 16 ++++------ 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index c60b62488..d8f38b29f 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -244,13 +244,8 @@ pub mod TimelockControllerComponent { self._schedule(id, delay); let mut index = 0; - loop { - if index == calls.len() { - break; - } - - let call = *calls.at(index); - self.emit(CallScheduled { id, index: index.into(), call, predecessor, delay }); + for call in calls { + self.emit(CallScheduled { id, index, call: *call, predecessor, delay }); index += 1; }; @@ -328,14 +323,9 @@ pub mod TimelockControllerComponent { self._before_call(id, predecessor); let mut index = 0; - loop { - if index == calls.len() { - break; - } - - let call = *calls.at(index); - self._execute(call); - self.emit(CallExecuted { id, index: index.into(), call }); + for call in calls { + self._execute(*call); + self.emit(CallExecuted { id, index, call: *call }); index += 1; }; @@ -584,25 +574,19 @@ pub mod TimelockControllerComponent { }; // Register proposers and cancellers - let mut i = 0; - while i < proposers.len() { - let proposer = proposers.at(i); + for proposer in proposers { access_component._grant_role(PROPOSER_ROLE, *proposer); access_component._grant_role(CANCELLER_ROLE, *proposer); - i += 1; }; // Register executors - let mut i = 0; - while i < executors.len() { - let executor = executors.at(i); + for executor in executors { access_component._grant_role(EXECUTOR_ROLE, *executor); - i += 1; }; // Set minimum delay self.TimelockController_min_delay.write(min_delay); - self.emit(MinDelayChanged { old_duration: 0, new_duration: min_delay }) + self.emit(MinDelayChanged { old_duration: 0, new_duration: min_delay }); } /// Validates that the caller has the given `role`. diff --git a/src/governance/timelock/utils/call_impls.cairo b/src/governance/timelock/utils/call_impls.cairo index 94a262d63..1fc81083f 100644 --- a/src/governance/timelock/utils/call_impls.cairo +++ b/src/governance/timelock/utils/call_impls.cairo @@ -2,29 +2,27 @@ // OpenZeppelin Contracts for Cairo v0.15.0-rc.0 (governance/timelock/utils/call_impls.cairo) use core::hash::{HashStateTrait, HashStateExTrait, Hash}; -use starknet::ContractAddress; use starknet::account::Call; pub(crate) impl HashCallImpl, +Drop> of Hash { fn update_state(mut state: S, value: Call) -> S { - let Call { to, selector, mut calldata } = value; + let Call { to, selector, calldata } = value; state = state.update_with(to).update_with(selector).update_with(calldata.len()); - while calldata.len() > 0 { - let elem = *calldata.pop_front().unwrap(); - state = state.update_with(elem); + for elem in calldata { + state = state.update_with(*elem); }; + state } } pub(crate) impl HashCallsImpl, +Drop> of Hash, S> { fn update_state(mut state: S, value: Span) -> S { - let mut calls = value; - state = state.update_with(calls.len()); - while calls.len() > 0 { - let call = calls.pop_front().unwrap(); + state = state.update_with(value.len()); + for call in value { state = state.update_with(*call); }; + state } } From a4c1f29f849afde41a806d780333d0dfdb16c69e Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 16 Jul 2024 19:56:18 -0500 Subject: [PATCH 094/103] fix PartialEq, add tests --- .../timelock/utils/call_impls.cairo | 14 +-- src/tests/governance.cairo | 1 + src/tests/governance/test_utils.cairo | 108 ++++++++++++++++++ 3 files changed, 113 insertions(+), 10 deletions(-) create mode 100644 src/tests/governance/test_utils.cairo diff --git a/src/governance/timelock/utils/call_impls.cairo b/src/governance/timelock/utils/call_impls.cairo index 1fc81083f..7ac9c2173 100644 --- a/src/governance/timelock/utils/call_impls.cairo +++ b/src/governance/timelock/utils/call_impls.cairo @@ -30,18 +30,12 @@ pub(crate) impl HashCallsImpl, +Drop> of Hash { #[inline(always)] fn eq(lhs: @Call, rhs: @Call) -> bool { - let mut lhs_arr = array![]; - Serde::serialize(lhs, ref lhs_arr); - let mut rhs_arr = array![]; - Serde::serialize(lhs, ref rhs_arr); - lhs_arr == rhs_arr + let Call {to: l_to, selector: l_selector, calldata: l_calldata } = lhs; + let Call {to: r_to, selector: r_selector, calldata: r_calldata } = rhs; + l_to == r_to && l_selector == r_selector && l_calldata == r_calldata } #[inline(always)] fn ne(lhs: @Call, rhs: @Call) -> bool { - let mut lhs_arr = array![]; - Serde::serialize(lhs, ref lhs_arr); - let mut rhs_arr = array![]; - Serde::serialize(lhs, ref rhs_arr); - !(lhs_arr == rhs_arr) + !(lhs == rhs) } } diff --git a/src/tests/governance.cairo b/src/tests/governance.cairo index c983e42d7..3aa8297b3 100644 --- a/src/tests/governance.cairo +++ b/src/tests/governance.cairo @@ -1 +1,2 @@ mod test_timelock; +mod test_utils; diff --git a/src/tests/governance/test_utils.cairo b/src/tests/governance/test_utils.cairo new file mode 100644 index 000000000..d099e0c44 --- /dev/null +++ b/src/tests/governance/test_utils.cairo @@ -0,0 +1,108 @@ +use openzeppelin::governance::timelock::utils::call_impls::CallPartialEq; +use starknet::account::Call; +use starknet::contract_address_const; + +// +// eq +// + +#[test] +fn test_eq_calls_no_calldata() { + let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![].span() }; + let call_2 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![].span() }; + assert_eq!(call_1, call_2); +} + +#[test] +fn test_eq_calls_with_calldata() { + let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; + let call_2 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; + assert_eq!(call_1, call_2); +} + +#[test] +#[should_panic] +fn test_eq_calls_ne_to() { + let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![].span() }; + let call_2 = Call { to: contract_address_const::<2>(), selector: 1, calldata: array![].span() }; + assert_eq!(call_1, call_2); +} + +#[test] +#[should_panic] +fn test_eq_calls_ne_selector() { + let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![].span() }; + let call_2 = Call { to: contract_address_const::<1>(), selector: 2, calldata: array![].span() }; + assert_eq!(call_1, call_2); +} + +#[test] +#[should_panic] +fn test_eq_calls_gt_calldata() { + let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1].span() }; + let call_2 = Call { to: contract_address_const::<1>(), selector: 2, calldata: array![].span() }; + assert_eq!(call_1, call_2); +} + +#[test] +#[should_panic] +fn test_eq_calls_lt_calldata() { + let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1].span() }; + let call_2 = Call { to: contract_address_const::<1>(), selector: 2, calldata: array![1, 2].span() }; + assert_eq!(call_1, call_2); +} + +#[test] +#[should_panic] +fn test_eq_calls_ne_calldata() { + let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2].span() }; + let call_2 = Call { to: contract_address_const::<1>(), selector: 2, calldata: array![2, 1].span() }; + assert_eq!(call_1, call_2); +} + +// +// ne +// + +#[test] +fn test_ne_calls_to() { + let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; + let call_2 = Call { to: contract_address_const::<2>(), selector: 1, calldata: array![1, 2, 3].span() }; + assert_ne!(call_1, call_2); +} + +#[test] +fn test_ne_calls_selector() { + let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; + let call_2 = Call { to: contract_address_const::<1>(), selector: 2, calldata: array![1, 2, 3].span() }; + assert_ne!(call_1, call_2); +} + +#[test] +fn test_ne_calls_gt_calldata() { + let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; + let call_2 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2].span() }; + assert_ne!(call_1, call_2); +} + +#[test] +fn test_ne_calls_lt_calldata() { + let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2].span() }; + let call_2 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; + assert_ne!(call_1, call_2); +} + +#[test] +fn test_ne_calls_eq_len_calldata() { + let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; + let call_2 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![3, 2, 1].span() }; + assert_ne!(call_1, call_2); +} + +#[test] +#[should_panic] +fn test_ne_calls_when_eq() { + let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; + let call_2 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; + assert_ne!(call_1, call_2); +} From 09a11b006b4cd1cdf22cc2541a33f5e12170398e Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 16 Jul 2024 19:57:17 -0500 Subject: [PATCH 095/103] fix fmt --- .../timelock/utils/call_impls.cairo | 4 +- src/tests/governance/test_utils.cairo | 76 ++++++++++++++----- 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/src/governance/timelock/utils/call_impls.cairo b/src/governance/timelock/utils/call_impls.cairo index 7ac9c2173..428259e57 100644 --- a/src/governance/timelock/utils/call_impls.cairo +++ b/src/governance/timelock/utils/call_impls.cairo @@ -30,8 +30,8 @@ pub(crate) impl HashCallsImpl, +Drop> of Hash { #[inline(always)] fn eq(lhs: @Call, rhs: @Call) -> bool { - let Call {to: l_to, selector: l_selector, calldata: l_calldata } = lhs; - let Call {to: r_to, selector: r_selector, calldata: r_calldata } = rhs; + let Call { to: l_to, selector: l_selector, calldata: l_calldata } = lhs; + let Call { to: r_to, selector: r_selector, calldata: r_calldata } = rhs; l_to == r_to && l_selector == r_selector && l_calldata == r_calldata } #[inline(always)] diff --git a/src/tests/governance/test_utils.cairo b/src/tests/governance/test_utils.cairo index d099e0c44..874fe2615 100644 --- a/src/tests/governance/test_utils.cairo +++ b/src/tests/governance/test_utils.cairo @@ -15,8 +15,12 @@ fn test_eq_calls_no_calldata() { #[test] fn test_eq_calls_with_calldata() { - let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; - let call_2 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; + let call_1 = Call { + to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() + }; + let call_2 = Call { + to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() + }; assert_eq!(call_1, call_2); } @@ -39,7 +43,9 @@ fn test_eq_calls_ne_selector() { #[test] #[should_panic] fn test_eq_calls_gt_calldata() { - let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1].span() }; + let call_1 = Call { + to: contract_address_const::<1>(), selector: 1, calldata: array![1].span() + }; let call_2 = Call { to: contract_address_const::<1>(), selector: 2, calldata: array![].span() }; assert_eq!(call_1, call_2); } @@ -47,16 +53,24 @@ fn test_eq_calls_gt_calldata() { #[test] #[should_panic] fn test_eq_calls_lt_calldata() { - let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1].span() }; - let call_2 = Call { to: contract_address_const::<1>(), selector: 2, calldata: array![1, 2].span() }; + let call_1 = Call { + to: contract_address_const::<1>(), selector: 1, calldata: array![1].span() + }; + let call_2 = Call { + to: contract_address_const::<1>(), selector: 2, calldata: array![1, 2].span() + }; assert_eq!(call_1, call_2); } #[test] #[should_panic] fn test_eq_calls_ne_calldata() { - let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2].span() }; - let call_2 = Call { to: contract_address_const::<1>(), selector: 2, calldata: array![2, 1].span() }; + let call_1 = Call { + to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2].span() + }; + let call_2 = Call { + to: contract_address_const::<1>(), selector: 2, calldata: array![2, 1].span() + }; assert_eq!(call_1, call_2); } @@ -66,43 +80,67 @@ fn test_eq_calls_ne_calldata() { #[test] fn test_ne_calls_to() { - let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; - let call_2 = Call { to: contract_address_const::<2>(), selector: 1, calldata: array![1, 2, 3].span() }; + let call_1 = Call { + to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() + }; + let call_2 = Call { + to: contract_address_const::<2>(), selector: 1, calldata: array![1, 2, 3].span() + }; assert_ne!(call_1, call_2); } #[test] fn test_ne_calls_selector() { - let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; - let call_2 = Call { to: contract_address_const::<1>(), selector: 2, calldata: array![1, 2, 3].span() }; + let call_1 = Call { + to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() + }; + let call_2 = Call { + to: contract_address_const::<1>(), selector: 2, calldata: array![1, 2, 3].span() + }; assert_ne!(call_1, call_2); } #[test] fn test_ne_calls_gt_calldata() { - let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; - let call_2 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2].span() }; + let call_1 = Call { + to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() + }; + let call_2 = Call { + to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2].span() + }; assert_ne!(call_1, call_2); } #[test] fn test_ne_calls_lt_calldata() { - let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2].span() }; - let call_2 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; + let call_1 = Call { + to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2].span() + }; + let call_2 = Call { + to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() + }; assert_ne!(call_1, call_2); } #[test] fn test_ne_calls_eq_len_calldata() { - let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; - let call_2 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![3, 2, 1].span() }; + let call_1 = Call { + to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() + }; + let call_2 = Call { + to: contract_address_const::<1>(), selector: 1, calldata: array![3, 2, 1].span() + }; assert_ne!(call_1, call_2); } #[test] #[should_panic] fn test_ne_calls_when_eq() { - let call_1 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; - let call_2 = Call { to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() }; + let call_1 = Call { + to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() + }; + let call_2 = Call { + to: contract_address_const::<1>(), selector: 1, calldata: array![1, 2, 3].span() + }; assert_ne!(call_1, call_2); } From 986e8888dd770fa2488f633c0f4264fc6fd96e23 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 16 Jul 2024 20:15:00 -0500 Subject: [PATCH 096/103] simplify mixin fns --- src/governance/timelock/timelock_controller.cairo | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index d8f38b29f..7e808fd8a 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -495,34 +495,29 @@ pub mod TimelockControllerComponent { fn hasRole( self: @ComponentState, role: felt252, account: ContractAddress ) -> bool { - let access_control = get_dep_component!(self, AccessControl); - access_control.hasRole(role, account) + Self::has_role(self, role, account) } fn getRoleAdmin(self: @ComponentState, role: felt252) -> felt252 { - let access_control = get_dep_component!(self, AccessControl); - access_control.getRoleAdmin(role) + Self::getRoleAdmin(self, role) } fn grantRole( ref self: ComponentState, role: felt252, account: ContractAddress ) { - let mut access_control = get_dep_component_mut!(ref self, AccessControl); - access_control.grantRole(role, account); + Self::grant_role(ref self, role, account); } fn revokeRole( ref self: ComponentState, role: felt252, account: ContractAddress ) { - let mut access_control = get_dep_component_mut!(ref self, AccessControl); - access_control.revokeRole(role, account); + Self::revoke_role(ref self, role, account); } fn renounceRole( ref self: ComponentState, role: felt252, account: ContractAddress ) { - let mut access_control = get_dep_component_mut!(ref self, AccessControl); - access_control.renounceRole(role, account); + Self::renounce_role(ref self, role, account); } } From 03f0a222681a48371c4119f6cd5c773d6ee6dee6 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 17 Jul 2024 10:52:40 -0500 Subject: [PATCH 097/103] switch Poseidon to Pedersen --- src/governance/timelock/timelock_controller.cairo | 6 +++--- src/tests/governance/test_timelock.cairo | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 7e808fd8a..494692025 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -16,7 +16,7 @@ pub mod TimelockControllerComponent { use core::hash::{HashStateTrait, HashStateExTrait}; use core::num::traits::Zero; - use core::poseidon::PoseidonTrait; + use core::pedersen::PedersenTrait; use openzeppelin::access::accesscontrol::AccessControlComponent::InternalTrait as AccessControlInternalTrait; use openzeppelin::access::accesscontrol::AccessControlComponent::{ AccessControlImpl, AccessControlCamelImpl @@ -176,7 +176,7 @@ pub mod TimelockControllerComponent { fn hash_operation( self: @ComponentState, call: Call, predecessor: felt252, salt: felt252 ) -> felt252 { - PoseidonTrait::new() + PedersenTrait::new(0) .update_with(call) .update_with(predecessor) .update_with(salt) @@ -190,7 +190,7 @@ pub mod TimelockControllerComponent { predecessor: felt252, salt: felt252 ) -> felt252 { - PoseidonTrait::new() + PedersenTrait::new(0) .update_with(calls) .update_with(predecessor) .update_with(salt) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index ec78c44cd..229432a6f 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -1,6 +1,6 @@ use core::hash::{HashStateTrait, HashStateExTrait}; use core::num::traits::Zero; -use core::poseidon::PoseidonTrait; +use core::pedersen::PedersenTrait; use openzeppelin::access::accesscontrol::AccessControlComponent::{ AccessControlImpl, InternalImpl as AccessControlInternalImpl }; @@ -183,7 +183,7 @@ fn test_hash_operation() { let hashed_operation = timelock.hash_operation(call, predecessor, salt); // Manually set hash elements - let mut expected_hash = PoseidonTrait::new() + let mut expected_hash = PedersenTrait::new(0) .update_with(target.contract_address) // call::to .update_with(selector!("set_number")) // call::selector .update_with(1) // call::calldata.len @@ -213,7 +213,7 @@ fn test_hash_operation_batch() { let hashed_operation = timelock.hash_operation_batch(calls, predecessor, salt); // Manually set hash elements - let mut expected_hash = PoseidonTrait::new() + let mut expected_hash = PedersenTrait::new(0) .update_with(3) // total number of Calls .update_with(target.contract_address) // call::to .update_with(selector!("set_number")) // call::selector From d4d10df408ddde0c99f3694a320afa034c37c3b8 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 17 Jul 2024 17:11:45 -0500 Subject: [PATCH 098/103] make admin a req in initializer --- .../timelock/timelock_controller.cairo | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 494692025..0fe4393cb 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -536,19 +536,21 @@ pub mod TimelockControllerComponent { /// - `min_delay`: initial minimum delay in seconds for operations. /// - `proposers`: accounts to be granted proposer and canceller roles. /// - `executors`: accounts to be granted executor role. - /// - `admin`: optional account to be granted admin role; disable with zero address. + /// - `admin`: account to be granted admin role. /// - /// WARNING: The optional admin can aid with initial configuration of roles after deployment - /// without being subject to delay, but this role should be subsequently renounced in favor - /// of administration through timelocked proposals. + /// WARNING: An initial admin is required in this implementation in order to grant + /// the timelock contract itself with the `DEFAULT_ADMIN_ROLE`. The contract's address cannot + /// be set during construction (without precomputing the address). + /// Further, the admin may also aid with the initial configuration of other roles after + /// deployment without being subject to delay, but this role should be subsequently + /// renounced in favor of administration through timelocked proposals. /// /// Emits two `RoleGranted` events for each account in `proposers` with `PROPOSER_ROLE` /// admin `CANCELLER_ROLE` roles. /// /// Emits a `RoleGranted` event for each account in `executors` with `EXECUTOR_ROLE` role. /// - /// May emit a `RoleGranted` event for `admin` with `DEFAULT_ADMIN_ROLE` role (if `admin` is - /// not zero). + /// Emits a `RoleGranted` event for `admin` with `DEFAULT_ADMIN_ROLE`. /// /// Emits `MinDelayChanged` event. fn initializer( @@ -558,15 +560,10 @@ pub mod TimelockControllerComponent { executors: Span, admin: ContractAddress ) { - // Register access control ID and self as default admin + // Register access control ID and admin as initial default admin let mut access_component = get_dep_component_mut!(ref self, AccessControl); access_component.initializer(); - access_component._grant_role(DEFAULT_ADMIN_ROLE, starknet::get_contract_address()); - - // Optional admin - if admin != Zero::zero() { - access_component._grant_role(DEFAULT_ADMIN_ROLE, admin) - }; + access_component._grant_role(DEFAULT_ADMIN_ROLE, admin); // Register proposers and cancellers for proposer in proposers { From 862a5c4ac9337ff63003d4c311d6ecbb155c51f8 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 17 Jul 2024 17:12:43 -0500 Subject: [PATCH 099/103] update tests with admin --- src/tests/governance/test_timelock.cairo | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 229432a6f..4809585a5 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -1102,21 +1102,24 @@ fn test_update_delay_scheduled() { // #[test] -fn test_initializer_single_role_and_no_admin() { +fn test_initializer_single_role() { let mut state = COMPONENT_STATE(); let contract_state = CONTRACT_STATE(); let min_delay = MIN_DELAY; let proposers = array![PROPOSER()].span(); let executors = array![EXECUTOR()].span(); - let admin_zero = ZERO(); + let admin = ADMIN(); - state.initializer(min_delay, proposers, executors, admin_zero); - assert!(contract_state.has_role(DEFAULT_ADMIN_ROLE, admin_zero)); + state.initializer(min_delay, proposers, executors, admin); + assert!(contract_state.has_role(PROPOSER_ROLE, PROPOSER())); + assert!(contract_state.has_role(CANCELLER_ROLE, PROPOSER())); + assert!(contract_state.has_role(EXECUTOR_ROLE, EXECUTOR())); + assert!(contract_state.has_role(DEFAULT_ADMIN_ROLE, admin)); } #[test] -fn test_initializer_multiple_roles_and_admin() { +fn test_initializer_multiple_roles() { let mut state = COMPONENT_STATE(); let contract_state = CONTRACT_STATE(); let min_delay = MIN_DELAY; @@ -1174,19 +1177,19 @@ fn test_initializer_min_delay() { let proposers = array![PROPOSER()].span(); let executors = array![EXECUTOR()].span(); - let admin_zero = ZERO(); + let admin = ADMIN(); - state.initializer(min_delay, proposers, executors, admin_zero); + state.initializer(min_delay, proposers, executors, admin); // Check minimum delay is set let delay = state.get_min_delay(); assert_eq!(delay, MIN_DELAY); // The initializer emits 4 `RoleGranted` events prior to `MinDelayChanged`: - // - Self administration // - 1 proposer // - 1 canceller // - 1 executor + // - 1 admin utils::drop_events(ZERO(), 4); assert_only_event_delay_change(ZERO(), 0, MIN_DELAY); } From 2de0eee3606918c87b5de5d819fd9d6ec77ad5a5 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 17 Jul 2024 17:13:07 -0500 Subject: [PATCH 100/103] fix fmt --- src/governance/timelock/timelock_controller.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 0fe4393cb..3f5d0435b 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -539,8 +539,8 @@ pub mod TimelockControllerComponent { /// - `admin`: account to be granted admin role. /// /// WARNING: An initial admin is required in this implementation in order to grant - /// the timelock contract itself with the `DEFAULT_ADMIN_ROLE`. The contract's address cannot - /// be set during construction (without precomputing the address). + /// the timelock contract itself with the `DEFAULT_ADMIN_ROLE`. The contract's address + /// cannot be set during construction (without precomputing the address). /// Further, the admin may also aid with the initial configuration of other roles after /// deployment without being subject to delay, but this role should be subsequently /// renounced in favor of administration through timelocked proposals. From edfe5fb3156e80472c296dd238bffd9e2809e1e5 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 17 Jul 2024 17:22:07 -0500 Subject: [PATCH 101/103] fix comment --- src/tests/governance/test_timelock.cairo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 4809585a5..28c4a9f09 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -135,9 +135,9 @@ fn deploy_timelock() -> TimelockABIDispatcher { let address = utils::deploy(TimelockControllerMock::TEST_CLASS_HASH, calldata); // Events dropped: - // - 5 RoleGranted: self, proposer, canceller, executor, admin - // - MinDelayChanged - utils::drop_events(address, 6); + // - 4 RoleGranted: proposer, canceller, executor, admin + // - 1 MinDelayChanged + utils::drop_events(address, 5); TimelockABIDispatcher { contract_address: address } } From 0f244160ea77e0014d7ab61948f888086c7a6591 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 18 Jul 2024 11:30:13 -0500 Subject: [PATCH 102/103] undo changes --- .../timelock/timelock_controller.cairo | 23 +++++++++-------- src/tests/governance/test_timelock.cairo | 25 ++++++++----------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/governance/timelock/timelock_controller.cairo b/src/governance/timelock/timelock_controller.cairo index 3f5d0435b..494692025 100644 --- a/src/governance/timelock/timelock_controller.cairo +++ b/src/governance/timelock/timelock_controller.cairo @@ -536,21 +536,19 @@ pub mod TimelockControllerComponent { /// - `min_delay`: initial minimum delay in seconds for operations. /// - `proposers`: accounts to be granted proposer and canceller roles. /// - `executors`: accounts to be granted executor role. - /// - `admin`: account to be granted admin role. + /// - `admin`: optional account to be granted admin role; disable with zero address. /// - /// WARNING: An initial admin is required in this implementation in order to grant - /// the timelock contract itself with the `DEFAULT_ADMIN_ROLE`. The contract's address - /// cannot be set during construction (without precomputing the address). - /// Further, the admin may also aid with the initial configuration of other roles after - /// deployment without being subject to delay, but this role should be subsequently - /// renounced in favor of administration through timelocked proposals. + /// WARNING: The optional admin can aid with initial configuration of roles after deployment + /// without being subject to delay, but this role should be subsequently renounced in favor + /// of administration through timelocked proposals. /// /// Emits two `RoleGranted` events for each account in `proposers` with `PROPOSER_ROLE` /// admin `CANCELLER_ROLE` roles. /// /// Emits a `RoleGranted` event for each account in `executors` with `EXECUTOR_ROLE` role. /// - /// Emits a `RoleGranted` event for `admin` with `DEFAULT_ADMIN_ROLE`. + /// May emit a `RoleGranted` event for `admin` with `DEFAULT_ADMIN_ROLE` role (if `admin` is + /// not zero). /// /// Emits `MinDelayChanged` event. fn initializer( @@ -560,10 +558,15 @@ pub mod TimelockControllerComponent { executors: Span, admin: ContractAddress ) { - // Register access control ID and admin as initial default admin + // Register access control ID and self as default admin let mut access_component = get_dep_component_mut!(ref self, AccessControl); access_component.initializer(); - access_component._grant_role(DEFAULT_ADMIN_ROLE, admin); + access_component._grant_role(DEFAULT_ADMIN_ROLE, starknet::get_contract_address()); + + // Optional admin + if admin != Zero::zero() { + access_component._grant_role(DEFAULT_ADMIN_ROLE, admin) + }; // Register proposers and cancellers for proposer in proposers { diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 28c4a9f09..229432a6f 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -135,9 +135,9 @@ fn deploy_timelock() -> TimelockABIDispatcher { let address = utils::deploy(TimelockControllerMock::TEST_CLASS_HASH, calldata); // Events dropped: - // - 4 RoleGranted: proposer, canceller, executor, admin - // - 1 MinDelayChanged - utils::drop_events(address, 5); + // - 5 RoleGranted: self, proposer, canceller, executor, admin + // - MinDelayChanged + utils::drop_events(address, 6); TimelockABIDispatcher { contract_address: address } } @@ -1102,24 +1102,21 @@ fn test_update_delay_scheduled() { // #[test] -fn test_initializer_single_role() { +fn test_initializer_single_role_and_no_admin() { let mut state = COMPONENT_STATE(); let contract_state = CONTRACT_STATE(); let min_delay = MIN_DELAY; let proposers = array![PROPOSER()].span(); let executors = array![EXECUTOR()].span(); - let admin = ADMIN(); + let admin_zero = ZERO(); - state.initializer(min_delay, proposers, executors, admin); - assert!(contract_state.has_role(PROPOSER_ROLE, PROPOSER())); - assert!(contract_state.has_role(CANCELLER_ROLE, PROPOSER())); - assert!(contract_state.has_role(EXECUTOR_ROLE, EXECUTOR())); - assert!(contract_state.has_role(DEFAULT_ADMIN_ROLE, admin)); + state.initializer(min_delay, proposers, executors, admin_zero); + assert!(contract_state.has_role(DEFAULT_ADMIN_ROLE, admin_zero)); } #[test] -fn test_initializer_multiple_roles() { +fn test_initializer_multiple_roles_and_admin() { let mut state = COMPONENT_STATE(); let contract_state = CONTRACT_STATE(); let min_delay = MIN_DELAY; @@ -1177,19 +1174,19 @@ fn test_initializer_min_delay() { let proposers = array![PROPOSER()].span(); let executors = array![EXECUTOR()].span(); - let admin = ADMIN(); + let admin_zero = ZERO(); - state.initializer(min_delay, proposers, executors, admin); + state.initializer(min_delay, proposers, executors, admin_zero); // Check minimum delay is set let delay = state.get_min_delay(); assert_eq!(delay, MIN_DELAY); // The initializer emits 4 `RoleGranted` events prior to `MinDelayChanged`: + // - Self administration // - 1 proposer // - 1 canceller // - 1 executor - // - 1 admin utils::drop_events(ZERO(), 4); assert_only_event_delay_change(ZERO(), 0, MIN_DELAY); } From c485f78e417bdf71391a1b215259261ffaf2eac1 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 18 Jul 2024 12:05:43 -0500 Subject: [PATCH 103/103] add no admin initializer test --- src/tests/governance/test_timelock.cairo | 30 ++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/tests/governance/test_timelock.cairo b/src/tests/governance/test_timelock.cairo index 229432a6f..c654c2082 100644 --- a/src/tests/governance/test_timelock.cairo +++ b/src/tests/governance/test_timelock.cairo @@ -1102,17 +1102,20 @@ fn test_update_delay_scheduled() { // #[test] -fn test_initializer_single_role_and_no_admin() { +fn test_initializer_single_role_and_admin() { let mut state = COMPONENT_STATE(); let contract_state = CONTRACT_STATE(); let min_delay = MIN_DELAY; let proposers = array![PROPOSER()].span(); let executors = array![EXECUTOR()].span(); - let admin_zero = ZERO(); + let admin = ADMIN(); - state.initializer(min_delay, proposers, executors, admin_zero); - assert!(contract_state.has_role(DEFAULT_ADMIN_ROLE, admin_zero)); + state.initializer(min_delay, proposers, executors, admin); + assert!(contract_state.has_role(PROPOSER_ROLE, *proposers.at(0))); + assert!(contract_state.has_role(CANCELLER_ROLE, *proposers.at(0))); + assert!(contract_state.has_role(EXECUTOR_ROLE, *executors.at(0))); + assert!(contract_state.has_role(DEFAULT_ADMIN_ROLE, admin)); } #[test] @@ -1147,6 +1150,25 @@ fn test_initializer_multiple_roles_and_admin() { }; } +#[test] +fn test_initializer_no_admin() { + let mut state = COMPONENT_STATE(); + let contract_state = CONTRACT_STATE(); + let min_delay = MIN_DELAY; + + let proposers = array![PROPOSER()].span(); + let executors = array![EXECUTOR()].span(); + let admin_zero = ZERO(); + + // The initializer grants the timelock contract address the `DEFAULT_ADMIN_ROLE` + // therefore, we need to set the address since it's not deployed in this context + testing::set_contract_address(contract_address_const::<'TIMELOCK_ADDRESS'>()); + state.initializer(min_delay, proposers, executors, admin_zero); + + let admin_does_not_have_role = !contract_state.has_role(DEFAULT_ADMIN_ROLE, admin_zero); + assert!(admin_does_not_have_role); +} + #[test] fn test_initializer_supported_interfaces() { let mut state = COMPONENT_STATE();