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 diff --git a/Scarb.lock b/Scarb.lock index 4750cc6e..0934fddd 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 a1a3c04f..7250bd3b 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -7,6 +7,7 @@ members = [ "examples/matchmaker", "examples/projectile", "token", + "governance" ] [workspace.package] diff --git a/governance/EXAMPLES.md b/governance/EXAMPLES.md new file mode 100644 index 00000000..6b016713 --- /dev/null +++ b/governance/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. 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/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..4392ae27 --- /dev/null +++ b/governance/src/lib.cairo @@ -0,0 +1,39 @@ +mod libraries { + mod events; + mod traits; +} + +mod models { + mod governor; + mod timelock; + mod token; +} + +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 { + mod mock_contract; + mod mock_contract_upgraded; + #[cfg(test)] + mod testing; +} + diff --git a/governance/src/libraries/events.cairo b/governance/src/libraries/events.cairo new file mode 100644 index 00000000..1ffff1d6 --- /dev/null +++ b/governance/src/libraries/events.cairo @@ -0,0 +1,137 @@ +mod tokenevents { + use starknet::ContractAddress; + + #[derive(Model, Copy, Drop, Serde)] + #[dojo::event] + struct DelegateChanged { + #[key] + delegator: ContractAddress, + from: ContractAddress, + to: ContractAddress, + } + + #[derive(Model, Copy, Drop, Serde)] + #[dojo::event] + struct DelegateVotesChanged { + #[key] + delegatee: ContractAddress, + prev_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, + } +} + +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, + } +} + +mod governorevents { + use governance::models::governor::Support; + 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: Support, + 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, + } +} diff --git a/governance/src/libraries/traits.cairo b/governance/src/libraries/traits.cairo new file mode 100644 index 00000000..bfde19c0 --- /dev/null +++ b/governance/src/libraries/traits.cairo @@ -0,0 +1,16 @@ +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>() + } +} + diff --git a/governance/src/models/governor.cairo b/governance/src/models/governor.cairo new file mode 100644 index 00000000..14ecfbea --- /dev/null +++ b/governance/src/models/governor.cairo @@ -0,0 +1,94 @@ +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, Debug, 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, + abstain_votes: u128, + against_votes: u128, + canceled: bool, + executed: bool, +} + +#[derive(Copy, Default, Drop, Introspect, Serde)] +struct Receipt { + has_voted: bool, + support: Support, + votes: u128 +} + +#[derive(Copy, Drop, Serde)] +enum ProposalState { + Pending, + Active, + Canceled, + Defeated, + Succeeded, + Queued, + Expired, + Executed +} + +#[derive(Copy, Default, Drop, Introspect, Serde)] +enum Support { + For, + Against, + #[default] + Abstain, +} 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, +} + diff --git a/governance/src/models/token.cairo b/governance/src/models/token.cairo new file mode 100644 index 00000000..0cc50c6e --- /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] + delegatee: 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, + address: ContractAddress, +} + +#[derive(Model, Copy, Drop, Serde)] +struct Checkpoints { + #[key] + account: ContractAddress, + #[key] + index: u64, + checkpoint: Checkpoint, +} + +#[derive(Model, Copy, Drop, Serde)] +struct NumCheckpoints { + #[key] + account: ContractAddress, + count: u64, +} + +#[derive(Model, Copy, Drop, Serde)] +struct Nonces { + #[key] + account: ContractAddress, + nonce: usize, +} + +#[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 new file mode 100644 index 00000000..13243777 --- /dev/null +++ b/governance/src/systems/governor/contract.cairo @@ -0,0 +1,274 @@ +#[dojo::contract] +mod governor { + use governance::libraries::events::governorevents; + use governance::models::{ + governor::{ + GovernorParams, ProposalParams, ProposalCount, Proposals, Proposal, Receipt, + ProposalState, LatestProposalIds, Receipts, Support + }, + timelock::{QueuedTransactions, TimelockParams} + }; + use governance::systems::{ + 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_timestamp + }; + + #[abi(embed_v0)] + 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 params = get!(world, get_contract_address(), GovernorParams); + assert!( + 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 { + contract: get_contract_address(), + quorum_votes, + threshold, + voting_delay, + voting_period + } + ); + } + + 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 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, time_now - 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 = 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 }); + let proposal_id = curr_proposal_count + 1; + + 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, + 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_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 }); + } + + 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; + + let timelock = ITimelockDispatcher { + 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 }); + } + + 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(()) => { + panic!("Governor::cancel: cannot cancel executed proposal"); + }, + _ => {} + } + + let mut proposal = get!(world, proposal_id, Proposals).proposal; + 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_timestamp() - 1); + assert!( + guardian == get_caller_address() || prior_votes < threshold, + "Governor::cancel: proposer above threshold" + ); + + proposal.canceled = true; + + 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 }); + } + + 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_timestamp(); + + 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: Support) { + 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 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; }, + Support::Abstain => { proposal.abstain_votes += votes; } + } + set!(world, Proposals { id: proposal_id, proposal }); + + 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"); + + 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/governor/interface.cairo b/governance/src/systems/governor/interface.cairo new file mode 100644 index 00000000..2e72df65 --- /dev/null +++ b/governance/src/systems/governor/interface.cairo @@ -0,0 +1,17 @@ +use governance::models::governor::{ProposalState, Receipt, Support}; +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) -> 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: Support); +} diff --git a/governance/src/systems/governor/tests.cairo b/governance/src/systems/governor/tests.cairo new file mode 100644 index 00000000..37add84c --- /dev/null +++ b/governance/src/systems/governor/tests.cairo @@ -0,0 +1,153 @@ +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 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/systems/timelock/contract.cairo b/governance/src/systems/timelock/contract.cairo new file mode 100644 index 00000000..a62dc385 --- /dev/null +++ b/governance/src/systems/timelock/contract.cairo @@ -0,0 +1,128 @@ +#[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; + + #[abi(embed_v0)] + impl TimelockImpl of ITimelock { + fn initialize(admin: ContractAddress, delay: u64) { + assert!(!admin.is_zero(), "Timelock::initialize: Admin address cannot be zero."); + assert!( + delay >= MINIMUM_DELAY, "Timelock::initialize: Delay must exceed minimum delay." + ); + assert!( + 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::initialize: Already initialized." + ); + set!(world, TimelockParams { contract, admin, delay }); + emit!( + world, + timelockevents::NewAdmin { contract, address: admin }, + 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 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 } + ); + } + } +} diff --git a/governance/src/systems/timelock/interface.cairo b/governance/src/systems/timelock/interface.cairo new file mode 100644 index 00000000..ca345061 --- /dev/null +++ b/governance/src/systems/timelock/interface.cairo @@ -0,0 +1,9 @@ +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/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 @@ + diff --git a/governance/src/systems/token/contract.cairo b/governance/src/systems/token/contract.cairo new file mode 100644 index 00000000..c0a0f595 --- /dev/null +++ b/governance/src/systems/token/contract.cairo @@ -0,0 +1,231 @@ +#[dojo::contract] +mod governancetoken { + use governance::libraries::events::tokenevents; + use governance::models::token::{ + Allowances, Metadata, TotalSupply, Balances, Delegates, NumCheckpoints, Checkpoints, + Checkpoint + }; + use governance::systems::token::interface::IGovernanceToken; + use integer::BoundedInt; + use starknet::{ContractAddress, get_caller_address, get_contract_address, get_block_timestamp,}; + + #[abi(embed_v0)] + impl GovernanceTokenImpl of IGovernanceToken { + fn initialize( + 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, timestamp: u64) -> u128 { + let world = self.world_dispatcher.read(); + 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 > timestamp { + 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 == timestamp { + votes = cp.votes; + break; + } else if cp.from_block < timestamp { + lower = center; + } else { + upper = center - 1; + } + }; + votes + } + } + + + // 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).address; + let delegator_balance = get!(world, delegator, Balances).amount; + set!(world, Delegates { account: delegator, address: delegatee }); + emit!( + world, tokenevents::DelegateChanged { delegator, from: current_delegate, to: delegatee } + ); + move_delegates(world, current_delegate, delegatee, delegator_balance); + } + + 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 }, + Balances { account: from, amount: from_balance - amount } + ) + ); + emit!(world, tokenevents::Transfer { from, to, amount }); + 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: ContractAddress, to: ContractAddress, amount: u128 + ) { + 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!(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 !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 to_new = to_old + amount; + write_checkpoint(world, to, to_num, to_old, to_new); + } + } + } + + fn write_checkpoint( + world: IWorldDispatcher, + delegatee: ContractAddress, + n_checkpoints: u64, + old_votes: u128, + new_votes: u128 + ) { + 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 == timestamp { + checkpoint.votes = new_votes; + set!( + world, Checkpoints { account: delegatee, index: n_checkpoints - 1, checkpoint } + ); + } + } else { + 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, + tokenevents::DelegateVotesChanged { + delegatee, prev_balance: old_votes, new_balance: new_votes + } + ); + } +} diff --git a/governance/src/systems/token/interface.cairo b/governance/src/systems/token/interface.cairo new file mode 100644 index 00000000..3145f5a7 --- /dev/null +++ b/governance/src/systems/token/interface.cairo @@ -0,0 +1,18 @@ +use starknet::ContractAddress; + +#[dojo::interface] +trait IGovernanceToken { + fn initialize( + 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 get_current_votes(account: ContractAddress) -> 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 new file mode 100644 index 00000000..f9b80c4e --- /dev/null +++ b/governance/src/systems/token/tests.cairo @@ -0,0 +1,174 @@ +use dojo::world::IWorldDispatcherTrait; +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] +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).address; + assert!(delegatee == delegate_after, "Delegatee is incorrect"); +} + +#[test] +fn test_get_current_votes() { + 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 delegatee = testing::ACCOUNT_1(); + + set_contract_address(testing::GOVERNOR()); + 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"); +} 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 new file mode 100644 index 00000000..9bddd223 --- /dev/null +++ b/governance/src/utils/testing.cairo @@ -0,0 +1,108 @@ +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 governance::utils::mock_contract::{ + hellostarknet, IHelloStarknetDispatcher, mock_balances, MockBalances +}; +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, + mock: IHelloStarknetDispatcher, +} + +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, + 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 = 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 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, world) +} + +#[test] +fn test_deploy() { + let (_systems, _world) = setup(); +}