diff --git a/src/factory.cairo b/src/factory.cairo index 5c554b2..7ab3d54 100644 --- a/src/factory.cairo +++ b/src/factory.cairo @@ -2,7 +2,7 @@ use governance::airdrop::{IAirdropDispatcher}; use governance::governance_token::{IGovernanceTokenDispatcher}; use governance::governor::{Config as GovernorConfig}; use governance::governor::{IGovernorDispatcher}; -use governance::timelock::{ITimelockDispatcher}; +use governance::timelock::{ITimelockDispatcher, TimelockConfig}; use starknet::{ContractAddress}; #[derive(Copy, Drop, Serde)] @@ -11,12 +11,6 @@ struct AirdropConfig { total: u128, } -#[derive(Copy, Drop, Serde)] -struct TimelockConfig { - delay: u64, - window: u64, -} - #[derive(Copy, Drop, Serde)] struct DeploymentParameters { name: felt252, diff --git a/src/factory_test.cairo b/src/factory_test.cairo index b90679f..1566df0 100644 --- a/src/factory_test.cairo +++ b/src/factory_test.cairo @@ -7,17 +7,14 @@ use governance::airdrop::{Airdrop}; use governance::airdrop::{IAirdropDispatcherTrait}; use governance::factory::{ IFactoryDispatcher, IFactoryDispatcherTrait, Factory, DeploymentParameters, AirdropConfig, - TimelockConfig, }; use governance::governance_token::{GovernanceToken}; use governance::governance_token::{IGovernanceTokenDispatcherTrait}; use governance::governor::{Config as GovernorConfig}; use governance::governor::{Governor}; - use governance::governor::{IGovernorDispatcherTrait}; use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; -use governance::timelock::{ITimelockDispatcherTrait}; -use governance::timelock::{Timelock}; +use governance::timelock::{Timelock, ITimelockDispatcherTrait, TimelockConfig}; use starknet::class_hash::{Felt252TryIntoClassHash}; use starknet::testing::{set_contract_address, set_block_timestamp, pop_log}; use starknet::{ @@ -99,5 +96,6 @@ fn test_deploy() { }, 'governor.config' ); - assert(result.timelock.get_configuration() == (320, 60), 'timelock config'); + assert(result.timelock.get_configuration().delay == 320, 'timelock config (delay)'); + assert(result.timelock.get_configuration().window == 60, 'timelock config (window)'); } diff --git a/src/lib.cairo b/src/lib.cairo index a3145b0..90dd1f1 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -20,3 +20,4 @@ mod test_utils; mod timelock; #[cfg(test)] mod timelock_test; +mod utils; diff --git a/src/timelock.cairo b/src/timelock.cairo index c6e54fc..1931c5a 100644 --- a/src/timelock.cairo +++ b/src/timelock.cairo @@ -1,6 +1,47 @@ +use core::option::OptionTrait; use core::result::ResultTrait; +use core::traits::TryInto; +use governance::utils::timestamps::{ThreeU64TupleStorePacking, TwoU64TupleStorePacking}; use starknet::account::{Call}; -use starknet::{ContractAddress}; +use starknet::contract_address::{ContractAddress}; +use starknet::storage_access::{StorePacking}; + +#[derive(Copy, Drop, Serde)] +struct ExecutionState { + started: u64, + executed: u64, + canceled: u64 +} + +impl ExecutionStateStorePacking of StorePacking { + #[inline(always)] + fn pack(value: ExecutionState) -> felt252 { + ThreeU64TupleStorePacking::pack((value.started, value.executed, value.canceled)) + } + #[inline(always)] + fn unpack(value: felt252) -> ExecutionState { + let (started, executed, canceled) = ThreeU64TupleStorePacking::unpack(value); + ExecutionState { started, executed, canceled } + } +} + +#[derive(Copy, Drop, Serde)] +struct TimelockConfig { + delay: u64, + window: u64, +} + +impl TimelockConfigStorePacking of StorePacking { + #[inline(always)] + fn pack(value: TimelockConfig) -> u128 { + TwoU64TupleStorePacking::pack((value.delay, value.window)) + } + #[inline(always)] + fn unpack(value: u128) -> TimelockConfig { + let (delay, window) = TwoU64TupleStorePacking::unpack(value); + TimelockConfig { delay, window } + } +} #[starknet::interface] trait ITimelock { @@ -14,33 +55,38 @@ trait ITimelock { fn execute(ref self: TStorage, calls: Span) -> Array>; // Return the execution window, i.e. the start and end timestamp in which the call can be executed - fn get_execution_window(self: @TStorage, id: felt252) -> (u64, u64); + fn get_execution_window(self: @TStorage, id: felt252) -> ExecutionWindow; // Get the current owner fn get_owner(self: @TStorage) -> ContractAddress; // Returns the delay and the window for call execution - fn get_configuration(self: @TStorage) -> (u64, u64); + fn get_configuration(self: @TStorage) -> TimelockConfig; // Transfer ownership, i.e. the address that can queue and cancel calls. This must be self-called via #queue. fn transfer(ref self: TStorage, to: ContractAddress); // Configure the delay and the window for call execution. This must be self-called via #queue. - fn configure(ref self: TStorage, delay: u64, window: u64); + fn configure(ref self: TStorage, config: TimelockConfig); +} + +#[derive(Copy, Drop, Serde)] +struct ExecutionWindow { + earliest: u64, + latest: u64 } #[starknet::contract] mod Timelock { - use core::array::{ArrayTrait, SpanTrait}; - use core::hash::{LegacyHash}; - use core::num::traits::zero::{Zero}; - use core::result::{ResultTrait}; - use core::traits::{Into}; + use core::hash::LegacyHash; use governance::call_trait::{CallTrait, HashCall}; use starknet::{ get_caller_address, get_contract_address, SyscallResult, syscalls::call_contract_syscall, ContractAddressIntoFelt252, get_block_timestamp }; - use super::{ITimelock, ContractAddress, Call}; + use super::{ + ITimelock, ContractAddress, Call, TimelockConfig, ExecutionState, + TimelockConfigStorePacking, ExecutionStateStorePacking, ExecutionWindow + }; #[derive(starknet::Event, Drop)] struct Queued { @@ -69,18 +115,15 @@ mod Timelock { #[storage] struct Storage { owner: ContractAddress, - delay: u64, - window: u64, - execution_started: LegacyMap, - executed: LegacyMap, - canceled: LegacyMap, + config: TimelockConfig, + // started_executed_canceled + execution_state: LegacyMap, } #[constructor] - fn constructor(ref self: ContractState, owner: ContractAddress, delay: u64, window: u64) { + fn constructor(ref self: ContractState, owner: ContractAddress, config: TimelockConfig) { self.owner.write(owner); - self.delay.write(delay); - self.window.write(window); + self.config.write(config); } // Take a list of calls and convert it to a unique identifier for the execution @@ -113,10 +156,15 @@ mod Timelock { self.check_owner(); let id = to_id(calls); + let execution_state = self.execution_state.read(id); - assert(self.execution_started.read(id).is_zero(), 'ALREADY_QUEUED'); + assert(execution_state.started.is_zero(), 'ALREADY_QUEUED'); - self.execution_started.write(id, get_block_timestamp()); + self + .execution_state + .write( + id, ExecutionState { started: get_block_timestamp(), executed: 0, canceled: 0 } + ); self.emit(Queued { id, calls, }); @@ -125,10 +173,20 @@ mod Timelock { fn cancel(ref self: ContractState, id: felt252) { self.check_owner(); - assert(self.execution_started.read(id).is_non_zero(), 'DOES_NOT_EXIST'); - assert(self.executed.read(id).is_zero(), 'ALREADY_EXECUTED'); - - self.execution_started.write(id, 0); + let execution_state = self.execution_state.read(id); + assert(execution_state.started.is_non_zero(), 'DOES_NOT_EXIST'); + assert(execution_state.executed.is_zero(), 'ALREADY_EXECUTED'); + + self + .execution_state + .write( + id, + ExecutionState { + started: 0, + executed: execution_state.executed, + canceled: execution_state.canceled + } + ); self.emit(Canceled { id, }); } @@ -136,15 +194,26 @@ mod Timelock { fn execute(ref self: ContractState, mut calls: Span) -> Array> { let id = to_id(calls); - assert(self.executed.read(id).is_zero(), 'ALREADY_EXECUTED'); + let execution_state = self.execution_state.read(id); - let (earliest, latest) = self.get_execution_window(id); + assert(execution_state.executed.is_zero(), 'ALREADY_EXECUTED'); + + let execution_window = self.get_execution_window(id); let time_current = get_block_timestamp(); - assert(time_current >= earliest, 'TOO_EARLY'); - assert(time_current < latest, 'TOO_LATE'); + assert(time_current >= execution_window.earliest, 'TOO_EARLY'); + assert(time_current < execution_window.latest, 'TOO_LATE'); - self.executed.write(id, time_current); + self + .execution_state + .write( + id, + ExecutionState { + started: execution_state.started, + executed: time_current, + canceled: execution_state.canceled + } + ); let mut results: Array> = ArrayTrait::new(); @@ -160,26 +229,27 @@ mod Timelock { results } - fn get_execution_window(self: @ContractState, id: felt252) -> (u64, u64) { - let start_time = self.execution_started.read(id); + fn get_execution_window(self: @ContractState, id: felt252) -> ExecutionWindow { + let start_time = self.execution_state.read(id).started; // this is how we prevent the 0 timestamp from being considered valid assert(start_time != 0, 'DOES_NOT_EXIST'); - let (delay, window) = (self.get_configuration()); + let configuration = (self.get_configuration()); + + let earliest = start_time + configuration.delay; - let earliest = start_time + delay; - let latest = earliest + window; + let latest = earliest + configuration.window; - (earliest, latest) + ExecutionWindow { earliest, latest } } fn get_owner(self: @ContractState) -> ContractAddress { self.owner.read() } - fn get_configuration(self: @ContractState) -> (u64, u64) { - (self.delay.read(), self.window.read()) + fn get_configuration(self: @ContractState) -> TimelockConfig { + self.config.read() } fn transfer(ref self: ContractState, to: ContractAddress) { @@ -188,11 +258,10 @@ mod Timelock { self.owner.write(to); } - fn configure(ref self: ContractState, delay: u64, window: u64) { + fn configure(ref self: ContractState, config: TimelockConfig) { self.check_self_call(); - self.delay.write(delay); - self.window.write(window); + self.config.write(config); } } } diff --git a/src/timelock_test.cairo b/src/timelock_test.cairo index 94ace1b..b36050f 100644 --- a/src/timelock_test.cairo +++ b/src/timelock_test.cairo @@ -1,12 +1,11 @@ -use core::array::{Array, ArrayTrait, SpanTrait}; -use core::option::{OptionTrait}; -use core::result::{Result, ResultTrait}; -use core::traits::{TryInto}; - -use governance::governance_token::{IGovernanceTokenDispatcher, IGovernanceTokenDispatcherTrait}; -use governance::governance_token_test::{deploy as deploy_token}; +use array::{Array, ArrayTrait, SpanTrait}; +use debug::PrintTrait; +use governance::governance_token_test::{deploy as deploy_token, IGovernanceTokenDispatcher}; use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; -use governance::timelock::{ITimelockDispatcher, ITimelockDispatcherTrait, Timelock}; +use governance::timelock::{ + ITimelockDispatcher, ITimelockDispatcherTrait, Timelock, TimelockConfig, + TimelockConfigStorePacking, ExecutionState, ExecutionStateStorePacking +}; use starknet::account::{Call}; use starknet::class_hash::Felt252TryIntoClassHash; use starknet::{ @@ -30,9 +29,9 @@ fn deploy(owner: ContractAddress, delay: u64, window: u64) -> ITimelockDispatche fn test_deploy() { let timelock = deploy(contract_address_const::<2300>(), 10239, 3600); - let (window, delay) = timelock.get_configuration(); - assert(window == 10239, 'window'); - assert(delay == 3600, 'delay'); + let configuration = timelock.get_configuration(); + assert(configuration.delay == 10239, 'delay'); + assert(configuration.window == 3600, 'window'); let owner = timelock.get_owner(); assert(owner == contract_address_const::<2300>(), 'owner'); } @@ -70,9 +69,9 @@ fn test_queue_execute() { let id = timelock.queue(single_call(transfer_call(token, recipient, 500_u256))); - let (earliest, latest) = timelock.get_execution_window(id); - assert(earliest == 86401, 'earliest'); - assert(latest == 90001, 'latest'); + let execution_window = timelock.get_execution_window(id); + assert(execution_window.earliest == 86401, 'earliest'); + assert(execution_window.latest == 90001, 'latest'); set_block_timestamp(86401); @@ -134,8 +133,8 @@ fn test_queue_executed_too_early() { let id = timelock.queue(single_call(transfer_call(token, recipient, 500_u256))); - let (earliest, latest) = timelock.get_execution_window(id); - set_block_timestamp(earliest - 1); + let execution_window = timelock.get_execution_window(id); + set_block_timestamp(execution_window.earliest - 1); timelock.execute(single_call(transfer_call(token, recipient, 500_u256))); } @@ -153,7 +152,7 @@ fn test_queue_executed_too_late() { let id = timelock.queue(single_call(transfer_call(token, recipient, 500_u256))); - let (earliest, latest) = timelock.get_execution_window(id); - set_block_timestamp(latest); + let execution_window = timelock.get_execution_window(id); + set_block_timestamp(execution_window.latest); timelock.execute(single_call(transfer_call(token, recipient, 500_u256))); } diff --git a/src/utils.cairo b/src/utils.cairo new file mode 100644 index 0000000..ea30424 --- /dev/null +++ b/src/utils.cairo @@ -0,0 +1 @@ +mod timestamps; diff --git a/src/utils/timestamps.cairo b/src/utils/timestamps.cairo new file mode 100644 index 0000000..7c275f0 --- /dev/null +++ b/src/utils/timestamps.cairo @@ -0,0 +1,31 @@ +use core::integer::{u128_to_felt252, u64_try_from_felt252, u128_safe_divmod}; +use starknet::storage_access::{StorePacking}; + +const TWO_POW_64: u128 = 0x10000000000000000; + +impl ThreeU64TupleStorePacking of StorePacking<(u64, u64, u64), felt252> { + #[inline(always)] + fn pack(value: (u64, u64, u64)) -> felt252 { + let (a, b, c) = value; + u256 { low: TwoU64TupleStorePacking::pack((a, b)), high: c.into() }.try_into().unwrap() + } + #[inline(always)] + fn unpack(value: felt252) -> (u64, u64, u64) { + let u256_value: u256 = value.into(); + let (a, b) = TwoU64TupleStorePacking::unpack(u256_value.low); + (a, b, (u256_value.high).try_into().unwrap()) + } +} + +impl TwoU64TupleStorePacking of StorePacking<(u64, u64), u128> { + #[inline(always)] + fn pack(value: (u64, u64)) -> u128 { + let (a, b) = value; + a.into() + b.into() * TWO_POW_64 + } + #[inline(always)] + fn unpack(value: u128) -> (u64, u64) { + let (q, r) = u128_safe_divmod(value, TWO_POW_64.try_into().unwrap()); + (r.try_into().unwrap(), q.try_into().unwrap()) + } +}