From 21b799e2e59bd064d761ce8af52f228e40b5d460 Mon Sep 17 00:00:00 2001 From: Silver36-ship-it Date: Sat, 29 Aug 2026 11:44:10 +0100 Subject: [PATCH 1/2] contracts: replace bare panic with typed ContractError::AlreadyInitialized; add test --- contracts/src/lib.rs | 42 +++++++++++++++++++++++++++++++++++++++--- contracts/src/test.rs | 16 ++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index a7097f26..ef328995 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -23,6 +23,44 @@ pub const MIN_DISPUTE_WINDOW_OVERRIDE: u64 = 60; /// Maximum allowed per-bounty dispute window override (30 days in seconds). pub const MAX_DISPUTE_WINDOW_OVERRIDE: u64 = 2_592_000; +// ─── Contract Errors ─────────────────────────────────────────────────── +#[contracterror] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ContractError { + InvalidAmount, + AmountTooSmall, + DeadlineMustBeInTheFuture, + ContractIsPaused, + FeeRecipientNotSet, + TokenNotAllowed, + DisputeWindowOverrideTooSmall, + DisputeWindowOverrideTooLarge, + BountyNotOpen, + MaintainerMismatch, + BountyMustBeReserved, + MissingContributor, + ContributorMismatch, + BountyMustBeSubmitted, + BountyAlreadyFinalized, + BountyNotExpiredYet, + CannotExtendFinalizedBounty, + DeadlineMustAdvance, + BountyExpired, + ArbiterNotSet, + NotArbiter, + DisputeWindowNotMet, + NotAdmin, + NoPendingArbiter, + TimelockNotElapsed, + BountyNotFound, + AlreadyInitialized, + StringTooLong, +} + +fn panic_error(err: ContractError) -> ! { + panic!("{:?}", err); +} + #[contracttype] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum BountyStatus { @@ -187,12 +225,10 @@ impl StellarBountyBoardContract { String::from_str(&_env, CONTRACT_VERSION) } - pub fn initialize(env: Env, fee_recipient: Address, arbiter: Address, dispute_window: u64) { - pub fn initialize(env: Env, admin: Address, fee_recipient: Address, arbiter: Address, dispute_window: u64) { // Prevent re-initialization if env.storage().persistent().has(&DataKey::FeeRecipient) { - panic!("already initialized"); + panic_error(ContractError::AlreadyInitialized); } env.storage() .persistent() diff --git a/contracts/src/test.rs b/contracts/src/test.rs index a6ec0d26..d1c63118 100644 --- a/contracts/src/test.rs +++ b/contracts/src/test.rs @@ -27,6 +27,22 @@ fn test_get_version_matches_cargo_toml() { ); } +#[test] +#[should_panic(expected = "AlreadyInitialized")] +fn test_initialize_twice_returns_already_initialized() { + let env = Env::default(); + let contract_id = env.register_contract(None, StellarBountyBoardContract); + let client = StellarBountyBoardContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let fee_recipient = Address::generate(&env); + let arbiter = Address::generate(&env); + + client.initialize(&admin, &fee_recipient, &arbiter, &600); + // Second call should fail with typed AlreadyInitialized error + client.initialize(&admin, &fee_recipient, &arbiter, &600); +} + #[test] fn test_contract_version_constant() { // Direct assertion on the compile-time constant From 61cadf43e441241faaf067fc287943522d92940f Mon Sep 17 00:00:00 2001 From: Silver36-ship-it Date: Sat, 29 Aug 2026 12:01:48 +0100 Subject: [PATCH 2/2] contracts: add repo/title length validation tests --- contracts/src/test.rs | 87 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/contracts/src/test.rs b/contracts/src/test.rs index d1c63118..bb98e831 100644 --- a/contracts/src/test.rs +++ b/contracts/src/test.rs @@ -45,6 +45,93 @@ fn test_initialize_twice_returns_already_initialized() { #[test] fn test_contract_version_constant() { + + #[test] + fn test_create_bounty_repo_title_at_limit_succeeds() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin, maintainer, _contributor, token_id, _fee_recipient, _arbiter) = setup_test(&env); + let token_admin = soroban_sdk::token::StellarAssetClient::new(&env, &token_id); + token_admin.mint(&maintainer, &1000); + + let rust_repo: alloc::string::String = core::iter::repeat('r').take(MAX_REPO_LEN as usize).collect(); + let rust_title: alloc::string::String = core::iter::repeat('t').take(MAX_TITLE_LEN as usize).collect(); + + let repo = String::from_str(&env, &rust_repo); + let title = String::from_str(&env, &rust_title); + let deadline = env.ledger().timestamp() + 1000; + + let bounty_id = client.create_bounty( + &maintainer, + &token_id, + &500, + &repo, + &1, + &title, + &deadline, + &0u32, + &None, + ); + + assert_eq!(bounty_id, 1); + } + + #[test] + #[should_panic(expected = "StringTooLong")] + fn test_create_bounty_repo_over_limit_fails() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin, maintainer, _contributor, token_id, _fee_recipient, _arbiter) = setup_test(&env); + let token_admin = soroban_sdk::token::StellarAssetClient::new(&env, &token_id); + token_admin.mint(&maintainer, &1000); + + // repo is one char longer than allowed + let rust_repo: alloc::string::String = core::iter::repeat('r').take((MAX_REPO_LEN as usize) + 1).collect(); + let title: alloc::string::String = core::iter::repeat('t').take(10).collect(); + + let repo = String::from_str(&env, &rust_repo); + let title = String::from_str(&env, &title); + let deadline = env.ledger().timestamp() + 1000; + + client.create_bounty( + &maintainer, + &token_id, + &500, + &repo, + &1, + &title, + &deadline, + &0u32, + &None, + ); + } + + #[test] + fn test_create_bounty_empty_repo_title_succeeds() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin, maintainer, _contributor, token_id, _fee_recipient, _arbiter) = setup_test(&env); + let token_admin = soroban_sdk::token::StellarAssetClient::new(&env, &token_id); + token_admin.mint(&maintainer, &1000); + + let repo = String::from_str(&env, ""); + let title = String::from_str(&env, ""); + let deadline = env.ledger().timestamp() + 1000; + + let bounty_id = client.create_bounty( + &maintainer, + &token_id, + &500, + &repo, + &1, + &title, + &deadline, + &0u32, + &None, + ); + + assert_eq!(bounty_id, 1); + } // Direct assertion on the compile-time constant assert_eq!(CONTRACT_VERSION, env!("CARGO_PKG_VERSION")); assert!(!CONTRACT_VERSION.is_empty());