Skip to content

Commit

Permalink
feat: dao_factory and dao_aa enhancement, part 1. (#501)
Browse files Browse the repository at this point in the history
  • Loading branch information
Birdmannn authored Feb 17, 2025
1 parent e5b77ad commit a511412
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 17 deletions.
76 changes: 59 additions & 17 deletions onchain/cairo/afk/src/dao/dao_aa.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ pub trait IDaoAA<TContractState> {
}

#[starknet::interface]
pub trait ISRC6<TState> {
fn __execute__(self: @TState, calls: Array<Call>) -> Array<Span<felt252>>;
fn __validate__(self: @TState, calls: Array<Call>) -> felt252;
fn is_valid_signature(self: @TState, hash: felt252, signature: Array<felt252>) -> felt252;
pub trait ISRC6<TContractState> {
fn __execute__(ref self: TContractState, calls: Array<Call>) -> Array<Span<felt252>>;
fn __validate__(self: @TContractState, calls: Array<Call>) -> felt252;
fn is_valid_signature(
self: @TContractState, hash: felt252, signature: Array<felt252>
) -> felt252;
}


Expand Down Expand Up @@ -167,6 +169,7 @@ pub mod DaoAA {
owner: ContractAddress,
token_contract_address: ContractAddress,
public_key: u256,
starknet_address: felt252
) {
// self.public_key.write(public_key);
self.owner.write(owner);
Expand All @@ -175,6 +178,7 @@ pub mod DaoAA {
self.is_only_dao_execution.write(true);
self.minimum_threshold_percentage.write(60);
// TODO: init self.starknet_address here
self.starknet_address.write(starknet_address);
// self.accesscontrol.initializer();
// self.accesscontrol._grant_role(ADMIN_ROLE, owner);
// self.accesscontrol._grant_role(MINTER_ROLE, admin);
Expand Down Expand Up @@ -448,26 +452,51 @@ pub mod DaoAA {
}
}

pub impl CallStructHash of StructHash<Call> {
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)]
impl ISRC6Impl of ISRC6<ContractState> {
// TODO
// Verify the TX is automated of the proposal is valid for this calldata
// CENSORED the owner/signature for a real AA Autonomous for DAO and agents
fn __execute__(self: @ContractState, calls: Array<Call>) -> Array<Span<felt252>> {

// TODO, security issue.
fn __execute__(ref self: ContractState, calls: Array<Call>) -> Array<Span<felt252>> {
assert!(get_caller_address().is_zero(), "invalid caller");

// Verify calls before executing
for i in 0
..calls
.len() {
// iterate through the max_executable_clone for each tx.
let current_call = *calls.at(i);
let current_call_hash = current_call.hash_struct();
let max_tx_count = self
.max_executable_clone
.entry(current_call_hash)
.read();
let mut tx_count = 1;
let mut is_executable = false;
while tx_count <= max_tx_count {
is_executable = self
.executable_tx
.entry((current_call_hash, tx_count))
.read();
if is_executable {
// mark the call as executed (now as a non-executable)
self
.executable_tx
.entry((current_call_hash, tx_count))
.write(false);
break;
}
tx_count += 1;
};
assert!(is_executable, "EXECUTION ERR: ONE CALL CHECK FAILED.");
self.executed_count.write(self.executed_count.read() + 1);
// TODO
// currently there's no way to set a Proposal as executed because this task
// will require the proposals id. In that case, it must be done manually.
};

// Check tx version
let tx_info = get_tx_info().unbox();
let tx_version: u256 = tx_info.version.into();
Expand Down Expand Up @@ -601,6 +630,18 @@ pub mod DaoAA {
};
}
}

pub impl CallStructHash of StructHash<Call> {
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()
}
}
}

#[cfg(test)]
Expand Down Expand Up @@ -653,6 +694,7 @@ mod tests {
constructor_calldata.append_serde(owner);
constructor_calldata.append_serde(token_contract_address);
constructor_calldata.append_serde(public_key);
constructor_calldata.append_serde('STARKNET ADDRESS');

let contract = declare("DaoAA").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@constructor_calldata).unwrap();
Expand Down
159 changes: 159 additions & 0 deletions onchain/cairo/afk/src/dao/dao_factory.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use starknet::{ContractAddress, ClassHash};

#[starknet::interface]
pub trait IDaoFactory<TContractState> {
fn create_dao(
ref self: TContractState,
token_contract_address: ContractAddress,
public_key: u256,
starknet_address: felt252
) -> ContractAddress;
fn get_dao_class_hash(self: @TContractState) -> ClassHash;
fn update_dao_class_hash(ref self: TContractState, new_class_hash: ClassHash);
// TODO: NOTE, THE DAO IS NOT UPGRADEABLE YET.
}

#[starknet::contract]
pub mod DaoFactory {
use core::num::traits::Zero;
use openzeppelin::access::ownable::OwnableComponent;
use starknet::storage::{
Map, StoragePointerReadAccess, StoragePointerWriteAccess, StoragePathEntry
};
use starknet::{ContractAddress, ClassHash, get_caller_address, syscalls::deploy_syscall};

component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);

#[abi(embed_v0)]
impl OwnableImpl = OwnableComponent::OwnableImpl<ContractState>;
impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;

#[storage]
struct Storage {
dao_aa_list: Map<(ContractAddress, ContractAddress), ClassHash>,
dao_class_hash: ClassHash,
#[substorage(v0)]
ownable: OwnableComponent::Storage,
}

#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
#[flat]
OwnableEvent: OwnableComponent::Event,
ClassHashUpdated: ClassHashUpdated,
DaoAACreated: DaoAACreated
}

