Skip to content

Commit f251b81

Browse files
authored
Merge pull request #813 from Unclebaffa/feat/governance-quorum-requirement
feat(governance): add configurable quorum requirement for closing pro…
2 parents 104accc + 352f93a commit f251b81

250 files changed

Lines changed: 15155 additions & 1282 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

creator-keys/src/events.rs

Lines changed: 135 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,25 @@
2121
//! - `payment`: Total amount paid by the buyer (for buy events, ≥ key price)
2222
2323
use crate::{
24-
constants, read_registered_creator_profile, CreatorKeysContract, CreatorKeysContractArgs,
25-
CreatorKeysContractClient,
24+
constants, read_creator_supply, read_registered_creator_profile, CreatorKeysContract,
25+
CreatorKeysContractArgs, CreatorKeysContractClient,
2626
};
2727
use soroban_sdk::{
2828
contracterror, contractimpl, contracttype, symbol_short, Address, Env, String, Symbol, Vec,
2929
};
3030

31+
/// Event name for protocol trade fee collected on a buy or sell.
32+
pub const FEE_COLLECTED_EVENT_NAME: Symbol = symbol_short!("fee_coll");
33+
34+
/// Event name for a sell rejected by the anti-flash-trade lockup window.
35+
pub const LOCKUP_BLOCKED_EVENT_NAME: Symbol = symbol_short!("lck_blk");
36+
37+
/// Event name for quorum threshold update.
38+
pub const QUORUM_UPDATED_EVENT_NAME: Symbol = symbol_short!("qrm_upd");
39+
40+
/// Event name for proposal/poll closed.
41+
pub const POLL_CLOSED_EVENT_NAME: Symbol = symbol_short!("poll_cls");
42+
3143
/// Event name for protocol pause.
3244
pub const PAUSE_EVENT_NAME: Symbol = symbol_short!("pause");
3345

@@ -742,94 +754,66 @@ pub fn keys_burned_topics(key_id: &Address) -> (Symbol, Address) {
742754
(KEYS_BURNED_EVENT_NAME, key_id.clone())
743755
}
744756

