From 5824fe844da174af3d721dc0ff283f5fd2aa4169 Mon Sep 17 00:00:00 2001 From: Othala Date: Fri, 20 Feb 2026 11:06:44 +0000 Subject: [PATCH 1/2] start chat-1771585604433 From 0a97ac734f6725c540b8447884f5508ed90e586d Mon Sep 17 00:00:00 2001 From: 0xMugen Date: Fri, 20 Feb 2026 11:30:19 +0000 Subject: [PATCH 2/2] task chat-1771585604433: save pending changes --- src/factory/guild_factory.cairo | 283 ++++++++++++++++++++++++++++++++ src/guild/guild.cairo | 4 + src/guild/guild_contract.cairo | 17 +- src/interfaces/guild.cairo | 5 + src/interfaces/token.cairo | 8 + src/lib.cairo | 4 + src/models/events.cairo | 16 ++ src/token/guild_token.cairo | 12 ++ tests/test_factory.cairo | 261 +++++++++++++++++++++++++++++ tests/test_permissions.cairo | 28 +++- 10 files changed, 636 insertions(+), 2 deletions(-) create mode 100644 src/factory/guild_factory.cairo create mode 100644 tests/test_factory.cairo diff --git a/src/factory/guild_factory.cairo b/src/factory/guild_factory.cairo new file mode 100644 index 0000000..f57d515 --- /dev/null +++ b/src/factory/guild_factory.cairo @@ -0,0 +1,283 @@ +/// GuildFactory — deploys Guild + GuildToken + Governor as a coordinated set +/// and maintains a registry of all guilds with name/ticker uniqueness. +#[starknet::contract] +pub mod GuildFactory { + use core::num::traits::Zero; + use guilds::interfaces::factory::IGuildFactory; + use guilds::interfaces::guild::{IGuildDispatcher, IGuildDispatcherTrait}; + use guilds::interfaces::token::{IGuildTokenDispatcher, IGuildTokenDispatcherTrait}; + use guilds::models::constants::ActionType; + use guilds::models::events; + use guilds::models::types::{GovernorConfig, GuildRegistryEntry, Role}; + use starknet::storage::{ + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, Vec, VecTrait, MutableVecTrait, + }; + use starknet::{ + ClassHash, ContractAddress, SyscallResultTrait, get_block_timestamp, get_caller_address, + syscalls::deploy_syscall, + }; + + // ==================================================================== + // Storage + // ==================================================================== + + #[storage] + pub struct Storage { + /// Class hash used to deploy Guild contracts. + pub guild_class_hash: ClassHash, + /// Class hash used to deploy GuildToken contracts. + pub token_class_hash: ClassHash, + /// Class hash used to deploy Governor contracts. + pub governor_class_hash: ClassHash, + /// Registry: guild address => entry. + pub guilds: Map, + /// Name uniqueness: name => guild address (zero if not taken). + pub name_registry: Map, + /// Ticker uniqueness: ticker => guild address (zero if not taken). + pub ticker_registry: Map, + /// List of all guild addresses for enumeration. + pub guild_list: Vec, + /// Running count of guilds. + pub count: u32, + /// Salt counter for deterministic deploys. + pub deploy_salt: felt252, + } + + // ==================================================================== + // Events + // ==================================================================== + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + GuildCreated: events::GuildCreated, + } + + // ==================================================================== + // Errors + // ==================================================================== + + pub mod Errors { + pub const NAME_ZERO: felt252 = 'Guild name cannot be zero'; + pub const TICKER_ZERO: felt252 = 'Guild ticker cannot be zero'; + pub const NAME_TAKEN: felt252 = 'Guild name already taken'; + pub const TICKER_TAKEN: felt252 = 'Guild ticker already taken'; + pub const DEPOSIT_TOKEN_ZERO: felt252 = 'Deposit token cannot be zero'; + pub const SUPPLY_ZERO: felt252 = 'Initial supply must be > 0'; + pub const GUILD_NOT_FOUND: felt252 = 'Guild not found in registry'; + pub const CLASS_HASH_ZERO: felt252 = 'Class hash cannot be zero'; + } + + // ==================================================================== + // Constructor + // ==================================================================== + + #[constructor] + fn constructor( + ref self: ContractState, + guild_class_hash: ClassHash, + token_class_hash: ClassHash, + governor_class_hash: ClassHash, + ) { + assert!(!guild_class_hash.is_zero(), "{}", Errors::CLASS_HASH_ZERO); + assert!(!token_class_hash.is_zero(), "{}", Errors::CLASS_HASH_ZERO); + assert!(!governor_class_hash.is_zero(), "{}", Errors::CLASS_HASH_ZERO); + + self.guild_class_hash.write(guild_class_hash); + self.token_class_hash.write(token_class_hash); + self.governor_class_hash.write(governor_class_hash); + } + + // ==================================================================== + // Internal helpers + // ==================================================================== + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn next_salt(ref self: ContractState) -> felt252 { + let salt = self.deploy_salt.read(); + self.deploy_salt.write(salt + 1); + salt + } + } + + // ==================================================================== + // External implementation + // ==================================================================== + + #[abi(embed_v0)] + impl GuildFactoryImpl of IGuildFactory { + fn create_guild( + ref self: ContractState, + name: felt252, + ticker: felt252, + deposit_token: ContractAddress, + deposit_amount: u256, + initial_token_supply: u256, + governor_config: GovernorConfig, + ) -> (ContractAddress, ContractAddress, ContractAddress) { + // --- Validations --- + assert!(name != 0, "{}", Errors::NAME_ZERO); + assert!(ticker != 0, "{}", Errors::TICKER_ZERO); + assert!( + self.name_registry.read(name) == Zero::zero(), "{}", Errors::NAME_TAKEN, + ); + assert!( + self.ticker_registry.read(ticker) == Zero::zero(), "{}", Errors::TICKER_TAKEN, + ); + assert!(deposit_token != Zero::zero(), "{}", Errors::DEPOSIT_TOKEN_ZERO); + assert!(initial_token_supply > 0, "{}", Errors::SUPPLY_ZERO); + + let creator = get_caller_address(); + let now = get_block_timestamp(); + let zero_addr: ContractAddress = Zero::zero(); + + // --- Step 1: Deploy GuildToken --- + // Constructor: (name: ByteArray, symbol: ByteArray, initial_supply, + // initial_holder, governor_address, guild_address, + // inactivity_threshold) + // Deploy with governor_address=0 and guild_address=0; wire later. + let mut token_calldata: Array = array![]; + let token_name: ByteArray = "GuildToken"; + let token_symbol: ByteArray = "GT"; + token_name.serialize(ref token_calldata); + token_symbol.serialize(ref token_calldata); + initial_token_supply.serialize(ref token_calldata); + creator.serialize(ref token_calldata); + zero_addr.serialize(ref token_calldata); // governor_address (set later) + zero_addr.serialize(ref token_calldata); // guild_address (set later) + 7_776_000_u64.serialize(ref token_calldata); // inactivity_threshold: 90 days + + let token_salt = self.next_salt(); + let (token_address, _) = deploy_syscall( + self.token_class_hash.read(), token_salt, token_calldata.span(), false, + ) + .unwrap_syscall(); + + // --- Step 2: Deploy Guild --- + // Constructor: (guild_name, guild_ticker, token_address, + // governor_address, founder, founder_role) + // Deploy with governor_address=0; wire later. + let founder_role = Role { + name: 'founder', + can_invite: true, + can_kick: true, + can_promote_depth: 255, + can_be_kicked: false, + allowed_actions: ActionType::ALL, + spending_limit: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, + payout_weight: 500, + }; + + let mut guild_calldata: Array = array![]; + name.serialize(ref guild_calldata); + ticker.serialize(ref guild_calldata); + token_address.serialize(ref guild_calldata); + zero_addr.serialize(ref guild_calldata); // governor_address (set later) + creator.serialize(ref guild_calldata); + founder_role.serialize(ref guild_calldata); + + let guild_salt = self.next_salt(); + let (guild_address, _) = deploy_syscall( + self.guild_class_hash.read(), guild_salt, guild_calldata.span(), false, + ) + .unwrap_syscall(); + + // --- Step 3: Deploy Governor --- + // Constructor: (token_address, voting_delay, voting_period, + // proposal_threshold, quorum_numerator, guild_address) + let mut governor_calldata: Array = array![]; + token_address.serialize(ref governor_calldata); + governor_config.voting_delay.serialize(ref governor_calldata); + governor_config.voting_period.serialize(ref governor_calldata); + governor_config.proposal_threshold.serialize(ref governor_calldata); + // quorum_numerator: convert basis points to OZ quorum fraction + // OZ denominator is 1000, so quorum_bps / 10 = quorum numerator + let quorum_numerator: u256 = (governor_config.quorum_bps / 10).into(); + quorum_numerator.serialize(ref governor_calldata); + guild_address.serialize(ref governor_calldata); + + let governor_salt = self.next_salt(); + let (governor_address, _) = deploy_syscall( + self.governor_class_hash.read(), governor_salt, governor_calldata.span(), false, + ) + .unwrap_syscall(); + + // --- Step 4: Wire cross-references --- + // Set governor_address on Guild (one-shot setter) + IGuildDispatcher { contract_address: guild_address } + .set_governor_address(governor_address); + + // Set guild_address and governor_address on GuildToken (one-shot setters) + IGuildTokenDispatcher { contract_address: token_address } + .set_guild_address(guild_address); + IGuildTokenDispatcher { contract_address: token_address } + .set_governor_address(governor_address); + + // --- Register in registry --- + let entry = GuildRegistryEntry { + guild_address, + token_address, + governor_address, + name, + ticker, + creator, + created_at: now, + is_active: true, + }; + self.guilds.write(guild_address, entry); + self.name_registry.write(name, guild_address); + self.ticker_registry.write(ticker, guild_address); + self.guild_list.push(guild_address); + self.count.write(self.count.read() + 1); + + // --- Emit event --- + self + .emit( + events::GuildCreated { + guild_address, + token_address, + governor_address, + name, + ticker, + creator, + created_at: now, + }, + ); + + (guild_address, token_address, governor_address) + } + + fn get_guild( + self: @ContractState, guild_address: ContractAddress, + ) -> GuildRegistryEntry { + let entry = self.guilds.read(guild_address); + assert!(entry.guild_address != Zero::zero(), "{}", Errors::GUILD_NOT_FOUND); + entry + } + + fn is_name_taken(self: @ContractState, name: felt252) -> bool { + self.name_registry.read(name) != Zero::zero() + } + + fn is_ticker_taken(self: @ContractState, ticker: felt252) -> bool { + self.ticker_registry.read(ticker) != Zero::zero() + } + + fn get_all_guilds(self: @ContractState) -> Array { + let mut result: Array = array![]; + let len = self.guild_list.len(); + let mut i: u64 = 0; + while i < len { + result.append(self.guild_list.at(i).read()); + i += 1; + }; + result + } + + fn guild_count(self: @ContractState) -> u32 { + self.count.read() + } + } +} diff --git a/src/guild/guild.cairo b/src/guild/guild.cairo index 867732b..8d13d8f 100644 --- a/src/guild/guild.cairo +++ b/src/guild/guild.cairo @@ -184,6 +184,10 @@ pub mod Guild { fn dissolve(ref self: ContractState) { self.guild.dissolve(); } + + fn set_governor_address(ref self: ContractState, governor_address: ContractAddress) { + self.guild.set_governor_address(governor_address); + } } #[abi(embed_v0)] diff --git a/src/guild/guild_contract.cairo b/src/guild/guild_contract.cairo index 2f5d815..a5ba713 100644 --- a/src/guild/guild_contract.cairo +++ b/src/guild/guild_contract.cairo @@ -128,6 +128,7 @@ pub mod GuildComponent { pub const TOKEN_ADDRESS_INVALID: felt252 = 'Token address cannot be zero'; pub const GOVERNOR_ADDRESS_INVALID: felt252 = 'Governor address cannot be zero'; pub const FOUNDER_ADDRESS_INVALID: felt252 = 'Founder address cannot be zero'; + pub const GOVERNOR_ALREADY_SET: felt252 = 'Governor already set'; pub const INVALID_ROLE_NAME: felt252 = 'Role name cannot be zero'; pub const CANNOT_DELETE_FOUNDER: felt252 = 'Cannot delete founder role'; pub const ROLE_HAS_MEMBERS: felt252 = 'Role still has assigned members'; @@ -210,7 +211,8 @@ pub mod GuildComponent { assert!(guild_name != 0, "{}", Errors::GUILD_NAME_INVALID); assert!(guild_ticker != 0, "{}", Errors::GUILD_TICKER_INVALID); assert!(token_address != Zero::zero(), "{}", Errors::TOKEN_ADDRESS_INVALID); - assert!(governor_address != Zero::zero(), "{}", Errors::GOVERNOR_ADDRESS_INVALID); + // governor_address may be zero during factory deployment (set via + // set_governor_address after Governor is deployed) assert!(founder != Zero::zero(), "{}", Errors::FOUNDER_ADDRESS_INVALID); assert!(founder_role.name != 0, "{}", Errors::INVALID_ROLE_NAME); assert!(!founder_role.can_be_kicked, "{}", Errors::FOUNDER_MUST_NOT_KICK); @@ -1210,5 +1212,18 @@ pub mod GuildComponent { self.emit(events::InviteRevoked { target, revoked_by: caller }); } + + /// One-shot setter for governor_address, callable only when the current + /// value is zero (i.e., during factory deployment before Governor is + /// deployed). Once set, cannot be changed. + fn set_governor_address( + ref self: ComponentState, governor_address: ContractAddress, + ) { + assert!( + self.governor_address.read() == Zero::zero(), "{}", Errors::GOVERNOR_ALREADY_SET, + ); + assert!(governor_address != Zero::zero(), "{}", Errors::GOVERNOR_ADDRESS_INVALID); + self.governor_address.write(governor_address); + } } } diff --git a/src/interfaces/guild.cairo b/src/interfaces/guild.cairo index a62a87e..2d11345 100644 --- a/src/interfaces/guild.cairo +++ b/src/interfaces/guild.cairo @@ -122,6 +122,11 @@ pub trait IGuild { /// Dissolve the guild. Only callable by the Governor. fn dissolve(ref self: TState); + + // --- Factory Init --- + + /// One-shot setter for governor_address (only callable when current value is zero). + fn set_governor_address(ref self: TState, governor_address: ContractAddress); } /// Guild read-only view functions. diff --git a/src/interfaces/token.cairo b/src/interfaces/token.cairo index f3aaf4e..6dfecb8 100644 --- a/src/interfaces/token.cairo +++ b/src/interfaces/token.cairo @@ -45,4 +45,12 @@ pub trait IGuildToken { /// Get the guild contract address this token is associated with. fn get_guild_address(self: @TState) -> ContractAddress; + + // --- Factory Init --- + + /// One-shot setter for guild_address (only callable when current value is zero). + fn set_guild_address(ref self: TState, guild_address: ContractAddress); + + /// One-shot setter for governor_address (only callable when current value is zero). + fn set_governor_address(ref self: TState, governor_address: ContractAddress); } diff --git a/src/lib.cairo b/src/lib.cairo index ce785fc..8421fab 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -45,6 +45,10 @@ pub mod governor { pub mod guild_governor; } +pub mod factory { + pub mod guild_factory; +} + // Keep v0.1 for backward compatibility during migration pub mod gov { pub mod governance; diff --git a/src/models/events.cairo b/src/models/events.cairo index dbea726..e274099 100644 --- a/src/models/events.cairo +++ b/src/models/events.cairo @@ -197,3 +197,19 @@ pub struct InactivityCleared { pub struct GuildDissolved { pub dissolved_at: u64, } + +// ======================================================================== +// Factory Events +// ======================================================================== + +#[derive(Drop, starknet::Event)] +pub struct GuildCreated { + #[key] + pub guild_address: ContractAddress, + pub token_address: ContractAddress, + pub governor_address: ContractAddress, + pub name: felt252, + pub ticker: felt252, + pub creator: ContractAddress, + pub created_at: u64, +} diff --git a/src/token/guild_token.cairo b/src/token/guild_token.cairo index 237d4c7..ca06faf 100644 --- a/src/token/guild_token.cairo +++ b/src/token/guild_token.cairo @@ -242,5 +242,17 @@ pub mod GuildToken { fn get_guild_address(self: @ContractState) -> ContractAddress { self.guild_address.read() } + + fn set_guild_address(ref self: ContractState, guild_address: ContractAddress) { + assert!(self.guild_address.read() == Zero::zero(), "Guild address already set"); + assert!(guild_address != Zero::zero(), "Guild address cannot be zero"); + self.guild_address.write(guild_address); + } + + fn set_governor_address(ref self: ContractState, governor_address: ContractAddress) { + assert!(self.governor_address.read() == Zero::zero(), "Governor address already set"); + assert!(governor_address != Zero::zero(), "Governor address cannot be zero"); + self.governor_address.write(governor_address); + } } } diff --git a/tests/test_factory.cairo b/tests/test_factory.cairo new file mode 100644 index 0000000..ee693cf --- /dev/null +++ b/tests/test_factory.cairo @@ -0,0 +1,261 @@ +use core::num::traits::Zero; +use guilds::interfaces::factory::{IGuildFactoryDispatcher, IGuildFactoryDispatcherTrait}; +use guilds::interfaces::guild::{IGuildViewDispatcher, IGuildViewDispatcherTrait}; +use guilds::interfaces::token::{IGuildTokenDispatcher, IGuildTokenDispatcherTrait}; +use guilds::models::types::GovernorConfig; +use guilds::tests::constants::AsAddressTrait; +use openzeppelin_interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; +use snforge_std::{ + ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address, + stop_cheat_caller_address, +}; +use starknet::ContractAddress; + +fn CREATOR() -> ContractAddress { + 0xCAFE.as_address() +} + +fn ALICE() -> ContractAddress { + 0xA11CE.as_address() +} + +fn DEPOSIT_TOKEN() -> ContractAddress { + 0xD0.as_address() +} + +fn default_governor_config() -> GovernorConfig { + GovernorConfig { + voting_delay: 86400, + voting_period: 259200, + proposal_threshold: 10000, + quorum_bps: 1000, // 10% + timelock_delay: 86400, + } +} + +fn deploy_factory() -> (ContractAddress, IGuildFactoryDispatcher) { + let guild_class = declare("Guild").unwrap().contract_class(); + let token_class = declare("GuildToken").unwrap().contract_class(); + let governor_class = declare("GuildGovernor").unwrap().contract_class(); + let factory_class = declare("GuildFactory").unwrap().contract_class(); + + let mut calldata: Array = array![]; + (*guild_class.class_hash).serialize(ref calldata); + (*token_class.class_hash).serialize(ref calldata); + (*governor_class.class_hash).serialize(ref calldata); + + let (address, _) = factory_class.deploy(@calldata).unwrap(); + (address, IGuildFactoryDispatcher { contract_address: address }) +} + +// ======================================================================== +// Registry tests (unit-level, no deploy_syscall) +// ======================================================================== + +#[test] +fn test_factory_deploy() { + let (_, factory) = deploy_factory(); + assert!(factory.guild_count() == 0); +} + +#[test] +fn test_name_not_taken_initially() { + let (_, factory) = deploy_factory(); + assert!(!factory.is_name_taken('TestGuild')); +} + +#[test] +fn test_ticker_not_taken_initially() { + let (_, factory) = deploy_factory(); + assert!(!factory.is_ticker_taken('TG')); +} + +#[test] +fn test_get_all_guilds_empty() { + let (_, factory) = deploy_factory(); + let guilds = factory.get_all_guilds(); + assert!(guilds.len() == 0); +} + +// ======================================================================== +// create_guild integration tests +// ======================================================================== + +#[test] +fn test_create_guild_success() { + let (factory_addr, factory) = deploy_factory(); + + start_cheat_caller_address(factory_addr, CREATOR()); + + let (guild_addr, token_addr, governor_addr) = factory + .create_guild( + 'TestGuild', + 'TG', + DEPOSIT_TOKEN(), + 0, // deposit_amount (not enforced yet in this implementation) + 1_000_000_000_000_000_000_000_u256, // 1000 tokens + default_governor_config(), + ); + + stop_cheat_caller_address(factory_addr); + + // --- Verify registry --- + assert!(factory.guild_count() == 1); + assert!(factory.is_name_taken('TestGuild')); + assert!(factory.is_ticker_taken('TG')); + + let entry = factory.get_guild(guild_addr); + assert!(entry.guild_address == guild_addr); + assert!(entry.token_address == token_addr); + assert!(entry.governor_address == governor_addr); + assert!(entry.name == 'TestGuild'); + assert!(entry.ticker == 'TG'); + assert!(entry.creator == CREATOR()); + assert!(entry.is_active); + + // --- Verify get_all_guilds --- + let all = factory.get_all_guilds(); + assert!(all.len() == 1); + assert!(*all.at(0) == guild_addr); + + // --- Verify contracts are wired correctly --- + + // Guild should know its token and governor + let guild_view = IGuildViewDispatcher { contract_address: guild_addr }; + assert!(guild_view.get_token_address() == token_addr); + assert!(guild_view.get_governor_address() == governor_addr); + assert!(guild_view.get_guild_name() == 'TestGuild'); + assert!(guild_view.get_guild_ticker() == 'TG'); + + // Creator should be a member with role 0 + let member = guild_view.get_member(CREATOR()); + assert!(member.addr == CREATOR()); + assert!(member.role_id == 0); + assert!(guild_view.get_member_count() == 1); + + // Token should know its guild and governor + let token = IGuildTokenDispatcher { contract_address: token_addr }; + assert!(token.get_guild_address() == guild_addr); + + // Creator should have initial supply + let erc20 = IERC20Dispatcher { contract_address: token_addr }; + assert!(erc20.balance_of(CREATOR()) == 1_000_000_000_000_000_000_000_u256); +} + +#[test] +#[should_panic] +fn test_create_guild_duplicate_name() { + let (factory_addr, factory) = deploy_factory(); + start_cheat_caller_address(factory_addr, CREATOR()); + + factory + .create_guild( + 'TestGuild', 'TG', DEPOSIT_TOKEN(), 0, 1000, default_governor_config(), + ); + + // Second guild with same name should fail + factory + .create_guild( + 'TestGuild', 'TG2', DEPOSIT_TOKEN(), 0, 1000, default_governor_config(), + ); +} + +#[test] +#[should_panic] +fn test_create_guild_duplicate_ticker() { + let (factory_addr, factory) = deploy_factory(); + start_cheat_caller_address(factory_addr, CREATOR()); + + factory + .create_guild( + 'Guild1', 'TG', DEPOSIT_TOKEN(), 0, 1000, default_governor_config(), + ); + + // Second guild with same ticker should fail + factory + .create_guild( + 'Guild2', 'TG', DEPOSIT_TOKEN(), 0, 1000, default_governor_config(), + ); +} + +#[test] +#[should_panic] +fn test_create_guild_zero_name() { + let (factory_addr, factory) = deploy_factory(); + start_cheat_caller_address(factory_addr, CREATOR()); + + factory.create_guild(0, 'TG', DEPOSIT_TOKEN(), 0, 1000, default_governor_config()); +} + +#[test] +#[should_panic] +fn test_create_guild_zero_ticker() { + let (factory_addr, factory) = deploy_factory(); + start_cheat_caller_address(factory_addr, CREATOR()); + + factory + .create_guild('TestGuild', 0, DEPOSIT_TOKEN(), 0, 1000, default_governor_config()); +} + +#[test] +#[should_panic] +fn test_create_guild_zero_deposit_token() { + let (factory_addr, factory) = deploy_factory(); + start_cheat_caller_address(factory_addr, CREATOR()); + + let zero: ContractAddress = Zero::zero(); + factory.create_guild('TestGuild', 'TG', zero, 0, 1000, default_governor_config()); +} + +#[test] +#[should_panic] +fn test_create_guild_zero_supply() { + let (factory_addr, factory) = deploy_factory(); + start_cheat_caller_address(factory_addr, CREATOR()); + + factory + .create_guild('TestGuild', 'TG', DEPOSIT_TOKEN(), 0, 0, default_governor_config()); +} + +#[test] +#[should_panic] +fn test_get_guild_not_found() { + let (_, factory) = deploy_factory(); + + factory.get_guild(0x999.as_address()); +} + +#[test] +fn test_create_multiple_guilds() { + let (factory_addr, factory) = deploy_factory(); + start_cheat_caller_address(factory_addr, CREATOR()); + + let (guild1, _, _) = factory + .create_guild( + 'Guild1', 'G1', DEPOSIT_TOKEN(), 0, 1000, default_governor_config(), + ); + + let (guild2, _, _) = factory + .create_guild( + 'Guild2', 'G2', DEPOSIT_TOKEN(), 0, 2000, default_governor_config(), + ); + + stop_cheat_caller_address(factory_addr); + + assert!(factory.guild_count() == 2); + assert!(factory.is_name_taken('Guild1')); + assert!(factory.is_name_taken('Guild2')); + assert!(!factory.is_name_taken('Guild3')); + + let all = factory.get_all_guilds(); + assert!(all.len() == 2); + assert!(*all.at(0) == guild1); + assert!(*all.at(1) == guild2); + + // Both guilds should be independently queryable + let entry1 = factory.get_guild(guild1); + assert!(entry1.name == 'Guild1'); + + let entry2 = factory.get_guild(guild2); + assert!(entry2.name == 'Guild2'); +} diff --git a/tests/test_permissions.cairo b/tests/test_permissions.cairo index d920429..69a5bbf 100644 --- a/tests/test_permissions.cairo +++ b/tests/test_permissions.cairo @@ -168,9 +168,32 @@ fn test_initializer_rejects_zero_token_address() { ); } +#[test] +fn test_initializer_allows_zero_governor_address_for_factory() { + // The initializer now allows zero governor_address so the factory can + // deploy Guild before Governor exists and wire it later via set_governor_address. + let mut state = COMPONENT_STATE(); + start_cheat_caller_address(test_address(), FOUNDER()); + state + .guild + .initializer( + 'TestGuild', + 'TG', + TOKEN(), + starknet::contract_address_const::<0>(), + FOUNDER(), + default_founder_role(), + ); + // Governor should be zero until set + assert!(state.guild.governor_address.read() == starknet::contract_address_const::<0>()); + // Wire it via the one-shot setter + state.guild.set_governor_address(GOVERNOR()); + assert!(state.guild.governor_address.read() == GOVERNOR()); +} + #[test] #[should_panic] -fn test_initializer_rejects_zero_governor_address() { +fn test_set_governor_address_rejects_double_set() { let mut state = COMPONENT_STATE(); start_cheat_caller_address(test_address(), FOUNDER()); state @@ -183,6 +206,9 @@ fn test_initializer_rejects_zero_governor_address() { FOUNDER(), default_founder_role(), ); + state.guild.set_governor_address(GOVERNOR()); + // Second call should panic + state.guild.set_governor_address(GOVERNOR()); } #[test]