Skip to content

Commit

Permalink
Merge branch 'main' into downgrade-contract
Browse files Browse the repository at this point in the history
  • Loading branch information
moodysalem committed Mar 6, 2024
2 parents 8d4907a + dabd67a commit 96617c6
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 54 deletions.
66 changes: 36 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,47 @@

[![Tests](https://github.com/EkuboProtocol/governance/actions/workflows/test.yaml/badge.svg)](https://github.com/EkuboProtocol/governance/actions/workflows/test.yaml)

Simple contracts for token governance on Starknet.
Simple contracts for governance on Starknet.

## Principles
## Components

These contracts follow the Compound governance architecture.
The contracts are not upgradeable, so the project is broken up into modular and replaceable components.
All contracts are intended to be upgraded by simply migrating to new ones.
Each component of the governance contracts in this repository may be used independently.

Even the token contract can be migrated, if necessary, by deploying a new contract that allows burning the old token to mint the new one.
### GovernanceToken

## Components
`GovernanceToken` is an ERC20 that tracks delegations as well as time-weighted average delegations for any period

- Users MAY delegate their tokens to other addresses
- The average historical delegation weight is computable over *any* historical period
- The contract has no owner and is not upgradeable.

### Airdrop

`Airdrop` can be used to distribute any fungible token, including the `GovernanceToken`. To use it, you must compute a binary merkle tree using the pedersen hash function. The root of this tree and the token address are passed as constructor arguments.

- Compute a merkle root by computing a list of amounts and recipients, hashing them, and arranging them into a merkle binary tree
- Deploy the airdrop with the root and the token address
- Transfer the total amount of tokens to the `Airdrop` contract
- The contract has no owner and is not upgradeable. Unclaimed tokens, by design, cannot be recovered.

### Timelock

- `Timelock` is an owned contract that allows a list of calls to be queued by an owner
- Anyone can execute the calls after a period of time, once queued by the owner
- Timelock is meant to own all assets, and rarely be upgraded
- In order to upgrade timelock, all assets must be transferred to a new timelock
- `Governor` manages voting on a _single call_ that can be queued into a timelock
- Designed to be the owner of Timelock
- The single call can be to `Timelock#queue(calls)`, which can execute multiple calls in a single proposal
- Timelock ownership may be transferred to a new governance contract in future, e.g. to migrate to a volition-based voting contract
- None of the proposal metadata is stored in governor, simply the number of votes
- Proposals can be canceled at any time if the voting weight of the proposer falls below the threshold
- `GovernanceToken` is an ERC20 token meant for voting in contracts like `Governor`
- Users must delegate their tokens to vote, and may delegate to themselves
- Allows other contracts to get the average voting weight for *any* historical period
- Average votes are used to compute voting weight in the `Governor`, over a configurable period of time
- `Airdrop` can be used to distribute GovernanceToken
- Compute a merkle root by computing a list of amounts and recipients, hashing them, and arranging them into a merkle binary tree
- Deploy the airdrop with the root and the token address
- Transfer the total amount of tokens to the `Airdrop` contract
- `Factory` allows creating the entire set of contracts with one call
`Timelock` allows a list of calls to be executed, after a configurable delay, by its owner

- Anyone can execute the calls after a period of time, once queued by the owner
- Designed to be the owner of all the assets held by a DAO
- Must re-configure, change ownership, or upgrade itself via a call queued to itself

### Governor

`Governor` allows `GovernanceToken` holders to vote on whether to make a _single call_
- The single call can be to `Timelock#queue(calls)`, which can execute multiple calls in a single proposal
- None of the proposal metadata is stored in governor, simply the number of votes
- Proposals can be canceled at any time if the voting weight of the proposer falls below the configurable threshold

### Factory

`Factory` allows creating the entire set of contracts with a single call.

## Testing

Expand All @@ -47,7 +57,3 @@ scarb test
## Disclaimer

These contracts are unaudited. Use at your own risk. Additional review is greatly appreciated.

## Credits

The [Airdrop](./src/airdrop.cairo) contract was heavily inspired by the [Carmine Options Airdrop contract](https://github.com/CarmineOptions/governance/blob/master/src/airdrop.cairo).
22 changes: 21 additions & 1 deletion src/airdrop_test.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,32 @@ fn test_claim_two_claims_via_claim_128() {
let root = hash_function(leaf_a, leaf_b);

let airdrop = deploy(token.contract_address, root);
token.transfer(airdrop.contract_address, 6789 + 789 + 1);
token.transfer(airdrop.contract_address, 6789 + 789);

assert_eq!(airdrop.claim_128(array![claim_a, claim_b].span(), array![].span()), 2);
assert_eq!(airdrop.claim_128(array![claim_a, claim_b].span(), array![].span()), 0);
}

#[test]
#[should_panic(expected: ('INVALID_PROOF', 'ENTRYPOINT_FAILED'))]
fn test_claim_three_claims_one_invalid_via_claim_128() {
let (_, token) = deploy_token('AIRDROP', 'AD', 1234567);

let claim_a = Claim { id: 0, claimee: contract_address_const::<2345>(), amount: 6789, };
let claim_b = Claim { id: 1, claimee: contract_address_const::<3456>(), amount: 789, };
let claim_b_2 = Claim { id: 2, claimee: contract_address_const::<3456>(), amount: 789, };

let leaf_a = hash_claim(claim_a);
let leaf_b = hash_claim(claim_b);

let root = hash_function(leaf_a, leaf_b);

let airdrop = deploy(token.contract_address, root);
token.transfer(airdrop.contract_address, 6789 + 789 + 789);

assert_eq!(airdrop.claim_128(array![claim_a, claim_b, claim_b_2].span(), array![].span()), 3);
}

fn test_claim_is_valid(root: felt252, claim: Claim, proof: Array<felt252>) {
let pspan = proof.span();
let (_, token) = deploy_token('AIRDROP', 'AD', claim.amount);
Expand Down
7 changes: 2 additions & 5 deletions src/call_trait.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,8 @@ pub impl HashCall<S, +HashStateTrait<S>, +Drop<S>, +Copy<S>> of Hash<@Call, S> {
let mut s = state.update_with((*value.to)).update_with(*value.selector);

let mut data_span: Span<felt252> = *value.calldata;
loop {
match data_span.pop_front() {
Option::Some(word) => { s = s.update(*word); },
Option::None => { break; }
};
while let Option::Some(word) = data_span.pop_front() {
s = s.update(*word);
};

s
Expand Down
1 change: 0 additions & 1 deletion src/call_trait_test.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ fn test_execute_invalid_call_data_too_short() {
call.execute();
}


#[test]
fn test_execute_valid_call_data() {
let (token, _) = deploy_token('TIMELOCK', 'TL', 1);
Expand Down
1 change: 0 additions & 1 deletion src/governance_token.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ pub mod GovernanceToken {
}
}


#[storage]
struct Storage {
name: felt252,
Expand Down
39 changes: 23 additions & 16 deletions src/timelock.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use core::result::ResultTrait;
use core::traits::TryInto;
use governance::utils::timestamps::{ThreeU64TupleStorePacking, TwoU64TupleStorePacking};
use starknet::account::{Call};
use starknet::class_hash::{ClassHash};
use starknet::contract_address::{ContractAddress};
use starknet::storage_access::{StorePacking};

Expand Down Expand Up @@ -63,8 +64,12 @@ pub trait ITimelock<TStorage> {

// Transfer ownership, i.e. the address that can queue and cancel calls. This must be self-called via #queue.
fn transfer(ref self: TStorage, to: ContractAddress);

// Configure the delay and the window for call execution. This must be self-called via #queue.
fn configure(ref self: TStorage, config: TimelockConfig);

// Replace the code at this address. This must be self-called via #queue.
fn upgrade(ref self: TStorage, class_hash: ClassHash);
}

#[derive(Copy, Drop, Serde)]
Expand All @@ -77,13 +82,14 @@ pub struct ExecutionWindow {
pub mod Timelock {
use core::hash::LegacyHash;
use core::num::traits::zero::{Zero};
use core::result::ResultTrait;
use governance::call_trait::{CallTrait, HashCall};
use starknet::{
get_caller_address, get_contract_address, SyscallResult, syscalls::call_contract_syscall,
get_block_timestamp
get_caller_address, get_contract_address, SyscallResult,
syscalls::{call_contract_syscall, replace_class_syscall}, get_block_timestamp
};
use super::{
ITimelock, ContractAddress, Call, TimelockConfig, ExecutionState,
ClassHash, ITimelock, ContractAddress, Call, TimelockConfig, ExecutionState,
TimelockConfigStorePacking, ExecutionStateStorePacking, ExecutionWindow
};

Expand Down Expand Up @@ -129,14 +135,12 @@ pub mod Timelock {
// Take a list of calls and convert it to a unique identifier for the execution
// Two lists of calls will always have the same ID if they are equivalent
// A list of calls can only be queued and executed once. To make 2 different calls, add an empty call.
pub fn to_id(mut calls: Span<Call>) -> felt252 {
let mut state = 0;
loop {
match calls.pop_front() {
Option::Some(call) => { state = LegacyHash::hash(state, call) },
Option::None => { break state; }
};
}
pub(crate) fn to_id(mut calls: Span<Call>) -> felt252 {
let mut state = selector!("ekubo::governance::Timelock");
while let Option::Some(call) = calls.pop_front() {
state = LegacyHash::hash(state, call);
};
state
}

#[generate_trait]
Expand Down Expand Up @@ -217,11 +221,8 @@ pub mod Timelock {

let mut results: Array<Span<felt252>> = ArrayTrait::new();

loop {
match calls.pop_front() {
Option::Some(call) => { results.append(call.execute()); },
Option::None => { break; }
};
while let Option::Some(call) = calls.pop_front() {
results.append(call.execute());
};

self.emit(Executed { id, });
Expand Down Expand Up @@ -263,5 +264,11 @@ pub mod Timelock {

self.config.write(config);
}

fn upgrade(ref self: ContractState, class_hash: ClassHash) {
self.check_self_call();

replace_class_syscall(class_hash).unwrap();
}
}
}

0 comments on commit 96617c6

Please sign in to comment.