745-
// --- Co-creator removal, auction, and staking reward events ---
746-
747-
/// Event name for co-creator removal.
748-
pub const CO_CREATOR_REMOVED_EVENT_NAME: Symbol = symbol_short!("co_rem");
749-
750-
/// Event name for auction configuration.
751-
pub const AUCTION_CONFIGURED_EVENT_NAME: Symbol = symbol_short!("auc_cfg");
752-
753-
/// Event name for auction cancellation.
754-
pub const AUCTION_CANCELLED_EVENT_NAME: Symbol = symbol_short!("auc_can");
755-
756-
/// Event name for a purchase made during a creator's auction phase.
757-
pub const AUCTION_PURCHASE_EVENT_NAME: Symbol = symbol_short!("auc_buy");
758-
759-
/// Event name for a staking reward claim.
760-
pub const STAKE_REWARD_CLAIMED_EVENT_NAME: Symbol = symbol_short!("stk_clm");
761-
762757
#[derive(Clone, Debug, Eq, PartialEq)]
763758
#[contracttype]
764-
pub struct CoCreatorRemovedEvent {
765-
pub creator_id: Address,
766-
pub co_creator: Address,
759+
pub struct FeeCollectedEvent {
760+
/// Treasury address that received the fee.
761+
pub treasury: Address,
762+
/// Fee amount deducted from the trade.
763+
pub amount: i128,
764+
/// Ledger sequence number at the time of the trade.
767765
pub ledger: u32,
768766
}
769767

770-
pub fn co_creator_removed_topics(creator: &Address) -> (Symbol, Address) {
771-
(CO_CREATOR_REMOVED_EVENT_NAME, creator.clone())
772-
}
773-
774-
#[derive(Clone, Debug, Eq, PartialEq)]
775-
#[contracttype]
776-
pub struct AuctionConfiguredEvent {
777-
pub creator_id: Address,
778-
pub auction_price: i128,
779-
pub auction_supply: u32,
780-
}
781-
782-
pub fn auction_configured_topics(creator: &Address) -> (Symbol, Address) {
783-
(AUCTION_CONFIGURED_EVENT_NAME, creator.clone())
768+
/// Shared fee collected event topics tuple.
769+
pub fn fee_collected_topics(treasury: &Address) -> (Symbol, Address) {
770+
(FEE_COLLECTED_EVENT_NAME, treasury.clone())
784771
}
785772

786773
#[derive(Clone, Debug, Eq, PartialEq)]
787774
#[contracttype]
788-
pub struct AuctionCancelledEvent {
775+
pub struct LockupBlockedEvent {
776+
/// Creator whose keys the seller attempted to sell.
789777
pub creator_id: Address,
790-
pub auction_price: i128,
791-
pub auction_supply: u32,
778+
/// Seller whose sale was rejected.
779+
pub seller: Address,
780+
/// Ledger timestamp of the seller's most recent buy.
781+
pub last_buy_timestamp: u64,
782+
/// Timestamp at which the lockup expires (exclusive).
783+
pub unlock_at: u64,
784+
/// Ledger timestamp at rejection.
785+
pub current_timestamp: u64,
792786
}
793787

794-
pub fn auction_cancelled_topics(creator: &Address) -> (Symbol, Address) {
795-
(AUCTION_CANCELLED_EVENT_NAME, creator.clone())
788+
/// Shared lockup blocked event topics tuple.
789+
pub fn lockup_blocked_topics(creator: &Address, seller: &Address) -> (Symbol, Address, Address) {
790+
(LOCKUP_BLOCKED_EVENT_NAME, creator.clone(), seller.clone())
796791
}
797792

798793
#[derive(Clone, Debug, Eq, PartialEq)]
799794
#[contracttype]
800-
pub struct AuctionPurchaseEvent {
801-
pub buyer: Address,
802-
pub creator_id: Address,
803-
pub quantity: u32,
804-
pub price_paid: i128,
805-
pub new_supply: u32,
806-
pub auction_sold: u32,
795+
pub struct QuorumUpdatedEvent {
796+
pub creator: Address,
797+
pub quorum_bps: u32,
807798
pub ledger: u32,
808799
}
809800

810-
pub fn auction_purchase_topics(creator: &Address, buyer: &Address) -> (Symbol, Address, Address) {
811-
(AUCTION_PURCHASE_EVENT_NAME, creator.clone(), buyer.clone())
801+
pub fn quorum_updated_topics(creator: &Address) -> (Symbol, Address) {
802+
(QUORUM_UPDATED_EVENT_NAME, creator.clone())
812803
}
813804

814805
#[derive(Clone, Debug, Eq, PartialEq)]
815806
#[contracttype]
816-
pub struct StakeRewardClaimedEvent {
817-
pub wallet: Address,
818-
pub key_id: Address,
819-
pub quantity_unlocked: u32,
820-
pub reward_amount: i128,
807+
pub struct PollClosedEvent {
808+
pub creator_id: Address,
809+
pub poll_id: u32,
810+
pub total_weight: u32,
811+
pub quorum_reached: bool,
821812
pub ledger: u32,
822813
}
823814

824-
pub fn stake_reward_claimed_topics(
825-
creator: &Address,
826-
wallet: &Address,
827-
) -> (Symbol, Address, Address) {
828-
(
829-
STAKE_REWARD_CLAIMED_EVENT_NAME,
830-
creator.clone(),
831-
wallet.clone(),
832-
)
815+
pub fn poll_closed_topics(creator: &Address, poll_id: u32) -> (Symbol, Address, u32) {
816+
(POLL_CLOSED_EVENT_NAME, creator.clone(), poll_id)
833817
}
834818

835819
#[contracterror]
@@ -845,6 +829,11 @@ pub enum PollError {
845829
PollExpired = 26,
846830
NotAHolder = 27,
847831
InvalidOption = 28,
832+
QuorumNotReached = 29,
833+
QuorumTooHigh = 30,
834+
QuorumTooLow = 31,
835+
Unauthorized = 32,
836+
AlreadyClosed = 33,
848837
}
849838

850839
#[derive(Clone)]
@@ -855,31 +844,33 @@ pub enum PollDataKey {
855844
Vote(Address, u32, Address),
856845
}
857846

858-
#[derive(Clone)]
847+
#[derive(Clone, Debug, Eq, PartialEq)]
859848
#[contracttype]
860849
pub struct Poll {
861850
pub question: String,
862851
pub options: Vec<String>,
863852
pub vote_counts: Vec<u32>,
864853
pub total_weight: u32,
865854
pub expires_at: u32,
855+
pub closed: bool,
866856
}
867857

868-
#[derive(Clone)]
858+
#[derive(Clone, Debug, Eq, PartialEq)]
869859
#[contracttype]
870860
pub struct PollVote {
871861
pub option_index: u32,
872862
pub weight: u32,
873863
}
874864

875-
#[derive(Clone)]
865+
#[derive(Clone, Debug, Eq, PartialEq)]
876866
#[contracttype]
877867
pub struct PollResult {
878868
pub question: String,
879869
pub options: Vec<String>,
880870
pub vote_counts: Vec<u32>,
881871
pub total_weight: u32,
882872
pub expired: bool,
873+
pub closed: bool,
883874
}
884875

885876
pub fn poll_storage_key(creator_id: &Address, poll_id: u32) -> PollDataKey {
@@ -962,6 +953,7 @@ impl CreatorKeysContract {
962953
vote_counts,
963954
total_weight: 0,
964955
expires_at,
956+
closed: false,
965957
};
966958

967959
env.storage()
@@ -991,6 +983,9 @@ impl CreatorKeysContract {
991983
voter.require_auth();
992984
let mut poll = read_poll(&env, &creator_id, poll_id)?;
993985

986+
if poll.closed {
987+
return Err(PollError::AlreadyClosed);
988+
}
994989
if is_poll_expired(&env, &poll) {
995990
return Err(PollError::PollExpired);
996991
}
@@ -1070,8 +1065,81 @@ impl CreatorKeysContract {
10701065
vote_counts: poll.vote_counts,
10711066
total_weight: poll.total_weight,
10721067
expired,
1068+
closed: poll.closed,
1069+
})
1070+
}
1071+
1072+
/// Closes a creator poll if the configured quorum threshold has been reached.
1073+
///
1074+
/// Computes participation as `total_voting_weight / circulating_supply` (in basis points)
1075+
/// against the creator's configured `quorum_bps`. If participation is below the quorum
1076+
/// threshold, returns `Err(PollError::QuorumNotReached)`.
1077+
pub fn close_poll(
1078+
env: Env,
1079+
creator_id: Address,
1080+
poll_id: u32,
1081+
) -> Result<PollResult, PollError> {
1082+
let mut poll = read_poll(&env, &creator_id, poll_id)?;
1083+
1084+
if poll.closed {
1085+
return Err(PollError::AlreadyClosed);
1086+
}
1087+
1088+
let circulating_supply = read_creator_supply(&env, &creator_id);
1089+
let quorum_key = constants::storage::quorum_bps(&creator_id);
1090+
let quorum_bps: u32 = env.storage().persistent().get(&quorum_key).unwrap_or(0);
1091+
1092+
if quorum_bps > 0 {
1093+
if circulating_supply == 0 {
1094+
return Err(PollError::QuorumNotReached);
1095+
}
1096+
let total_weight_bps = (poll.total_weight as u128)
1097+
.checked_mul(10_000)
1098+
.ok_or(PollError::Overflow)?;
1099+
let required_bps = (circulating_supply as u128)
1100+
.checked_mul(quorum_bps as u128)
1101+
.ok_or(PollError::Overflow)?;
1102+
1103+
if total_weight_bps < required_bps {
1104+
return Err(PollError::QuorumNotReached);
1105+
}
1106+
}
1107+
1108+
poll.closed = true;
1109+
env.storage()
1110+
.persistent()
1111+
.set(&poll_storage_key(&creator_id, poll_id), &poll);
1112+
1113+
env.events().publish(
1114+
poll_closed_topics(&creator_id, poll_id),
1115+
PollClosedEvent {
1116+
creator_id: creator_id.clone(),
1117+
poll_id,
1118+
total_weight: poll.total_weight,
1119+
quorum_reached: true,
1120+
ledger: env.ledger().sequence(),
1121+
},
1122+
);
1123+
1124+
let expired = is_poll_expired(&env, &poll);
1125+
Ok(PollResult {
1126+
question: poll.question,
1127+
options: poll.options,
1128+
vote_counts: poll.vote_counts,
1129+
total_weight: poll.total_weight,
1130+
expired,
1131+
closed: true,
10731132
})
10741133
}
1134+
1135+
/// Alias for `close_poll`.
1136+
pub fn close_proposal(
1137+
env: Env,
1138+
creator_id: Address,
1139+
poll_id: u32,
1140+
) -> Result<PollResult, PollError> {
1141+
Self::close_poll(env, creator_id, poll_id)
1142+
}
10751143
}
10761144

10771145
/// Event name for batch buy completion.

0 commit comments

Comments
 (0)