From e9ae66c838462a43a56f10d9b688a6da8e30254c Mon Sep 17 00:00:00 2001 From: Birdmannn Date: Fri, 14 Feb 2025 20:02:24 +0100 Subject: [PATCH 1/4] fix: minor refactor --- onchain/cairo/afk/src/dao/dao_aa.cairo | 34 +++++++++++-------- onchain/cairo/afk/src/interfaces/voting.cairo | 10 +++++- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/onchain/cairo/afk/src/dao/dao_aa.cairo b/onchain/cairo/afk/src/dao/dao_aa.cairo index e53b958c..98d63291 100644 --- a/onchain/cairo/afk/src/dao/dao_aa.cairo +++ b/onchain/cairo/afk/src/dao/dao_aa.cairo @@ -34,7 +34,7 @@ pub mod DaoAA { use afk::interfaces::voting::{ IVoteProposal, Proposal, ProposalParams, ProposalResult, ProposalType, UserVote, VoteState, ProposalCreated, SET_PROPOSAL_DURATION_IN_SECONDS, TOKEN_DECIMALS, ProposalVoted, - ProposalResolved, ConfigParams, ConfigResponse, ProposalCanceled, + ProposalResolved, ConfigParams, ConfigResponse, ProposalCanceled, Calldata, }; use afk::social::request::{SocialRequest, SocialRequestImpl, SocialRequestTrait, Encode}; use afk::social::transfer::Transfer; @@ -101,7 +101,7 @@ pub mod DaoAA { minimum_threshold_percentage: u64, transfers: Map, proposals: Map>, // Map ProposalID => Proposal - proposals_calldata: Map>, // Map ProposalID => calldata + proposals_calldata: Map, // Map ProposalID => calldata proposal_by_user: Map, total_proposal: u256, vote_state_by_proposal: Map, // Map ProposalID => VoteState @@ -191,9 +191,8 @@ pub mod DaoAA { // Check if ERC20 minimal balance to create a proposal is needed, if yes check the balance // Add TX Calldata for this proposal fn create_proposal( - ref self: ContractState, proposal_params: ProposalParams, calldata: Array, + ref self: ContractState, proposal_params: ProposalParams, calldata: Call, ) -> u256 { - assert(calldata.len() > 0, 'EMPTY CALLDATA'); let owner = get_caller_address(); let minimal_balance = self.minimal_balance_create_proposal.read(); @@ -227,12 +226,13 @@ pub mod DaoAA { self.proposals.entry(id).write(Option::Some(proposal)); let proposal_calldata = self.proposals_calldata.entry(id); - for i in 0 - ..calldata - .len() { - let data = *calldata.at(i); - proposal_calldata.append().write(data); - }; + + proposal_calldata.to.write(calldata.to); + proposal_calldata.selector.write(calldata.selector); + proposal_calldata.is_executed.write(false); + for data in calldata.calldata { + proposal_calldata.calldata.append().write(*data); + }; self.total_proposal.write(id); self.emit(ProposalCreated { id, owner, created_at, end_at }); @@ -527,7 +527,7 @@ mod tests { use afk::interfaces::voting::{ Proposal, ProposalParams, ProposalResult, ProposalType, UserVote, VoteState, ProposalCreated, SET_PROPOSAL_DURATION_IN_SECONDS, ProposalVoted, IVoteProposalDispatcher, - IVoteProposalDispatcherTrait, ConfigParams, ConfigResponse, ProposalResolved + IVoteProposalDispatcherTrait, ConfigParams, ConfigResponse, ProposalResolved, }; use afk::tokens::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; use openzeppelin::utils::serde::SerializedAppend; @@ -536,6 +536,7 @@ mod tests { EventsFilterTrait, cheat_caller_address, cheatcodes::events::Event, declare, spy_events, cheat_block_timestamp, }; + use starknet::account::Call; use starknet::{ContractAddress, contract_address_const}; use super::{IDaoAADispatcher, IDaoAADispatcherTrait}; @@ -592,10 +593,13 @@ mod tests { proposal_type: Default::default(), proposal_automated_transaction: Default::default(), }; - - let mock_calldata: Array = array!['data 1', 'data 2']; + let calldata = Call { + to: contract_address_const::<'TO'>(), + selector: 'selector', + calldata: array!['data 1', 'data 2'].span() + }; // created by 'CREATOR' - proposal_dispatcher.create_proposal(proposal_params, mock_calldata) + proposal_dispatcher.create_proposal(proposal_params, calldata) } /// TESTS @@ -817,7 +821,7 @@ mod tests { proposal_dispatcher.process_result(proposal_id); let expected_event = super::DaoAA::Event::ProposalResolved( - ProposalResolved { id: proposal_id, owner: CREATOR(), result: ProposalResult::Passed } + ProposalResolved { id: proposal_id, owner: CREATOR(), result: ProposalResult::Passed }, ); spy.assert_emitted(@array![(proposal_contract, expected_event)]); diff --git a/onchain/cairo/afk/src/interfaces/voting.cairo b/onchain/cairo/afk/src/interfaces/voting.cairo index 474076a5..427541e9 100644 --- a/onchain/cairo/afk/src/interfaces/voting.cairo +++ b/onchain/cairo/afk/src/interfaces/voting.cairo @@ -1,3 +1,4 @@ +use starknet::account::Call; use starknet::storage::{ Map, Vec // MutableEntryStoragePathEntry, StorableEntryReadAccess, StorageAsPathReadForward, // MutableStorableEntryReadAccess, MutableStorableEntryWriteAccess, @@ -143,6 +144,13 @@ pub struct ConfigResponse { pub minimum_threshold_percentage: u64 } +#[starknet::storage_node] +pub struct Calldata { + pub to: ContractAddress, + pub selector: felt252, + pub calldata: Vec, + pub is_executed: bool +} // #[derive(Drop, Serde, Copy, starknet::Store, PartialEq)] // pub struct VoteState { @@ -166,7 +174,7 @@ pub struct VoteState { #[starknet::interface] pub trait IVoteProposal { fn create_proposal( - ref self: TContractState, proposal_params: ProposalParams, calldata: Array + ref self: TContractState, proposal_params: ProposalParams, calldata: Call ) -> u256; fn cast_vote(ref self: TContractState, proposal_id: u256, opt_vote_type: Option); fn get_proposal(self: @TContractState, proposal_id: u256) -> Proposal; From 0fd216e92a56a82de7d2fa4dbe17fbbdb83eabb3 Mon Sep 17 00:00:00 2001 From: Birdmannn Date: Sat, 15 Feb 2025 13:48:18 +0100 Subject: [PATCH 2/4] feat: enhanced process_result calldata --- onchain/cairo/afk/src/dao/dao_aa.cairo | 18 ++++++++++++++++++ onchain/cairo/afk/src/interfaces/voting.cairo | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/onchain/cairo/afk/src/dao/dao_aa.cairo b/onchain/cairo/afk/src/dao/dao_aa.cairo index 98d63291..908500be 100644 --- a/onchain/cairo/afk/src/dao/dao_aa.cairo +++ b/onchain/cairo/afk/src/dao/dao_aa.cairo @@ -108,6 +108,10 @@ pub mod DaoAA { // vote_by_proposal: Map, tx_data_per_proposal: Map>, // starknet_address: felt252, + executable_calls: Map< + (ContractAddress, felt252), Vec> + >, // Map (target_contract, selector) => (selector, calldata). Here validate that the selector and calldata actually belong to the value + executables_count: u64, // votes_by_proposal: Map, // Maps proposal ID to vote count // here // user_votes: Map< @@ -230,6 +234,7 @@ pub mod DaoAA { proposal_calldata.to.write(calldata.to); proposal_calldata.selector.write(calldata.selector); proposal_calldata.is_executed.write(false); + for data in calldata.calldata { proposal_calldata.calldata.append().write(*data); }; @@ -382,6 +387,19 @@ pub mod DaoAA { if valid_threshold_percentage >= self.minimum_threshold_percentage.read() { proposal.proposal_result = ProposalResult::Passed; + let proposal_calldata = self.proposals_calldata.entry(proposal_id); + let target_contract = proposal_calldata.to.read(); + let selector = proposal_calldata.selector.read(); + + let vec = self.executable_calls.entry((target_contract, selector)); + let executables_count = self.executables_count.read(); + for i in 0 + ..proposal_calldata + .calldata + .len() { + vec.append().append().write(proposal_calldata.calldata.at(i).read()); + }; + self.executables_count.write(executables_count + 1); } else { proposal.proposal_result = ProposalResult::Failed; } diff --git a/onchain/cairo/afk/src/interfaces/voting.cairo b/onchain/cairo/afk/src/interfaces/voting.cairo index 427541e9..22429540 100644 --- a/onchain/cairo/afk/src/interfaces/voting.cairo +++ b/onchain/cairo/afk/src/interfaces/voting.cairo @@ -149,7 +149,7 @@ pub struct Calldata { pub to: ContractAddress, pub selector: felt252, pub calldata: Vec, - pub is_executed: bool + pub is_executed: bool, } // #[derive(Drop, Serde, Copy, starknet::Store, PartialEq)] From c9d68acf0662ddd671d19b9006c9f871cf863a1d Mon Sep 17 00:00:00 2001 From: Birdmannn Date: Sat, 15 Feb 2025 15:30:48 +0100 Subject: [PATCH 3/4] fix: optimized calldata storage --- onchain/cairo/afk/src/dao/dao_aa.cairo | 54 +++++++++++++------ onchain/cairo/afk/src/interfaces/voting.cairo | 2 + 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/onchain/cairo/afk/src/dao/dao_aa.cairo b/onchain/cairo/afk/src/dao/dao_aa.cairo index 908500be..ff652995 100644 --- a/onchain/cairo/afk/src/dao/dao_aa.cairo +++ b/onchain/cairo/afk/src/dao/dao_aa.cairo @@ -43,11 +43,14 @@ pub mod DaoAA { MIN_TRANSACTION_VERSION, QUERY_OFFSET, execute_calls // is_valid_stark_signature }; use core::ecdsa::check_ecdsa_signature; + use core::hash::{HashStateExTrait, HashStateTrait}; use core::num::traits::Zero; + use core::poseidon::{PoseidonTrait, poseidon_hash_span}; use openzeppelin::access::accesscontrol::AccessControlComponent; use openzeppelin::governance::timelock::TimelockControllerComponent; use openzeppelin::introspection::src5::SRC5Component; use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + use openzeppelin::utils::cryptography::snip12::StructHash; use starknet::account::Call; use starknet::storage::{ @@ -104,13 +107,14 @@ pub mod DaoAA { proposals_calldata: Map, // Map ProposalID => calldata proposal_by_user: Map, total_proposal: u256, + executable_tx: Map, // Map Hashed Call => executable + proposal_tx: Map< + u256, felt252 + >, // Map Proposal ID => Hashed Call (for one call, multicall excluded) vote_state_by_proposal: Map, // Map ProposalID => VoteState // vote_by_proposal: Map, tx_data_per_proposal: Map>, // starknet_address: felt252, - executable_calls: Map< - (ContractAddress, felt252), Vec> - >, // Map (target_contract, selector) => (selector, calldata). Here validate that the selector and calldata actually belong to the value executables_count: u64, // votes_by_proposal: Map, // Maps proposal ID to vote count // here @@ -227,6 +231,7 @@ pub mod DaoAA { proposal_result_by: contract_address_const::<0x0>(), }; + // check self.proposals.entry(id).write(Option::Some(proposal)); let proposal_calldata = self.proposals_calldata.entry(id); @@ -238,7 +243,9 @@ pub mod DaoAA { for data in calldata.calldata { proposal_calldata.calldata.append().write(*data); }; + // end check. + self.proposal_tx.entry(id).write(calldata.hash_struct()); self.total_proposal.write(id); self.emit(ProposalCreated { id, owner, created_at, end_at }); @@ -387,18 +394,8 @@ pub mod DaoAA { if valid_threshold_percentage >= self.minimum_threshold_percentage.read() { proposal.proposal_result = ProposalResult::Passed; - let proposal_calldata = self.proposals_calldata.entry(proposal_id); - let target_contract = proposal_calldata.to.read(); - let selector = proposal_calldata.selector.read(); - - let vec = self.executable_calls.entry((target_contract, selector)); - let executables_count = self.executables_count.read(); - for i in 0 - ..proposal_calldata - .calldata - .len() { - vec.append().append().write(proposal_calldata.calldata.at(i).read()); - }; + let proposal_tx = self.proposal_tx.entry(proposal_id).read(); + self.executable_tx.entry(proposal_tx).write(true); self.executables_count.write(executables_count + 1); } else { proposal.proposal_result = ProposalResult::Failed; @@ -412,6 +409,22 @@ pub mod DaoAA { ); self.proposals.entry(proposal_id).write(Option::Some(proposal)); } + + fn is_executable(ref self: ContractState, calldata: Call) -> bool { + self.executable_tx.entry(calldata.hash_struct()).read() + } + } + + pub impl CallStructHash of StructHash { + fn hash_struct(self: @Call) -> felt252 { + let hash_state = PoseidonTrait::new(); + hash_state + .update_with('AFK_DAO') + .update_with(*self.to) + .update_with(*self.selector) + .update_with(poseidon_hash_span(*self.calldata)) + .finalize() + } } #[abi(embed_v0)] @@ -617,7 +630,9 @@ mod tests { calldata: array!['data 1', 'data 2'].span() }; // created by 'CREATOR' - proposal_dispatcher.create_proposal(proposal_params, calldata) + let proposal_id = proposal_dispatcher.create_proposal(proposal_params, calldata); + assert(!proposal_dispatcher.is_executable(calldata), ''); + proposal_id } /// TESTS @@ -843,5 +858,12 @@ mod tests { ); spy.assert_emitted(@array![(proposal_contract, expected_event)]); + let calldata = Call { + to: contract_address_const::<'TO'>(), + selector: 'selector', + calldata: array!['data 1', 'data 2'].span() + }; + // the creating call should be executable + assert(proposal_dispatcher.is_executable(calldata), 'NOT EXECUTABLE'); } } diff --git a/onchain/cairo/afk/src/interfaces/voting.cairo b/onchain/cairo/afk/src/interfaces/voting.cairo index 22429540..15a56cb5 100644 --- a/onchain/cairo/afk/src/interfaces/voting.cairo +++ b/onchain/cairo/afk/src/interfaces/voting.cairo @@ -181,6 +181,8 @@ pub trait IVoteProposal { fn get_user_vote(self: @TContractState, proposal_id: u256, user: ContractAddress) -> UserVote; fn cancel_proposal(ref self: TContractState, proposal_id: u256); fn process_result(ref self: TContractState, proposal_id: u256); + // debugging + fn is_executable(ref self: TContractState, calldata: Call) -> bool; } // Possible extracted Proposal Functions // Mint the token with a specific ratio From 54efaa081df4d7e02935cf3c5db3a9b1ea34315b Mon Sep 17 00:00:00 2001 From: Birdmannn Date: Sat, 15 Feb 2025 15:53:38 +0100 Subject: [PATCH 4/4] fix: minor fix --- onchain/cairo/afk/src/dao/dao_aa.cairo | 1 + 1 file changed, 1 insertion(+) diff --git a/onchain/cairo/afk/src/dao/dao_aa.cairo b/onchain/cairo/afk/src/dao/dao_aa.cairo index ff652995..4ad583a7 100644 --- a/onchain/cairo/afk/src/dao/dao_aa.cairo +++ b/onchain/cairo/afk/src/dao/dao_aa.cairo @@ -392,6 +392,7 @@ pub mod DaoAA { let total_votes = yes_votes + no_votes; let valid_threshold_percentage = yes_votes * 100 / total_votes; + let executables_count = self.executables_count.read(); if valid_threshold_percentage >= self.minimum_threshold_percentage.read() { proposal.proposal_result = ProposalResult::Passed; let proposal_tx = self.proposal_tx.entry(proposal_id).read();