diff --git a/packages/circle/src/contract.rs b/packages/circle/src/contract.rs index 0be5c3d..48eb347 100644 --- a/packages/circle/src/contract.rs +++ b/packages/circle/src/contract.rs @@ -2,13 +2,13 @@ use soroban_sdk::{Address,BytesN,Env,Vec,Map,symbol_short};use crate::types::*;u pub fn init(env:&Env,admin:&Address,factory:&Address,config:&CircleConfig)->Result<(),CircleError>{if config.max_members<2||config.contribution_amount<=0||config.total_rounds==0||config.payout_type>3{return Err(CircleError::InvalidAmount);}if config.max_members>scoring::max_circle_size(env,&config.organizer){return Err(CircleError::CircleSizeExceedsTier);}if config.contribution_amount>scoring::max_contribution(env,&config.organizer){return Err(CircleError::ContributionExceedsTier);}let circle=Circle{id:env.current_contract_address(),token:config.token.clone(),name:config.name.clone(),organizer:config.organizer.clone(),factory:factory.clone(),contribution_amount:config.contribution_amount,max_members:config.max_members,member_count:0,payout_type:config.payout_type,total_rounds:config.total_rounds,current_round:0,status:STATUS_PENDING,started_at:0,created_at:env.ledger().timestamp(),contribution_deadline_seconds:config.contribution_deadline_seconds,min_moi_score:config.min_moi_score,collateral_amount:config.collateral_amount,penalty_bps:config.penalty_bps,grace_period_seconds:config.grace_period_seconds,max_strikes:config.max_strikes,payout_bitmap:0,total_payouts:0,total_fees:0,slug:config.slug.clone()};env.storage().instance().set(&DataKey::Circle,&circle);env.storage().instance().set(&DataKey::Admin,admin);env.storage().instance().set(&DataKey::Factory,factory);env.storage().persistent().set(&DataKey::Members,&Vec::::new(env));env.storage().persistent().set(&DataKey::Contributions,&Vec::::new(env));env.storage().persistent().set(&DataKey::Payouts,&Vec::::new(env));env.storage().persistent().set(&DataKey::Bids,&Vec::::new(env));env.storage().persistent().set(&DataKey::Votes,&Vec::::new(env));Ok(())} pub fn join(env:&Env,member:&Address)->Result<(),CircleError>{pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;let _guard = ReentrancyGuard::new(env).map_err(|_| CircleError::NotActive)?;member.require_auth();let mut circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;if circle.status==STATUS_DISPUTED||circle.status==STATUS_COMPLETED{return Err(CircleError::NotActive);}let allowlist=get_allowlist(env);if allowlist.len()>0{let mut permitted=false;for i in 0..allowlist.len(){if allowlist.get(i).ok_or(CircleError::VecAccessError)?==*member{permitted=true;break;}}if !permitted{return Err(CircleError::AllowlistNotPermitted);}}let score = scoring::get_score(env, member);if score < circle.min_moi_score {return Err(CircleError::InsufficientMoiScore);}let mut members:Vec=env.storage().persistent().get(&DataKey::Members).unwrap_or_else(||Vec::new(env));for i in 0..members.len(){if members.get(i).ok_or(CircleError::VecAccessError)?.address==*member{return Err(CircleError::AlreadyMember);}}if members.len()as u32>=circle.max_members{return Err(CircleError::CircleFull);}if circle.collateral_amount>0{let token_client=soroban_sdk::token::Client::new(env,&circle.token);token_client.transfer(member,&circle.id,&circle.collateral_amount);}let now=env.ledger().timestamp();let pos=members.len()as u32;members.push_back(Member{address:member.clone(),position:pos,joined_at:now,strikes:0,status:MEMBER_ACTIVE,exited_at:0,total_contributions:0,total_received:0});circle.member_count=circle.member_count.wrapping_add(1);if circle.member_count>=circle.max_members&&circle.status==STATUS_PENDING{circle.status=STATUS_ACTIVE;circle.started_at=now;}env.storage().instance().set(&DataKey::Circle,&circle);env.storage().persistent().set(&DataKey::Members,&members);env.events().publish((env.current_contract_address(),symbol_short!("joined")),MemberJoined{member:member.clone(),position:pos});Ok(())} pub fn contribute(env:&Env,member:&Address,amount:i128,round:u32)->Result<(),CircleError>{pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;let _guard = ReentrancyGuard::new(env).map_err(|_| CircleError::NotActive)?;member.require_auth();let circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;if circle.status!=STATUS_ACTIVE{return Err(CircleError::NotActive);}if round!=circle.current_round{return Err(CircleError::RoundNotCurrent);}if amount<=0{return Err(CircleError::InvalidAmount);}if amount>circle.contribution_amount{return Err(CircleError::ContributionMismatch);}if amount>scoring::max_contribution(env,member){return Err(CircleError::ContributionExceedsTier);}let mut members:Vec=env.storage().persistent().get(&DataKey::Members).ok_or(CircleError::NotInitialized)?;let mut member_index:Option=None;for i in 0..members.len(){let m=members.get(i).ok_or(CircleError::VecAccessError)?;if m.address==*member{if m.status!=MEMBER_ACTIVE{return Err(CircleError::InvalidMemberStatus);}member_index=Some(i);break;}}if member_index.is_none(){return Err(CircleError::NotMember);}let mut contributions:Vec=env.storage().persistent().get(&DataKey::Contributions).unwrap_or_else(||Vec::new(env));let mut contributed_for_round:i128=0;for i in 0..contributions.len(){let c=contributions.get(i).ok_or(CircleError::VecAccessError)?;if c.member==*member&&c.round==round{contributed_for_round=math::safe_add(contributed_for_round,c.amount).map_err(|_|CircleError::InvalidAmount)?;}}if contributed_for_round>=circle.contribution_amount{return Err(CircleError::AlreadyContributed);}let remaining_required=math::safe_sub(circle.contribution_amount,contributed_for_round).map_err(|_|CircleError::InvalidAmount)?;if amount!=remaining_required{return Err(CircleError::ContributionMismatch);}let token_client=soroban_sdk::token::Client::new(env,&circle.token);token_client.transfer(member,&circle.id,&amount);let now=env.ledger().timestamp();let on_time=now<=circle.started_at.wrapping_add(circle.contribution_deadline_seconds);contributions.push_back(Contribution{member:member.clone(),round,amount,timestamp:now,on_time,time_weight:now});env.storage().persistent().set(&DataKey::Contributions,&contributions);if let Some(i)=member_index{let mut stored_member=members.get(i).ok_or(CircleError::VecAccessError)?;stored_member.total_contributions=math::safe_add(stored_member.total_contributions,amount).map_err(|_|CircleError::InvalidAmount)?;members.set(i,stored_member);env.storage().persistent().set(&DataKey::Members,&members);}env.events().publish((env.current_contract_address(),symbol_short!("contrib")),ContributionRecorded{member:member.clone(),round,amount,on_time});scoring::record_on_time_payment(env,member,&circle.id,amount);Ok(())} -pub fn trigger_payout(env:&Env,caller:&Address,round:u32)->Result<(),CircleError>{pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;let _guard = ReentrancyGuard::new(env).map_err(|_| CircleError::NotActive)?;let mut circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;let stored_admin:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if caller!=&circle.organizer&&caller!=&stored_admin{return Err(CircleError::Unauthorized);}if circle.status!=STATUS_ACTIVE{return Err(CircleError::NotActive);}if round!=circle.current_round{return Err(CircleError::RoundNotCurrent);}let members_check:Vec=env.storage().persistent().get(&DataKey::Members).ok_or(CircleError::NotInitialized)?;let contribs_check:Vec=env.storage().persistent().get(&DataKey::Contributions).unwrap_or_else(||Vec::new(env));for i in 0..members_check.len(){let m=members_check.get(i).ok_or(CircleError::VecAccessError)?;if m.status==MEMBER_ACTIVE{let mut contributed=false;for j in 0..contribs_check.len(){let c=contribs_check.get(j).ok_or(CircleError::VecAccessError)?;if c.member==m.address&&c.round==round{contributed=true;break;}}if!contributed{return Err(CircleError::ContributionMismatch);}}}let(recipient,payout_type)=match circle.payout_type{PAYOUT_RANDOM=>(payout::resolve_random(env,&circle,round)?,PAYOUT_RANDOM),PAYOUT_FIXED=>(payout::resolve_fixed(env,&circle,round)?,PAYOUT_FIXED),PAYOUT_AUCTION=>{let(w,_)=payout::resolve_auction(env,&circle,round)?;(w,PAYOUT_AUCTION)}PAYOUT_VOTE=>(payout::resolve_vote(env,&circle,round)?,PAYOUT_VOTE),_=>return Err(CircleError::InvalidPayoutType)};let pool=math::safe_mul(circle.contribution_amount,circle.member_count as i128).map_err(|_|CircleError::InvalidAmount)?;let fee_bps:u32=env.storage().instance().get(&DataKey::FeeBps).unwrap_or(0u32);let(net,fee)=math::apply_fee(pool,fee_bps as i128).map_err(|_|CircleError::InvalidAmount)?;let token_client=soroban_sdk::token::Client::new(env,&circle.token);let now=env.ledger().timestamp();let all_contributions:Vec=env.storage().persistent().get(&DataKey::Contributions).unwrap_or_else(||Vec::new(env));let mut total_weighted:u128=0;let mut member_weighted:Map=Map::new(env);for i in 0..all_contributions.len(){let c=all_contributions.get(i).ok_or(CircleError::VecAccessError)?;if c.round==round{let time_held=(now as u128).saturating_sub(c.time_weight as u128);let w=(c.amount as u128).saturating_mul(time_held);total_weighted=total_weighted.saturating_add(w);let prev=member_weighted.get(c.member.clone()).unwrap_or(0);member_weighted.set(c.member.clone(),prev.saturating_add(w));}}let mut payouts:Vec=env.storage().persistent().get(&DataKey::Payouts).unwrap_or_else(||Vec::new(env));let mut members:Vec=env.storage().persistent().get(&DataKey::Members).ok_or(CircleError::NotInitialized)?;let mut distributed:i128=0;let net_u=net as u128;for i in 0..members.len(){let m=members.get(i).ok_or(CircleError::VecAccessError)?;if let Some(w)=member_weighted.get(m.address.clone()){if total_weighted>0{let share=if distributed==0&&w==total_weighted{net}else{(net_u.saturating_mul(w)/total_weighted)as i128};if share>0{token_client.transfer(&circle.id,&m.address,&share);distributed=math::safe_add(distributed,share).map_err(|_|CircleError::InvalidAmount)?;payouts.push_back(PayoutRecipient{recipient:m.address.clone(),round,amount:share,fee:0,payout_type,timestamp:now});let mut found=false;for j in 0..members.len(){let mut m2=members.get(j).ok_or(CircleError::VecAccessError)?;if m2.address==m.address{m2.total_received=math::safe_add(m2.total_received,share).map_err(|_|CircleError::InvalidAmount)?;members.set(j,m2);found=true;}}}}}}if fee>0&&distributed>0{if let Some(treasury)=env.storage().instance().get::(&DataKey::Treasury){token_client.transfer(&circle.id,&treasury,&fee);}}if distributed=circle.total_rounds{circle.status=STATUS_COMPLETED;}env.storage().instance().set(&DataKey::Circle,&circle);env.storage().persistent().set(&DataKey::Payouts,&payouts);env.storage().persistent().set(&DataKey::Members,&members);env.events().publish((env.current_contract_address(),symbol_short!("payout")),PayoutExecuted{recipient,round,amount:distributed,fee,payout_type});if circle.status==STATUS_COMPLETED{env.events().publish((env.current_contract_address(),symbol_short!("complete")),CircleCompleted{total_payouts:circle.total_payouts});if circle.collateral_amount>0{for i in 0..members.len(){let m=members.get(i).ok_or(CircleError::NotInitialized)?;if m.status==MEMBER_ACTIVE{token_client.transfer(&circle.id,&m.address,&circle.collateral_amount);}}}let final_members:Vec=env.storage().persistent().get(&DataKey::Members).ok_or(CircleError::NotInitialized)?;for i in 0..final_members.len(){let m=final_members.get(i).ok_or(CircleError::NotInitialized)?;if m.status==MEMBER_ACTIVE{scoring::record_circle_completion(env,&m.address);}}}Ok()} -pub fn auction_bid(env:&Env,bidder:&Address,discount_bips:u32,round:u32)->Result<(),CircleError>{pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;let _guard = ReentrancyGuard::new(env).map_err(|_| CircleError::NotActive)?;bidder.require_auth();let circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;if circle.payout_type!=PAYOUT_AUCTION{return Err(CircleError::InvalidPayoutType);}if discount_bips>10000{return Err(CircleError::InvalidBid);}if round!=circle.current_round{return Err(CircleError::RoundNotCurrent);}let mut bids:Vec=env.storage().persistent().get(&DataKey::Bids).unwrap_or_else(||Vec::new(env));for i in 0..bids.len(){let b=bids.get(i).ok_or(CircleError::VecAccessError)?;if b.bidder==*bidder&&b.round==round{return Err(CircleError::AlreadyBidded);}}bids.push_back(AuctionBid{bidder:bidder.clone(),discount_bips,round,timestamp:env.ledger().timestamp()});env.storage().persistent().set(&DataKey::Bids,&bids);env.events().publish((env.current_contract_address(),symbol_short!("bid")),AuctionBidPlaced{bidder:bidder.clone(),discount_bips,round});Ok()} -pub fn vote_payout(env:&Env,voter:&Address,vote_for:&Address,round:u32)->Result<(),CircleError>{pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;let _guard = ReentrancyGuard::new(env).map_err(|_| CircleError::NotActive)?;voter.require_auth();let circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;if circle.payout_type!=PAYOUT_VOTE{return Err(CircleError::InvalidPayoutType);}if round!=circle.current_round{return Err(CircleError::RoundNotCurrent);}let members:Vec=env.storage().persistent().get(&DataKey::Members).ok_or(CircleError::NotInitialized)?;let mut is_member=false;for i in 0..members.len(){let m=members.get(i).ok_or(CircleError::VecAccessError)?;if m.address==*voter{if m.status!=MEMBER_ACTIVE{return Err(CircleError::InvalidMemberStatus);}is_member=true;}}if!is_member{return Err(CircleError::NotMember);}let mut votes:Vec=env.storage().persistent().get(&DataKey::Votes).unwrap_or_else(||Vec::new(env));for i in 0..votes.len(){let v=votes.get(i).ok_or(CircleError::VecAccessError)?;if v.voter==*voter&&v.round==round{return Err(CircleError::AlreadyVoted);}}votes.push_back(VoteEntry{voter:voter.clone(),vote_for:vote_for.clone(),round,timestamp:env.ledger().timestamp()});env.storage().persistent().set(&DataKey::Votes,&votes);env.events().publish((env.current_contract_address(),symbol_short!("vote")),VoteCast{voter:voter.clone(),vote_for:vote_for.clone(),round});Ok()} -pub fn exit(env:&Env,member:&Address)->Result<(),CircleError>{pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;let _guard = ReentrancyGuard::new(env).map_err(|_| CircleError::NotActive)?;member.require_auth();let circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;if circle.status==STATUS_COMPLETED{return Err(CircleError::NotActive);}let mut members:Vec=env.storage().persistent().get(&DataKey::Members).ok_or(CircleError::NotInitialized)?;let mut penalty:i128=0;for i in 0..members.len(){let mut m=members.get(i).ok_or(CircleError::VecAccessError)?;if m.address==*member{if m.status!=MEMBER_ACTIVE{return Err(CircleError::InvalidMemberStatus);}let contributions:Vec=env.storage().persistent().get(&DataKey::Contributions).unwrap_or_else(||Vec::new(env));let mut ctotal:i128=0;for j in 0..contributions.len(){let c=contributions.get(j).ok_or(CircleError::VecAccessError)?;if c.member==*member{ctotal=math::safe_add(ctotal,c.amount).map_err(|_|CircleError::InvalidAmount)?;}}penalty=math::calculate_penalty(ctotal,circle.penalty_bps as i128).map_err(|_|CircleError::InvalidAmount)?;m.status=MEMBER_EXITED;m.exited_at=env.ledger().timestamp();members.set(i,m);}}env.storage().persistent().set(&DataKey::Members,&members);if circle.collateral_amount>0{let token_client=soroban_sdk::token::Client::new(env,&circle.token);token_client.transfer(&circle.id,member,&circle.collateral_amount);}env.events().publish((env.current_contract_address(),symbol_short!("exited")),MemberExited{member:member.clone(),penalty});Ok()} -pub fn report_late(env:&Env,reporter:&Address,late_member:&Address,round:u32)->Result<(),CircleError>{reporter.require_auth();pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;let _guard = ReentrancyGuard::new(env).map_err(|_| CircleError::NotActive)?;let circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;if circle.status!=STATUS_ACTIVE{return Err(CircleError::NotActive);}let members_check:Vec=env.storage().persistent().get(&DataKey::Members).ok_or(CircleError::NotInitialized)?;let mut reporter_active=false;for i in 0..members_check.len(){let m=members_check.get(i).ok_or(CircleError::VecAccessError)?;if m.address==*reporter{if m.status!=MEMBER_ACTIVE{return Err(CircleError::InvalidMemberStatus);}reporter_active=true;break;}}if !reporter_active{return Err(CircleError::NotMember);}let contributions:Vec=env.storage().persistent().get(&DataKey::Contributions).unwrap_or_else(||Vec::new(env));let mut found=false;for i in 0..contributions.len(){let c=contributions.get(i).ok_or(CircleError::VecAccessError)?;if c.member==*late_member&&c.round==round&&!c.on_time{found=true;}}if!found{return Err(CircleError::NotMember);}let mut members:Vec=env.storage().persistent().get(&DataKey::Members).ok_or(CircleError::NotInitialized)?;for i in 0..members.len(){let mut m=members.get(i).ok_or(CircleError::VecAccessError)?;if m.address==*late_member{m.strikes=m.strikes.wrapping_add(1);if m.strikes>=circle.max_strikes{m.status=MEMBER_DEFAULTED;scoring::record_default(env,&m.address);env.events().publish((env.current_contract_address(),symbol_short!("default")),MemberDefaulted{member:late_member.clone(),strikes:m.strikes});}members.set(i,m);}}env.storage().persistent().set(&DataKey::Members,&members);Ok()} -pub fn raise_dispute(env:&Env,member:&Address,evidence_hash:&BytesN<32>)->Result<(),CircleError>{pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;let _guard = ReentrancyGuard::new(env).map_err(|_| CircleError::NotActive)?;member.require_auth();let mut circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;if circle.status==STATUS_DISPUTED{return Err(CircleError::DisputeAlreadyRaised);}if env.storage().persistent().get::(&DataKey::Dispute).is_some(){return Err(CircleError::DisputeAlreadyRaised);}circle.status=STATUS_DISPUTED;env.storage().instance().set(&DataKey::Circle,&circle);env.storage().persistent().set(&DataKey::Dispute,&DisputeEntry{raised_by:member.clone(),evidence_hash:evidence_hash.clone(),raised_at:env.ledger().timestamp(),resolved_at:0,resolution:0,resolved_by:env.current_contract_address()});env.events().publish((env.current_contract_address(),symbol_short!("disputed")),DisputeRaised{member:member.clone(),evidence_hash:evidence_hash.clone()});Ok()} -pub fn resolve_dispute(env:&Env,admin:&Address,resolution:u32)->Result<(),CircleError>{admin.require_auth();let s:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if admin!=&s{return Err(CircleError::Unauthorized);}let mut circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;let mut dispute:DisputeEntry=env.storage().persistent().get(&DataKey::Dispute).ok_or(CircleError::NoActiveDispute)?;if resolution>RESOLVE_FORCE_PAYOUT{return Err(CircleError::InvalidAmount);}match resolution{RESOLVE_DISMISS|RESOLVE_PENALIZE|RESOLVE_FORCE_PAYOUT=>circle.status=STATUS_ACTIVE,_=>return Err(CircleError::InvalidAmount)}dispute.resolved_at=env.ledger().timestamp();dispute.resolution=resolution;dispute.resolved_by=admin.clone();env.storage().instance().set(&DataKey::Circle,&circle);env.storage().persistent().set(&DataKey::Dispute,&dispute);Ok()} +pub fn trigger_payout(env:&Env,caller:&Address,round:u32)->Result<(),CircleError>{pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;let _guard = ReentrancyGuard::new(env).map_err(|_| CircleError::NotActive)?;let mut circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;let stored_admin:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if caller!=&circle.organizer&&caller!=&stored_admin{return Err(CircleError::Unauthorized);}if circle.status!=STATUS_ACTIVE{return Err(CircleError::NotActive);}if round!=circle.current_round{return Err(CircleError::RoundNotCurrent);}let members_check:Vec=env.storage().persistent().get(&DataKey::Members).ok_or(CircleError::NotInitialized)?;let contribs_check:Vec=env.storage().persistent().get(&DataKey::Contributions).unwrap_or_else(||Vec::new(env));for i in 0..members_check.len(){let m=members_check.get(i).ok_or(CircleError::VecAccessError)?;if m.status==MEMBER_ACTIVE{let mut contributed=false;for j in 0..contribs_check.len(){let c=contribs_check.get(j).ok_or(CircleError::VecAccessError)?;if c.member==m.address&&c.round==round{contributed=true;break;}}if!contributed{return Err(CircleError::ContributionMismatch);}}}let(recipient,payout_type)=match circle.payout_type{PAYOUT_RANDOM=>(payout::resolve_random(env,&circle,round)?,PAYOUT_RANDOM),PAYOUT_FIXED=>(payout::resolve_fixed(env,&circle,round)?,PAYOUT_FIXED),PAYOUT_AUCTION=>{let(w,_)=payout::resolve_auction(env,&circle,round)?;(w,PAYOUT_AUCTION)}PAYOUT_VOTE=>(payout::resolve_vote(env,&circle,round)?,PAYOUT_VOTE),_=>return Err(CircleError::InvalidPayoutType)};let pool=math::safe_mul(circle.contribution_amount,circle.member_count as i128).map_err(|_|CircleError::InvalidAmount)?;let fee_bps:u32=env.storage().instance().get(&DataKey::FeeBps).unwrap_or(0u32);let(net,fee)=math::apply_fee(pool,fee_bps as i128).map_err(|_|CircleError::InvalidAmount)?;let token_client=soroban_sdk::token::Client::new(env,&circle.token);let now=env.ledger().timestamp();let all_contributions:Vec=env.storage().persistent().get(&DataKey::Contributions).unwrap_or_else(||Vec::new(env));let mut total_weighted:u128=0;let mut member_weighted:Map=Map::new(env);for i in 0..all_contributions.len(){let c=all_contributions.get(i).ok_or(CircleError::VecAccessError)?;if c.round==round{let time_held=(now as u128).saturating_sub(c.time_weight as u128);let w=(c.amount as u128).saturating_mul(time_held);total_weighted=total_weighted.saturating_add(w);let prev=member_weighted.get(c.member.clone()).unwrap_or(0);member_weighted.set(c.member.clone(),prev.saturating_add(w));}}let mut payouts:Vec=env.storage().persistent().get(&DataKey::Payouts).unwrap_or_else(||Vec::new(env));let mut members:Vec=env.storage().persistent().get(&DataKey::Members).ok_or(CircleError::NotInitialized)?;let mut distributed:i128=0;let net_u=net as u128;for i in 0..members.len(){let m=members.get(i).ok_or(CircleError::VecAccessError)?;if let Some(w)=member_weighted.get(m.address.clone()){if total_weighted>0{let share=if distributed==0&&w==total_weighted{net}else{(net_u.saturating_mul(w)/total_weighted)as i128};if share>0{token_client.transfer(&circle.id,&m.address,&share);distributed=math::safe_add(distributed,share).map_err(|_|CircleError::InvalidAmount)?;payouts.push_back(PayoutRecipient{recipient:m.address.clone(),round,amount:share,fee:0,payout_type,timestamp:now});let mut found=false;for j in 0..members.len(){let mut m2=members.get(j).ok_or(CircleError::VecAccessError)?;if m2.address==m.address{m2.total_received=math::safe_add(m2.total_received,share).map_err(|_|CircleError::InvalidAmount)?;members.set(j,m2);found=true;}}}}}}if fee>0&&distributed>0{if let Some(treasury)=env.storage().instance().get::(&DataKey::Treasury){token_client.transfer(&circle.id,&treasury,&fee);}}if distributed=circle.total_rounds{circle.status=STATUS_COMPLETED;}env.storage().instance().set(&DataKey::Circle,&circle);env.storage().persistent().set(&DataKey::Payouts,&payouts);env.storage().persistent().set(&DataKey::Members,&members);env.events().publish((env.current_contract_address(),symbol_short!("payout")),PayoutExecuted{recipient,round,amount:distributed,fee,payout_type});if circle.status==STATUS_COMPLETED{env.events().publish((env.current_contract_address(),symbol_short!("complete")),CircleCompleted{total_payouts:circle.total_payouts});if circle.collateral_amount>0{for i in 0..members.len(){let m=members.get(i).ok_or(CircleError::NotInitialized)?;if m.status==MEMBER_ACTIVE{token_client.transfer(&circle.id,&m.address,&circle.collateral_amount);}}}let final_members:Vec=env.storage().persistent().get(&DataKey::Members).ok_or(CircleError::NotInitialized)?;for i in 0..final_members.len(){let m=final_members.get(i).ok_or(CircleError::NotInitialized)?;if m.status==MEMBER_ACTIVE{scoring::record_circle_completion(env,&m.address);}}}Ok(())} +pub fn auction_bid(env:&Env,bidder:&Address,discount_bips:u32,round:u32)->Result<(),CircleError>{pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;let _guard = ReentrancyGuard::new(env).map_err(|_| CircleError::NotActive)?;bidder.require_auth();let circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;if circle.payout_type!=PAYOUT_AUCTION{return Err(CircleError::InvalidPayoutType);}if discount_bips>10000{return Err(CircleError::InvalidBid);}if round!=circle.current_round{return Err(CircleError::RoundNotCurrent);}let mut bids:Vec=env.storage().persistent().get(&DataKey::Bids).unwrap_or_else(||Vec::new(env));for i in 0..bids.len(){let b=bids.get(i).ok_or(CircleError::VecAccessError)?;if b.bidder==*bidder&&b.round==round{return Err(CircleError::AlreadyBidded);}}bids.push_back(AuctionBid{bidder:bidder.clone(),discount_bips,round,timestamp:env.ledger().timestamp()});env.storage().persistent().set(&DataKey::Bids,&bids);env.events().publish((env.current_contract_address(),symbol_short!("bid")),AuctionBidPlaced{bidder:bidder.clone(),discount_bips,round});Ok(())} +pub fn vote_payout(env:&Env,voter:&Address,vote_for:&Address,round:u32)->Result<(),CircleError>{pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;let _guard = ReentrancyGuard::new(env).map_err(|_| CircleError::NotActive)?;voter.require_auth();let circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;if circle.payout_type!=PAYOUT_VOTE{return Err(CircleError::InvalidPayoutType);}if round!=circle.current_round{return Err(CircleError::RoundNotCurrent);}let members:Vec=env.storage().persistent().get(&DataKey::Members).ok_or(CircleError::NotInitialized)?;let mut is_member=false;for i in 0..members.len(){let m=members.get(i).ok_or(CircleError::VecAccessError)?;if m.address==*voter{if m.status!=MEMBER_ACTIVE{return Err(CircleError::InvalidMemberStatus);}is_member=true;}}if!is_member{return Err(CircleError::NotMember);}let mut votes:Vec=env.storage().persistent().get(&DataKey::Votes).unwrap_or_else(||Vec::new(env));for i in 0..votes.len(){let v=votes.get(i).ok_or(CircleError::VecAccessError)?;if v.voter==*voter&&v.round==round{return Err(CircleError::AlreadyVoted);}}votes.push_back(VoteEntry{voter:voter.clone(),vote_for:vote_for.clone(),round,timestamp:env.ledger().timestamp()});env.storage().persistent().set(&DataKey::Votes,&votes);env.events().publish((env.current_contract_address(),symbol_short!("vote")),VoteCast{voter:voter.clone(),vote_for:vote_for.clone(),round});Ok(())} +pub fn exit(env:&Env,member:&Address)->Result<(),CircleError>{pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;let _guard = ReentrancyGuard::new(env).map_err(|_| CircleError::NotActive)?;member.require_auth();let circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;if circle.status==STATUS_COMPLETED{return Err(CircleError::NotActive);}let mut members:Vec=env.storage().persistent().get(&DataKey::Members).ok_or(CircleError::NotInitialized)?;let mut penalty:i128=0;for i in 0..members.len(){let mut m=members.get(i).ok_or(CircleError::VecAccessError)?;if m.address==*member{if m.status!=MEMBER_ACTIVE{return Err(CircleError::InvalidMemberStatus);}let contributions:Vec=env.storage().persistent().get(&DataKey::Contributions).unwrap_or_else(||Vec::new(env));let mut ctotal:i128=0;for j in 0..contributions.len(){let c=contributions.get(j).ok_or(CircleError::VecAccessError)?;if c.member==*member{ctotal=math::safe_add(ctotal,c.amount).map_err(|_|CircleError::InvalidAmount)?;}}penalty=math::calculate_penalty(ctotal,circle.penalty_bps as i128).map_err(|_|CircleError::InvalidAmount)?;m.status=MEMBER_EXITED;m.exited_at=env.ledger().timestamp();members.set(i,m);}}env.storage().persistent().set(&DataKey::Members,&members);if circle.collateral_amount>0{let token_client=soroban_sdk::token::Client::new(env,&circle.token);token_client.transfer(&circle.id,member,&circle.collateral_amount);}env.events().publish((env.current_contract_address(),symbol_short!("exited")),MemberExited{member:member.clone(),penalty});Ok(())} +pub fn report_late(env:&Env,reporter:&Address,late_member:&Address,round:u32)->Result<(),CircleError>{reporter.require_auth();pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;let _guard = ReentrancyGuard::new(env).map_err(|_| CircleError::NotActive)?;let circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;if circle.status!=STATUS_ACTIVE{return Err(CircleError::NotActive);}let members_check:Vec=env.storage().persistent().get(&DataKey::Members).ok_or(CircleError::NotInitialized)?;let mut reporter_active=false;for i in 0..members_check.len(){let m=members_check.get(i).ok_or(CircleError::VecAccessError)?;if m.address==*reporter{if m.status!=MEMBER_ACTIVE{return Err(CircleError::InvalidMemberStatus);}reporter_active=true;break;}}if !reporter_active{return Err(CircleError::NotMember);}let contributions:Vec=env.storage().persistent().get(&DataKey::Contributions).unwrap_or_else(||Vec::new(env));let mut found=false;for i in 0..contributions.len(){let c=contributions.get(i).ok_or(CircleError::VecAccessError)?;if c.member==*late_member&&c.round==round&&!c.on_time{found=true;}}if!found{return Err(CircleError::NotMember);}let mut members:Vec=env.storage().persistent().get(&DataKey::Members).ok_or(CircleError::NotInitialized)?;for i in 0..members.len(){let mut m=members.get(i).ok_or(CircleError::VecAccessError)?;if m.address==*late_member{m.strikes=m.strikes.wrapping_add(1);if m.strikes>=circle.max_strikes{m.status=MEMBER_DEFAULTED;scoring::record_default(env,&m.address);env.events().publish((env.current_contract_address(),symbol_short!("default")),MemberDefaulted{member:late_member.clone(),strikes:m.strikes});}members.set(i,m);}}env.storage().persistent().set(&DataKey::Members,&members);Ok(())} +pub fn raise_dispute(env:&Env,member:&Address,evidence_hash:&BytesN<32>)->Result<(),CircleError>{pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;let _guard = ReentrancyGuard::new(env).map_err(|_| CircleError::NotActive)?;member.require_auth();let mut circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;if circle.status==STATUS_DISPUTED{return Err(CircleError::DisputeAlreadyRaised);}if env.storage().persistent().get::(&DataKey::Dispute).is_some(){return Err(CircleError::DisputeAlreadyRaised);}circle.status=STATUS_DISPUTED;env.storage().instance().set(&DataKey::Circle,&circle);env.storage().persistent().set(&DataKey::Dispute,&DisputeEntry{raised_by:member.clone(),evidence_hash:evidence_hash.clone(),raised_at:env.ledger().timestamp(),resolved_at:0,resolution:0,resolved_by:env.current_contract_address()});env.events().publish((env.current_contract_address(),symbol_short!("disputed")),DisputeRaised{member:member.clone(),evidence_hash:evidence_hash.clone()});Ok(())} +pub fn resolve_dispute(env:&Env,admin:&Address,resolution:u32)->Result<(),CircleError>{admin.require_auth();let s:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if admin!=&s{return Err(CircleError::Unauthorized);}let mut circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;let mut dispute:DisputeEntry=env.storage().persistent().get(&DataKey::Dispute).ok_or(CircleError::NoActiveDispute)?;if resolution>RESOLVE_FORCE_PAYOUT{return Err(CircleError::InvalidAmount);}match resolution{RESOLVE_DISMISS|RESOLVE_PENALIZE|RESOLVE_FORCE_PAYOUT=>circle.status=STATUS_ACTIVE,_=>return Err(CircleError::InvalidAmount)}dispute.resolved_at=env.ledger().timestamp();dispute.resolution=resolution;dispute.resolved_by=admin.clone();env.storage().instance().set(&DataKey::Circle,&circle);env.storage().persistent().set(&DataKey::Dispute,&dispute);Ok(())} pub fn get_status(env:&Env)->Circle{env.storage().instance().get(&DataKey::Circle).unwrap_or(Circle{id:env.current_contract_address(),token:env.current_contract_address(),name:soroban_sdk::String::from_str(env,""),organizer:env.current_contract_address(),factory:env.current_contract_address(),contribution_amount:0,max_members:0,member_count:0,payout_type:0,total_rounds:0,current_round:0,status:3,started_at:0,created_at:0,contribution_deadline_seconds:0,min_moi_score:0,collateral_amount:0,penalty_bps:0,grace_period_seconds:0,max_strikes:0,payout_bitmap:0,total_payouts:0,total_fees:0,slug:soroban_sdk::String::from_str(env,"")})} pub fn get_members(env:&Env)->Vec{env.storage().persistent().get(&DataKey::Members).unwrap_or_else(||Vec::new(env))} pub fn get_contributions(env:&Env,member:&Address)->Vec{let all:Vec=env.storage().persistent().get(&DataKey::Contributions).unwrap_or_else(||Vec::new(env));let mut out=Vec::new(env);for i in 0..all.len(){if let Some(c)=all.get(i){if c.member==*member{out.push_back(c);}}}out} @@ -17,17 +17,18 @@ pub fn pause_circle(env:&Env,admin:&Address)->Result<(),CircleError>{let s:Addre pub fn unpause_circle(env:&Env,admin:&Address)->Result<(),CircleError>{let s:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if admin!=&s{return Err(CircleError::Unauthorized);}pause::unpause(env,admin).map_err(|_|CircleError::ContractPaused)} pub fn batch_invite(env:&Env,caller:&Address,members:&Vec
)->Result<(),CircleError>{pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;let _guard = ReentrancyGuard::new(env).map_err(|_| CircleError::NotActive)?;let circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;if caller!=&circle.organizer{return Err(CircleError::NotOrganizer);}if circle.status==STATUS_DISPUTED||circle.status==STATUS_COMPLETED{return Err(CircleError::NotActive);}let mut members_vec:Vec=env.storage().persistent().get(&DataKey::Members).unwrap_or_else(||Vec::new(env));for mi in 0..members.len(){let member=members.get(mi).ok_or(CircleError::VecAccessError)?;let score=scoring::get_score(env,&member);if score=circle.max_members{return Err(CircleError::CircleFull);}let now=env.ledger().timestamp();let pos=members_vec.len()as u32;members_vec.push_back(Member{address:member.clone(),position:pos,joined_at:now,strikes:0,status:MEMBER_ACTIVE,exited_at:0,total_contributions:0,total_received:0});}env.storage().persistent().set(&DataKey::Members,&members_vec);let circle_status=circle.status;let member_count=members_vec.len()as u32;let max_members=circle.max_members;let mut stored_circle=env.storage().instance().get::(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;stored_circle.member_count=member_count;if member_count>=max_members&&circle_status==STATUS_PENDING{stored_circle.status=STATUS_ACTIVE;stored_circle.started_at=env.ledger().timestamp();}env.storage().instance().set(&DataKey::Circle,&stored_circle);for mi in 0..members.len(){let member=members.get(mi).ok_or(CircleError::VecAccessError)?;let pos=members_vec.get(mi).map(|m| m.position).unwrap_or(0);env.events().publish((env.current_contract_address(),symbol_short!("joined")),MemberJoined{member:member.clone(),position:pos});}Ok(())} pub fn batch_payout(env:&Env,caller:&Address,recipients:&Vec
,amounts:&Vec,round:u32)->Result<(),CircleError>{pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;let _guard = ReentrancyGuard::new(env).map_err(|_| CircleError::NotActive)?;let circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;let stored_admin:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if caller!=&circle.organizer&&caller!=&stored_admin{return Err(CircleError::Unauthorized);}if circle.status!=STATUS_ACTIVE{return Err(CircleError::NotActive);}if round!=circle.current_round{return Err(CircleError::RoundNotCurrent);}if recipients.len()==0||recipients.len()>10||recipients.len()!=amounts.len(){return Err(CircleError::InvalidAmount);}let token_client=soroban_sdk::token::Client::new(env,&circle.token);let now=env.ledger().timestamp();let mut payouts:Vec=env.storage().persistent().get(&DataKey::Payouts).unwrap_or_else(||Vec::new(env));let mut members:Vec=env.storage().persistent().get(&DataKey::Members).ok_or(CircleError::NotInitialized)?;for i in 0..recipients.len(){let recipient=recipients.get(i).ok_or(CircleError::VecAccessError)?;let amount=amounts.get(i).ok_or(CircleError::VecAccessError)?;if amount<=0{return Err(CircleError::InvalidAmount);}token_client.transfer(&circle.id,&recipient,&amount);payouts.push_back(PayoutRecipient{recipient:recipient.clone(),round,amount,fee:0,payout_type:circle.payout_type,timestamp:now});for j in 0..members.len(){let mut member=members.get(j).ok_or(CircleError::VecAccessError)?;if member.address==recipient{member.total_received=math::safe_add(member.total_received,amount).map_err(|_|CircleError::InvalidAmount)?;members.set(j,member);break;}}env.events().publish((env.current_contract_address(),symbol_short!("payout")),PayoutExecuted{recipient,round,amount,fee:0,payout_type:circle.payout_type});}env.storage().persistent().set(&DataKey::Payouts,&payouts);env.storage().persistent().set(&DataKey::Members,&members);Ok(())} -pub fn register_referral(env:&Env,referrer:&Address,referred:&Address,bonus_pct:u32)->Result<(),CircleError>{pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;referrer.require_auth();if referrer==referred{return Err(CircleError::SelfReferral);}if bonus_pct>10000{return Err(CircleError::InvalidAmount);}let mut referrals:Vec=env.storage().persistent().get(&DataKey::Referrals).unwrap_or_else(||Vec::new(env));for i in 0..referrals.len(){let r=referrals.get(i).ok_or(CircleError::VecAccessError)?;if r.referrer==*referrer&&r.referred==*referred{return Err(CircleError::AlreadyMember);}}referrals.push_back(Referral{referrer:referrer.clone(),referred:referred.clone(),bonus_pct,timestamp:env.ledger().timestamp()});env.storage().persistent().set(&DataKey::Referrals,&referrals);env.events().publish((env.current_contract_address(),symbol_short!("referral")),ReferralRegistered{referrer:referrer.clone(),referred:referred.clone(),bonus_pct});Ok()} -pub fn claim_referral_bonus(env:&Env,referrer:&Address,_treasury:&Address)->Result<(),CircleError>{let token_address:Address=env.storage().instance().get(&DataKey::Token).ok_or(CircleError::NotInitialized)?;let token_client=soroban_sdk::token::Client::new(env,&token_address);let contract_balance=token_client.balance(&env.current_contract_address());if contract_balance<=0{return Err(CircleError::InsufficientContractBalance);}token_client.transfer(&env.current_contract_address(),referrer,&contract_balance);Ok()} +pub fn register_referral(env:&Env,referrer:&Address,referred:&Address,bonus_pct:u32)->Result<(),CircleError>{pause::when_not_paused(env).map_err(|_|CircleError::ContractPaused)?;referrer.require_auth();if referrer==referred{return Err(CircleError::SelfReferral);}if bonus_pct>10000{return Err(CircleError::InvalidAmount);}let mut referrals:Vec=env.storage().persistent().get(&DataKey::Referrals).unwrap_or_else(||Vec::new(env));for i in 0..referrals.len(){let r=referrals.get(i).ok_or(CircleError::VecAccessError)?;if r.referrer==*referrer&&r.referred==*referred{return Err(CircleError::AlreadyMember);}}referrals.push_back(Referral{referrer:referrer.clone(),referred:referred.clone(),bonus_pct,timestamp:env.ledger().timestamp()});env.storage().persistent().set(&DataKey::Referrals,&referrals);env.events().publish((env.current_contract_address(),symbol_short!("referral")),ReferralRegistered{referrer:referrer.clone(),referred:referred.clone(),bonus_pct});Ok(())} +pub fn claim_referral_bonus(env:&Env,referrer:&Address,_treasury:&Address)->Result<(),CircleError>{let token_address:Address=env.storage().instance().get(&DataKey::Token).ok_or(CircleError::NotInitialized)?;let token_client=soroban_sdk::token::Client::new(env,&token_address);let contract_balance=token_client.balance(&env.current_contract_address());if contract_balance<=0{return Err(CircleError::InsufficientContractBalance);}token_client.transfer(&env.current_contract_address(),referrer,&contract_balance);Ok(())} pub fn update_streak(env:&Env,_member:&Address,_round:u32)->Result<(),CircleError>{Err(CircleError::NotImplemented)} -pub fn claim_streak_bonus(env:&Env,member:&Address,_treasury:&Address)->Result<(),CircleError>{let token_address:Address=env.storage().instance().get(&DataKey::Token).ok_or(CircleError::NotInitialized)?;let token_client=soroban_sdk::token::Client::new(env,&token_address);let contract_balance=token_client.balance(&env.current_contract_address());if contract_balance<=0{return Err(CircleError::InsufficientContractBalance);}token_client.transfer(&env.current_contract_address(),member,&contract_balance);Ok()} +pub fn claim_streak_bonus(env:&Env,member:&Address,_treasury:&Address)->Result<(),CircleError>{let token_address:Address=env.storage().instance().get(&DataKey::Token).ok_or(CircleError::NotInitialized)?;let token_client=soroban_sdk::token::Client::new(env,&token_address);let contract_balance=token_client.balance(&env.current_contract_address());if contract_balance<=0{return Err(CircleError::InsufficientContractBalance);}token_client.transfer(&env.current_contract_address(),member,&contract_balance);Ok(())} pub fn get_referrals(env:&Env)->Vec{env.storage().persistent().get(&DataKey::Referrals).unwrap_or_else(||Vec::new(env))} pub fn get_streaks(env:&Env)->Vec{Vec::new(env)} pub fn get_member_streak(env:&Env,_member:&Address)->Streak{Streak{member:_member.clone(),current_streak:0,longest_streak:0,last_round:0}} -pub fn set_reputation_registry(env:&Env,admin:&Address,registry:&Address)->Result<(),CircleError>{let s:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if admin!=&s{return Err(CircleError::Unauthorized);}env.storage().instance().set(&DataKey::ReputationRegistry,registry);Ok()} +pub fn set_reputation_registry(env:&Env,admin:&Address,registry:&Address)->Result<(),CircleError>{let s:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if admin!=&s{return Err(CircleError::Unauthorized);}env.storage().instance().set(&DataKey::ReputationRegistry,registry);Ok(())} pub fn get_reputation_registry(env:&Env)->Option
{env.storage().instance().get(&DataKey::ReputationRegistry)} -pub fn set_treasury(env:&Env,admin:&Address,treasury:&Address)->Result<(),CircleError>{let s:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if admin!=&s{return Err(CircleError::Unauthorized);}env.storage().instance().set(&DataKey::Treasury,treasury);Ok()} -pub fn set_token(env:&Env,admin:&Address,token:&Address)->Result<(),CircleError>{let s:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if admin!=&s{return Err(CircleError::Unauthorized);}env.storage().instance().set(&DataKey::Token,token);Ok()} -pub fn set_fee_bps(env:&Env,admin:&Address,fee_bps:u32)->Result<(),CircleError>{let s:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if admin!=&s{return Err(CircleError::Unauthorized);}if fee_bps>10_000{return Err(CircleError::InvalidAmount);}env.storage().instance().set(&DataKey::FeeBps,&fee_bps);Ok()} -pub fn set_allowlist(env:&Env,admin:&Address,allowlist:Vec
)->Result<(),CircleError>{let s:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if admin!=&s{return Err(CircleError::Unauthorized);}env.storage().persistent().set(&DataKey::Allowlist,&allowlist);Ok()} +pub fn set_treasury(env:&Env,admin:&Address,treasury:&Address)->Result<(),CircleError>{let s:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if admin!=&s{return Err(CircleError::Unauthorized);}env.storage().instance().set(&DataKey::Treasury,treasury);Ok(())} +pub fn set_token(env:&Env,admin:&Address,token:&Address)->Result<(),CircleError>{let s:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if admin!=&s{return Err(CircleError::Unauthorized);}env.storage().instance().set(&DataKey::Token,token);Ok(())} +pub fn set_fee_bps(env:&Env,admin:&Address,fee_bps:u32)->Result<(),CircleError>{let s:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if admin!=&s{return Err(CircleError::Unauthorized);}if fee_bps>10_000{return Err(CircleError::InvalidAmount);}env.storage().instance().set(&DataKey::FeeBps,&fee_bps);Ok(())} +pub fn set_allowlist(env:&Env,admin:&Address,allowlist:Vec
)->Result<(),CircleError>{let s:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if admin!=&s{return Err(CircleError::Unauthorized);}env.storage().persistent().set(&DataKey::Allowlist,&allowlist);Ok(())} pub fn get_allowlist(env:&Env)->Vec
{env.storage().persistent().get(&DataKey::Allowlist).unwrap_or_else(||Vec::new(env))} +pub fn update_config(env:&Env,admin:&Address,new_config:&CircleConfigUpdate)->Result<(),CircleError>{let s:Address=env.storage().instance().get(&DataKey::Admin).ok_or(CircleError::NotInitialized)?;if admin!=&s{return Err(CircleError::Unauthorized);}admin.require_auth();let mut circle:Circle=env.storage().instance().get(&DataKey::Circle).ok_or(CircleError::NotInitialized)?;if circle.status!=STATUS_PENDING{return Err(CircleError::NotActive);}if new_config.max_members<2||new_config.contribution_amount<=0||new_config.total_rounds==0||new_config.payout_type>3{return Err(CircleError::InvalidAmount);}if new_config.max_membersResult<(),types::CircleError>{contract::set_fee_bps(&env,&admin,fee_bps)} pub fn set_allowlist(env:Env,admin:Address,allowlist:soroban_sdk::Vec
)->Result<(),types::CircleError>{contract::set_allowlist(&env,&admin,allowlist)} pub fn get_allowlist(env:Env)->soroban_sdk::Vec
{contract::get_allowlist(&env)} + pub fn update_config(env:Env,admin:Address,new_config:types::CircleConfigUpdate)->Result<(),types::CircleError>{contract::update_config(&env,&admin,&new_config)} } diff --git a/packages/circle/src/types.rs b/packages/circle/src/types.rs index cbf6b3d..11134b4 100644 --- a/packages/circle/src/types.rs +++ b/packages/circle/src/types.rs @@ -6,6 +6,8 @@ pub const RESOLVE_DISMISS:u32=1;pub const RESOLVE_PENALIZE:u32=2;pub const RESOL #[contracttype]#[derive(Clone,Debug)] pub struct CircleConfig{pub organizer:Address,pub token:Address,pub name:String,pub contribution_amount:i128,pub max_members:u32,pub payout_type:u32,pub total_rounds:u32,pub contribution_deadline_seconds:u64,pub min_moi_score:u32,pub collateral_amount:i128,pub penalty_bps:u32,pub grace_period_seconds:u64,pub max_strikes:u32,pub slug:String} #[contracttype]#[derive(Clone,Debug)] +pub struct CircleConfigUpdate{pub contribution_amount:i128,pub max_members:u32,pub payout_type:u32,pub total_rounds:u32,pub contribution_deadline_seconds:u64,pub min_moi_score:u32,pub collateral_amount:i128,pub penalty_bps:u32,pub grace_period_seconds:u64,pub max_strikes:u32} +#[contracttype]#[derive(Clone,Debug)] pub struct Circle{pub id:Address,pub token:Address,pub name:String,pub organizer:Address,pub factory:Address,pub contribution_amount:i128,pub max_members:u32,pub member_count:u32,pub payout_type:u32,pub total_rounds:u32,pub current_round:u32,pub status:u32,pub started_at:u64,pub created_at:u64,pub contribution_deadline_seconds:u64,pub min_moi_score:u32,pub collateral_amount:i128,pub penalty_bps:u32,pub grace_period_seconds:u64,pub max_strikes:u32,pub payout_bitmap:u128,pub total_payouts:i128,pub total_fees:i128,pub slug:String} #[contracttype]#[derive(Clone,Debug)]pub struct Member{pub address:Address,pub position:u32,pub joined_at:u64,pub strikes:u32,pub status:u32,pub exited_at:u64,pub total_contributions:i128,pub total_received:i128} #[contracttype]#[derive(Clone,Debug)]pub struct Contribution{pub member:Address,pub round:u32,pub amount:i128,pub timestamp:u64,pub on_time:bool,pub time_weight:u64} @@ -27,6 +29,7 @@ pub struct CircleCompleted{pub total_payouts:i128} #[contracttype]#[derive(Clone,Debug)]pub struct AuctionBidPlaced{pub bidder:Address,pub discount_bips:u32,pub round:u32} #[contracttype]#[derive(Clone,Debug)]pub struct VoteCast{pub voter:Address,pub vote_for:Address,pub round:u32} #[contracttype]#[derive(Clone,Debug)]pub struct ReferralRegistered{pub referrer:Address,pub referred:Address,pub bonus_pct:u32} +#[contracttype]#[derive(Clone,Debug)]pub struct CircleConfigUpdated{pub updated_by:Address} #[contracttype]#[derive(Clone,Debug,PartialEq)]pub enum PayoutType{Random=0,Fixed=1,Auction=2,Vote=3} #[contracttype]#[derive(Clone,Debug,PartialEq)]pub enum CircleStatus{Pending=0,Active=1,Completed=2,Cancelled=3,Disputed=4} #[contracttype]#[derive(Clone,Debug,PartialEq)]pub enum MemberStatus{Active=0,Exited=1,Defaulted=2}