Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions src/guild/guild.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ pub mod Guild {
use guilds::guild::guild_contract::GuildComponent::InternalImpl;
use guilds::interfaces::guild::{IGuild, IGuildView};
use guilds::models::types::{
DistributionPolicy, EpochSnapshot, Member, PendingInvite, PluginConfig, RedemptionWindow,
Role, ShareOffer,
DistributionPolicy, EpochSnapshot, GuildScore, Member, PendingInvite, PluginConfig,
RedemptionWindow, Role, Season, ShareOffer,
};
use starknet::ContractAddress;
use starknet::storage::{StorageMapReadAccess, StoragePointerReadAccess};
Expand Down Expand Up @@ -181,6 +181,20 @@ pub mod Guild {
self.guild.redeem_shares(amount);
}

fn create_season(
ref self: ContractState, name: felt252, starts_at: u64, ends_at: u64,
) {
self.guild.create_season(name, starts_at, ends_at);
}

fn finalize_season(ref self: ContractState, season_id: u64) {
self.guild.finalize_season(season_id);
}

fn record_score(ref self: ContractState, season_id: u64, points: u64) {
self.guild.record_score(season_id, points);
}

fn dissolve(ref self: ContractState) {
self.guild.dissolve();
}
Expand Down Expand Up @@ -255,5 +269,25 @@ pub mod Guild {
fn get_redemption_window(self: @ContractState) -> RedemptionWindow {
self.guild.redemption_window.read()
}

fn is_dissolved(self: @ContractState) -> bool {
self.guild.is_dissolved.read()
}

fn get_revenue_token(self: @ContractState) -> ContractAddress {
self.guild.revenue_token.read()
}

fn get_season(self: @ContractState, season_id: u64) -> Season {
self.guild.seasons.read(season_id)
}

fn get_season_count(self: @ContractState) -> u64 {
self.guild.season_count.read()
}

fn get_guild_score(self: @ContractState, season_id: u64) -> GuildScore {
self.guild.guild_scores.read(season_id)
}
}
}
92 changes: 90 additions & 2 deletions src/guild/guild_contract.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ pub mod GuildComponent {
};
use guilds::models::events;
use guilds::models::types::{
DistributionPolicy, EpochSnapshot, Member, PendingInvite, PluginConfig, RedemptionWindow,
Role, ShareOffer,
DistributionPolicy, EpochSnapshot, GuildScore, Member, PendingInvite, PluginConfig,
RedemptionWindow, Role, Season, ShareOffer,
};
use openzeppelin_interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait};
use openzeppelin_interfaces::votes::{IVotesDispatcher, IVotesDispatcherTrait};
Expand Down Expand Up @@ -78,6 +78,10 @@ pub mod GuildComponent {
pub has_active_offer: bool,
pub redemption_window: RedemptionWindow,
pub member_last_redemption_epoch: Map<ContractAddress, u64>,
// --- Seasons ---
pub season_count: u64,
pub seasons: Map<u64, Season>,
pub guild_scores: Map<u64, GuildScore>,
// --- Lifecycle ---
pub is_dissolved: bool,
}
Expand Down Expand Up @@ -109,6 +113,9 @@ pub mod GuildComponent {
ShareOfferCreated: events::ShareOfferCreated,
SharesPurchased: events::SharesPurchased,
SharesRedeemed: events::SharesRedeemed,
SeasonCreated: events::SeasonCreated,
SeasonFinalized: events::SeasonFinalized,
ScoreRecorded: events::ScoreRecorded,
GuildDissolved: events::GuildDissolved,
}

Expand Down Expand Up @@ -187,6 +194,12 @@ pub mod GuildComponent {
pub const REDEMPTION_LIMIT_EXCEEDED: felt252 = 'Exceeds epoch redemption limit';
pub const REDEMPTION_COOLDOWN_ACTIVE: felt252 = 'Redemption cooldown active';
pub const REDEMPTION_PAYOUT_ZERO: felt252 = 'Payout is zero';
pub const SEASON_NAME_INVALID: felt252 = 'Season name cannot be zero';
pub const SEASON_NOT_FOUND: felt252 = 'Season does not exist';
pub const SEASON_ALREADY_FINALIZED: felt252 = 'Season already finalized';
pub const SEASON_NOT_ACTIVE: felt252 = 'Season is not active';
pub const SEASON_TIMING_INVALID: felt252 = 'Season timing invalid';
pub const SCORE_ZERO: felt252 = 'Score must be > 0';
}

