Skip to content

Commit

Permalink
feat: add cast vote by sig
Browse files Browse the repository at this point in the history
  • Loading branch information
ericnordelo committed Nov 14, 2024
1 parent 4936537 commit bdcdec4
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 5 deletions.
1 change: 1 addition & 0 deletions packages/governance/src/governor.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod extensions;
pub mod governor;
pub mod interface;
pub mod proposal_core;
pub mod vote;

pub use governor::{GovernorComponent, DefaultConfig};
pub use proposal_core::ProposalCore;
85 changes: 81 additions & 4 deletions packages/governance/src/governor/governor.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod GovernorComponent {
use core::pedersen::PedersenTrait;
use crate::governor::ProposalCore;
use crate::governor::interface::{ProposalState, IGovernor, IGOVERNOR_ID};
use crate::governor::vote::{Vote, VoteWithReasonAndParams};
use crate::utils::call_impls::{HashCallImpl, HashCallsImpl};
use openzeppelin_introspection::src5::SRC5Component::InternalImpl as SRC5InternalImpl;
use openzeppelin_introspection::src5::SRC5Component;
Expand All @@ -25,6 +26,7 @@ pub mod GovernorComponent {
#[storage]
pub struct Storage {
pub Governor_proposals: Map<ProposalId, ProposalCore>,
pub Governor_nonces: Map<ContractAddress, felt252>
}

#[event]
Expand Down Expand Up @@ -110,6 +112,7 @@ pub mod GovernorComponent {
pub const INSUFFICIENT_PROPOSER_VOTES: felt252 = 'Insufficient votes';
pub const UNEXPECTED_PROPOSAL_STATE: felt252 = 'Unexpected proposal state';
pub const QUEUE_NOT_IMPLEMENTED: felt252 = 'Queue not implemented';
pub const INVALID_SIGNATURE: felt252 = 'Invalid signature';
}

/// Constants expected to be defined at the contract level used to configure the component
Expand Down Expand Up @@ -595,18 +598,56 @@ pub mod GovernorComponent {
self._cast_vote(proposal_id, voter, support, reason, params)
}

/// TODO: implement vote by signature
/// Cast a vote using the `voter`'s signature.
///
/// Requirements:
///
/// - The proposal must be active.
/// - The nonce in the signed message must match the account's current nonce.
/// - `voter` must implement `SRC6::is_valid_signature`.
/// - `signature` should be valid for the message hash.
///
/// Emits a `VoteCast` event.
fn cast_vote_by_sig(
ref self: ComponentState<TContractState>,
proposal_id: felt252,
support: u8,
voter: ContractAddress,
signature: Span<felt252>
) -> u256 {
1
// 1. Get and increase current nonce
let nonce = self.use_nonce(voter);

// 2. Build hash for calling `is_valid_signature`
let verifying_contract = starknet::get_contract_address();
let vote = Vote { verifying_contract, nonce, proposal_id, support, voter };
let hash = vote.get_message_hash(voter);

let is_valid_signature_felt = ISRC6Dispatcher { contract_address: voter }
.is_valid_signature(hash, signature.into());

// 3. Check either 'VALID' or true for backwards compatibility
let is_valid_signature = is_valid_signature_felt == starknet::VALIDATED
|| is_valid_signature_felt == 1;

assert(is_valid_signature, Errors::INVALID_SIGNATURE);

// 4. Cast vote
self._cast_vote(proposal_id, voter, support, "", Immutable::DEFAULT_PARAMS())
}

/// TODO: implement vote by signature
/// Cast a vote with a `reason` and additional serialized `params` using the `voter`'s
/// signature.
///
/// Requirements:
///
/// - The proposal must be active.
/// - The nonce in the signed message must match the account's current nonce.
/// - `voter` must implement `SRC6::is_valid_signature`.
/// - `signature` should be valid for the message hash.
///
/// Emits a `VoteCast` event if no params are provided.
/// Emits a `VoteCastWithParams` event otherwise.
fn cast_vote_with_reason_and_params_by_sig(
ref self: ComponentState<TContractState>,
proposal_id: felt252,
Expand All @@ -616,7 +657,33 @@ pub mod GovernorComponent {
params: Span<felt252>,
signature: Span<felt252>
) -> u256 {
1
// 1. Get and increase current nonce
let nonce = self.use_nonce(voter);

// 2. Build hash for calling `is_valid_signature`
let verifying_contract = starknet::get_contract_address();
let reason_hash = reason.hash();
let vote = VoteWithReasonAndParams {
verifying_contract, nonce, proposal_id, support, voter, reason_hash, params
};
let hash = vote.get_message_hash(voter);

let is_valid_signature_felt = ISRC6Dispatcher { contract_address: voter }
.is_valid_signature(hash, signature.into());

// 3. Check either 'VALID' or true for backwards compatibility
let is_valid_signature = is_valid_signature_felt == starknet::VALIDATED
|| is_valid_signature_felt == 1;

assert(is_valid_signature, Errors::INVALID_SIGNATURE);

// 4. Cast vote
self._cast_vote(proposal_id, voter, support, reason, params)
}

/// Returns the next unused nonce for an address.
fn nonces(self: @ComponentState<TContractState>, voter: ContractAddress) -> felt252 {
self.Governor_nonces.read(voter)
}

/// Relays a transaction or function call to an arbitrary target.
Expand Down Expand Up @@ -781,6 +848,16 @@ pub mod GovernorComponent {
assert(found, Errors::UNEXPECTED_PROPOSAL_STATE);
}

/// Consumes a nonce, returns the current value, and increments nonce.
fn use_nonce(ref self: ComponentState<TContractState>, voter: ContractAddress) -> felt252 {
// For each account, the nonce has an initial value of 0, can only be incremented by
// one, and cannot be decremented or reset. This guarantees that the nonce never
// overflows.
let nonce = self.Governor_nonces.read(voter);
self.Governor_nonces.write(voter, nonce + 1);
nonce
}

/// Internal wrapper for `GovernorVotesTrait::get_votes`.
fn _get_votes(
self: @ComponentState<TContractState>,
Expand Down
3 changes: 3 additions & 0 deletions packages/governance/src/governor/interface.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ pub trait IGovernor<TState> {
signature: Span<felt252>
) -> u256;

/// Returns the next unused nonce for an address.
fn nonces(self: @TState, voter: ContractAddress) -> felt252;

/// Relays a transaction or function call to an arbitrary target.
///
/// In cases where the governance executor is some contract other than the governor itself, like
Expand Down
67 changes: 67 additions & 0 deletions packages/governance/src/governor/vote.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts for Cairo v0.19.0 (governance/governor/vote.cairo)

use core::hash::{HashStateTrait, HashStateExTrait};
use core::poseidon::PoseidonTrait;
use openzeppelin_utils::cryptography::snip12::StructHash;
use starknet::ContractAddress;

// sn_keccak(
// "\"Vote\"(\"verifying_contract\":\"ContractAddress\",
// \"nonce\":\"felt\",
// \"proposal_id\":\"felt\",
// \"support\":\"u8\",
// \"voter\":\"ContractAddress\")"
// )
//
// Since there's no u8 type in SNIP-12, we use u128 for `support` in the type hash generation.
pub const VOTE_TYPE_HASH: felt252 =
0x21d38a715b9e9f6da132e4d01c8e4bd956340b0407942182043d516d8e27f3f;

#[derive(Copy, Drop, Hash)]
pub struct Vote {
pub verifying_contract: ContractAddress,
pub nonce: felt252,
pub proposal_id: felt252,
pub support: u8,
pub voter: ContractAddress,
}

impl StructHashImpl of StructHash<Vote> {
fn hash_struct(self: @Vote) -> felt252 {
let hash_state = PoseidonTrait::new();
hash_state.update_with(DELEGATION_TYPE_HASH).update_with(*self).finalize()
}
}

// sn_keccak(
// "\"Vote\"(\"verifying_contract\":\"ContractAddress\",
// \"nonce\":\"felt\",
// \"proposal_id\":\"felt\",
// \"support\":\"u8\",
// \"voter\":\"ContractAddress\",
// \"reason_hash\":\"felt\",
// \"params\":\"felt*\")"
// )
//
// Since there's no u8 type in SNIP-12, we use u128 for `support` in the type hash generation.
pub const VOTE_WITH_REASON_AND_PARAMS_TYPE_HASH: felt252 =
0x3866b6236bd1166c5b7eeda1b8e6d1d8f3cd5b82bccd3dac6f8d476d4848dd4;

#[derive(Copy, Drop, Hash)]
pub struct VoteWithReasonAndParams {
pub verifying_contract: ContractAddress,
pub nonce: felt252,
pub proposal_id: felt252,
pub support: u8,
pub voter: ContractAddress,
pub reason_hash: felt252,
pub params: Span<felt252>,
}

impl StructHashImpl of StructHash<Vote> {
fn hash_struct(self: @Vote) -> felt252 {
let hash_state = PoseidonTrait::new();
hash_state.update_with(DELEGATION_TYPE_HASH).update_with(*self).finalize()
}
}
3 changes: 2 additions & 1 deletion packages/utils/src/bytearray.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ pub fn to_byte_array<T, +Into<T, felt252>, +Copy<T>>(
) -> ByteArray {
let value: felt252 = (*value).into();
let base: felt252 = base.into();
let mut byte_array = value.format_as_byte_array(base.try_into().expect('ByteArray: base cannot be 0'));
let mut byte_array = value
.format_as_byte_array(base.try_into().expect('ByteArray: base cannot be 0'));

if padding.into() > byte_array.len() {
let mut padding = padding.into() - byte_array.len();
Expand Down

0 comments on commit bdcdec4

Please sign in to comment.