Skip to content
Merged
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
57 changes: 57 additions & 0 deletions packages/circle-factory/circle_factory.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// src/contracts/circle_factory.rs
use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, Result};
use soroban_sdk::token::Client as TokenClient;

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CircleConfig {
pub organizer: Address,
pub token: Address,
pub contribution_amount: i128,
pub fee_bps: u32,
}

#[contracttype]
pub enum FactoryError {
DeploymentFailed = 1,
}

#[contracttype]
pub enum DataKey {
Treasury,
}

#[contract]
pub struct CircleFactoryContract;

#[contractimpl]
impl CircleFactoryContract {
pub fn deploy_circle(
env: Env,
config: CircleConfig,
salt: BytesN<32>,
deployer_wasm_hash: BytesN<32>,
) -> Result<Address, FactoryError> {
config.organizer.require_auth();

// Calculate and charge deployment fee from organizer to treasury based on fee_bps and contribution amount
let fee_amount = (config.contribution_amount * (config.fee_bps as i128)) / 10000;
if fee_amount > 0 {
let treasury: Address = env
.storage()
.instance()
.get(&DataKey::Treasury)
.unwrap_or(config.organizer.clone());
let token_client = TokenClient::new(&env, &config.token);
token_client.transfer(&config.organizer, &treasury, &fee_amount);
}

// Deploy circle contract instance securely via Soroban deployer API
let deployed_address = env
.deployer()
.with_address(config.organizer.clone(), salt)
.deploy(deployer_wasm_hash);

Ok(deployed_address)
}
}
247 changes: 247 additions & 0 deletions packages/governance/governance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, Symbol};
use soroban_sdk::token::Client as TokenClient;
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, Symbol, Val, Vec};
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, Symbol};
use soroban_sdk::token::Client as TokenClient;

#[contracttype]
pub enum DataKey {
GovernanceToken,
Deposit(u64),
Proposer(u64),
ProposalStatus(u64),
}

#[contract]
pub struct GovernanceContract;

#[contractimpl]
impl GovernanceContract {
pub fn create_proposal(
env: Env,
proposer: Address,
proposal_id: u64,
deposit_amount: i128,
) {
proposer.require_auth();

if deposit_amount <= 0 {
panic!("Deposit amount must be positive");
}

if env.storage().persistent().has(&DataKey::Deposit(proposal_id)) {
panic!("Proposal already exists");
}

let token: Address = env
.storage()
.instance()
.get(&DataKey::GovernanceToken)
.unwrap_or_else(|| panic!("Governance token not configured"));

let token_client = TokenClient::new(&env, &token);

// FIX: Transfer deposit from proposer to the governance contract to enforce sybil resistance
token_client.transfer(&proposer, &env.current_contract_address(), &deposit_amount);

env.storage().persistent().set(&DataKey::Deposit(proposal_id), &deposit_amount);
env.storage().persistent().set(&DataKey::Proposer(proposal_id), &proposer.clone());
env.storage().persistent().set(&DataKey::ProposalStatus(proposal_id), &Symbol::new(&env, "Active"));

env.events().publish((Symbol::new(&env, "ProposalCreated"), proposal_id), (proposer, deposit_amount));
}

pub fn cancel_proposal(env: Env, proposer: Address, proposal_id: u64) {
proposer.require_auth();

let stored_proposer: Address = env
.storage()
.persistent()
.get(&DataKey::Proposer(proposal_id))
.unwrap_or_else(|| panic!("Proposal not found"));

if proposer != stored_proposer {
panic!("Only the proposer can cancel this proposal");
}

let status: Symbol = env.storage().persistent().get(&DataKey::ProposalStatus(proposal_id)).unwrap();
if status != Symbol::new(&env, "Active") {
panic!("Proposal is not active");
}

let deposit_amount: i128 = env
.storage()
.persistent()
.get(&DataKey::Deposit(proposal_id))
.unwrap_or(0);

if deposit_amount > 0 {
let token: Address = env.storage().instance().get(&DataKey::GovernanceToken).unwrap();
let token_client = TokenClient::new(&env, &token);

// FIX: Refund the actual token deposit back to the proposer upon cancellation
token_client.transfer(&env.current_contract_address(), &proposer, &deposit_amount);
}

env.storage().persistent().remove(&DataKey::Deposit(proposal_id));
env.storage().persistent().set(&DataKey::ProposalStatus(proposal_id), &Symbol::new(&env, "Cancelled"));

env.events().publish((Symbol::new(&env, "ProposalCancelled"), proposal_id), proposer);
}
}


