From 3c0a91afe0b65a8c7bc088c65f6bfdefe167a7f3 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sun, 7 Apr 2024 16:38:24 +0800 Subject: [PATCH 01/35] repo init --- Scarb.lock | 7 +++++ Scarb.toml | 1 + governance/Scarb.lock | 16 +++++++++++ governance/Scarb.toml | 15 ++++++++++ governance/scripts/default_auth.sh | 23 +++++++++++++++ governance/scripts/spawn.sh | 12 ++++++++ governance/src/lib.cairo | 28 +++++++++++++++++++ governance/src/models/delegate.cairo | 1 + governance/src/models/delegator.cairo | 1 + .../src/systems/delegate/contract.cairo | 1 + .../src/systems/delegate/interface.cairo | 1 + governance/src/systems/delegate/tests.cairo | 1 + .../src/systems/delegator/contract.cairo | 1 + .../src/systems/delegator/interface.cairo | 1 + governance/src/systems/delegator/tests.cairo | 1 + governance/src/systems/token/tests.cairo | 1 + 16 files changed, 111 insertions(+) create mode 100644 governance/Scarb.lock create mode 100644 governance/Scarb.toml create mode 100755 governance/scripts/default_auth.sh create mode 100755 governance/scripts/spawn.sh create mode 100644 governance/src/lib.cairo create mode 100644 governance/src/models/delegate.cairo create mode 100644 governance/src/models/delegator.cairo create mode 100644 governance/src/systems/delegate/contract.cairo create mode 100644 governance/src/systems/delegate/interface.cairo create mode 100644 governance/src/systems/delegate/tests.cairo create mode 100644 governance/src/systems/delegator/contract.cairo create mode 100644 governance/src/systems/delegator/interface.cairo create mode 100644 governance/src/systems/delegator/tests.cairo create mode 100644 governance/src/systems/token/tests.cairo diff --git a/Scarb.lock b/Scarb.lock index ca8796db..743861c7 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -26,6 +26,13 @@ name = "dojo_plugin" version = "0.3.11" source = "git+https://github.com/dojoengine/dojo?tag=v0.3.11#1e651b5d4d3b79b14a7d8aa29a92062fcb9e6659" +[[package]] +name = "governance" +version = "0.0.0" +dependencies = [ + "dojo", +] + [[package]] name = "hex_map" version = "0.0.0" diff --git a/Scarb.toml b/Scarb.toml index a787a887..7b7b6b7d 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -6,6 +6,7 @@ members = [ "examples/market", "examples/projectile", "token", + "governance" ] [workspace.package] diff --git a/governance/Scarb.lock b/governance/Scarb.lock new file mode 100644 index 00000000..1b881ebd --- /dev/null +++ b/governance/Scarb.lock @@ -0,0 +1,16 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "dojo" +version = "0.6.0" +source = "git+https://github.com/dojoengine/dojo?tag=v0.6.0#fc5ad790c1993713e59f3fc65739160f132f29f0" +dependencies = [ + "dojo_plugin", +] + +[[package]] +name = "dojo_plugin" +version = "0.3.11" +source = "git+https://github.com/dojoengine/dojo?tag=v0.3.11#1e651b5d4d3b79b14a7d8aa29a92062fcb9e6659" + diff --git a/governance/Scarb.toml b/governance/Scarb.toml new file mode 100644 index 00000000..389db21a --- /dev/null +++ b/governance/Scarb.toml @@ -0,0 +1,15 @@ +[package] +name = "governance" +version = "0.0.0" +description = "Implementations of Compound Governance standards for the Dojo framework." +homepage = "https://github.com/dojoengine/origami/tree/governance" + +[dependencies] +dojo.workspace = true + +[tool.fmt] +sort-module-level-items = true + +[lib] + +[[target.dojo]] diff --git a/governance/scripts/default_auth.sh b/governance/scripts/default_auth.sh new file mode 100755 index 00000000..e3954988 --- /dev/null +++ b/governance/scripts/default_auth.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -euo pipefail +pushd $(dirname "$0")/.. + +export RPC_URL="http://localhost:5050" + +export WORLD_ADDRESS=$(cat ./manifests/deployments/KATANA.json | jq -r '.world.address') + +export ACTIONS_ADDRESS=$(cat ./manifests/deployments/KATANA.json | jq -r '.contracts[] | select(.name == "dojo_starter::systems::actions::actions" ).address') + +echo "---------------------------------------------------------------------------" +echo world : $WORLD_ADDRESS +echo " " +echo actions : $ACTIONS_ADDRESS +echo "---------------------------------------------------------------------------" + +# enable system -> models authorizations +sozo auth grant --world $WORLD_ADDRESS --wait writer \ + Position,$ACTIONS_ADDRESS \ + Moves,$ACTIONS_ADDRESS \ + >/dev/null + +echo "Default authorizations have been successfully set." diff --git a/governance/scripts/spawn.sh b/governance/scripts/spawn.sh new file mode 100755 index 00000000..506060f9 --- /dev/null +++ b/governance/scripts/spawn.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -euo pipefail +pushd $(dirname "$0")/.. + +export RPC_URL="http://localhost:5050"; + +export WORLD_ADDRESS=$(cat ./manifests/deployments/KATANA.json | jq -r '.world.address') + +export ACTIONS_ADDRESS=$(cat ./manifests/deployments/KATANA.json | jq -r '.contracts[] | select(.name == "dojo_starter::systems::actions::actions" ).address') + +# sozo execute --world +sozo execute --world $WORLD_ADDRESS $ACTIONS_ADDRESS spawn --wait diff --git a/governance/src/lib.cairo b/governance/src/lib.cairo new file mode 100644 index 00000000..537d4a50 --- /dev/null +++ b/governance/src/lib.cairo @@ -0,0 +1,28 @@ +mod libraries { + mod events; +} + +mod models { + mod delegate; + mod delegator; + mod token; +} + +mod systems { + mod delegate { + mod contract; + mod interface; + mod tests; + } + mod delegator { + mod contract; + mod interface; + mod tests; + } + mod token { + mod contract; + mod interface; + mod tests; + } +} + diff --git a/governance/src/models/delegate.cairo b/governance/src/models/delegate.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/governance/src/models/delegate.cairo @@ -0,0 +1 @@ + diff --git a/governance/src/models/delegator.cairo b/governance/src/models/delegator.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/governance/src/models/delegator.cairo @@ -0,0 +1 @@ + diff --git a/governance/src/systems/delegate/contract.cairo b/governance/src/systems/delegate/contract.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/governance/src/systems/delegate/contract.cairo @@ -0,0 +1 @@ + diff --git a/governance/src/systems/delegate/interface.cairo b/governance/src/systems/delegate/interface.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/governance/src/systems/delegate/interface.cairo @@ -0,0 +1 @@ + diff --git a/governance/src/systems/delegate/tests.cairo b/governance/src/systems/delegate/tests.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/governance/src/systems/delegate/tests.cairo @@ -0,0 +1 @@ + diff --git a/governance/src/systems/delegator/contract.cairo b/governance/src/systems/delegator/contract.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/governance/src/systems/delegator/contract.cairo @@ -0,0 +1 @@ + diff --git a/governance/src/systems/delegator/interface.cairo b/governance/src/systems/delegator/interface.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/governance/src/systems/delegator/interface.cairo @@ -0,0 +1 @@ + diff --git a/governance/src/systems/delegator/tests.cairo b/governance/src/systems/delegator/tests.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/governance/src/systems/delegator/tests.cairo @@ -0,0 +1 @@ + diff --git a/governance/src/systems/token/tests.cairo b/governance/src/systems/token/tests.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/governance/src/systems/token/tests.cairo @@ -0,0 +1 @@ + From 4ea0ddb0289e878ce19d12597bfd8c44df2ba032 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sun, 7 Apr 2024 16:39:41 +0800 Subject: [PATCH 02/35] added token events --- governance/src/libraries/events.cairo | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 governance/src/libraries/events.cairo diff --git a/governance/src/libraries/events.cairo b/governance/src/libraries/events.cairo new file mode 100644 index 00000000..23fe8204 --- /dev/null +++ b/governance/src/libraries/events.cairo @@ -0,0 +1,39 @@ +mod tokenevents { + use starknet::ContractAddress; + + #[derive(Model, Copy, Drop, Serde)] + #[dojo::event] + struct DelegateChanged { + #[key] + delegator: ContractAddress, + from_delegate: ContractAddress, + to_delegate: ContractAddress, + } + + #[derive(Model, Copy, Drop, Serde)] + #[dojo::event] + struct DelegateVotesChanged { + #[key] + delegate: ContractAddress, + previous_balance: u128, + new_balance: u128, + } + + #[derive(Model, Copy, Drop, Serde)] + #[dojo::event] + struct Transfer { + #[key] + from: ContractAddress, + to: ContractAddress, + amount: u128, + } + + #[derive(Model, Copy, Drop, Serde)] + #[dojo::event] + struct Approval { + #[key] + owner: ContractAddress, + spender: ContractAddress, + amount: u128, + } +} From 76e0915341a29ce9384011da4f9c6b06acd7c3dc Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sun, 7 Apr 2024 16:39:56 +0800 Subject: [PATCH 03/35] added token models --- governance/src/models/token.cairo | 69 +++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 governance/src/models/token.cairo diff --git a/governance/src/models/token.cairo b/governance/src/models/token.cairo new file mode 100644 index 00000000..fd19f4b4 --- /dev/null +++ b/governance/src/models/token.cairo @@ -0,0 +1,69 @@ +use starknet::ContractAddress; + +#[derive(Model, Copy, Drop, Serde)] +struct Metadata { + #[key] + token: ContractAddress, + name: felt252, + symbol: felt252, + decimals: u8, +} + +#[derive(Model, Copy, Drop, Serde)] +struct TotalSupply { + #[key] + token: ContractAddress, + amount: u128, +} + +#[derive(Model, Copy, Drop, Serde)] +struct Allowances { + #[key] + delegator: ContractAddress, + #[key] + delegate: ContractAddress, + amount: u128, +} + +#[derive(Model, Copy, Drop, Serde)] +struct Balances { + #[key] + account: ContractAddress, + amount: u128, +} + +#[derive(Model, Copy, Drop, Serde)] +struct Delegates { + #[key] + account: ContractAddress, + delegate: ContractAddress, +} + +#[derive(Model, Copy, Drop, Serde)] +struct Checkpoints { + #[key] + account: ContractAddress, + #[key] + index: u128, + checkpoint: Checkpoint, +} + +#[derive(Model, Copy, Drop, Serde)] +struct NumCheckpoints { + #[key] + account: ContractAddress, + count: u128, +} + +#[derive(Model, Copy, Drop, Serde)] +struct Nonces { + #[key] + account: ContractAddress, + nonce: usize, +} + +#[derive(Copy, Drop, Introspect, Serde)] +struct Checkpoint { + from_block: u64, + votes: u128, +} From 858ad2016540beca4bfc559430c9ee063cce1727 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sun, 7 Apr 2024 16:40:05 +0800 Subject: [PATCH 04/35] added token interface --- governance/src/systems/token/interface.cairo | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 governance/src/systems/token/interface.cairo diff --git a/governance/src/systems/token/interface.cairo b/governance/src/systems/token/interface.cairo new file mode 100644 index 00000000..17149e34 --- /dev/null +++ b/governance/src/systems/token/interface.cairo @@ -0,0 +1,21 @@ +use starknet::ContractAddress; + +#[dojo::interface] +trait IGovernanceToken { + fn spaw( + name: felt252, + symbol: felt252, + decimals: u8, + initial_supply: u128, + recipient: ContractAddress + ); + fn approve(spender: ContractAddress, amount: u128); + fn transfer(to: ContractAddress, amount: u128); + fn transfer_from(from: ContractAddress, to: ContractAddress, amount: u128); + fn delegate(delegatee: ContractAddress); + fn delegate_by_signature( + delegatee: ContractAddress, nonce: usize, expiry: u64, pk: felt252, r: felt252, s: felt252 + ); + fn get_current_votes(account: ContractAddress) -> u128; + fn get_prior_votes(account: ContractAddress, block_number: u64) -> u128; +} From 6fb7771bf1bd53c6d5ee0d8564efe1884ef112d7 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sun, 7 Apr 2024 16:56:11 +0800 Subject: [PATCH 05/35] renamed spawn -> constructor for gov token --- governance/src/systems/token/interface.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/governance/src/systems/token/interface.cairo b/governance/src/systems/token/interface.cairo index 17149e34..84e08958 100644 --- a/governance/src/systems/token/interface.cairo +++ b/governance/src/systems/token/interface.cairo @@ -2,7 +2,7 @@ use starknet::ContractAddress; #[dojo::interface] trait IGovernanceToken { - fn spaw( + fn constructor( name: felt252, symbol: felt252, decimals: u8, From 15647c0d158b4f667bcb13ec6e987a08c6819273 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sun, 7 Apr 2024 18:34:30 +0800 Subject: [PATCH 06/35] removed delegate by signature --- governance/src/systems/token/interface.cairo | 3 --- 1 file changed, 3 deletions(-) diff --git a/governance/src/systems/token/interface.cairo b/governance/src/systems/token/interface.cairo index 84e08958..e54a9f76 100644 --- a/governance/src/systems/token/interface.cairo +++ b/governance/src/systems/token/interface.cairo @@ -13,9 +13,6 @@ trait IGovernanceToken { fn transfer(to: ContractAddress, amount: u128); fn transfer_from(from: ContractAddress, to: ContractAddress, amount: u128); fn delegate(delegatee: ContractAddress); - fn delegate_by_signature( - delegatee: ContractAddress, nonce: usize, expiry: u64, pk: felt252, r: felt252, s: felt252 - ); fn get_current_votes(account: ContractAddress) -> u128; fn get_prior_votes(account: ContractAddress, block_number: u64) -> u128; } From d5615ee06bcd562437d3e5b034b5f4cf5fe74a5e Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sun, 7 Apr 2024 18:37:03 +0800 Subject: [PATCH 07/35] renamed delegate -> delegatee to prevent clash with fn naming --- governance/src/libraries/events.cairo | 8 ++++---- governance/src/models/token.cairo | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/governance/src/libraries/events.cairo b/governance/src/libraries/events.cairo index 23fe8204..4924905f 100644 --- a/governance/src/libraries/events.cairo +++ b/governance/src/libraries/events.cairo @@ -6,16 +6,16 @@ mod tokenevents { struct DelegateChanged { #[key] delegator: ContractAddress, - from_delegate: ContractAddress, - to_delegate: ContractAddress, + from: ContractAddress, + to: ContractAddress, } #[derive(Model, Copy, Drop, Serde)] #[dojo::event] struct DelegateVotesChanged { #[key] - delegate: ContractAddress, - previous_balance: u128, + delegatee: ContractAddress, + prev_balance: u128, new_balance: u128, } diff --git a/governance/src/models/token.cairo b/governance/src/models/token.cairo index fd19f4b4..558aa2cf 100644 --- a/governance/src/models/token.cairo +++ b/governance/src/models/token.cairo @@ -21,7 +21,7 @@ struct Allowances { #[key] delegator: ContractAddress, #[key] - delegate: ContractAddress, + delegatee: ContractAddress, amount: u128, } @@ -36,7 +36,7 @@ struct Balances { struct Delegates { #[key] account: ContractAddress, - delegate: ContractAddress, + delegatee: ContractAddress, } #[derive(Model, Copy, Drop, Serde)] @@ -44,7 +44,7 @@ struct Checkpoints { #[key] account: ContractAddress, #[key] - index: u128, + index: u64, checkpoint: Checkpoint, } @@ -52,7 +52,7 @@ struct Checkpoints { struct NumCheckpoints { #[key] account: ContractAddress, - count: u128, + count: u64, } #[derive(Model, Copy, Drop, Serde)] From 943bb1f6b12a82361c46aee6a8be029a9e0f7d13 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sun, 7 Apr 2024 18:37:44 +0800 Subject: [PATCH 08/35] added gov token contract --- governance/src/systems/token/contract.cairo | 201 ++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 governance/src/systems/token/contract.cairo diff --git a/governance/src/systems/token/contract.cairo b/governance/src/systems/token/contract.cairo new file mode 100644 index 00000000..b99bc6fb --- /dev/null +++ b/governance/src/systems/token/contract.cairo @@ -0,0 +1,201 @@ +#[dojo::contract] +mod governancetoken { + use governance::libraries::events::tokenevents; + use governance::models::token::{ + Allowances, Metadata, TotalSupply, Balances, Delegates, NumCheckpoints, Checkpoints + }; + use governance::systems::token::interface::IGovernanceToken; + use starknet::{ + ContractAddress, get_caller_address, get_contract_address, + info::{get_block_number, get_execution_info}, + }; + use integer::BoundedInt; + use poseidon::poseidon_hash_span; + + impl GovernanceTokenImpl of IGovernanceToken { + fn constructor( + name: felt252, + symbol: felt252, + decimals: u8, + initial_supply: u128, + recipient: ContractAddress + ) { + let world = self.world_dispatcher.read(); + let token = get_contract_address(); + let metadata = get!(world, token, Metadata); + let total_supply = get!(world, token, TotalSupply).amount; + assert!( + metadata.name.is_zero() + && metadata.symbol.is_zero() + && metadata.decimals.is_zero() + && total_supply.is_zero(), + "Governance Token: already initialized" + ); + set!( + world, + ( + Metadata { token, name, symbol, decimals }, + TotalSupply { token, amount: initial_supply }, + Balances { account: recipient, amount: initial_supply } + ) + ); + emit!( + world, + tokenevents::Transfer { + from: Zeroable::zero(), to: recipient, amount: initial_supply + } + ) + } + + fn approve(spender: ContractAddress, amount: u128) { + let world = self.world_dispatcher.read(); + let caller = get_caller_address(); + set!(world, (Allowances { delegator: caller, delegatee: spender, amount })); + emit!(world, tokenevents::Approval { owner: caller, spender, amount }) + } + + fn transfer(to: ContractAddress, amount: u128) { + transfer_tokens(self.world_dispatcher.read(), get_caller_address(), to, amount); + } + + fn transfer_from(from: ContractAddress, to: ContractAddress, amount: u128) { + let world = self.world_dispatcher.read(); + let spender = get_caller_address(); + let spender_allowance = get!(world, (from, spender), Allowances).amount; + + if spender != from && spender_allowance != BoundedInt::max() { + assert!( + spender_allowance >= amount, + "Governance Token: transfer amount exceeds spender allowance" + ); + let new_allowance = spender_allowance - amount; + set!( + world, Allowances { delegator: from, delegatee: spender, amount: new_allowance } + ); + emit!(world, tokenevents::Approval { owner: from, spender, amount: new_allowance }); + } + transfer_tokens(world, from, to, amount); + } + + fn delegate(delegatee: ContractAddress) { + delegate(self.world_dispatcher.read(), get_caller_address(), delegatee); + } + + fn get_current_votes(account: ContractAddress) -> u128 { + let world = self.world_dispatcher.read(); + let n_checkpoints = get!(world, account, NumCheckpoints).count; + if n_checkpoints > 0 { + get!(world, (account, n_checkpoints - 1), Checkpoints).checkpoint.votes + } else { + 0 + } + } + + fn get_prior_votes(account: ContractAddress, block_number: u64) -> u128 { + let block_number = get_block_number(); + assert!(block_number < block_number, "Governance Token: not yet determined"); + let world = self.world_dispatcher.read(); + let n_checkpoints = get!(world, account, NumCheckpoints).count; + if n_checkpoints.is_zero() { + return 0; + } + let most_recent_checkpoint = get!(world, (account, n_checkpoints - 1), Checkpoints) + .checkpoint; + if most_recent_checkpoint.from_block > block_number { + return 0; + } + let mut lower = 0; + let mut upper = n_checkpoints - 1; + let mut votes = 0; + loop { + if lower == upper { + votes = get!(world, (account, lower), Checkpoints).checkpoint.votes; + break; + } + let center = upper - (upper - lower) / 2; + let cp = get!(world, (account, center), Checkpoints).checkpoint; + if cp.from_block == block_number { + votes = cp.votes; + break; + } else if cp.from_block < block_number { + lower = center; + } else { + upper = center - 1; + } + }; + votes + } + } + + + fn delegate(world: IWorldDispatcher, delegator: ContractAddress, delegatee: ContractAddress) { + let currentDelegate = get!(world, delegator, Delegates).delegatee; + let delegatorBalance = get!(world, delegator, Balances).amount; + set!(world, Delegates { account: delegator, delegatee }); + emit!( + world, tokenevents::DelegateChanged { delegator, from: currentDelegate, to: delegatee } + ); + move_delegates(world, currentDelegate, delegatee, delegatorBalance); + } + + fn transfer_tokens( + world: IWorldDispatcher, from: ContractAddress, to: ContractAddress, amount: u128 + ) { + assert!(!from.is_zero(), "Governance Token: transfer from zero address"); + assert!(!to.is_zero(), "Governance Token: transfer to zero address"); + + let from_balance = get!(world, from, Balances).amount; + assert!(from_balance >= amount, "Governance Token: insufficient balance"); + + let to_balance = get!(world, to, Balances).amount; + set!(world, Balances { account: to, amount: to_balance + amount }); + emit!(world, tokenevents::Transfer { from, to, amount }); + let from_rep = get!(world, from, Delegates).delegatee; + let to_rep = get!(world, to, Delegates).delegatee; + + move_delegates(world, from_rep, to_rep, amount); + } + + fn move_delegates( + world: IWorldDispatcher, from_rep: ContractAddress, to_rep: ContractAddress, amount: u128 + ) { + if from_rep != to_rep && !amount.is_zero() { + if !from_rep.is_zero() { + let from_rep_num = get!(world, from_rep, NumCheckpoints).count; + let from_rep_old = if !from_rep_num.is_zero() { + get!(world, (from_rep, from_rep_num - 1), Checkpoints).checkpoint.votes + } else { + 0 + }; + assert!(from_rep_old >= amount, "Governance Token: vote amount underflows"); + let from_rep_new = from_rep_old - amount; + write_checkpoint(world, from_rep, from_rep_num, from_rep_old, from_rep_new); + } + } + } + + fn write_checkpoint( + world: IWorldDispatcher, + delegatee: ContractAddress, + n_checkpoints: u64, + old_votes: u128, + new_votes: u128 + ) { + let block_number = get_block_number(); + let mut checkpoint = get!(world, (delegatee, n_checkpoints - 1), Checkpoints).checkpoint; + if !n_checkpoints.is_zero() && checkpoint.from_block == n_checkpoints { + checkpoint.votes = new_votes; + set!(world, Checkpoints { account: delegatee, index: n_checkpoints - 1, checkpoint }); + } else { + checkpoint.from_block = block_number; + checkpoint.votes = new_votes; + set!(world, Checkpoints { account: delegatee, index: n_checkpoints, checkpoint }); + } + emit!( + world, + tokenevents::DelegateVotesChanged { + delegatee, prev_balance: old_votes, new_balance: new_votes + } + ); + } +} From 39387d9d59e214aee6c5039d5d42a40254fc62f3 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sun, 7 Apr 2024 23:03:27 +0800 Subject: [PATCH 09/35] added timelock contracts to lib --- governance/src/lib.cairo | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/governance/src/lib.cairo b/governance/src/lib.cairo index 537d4a50..782ffc4d 100644 --- a/governance/src/lib.cairo +++ b/governance/src/lib.cairo @@ -5,6 +5,8 @@ mod libraries { mod models { mod delegate; mod delegator; + mod governor; + mod timelock; mod token; } @@ -19,6 +21,16 @@ mod systems { mod interface; mod tests; } + mod governor { + mod contract; + mod interface; + mod tests; + } + mod timelock { + mod contract; + mod interface; + mod tests; + } mod token { mod contract; mod interface; From 7626083694ee84a8fcdbe2ad6c65ff1a814c7676 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sun, 7 Apr 2024 23:03:43 +0800 Subject: [PATCH 10/35] added timelock events --- governance/src/libraries/events.cairo | 47 +++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/governance/src/libraries/events.cairo b/governance/src/libraries/events.cairo index 4924905f..5c182458 100644 --- a/governance/src/libraries/events.cairo +++ b/governance/src/libraries/events.cairo @@ -37,3 +37,50 @@ mod tokenevents { amount: u128, } } + +mod timelockevents { + use starknet::{ContractAddress, ClassHash}; + + #[derive(Model, Copy, Drop, Serde)] + #[dojo::event] + struct NewAdmin { + #[key] + contract: ContractAddress, + address: ContractAddress, + } + + #[derive(Model, Copy, Drop, Serde)] + #[dojo::event] + struct NewDelay { + #[key] + contract: ContractAddress, + value: u64, + } + + #[derive(Model, Copy, Drop, Serde)] + #[dojo::event] + struct CancelTransaction { + #[key] + target: ContractAddress, + class_hash: ClassHash, + eta: u64, + } + + #[derive(Model, Copy, Drop, Serde)] + #[dojo::event] + struct ExecuteTransaction { + #[key] + target: ContractAddress, + class_hash: ClassHash, + eta: u64, + } + + #[derive(Model, Copy, Drop, Serde)] + #[dojo::event] + struct QueueTransaction { + #[key] + target: ContractAddress, + class_hash: ClassHash, + eta: u64, + } +} From ecd04ca5c2c7471e0de4f085575854a316ca33aa Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sun, 7 Apr 2024 23:04:14 +0800 Subject: [PATCH 11/35] setup governor modules --- governance/src/models/governor.cairo | 1 + governance/src/systems/governor/contract.cairo | 1 + governance/src/systems/governor/interface.cairo | 1 + governance/src/systems/governor/tests.cairo | 1 + 4 files changed, 4 insertions(+) create mode 100644 governance/src/models/governor.cairo create mode 100644 governance/src/systems/governor/contract.cairo create mode 100644 governance/src/systems/governor/interface.cairo create mode 100644 governance/src/systems/governor/tests.cairo diff --git a/governance/src/models/governor.cairo b/governance/src/models/governor.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/governance/src/models/governor.cairo @@ -0,0 +1 @@ + diff --git a/governance/src/systems/governor/contract.cairo b/governance/src/systems/governor/contract.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/governance/src/systems/governor/contract.cairo @@ -0,0 +1 @@ + diff --git a/governance/src/systems/governor/interface.cairo b/governance/src/systems/governor/interface.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/governance/src/systems/governor/interface.cairo @@ -0,0 +1 @@ + diff --git a/governance/src/systems/governor/tests.cairo b/governance/src/systems/governor/tests.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/governance/src/systems/governor/tests.cairo @@ -0,0 +1 @@ + From 71db7c32fe04e38c75caea359a03ae0043984756 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sun, 7 Apr 2024 23:04:43 +0800 Subject: [PATCH 12/35] added timelock models --- governance/src/models/timelock.cairo | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 governance/src/models/timelock.cairo diff --git a/governance/src/models/timelock.cairo b/governance/src/models/timelock.cairo new file mode 100644 index 00000000..9208ccca --- /dev/null +++ b/governance/src/models/timelock.cairo @@ -0,0 +1,26 @@ +use starknet::{ContractAddress, ClassHash}; + +#[derive(Model, Copy, Drop, Serde)] +struct TimelockParams { + #[key] + contract: ContractAddress, + admin: ContractAddress, + delay: u64, +} + +#[derive(Model, Copy, Drop, Serde)] +struct PendingAdmin { + #[key] + contract: ContractAddress, + address: ContractAddress, +} + +#[derive(Model, Copy, Drop, Serde)] +struct QueuedTransactions { + #[key] + contract: ContractAddress, + #[key] + class_hash: ClassHash, + queued: bool, +} + From 632aec2f0bf943447557fdded549f6d6a6d37968 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sun, 7 Apr 2024 23:05:14 +0800 Subject: [PATCH 13/35] added empty timelock test --- governance/src/systems/timelock/tests.cairo | 1 + 1 file changed, 1 insertion(+) create mode 100644 governance/src/systems/timelock/tests.cairo diff --git a/governance/src/systems/timelock/tests.cairo b/governance/src/systems/timelock/tests.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/governance/src/systems/timelock/tests.cairo @@ -0,0 +1 @@ + From 9c87b5a66f0e47cb9073a8a15a760ef4f0813c51 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sun, 7 Apr 2024 23:05:24 +0800 Subject: [PATCH 14/35] added timelock contract --- .../src/systems/timelock/contract.cairo | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 governance/src/systems/timelock/contract.cairo diff --git a/governance/src/systems/timelock/contract.cairo b/governance/src/systems/timelock/contract.cairo new file mode 100644 index 00000000..09330c87 --- /dev/null +++ b/governance/src/systems/timelock/contract.cairo @@ -0,0 +1,119 @@ +#[dojo::contract] +mod timelock { + use governance::libraries::events::timelockevents; + use governance::models::timelock::{PendingAdmin, QueuedTransactions, TimelockParams}; + use governance::systems::timelock::interface::ITimelock; + use starknet::{ + ContractAddress, ClassHash, get_caller_address, get_block_timestamp, get_contract_address, + Zeroable + }; + + // The following constants are defined is seconds based on the Compounds Timelock contract, + // but can be adjusted to fit the needs of the project. + const GRACE_PERIOD: u64 = 1_209_600; // 14 days + const MINIMUM_DELAY: u64 = 172_800; // 2 days; + const MAXIMUM_DELAY: u64 = 2_592_000; // 30 days; + + impl TimelockImpl of ITimelock { + fn constructor(admin: ContractAddress, delay: u64) { + assert!(!admin.is_zero(), "Timelock::constructor: Admin address cannot be zero."); + assert!( + delay >= MINIMUM_DELAY, "Timelock::constructor: Delay must exceed minimum delay." + ); + assert!( + delay <= MAXIMUM_DELAY, + "Timelock::constructor: Delay must not exceed maximum delay." + ); + let world = self.world_dispatcher.read(); + let contract = get_contract_address(); + let curr_params = get!(world, contract, TimelockParams); + assert!( + curr_params.admin == Zeroable::zero(), "Timelock::constructor: Already initialized." + ); + set!(world, TimelockParams { contract, admin, delay }); + emit!( + world, + timelockevents::NewAdmin { contract, address: admin }, + timelockevents::NewDelay { contract, value: delay } + ); + } + + fn que_transaction(target: ContractAddress, new_implementation: ClassHash, eta: u64) { + let world = self.world_dispatcher.read(); + let params = get!(world, get_contract_address(), TimelockParams); + assert!( + get_caller_address() == params.admin, + "Timelock::queueTransaction: Call must come from admin." + ); + assert!( + eta >= get_block_timestamp() + params.delay, + "Timelock::queueTransaction: Estimated execution block must satisfy delay." + ); + set!( + world, + QueuedTransactions { + contract: target, class_hash: new_implementation, queued: true + } + ); + emit!( + world, + timelockevents::QueueTransaction { target, class_hash: new_implementation, eta } + ); + } + + fn cancel_transaction(target: ContractAddress, new_implementation: ClassHash, eta: u64) { + let world = self.world_dispatcher.read(); + let params = get!(world, get_contract_address(), TimelockParams); + assert!( + get_caller_address() == params.admin, + "Timelock::cancel_transaction: Call must come from admin." + ); + set!( + world, + QueuedTransactions { + contract: target, class_hash: new_implementation, queued: false + } + ); + emit!( + world, + timelockevents::CancelTransaction { target, class_hash: new_implementation, eta } + ); + } + + fn execute_transaction( + target: ContractAddress, new_implementation: ClassHash, eta: u64 + ) -> ClassHash { + let world = self.world_dispatcher.read(); + let params = get!(world, get_contract_address(), TimelockParams); + assert!( + get_caller_address() == params.admin, + "Timelock::execute_transaction: Call must come from admin." + ); + let queued_tx = get!(world, (target, new_implementation), QueuedTransactions); + assert!( + queued_tx.queued, "Timelock::execute_transaction: Transaction hasn't been queued." + ); + let timestamp = get_block_timestamp(); + assert!( + timestamp >= eta, + "Timelock::execute_transaction: Transaction hasn't surpassed time lock." + ); + assert!( + timestamp <= eta + GRACE_PERIOD, + "Timelock::execute_transaction: Transaction is stale." + ); + set!( + world, + QueuedTransactions { + contract: target, class_hash: new_implementation, queued: false + } + ); + let upgraded_class_hash = world.upgrade_contract(target, new_implementation); + emit!( + world, + timelockevents::ExecuteTransaction { target, class_hash: upgraded_class_hash, eta } + ); + upgraded_class_hash + } + } +} From 5755757fad691c62e2ae5fbaffa421986a4e221b Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sun, 7 Apr 2024 23:05:37 +0800 Subject: [PATCH 15/35] added timelock interface --- governance/src/systems/timelock/interface.cairo | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 governance/src/systems/timelock/interface.cairo diff --git a/governance/src/systems/timelock/interface.cairo b/governance/src/systems/timelock/interface.cairo new file mode 100644 index 00000000..4d883800 --- /dev/null +++ b/governance/src/systems/timelock/interface.cairo @@ -0,0 +1,11 @@ +use starknet::{ContractAddress, ClassHash}; + +#[dojo::interface] +trait ITimelock { + fn constructor(admin: ContractAddress, delay: u64); + fn que_transaction(target: ContractAddress, new_implementation: ClassHash, eta: u64); + fn cancel_transaction(target: ContractAddress, new_implementation: ClassHash, eta: u64); + fn execute_transaction( + target: ContractAddress, new_implementation: ClassHash, eta: u64 + ) -> ClassHash; +} From c013dd3a99daf02eeb78ff6d24cd24381826be38 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sun, 7 Apr 2024 23:05:44 +0800 Subject: [PATCH 16/35] fmt --- governance/src/systems/timelock/contract.cairo | 4 ++-- governance/src/systems/token/contract.cairo | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/governance/src/systems/timelock/contract.cairo b/governance/src/systems/timelock/contract.cairo index 09330c87..089f32b8 100644 --- a/governance/src/systems/timelock/contract.cairo +++ b/governance/src/systems/timelock/contract.cairo @@ -43,11 +43,11 @@ mod timelock { let params = get!(world, get_contract_address(), TimelockParams); assert!( get_caller_address() == params.admin, - "Timelock::queueTransaction: Call must come from admin." + "Timelock::queue_transaction: Call must come from admin." ); assert!( eta >= get_block_timestamp() + params.delay, - "Timelock::queueTransaction: Estimated execution block must satisfy delay." + "Timelock::queue_transaction: Estimated execution block must satisfy delay." ); set!( world, diff --git a/governance/src/systems/token/contract.cairo b/governance/src/systems/token/contract.cairo index b99bc6fb..c7f89150 100644 --- a/governance/src/systems/token/contract.cairo +++ b/governance/src/systems/token/contract.cairo @@ -5,12 +5,11 @@ mod governancetoken { Allowances, Metadata, TotalSupply, Balances, Delegates, NumCheckpoints, Checkpoints }; use governance::systems::token::interface::IGovernanceToken; + use integer::BoundedInt; use starknet::{ ContractAddress, get_caller_address, get_contract_address, info::{get_block_number, get_execution_info}, }; - use integer::BoundedInt; - use poseidon::poseidon_hash_span; impl GovernanceTokenImpl of IGovernanceToken { fn constructor( From e19dd2818eebb9cb5dee84bebd9f89459643fa56 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Mon, 8 Apr 2024 08:56:25 +0800 Subject: [PATCH 17/35] renamed constructor -> initialize --- governance/src/systems/timelock/contract.cairo | 2 +- governance/src/systems/timelock/interface.cairo | 2 +- governance/src/systems/token/contract.cairo | 2 +- governance/src/systems/token/interface.cairo | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/governance/src/systems/timelock/contract.cairo b/governance/src/systems/timelock/contract.cairo index 089f32b8..7c6c0b1c 100644 --- a/governance/src/systems/timelock/contract.cairo +++ b/governance/src/systems/timelock/contract.cairo @@ -15,7 +15,7 @@ mod timelock { const MAXIMUM_DELAY: u64 = 2_592_000; // 30 days; impl TimelockImpl of ITimelock { - fn constructor(admin: ContractAddress, delay: u64) { + fn initialize(admin: ContractAddress, delay: u64) { assert!(!admin.is_zero(), "Timelock::constructor: Admin address cannot be zero."); assert!( delay >= MINIMUM_DELAY, "Timelock::constructor: Delay must exceed minimum delay." diff --git a/governance/src/systems/timelock/interface.cairo b/governance/src/systems/timelock/interface.cairo index 4d883800..f659f426 100644 --- a/governance/src/systems/timelock/interface.cairo +++ b/governance/src/systems/timelock/interface.cairo @@ -2,7 +2,7 @@ use starknet::{ContractAddress, ClassHash}; #[dojo::interface] trait ITimelock { - fn constructor(admin: ContractAddress, delay: u64); + fn initialize(admin: ContractAddress, delay: u64); fn que_transaction(target: ContractAddress, new_implementation: ClassHash, eta: u64); fn cancel_transaction(target: ContractAddress, new_implementation: ClassHash, eta: u64); fn execute_transaction( diff --git a/governance/src/systems/token/contract.cairo b/governance/src/systems/token/contract.cairo index c7f89150..fc600dc3 100644 --- a/governance/src/systems/token/contract.cairo +++ b/governance/src/systems/token/contract.cairo @@ -12,7 +12,7 @@ mod governancetoken { }; impl GovernanceTokenImpl of IGovernanceToken { - fn constructor( + fn initialize( name: felt252, symbol: felt252, decimals: u8, diff --git a/governance/src/systems/token/interface.cairo b/governance/src/systems/token/interface.cairo index e54a9f76..ec268942 100644 --- a/governance/src/systems/token/interface.cairo +++ b/governance/src/systems/token/interface.cairo @@ -2,7 +2,7 @@ use starknet::ContractAddress; #[dojo::interface] trait IGovernanceToken { - fn constructor( + fn initialize( name: felt252, symbol: felt252, decimals: u8, From d4f31c4713121f2b14cca16227ee0e9fec7e4c17 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Mon, 8 Apr 2024 12:32:42 +0800 Subject: [PATCH 18/35] added default for ContractAddress + ClassHash --- governance/src/lib.cairo | 1 + governance/src/libraries/traits.cairo | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 governance/src/libraries/traits.cairo diff --git a/governance/src/lib.cairo b/governance/src/lib.cairo index 782ffc4d..b1c59a15 100644 --- a/governance/src/lib.cairo +++ b/governance/src/lib.cairo @@ -1,5 +1,6 @@ mod libraries { mod events; + mod traits; } mod models { diff --git a/governance/src/libraries/traits.cairo b/governance/src/libraries/traits.cairo new file mode 100644 index 00000000..bbd11b3e --- /dev/null +++ b/governance/src/libraries/traits.cairo @@ -0,0 +1,15 @@ +use starknet::{ClassHash, ContractAddress, class_hash_const, contract_address_const}; + +impl ContractAddressDefault of Default { + #[inline(always)] + fn default() -> ContractAddress nopanic { + contract_address_const::<0>() + } +} + +impl ClassHashDefault of Default { + #[inline(always)] + fn default() -> ClassHash nopanic { + class_hash_const::<0>() + } +} From 08bd2348765c96f6ebbcf8ef276ec9e3fcd6dc4f Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Mon, 8 Apr 2024 12:32:57 +0800 Subject: [PATCH 19/35] add governor events --- governance/src/libraries/events.cairo | 50 +++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/governance/src/libraries/events.cairo b/governance/src/libraries/events.cairo index 5c182458..dc86948f 100644 --- a/governance/src/libraries/events.cairo +++ b/governance/src/libraries/events.cairo @@ -84,3 +84,53 @@ mod timelockevents { eta: u64, } } + +mod governorevents { + use starknet::{ContractAddress, ClassHash}; + + #[derive(Model, Copy, Drop, Serde)] + #[dojo::event] + struct ProposalCreated { + #[key] + id: usize, + proposer: ContractAddress, + target: ContractAddress, + class_hash: ClassHash, + start_block: u64, + end_block: u64, + } + + #[derive(Model, Copy, Drop, Serde)] + #[dojo::event] + struct VoteCast { + #[key] + voter: ContractAddress, + proposal_id: usize, + support: bool, + votes: u128, + } + + #[derive(Model, Copy, Drop, Serde)] + #[dojo::event] + struct ProposalCanceled { + #[key] + id: usize, + cancelled: bool, + } + + #[derive(Model, Copy, Drop, Serde)] + #[dojo::event] + struct ProposalQueued { + #[key] + id: usize, + eta: u64, + } + + #[derive(Model, Copy, Drop, Serde)] + #[dojo::event] + struct ProposalExecuted { + #[key] + id: usize, + executed: bool, + } +} From 984693e2476a94b213ee5b1f22ddb779a1f84ba7 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Mon, 8 Apr 2024 12:33:11 +0800 Subject: [PATCH 20/35] add governor models --- governance/src/models/governor.cairo | 84 ++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/governance/src/models/governor.cairo b/governance/src/models/governor.cairo index 8b137891..6f0a7991 100644 --- a/governance/src/models/governor.cairo +++ b/governance/src/models/governor.cairo @@ -1 +1,85 @@ +use governance::libraries::traits::{ContractAddressDefault, ClassHashDefault}; +use starknet::{ContractAddress, ClassHash}; +#[derive(Model, Copy, Drop, Serde)] +struct GovernorParams { + #[key] + contract: ContractAddress, + timelock: ContractAddress, + gov_token: ContractAddress, + guardian: ContractAddress, +} + +#[derive(Model, Copy, Drop, Serde)] +struct ProposalParams { + #[key] + contract: ContractAddress, + quorum_votes: u128, + threshold: u128, + voting_delay: u64, + voting_period: u64, +} + +#[derive(Model, Copy, Drop, Serde)] +struct ProposalCount { + #[key] + contract: ContractAddress, + count: usize, +} + +#[derive(Model, Copy, Drop, Serde)] +struct Proposals { + #[key] + id: usize, + proposal: Proposal, +} + +#[derive(Model, Copy, Drop, Serde)] +struct Receipts { + #[key] + proposal_id: usize, + #[key] + voter: ContractAddress, + receipt: Receipt, +} + +#[derive(Model, Copy, Drop, Serde)] +struct LatestProposalIds { + #[key] + address: ContractAddress, + id: usize, +} + +#[derive(Copy, Drop, Default, Introspect, Serde)] +struct Proposal { + id: usize, + proposer: ContractAddress, + eta: u64, + target: ContractAddress, + class_hash: ClassHash, + start_block: u64, + end_block: u64, + for_votes: u128, + against_votes: u128, + canceled: bool, + executed: bool, +} + +#[derive(Copy, Default, Drop, Introspect, Serde)] +struct Receipt { + has_voted: bool, + support: bool, + votes: u128 +} + +#[derive(Copy, Drop, Serde)] +enum ProposalState { + Pending, + Active, + Canceled, + Defeated, + Succeeded, + Queued, + Expired, + Executed +} From d52c25e0c76931193ef292507c09ea56c38958ee Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Mon, 8 Apr 2024 12:33:27 +0800 Subject: [PATCH 21/35] add governor interface --- governance/src/systems/governor/interface.cairo | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/governance/src/systems/governor/interface.cairo b/governance/src/systems/governor/interface.cairo index 8b137891..6ae4e847 100644 --- a/governance/src/systems/governor/interface.cairo +++ b/governance/src/systems/governor/interface.cairo @@ -1 +1,17 @@ +use governance::models::governor::{ProposalState, Receipt}; +use starknet::{ContractAddress, ClassHash}; +#[dojo::interface] +trait IGovernor { + fn initialize(timelock: ContractAddress, gov_token: ContractAddress, guardian: ContractAddress); + fn set_proposal_params( + quorum_votes: u128, threshold: u128, voting_delay: u64, voting_period: u64, + ); + fn propose(target: ContractAddress, class_hash: ClassHash, description: ByteArray) -> usize; + fn queue(proposal_id: usize); + fn execute(proposal_id: usize); + fn cancel(proposal_id: usize); + fn get_action(proposal_id: usize) -> (ContractAddress, ClassHash); + fn state(proposal_id: usize) -> ProposalState; + fn cast_vote(proposal_id: usize, support: bool); +} From 2a8490906bf740e349bc2bfa8ec1af7455136cb1 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Mon, 8 Apr 2024 12:33:41 +0800 Subject: [PATCH 22/35] add governor contract --- .../src/systems/governor/contract.cairo | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) diff --git a/governance/src/systems/governor/contract.cairo b/governance/src/systems/governor/contract.cairo index 8b137891..af1e09f9 100644 --- a/governance/src/systems/governor/contract.cairo +++ b/governance/src/systems/governor/contract.cairo @@ -1 +1,248 @@ +#[dojo::contract] +mod governor { + use governance::libraries::events::governorevents; + use governance::models::{ + governor::{ + GovernorParams, ProposalParams, ProposalCount, Proposals, Proposal, Receipt, + ProposalState, LatestProposalIds, Receipts + }, + timelock::{QueuedTransactions, TimelockParams} + }; + use governance::systems::{ + governor::interface::IGovernor, timelock::contract::timelock, + token::contract::governancetoken + }; + use starknet::{ + ContractAddress, ClassHash, get_contract_address, get_caller_address, info::get_block_number + }; + impl GovernorImpl of IGovernor { + fn initialize( + timelock: ContractAddress, gov_token: ContractAddress, guardian: ContractAddress + ) { + let world = self.world_dispatcher.read(); + let contract = get_contract_address(); + let curr_params = get!(world, contract, GovernorParams); + assert!( + curr_params.timelock == Zeroable::zero() + && curr_params.gov_token == Zeroable::zero() + && curr_params.guardian == Zeroable::zero() + && curr_params.guardian == Zeroable::zero(), + "Governor::initialize: already initialized" + ); + set!(world, GovernorParams { contract, timelock, gov_token, guardian }); + } + + fn set_proposal_params( + quorum_votes: u128, threshold: u128, voting_delay: u64, voting_period: u64, + ) { + let world = self.world_dispatcher.read(); + let guardian = get!(world, get_contract_address(), GovernorParams).guardian; + assert!( + guardian == get_contract_address(), + "Governor::set_proposal_params: only guardian can set proposal params" + ); + set!( + world, + ProposalParams { + contract: get_contract_address(), + quorum_votes, + threshold, + voting_delay, + voting_period + } + ); + } + + fn propose( + target: ContractAddress, class_hash: ClassHash, description: ByteArray + ) -> usize { + let world = self.world_dispatcher.read(); + let contract = get_contract_address(); + let caller = get_caller_address(); + let params = get!(world, contract, ProposalParams); + let curr_block = get_block_number(); + let prior_votes = governancetoken::get_prior_votes(world, caller, curr_block - 1); + assert!( + prior_votes > params.threshold, + "Governor::propose: proposer votes below proposal threshold" + ); + + let latest_proposal_id = get!(world, caller, LatestProposalIds).id; + if !latest_proposal_id.is_zero() { + let state = self.state(latest_proposal_id); + match state { + ProposalState::Active(()) => { + panic!( + "Governor::propose: one live proposal per proposer, found an already active proposal" + ); + }, + ProposalState::Pending(()) => { + panic!( + "Governor::propose: one live proposal per proposer, found an already pending proposal" + ); + }, + _ => {} + } + } + + let start_block = curr_block + params.voting_delay; + let end_block = start_block + params.voting_period; + let curr_proposal_count = get!(world, contract, ProposalCount).count; + set!(world, ProposalCount { contract, count: curr_proposal_count + 1 }); + let proposal_id = curr_proposal_count + 1; + + let mut new_proposal: Proposal = Default::default(); + new_proposal.id = proposal_id; + new_proposal.proposer = caller; + new_proposal.target = target; + new_proposal.start_block = start_block; + new_proposal.end_block = end_block; + // TODO: unsure if default bool is false, hardcoded for now, can remove if is false + new_proposal.canceled = false; + new_proposal.executed = false; + + set!(world, LatestProposalIds { address: caller, id: proposal_id }); + + emit!( + world, + governorevents::ProposalCreated { + id: proposal_id, proposer: caller, target, class_hash, start_block, end_block, + } + ); + new_proposal.id + } + + fn queue(proposal_id: usize) { + let world = self.world_dispatcher.read(); + let state = self.state(proposal_id); + match state { + ProposalState::Succeeded(()) => {}, + _ => { panic!("Governor::queue: proposal can only be queued if it is succeeded"); } + } + let mut proposal = get!(world, proposal_id, Proposals).proposal; + let timelock_addr = get!(world, get_contract_address(), GovernorParams).timelock; + let timelock_delay = get!(world, timelock_addr, TimelockParams).delay; + let eta = get_block_number() + timelock_delay; + queue_or_revert(world, proposal.target, proposal.class_hash, proposal.eta); + proposal.eta = eta; + emit!(world, governorevents::ProposalQueued { id: proposal_id, eta }); + } + + fn execute(proposal_id: usize) { + let world = self.world_dispatcher.read(); + let state = self.state(proposal_id); + match state { + ProposalState::Queued(()) => {}, + _ => { panic!("Governor::execute: proposal can only be executed if it is queued"); } + } + + let mut proposal = get!(world, proposal_id, Proposals).proposal; + proposal.executed = true; + timelock::execute_transaction( + world, proposal.target, proposal.class_hash, proposal.eta + ); + emit!(world, governorevents::ProposalExecuted { id: proposal_id, executed: true }); + } + + fn cancel(proposal_id: usize) { + let world = self.world_dispatcher.read(); + let state = self.state(proposal_id); + + match state { + ProposalState::Executed(()) => { + panic!("Governor::cancel: cannot cancel executed proposal"); + }, + _ => {} + } + + let mut proposal = get!(world, proposal_id, Proposals).proposal; + let guardian = get!(world, get_contract_address(), GovernorParams).guardian; + let threshold = get!(world, get_contract_address(), ProposalParams).threshold; + let prior_votes = governancetoken::get_prior_votes( + world, proposal.proposer, get_block_number() - 1 + ); + assert!( + guardian == get_caller_address() || prior_votes < threshold, + "Governor::cancel: proposer above threshold" + ); + + proposal.canceled = true; + timelock::cancel_transaction(world, proposal.target, proposal.class_hash, proposal.eta); + emit!(world, governorevents::ProposalCanceled { id: proposal_id, cancelled: true }); + } + + fn get_action(proposal_id: usize) -> (ContractAddress, ClassHash) { + let world = self.world_dispatcher.read(); + let proposal = get!(world, proposal_id, Proposals).proposal; + (proposal.target, proposal.class_hash) + } + + fn state(proposal_id: usize) -> ProposalState { + let world = self.world_dispatcher.read(); + let contract = get_contract_address(); + let proposal_count = get!(world, contract, ProposalCount).count; + assert!( + proposal_id <= proposal_count && !proposal_id.is_zero(), + "Governor::state: invalid proposal id" + ); + + let proposal = get!(world, proposal_id, Proposals).proposal; + let quorum_votes = get!(world, contract, ProposalParams).quorum_votes; + let block_number = get_block_number(); + + if proposal.canceled { + return ProposalState::Canceled(()); + } else if block_number <= proposal.start_block { + return ProposalState::Pending(()); + } else if block_number <= proposal.end_block { + return ProposalState::Active(()); + } else if proposal.for_votes <= proposal.against_votes + || proposal.for_votes < quorum_votes { + return ProposalState::Defeated(()); + } else if proposal.eta == 0 { + return ProposalState::Succeeded(()); + } else if proposal.executed { + return ProposalState::Executed(()); + } else if block_number >= proposal.eta + timelock::GRACE_PERIOD { + return ProposalState::Expired(()); + } else { + return ProposalState::Queued(()); + } + } + + fn cast_vote(proposal_id: usize, support: bool) { + let world = self.world_dispatcher.read(); + let state = self.state(proposal_id); + match state { + ProposalState::Active(()) => {}, + _ => { panic!("Governor::cast_vote: voting is closed"); } + } + + let mut proposal = get!(world, proposal_id, Proposals).proposal; + let caller = get_caller_address(); + let mut receipt = get!(world, (proposal_id, caller), Receipts).receipt; + assert!(!receipt.has_voted, "Governor::cast_vote: voter already voted"); + + let votes = governancetoken::get_prior_votes(world, caller, proposal.start_block); + if support { + proposal.for_votes += votes; + } else { + proposal.against_votes += votes; + } + + receipt.has_voted = true; + receipt.support = support; + receipt.votes = votes; + emit!(world, governorevents::VoteCast { voter: caller, proposal_id, support, votes }); + } + } + + fn queue_or_revert( + world: IWorldDispatcher, target: ContractAddress, class_hash: ClassHash, eta: u64 + ) { + let queued_tx = get!(world, (target, class_hash), QueuedTransactions).queued; + assert!(!queued_tx, "Governor::queue_or_revert: proposal action already queued at eta"); + timelock::que_transaction(world, target, class_hash, eta); + } +} From e334318b2d8b0b1a88a0b91e624a7de32d79978f Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Mon, 8 Apr 2024 12:34:17 +0800 Subject: [PATCH 23/35] moved timelock fn from impl + interface to free --- .../src/systems/timelock/contract.cairo | 139 ++++++++---------- .../src/systems/timelock/interface.cairo | 5 - 2 files changed, 64 insertions(+), 80 deletions(-) diff --git a/governance/src/systems/timelock/contract.cairo b/governance/src/systems/timelock/contract.cairo index 7c6c0b1c..85a5e37a 100644 --- a/governance/src/systems/timelock/contract.cairo +++ b/governance/src/systems/timelock/contract.cairo @@ -37,83 +37,72 @@ mod timelock { timelockevents::NewDelay { contract, value: delay } ); } + } - fn que_transaction(target: ContractAddress, new_implementation: ClassHash, eta: u64) { - let world = self.world_dispatcher.read(); - let params = get!(world, get_contract_address(), TimelockParams); - assert!( - get_caller_address() == params.admin, - "Timelock::queue_transaction: Call must come from admin." - ); - assert!( - eta >= get_block_timestamp() + params.delay, - "Timelock::queue_transaction: Estimated execution block must satisfy delay." - ); - set!( - world, - QueuedTransactions { - contract: target, class_hash: new_implementation, queued: true - } - ); - emit!( - world, - timelockevents::QueueTransaction { target, class_hash: new_implementation, eta } - ); - } + fn execute_transaction( + world: IWorldDispatcher, target: ContractAddress, new_implementation: ClassHash, eta: u64 + ) { + let params = get!(world, get_contract_address(), TimelockParams); + assert!( + get_caller_address() == params.admin, + "Timelock::execute_transaction: Call must come from admin." + ); + let queued_tx = get!(world, (target, new_implementation), QueuedTransactions); + assert!(queued_tx.queued, "Timelock::execute_transaction: Transaction hasn't been queued."); + let timestamp = get_block_timestamp(); + assert!( + timestamp >= eta, + "Timelock::execute_transaction: Transaction hasn't surpassed time lock." + ); + assert!( + timestamp <= eta + GRACE_PERIOD, "Timelock::execute_transaction: Transaction is stale." + ); + set!( + world, + QueuedTransactions { contract: target, class_hash: new_implementation, queued: false } + ); + let upgraded_class_hash = world.upgrade_contract(target, new_implementation); + emit!( + world, + timelockevents::ExecuteTransaction { target, class_hash: upgraded_class_hash, eta } + ); + } - fn cancel_transaction(target: ContractAddress, new_implementation: ClassHash, eta: u64) { - let world = self.world_dispatcher.read(); - let params = get!(world, get_contract_address(), TimelockParams); - assert!( - get_caller_address() == params.admin, - "Timelock::cancel_transaction: Call must come from admin." - ); - set!( - world, - QueuedTransactions { - contract: target, class_hash: new_implementation, queued: false - } - ); - emit!( - world, - timelockevents::CancelTransaction { target, class_hash: new_implementation, eta } - ); - } + fn que_transaction( + world: IWorldDispatcher, target: ContractAddress, new_implementation: ClassHash, eta: u64 + ) { + let params = get!(world, get_contract_address(), TimelockParams); + assert!( + get_caller_address() == params.admin, + "Timelock::queue_transaction: Call must come from admin." + ); + assert!( + eta >= get_block_timestamp() + params.delay, + "Timelock::queue_transaction: Estimated execution block must satisfy delay." + ); + set!( + world, + QueuedTransactions { contract: target, class_hash: new_implementation, queued: true } + ); + emit!( + world, timelockevents::QueueTransaction { target, class_hash: new_implementation, eta } + ); + } - fn execute_transaction( - target: ContractAddress, new_implementation: ClassHash, eta: u64 - ) -> ClassHash { - let world = self.world_dispatcher.read(); - let params = get!(world, get_contract_address(), TimelockParams); - assert!( - get_caller_address() == params.admin, - "Timelock::execute_transaction: Call must come from admin." - ); - let queued_tx = get!(world, (target, new_implementation), QueuedTransactions); - assert!( - queued_tx.queued, "Timelock::execute_transaction: Transaction hasn't been queued." - ); - let timestamp = get_block_timestamp(); - assert!( - timestamp >= eta, - "Timelock::execute_transaction: Transaction hasn't surpassed time lock." - ); - assert!( - timestamp <= eta + GRACE_PERIOD, - "Timelock::execute_transaction: Transaction is stale." - ); - set!( - world, - QueuedTransactions { - contract: target, class_hash: new_implementation, queued: false - } - ); - let upgraded_class_hash = world.upgrade_contract(target, new_implementation); - emit!( - world, - timelockevents::ExecuteTransaction { target, class_hash: upgraded_class_hash, eta } - ); - upgraded_class_hash - } + fn cancel_transaction( + world: IWorldDispatcher, target: ContractAddress, new_implementation: ClassHash, eta: u64 + ) { + let params = get!(world, get_contract_address(), TimelockParams); + assert!( + get_caller_address() == params.admin, + "Timelock::cancel_transaction: Call must come from admin." + ); + set!( + world, + QueuedTransactions { contract: target, class_hash: new_implementation, queued: false } + ); + emit!( + world, timelockevents::CancelTransaction { target, class_hash: new_implementation, eta } + ); } } diff --git a/governance/src/systems/timelock/interface.cairo b/governance/src/systems/timelock/interface.cairo index f659f426..17b53c84 100644 --- a/governance/src/systems/timelock/interface.cairo +++ b/governance/src/systems/timelock/interface.cairo @@ -3,9 +3,4 @@ use starknet::{ContractAddress, ClassHash}; #[dojo::interface] trait ITimelock { fn initialize(admin: ContractAddress, delay: u64); - fn que_transaction(target: ContractAddress, new_implementation: ClassHash, eta: u64); - fn cancel_transaction(target: ContractAddress, new_implementation: ClassHash, eta: u64); - fn execute_transaction( - target: ContractAddress, new_implementation: ClassHash, eta: u64 - ) -> ClassHash; } From 7d7f0991b3d58f6c9e71852368e4ab5a3c9d5905 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Mon, 8 Apr 2024 12:34:55 +0800 Subject: [PATCH 24/35] moved token fn from impl + interface to free --- governance/src/systems/token/contract.cairo | 73 ++++++++++---------- governance/src/systems/token/interface.cairo | 2 +- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/governance/src/systems/token/contract.cairo b/governance/src/systems/token/contract.cairo index fc600dc3..807a18c0 100644 --- a/governance/src/systems/token/contract.cairo +++ b/governance/src/systems/token/contract.cairo @@ -89,52 +89,53 @@ mod governancetoken { 0 } } + } - fn get_prior_votes(account: ContractAddress, block_number: u64) -> u128 { - let block_number = get_block_number(); - assert!(block_number < block_number, "Governance Token: not yet determined"); - let world = self.world_dispatcher.read(); - let n_checkpoints = get!(world, account, NumCheckpoints).count; - if n_checkpoints.is_zero() { - return 0; + fn get_prior_votes( + world: IWorldDispatcher, account: ContractAddress, block_number: u64 + ) -> u128 { + let block_number = get_block_number(); + assert!(block_number < block_number, "Governance Token: not yet determined"); + let n_checkpoints = get!(world, account, NumCheckpoints).count; + if n_checkpoints.is_zero() { + return 0; + } + let most_recent_checkpoint = get!(world, (account, n_checkpoints - 1), Checkpoints) + .checkpoint; + if most_recent_checkpoint.from_block > block_number { + return 0; + } + let mut lower = 0; + let mut upper = n_checkpoints - 1; + let mut votes = 0; + loop { + if lower == upper { + votes = get!(world, (account, lower), Checkpoints).checkpoint.votes; + break; } - let most_recent_checkpoint = get!(world, (account, n_checkpoints - 1), Checkpoints) - .checkpoint; - if most_recent_checkpoint.from_block > block_number { - return 0; + let center = upper - (upper - lower) / 2; + let cp = get!(world, (account, center), Checkpoints).checkpoint; + if cp.from_block == block_number { + votes = cp.votes; + break; + } else if cp.from_block < block_number { + lower = center; + } else { + upper = center - 1; } - let mut lower = 0; - let mut upper = n_checkpoints - 1; - let mut votes = 0; - loop { - if lower == upper { - votes = get!(world, (account, lower), Checkpoints).checkpoint.votes; - break; - } - let center = upper - (upper - lower) / 2; - let cp = get!(world, (account, center), Checkpoints).checkpoint; - if cp.from_block == block_number { - votes = cp.votes; - break; - } else if cp.from_block < block_number { - lower = center; - } else { - upper = center - 1; - } - }; - votes - } + }; + votes } fn delegate(world: IWorldDispatcher, delegator: ContractAddress, delegatee: ContractAddress) { - let currentDelegate = get!(world, delegator, Delegates).delegatee; - let delegatorBalance = get!(world, delegator, Balances).amount; + let current_delegate = get!(world, delegator, Delegates).delegatee; + let delegator_balance = get!(world, delegator, Balances).amount; set!(world, Delegates { account: delegator, delegatee }); emit!( - world, tokenevents::DelegateChanged { delegator, from: currentDelegate, to: delegatee } + world, tokenevents::DelegateChanged { delegator, from: current_delegate, to: delegatee } ); - move_delegates(world, currentDelegate, delegatee, delegatorBalance); + move_delegates(world, current_delegate, delegatee, delegator_balance); } fn transfer_tokens( diff --git a/governance/src/systems/token/interface.cairo b/governance/src/systems/token/interface.cairo index ec268942..97b8a9fa 100644 --- a/governance/src/systems/token/interface.cairo +++ b/governance/src/systems/token/interface.cairo @@ -14,5 +14,5 @@ trait IGovernanceToken { fn transfer_from(from: ContractAddress, to: ContractAddress, amount: u128); fn delegate(delegatee: ContractAddress); fn get_current_votes(account: ContractAddress) -> u128; - fn get_prior_votes(account: ContractAddress, block_number: u64) -> u128; +// fn get_prior_votes(account: ContractAddress, block_number: u64) -> u128; } From f2e42f289e764f06f6481fb9a6b1e509f63fff89 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Mon, 8 Apr 2024 13:35:17 +0800 Subject: [PATCH 25/35] added option for abstain vote --- governance/src/libraries/events.cairo | 3 ++- governance/src/libraries/traits.cairo | 8 ++++++++ governance/src/models/governor.cairo | 12 ++++++++++-- governance/src/systems/governor/contract.cairo | 12 ++++++------ governance/src/systems/governor/interface.cairo | 4 ++-- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/governance/src/libraries/events.cairo b/governance/src/libraries/events.cairo index dc86948f..1ffff1d6 100644 --- a/governance/src/libraries/events.cairo +++ b/governance/src/libraries/events.cairo @@ -86,6 +86,7 @@ mod timelockevents { } mod governorevents { + use governance::models::governor::Support; use starknet::{ContractAddress, ClassHash}; #[derive(Model, Copy, Drop, Serde)] @@ -106,7 +107,7 @@ mod governorevents { #[key] voter: ContractAddress, proposal_id: usize, - support: bool, + support: Support, votes: u128, } diff --git a/governance/src/libraries/traits.cairo b/governance/src/libraries/traits.cairo index bbd11b3e..9a18f48e 100644 --- a/governance/src/libraries/traits.cairo +++ b/governance/src/libraries/traits.cairo @@ -1,3 +1,4 @@ +use governance::models::governor::Support; use starknet::{ClassHash, ContractAddress, class_hash_const, contract_address_const}; impl ContractAddressDefault of Default { @@ -13,3 +14,10 @@ impl ClassHashDefault of Default { class_hash_const::<0>() } } + +impl SupportDefault of Default { + #[inline(always)] + fn default() -> Support nopanic { + Support::Abstain(()) + } +} diff --git a/governance/src/models/governor.cairo b/governance/src/models/governor.cairo index 6f0a7991..8604e73c 100644 --- a/governance/src/models/governor.cairo +++ b/governance/src/models/governor.cairo @@ -1,4 +1,4 @@ -use governance::libraries::traits::{ContractAddressDefault, ClassHashDefault}; +use governance::libraries::traits::{ContractAddressDefault, ClassHashDefault, SupportDefault}; use starknet::{ContractAddress, ClassHash}; #[derive(Model, Copy, Drop, Serde)] @@ -60,6 +60,7 @@ struct Proposal { start_block: u64, end_block: u64, for_votes: u128, + abstain_votes: u128, against_votes: u128, canceled: bool, executed: bool, @@ -68,7 +69,7 @@ struct Proposal { #[derive(Copy, Default, Drop, Introspect, Serde)] struct Receipt { has_voted: bool, - support: bool, + support: Support, votes: u128 } @@ -83,3 +84,10 @@ enum ProposalState { Expired, Executed } + +#[derive(Copy, Drop, Introspect, Serde)] +enum Support { + For, + Against, + Abstain, +} diff --git a/governance/src/systems/governor/contract.cairo b/governance/src/systems/governor/contract.cairo index af1e09f9..2ff1cd6a 100644 --- a/governance/src/systems/governor/contract.cairo +++ b/governance/src/systems/governor/contract.cairo @@ -4,7 +4,7 @@ mod governor { use governance::models::{ governor::{ GovernorParams, ProposalParams, ProposalCount, Proposals, Proposal, Receipt, - ProposalState, LatestProposalIds, Receipts + ProposalState, LatestProposalIds, Receipts, Support }, timelock::{QueuedTransactions, TimelockParams} }; @@ -211,7 +211,7 @@ mod governor { } } - fn cast_vote(proposal_id: usize, support: bool) { + fn cast_vote(proposal_id: usize, support: Support) { let world = self.world_dispatcher.read(); let state = self.state(proposal_id); match state { @@ -225,10 +225,10 @@ mod governor { assert!(!receipt.has_voted, "Governor::cast_vote: voter already voted"); let votes = governancetoken::get_prior_votes(world, caller, proposal.start_block); - if support { - proposal.for_votes += votes; - } else { - proposal.against_votes += votes; + match support { + Support::For => { proposal.for_votes += votes; }, + Support::Against => { proposal.against_votes += votes; }, + Support::Abstain => { proposal.abstain_votes += votes; } } receipt.has_voted = true; diff --git a/governance/src/systems/governor/interface.cairo b/governance/src/systems/governor/interface.cairo index 6ae4e847..ebbbacff 100644 --- a/governance/src/systems/governor/interface.cairo +++ b/governance/src/systems/governor/interface.cairo @@ -1,4 +1,4 @@ -use governance::models::governor::{ProposalState, Receipt}; +use governance::models::governor::{ProposalState, Receipt, Support}; use starknet::{ContractAddress, ClassHash}; #[dojo::interface] @@ -13,5 +13,5 @@ trait IGovernor { fn cancel(proposal_id: usize); fn get_action(proposal_id: usize) -> (ContractAddress, ClassHash); fn state(proposal_id: usize) -> ProposalState; - fn cast_vote(proposal_id: usize, support: bool); + fn cast_vote(proposal_id: usize, support: Support); } From 8778574c921c4a078c08b9481023fa5b25aed259 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Mon, 8 Apr 2024 17:03:42 +0800 Subject: [PATCH 26/35] removed custom default impl for enum --- governance/src/libraries/traits.cairo | 7 ------- governance/src/models/governor.cairo | 5 +++-- governance/src/systems/governor/contract.cairo | 3 --- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/governance/src/libraries/traits.cairo b/governance/src/libraries/traits.cairo index 9a18f48e..bfde19c0 100644 --- a/governance/src/libraries/traits.cairo +++ b/governance/src/libraries/traits.cairo @@ -1,4 +1,3 @@ -use governance::models::governor::Support; use starknet::{ClassHash, ContractAddress, class_hash_const, contract_address_const}; impl ContractAddressDefault of Default { @@ -15,9 +14,3 @@ impl ClassHashDefault of Default { } } -impl SupportDefault of Default { - #[inline(always)] - fn default() -> Support nopanic { - Support::Abstain(()) - } -} diff --git a/governance/src/models/governor.cairo b/governance/src/models/governor.cairo index 8604e73c..73d1613a 100644 --- a/governance/src/models/governor.cairo +++ b/governance/src/models/governor.cairo @@ -1,4 +1,4 @@ -use governance::libraries::traits::{ContractAddressDefault, ClassHashDefault, SupportDefault}; +use governance::libraries::traits::{ContractAddressDefault, ClassHashDefault}; use starknet::{ContractAddress, ClassHash}; #[derive(Model, Copy, Drop, Serde)] @@ -85,9 +85,10 @@ enum ProposalState { Executed } -#[derive(Copy, Drop, Introspect, Serde)] +#[derive(Copy, Default, Drop, Introspect, Serde)] enum Support { For, Against, + #[default] Abstain, } diff --git a/governance/src/systems/governor/contract.cairo b/governance/src/systems/governor/contract.cairo index 2ff1cd6a..bd584d14 100644 --- a/governance/src/systems/governor/contract.cairo +++ b/governance/src/systems/governor/contract.cairo @@ -98,9 +98,6 @@ mod governor { new_proposal.target = target; new_proposal.start_block = start_block; new_proposal.end_block = end_block; - // TODO: unsure if default bool is false, hardcoded for now, can remove if is false - new_proposal.canceled = false; - new_proposal.executed = false; set!(world, LatestProposalIds { address: caller, id: proposal_id }); From 4133cb50899322b95620b1de1b41623570ca38c8 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Mon, 8 Apr 2024 17:04:02 +0800 Subject: [PATCH 27/35] cleanup --- governance/src/lib.cairo | 12 ------------ governance/src/models/delegate.cairo | 1 - governance/src/models/delegator.cairo | 1 - governance/src/systems/delegate/contract.cairo | 1 - governance/src/systems/delegate/interface.cairo | 1 - governance/src/systems/delegate/tests.cairo | 1 - governance/src/systems/delegator/contract.cairo | 1 - governance/src/systems/delegator/interface.cairo | 1 - governance/src/systems/delegator/tests.cairo | 1 - 9 files changed, 20 deletions(-) delete mode 100644 governance/src/models/delegate.cairo delete mode 100644 governance/src/models/delegator.cairo delete mode 100644 governance/src/systems/delegate/contract.cairo delete mode 100644 governance/src/systems/delegate/interface.cairo delete mode 100644 governance/src/systems/delegate/tests.cairo delete mode 100644 governance/src/systems/delegator/contract.cairo delete mode 100644 governance/src/systems/delegator/interface.cairo delete mode 100644 governance/src/systems/delegator/tests.cairo diff --git a/governance/src/lib.cairo b/governance/src/lib.cairo index b1c59a15..77699900 100644 --- a/governance/src/lib.cairo +++ b/governance/src/lib.cairo @@ -4,24 +4,12 @@ mod libraries { } mod models { - mod delegate; - mod delegator; mod governor; mod timelock; mod token; } mod systems { - mod delegate { - mod contract; - mod interface; - mod tests; - } - mod delegator { - mod contract; - mod interface; - mod tests; - } mod governor { mod contract; mod interface; diff --git a/governance/src/models/delegate.cairo b/governance/src/models/delegate.cairo deleted file mode 100644 index 8b137891..00000000 --- a/governance/src/models/delegate.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/governance/src/models/delegator.cairo b/governance/src/models/delegator.cairo deleted file mode 100644 index 8b137891..00000000 --- a/governance/src/models/delegator.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/governance/src/systems/delegate/contract.cairo b/governance/src/systems/delegate/contract.cairo deleted file mode 100644 index 8b137891..00000000 --- a/governance/src/systems/delegate/contract.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/governance/src/systems/delegate/interface.cairo b/governance/src/systems/delegate/interface.cairo deleted file mode 100644 index 8b137891..00000000 --- a/governance/src/systems/delegate/interface.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/governance/src/systems/delegate/tests.cairo b/governance/src/systems/delegate/tests.cairo deleted file mode 100644 index 8b137891..00000000 --- a/governance/src/systems/delegate/tests.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/governance/src/systems/delegator/contract.cairo b/governance/src/systems/delegator/contract.cairo deleted file mode 100644 index 8b137891..00000000 --- a/governance/src/systems/delegator/contract.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/governance/src/systems/delegator/interface.cairo b/governance/src/systems/delegator/interface.cairo deleted file mode 100644 index 8b137891..00000000 --- a/governance/src/systems/delegator/interface.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/governance/src/systems/delegator/tests.cairo b/governance/src/systems/delegator/tests.cairo deleted file mode 100644 index 8b137891..00000000 --- a/governance/src/systems/delegator/tests.cairo +++ /dev/null @@ -1 +0,0 @@ - From a311cbdbbbb6206bead3f7bcdc9f19ee821ee5d1 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Mon, 8 Apr 2024 17:37:08 +0800 Subject: [PATCH 28/35] reexposed fn that were prev free --- .../src/systems/governor/contract.cairo | 49 ++++-- .../src/systems/timelock/contract.cairo | 148 ++++++++++-------- .../src/systems/timelock/interface.cairo | 3 + governance/src/systems/token/contract.cairo | 66 ++++---- governance/src/systems/token/interface.cairo | 2 +- 5 files changed, 155 insertions(+), 113 deletions(-) diff --git a/governance/src/systems/governor/contract.cairo b/governance/src/systems/governor/contract.cairo index bd584d14..bf3bbf42 100644 --- a/governance/src/systems/governor/contract.cairo +++ b/governance/src/systems/governor/contract.cairo @@ -9,8 +9,9 @@ mod governor { timelock::{QueuedTransactions, TimelockParams} }; use governance::systems::{ - governor::interface::IGovernor, timelock::contract::timelock, - token::contract::governancetoken + governor::interface::IGovernor, + timelock::{contract::timelock, interface::{ITimelockDispatcher, ITimelockDispatcherTrait}}, + token::interface::{IGovernanceTokenDispatcher, IGovernanceTokenDispatcherTrait} }; use starknet::{ ContractAddress, ClassHash, get_contract_address, get_caller_address, info::get_block_number @@ -62,7 +63,10 @@ mod governor { let caller = get_caller_address(); let params = get!(world, contract, ProposalParams); let curr_block = get_block_number(); - let prior_votes = governancetoken::get_prior_votes(world, caller, curr_block - 1); + let gov_token = IGovernanceTokenDispatcher { + contract_address: get!(world, contract, GovernorParams).gov_token + }; + let prior_votes = gov_token.get_prior_votes(caller, curr_block - 1); assert!( prior_votes > params.threshold, "Governor::propose: proposer votes below proposal threshold" @@ -136,15 +140,19 @@ mod governor { let mut proposal = get!(world, proposal_id, Proposals).proposal; proposal.executed = true; - timelock::execute_transaction( - world, proposal.target, proposal.class_hash, proposal.eta - ); + + let timelock = ITimelockDispatcher { + contract_address: get!(world, get_contract_address(), GovernorParams).timelock + }; + timelock.execute_transaction(proposal.target, proposal.class_hash, proposal.eta); + emit!(world, governorevents::ProposalExecuted { id: proposal_id, executed: true }); } fn cancel(proposal_id: usize) { let world = self.world_dispatcher.read(); let state = self.state(proposal_id); + let contract = get_contract_address(); match state { ProposalState::Executed(()) => { @@ -154,18 +162,24 @@ mod governor { } let mut proposal = get!(world, proposal_id, Proposals).proposal; - let guardian = get!(world, get_contract_address(), GovernorParams).guardian; - let threshold = get!(world, get_contract_address(), ProposalParams).threshold; - let prior_votes = governancetoken::get_prior_votes( - world, proposal.proposer, get_block_number() - 1 - ); + let guardian = get!(world, contract, GovernorParams).guardian; + let threshold = get!(world, contract, ProposalParams).threshold; + let gov_token = IGovernanceTokenDispatcher { + contract_address: get!(world, contract, GovernorParams).gov_token + }; + let prior_votes = gov_token.get_prior_votes(proposal.proposer, get_block_number() - 1); assert!( guardian == get_caller_address() || prior_votes < threshold, "Governor::cancel: proposer above threshold" ); proposal.canceled = true; - timelock::cancel_transaction(world, proposal.target, proposal.class_hash, proposal.eta); + + let timelock = ITimelockDispatcher { + contract_address: get!(world, contract, GovernorParams).timelock + }; + timelock.cancel_transaction(proposal.target, proposal.class_hash, proposal.eta); + emit!(world, governorevents::ProposalCanceled { id: proposal_id, cancelled: true }); } @@ -221,7 +235,10 @@ mod governor { let mut receipt = get!(world, (proposal_id, caller), Receipts).receipt; assert!(!receipt.has_voted, "Governor::cast_vote: voter already voted"); - let votes = governancetoken::get_prior_votes(world, caller, proposal.start_block); + let gov_token = IGovernanceTokenDispatcher { + contract_address: get!(world, get_contract_address(), GovernorParams).gov_token + }; + let votes = gov_token.get_prior_votes(caller, proposal.start_block); match support { Support::For => { proposal.for_votes += votes; }, Support::Against => { proposal.against_votes += votes; }, @@ -240,6 +257,10 @@ mod governor { ) { let queued_tx = get!(world, (target, class_hash), QueuedTransactions).queued; assert!(!queued_tx, "Governor::queue_or_revert: proposal action already queued at eta"); - timelock::que_transaction(world, target, class_hash, eta); + + let timelock = ITimelockDispatcher { + contract_address: get!(world, get_contract_address(), GovernorParams).timelock + }; + timelock.que_transaction(target, class_hash, eta); } } diff --git a/governance/src/systems/timelock/contract.cairo b/governance/src/systems/timelock/contract.cairo index 85a5e37a..6e1315ac 100644 --- a/governance/src/systems/timelock/contract.cairo +++ b/governance/src/systems/timelock/contract.cairo @@ -37,72 +37,92 @@ mod timelock { timelockevents::NewDelay { contract, value: delay } ); } - } - fn execute_transaction( - world: IWorldDispatcher, target: ContractAddress, new_implementation: ClassHash, eta: u64 - ) { - let params = get!(world, get_contract_address(), TimelockParams); - assert!( - get_caller_address() == params.admin, - "Timelock::execute_transaction: Call must come from admin." - ); - let queued_tx = get!(world, (target, new_implementation), QueuedTransactions); - assert!(queued_tx.queued, "Timelock::execute_transaction: Transaction hasn't been queued."); - let timestamp = get_block_timestamp(); - assert!( - timestamp >= eta, - "Timelock::execute_transaction: Transaction hasn't surpassed time lock." - ); - assert!( - timestamp <= eta + GRACE_PERIOD, "Timelock::execute_transaction: Transaction is stale." - ); - set!( - world, - QueuedTransactions { contract: target, class_hash: new_implementation, queued: false } - ); - let upgraded_class_hash = world.upgrade_contract(target, new_implementation); - emit!( - world, - timelockevents::ExecuteTransaction { target, class_hash: upgraded_class_hash, eta } - ); - } + fn execute_transaction( + world: IWorldDispatcher, + target: ContractAddress, + new_implementation: ClassHash, + eta: u64 + ) { + let params = get!(world, get_contract_address(), TimelockParams); + assert!( + get_caller_address() == params.admin, + "Timelock::execute_transaction: Call must come from admin." + ); + let queued_tx = get!(world, (target, new_implementation), QueuedTransactions); + assert!( + queued_tx.queued, "Timelock::execute_transaction: Transaction hasn't been queued." + ); + let timestamp = get_block_timestamp(); + assert!( + timestamp >= eta, + "Timelock::execute_transaction: Transaction hasn't surpassed time lock." + ); + assert!( + timestamp <= eta + GRACE_PERIOD, + "Timelock::execute_transaction: Transaction is stale." + ); + set!( + world, + QueuedTransactions { + contract: target, class_hash: new_implementation, queued: false + } + ); + let upgraded_class_hash = world.upgrade_contract(target, new_implementation); + emit!( + world, + timelockevents::ExecuteTransaction { target, class_hash: upgraded_class_hash, eta } + ); + } - fn que_transaction( - world: IWorldDispatcher, target: ContractAddress, new_implementation: ClassHash, eta: u64 - ) { - let params = get!(world, get_contract_address(), TimelockParams); - assert!( - get_caller_address() == params.admin, - "Timelock::queue_transaction: Call must come from admin." - ); - assert!( - eta >= get_block_timestamp() + params.delay, - "Timelock::queue_transaction: Estimated execution block must satisfy delay." - ); - set!( - world, - QueuedTransactions { contract: target, class_hash: new_implementation, queued: true } - ); - emit!( - world, timelockevents::QueueTransaction { target, class_hash: new_implementation, eta } - ); - } + fn que_transaction( + world: IWorldDispatcher, + target: ContractAddress, + new_implementation: ClassHash, + eta: u64 + ) { + let params = get!(world, get_contract_address(), TimelockParams); + assert!( + get_caller_address() == params.admin, + "Timelock::queue_transaction: Call must come from admin." + ); + assert!( + eta >= get_block_timestamp() + params.delay, + "Timelock::queue_transaction: Estimated execution block must satisfy delay." + ); + set!( + world, + QueuedTransactions { + contract: target, class_hash: new_implementation, queued: true + } + ); + emit!( + world, + timelockevents::QueueTransaction { target, class_hash: new_implementation, eta } + ); + } - fn cancel_transaction( - world: IWorldDispatcher, target: ContractAddress, new_implementation: ClassHash, eta: u64 - ) { - let params = get!(world, get_contract_address(), TimelockParams); - assert!( - get_caller_address() == params.admin, - "Timelock::cancel_transaction: Call must come from admin." - ); - set!( - world, - QueuedTransactions { contract: target, class_hash: new_implementation, queued: false } - ); - emit!( - world, timelockevents::CancelTransaction { target, class_hash: new_implementation, eta } - ); + fn cancel_transaction( + world: IWorldDispatcher, + target: ContractAddress, + new_implementation: ClassHash, + eta: u64 + ) { + let params = get!(world, get_contract_address(), TimelockParams); + assert!( + get_caller_address() == params.admin, + "Timelock::cancel_transaction: Call must come from admin." + ); + set!( + world, + QueuedTransactions { + contract: target, class_hash: new_implementation, queued: false + } + ); + emit!( + world, + timelockevents::CancelTransaction { target, class_hash: new_implementation, eta } + ); + } } } diff --git a/governance/src/systems/timelock/interface.cairo b/governance/src/systems/timelock/interface.cairo index 17b53c84..ca345061 100644 --- a/governance/src/systems/timelock/interface.cairo +++ b/governance/src/systems/timelock/interface.cairo @@ -3,4 +3,7 @@ use starknet::{ContractAddress, ClassHash}; #[dojo::interface] trait ITimelock { fn initialize(admin: ContractAddress, delay: u64); + fn execute_transaction(target: ContractAddress, new_implementation: ClassHash, eta: u64); + fn que_transaction(target: ContractAddress, new_implementation: ClassHash, eta: u64); + fn cancel_transaction(target: ContractAddress, new_implementation: ClassHash, eta: u64); } diff --git a/governance/src/systems/token/contract.cairo b/governance/src/systems/token/contract.cairo index 807a18c0..6327642c 100644 --- a/governance/src/systems/token/contract.cairo +++ b/governance/src/systems/token/contract.cairo @@ -89,45 +89,43 @@ mod governancetoken { 0 } } - } - fn get_prior_votes( - world: IWorldDispatcher, account: ContractAddress, block_number: u64 - ) -> u128 { - let block_number = get_block_number(); - assert!(block_number < block_number, "Governance Token: not yet determined"); - let n_checkpoints = get!(world, account, NumCheckpoints).count; - if n_checkpoints.is_zero() { - return 0; - } - let most_recent_checkpoint = get!(world, (account, n_checkpoints - 1), Checkpoints) - .checkpoint; - if most_recent_checkpoint.from_block > block_number { - return 0; - } - let mut lower = 0; - let mut upper = n_checkpoints - 1; - let mut votes = 0; - loop { - if lower == upper { - votes = get!(world, (account, lower), Checkpoints).checkpoint.votes; - break; + fn get_prior_votes(account: ContractAddress, block_number: u64) -> u128 { + let world = self.world_dispatcher.read(); + let block_number = get_block_number(); + assert!(block_number < block_number, "Governance Token: not yet determined"); + let n_checkpoints = get!(world, account, NumCheckpoints).count; + if n_checkpoints.is_zero() { + return 0; } - let center = upper - (upper - lower) / 2; - let cp = get!(world, (account, center), Checkpoints).checkpoint; - if cp.from_block == block_number { - votes = cp.votes; - break; - } else if cp.from_block < block_number { - lower = center; - } else { - upper = center - 1; + let most_recent_checkpoint = get!(world, (account, n_checkpoints - 1), Checkpoints) + .checkpoint; + if most_recent_checkpoint.from_block > block_number { + return 0; } - }; - votes + let mut lower = 0; + let mut upper = n_checkpoints - 1; + let mut votes = 0; + loop { + if lower == upper { + votes = get!(world, (account, lower), Checkpoints).checkpoint.votes; + break; + } + let center = upper - (upper - lower) / 2; + let cp = get!(world, (account, center), Checkpoints).checkpoint; + if cp.from_block == block_number { + votes = cp.votes; + break; + } else if cp.from_block < block_number { + lower = center; + } else { + upper = center - 1; + } + }; + votes + } } - fn delegate(world: IWorldDispatcher, delegator: ContractAddress, delegatee: ContractAddress) { let current_delegate = get!(world, delegator, Delegates).delegatee; let delegator_balance = get!(world, delegator, Balances).amount; diff --git a/governance/src/systems/token/interface.cairo b/governance/src/systems/token/interface.cairo index 97b8a9fa..ec268942 100644 --- a/governance/src/systems/token/interface.cairo +++ b/governance/src/systems/token/interface.cairo @@ -14,5 +14,5 @@ trait IGovernanceToken { fn transfer_from(from: ContractAddress, to: ContractAddress, amount: u128); fn delegate(delegatee: ContractAddress); fn get_current_votes(account: ContractAddress) -> u128; -// fn get_prior_votes(account: ContractAddress, block_number: u64) -> u128; + fn get_prior_votes(account: ContractAddress, block_number: u64) -> u128; } From 6e604ed8ae794d8513aed5ac67301c329ba49698 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Tue, 9 Apr 2024 07:32:49 +0800 Subject: [PATCH 29/35] add #[abi(embed_v0)] to impl --- governance/src/lib.cairo | 8 + governance/src/models/token.cairo | 4 +- .../src/systems/governor/contract.cairo | 1 + .../src/systems/timelock/contract.cairo | 10 +- governance/src/systems/token/contract.cairo | 83 ++++++--- governance/src/systems/token/tests.cairo | 165 ++++++++++++++++++ governance/src/utils/testing.cairo | 99 +++++++++++ 7 files changed, 342 insertions(+), 28 deletions(-) create mode 100644 governance/src/utils/testing.cairo diff --git a/governance/src/lib.cairo b/governance/src/lib.cairo index 77699900..1811d3ac 100644 --- a/governance/src/lib.cairo +++ b/governance/src/lib.cairo @@ -13,17 +13,25 @@ mod systems { mod governor { mod contract; mod interface; + #[cfg(test)] mod tests; } mod timelock { mod contract; mod interface; + #[cfg(test)] mod tests; } mod token { mod contract; mod interface; + #[cfg(test)] mod tests; } } +mod utils { + #[cfg(test)] + mod testing; +} + diff --git a/governance/src/models/token.cairo b/governance/src/models/token.cairo index 558aa2cf..0cc50c6e 100644 --- a/governance/src/models/token.cairo +++ b/governance/src/models/token.cairo @@ -36,7 +36,7 @@ struct Balances { struct Delegates { #[key] account: ContractAddress, - delegatee: ContractAddress, + address: ContractAddress, } #[derive(Model, Copy, Drop, Serde)] @@ -62,7 +62,7 @@ struct Nonces { nonce: usize, } -#[derive(Copy, Drop, Introspect, Serde)] +#[derive(Copy, Debug, Drop, Introspect, Serde)] struct Checkpoint { from_block: u64, votes: u128, diff --git a/governance/src/systems/governor/contract.cairo b/governance/src/systems/governor/contract.cairo index bf3bbf42..e5086b6f 100644 --- a/governance/src/systems/governor/contract.cairo +++ b/governance/src/systems/governor/contract.cairo @@ -17,6 +17,7 @@ mod governor { ContractAddress, ClassHash, get_contract_address, get_caller_address, info::get_block_number }; + #[abi(embed_v0)] impl GovernorImpl of IGovernor { fn initialize( timelock: ContractAddress, gov_token: ContractAddress, guardian: ContractAddress diff --git a/governance/src/systems/timelock/contract.cairo b/governance/src/systems/timelock/contract.cairo index 6e1315ac..a62dc385 100644 --- a/governance/src/systems/timelock/contract.cairo +++ b/governance/src/systems/timelock/contract.cairo @@ -14,21 +14,21 @@ mod timelock { const MINIMUM_DELAY: u64 = 172_800; // 2 days; const MAXIMUM_DELAY: u64 = 2_592_000; // 30 days; + #[abi(embed_v0)] impl TimelockImpl of ITimelock { fn initialize(admin: ContractAddress, delay: u64) { - assert!(!admin.is_zero(), "Timelock::constructor: Admin address cannot be zero."); + assert!(!admin.is_zero(), "Timelock::initialize: Admin address cannot be zero."); assert!( - delay >= MINIMUM_DELAY, "Timelock::constructor: Delay must exceed minimum delay." + delay >= MINIMUM_DELAY, "Timelock::initialize: Delay must exceed minimum delay." ); assert!( - delay <= MAXIMUM_DELAY, - "Timelock::constructor: Delay must not exceed maximum delay." + delay <= MAXIMUM_DELAY, "Timelock::initialize: Delay must not exceed maximum delay." ); let world = self.world_dispatcher.read(); let contract = get_contract_address(); let curr_params = get!(world, contract, TimelockParams); assert!( - curr_params.admin == Zeroable::zero(), "Timelock::constructor: Already initialized." + curr_params.admin == Zeroable::zero(), "Timelock::initialize: Already initialized." ); set!(world, TimelockParams { contract, admin, delay }); emit!( diff --git a/governance/src/systems/token/contract.cairo b/governance/src/systems/token/contract.cairo index 6327642c..a5d4e42e 100644 --- a/governance/src/systems/token/contract.cairo +++ b/governance/src/systems/token/contract.cairo @@ -2,7 +2,8 @@ mod governancetoken { use governance::libraries::events::tokenevents; use governance::models::token::{ - Allowances, Metadata, TotalSupply, Balances, Delegates, NumCheckpoints, Checkpoints + Allowances, Metadata, TotalSupply, Balances, Delegates, NumCheckpoints, Checkpoints, + Checkpoint }; use governance::systems::token::interface::IGovernanceToken; use integer::BoundedInt; @@ -11,6 +12,7 @@ mod governancetoken { info::{get_block_number, get_execution_info}, }; + #[abi(embed_v0)] impl GovernanceTokenImpl of IGovernanceToken { fn initialize( name: felt252, @@ -54,6 +56,7 @@ mod governancetoken { } fn transfer(to: ContractAddress, amount: u128) { + println!("caller {:?}", get_caller_address()); transfer_tokens(self.world_dispatcher.read(), get_caller_address(), to, amount); } @@ -126,10 +129,21 @@ mod governancetoken { } } + + // function _delegate(address delegator, address delegatee) internal { + // address currentDelegate = delegates[delegator]; + // uint96 delegatorBalance = balances[delegator]; + // delegates[delegator] = delegatee; + + // emit DelegateChanged(delegator, currentDelegate, delegatee); + + // _moveDelegates(currentDelegate, delegatee, delegatorBalance); + // } + fn delegate(world: IWorldDispatcher, delegator: ContractAddress, delegatee: ContractAddress) { - let current_delegate = get!(world, delegator, Delegates).delegatee; + let current_delegate = get!(world, delegator, Delegates).address; let delegator_balance = get!(world, delegator, Balances).amount; - set!(world, Delegates { account: delegator, delegatee }); + set!(world, Delegates { account: delegator, address: delegatee }); emit!( world, tokenevents::DelegateChanged { delegator, from: current_delegate, to: delegatee } ); @@ -146,28 +160,48 @@ mod governancetoken { assert!(from_balance >= amount, "Governance Token: insufficient balance"); let to_balance = get!(world, to, Balances).amount; - set!(world, Balances { account: to, amount: to_balance + amount }); + set!( + world, + ( + Balances { account: to, amount: to_balance + amount }, + Balances { account: from, amount: from_balance - amount } + ) + ); emit!(world, tokenevents::Transfer { from, to, amount }); - let from_rep = get!(world, from, Delegates).delegatee; - let to_rep = get!(world, to, Delegates).delegatee; + let from_rep = get!(world, from, Delegates).address; + let to_rep = get!(world, to, Delegates).address; move_delegates(world, from_rep, to_rep, amount); } fn move_delegates( - world: IWorldDispatcher, from_rep: ContractAddress, to_rep: ContractAddress, amount: u128 + world: IWorldDispatcher, src_rep: ContractAddress, dst_rep: ContractAddress, amount: u128 ) { - if from_rep != to_rep && !amount.is_zero() { - if !from_rep.is_zero() { - let from_rep_num = get!(world, from_rep, NumCheckpoints).count; - let from_rep_old = if !from_rep_num.is_zero() { - get!(world, (from_rep, from_rep_num - 1), Checkpoints).checkpoint.votes + println!("amount {}", amount); + println!("src_rep {:?}", src_rep); + println!("dst_rep {:?}", dst_rep); + if src_rep != dst_rep && !amount.is_zero() { + if !src_rep.is_zero() { + let src_rep_num = get!(world, src_rep, NumCheckpoints).count; + let src_rep_old = if !src_rep_num.is_zero() { + get!(world, (src_rep, src_rep_num - 1), Checkpoints).checkpoint.votes + } else { + 0 + }; + assert!(src_rep_old >= amount, "Governance Token: vote amount underflows"); + let src_rep_new = src_rep_old - amount; + write_checkpoint(world, src_rep, src_rep_num, src_rep_old, src_rep_new); + } + + if !dst_rep.is_zero() { + let dst_rep_num = get!(world, dst_rep, NumCheckpoints).count; + let dst_rep_old = if !dst_rep_num.is_zero() { + get!(world, (dst_rep, dst_rep_num - 1), Checkpoints).checkpoint.votes } else { 0 }; - assert!(from_rep_old >= amount, "Governance Token: vote amount underflows"); - let from_rep_new = from_rep_old - amount; - write_checkpoint(world, from_rep, from_rep_num, from_rep_old, from_rep_new); + let dst_rep_new = dst_rep_old + amount; + write_checkpoint(world, dst_rep, dst_rep_num, dst_rep_old, dst_rep_new); } } } @@ -179,14 +213,21 @@ mod governancetoken { old_votes: u128, new_votes: u128 ) { + println!("old_votes {}", old_votes); + println!("new_votes {}", new_votes); + println!("n_checkpoints {}", n_checkpoints); let block_number = get_block_number(); - let mut checkpoint = get!(world, (delegatee, n_checkpoints - 1), Checkpoints).checkpoint; - if !n_checkpoints.is_zero() && checkpoint.from_block == n_checkpoints { - checkpoint.votes = new_votes; - set!(world, Checkpoints { account: delegatee, index: n_checkpoints - 1, checkpoint }); + if !n_checkpoints.is_zero() { + let mut checkpoint = get!(world, (delegatee, n_checkpoints - 1), Checkpoints) + .checkpoint; + if checkpoint.from_block == block_number { + checkpoint.votes = new_votes; + set!( + world, Checkpoints { account: delegatee, index: n_checkpoints - 1, checkpoint } + ); + } } else { - checkpoint.from_block = block_number; - checkpoint.votes = new_votes; + let mut checkpoint = Checkpoint { from_block: block_number, votes: new_votes }; set!(world, Checkpoints { account: delegatee, index: n_checkpoints, checkpoint }); } emit!( diff --git a/governance/src/systems/token/tests.cairo b/governance/src/systems/token/tests.cairo index 8b137891..8b61d709 100644 --- a/governance/src/systems/token/tests.cairo +++ b/governance/src/systems/token/tests.cairo @@ -1 +1,166 @@ +use dojo::world::IWorldDispatcherTrait; +use governance::utils::testing; +use governance::models::token::{ + Metadata, TotalSupply, Allowances, Balances, Delegates, Checkpoints, NumCheckpoints +}; +use governance::systems::token::interface::IGovernanceTokenDispatcherTrait; +use starknet::testing::set_contract_address; +#[test] +fn test_initialize_token() { + let (systems, world) = testing::setup(); + + let metadata = get!(world, systems.token.contract_address, Metadata); + assert!(metadata.name == 'Gov Token', "Name is incorrect"); + assert!(metadata.symbol == 'GOV', "Symbol is incorrect"); + assert!(metadata.decimals == 18, "Decimals is incorrect"); + + let total_supply = get!(world, systems.token.contract_address, TotalSupply).amount; + assert!(total_supply == 100_000_000 * testing::E18, "Total supply is incorrect"); + + let governor_balance = get!(world, testing::GOVERNOR(), Balances).amount; + assert!(governor_balance == 100_000_000 * testing::E18, "Governor balance is incorrect"); +} + +#[test] +fn test_transfer_token() { + let (systems, world) = testing::setup(); + + let amount = 1_000 * testing::E18; + let recipient = testing::ACCOUNT_1(); + + let governor_balance = get!(world, testing::GOVERNOR(), Balances).amount; + let recipient_balance = get!(world, recipient, Balances).amount; + assert!(recipient_balance == 0, "Recipient balance is incorrect"); + + set_contract_address(testing::GOVERNOR()); + systems.token.transfer(recipient, amount); + + let governor_balance_after = get!(world, testing::GOVERNOR(), Balances).amount; + let recipient_balance_after = get!(world, recipient, Balances).amount; + + assert!(governor_balance_after == governor_balance - amount, "Governor balance is incorrect"); + assert!( + recipient_balance_after == recipient_balance + amount, "Recipient balance is incorrect" + ); +} + +#[test] +#[should_panic(expected: ("Governance Token: insufficient balance", 'ENTRYPOINT_FAILED'))] +fn test_transfer_token_fails_insufficient_balance() { + let (systems, _world) = testing::setup(); + + let amount = testing::INITIAL_SUPPLY + 1; + let recipient = testing::ACCOUNT_1(); + + set_contract_address(testing::GOVERNOR()); + systems.token.transfer(recipient, amount); +} + +#[test] +fn test_approve_token() { + let (systems, world) = testing::setup(); + + let amount = 1_000 * testing::E18; + let recipient = testing::ACCOUNT_1(); + + set_contract_address(testing::GOVERNOR()); + systems.token.transfer(recipient, amount); + + set_contract_address(testing::ACCOUNT_1()); + systems.token.approve(testing::GOVERNOR(), amount); + + let allowance = get!(world, (testing::ACCOUNT_1(), testing::GOVERNOR()), Allowances).amount; + assert!(allowance == amount, "Allowance is incorrect"); +} + +#[test] +fn test_transfer_from_token() { + let (systems, world) = testing::setup(); + + let amount = 1_000 * testing::E18; + let recipient = testing::ACCOUNT_1(); + + set_contract_address(testing::GOVERNOR()); + systems.token.transfer(recipient, amount); + + set_contract_address(testing::ACCOUNT_1()); + systems.token.approve(testing::ACCOUNT_2(), amount); + + set_contract_address(testing::ACCOUNT_2()); + systems.token.transfer_from(testing::ACCOUNT_1(), testing::ACCOUNT_3(), amount); + let recipient_balance = get!(world, testing::ACCOUNT_3(), Balances).amount; + assert!(recipient_balance == amount, "Recipient balance is incorrect"); +} + +#[test] +#[should_panic( + expected: ("Governance Token: transfer amount exceeds spender allowance", 'ENTRYPOINT_FAILED') +)] +fn test_transfer_from_fails_allowance_exceeded() { + let (systems, _world) = testing::setup(); + + let amount = 1_000 * testing::E18; + let recipient = testing::ACCOUNT_1(); + + set_contract_address(testing::GOVERNOR()); + systems.token.transfer(recipient, amount); + + set_contract_address(testing::ACCOUNT_1()); + systems.token.approve(testing::ACCOUNT_2(), amount); + + set_contract_address(testing::ACCOUNT_2()); + systems.token.transfer_from(testing::ACCOUNT_1(), testing::ACCOUNT_3(), amount + 1); +} + +#[test] +fn test_delegate_token() { + let (systems, world) = testing::setup(); + + let delegate = testing::ACCOUNT_1(); + + set_contract_address(testing::GOVERNOR()); + systems.token.delegate(delegate); + + let delegatee = get!(world, testing::GOVERNOR(), Delegates).address; + assert!(delegatee == delegate, "Delegatee is incorrect"); +} + +// #[test] +// fn test_change_delegate_token() { +// let (systems, world) = testing::setup(); + +// let delegator = testing::ACCOUNT_1(); +// let delegate_before = testing::ACCOUNT_2(); +// let delegate_after = testing::ACCOUNT_3(); + +// set_contract_address(testing::GOVERNOR()); +// systems.token.transfer(delegator, 100 * testing::E18); + +// set_contract_address(delegator); +// systems.token.delegate(delegate_before); +// systems.token.delegate(delegate_after); + +// let delegatee = get!(world, testing::ACCOUNT_1(), Delegates).delegatee; +// assert!(delegatee == delegate_after, "Delegatee is incorrect"); +// } + +#[test] +fn test_get_current_votes() { + let (systems, world) = testing::setup(); + + let delegate = testing::ACCOUNT_2(); + + set_contract_address(testing::GOVERNOR()); + systems.token.transfer(testing::ACCOUNT_1(), 100 * testing::E18); + let balance = get!(world, testing::ACCOUNT_1(), Balances).amount; + println!("Balance: {}", balance); +// set_contract_address(testing::ACCOUNT_1()); +// systems.token.delegate(testing::ACCOUNT_1()); +// let current_votes = systems.token.get_current_votes(testing::ACCOUNT_1()); +// println!("Current votes before delegation: {}", current_votes); +// systems.token.delegate(delegate); +// let current_votes = systems.token.get_current_votes(testing::ACCOUNT_1()); +// println!("Current votes after: {}", current_votes); +// assert!(current_votes == 100 * testing::E18, "Current votes is incorrect"); +} diff --git a/governance/src/utils/testing.cairo b/governance/src/utils/testing.cairo new file mode 100644 index 00000000..3282f6ca --- /dev/null +++ b/governance/src/utils/testing.cairo @@ -0,0 +1,99 @@ +use dojo::test_utils::{spawn_test_world}; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; +use governance::models::{ + governor::{ + GovernorParams, ProposalParams, ProposalCount, LatestProposalIds, governor_params, + proposal_params, proposal_count, latest_proposal_ids + }, + timelock::{ + TimelockParams, PendingAdmin, QueuedTransactions, timelock_params, pending_admin, + queued_transactions + }, + token::{ + Metadata, TotalSupply, Allowances, Balances, Delegates, Checkpoints, NumCheckpoints, Nonces, + metadata, total_supply, allowances, balances, delegates, checkpoints, num_checkpoints, + nonces + } +}; +use governance::systems::{ + governor::{contract::governor, interface::{IGovernorDispatcher, IGovernorDispatcherTrait}}, + timelock::{contract::timelock, interface::{ITimelockDispatcher, ITimelockDispatcherTrait}}, + token::{ + contract::governancetoken, + interface::{IGovernanceTokenDispatcher, IGovernanceTokenDispatcherTrait} + } +}; +use starknet::{ContractAddress, contract_address_const}; + +const DAY: u64 = 86400; +const E18: u128 = 1_000_000_000_000_000_000; +const INITIAL_SUPPLY: u128 = 100_000_000_000_000_000_000_000_000; + +fn GOVERNOR() -> ContractAddress { + contract_address_const::<'governor'>() +} +fn ACCOUNT_1() -> ContractAddress { + contract_address_const::<0x1>() +} +fn ACCOUNT_2() -> ContractAddress { + contract_address_const::<0x2>() +} +fn ACCOUNT_3() -> ContractAddress { + contract_address_const::<0x3>() +} +fn ACCOUNT_4() -> ContractAddress { + contract_address_const::<0x4>() +} +fn ACCOUNT_5() -> ContractAddress { + contract_address_const::<0x5>() +} + +#[derive(Clone, Copy, Drop, Serde)] +struct Systems { + governor: IGovernorDispatcher, + timelock: ITimelockDispatcher, + token: IGovernanceTokenDispatcher +} + +fn setup() -> (Systems, IWorldDispatcher) { + let models = array![ + governor_params::TEST_CLASS_HASH, + proposal_params::TEST_CLASS_HASH, + proposal_count::TEST_CLASS_HASH, + latest_proposal_ids::TEST_CLASS_HASH, + timelock_params::TEST_CLASS_HASH, + pending_admin::TEST_CLASS_HASH, + queued_transactions::TEST_CLASS_HASH, + metadata::TEST_CLASS_HASH, + total_supply::TEST_CLASS_HASH, + allowances::TEST_CLASS_HASH, + balances::TEST_CLASS_HASH, + delegates::TEST_CLASS_HASH, + checkpoints::TEST_CLASS_HASH, + num_checkpoints::TEST_CLASS_HASH, + nonces::TEST_CLASS_HASH + ]; + let world = spawn_test_world(models); + + let contract_address = world.deploy_contract(1, governor::TEST_CLASS_HASH.try_into().unwrap()); + let governor_contract = IGovernorDispatcher { contract_address }; + + let contract_address = world.deploy_contract(2, timelock::TEST_CLASS_HASH.try_into().unwrap()); + let timelock = ITimelockDispatcher { contract_address }; + + let contract_address = world + .deploy_contract(3, governancetoken::TEST_CLASS_HASH.try_into().unwrap()); + let token = IGovernanceTokenDispatcher { contract_address }; + + let systems = Systems { governor: governor_contract, timelock, token }; + + systems.governor.initialize(timelock.contract_address, token.contract_address, GOVERNOR()); + systems.token.initialize('Gov Token', 'GOV', 18, INITIAL_SUPPLY, GOVERNOR()); + systems.timelock.initialize(systems.governor.contract_address, DAY * 2); + (systems, world) +} + +#[test] +fn test_deploy() { + let (_systems, _world) = setup(); +} From a1d00b07c00f6fe174328b67d15742ba880b54f5 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sat, 27 Apr 2024 06:29:50 +0800 Subject: [PATCH 30/35] fix on token tests --- governance/src/systems/token/contract.cairo | 61 ++++++++----------- governance/src/systems/token/interface.cairo | 2 +- governance/src/systems/token/tests.cairo | 64 +++++++++++--------- 3 files changed, 63 insertions(+), 64 deletions(-) diff --git a/governance/src/systems/token/contract.cairo b/governance/src/systems/token/contract.cairo index a5d4e42e..c0a0f595 100644 --- a/governance/src/systems/token/contract.cairo +++ b/governance/src/systems/token/contract.cairo @@ -7,10 +7,7 @@ mod governancetoken { }; use governance::systems::token::interface::IGovernanceToken; use integer::BoundedInt; - use starknet::{ - ContractAddress, get_caller_address, get_contract_address, - info::{get_block_number, get_execution_info}, - }; + use starknet::{ContractAddress, get_caller_address, get_contract_address, get_block_timestamp,}; #[abi(embed_v0)] impl GovernanceTokenImpl of IGovernanceToken { @@ -56,7 +53,6 @@ mod governancetoken { } fn transfer(to: ContractAddress, amount: u128) { - println!("caller {:?}", get_caller_address()); transfer_tokens(self.world_dispatcher.read(), get_caller_address(), to, amount); } @@ -93,17 +89,17 @@ mod governancetoken { } } - fn get_prior_votes(account: ContractAddress, block_number: u64) -> u128 { + fn get_prior_votes(account: ContractAddress, timestamp: u64) -> u128 { let world = self.world_dispatcher.read(); - let block_number = get_block_number(); - assert!(block_number < block_number, "Governance Token: not yet determined"); + let time_now = get_block_timestamp(); + assert!(time_now > timestamp, "Governance Token: not yet determined"); let n_checkpoints = get!(world, account, NumCheckpoints).count; if n_checkpoints.is_zero() { return 0; } let most_recent_checkpoint = get!(world, (account, n_checkpoints - 1), Checkpoints) .checkpoint; - if most_recent_checkpoint.from_block > block_number { + if most_recent_checkpoint.from_block > timestamp { return 0; } let mut lower = 0; @@ -116,10 +112,10 @@ mod governancetoken { } let center = upper - (upper - lower) / 2; let cp = get!(world, (account, center), Checkpoints).checkpoint; - if cp.from_block == block_number { + if cp.from_block == timestamp { votes = cp.votes; break; - } else if cp.from_block < block_number { + } else if cp.from_block < timestamp { lower = center; } else { upper = center - 1; @@ -175,33 +171,30 @@ mod governancetoken { } fn move_delegates( - world: IWorldDispatcher, src_rep: ContractAddress, dst_rep: ContractAddress, amount: u128 + world: IWorldDispatcher, from: ContractAddress, to: ContractAddress, amount: u128 ) { - println!("amount {}", amount); - println!("src_rep {:?}", src_rep); - println!("dst_rep {:?}", dst_rep); - if src_rep != dst_rep && !amount.is_zero() { - if !src_rep.is_zero() { - let src_rep_num = get!(world, src_rep, NumCheckpoints).count; - let src_rep_old = if !src_rep_num.is_zero() { - get!(world, (src_rep, src_rep_num - 1), Checkpoints).checkpoint.votes + if from != to && !amount.is_zero() { + if !from.is_zero() { + let from_num = get!(world, from, NumCheckpoints).count; + let from_old = if !from_num.is_zero() { + get!(world, (from, from_num - 1), Checkpoints).checkpoint.votes } else { 0 }; - assert!(src_rep_old >= amount, "Governance Token: vote amount underflows"); - let src_rep_new = src_rep_old - amount; - write_checkpoint(world, src_rep, src_rep_num, src_rep_old, src_rep_new); + assert!(from_old >= amount, "Governance Token: vote amount underflows"); + let from_new = from_old - amount; + write_checkpoint(world, from, from_num, from_old, from_new); } - if !dst_rep.is_zero() { - let dst_rep_num = get!(world, dst_rep, NumCheckpoints).count; - let dst_rep_old = if !dst_rep_num.is_zero() { - get!(world, (dst_rep, dst_rep_num - 1), Checkpoints).checkpoint.votes + if !to.is_zero() { + let to_num = get!(world, to, NumCheckpoints).count; + let to_old = if !to_num.is_zero() { + get!(world, (to, to_num - 1), Checkpoints).checkpoint.votes } else { 0 }; - let dst_rep_new = dst_rep_old + amount; - write_checkpoint(world, dst_rep, dst_rep_num, dst_rep_old, dst_rep_new); + let to_new = to_old + amount; + write_checkpoint(world, to, to_num, to_old, to_new); } } } @@ -213,22 +206,20 @@ mod governancetoken { old_votes: u128, new_votes: u128 ) { - println!("old_votes {}", old_votes); - println!("new_votes {}", new_votes); - println!("n_checkpoints {}", n_checkpoints); - let block_number = get_block_number(); + let timestamp = get_block_timestamp(); if !n_checkpoints.is_zero() { let mut checkpoint = get!(world, (delegatee, n_checkpoints - 1), Checkpoints) .checkpoint; - if checkpoint.from_block == block_number { + if checkpoint.from_block == timestamp { checkpoint.votes = new_votes; set!( world, Checkpoints { account: delegatee, index: n_checkpoints - 1, checkpoint } ); } } else { - let mut checkpoint = Checkpoint { from_block: block_number, votes: new_votes }; + let mut checkpoint = Checkpoint { from_block: timestamp, votes: new_votes }; set!(world, Checkpoints { account: delegatee, index: n_checkpoints, checkpoint }); + set!(world, NumCheckpoints { account: delegatee, count: n_checkpoints + 1 }); } emit!( world, diff --git a/governance/src/systems/token/interface.cairo b/governance/src/systems/token/interface.cairo index ec268942..3145f5a7 100644 --- a/governance/src/systems/token/interface.cairo +++ b/governance/src/systems/token/interface.cairo @@ -14,5 +14,5 @@ trait IGovernanceToken { fn transfer_from(from: ContractAddress, to: ContractAddress, amount: u128); fn delegate(delegatee: ContractAddress); fn get_current_votes(account: ContractAddress) -> u128; - fn get_prior_votes(account: ContractAddress, block_number: u64) -> u128; + fn get_prior_votes(account: ContractAddress, timestamp: u64) -> u128; } diff --git a/governance/src/systems/token/tests.cairo b/governance/src/systems/token/tests.cairo index 8b61d709..e3f3ac5f 100644 --- a/governance/src/systems/token/tests.cairo +++ b/governance/src/systems/token/tests.cairo @@ -4,7 +4,7 @@ use governance::models::token::{ Metadata, TotalSupply, Allowances, Balances, Delegates, Checkpoints, NumCheckpoints }; use governance::systems::token::interface::IGovernanceTokenDispatcherTrait; -use starknet::testing::set_contract_address; +use starknet::testing::{set_contract_address, set_block_timestamp}; #[test] fn test_initialize_token() { @@ -126,41 +126,49 @@ fn test_delegate_token() { assert!(delegatee == delegate, "Delegatee is incorrect"); } -// #[test] -// fn test_change_delegate_token() { -// let (systems, world) = testing::setup(); +#[test] +fn test_change_delegate_token() { + let (systems, world) = testing::setup(); -// let delegator = testing::ACCOUNT_1(); -// let delegate_before = testing::ACCOUNT_2(); -// let delegate_after = testing::ACCOUNT_3(); + let delegator = testing::ACCOUNT_1(); + let delegate_before = testing::ACCOUNT_2(); + let delegate_after = testing::ACCOUNT_3(); -// set_contract_address(testing::GOVERNOR()); -// systems.token.transfer(delegator, 100 * testing::E18); + set_contract_address(testing::GOVERNOR()); + systems.token.transfer(delegator, 100 * testing::E18); -// set_contract_address(delegator); -// systems.token.delegate(delegate_before); -// systems.token.delegate(delegate_after); + set_contract_address(delegator); + systems.token.delegate(delegate_before); + systems.token.delegate(delegate_after); -// let delegatee = get!(world, testing::ACCOUNT_1(), Delegates).delegatee; -// assert!(delegatee == delegate_after, "Delegatee is incorrect"); -// } + let delegatee = get!(world, testing::ACCOUNT_1(), Delegates).address; + assert!(delegatee == delegate_after, "Delegatee is incorrect"); +} #[test] fn test_get_current_votes() { - let (systems, world) = testing::setup(); + let (systems, _) = testing::setup(); + + set_contract_address(testing::GOVERNOR()); + systems.token.delegate(testing::GOVERNOR()); + let votes = systems.token.get_current_votes(testing::GOVERNOR()); + assert!(votes == testing::INITIAL_SUPPLY, "Current votes is incorrect"); +} + +#[test] +fn test_get_prior_votes() { + let (systems, _) = testing::setup(); - let delegate = testing::ACCOUNT_2(); + let delegatee = testing::ACCOUNT_1(); set_contract_address(testing::GOVERNOR()); - systems.token.transfer(testing::ACCOUNT_1(), 100 * testing::E18); - let balance = get!(world, testing::ACCOUNT_1(), Balances).amount; - println!("Balance: {}", balance); -// set_contract_address(testing::ACCOUNT_1()); -// systems.token.delegate(testing::ACCOUNT_1()); -// let current_votes = systems.token.get_current_votes(testing::ACCOUNT_1()); -// println!("Current votes before delegation: {}", current_votes); -// systems.token.delegate(delegate); -// let current_votes = systems.token.get_current_votes(testing::ACCOUNT_1()); -// println!("Current votes after: {}", current_votes); -// assert!(current_votes == 100 * testing::E18, "Current votes is incorrect"); + set_block_timestamp('ts1'); + systems.token.delegate(delegatee); + + let prior_votes = systems.token.get_prior_votes(testing::ACCOUNT_1(), 0); + assert!(prior_votes == 0, "Prior votes is incorrect"); + set_block_timestamp('ts2'); + + let prior_votes_at_ts1 = systems.token.get_prior_votes(testing::ACCOUNT_1(), 'ts1'); + assert!(prior_votes_at_ts1 == testing::INITIAL_SUPPLY, "Prior votes at ts1 is incorrect"); } From 6e83810b8b2fb44431b119278ebfd0e8cdf8f130 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sat, 27 Apr 2024 11:22:48 +0800 Subject: [PATCH 31/35] partial tests + fixes for governor --- governance/src/lib.cairo | 2 + governance/src/models/governor.cairo | 2 +- .../src/systems/governor/contract.cairo | 33 ++-- .../src/systems/governor/interface.cairo | 2 +- governance/src/systems/governor/tests.cairo | 152 ++++++++++++++++++ governance/src/utils/mock_contract.cairo | 34 ++++ .../src/utils/mock_contract_upgraded.cairo | 43 +++++ governance/src/utils/testing.cairo | 19 ++- 8 files changed, 267 insertions(+), 20 deletions(-) create mode 100644 governance/src/utils/mock_contract.cairo create mode 100644 governance/src/utils/mock_contract_upgraded.cairo diff --git a/governance/src/lib.cairo b/governance/src/lib.cairo index 1811d3ac..3ce18ab7 100644 --- a/governance/src/lib.cairo +++ b/governance/src/lib.cairo @@ -33,5 +33,7 @@ mod systems { mod utils { #[cfg(test)] mod testing; + mod mock_contract; + mod mock_contract_upgraded; } diff --git a/governance/src/models/governor.cairo b/governance/src/models/governor.cairo index 73d1613a..14ecfbea 100644 --- a/governance/src/models/governor.cairo +++ b/governance/src/models/governor.cairo @@ -50,7 +50,7 @@ struct LatestProposalIds { id: usize, } -#[derive(Copy, Drop, Default, Introspect, Serde)] +#[derive(Copy, Debug, Drop, Default, Introspect, Serde)] struct Proposal { id: usize, proposer: ContractAddress, diff --git a/governance/src/systems/governor/contract.cairo b/governance/src/systems/governor/contract.cairo index e5086b6f..13243777 100644 --- a/governance/src/systems/governor/contract.cairo +++ b/governance/src/systems/governor/contract.cairo @@ -14,7 +14,8 @@ mod governor { token::interface::{IGovernanceTokenDispatcher, IGovernanceTokenDispatcherTrait} }; use starknet::{ - ContractAddress, ClassHash, get_contract_address, get_caller_address, info::get_block_number + ContractAddress, ClassHash, get_contract_address, get_caller_address, + info::get_block_timestamp }; #[abi(embed_v0)] @@ -39,11 +40,13 @@ mod governor { quorum_votes: u128, threshold: u128, voting_delay: u64, voting_period: u64, ) { let world = self.world_dispatcher.read(); - let guardian = get!(world, get_contract_address(), GovernorParams).guardian; + let params = get!(world, get_contract_address(), GovernorParams); assert!( - guardian == get_contract_address(), + params.guardian == get_caller_address(), "Governor::set_proposal_params: only guardian can set proposal params" ); + ITimelockDispatcher { contract_address: params.timelock } + .initialize(get_contract_address(), voting_delay); set!( world, ProposalParams { @@ -56,18 +59,16 @@ mod governor { ); } - fn propose( - target: ContractAddress, class_hash: ClassHash, description: ByteArray - ) -> usize { + fn propose(target: ContractAddress, class_hash: ClassHash,) -> usize { let world = self.world_dispatcher.read(); let contract = get_contract_address(); let caller = get_caller_address(); let params = get!(world, contract, ProposalParams); - let curr_block = get_block_number(); + let time_now = get_block_timestamp(); let gov_token = IGovernanceTokenDispatcher { contract_address: get!(world, contract, GovernorParams).gov_token }; - let prior_votes = gov_token.get_prior_votes(caller, curr_block - 1); + let prior_votes = gov_token.get_prior_votes(caller, time_now - 1); assert!( prior_votes > params.threshold, "Governor::propose: proposer votes below proposal threshold" @@ -91,7 +92,7 @@ mod governor { } } - let start_block = curr_block + params.voting_delay; + let start_block = time_now + params.voting_delay; let end_block = start_block + params.voting_period; let curr_proposal_count = get!(world, contract, ProposalCount).count; set!(world, ProposalCount { contract, count: curr_proposal_count + 1 }); @@ -99,12 +100,14 @@ mod governor { let mut new_proposal: Proposal = Default::default(); new_proposal.id = proposal_id; + new_proposal.class_hash = class_hash; new_proposal.proposer = caller; new_proposal.target = target; new_proposal.start_block = start_block; new_proposal.end_block = end_block; set!(world, LatestProposalIds { address: caller, id: proposal_id }); + set!(world, Proposals { id: proposal_id, proposal: new_proposal }); emit!( world, @@ -125,9 +128,10 @@ mod governor { let mut proposal = get!(world, proposal_id, Proposals).proposal; let timelock_addr = get!(world, get_contract_address(), GovernorParams).timelock; let timelock_delay = get!(world, timelock_addr, TimelockParams).delay; - let eta = get_block_number() + timelock_delay; - queue_or_revert(world, proposal.target, proposal.class_hash, proposal.eta); + let eta = get_block_timestamp() + timelock_delay; + queue_or_revert(world, proposal.target, proposal.class_hash, eta); proposal.eta = eta; + set!(world, Proposals { id: proposal_id, proposal }); emit!(world, governorevents::ProposalQueued { id: proposal_id, eta }); } @@ -146,6 +150,7 @@ mod governor { contract_address: get!(world, get_contract_address(), GovernorParams).timelock }; timelock.execute_transaction(proposal.target, proposal.class_hash, proposal.eta); + set!(world, Proposals { id: proposal_id, proposal }); emit!(world, governorevents::ProposalExecuted { id: proposal_id, executed: true }); } @@ -168,7 +173,8 @@ mod governor { let gov_token = IGovernanceTokenDispatcher { contract_address: get!(world, contract, GovernorParams).gov_token }; - let prior_votes = gov_token.get_prior_votes(proposal.proposer, get_block_number() - 1); + let prior_votes = gov_token + .get_prior_votes(proposal.proposer, get_block_timestamp() - 1); assert!( guardian == get_caller_address() || prior_votes < threshold, "Governor::cancel: proposer above threshold" @@ -201,7 +207,7 @@ mod governor { let proposal = get!(world, proposal_id, Proposals).proposal; let quorum_votes = get!(world, contract, ProposalParams).quorum_votes; - let block_number = get_block_number(); + let block_number = get_block_timestamp(); if proposal.canceled { return ProposalState::Canceled(()); @@ -245,6 +251,7 @@ mod governor { Support::Against => { proposal.against_votes += votes; }, Support::Abstain => { proposal.abstain_votes += votes; } } + set!(world, Proposals { id: proposal_id, proposal }); receipt.has_voted = true; receipt.support = support; diff --git a/governance/src/systems/governor/interface.cairo b/governance/src/systems/governor/interface.cairo index ebbbacff..2e72df65 100644 --- a/governance/src/systems/governor/interface.cairo +++ b/governance/src/systems/governor/interface.cairo @@ -7,7 +7,7 @@ trait IGovernor { fn set_proposal_params( quorum_votes: u128, threshold: u128, voting_delay: u64, voting_period: u64, ); - fn propose(target: ContractAddress, class_hash: ClassHash, description: ByteArray) -> usize; + fn propose(target: ContractAddress, class_hash: ClassHash) -> usize; fn queue(proposal_id: usize); fn execute(proposal_id: usize); fn cancel(proposal_id: usize); diff --git a/governance/src/systems/governor/tests.cairo b/governance/src/systems/governor/tests.cairo index 8b137891..00956151 100644 --- a/governance/src/systems/governor/tests.cairo +++ b/governance/src/systems/governor/tests.cairo @@ -1 +1,153 @@ +use dojo::world::IWorldDispatcherTrait; +use governance::utils::{ + mock_contract_upgraded::{ + hellostarknetupgraded, IHelloStarknetUgradedDispatcher, IHelloStarknetUgradedDispatcherTrait + }, + mock_contract::{IHelloStarknetDispatcherTrait}, testing +}; +use governance::models::governor::{ProposalParams, Proposals, ProposalCount, Support}; +use governance::systems::governor::interface::IGovernorDispatcherTrait; +use governance::systems::token::interface::IGovernanceTokenDispatcherTrait; +use governance::systems::timelock::interface::ITimelockDispatcherTrait; +use starknet::testing::{set_contract_address, set_block_timestamp}; +const QUORUM: u128 = 5; +const THRESHOLD: u128 = 10; +const DELAY: u64 = 172_801; // 2 days;; +const PERIOD: u64 = 20; + +#[test] +fn test_set_proposal_params() { + let (systems, world) = testing::setup(); + + set_contract_address(testing::GOVERNOR()); + systems.governor.set_proposal_params(QUORUM, THRESHOLD, DELAY, PERIOD); + + let new_proposal_params = get!(world, systems.governor.contract_address, ProposalParams); + assert!(new_proposal_params.quorum_votes == QUORUM); + assert!(new_proposal_params.threshold == THRESHOLD); + assert!(new_proposal_params.voting_delay == DELAY); + assert!(new_proposal_params.voting_period == PERIOD); +} + +#[test] +fn test_propose() { + let (systems, world) = testing::setup(); + + set_contract_address(testing::GOVERNOR()); + systems.governor.set_proposal_params(QUORUM, THRESHOLD, DELAY, PERIOD); + systems.token.delegate(testing::ACCOUNT_1()); + + let proposal_target: starknet::ContractAddress = 'target'.try_into().unwrap(); + let proposal_class_hash: starknet::ClassHash = 'new_class_hash'.try_into().unwrap(); + + set_contract_address(testing::ACCOUNT_1()); + + set_block_timestamp('ts1'); + systems.governor.propose(proposal_target, proposal_class_hash); + + assert!(get!(world, systems.governor.contract_address, ProposalCount).count == 1); + let proposal = get!(world, 1, Proposals).proposal; + assert!(proposal.proposer == testing::ACCOUNT_1(), "proposer is not correct"); + assert!(proposal.target == proposal_target, "target is not correct"); + assert!(proposal.class_hash == proposal_class_hash, "class_hash is not correct"); + assert!(proposal.start_block == 'ts1' + DELAY, "start_block is not correct"); + assert!(proposal.end_block == 'ts1' + DELAY + PERIOD, "end_block is not correct"); +} + +#[test] +fn test_cast_vote() { + let (systems, world) = testing::setup(); + let proposal_target: starknet::ContractAddress = 'target'.try_into().unwrap(); + let proposal_class_hash: starknet::ClassHash = 'new_class_hash'.try_into().unwrap(); + + set_contract_address(testing::GOVERNOR()); + systems.governor.set_proposal_params(QUORUM, THRESHOLD, DELAY, PERIOD); + + systems.token.transfer(testing::ACCOUNT_1(), 200); + systems.token.transfer(testing::ACCOUNT_2(), 100); + + set_contract_address(testing::ACCOUNT_2()); + systems.token.delegate(testing::ACCOUNT_2()); + set_contract_address(testing::ACCOUNT_1()); + systems.token.delegate(testing::ACCOUNT_1()); + set_block_timestamp('ts1'); + systems.governor.propose(proposal_target, proposal_class_hash); + set_block_timestamp('ts1' + DELAY + 1); + systems.governor.cast_vote(1, Support::For); + + set_contract_address(testing::ACCOUNT_2()); + systems.governor.cast_vote(1, Support::For); + + let proposal = get!(world, 1, Proposals).proposal; + println!("proposal: {:?}", proposal); + let one_voting_power = systems.token.get_current_votes(testing::ACCOUNT_1()); + let two_voting_power = systems.token.get_current_votes(testing::ACCOUNT_2()); + assert!(proposal.for_votes == one_voting_power + two_voting_power, "for_votes is not correct"); + assert!(proposal.against_votes == 0, "against_votes is not correct"); + assert!(proposal.abstain_votes == 0, "abstain_votes is not correct"); +} + +#[test] +fn test_queue_proposal() { + let (systems, world) = testing::setup(); + let proposal_target: starknet::ContractAddress = 'target'.try_into().unwrap(); + let proposal_class_hash: starknet::ClassHash = 'new_class_hash'.try_into().unwrap(); + + set_contract_address(testing::GOVERNOR()); + systems.governor.set_proposal_params(QUORUM, THRESHOLD, DELAY, PERIOD); + + systems.token.transfer(testing::ACCOUNT_1(), 200); + systems.token.transfer(testing::ACCOUNT_2(), 100); + + set_contract_address(testing::ACCOUNT_2()); + systems.token.delegate(testing::ACCOUNT_2()); + set_contract_address(testing::ACCOUNT_1()); + systems.token.delegate(testing::ACCOUNT_1()); + set_block_timestamp('ts1'); + systems.governor.propose(proposal_target, proposal_class_hash); + set_block_timestamp('ts1' + DELAY + 1); + systems.governor.cast_vote(1, Support::For); + set_contract_address(testing::ACCOUNT_2()); + systems.governor.cast_vote(1, Support::For); + set_block_timestamp('ts1' + DELAY + PERIOD + 1); + systems.governor.queue(1); + + let proposal = get!(world, 1, Proposals).proposal; + assert!(proposal.eta == 'ts1' + DELAY * 2 + PERIOD + 1, "eta is not correct"); +} + +#[test] +fn test_execute_proposal() { + let (systems, world) = testing::setup(); + systems.mock.increase_balance(1000); + let proposal_class_hash = hellostarknetupgraded::TEST_CLASS_HASH.try_into().unwrap(); + + set_contract_address(testing::GOVERNOR()); + systems.governor.set_proposal_params(QUORUM, THRESHOLD, DELAY, PERIOD); + + systems.token.transfer(testing::ACCOUNT_1(), 200); + systems.token.transfer(testing::ACCOUNT_2(), 100); + + set_contract_address(testing::ACCOUNT_2()); + systems.token.delegate(testing::ACCOUNT_2()); + set_contract_address(testing::ACCOUNT_1()); + systems.token.delegate(testing::ACCOUNT_1()); + set_block_timestamp('ts1'); + systems.governor.propose(systems.mock.contract_address, proposal_class_hash); + set_block_timestamp('ts1' + DELAY + 1); + systems.governor.cast_vote(1, Support::For); + set_contract_address(testing::ACCOUNT_2()); + systems.governor.cast_vote(1, Support::For); + set_block_timestamp('ts1' + DELAY + PERIOD + 1); + systems.governor.queue(1); + + set_block_timestamp('ts1' + DELAY * 2 + PERIOD + 1); + systems.governor.execute(1); + + let proposal = get!(world, 1, Proposals).proposal; + assert!(proposal.executed == true, "executed is not correct"); + + IHelloStarknetUgradedDispatcher { contract_address: systems.mock.contract_address } + .decrease_balance(1000); +} diff --git a/governance/src/utils/mock_contract.cairo b/governance/src/utils/mock_contract.cairo new file mode 100644 index 00000000..41b0ed23 --- /dev/null +++ b/governance/src/utils/mock_contract.cairo @@ -0,0 +1,34 @@ +use starknet::ContractAddress; + +#[dojo::interface] +trait IHelloStarknet { + fn increase_balance(amount: u128); + fn get_balance() -> u128; +} + +#[derive(Model, Copy, Drop, Serde)] +struct MockBalances { + #[key] + account: u128, + balance: u128, +} +#[dojo::contract] +mod hellostarknet { + use super::{MockBalances, IHelloStarknet}; + + #[abi(embed_v0)] + impl HelloStarknetImpl of IHelloStarknet { + // Increases the balance by the given amount. + fn increase_balance(amount: u128) { + let world = self.world_dispatcher.read(); + let curr_balance = get!(world, 1, MockBalances).balance; + set!(world, MockBalances { account: 1, balance: curr_balance + amount }); + } + + // Gets the balance. + fn get_balance() -> u128 { + let world = self.world_dispatcher.read(); + get!(world, 1, MockBalances).balance + } + } +} diff --git a/governance/src/utils/mock_contract_upgraded.cairo b/governance/src/utils/mock_contract_upgraded.cairo new file mode 100644 index 00000000..e83bdacb --- /dev/null +++ b/governance/src/utils/mock_contract_upgraded.cairo @@ -0,0 +1,43 @@ +use starknet::ContractAddress; + +#[dojo::interface] +trait IHelloStarknetUgraded { + fn increase_balance(amount: u128); + fn decrease_balance(amount: u128); + fn get_balance() -> u128; +} + +#[derive(Model, Copy, Drop, Serde)] +struct MockBalances { + #[key] + account: u128, + balance: u128, +} + +#[dojo::contract] +mod hellostarknetupgraded { + use super::{MockBalances, IHelloStarknetUgraded}; + + #[abi(embed_v0)] + impl HelloStarknetImpl of IHelloStarknetUgraded { + // Increases the balance by the given amount. + fn increase_balance(amount: u128) { + let world = self.world_dispatcher.read(); + let curr_balance = get!(world, 1, MockBalances).balance; + set!(world, MockBalances { account: 1, balance: curr_balance + amount }); + } + + // Decreases the balance by the given amount. + fn decrease_balance(amount: u128) { + let world = self.world_dispatcher.read(); + let curr_balance = get!(world, 1, MockBalances).balance; + set!(world, MockBalances { account: 1, balance: curr_balance - amount }); + } + + // Gets the balance. + fn get_balance() -> u128 { + let world = self.world_dispatcher.read(); + get!(world, 1, MockBalances).balance + } + } +} diff --git a/governance/src/utils/testing.cairo b/governance/src/utils/testing.cairo index 3282f6ca..9bddd223 100644 --- a/governance/src/utils/testing.cairo +++ b/governance/src/utils/testing.cairo @@ -23,6 +23,9 @@ use governance::systems::{ interface::{IGovernanceTokenDispatcher, IGovernanceTokenDispatcherTrait} } }; +use governance::utils::mock_contract::{ + hellostarknet, IHelloStarknetDispatcher, mock_balances, MockBalances +}; use starknet::{ContractAddress, contract_address_const}; const DAY: u64 = 86400; @@ -52,7 +55,8 @@ fn ACCOUNT_5() -> ContractAddress { struct Systems { governor: IGovernorDispatcher, timelock: ITimelockDispatcher, - token: IGovernanceTokenDispatcher + token: IGovernanceTokenDispatcher, + mock: IHelloStarknetDispatcher, } fn setup() -> (Systems, IWorldDispatcher) { @@ -71,12 +75,13 @@ fn setup() -> (Systems, IWorldDispatcher) { delegates::TEST_CLASS_HASH, checkpoints::TEST_CLASS_HASH, num_checkpoints::TEST_CLASS_HASH, - nonces::TEST_CLASS_HASH + nonces::TEST_CLASS_HASH, + mock_balances::TEST_CLASS_HASH ]; let world = spawn_test_world(models); let contract_address = world.deploy_contract(1, governor::TEST_CLASS_HASH.try_into().unwrap()); - let governor_contract = IGovernorDispatcher { contract_address }; + let governor = IGovernorDispatcher { contract_address }; let contract_address = world.deploy_contract(2, timelock::TEST_CLASS_HASH.try_into().unwrap()); let timelock = ITimelockDispatcher { contract_address }; @@ -85,11 +90,15 @@ fn setup() -> (Systems, IWorldDispatcher) { .deploy_contract(3, governancetoken::TEST_CLASS_HASH.try_into().unwrap()); let token = IGovernanceTokenDispatcher { contract_address }; - let systems = Systems { governor: governor_contract, timelock, token }; + let contract_address = world + .deploy_contract(4, hellostarknet::TEST_CLASS_HASH.try_into().unwrap()); + let mock = IHelloStarknetDispatcher { contract_address }; + + let systems = Systems { governor, timelock, token, mock }; systems.governor.initialize(timelock.contract_address, token.contract_address, GOVERNOR()); systems.token.initialize('Gov Token', 'GOV', 18, INITIAL_SUPPLY, GOVERNOR()); - systems.timelock.initialize(systems.governor.contract_address, DAY * 2); + // systems.timelock.initialize(systems.governor.contract_address, DAY * 2); (systems, world) } From 0a785b8325afd78208e8a52c93a69d7d1a3f919d Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sat, 27 Apr 2024 13:41:06 +0800 Subject: [PATCH 32/35] added readme + examples --- governance/README.md | 307 +++++++++++++++++++++++++++++++++++++ governance/src/EXAMPLES.md | 106 +++++++++++++ 2 files changed, 413 insertions(+) create mode 100644 governance/README.md create mode 100644 governance/src/EXAMPLES.md diff --git a/governance/README.md b/governance/README.md new file mode 100644 index 00000000..2ea5811a --- /dev/null +++ b/governance/README.md @@ -0,0 +1,307 @@ +# Governance Token + +A governance token implementation. It provides functionality for token management, delegation, and voting. + +## Contract State + +The contract state is managed using the following data structures: + +- `Metadata`: Stores the token metadata, including name, symbol, and decimals. +- `TotalSupply`: Keeps track of the total supply of the token. +- `Balances`: Maintains the token balance of each account. +- `Delegates`: Stores the delegate information for each account. +- `NumCheckpoints`: Keeps track of the number of checkpoints for each account. +- `Checkpoints`: Stores the checkpoint data for each account and index. + +## Contract Functions + +### `initialize` + +```rust +fn initialize( + name: felt252, + symbol: felt252, + decimals: u8, + initial_supply: u128, + recipient: ContractAddress +) +``` + +This function initializes the governance token contract with the provided parameters. It sets the token metadata, total supply, and assigns the initial supply to the specified recipient address. It can only be called once during contract deployment. + +```rust +fn approve(spender: ContractAddress, amount: u128) +``` + +This function allows the caller to approve a spender to spend a specified amount of tokens on their behalf. It updates the allowance for the spender and emits an `Approval` event. + +```rust +fn transfer(to: ContractAddress, amount: u128) +``` + +This function transfers a specified amount of tokens from the caller's account to the recipient's account. It updates the balances and emits a `Transfer` event. + + +```rust +fn transfer_from(from: ContractAddress, to: ContractAddress, amount: u128) +``` + +This function allows a spender to transfer tokens from one account to another, provided that the spender has sufficient allowance. It updates the balances, allowances, and emits a `Transfer` event. + +```rust +fn delegate(delegatee: ContractAddress) +``` + +This function allows the caller to delegate their voting power to another account. It updates the delegate information and moves the delegated votes accordingly. + + +```rust +fn get_current_votes(account: ContractAddress) -> u128 +``` + +This function retrieves the current number of votes for a given account. It looks up the most recent checkpoint for the account and returns the corresponding vote count. + +```rust +fn get_prior_votes(account: ContractAddress, timestamp: u64) -> u128 +``` + +This function retrieves the number of votes for a given account at a specific timestamp. It performs a binary search on the account's checkpoints to find the checkpoint immediately preceding the given timestamp and returns the corresponding vote count. + +## Internal Functions + +The contract also includes several internal functions that are used to implement the core functionality: + +- `delegate`: Handles the delegation of votes from one account to another. +- `transfer_tokens`: Performs the actual token transfer between accounts. +- `move_delegates`: Updates the vote counts when tokens are transferred or delegated. +- `write_checkpoint`: Writes a new checkpoint for an account's vote count. + +## Events + +The contract emits the following events: + +- `Transfer`: Emitted when tokens are transferred between accounts. +- `Approval`: Emitted when an account approves another account to spend tokens on their behalf. +- `DelegateChanged`: Emitted when an account changes their delegate. +- `DelegateVotesChanged`: Emitted when the vote count for a delegate changes. + +Please note that this documentation provides a high-level overview of the governance token smart contract. For more detailed information, refer to the contract code and the associated libraries and models used in the implementation. + +# Timelock + +The Timelock is designed to provide a secure way to manage and execute transactions with a time delay. It allows an admin to queue transactions, which can be executed only after a specified time period has passed. This contract is based on the Compound's Timelock contract and is implemented using the Dojo framework. + +## Constants + +The contract defines the following constants: + +- `GRACE_PERIOD`: The grace period (in seconds) after the time lock has passed during which a transaction can be executed. Set to 14 days (1,209,600 seconds). +- `MINIMUM_DELAY`: The minimum delay (in seconds) required before a transaction can be executed. Set to 2 days (172,800 seconds). +- `MAXIMUM_DELAY`: The maximum delay (in seconds) allowed for a transaction. Set to 30 days (2,592,000 seconds). + +## Functions + +```rust +fn initialize(admin: ContractAddress, delay: u64) +``` + +This function initializes the Timelock contract with the specified admin address and delay. It can only be called once during the contract's lifetime. + +- `admin`: The address of the admin who will have the authority to queue and execute transactions. +- `delay`: The delay (in seconds) required before a queued transaction can be executed. Must be within the `MINIMUM_DELAY` and `MAXIMUM_DELAY` range. + +```rust +fn execute_transaction(world: IWorldDispatcher, target: ContractAddress, new_implementation: ClassHash, eta: u64) +``` + +This function executes a previously queued transaction. + +- `world`: The world dispatcher used to interact with the contract state. +- `target`: The address of the contract to be upgraded. +- `new_implementation`: The class hash of the new implementation for the target contract. +- `eta`: The estimated execution time (timestamp) of the transaction. + +The function checks that the caller is the admin, the transaction has been queued, the current timestamp is within the allowed execution window (between `eta` and `eta + GRACE_PERIOD`), and then executes the transaction by upgrading the target contract with the new implementation. + +```rust +fn que_transaction(world: IWorldDispatcher, target: ContractAddress, new_implementation: ClassHash, eta: u64) +``` + +This function queues a transaction for future execution. + +- `world`: The world dispatcher used to interact with the contract state. +- `target`: The address of the contract to be upgraded. +- `new_implementation`: The class hash of the new implementation for the target contract. +- `eta`: The estimated execution time (timestamp) of the transaction. Must be at least `delay` seconds in the future. + +The function checks that the caller is the admin and the `eta` satisfies the required delay, then marks the transaction as queued. + +```rust +fn cancel_transaction(world: IWorldDispatcher, target: ContractAddress, new_implementation: ClassHash, eta: u64) +``` + +This function cancels a previously queued transaction. + +- `world`: The world dispatcher used to interact with the contract state. +- `target`: The address of the contract associated with the transaction to be canceled. +- `new_implementation`: The class hash of the new implementation associated with the transaction to be canceled. +- `eta`: The estimated execution time (timestamp) of the transaction to be canceled. + +The function checks that the caller is the admin and then marks the transaction as not queued, effectively canceling it. + +## Events + +The contract emits the following events: + +- `NewAdmin`: Emitted when a new admin is set during contract initialization. +- `NewDelay`: Emitted when a new delay is set during contract initialization. +- `ExecuteTransaction`: Emitted when a transaction is executed. +- `QueueTransaction`: Emitted when a transaction is queued. +- `CancelTransaction`: Emitted when a transaction is canceled. + +These events provide transparency and allow off-chain monitoring of the contract's activity. + +# Governor + +The Governor is responsible for managing the governance process of a decentralized system. + +## Contract Structure + +The Governor contract is implemented in the `governor` module and consists of the following main components: + +1. `GovernorImpl`: The main implementation of the Governor contract, which defines the core functionality. +2. `governorevents`: A library that defines the events emitted by the Governor contract. +3. `models`: A module that defines the data structures used by the Governor contract, such as `GovernorParams`, `ProposalParams`, `Proposal`, and `Receipt`. +4. `systems`: A module that defines the interfaces and contracts used by the Governor contract, such as `IGovernor`, `ITimelockDispatcher`, and `IGovernanceTokenDispatcher`. + +## Contract Functions + +### `initialize` + +```rust +fn initialize( + timelock: ContractAddress, gov_token: ContractAddress, guardian: ContractAddress +) +``` + +- `timelock`: The address of the Timelock contract. +- `gov_token`: The address of the Governance Token contract. +- `guardian`: The address of the guardian account. + +The `initialize` function is used to set the initial parameters of the Governor contract. It can only be called once. + +### `set_proposal_params` + +```rust +fn set_proposal_params( + quorum_votes: u128, threshold: u128, voting_delay: u64, voting_period: u64, +) +``` + +- `quorum_votes`: The minimum number of votes required for a proposal to reach quorum. +- `threshold`: The minimum number of votes required for a proposer to create a proposal. +- `voting_delay`: The delay (in blocks) between the proposal's creation and the start of the voting period. +- `voting_period`: The duration (in blocks) of the voting period. + +The `set_proposal_params` function allows the guardian to set the parameters for creating and voting on proposals. + +### `propose` + +```rust +fn propose(target: ContractAddress, class_hash: ClassHash) -> usize +``` + +- `target`: The address of the contract to be called by the proposal. +- `class_hash`: The class hash of the contract to be called by the proposal. + +The `propose` function is used to create a new proposal. It returns the `proposal_id` of the created proposal. + +### `queue` + +```rust +fn queue(proposal_id: usize) +``` + +- `proposal_id`: The ID of the proposal to be queued for execution. + +The `queue` function is used to queue a succeeded proposal for execution. + +### `execute` + +```rust +fn execute(proposal_id: usize) +``` + +- `proposal_id`: The ID of the proposal to be executed. + +The `execute` function is used to execute a queued proposal. + +### `cancel` + +```rust +fn cancel(proposal_id: usize) +``` + +- `proposal_id`: The ID of the proposal to be canceled. + +The `cancel` function is used to cancel a proposal. It can only be called by the guardian or the proposer (if their votes are below the threshold). + +### `get_action` + +```rust +fn get_action(proposal_id: usize) -> (ContractAddress, ClassHash) +``` + +- `proposal_id`: The ID of the proposal. + +The `get_action` function returns the target contract address and class hash of a proposal. + +### `state` + +```rust +fn state(proposal_id: usize) -> ProposalState +``` + +- `proposal_id`: The ID of the proposal. + +The `state` function returns the current state of a proposal. + +### `cast_vote` + +```rust +fn cast_vote(proposal_id: usize, support: Support) +``` + +- `proposal_id`: The ID of the proposal to vote on. +- `support`: The user's vote, which can be either `For`, `Against`, or `Abstain`. + +The `cast_vote` function is used by users to vote on active proposals. + +## Internal Function + +### `queue_or_revert` + +```rust +fn queue_or_revert( + world: IWorldDispatcher, target: ContractAddress, class_hash: ClassHash, eta: u64 +) +``` + +- `world`: The world dispatcher used to access contract storage. +- `target`: The address of the contract to be called by the proposal. +- `class_hash`: The class hash of the contract to be called by the proposal. +- `eta`: The timestamp at which the proposal can be executed. + +The `queue_or_revert` function is an internal function used to queue a proposal for execution or revert if the proposal is already queued. + +## Events + +The Governor contract emits the following events: + +- `ProposalCreated`: Emitted when a new proposal is created. +- `ProposalQueued`: Emitted when a proposal is queued for execution. +- `ProposalExecuted`: Emitted when a proposal is executed. +- `ProposalCanceled`: Emitted when a proposal is canceled. +- `VoteCast`: Emitted when a user casts a vote on a proposal. + +These events provide transparency and allow users to track the progress of proposals and the actions taken by the Governor contract. \ No newline at end of file diff --git a/governance/src/EXAMPLES.md b/governance/src/EXAMPLES.md new file mode 100644 index 00000000..6b016713 --- /dev/null +++ b/governance/src/EXAMPLES.md @@ -0,0 +1,106 @@ +## Introduction: +To set up a governance protocol, three main systems need to be deployed: the Governance Token, the Timelock, and the Governor. + +1. Governance Token: +The Governance Token is an ERC20-compatible token that represents voting power within the governance system. It is used to determine the weight of each user's vote and to ensure that only token holders can participate in the governance process. The Governance Token contract manages the token supply, balances, and delegation of voting power. + +2. Timelock: +The Timelock contract acts as a safety mechanism to prevent immediate execution of sensitive actions. It introduces a delay between the moment a proposal is approved and when it can be executed. This delay provides an opportunity for users to exit the system if they disagree with a decision, and it helps protect against malicious proposals. The Timelock contract is controlled by the Governor and ensures that approved proposals are executed only after a specified time period has passed. + +3. Governor: +The Governor contract is the central component of the governance system. It manages the proposal lifecycle, including proposal creation, voting, queuing, and execution. Users with sufficient Governance Tokens can create proposals, which can be voted on by other token holders. The Governor contract tracks the voting process, determines the outcome based on predefined rules, and interacts with the Timelock contract to schedule and execute approved proposals. + +To set up a governance protocol, you need to deploy these three contracts in the following order: + +1. Deploy the Governance Token contract, specifying the token's name, symbol, and initial distribution. +2. Deploy the Timelock contract, providing the address of the Governor contract as the admin and setting the desired delay for executing proposals. +3. Deploy the Governor contract, specifying the addresses of the Governance Token and Timelock contracts, and setting the initial governance parameters such as quorum, threshold, voting delay, and voting period. + +Once these contracts are deployed, users can start participating in the governance process by acquiring Governance Tokens, creating proposals, voting on proposals, and executing approved proposals through the Governor contract. + +## 1. Upgrading a Contract: +```rust +// Create a new proposal to upgrade a contract +let proposal_id = governor.propose(target_contract_address, new_implementation_class_hash); + +// Users can vote on the proposal +governor.cast_vote(proposal_id, Support::For); + +// After the voting period, if the proposal succeeds, it can be queued for execution +governor.queue(proposal_id); + +// After the timelock delay, the proposal can be executed to upgrade the contract +governor.execute(proposal_id); +``` + +## 2. Changing Governance Parameters With a New Proposal: +```rust +// Assume a proposal has been created and voted on, and has succeeded +let proposal_id = 1; + +// Queue the proposal for execution +governor.queue(proposal_id); + +// Wait for the timelock delay to pass +// ... + +// Execute the proposal +governor.execute(proposal_id); +``` + +In this example, we assume that a proposal with `proposal_id` equal to 1 has been created, voted on, and has succeeded. The proposal is now ready to be executed. + +1. Queuing the Proposal: + - The `queue` function is called on the Governor contract, passing the `proposal_id` as an argument. + - This function checks if the proposal has succeeded and if it is ready to be queued for execution. + - If the checks pass, the proposal is marked as queued, and the `ProposalQueued` event is emitted. + - The proposal's execution timestamp (`eta`) is set to the current timestamp plus the timelock delay. + +2. Waiting for the Timelock Delay: + - After the proposal is queued, it enters a timelock period defined by the Timelock contract. + - During this period, the proposal cannot be executed immediately. It must wait until the specified timelock delay has passed. + - The purpose of the timelock delay is to provide a safety mechanism and allow stakeholders to review and potentially cancel the proposal if necessary. + +3. Executing the Proposal: + - Once the timelock delay has passed, the `execute` function can be called on the Governor contract, passing the `proposal_id` as an argument. + - The function checks if the proposal is in the "Queued" state and if the current timestamp is greater than or equal to the proposal's execution timestamp (`eta`). + - If the checks pass, the proposal is executed by calling the target contract with the specified function and parameters defined in the proposal. + - The `ProposalExecuted` event is emitted to indicate that the proposal has been executed. + +After the proposal is executed, the following occurs: + +1. Contract Upgrade: + - If the proposal was for upgrading a contract, the target contract's implementation is updated to the new class hash specified in the proposal. + - The contract's state and storage are preserved, but the contract's behavior is updated to reflect the new implementation. + +2. Governance Parameter Changes: + - If the proposal was for changing governance parameters (e.g., quorum votes, threshold, voting delay, voting period), the new parameter values take effect immediately after the proposal is executed. + - The Governor contract's state is updated to reflect the new parameter values. + +3. Other Actions: + - Depending on the specific proposal and its defined actions, other changes or actions may occur after the proposal is executed. + - These actions could include transferring funds, modifying contract state, or triggering external interactions with other contracts or systems. + +It's important to note that the execution of a proposal is a critical step in the governance process. It allows the approved changes to take effect and modifies the state of the system according to the proposal's specifications. The timelock delay provides an additional layer of security and allows for a final review period before the changes are irreversibly applied. + + +## 3. Canceling a Proposal: +```rust +// The guardian or the proposer (if their votes are below the threshold) can cancel a proposal +let proposal_id = 1; +governor.cancel(proposal_id); +``` + +## 4. Retrieving Proposal Information: +```rust +// Get the target contract address and class hash of a proposal +let proposal_id = 1; +let (target_contract_address, class_hash) = governor.get_action(proposal_id); + +// Get the current state of a proposal +let proposal_state = governor.state(proposal_id); +``` + +These examples demonstrate how the Governor contract can be used to manage upgrades, change governance parameters, cancel proposals, and retrieve proposal information. The contract provides a decentralized way for stakeholders to participate in the decision-making process of a system. + +Note: The code examples assume that the necessary imports and contract instances are available, and the contract addresses and class hashes are replaced with actual values. From f578c22da95a146f7ea9eb38681aee1111224f8c Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sat, 27 Apr 2024 13:43:58 +0800 Subject: [PATCH 33/35] added ci tests for governance --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a386f29..c4a66b0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,3 +95,14 @@ jobs: - name: Test run: sozo test -f hex_map shell: bash + + governance: + needs: [check, build] + runs-on: ubuntu-latest + name: Test example hex map + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - name: Test + run: sozo test -f governance + shell: bash From 79f29c338429b18c9a5ff89bbe2e88f89ccb2a92 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sat, 27 Apr 2024 13:44:03 +0800 Subject: [PATCH 34/35] fmt --- governance/src/lib.cairo | 4 ++-- governance/src/systems/governor/tests.cairo | 8 ++++---- governance/src/systems/token/tests.cairo | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/governance/src/lib.cairo b/governance/src/lib.cairo index 3ce18ab7..4392ae27 100644 --- a/governance/src/lib.cairo +++ b/governance/src/lib.cairo @@ -31,9 +31,9 @@ mod systems { } mod utils { - #[cfg(test)] - mod testing; mod mock_contract; mod mock_contract_upgraded; + #[cfg(test)] + mod testing; } diff --git a/governance/src/systems/governor/tests.cairo b/governance/src/systems/governor/tests.cairo index 00956151..37add84c 100644 --- a/governance/src/systems/governor/tests.cairo +++ b/governance/src/systems/governor/tests.cairo @@ -1,14 +1,14 @@ use dojo::world::IWorldDispatcherTrait; +use governance::models::governor::{ProposalParams, Proposals, ProposalCount, Support}; +use governance::systems::governor::interface::IGovernorDispatcherTrait; +use governance::systems::timelock::interface::ITimelockDispatcherTrait; +use governance::systems::token::interface::IGovernanceTokenDispatcherTrait; use governance::utils::{ mock_contract_upgraded::{ hellostarknetupgraded, IHelloStarknetUgradedDispatcher, IHelloStarknetUgradedDispatcherTrait }, mock_contract::{IHelloStarknetDispatcherTrait}, testing }; -use governance::models::governor::{ProposalParams, Proposals, ProposalCount, Support}; -use governance::systems::governor::interface::IGovernorDispatcherTrait; -use governance::systems::token::interface::IGovernanceTokenDispatcherTrait; -use governance::systems::timelock::interface::ITimelockDispatcherTrait; use starknet::testing::{set_contract_address, set_block_timestamp}; const QUORUM: u128 = 5; diff --git a/governance/src/systems/token/tests.cairo b/governance/src/systems/token/tests.cairo index e3f3ac5f..f9b80c4e 100644 --- a/governance/src/systems/token/tests.cairo +++ b/governance/src/systems/token/tests.cairo @@ -1,9 +1,9 @@ use dojo::world::IWorldDispatcherTrait; -use governance::utils::testing; use governance::models::token::{ Metadata, TotalSupply, Allowances, Balances, Delegates, Checkpoints, NumCheckpoints }; use governance::systems::token::interface::IGovernanceTokenDispatcherTrait; +use governance::utils::testing; use starknet::testing::{set_contract_address, set_block_timestamp}; #[test] From 84f9b24c734712f035c12ef5ea533dded66c7a18 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sat, 27 Apr 2024 13:50:00 +0800 Subject: [PATCH 35/35] moved examples to root of package --- governance/{src => }/EXAMPLES.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename governance/{src => }/EXAMPLES.md (100%) diff --git a/governance/src/EXAMPLES.md b/governance/EXAMPLES.md similarity index 100% rename from governance/src/EXAMPLES.md rename to governance/EXAMPLES.md