Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

timelock optimization #24

Merged
merged 11 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions src/factory.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,14 @@ use governance::governor::{Config as GovernorConfig};
use governance::governor::{IGovernorDispatcher};
use governance::governance_token::{IGovernanceTokenDispatcher};
use governance::airdrop::{IAirdropDispatcher};
use governance::timelock::{ITimelockDispatcher};
use governance::timelock::{ITimelockDispatcher, TimelockConfig};

#[derive(Copy, Drop, Serde)]
struct AirdropConfig {
root: felt252,
total: u128,
}

#[derive(Copy, Drop, Serde)]
struct TimelockConfig {
delay: u64,
window: u64,
}

#[derive(Copy, Drop, Serde)]
struct DeploymentParameters {
name: felt252,
Expand Down
6 changes: 3 additions & 3 deletions src/factory_test.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait};
use governance::governor::{Config as GovernorConfig};
use governance::factory::{
IFactoryDispatcher, IFactoryDispatcherTrait, Factory, DeploymentParameters, AirdropConfig,
TimelockConfig,
};
use governance::governance_token::{GovernanceToken};
use governance::governor::{Governor};
use governance::timelock::{Timelock};
use governance::timelock::{Timelock, TimelockConfig};
use governance::airdrop::{Airdrop};
use starknet::{
get_contract_address, deploy_syscall, ClassHash, contract_address_const, ContractAddress,
Expand Down Expand Up @@ -100,5 +99,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)');
}
2 changes: 1 addition & 1 deletion src/governor.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ mod Governor {
let data = call.execute();

self.emit(Executed { id, });

data
}

Expand Down
1 change: 1 addition & 0 deletions src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod governance_token;
mod governor;
mod interfaces;
mod timelock;
mod utils;

#[cfg(test)]
mod airdrop_test;
Expand Down
141 changes: 107 additions & 34 deletions src/timelock.cairo
Original file line number Diff line number Diff line change
@@ -1,6 +1,47 @@
use core::option::OptionTrait;
use core::traits::TryInto;
use core::result::ResultTrait;
use starknet::{ContractAddress};
use starknet::account::{Call};
use starknet::storage_access::{StorePacking};
use governance::utils::timestamps::{ThreeU64TupleStorePacking, TwoU64TupleStorePacking};

#[derive(Copy, Drop, Serde)]
struct ExecutionState {
started: u64,
executed: u64,
canceled: u64
}

impl ExecutionStateStorePacking of StorePacking<ExecutionState, felt252> {
#[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<TimelockConfig, u128> {
#[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<TStorage> {
Expand All @@ -14,23 +55,32 @@ trait ITimelock<TStorage> {
fn execute(ref self: TStorage, calls: Span<Call>) -> Array<Span<felt252>>;

// 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 super::{ITimelock, ContractAddress, Call};
use super::{
ITimelock, ContractAddress, Call, TimelockConfig, ExecutionState,
TimelockConfigStorePacking, ExecutionStateStorePacking, ExecutionWindow
};
use governance::call_trait::{CallTrait, HashCall};
use hash::{LegacyHash};
use array::{ArrayTrait, SpanTrait};
Expand Down Expand Up @@ -69,18 +119,15 @@ mod Timelock {
#[storage]
struct Storage {
owner: ContractAddress,
delay: u64,
window: u64,
execution_started: LegacyMap<felt252, u64>,
executed: LegacyMap<felt252, u64>,
canceled: LegacyMap<felt252, u64>,
config: TimelockConfig,
// started_executed_canceled
execution_state: LegacyMap<felt252, ExecutionState>,
}

#[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
Expand Down Expand Up @@ -113,10 +160,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, });

Expand All @@ -125,26 +177,47 @@ 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, });
}

fn execute(ref self: ContractState, mut calls: Span<Call>) -> Array<Span<felt252>> {
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this causes a second read of the same slot

not super costly, but wasteful
better to revert if already executed or canceled from this function, since there is no execution window

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<Span<felt252>> = ArrayTrait::new();

Expand All @@ -160,26 +233,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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm maybe we could just check that canceled is nonzero as well and remove the other logic changes. sorry, i realize this is out of scope for the initial pr.


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) {
Expand All @@ -188,11 +262,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);
}
}
}
25 changes: 14 additions & 11 deletions src/timelock_test.cairo
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use array::{Array, ArrayTrait, SpanTrait};
use debug::PrintTrait;
use governance::timelock::{ITimelockDispatcher, ITimelockDispatcherTrait, Timelock};
use governance::timelock::{
ITimelockDispatcher, ITimelockDispatcherTrait, Timelock, TimelockConfig,
TimelockConfigStorePacking, ExecutionState, ExecutionStateStorePacking
};
use governance::governance_token_test::{deploy as deploy_token};
use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait};
use governance::governance_token::{IGovernanceTokenDispatcher, IGovernanceTokenDispatcherTrait};
Expand Down Expand Up @@ -30,9 +33,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');
}
Expand Down Expand Up @@ -70,9 +73,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);

Expand Down Expand Up @@ -134,8 +137,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)));
}

Expand All @@ -153,7 +156,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)));
}
1 change: 1 addition & 0 deletions src/utils.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod timestamps;
31 changes: 31 additions & 0 deletions src/utils/timestamps.cairo
Original file line number Diff line number Diff line change
@@ -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())
}
}