Skip to content
Open
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
18 changes: 18 additions & 0 deletions contract/ERRORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Contract Error Codes

This document maps the `u32` error codes returned by the Soroban contracts to their respective meanings for frontend developers.

| Error Code (u32) | Name | Description | Recommended Tooltip |
|------------------|------|-------------|----------------------|
| 1 | `Unauthorized` | The caller does not have the necessary permissions for this action. | "You lack the required credentials to perform this action." |
| 2 | `InsufficientFunds` | The account or contract does not have enough balance to complete the transaction. | "Insufficient funds to complete the transaction." |
| 3 | `BountyNotFound` | The requested bounty ID does not exist in the contract storage. | "The specified bounty could not be found." |
| 4 | `AlreadyInitialized` | The contract has already been initialized. | "Contract is already initialized." |
| 5 | `TreasuryNotFound` | The requested treasury ID does not exist. | "The specified treasury could not be found." |
| 6 | `TransactionNotFound` | The requested transaction ID does not exist. | "The specified transaction could not be found." |
| 7 | `BudgetExceeded` | The transaction amount exceeds the allocated budget. | "Budget exceeded for this category." |
| 8 | `AllowanceExceeded` | The transaction amount exceeds the allocated allowance. | "Allowance exceeded for this administrator." |

## Usage for Frontend Developers

When a transaction fails with an error code, you can use this mapping to display user-friendly tooltips. In the Stellar SDK or Soroban SDK, these are typically returned as part of the `Result` or visible in transaction dry-runs.
33 changes: 17 additions & 16 deletions contract/src/bounty/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ use crate::events::topics::{
};
use crate::guild::membership::has_permission;
use crate::guild::types::Role;
use soroban_sdk::{Address, Env, String, Vec};
use crate::errors::ErrorCode;
use soroban_sdk::{panic_with_error, Address, Env, String, Vec};

pub use types::{Bounty, BountyStatus, PayoutSplit};