// ====================================================================
Expand Down Expand Up @@ -940,6 +953,81 @@ pub mod GuildComponent {
self.emit(events::SharesRedeemed { redeemer: caller, amount, payout });
}

// ----------------------------------------------------------------
// Season scoring
// ----------------------------------------------------------------

fn create_season(
ref self: ComponentState<TContractState>,
name: felt252,
starts_at: u64,
ends_at: u64,
) -> u64 {
self.assert_not_dissolved();
self.only_governor();
assert!(name != 0, "{}", Errors::SEASON_NAME_INVALID);
if ends_at > 0 {
assert!(ends_at > starts_at, "{}", Errors::SEASON_TIMING_INVALID);
}

let season_id = self.season_count.read();
self
.seasons
.write(season_id, Season { name, starts_at, ends_at, finalized: false });
self.season_count.write(season_id + 1);

self.emit(events::SeasonCreated { season_id, name, starts_at, ends_at });

season_id
}

fn finalize_season(ref self: ComponentState<TContractState>, season_id: u64) {
self.assert_not_dissolved();
self.only_governor();

assert!(season_id < self.season_count.read(), "{}", Errors::SEASON_NOT_FOUND);
let mut season = self.seasons.read(season_id);
assert!(!season.finalized, "{}", Errors::SEASON_ALREADY_FINALIZED);

season.finalized = true;
self.seasons.write(season_id, season);

self
.emit(
events::SeasonFinalized {
season_id, finalized_at: get_block_timestamp(),
},
);
}

fn record_score(
ref self: ComponentState<TContractState>, season_id: u64, points: u64,
) {
let caller = get_caller_address();
self.check_permission(caller, ActionType::SCORE, 0);

assert!(season_id < self.season_count.read(), "{}", Errors::SEASON_NOT_FOUND);
let season = self.seasons.read(season_id);
assert!(!season.finalized, "{}", Errors::SEASON_ALREADY_FINALIZED);

let now = get_block_timestamp();
if season.starts_at > 0 {
assert!(now >= season.starts_at, "{}", Errors::SEASON_NOT_ACTIVE);
}
if season.ends_at > 0 {
assert!(now < season.ends_at, "{}", Errors::SEASON_NOT_ACTIVE);
}

assert!(points > 0, "{}", Errors::SCORE_ZERO);

let mut score = self.guild_scores.read(season_id);
score.points = score.points + points;
score.last_updated = now;
self.guild_scores.write(season_id, score);

self.emit(events::ScoreRecorded { season_id, points, recorded_by: caller });
}