#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProposalExecution {
pub target: Address,
pub function: Symbol,
pub args: Vec<Val>,
pub retry_count: u32,
pub max_retries: u32,
}

#[contracttype]
pub enum DataKey {
ProposalStatus(u64),
ProposalExecutionData(u64),
}

#[contract]
pub struct GovernanceContract;

#[contractimpl]
impl GovernanceContract {
pub fn execute_proposal(env: Env, proposal_id: u64) {
let status: Symbol = env
.storage()
.persistent()
.get(&DataKey::ProposalStatus(proposal_id))
.unwrap_or_else(|| panic!("Proposal not found"));

if status != Symbol::new(&env, "Queued") {
panic!("Proposal is not in Queued status");
}

let mut exec_data: ProposalExecution = env
.storage()
.persistent()
.get(&DataKey::ProposalExecutionData(proposal_id))
.unwrap_or_else(|| panic!("Execution data not found"));

if exec_data.retry_count >= exec_data.max_retries {
env.storage().persistent().set(
&DataKey::ProposalStatus(proposal_id),
&Symbol::new(&env, "Failed"),
);
panic!("Max retry limit reached; proposal marked as Failed");
}

exec_data.retry_count += 1;
env.storage()
.persistent()
.set(&DataKey::ProposalExecutionData(proposal_id), &exec_data);

// FIX: Utilize try_invoke_contract to catch execution errors, update retry count, and transition to Failed upon limit exhaustion
let result: Result<Val, _> = env.try_invoke_contract(
&exec_data.target,
&exec_data.function,
exec_data.args,
);

match result {
Ok(_val) => {
env.storage().persistent().set(
&DataKey::ProposalStatus(proposal_id),
&Symbol::new(&env, "Executed"),
);
env.events().publish(
(Symbol::new(&env, "ProposalExecuted"), proposal_id),
exec_data.target,
);
}
Err(_) => {
if exec_data.retry_count >= exec_data.max_retries {
env.storage().persistent().set(
&DataKey::ProposalStatus(proposal_id),
&Symbol::new(&env, "Failed"),
);
}
panic!("Proposal execution failed; retry count incremented");
}
}
}
}


#[contracttype]
pub enum DataKey {
GovernanceToken,
Deposit(u64),
Proposer(u64),
ProposalStatus(u64),
}

#[contract]
pub struct GovernanceContract;

#[contractimpl]
impl GovernanceContract {
pub fn cancel_proposal(env: Env, proposer: Address, proposal_id: u64) {
proposer.require_auth();

let stored_proposer: Address = env
.storage()
.persistent()
.get(&DataKey::Proposer(proposal_id))
.unwrap_or_else(|| panic!("Proposal not found"));

if proposer != stored_proposer {
panic!("Only the proposer can cancel this proposal");
}

let status: Symbol = env
.storage()
.persistent()
.get(&DataKey::ProposalStatus(proposal_id))
.unwrap_or_else(|| panic!("Proposal status not found"));

if status != Symbol::new(&env, "Active") {
panic!("Proposal is not active and cannot be cancelled");
}

let deposit_amount: i128 = env
.storage()
.persistent()
.get(&DataKey::Deposit(proposal_id))
.unwrap_or(0);

if deposit_amount > 0 {
let token: Address = env
.storage()
.instance()
.get(&DataKey::GovernanceToken)
.unwrap_or_else(|| panic!("Governance token not configured"));

let token_client = TokenClient::new(&env, &token);

// FIX: Actually transfer deposited tokens back from contract to proposer upon cancellation
token_client.transfer(
&env.current_contract_address(),
&proposer,
&deposit_amount,
);
}

env.storage().persistent().remove(&DataKey::Deposit(proposal_id));
env.storage().persistent().set(
&DataKey::ProposalStatus(proposal_id),
&Symbol::new(&env, "Cancelled"),
);

env.events().publish(
(Symbol::new(&env, "ProposalCancelled"), proposal_id),
(proposer, deposit_amount),
);
}
}