Expand All @@ -60,7 +61,7 @@ pub fn create_bounty(
creator.require_auth();

if !has_permission(env, guild_id, creator.clone(), Role::Admin) {
panic!("Unauthorized: Creator must be a guild admin or owner");
panic_with_error!(env, ErrorCode::Unauthorized);
}
if reward_amount < 0 {
panic!("Invalid reward amount: must be non-negative");
Expand Down Expand Up @@ -203,7 +204,7 @@ pub fn fund_bounty(env: &Env, bounty_id: u64, funder: Address, amount: i128) ->
panic!("Amount must be positive");
}

let mut bounty = get_bounty(env, bounty_id).expect("Bounty not found");
let mut bounty = get_bounty(env, bounty_id).unwrap_or_else(|| panic_with_error!(env, ErrorCode::BountyNotFound));

let now = env.ledger().timestamp();
if now > bounty.expires_at {
Expand Down Expand Up @@ -257,7 +258,7 @@ pub fn fund_bounty(env: &Env, bounty_id: u64, funder: Address, amount: i128) ->
pub fn claim_bounty(env: &Env, bounty_id: u64, claimer: Address) -> bool {
claimer.require_auth();

let mut bounty = get_bounty(env, bounty_id).expect("Bounty not found");
let mut bounty = get_bounty(env, bounty_id).unwrap_or_else(|| panic_with_error!(env, ErrorCode::BountyNotFound));

let now = env.ledger().timestamp();
if now > bounty.expires_at {
Expand Down Expand Up @@ -301,7 +302,7 @@ pub fn claim_bounty(env: &Env, bounty_id: u64, claimer: Address) -> bool {
/// # Events emitted
/// - `(bounty, submitted)` → `WorkSubmittedEvent`
pub fn submit_work(env: &Env, bounty_id: u64, submission_url: String) -> bool {
let mut bounty = get_bounty(env, bounty_id).expect("Bounty not found");
let mut bounty = get_bounty(env, bounty_id).unwrap_or_else(|| panic_with_error!(env, ErrorCode::BountyNotFound));

let claimer = bounty.claimer.clone().expect("No claimer for this bounty");
claimer.require_auth();
Expand Down Expand Up @@ -338,10 +339,10 @@ pub fn submit_work(env: &Env, bounty_id: u64, submission_url: String) -> bool {
pub fn approve_bounty(env: &Env, bounty_id: u64, approver: Address, claimer: Address) -> bool {
approver.require_auth();

let mut bounty = get_bounty(env, bounty_id).expect("Bounty not found");
let mut bounty = get_bounty(env, bounty_id).unwrap_or_else(|| panic_with_error!(env, ErrorCode::BountyNotFound));

if !has_permission(env, bounty.guild_id, approver.clone(), Role::Admin) {
panic!("Unauthorized: Approver must be a guild admin or owner");
panic_with_error!(env, ErrorCode::Unauthorized);
}
if bounty.status != BountyStatus::Funded {
panic!("Bounty is not funded");
Expand Down Expand Up @@ -369,10 +370,10 @@ pub fn approve_bounty(env: &Env, bounty_id: u64, approver: Address, claimer: Add
pub fn approve_completion(env: &Env, bounty_id: u64, approver: Address) -> bool {
approver.require_auth();

let mut bounty = get_bounty(env, bounty_id).expect("Bounty not found");
let mut bounty = get_bounty(env, bounty_id).unwrap_or_else(|| panic_with_error!(env, ErrorCode::BountyNotFound));

if !has_permission(env, bounty.guild_id, approver.clone(), Role::Admin) {
panic!("Unauthorized: Approver must be a guild admin or owner");
panic_with_error!(env, ErrorCode::Unauthorized);
}
if bounty.status != BountyStatus::UnderReview {
panic!("Bounty is not under review");
Expand Down Expand Up @@ -404,7 +405,7 @@ pub fn release_escrow(env: &Env, bounty_id: u64) -> bool {
panic!("Bounty is in active dispute");
}

let mut bounty = get_bounty(env, bounty_id).expect("Bounty not found");
let mut bounty = get_bounty(env, bounty_id).unwrap_or_else(|| panic_with_error!(env, ErrorCode::BountyNotFound));

if bounty.status != BountyStatus::Completed {
panic!("Bounty is not completed");
Expand Down Expand Up @@ -445,7 +446,7 @@ pub fn cancel_bounty(env: &Env, bounty_id: u64, canceller: Address) -> bool {
panic!("Bounty is in active dispute");
}

let mut bounty = get_bounty(env, bounty_id).expect("Bounty not found");
let mut bounty = get_bounty(env, bounty_id).unwrap_or_else(|| panic_with_error!(env, ErrorCode::BountyNotFound));

match bounty.status {
BountyStatus::Completed | BountyStatus::Cancelled => {
Expand All @@ -458,7 +459,7 @@ pub fn cancel_bounty(env: &Env, bounty_id: u64, canceller: Address) -> bool {
let is_admin = has_permission(env, bounty.guild_id, canceller.clone(), Role::Admin);

if !is_creator && !is_admin {
panic!("Unauthorized: Only creator or guild admin can cancel");
panic_with_error!(env, ErrorCode::Unauthorized);
}

let refund_amount = bounty.funded_amount;
Expand Down Expand Up @@ -496,7 +497,7 @@ pub fn expire_bounty(env: &Env, bounty_id: u64) -> bool {
panic!("Bounty is in active dispute");
}

let mut bounty = get_bounty(env, bounty_id).expect("Bounty not found");
let mut bounty = get_bounty(env, bounty_id).unwrap_or_else(|| panic_with_error!(env, ErrorCode::BountyNotFound));

if bounty.status == BountyStatus::Expired
|| bounty.status == BountyStatus::Completed
Expand Down Expand Up @@ -547,15 +548,15 @@ pub fn claim_payout(
panic!("Bounty is in active dispute");
}

let mut bounty = get_bounty(env, bounty_id).expect("Bounty not found");
let mut bounty = get_bounty(env, bounty_id).unwrap_or_else(|| panic_with_error!(env, ErrorCode::BountyNotFound));

if bounty.status != BountyStatus::Completed {
panic!("Bounty is not completed");
}

let stored_claimer = bounty.claimer.clone().expect("No claimer for this bounty");
if stored_claimer != claimer {
panic!("Unauthorized: Only the approved claimer can claim payout");
panic_with_error!(env, ErrorCode::Unauthorized);
}

validate_payout_splits(&recipients);
Expand All @@ -576,7 +577,7 @@ pub fn claim_payout(
// â"€â"€â"€ Query helpers â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€

pub fn get_bounty_data(env: &Env, bounty_id: u64) -> Bounty {
get_bounty(env, bounty_id).expect("Bounty not found")
get_bounty(env, bounty_id).unwrap_or_else(|| panic_with_error!(env, ErrorCode::BountyNotFound))
}

pub fn get_guild_bounties_list(env: &Env, guild_id: u64) -> Vec<Bounty> {
Expand Down
12 changes: 12 additions & 0 deletions contract/src/dummy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use soroban_sdk::{panic_with_error, Env};
use crate::errors::ErrorCode;

pub fn restricted_function(env: &Env) {
// This was previously: panic!("Unauthorized")
panic_with_error!(env, ErrorCode::Unauthorized);
}

pub fn admin_only_action(env: &Env) {
// This was previously: panic!("Access denied")
panic_with_error!(env, ErrorCode::Unauthorized);
}
15 changes: 15 additions & 0 deletions contract/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use soroban_sdk::contracterror;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum ErrorCode {
Unauthorized = 1,
InsufficientFunds = 2,
BountyNotFound = 3,
AlreadyInitialized = 4,
TreasuryNotFound = 5,
TransactionNotFound = 6,
BudgetExceeded = 7,
AllowanceExceeded = 8,
}
8 changes: 6 additions & 2 deletions contract/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
#![no_std]

use soroban_sdk::{contract, contractimpl, Address, Env, String, Vec};
use crate::errors::ErrorCode;
use soroban_sdk::{contract, contractimpl, panic_with_error, Address, Env, String, Vec};

mod events;
mod guild;
mod integration;
mod interfaces;
mod utils;
mod errors;
mod dummy;

use guild::membership::{
add_member, create_guild, get_all_members, get_member, has_permission, is_member, join_guild,
remove_member, reque_permissions as guild_reque_permissions,
Expand Down Expand Up @@ -204,7 +208,7 @@ pub struct StellarGuildsContract;
impl StellarGuildsContract {
pub fn initialize(env: Env, admin: Address) -> bool {
if env.storage().instance().has(&DataKey::Initialized) {
panic!("Already initialized");
panic_with_error!(&env, ErrorCode::AlreadyInitialized);
}

env.storage().instance().set(&DataKey::Admin, &admin);
Expand Down
17 changes: 9 additions & 8 deletions contract/src/treasury/management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use crate::events::topics::{
ACT_APPROVED, ACT_CREATED, ACT_EXECUTED, ACT_FUNDED, ACT_GRANTED, ACT_PAUSED, ACT_PROPOSED,
ACT_RESUMED, ACT_UPDATED, MOD_TREASURY,
};
use soroban_sdk::{token::Client as TokenClient, Address, Env, String, Vec};
use crate::errors::ErrorCode;
use soroban_sdk::{panic_with_error, token::Client as TokenClient, Address, Env, String, Vec};

use crate::analytics::storage::store_snapshot;
use crate::analytics::types::TreasurySnapshot;
Expand Down Expand Up @@ -85,7 +86,7 @@ pub fn deposit(
panic!("amount must be positive");
}

let mut treasury = get_treasury(env, treasury_id).expect("treasury not found");
let mut treasury = get_treasury(env, treasury_id).unwrap_or_else(|| panic_with_error!(env, ErrorCode::TreasuryNotFound));
if treasury.paused {
panic!("treasury is paused");
}
Expand Down Expand Up @@ -198,7 +199,7 @@ pub fn propose_withdrawal(
pub fn approve_transaction(env: &Env, tx_id: u64, approver: Address) -> bool {
approver.require_auth();

let mut tx = crate::treasury::storage::get_transaction(env, tx_id).expect("tx not found");
let mut tx = crate::treasury::storage::get_transaction(env, tx_id).unwrap_or_else(|| panic_with_error!(env, ErrorCode::TransactionNotFound));
let treasury = get_treasury(env, tx.treasury_id).expect("treasury not found");

let now = env.ledger().timestamp();
Expand Down Expand Up @@ -301,7 +302,7 @@ fn enforce_allowance(
pub fn execute_transaction(env: &Env, tx_id: u64, executor: Address) -> bool {
executor.require_auth();

let mut tx = crate::treasury::storage::get_transaction(env, tx_id).expect("tx not found");
let mut tx = crate::treasury::storage::get_transaction(env, tx_id).unwrap_or_else(|| panic_with_error!(env, ErrorCode::TransactionNotFound));
let mut treasury = get_treasury(env, tx.treasury_id).expect("treasury not found");

let now = env.ledger().timestamp();
Expand Down Expand Up @@ -342,8 +343,8 @@ pub fn execute_transaction(env: &Env, tx_id: u64, executor: Address) -> bool {
// This creates a proper contract error (all panics in Soroban become contract errors)
// while maintaining the expected error message for test compatibility
enforce_budget(env, tx.treasury_id, &category, tx.amount).unwrap_or_else(|e| match e {
TreasuryError::BudgetExceeded => panic!("budget exceeded"),
TreasuryError::AllowanceExceeded => panic!("allowance exceeded"),
TreasuryError::BudgetExceeded => panic_with_error!(env, ErrorCode::BudgetExceeded),
TreasuryError::AllowanceExceeded => panic_with_error!(env, ErrorCode::AllowanceExceeded),
});

let op_type = match tx.tx_type {
Expand Down Expand Up @@ -377,7 +378,7 @@ pub fn execute_transaction(env: &Env, tx_id: u64, executor: Address) -> bool {
let mut balances = treasury.token_balances.clone();
let current = balances.get(token_addr.clone()).unwrap_or(0i128);
if current < tx.amount {
panic!("insufficient treasury balance");
panic_with_error!(env, ErrorCode::InsufficientFunds);
}
balances.set(token_addr.clone(), current - tx.amount);
treasury.token_balances = balances;
Expand All @@ -386,7 +387,7 @@ pub fn execute_transaction(env: &Env, tx_id: u64, executor: Address) -> bool {
}
None => {
if treasury.balance_xlm < tx.amount {
panic!("insufficient XLM balance");
panic_with_error!(env, ErrorCode::InsufficientFunds);
}
treasury.balance_xlm -= tx.amount;
}
Expand Down