fn dissolve(ref self: ComponentState<TContractState>) {
self.assert_not_dissolved();
self.only_governor();
Expand Down
20 changes: 18 additions & 2 deletions src/interfaces/guild.cairo
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use guilds::models::types::{
DistributionPolicy, EpochSnapshot, Member, PendingInvite, PluginConfig, RedemptionWindow, Role,
ShareOffer,
DistributionPolicy, EpochSnapshot, GuildScore, Member, PendingInvite, PluginConfig,
RedemptionWindow, Role, Season, ShareOffer,
};
use starknet::ContractAddress;

Expand Down Expand Up @@ -118,6 +118,17 @@ pub trait IGuild<TState> {
/// Redeem (burn) shares for proportional treasury value.
fn redeem_shares(ref self: TState, amount: u256);

// --- Season Scoring ---

/// Create a new season. Only callable by the Governor.
fn create_season(ref self: TState, name: felt252, starts_at: u64, ends_at: u64);

/// Finalize a season (freeze scores). Only callable by the Governor.
fn finalize_season(ref self: TState, season_id: u64);

/// Record score points for the guild in a season. Requires SCORE permission.
fn record_score(ref self: TState, season_id: u64, points: u64);

// --- Lifecycle ---

/// Dissolve the guild. Only callable by the Governor.
Expand Down Expand Up @@ -148,4 +159,9 @@ pub trait IGuildView<TState> {
fn get_active_offer(self: @TState) -> ShareOffer;
fn has_active_offer(self: @TState) -> bool;
fn get_redemption_window(self: @TState) -> RedemptionWindow;
fn is_dissolved(self: @TState) -> bool;
fn get_revenue_token(self: @TState) -> ContractAddress;
fn get_season(self: @TState, season_id: u64) -> Season;
fn get_season_count(self: @TState) -> u64;
fn get_guild_score(self: @TState, season_id: u64) -> GuildScore;
}
8 changes: 5 additions & 3 deletions src/models/constants.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ pub mod ActionType {
pub const SHARE_MGMT: u32 = 0x10; // bit 4
/// Trigger epoch finalization and distribution
pub const DISTRIBUTE: u32 = 0x20; // bit 5
// bits 6-7 reserved for future core actions
/// Record season scores for guild standings
pub const SCORE: u32 = 0x40; // bit 6
// bit 7 reserved for future core action

/// PonziLand plugin actions (bits 8-15)
pub const PONZI_BUY_LAND: u32 = 0x100; // bit 8
Expand All @@ -33,11 +35,11 @@ pub mod ActionType {
// bits 24-31: available for plugin slot 3

/// Convenience: all core actions
pub const ALL_CORE: u32 = 0x3F; // bits 0-5
pub const ALL_CORE: u32 = 0x7F; // bits 0-6
/// Convenience: all PonziLand actions
pub const ALL_PONZI: u32 = 0x3F00; // bits 8-13
/// Convenience: all actions (core + ponziland)
pub const ALL: u32 = 0x3F3F; // bits 0-5 + 8-13
pub const ALL: u32 = 0x3F7F; // bits 0-6 + 8-13
}

/// Basis points denominator (100% = 10000 bps)
Expand Down
25 changes: 25 additions & 0 deletions src/models/events.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,31 @@ pub struct GuildDissolved {
pub dissolved_at: u64,
}

// ========================================================================
// Season Events
// ========================================================================

#[derive(Drop, starknet::Event)]
pub struct SeasonCreated {
pub season_id: u64,
pub name: felt252,
pub starts_at: u64,
pub ends_at: u64,
}

#[derive(Drop, starknet::Event)]
pub struct SeasonFinalized {
pub season_id: u64,
pub finalized_at: u64,
}

#[derive(Drop, starknet::Event)]
pub struct ScoreRecorded {
pub season_id: u64,
pub points: u64,
pub recorded_by: ContractAddress,
}

// ========================================================================
// Factory Events
// ========================================================================
Expand Down
23 changes: 23 additions & 0 deletions src/models/types.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,29 @@ pub struct GuildRegistryEntry {
pub is_active: bool,
}

/// A season defines a competitive period for guild standings.
/// Seasons are created and finalized through governance.
#[derive(Drop, Serde, Copy, starknet::Store, PartialEq)]
pub struct Season {
/// Human-readable identifier for the season
pub name: felt252,
/// Block timestamp when the season started
pub starts_at: u64,
/// Block timestamp when the season ends (0 = open-ended)
pub ends_at: u64,
/// Whether the season has been finalized (scores are frozen)
pub finalized: bool,
}

/// A guild's score entry for a specific season.
#[derive(Drop, Serde, Copy, starknet::Store, PartialEq)]
pub struct GuildScore {
/// Accumulated points for this guild in this season
pub points: u64,
/// Block timestamp of the last score update
pub last_updated: u64,
}

/// Governor configuration used during guild deployment.
/// Not stored on-chain after initialization — passed to Governor constructor.
#[derive(Drop, Serde, Copy)]
Expand Down
Loading
Loading