#[derive(Drop, starknet::Event)]
pub struct ClassHashUpdated {
pub old_class_hash: ClassHash,
#[key]
pub new_class_hash: ClassHash
}

#[derive(Drop, starknet::Event)]
pub struct DaoAACreated {
pub creator: ContractAddress,
#[key]
pub contract_address: ContractAddress,
}

#[constructor]
fn constructor(ref self: ContractState, class_hash: ClassHash) {
assert(class_hash.is_non_zero(), 'CLASS HASH IS ZERO');
self.dao_class_hash.write(class_hash);
self.ownable.initializer(get_caller_address());
}

#[abi(embed_v0)]
impl DaoFactoryImpl of super::IDaoFactory<ContractState> {
fn create_dao(
ref self: ContractState,
token_contract_address: ContractAddress,
public_key: u256,
starknet_address: felt252
) -> ContractAddress {
let mut calldata = array![];
let creator = get_caller_address();
(creator, token_contract_address, public_key, starknet_address).serialize(ref calldata);
let (contract_address, _) = deploy_syscall(
self.dao_class_hash.read(), 0, calldata.span(), false
)
.unwrap();
// track instances
self.dao_aa_list.entry((creator, contract_address)).write(self.dao_class_hash.read());

self.emit(DaoAACreated { creator, contract_address });

contract_address
}

fn get_dao_class_hash(self: @ContractState) -> ClassHash {
self.dao_class_hash.read()
}

fn update_dao_class_hash(ref self: ContractState, new_class_hash: ClassHash) {
self.ownable.assert_only_owner();
assert(new_class_hash.is_non_zero(), 'CLASS HASH IS ZERO');
let old_class_hash = self.dao_class_hash.read();
self.dao_class_hash.write(new_class_hash);

self.emit(ClassHashUpdated { old_class_hash, new_class_hash });
}
}
}

#[cfg(test)]
mod tests {
use afk::dao::dao_factory::{IDaoFactoryDispatcher, IDaoFactoryDispatcherTrait};
use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait};
use snforge_std::{
declare, cheat_caller_address, ContractClassTrait, DeclareResultTrait, CheatSpan,
spy_events, EventSpyAssertionsTrait
};
use starknet::{ContractAddress, contract_address_const, ClassHash, get_contract_address};

fn CREATOR() -> ContractAddress {
contract_address_const::<'CREATOR'>()
}

fn OWNER() -> ContractAddress {
contract_address_const::<'OWNER'>()
}

fn deploy_dao_factory(class_hash: ClassHash) -> ContractAddress {
let mut constructor_calldata = array![];
class_hash.serialize(ref constructor_calldata);
let contract = declare("DaoFactory").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@constructor_calldata).unwrap();

contract_address
}

#[test]
fn test_dao_factory_create_dao() {
let contract = declare("DaoAA").unwrap().contract_class();
let class_hash: ClassHash = *contract.class_hash;
let dao_factory_contract = deploy_dao_factory(class_hash);
let dispatcher = IDaoFactoryDispatcher { contract_address: dao_factory_contract };
let ownable = IOwnableDispatcher { contract_address: dao_factory_contract };

let mut spy = spy_events();
assert(ownable.owner() == get_contract_address(), 'WRONG INIT');

// create a dao
cheat_caller_address(dao_factory_contract, CREATOR(), CheatSpan::TargetCalls(1));
let contract = dispatcher
.create_dao(contract_address_const::<'TOKEN'>(), 6732_u256, 'STRK TOKEN');

println!("New Contract: {:?}", contract);
assert(contract > contract_address_const::<0x0>(), 'CREATION FAILED');

let creation_event = super::DaoFactory::Event::DaoAACreated(
super::DaoFactory::DaoAACreated { creator: CREATOR(), contract_address: contract }
);

spy.assert_emitted(@array![(dao_factory_contract, creation_event)]);
}
}
1 change: 1 addition & 0 deletions onchain/cairo/afk/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub mod afk_id {

pub mod dao {
pub mod dao_aa;
pub mod dao_factory;
}
pub mod defi {
pub mod vault;
Expand Down

0 comments on commit a511412

Please sign in to comment.