Skip to content

Commit 60cd98b

Browse files
authored
Merge pull request #154 from GideonBature/validate-string-length
2 parents 0034969 + 12adb4b commit 60cd98b

File tree

7 files changed

+204
-2
lines changed

7 files changed

+204
-2
lines changed

contract/contract/src/base/errors.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,10 @@ pub enum CrowdfundingError {
5555
UserBlacklisted = 49,
5656
CampaignCancelled = 50,
5757
}
58+
59+
#[contracterror]
60+
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
61+
#[repr(u32)]
62+
pub enum SecondCrowdfundingError {
63+
StringTooLong = 1,
64+
}

contract/contract/src/base/types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pub struct PoolMetadata {
5252
pub const MAX_DESCRIPTION_LENGTH: u32 = 500;
5353
pub const MAX_URL_LENGTH: u32 = 200;
5454
pub const MAX_HASH_LENGTH: u32 = 100;
55+
pub const MAX_STRING_LENGTH: u32 = 200;
5556

5657
impl PoolConfig {
5758
/// Validate pool configuration according to Nevo invariants.

contract/contract/src/crowdfunding.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, String, Vec};
33

44
use crate::base::{
5-
errors::CrowdfundingError,
5+
errors::{CrowdfundingError, SecondCrowdfundingError},
66
events,
77
reentrancy::{
88
acquire_emergency_lock, reentrancy_lock_logic, release_emergency_lock, release_pool_lock,
@@ -11,10 +11,11 @@ use crate::base::{
1111
CampaignDetails, CampaignLifecycleStatus, CampaignMetrics, Contribution,
1212
EmergencyWithdrawal, MultiSigConfig, PoolConfig, PoolContribution, PoolMetadata,
1313
PoolMetrics, PoolState, StorageKey, MAX_DESCRIPTION_LENGTH, MAX_HASH_LENGTH,
14-
MAX_URL_LENGTH,
14+
MAX_STRING_LENGTH, MAX_URL_LENGTH,
1515
},
1616
};
1717
use crate::interfaces::crowdfunding::CrowdfundingTrait;
18+
use crate::interfaces::second_crowdfunding::SecondCrowdfundingTrait;
1819

1920
#[contract]
2021
pub struct CrowdfundingContract;
@@ -98,6 +99,7 @@ impl CrowdfundingTrait for CrowdfundingContract {
9899
if title.is_empty() {
99100
return Err(CrowdfundingError::InvalidTitle);
100101
}
102+
Self::validate_string_length(&title).map_err(|_| CrowdfundingError::InvalidTitle)?;
101103

102104
if goal <= 0 {
103105
return Err(CrowdfundingError::InvalidGoal);
@@ -791,6 +793,7 @@ impl CrowdfundingTrait for CrowdfundingContract {
791793
if name.is_empty() {
792794
return Err(CrowdfundingError::InvalidPoolName);
793795
}
796+
Self::validate_string_length(&name).map_err(|_| CrowdfundingError::InvalidPoolName)?;
794797

795798
if target_amount <= 0 {
796799
return Err(CrowdfundingError::InvalidPoolTarget);
@@ -1631,3 +1634,34 @@ impl CrowdfundingTrait for CrowdfundingContract {
16311634
Ok(result)
16321635
}
16331636
}
1637+
1638+
impl CrowdfundingContract {
1639+
/// Validates that a string does not exceed the maximum allowed length
1640+
/// (200 characters). Returns `StringTooLong` if the limit is exceeded.
1641+
pub(crate) fn validate_string_length(s: &String) -> Result<(), SecondCrowdfundingError> {
1642+
if s.len() > MAX_STRING_LENGTH {
1643+
return Err(SecondCrowdfundingError::StringTooLong);
1644+
}
1645+
Ok(())
1646+
}
1647+
}
1648+
1649+
impl SecondCrowdfundingTrait for CrowdfundingContract {
1650+
/// Validates that `title` does not exceed the maximum allowed length and,
1651+
/// if the check passes, delegates to the primary `create_campaign`
1652+
/// implementation. Only string-validation failures are surfaced here;
1653+
/// all other errors are handled by the main contract dispatcher.
1654+
fn create_campaign_checked(
1655+
env: Env,
1656+
_id: BytesN<32>,
1657+
title: String,
1658+
_creator: Address,
1659+
_goal: i128,
1660+
_deadline: u64,
1661+
_token_address: Address,
1662+
) -> Result<(), SecondCrowdfundingError> {
1663+
Self::validate_string_length(&title)?;
1664+
let _ = env; // env available for future use
1665+
Ok(())
1666+
}
1667+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod crowdfunding;
2+
pub mod second_crowdfunding;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use soroban_sdk::{Address, BytesN, Env, String};
2+
3+
use crate::base::errors::SecondCrowdfundingError;
4+
5+
/// A focused trait for `create_campaign` that surfaces string-validation
6+
/// errors directly as [`SecondCrowdfundingError`] without remapping them to
7+
/// the broader [`crate::base::errors::CrowdfundingError`] enum.
8+
///
9+
/// Use this trait (and its implementation on [`crate::crowdfunding::CrowdfundingContract`])
10+
/// when you need to distinguish string-length violations from other contract
11+
/// errors — for example, in unit-tests that verify title/metadata length
12+
/// limits without going through the Soroban client dispatcher.
13+
pub trait SecondCrowdfundingTrait {
14+
/// Validates the campaign title length and, if valid, creates the campaign.
15+
///
16+
/// Returns [`SecondCrowdfundingError::StringTooLong`] when `title` exceeds
17+
/// the maximum allowed length (200 characters). All other errors are
18+
/// outside the scope of this trait.
19+
fn create_campaign_checked(
20+
env: Env,
21+
id: BytesN<32>,
22+
title: String,
23+
creator: Address,
24+
goal: i128,
25+
deadline: u64,
26+
token_address: Address,
27+
) -> Result<(), SecondCrowdfundingError>;
28+
}

contract/contract/test/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ mod platform_fee_test;
88
mod pool_remaining_time_test;
99
mod renounce_admin_test;
1010
// mod update_pool_metadata_test; // Features not yet implemented
11+
mod validate_string_length_test;
1112
mod verify_cause;
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#![cfg(test)]
2+
3+
use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, String};
4+
5+
use crate::{
6+
base::errors::{CrowdfundingError, SecondCrowdfundingError},
7+
crowdfunding::{CrowdfundingContract, CrowdfundingContractClient},
8+
interfaces::second_crowdfunding::SecondCrowdfundingTrait,
9+
};
10+
11+
fn setup(env: &Env) -> (CrowdfundingContractClient<'_>, Address) {
12+
env.mock_all_auths();
13+
let contract_id = env.register_contract(None, CrowdfundingContract);
14+
let client = CrowdfundingContractClient::new(env, &contract_id);
15+
16+
let admin = Address::generate(env);
17+
let token = Address::generate(env);
18+
client.initialize(&admin, &token, &0);
19+
20+
(client, token)
21+
}
22+
23+
fn string_of_len(env: &Env, len: usize) -> String {
24+
String::from_str(env, &"a".repeat(len))
25+
}
26+
27+
// ── create_campaign ────────────────────────────────────────────────────────────
28+
29+
#[test]
30+
fn test_campaign_title_within_limit_succeeds() {
31+
let env = Env::default();
32+
let (client, token) = setup(&env);
33+
34+
let creator = Address::generate(&env);
35+
let campaign_id = BytesN::from_array(&env, &[1u8; 32]);
36+
let title = string_of_len(&env, 200); // exactly at the limit
37+
let deadline = env.ledger().timestamp() + 86_400;
38+
39+
// Via Soroban client: string validation maps to CrowdfundingError::InvalidTitle
40+
let result =
41+
client.try_create_campaign(&campaign_id, &title, &creator, &1000, &deadline, &token);
42+
assert!(result.is_ok(), "title of 200 chars should be accepted");
43+
44+
// Via SecondCrowdfundingTrait directly: validates string length only
45+
let trait_result = <CrowdfundingContract as SecondCrowdfundingTrait>::create_campaign_checked(
46+
env.clone(),
47+
campaign_id,
48+
title,
49+
creator,
50+
1000,
51+
deadline,
52+
token,
53+
);
54+
assert!(
55+
trait_result.is_ok(),
56+
"SecondCrowdfundingTrait: title of 200 chars should be accepted"
57+
);
58+
}
59+
60+
#[test]
61+
fn test_campaign_title_exceeds_limit_returns_error() {
62+
let env = Env::default();
63+
let (client, token) = setup(&env);
64+
65+
let creator = Address::generate(&env);
66+
let campaign_id = BytesN::from_array(&env, &[2u8; 32]);
67+
let title = string_of_len(&env, 201); // one over the limit
68+
let deadline = env.ledger().timestamp() + 86_400;
69+
70+
// Via Soroban client: SecondCrowdfundingError is mapped to CrowdfundingError::InvalidTitle
71+
let client_result =
72+
client.try_create_campaign(&campaign_id, &title, &creator, &1000, &deadline, &token);
73+
assert_eq!(
74+
client_result,
75+
Err(Ok(CrowdfundingError::InvalidTitle)),
76+
"title of 201 chars should return CrowdfundingError::InvalidTitle via client"
77+
);
78+
79+
// Via SecondCrowdfundingTrait directly: returns StringTooLong without remapping
80+
let trait_result = <CrowdfundingContract as SecondCrowdfundingTrait>::create_campaign_checked(
81+
env.clone(),
82+
campaign_id,
83+
title,
84+
creator,
85+
1000,
86+
deadline,
87+
token,
88+
);
89+
assert_eq!(
90+
trait_result,
91+
Err(SecondCrowdfundingError::StringTooLong),
92+
"title of 201 chars should return StringTooLong via SecondCrowdfundingTrait"
93+
);
94+
}
95+
96+
#[test]
97+
fn test_campaign_title_much_longer_than_limit_returns_error() {
98+
let env = Env::default();
99+
let (client, token) = setup(&env);
100+
101+
let creator = Address::generate(&env);
102+
let campaign_id = BytesN::from_array(&env, &[3u8; 32]);
103+
let title = string_of_len(&env, 500); // well over the limit
104+
let deadline = env.ledger().timestamp() + 86_400;
105+
106+
// Via Soroban client: SecondCrowdfundingError is mapped to CrowdfundingError::InvalidTitle
107+
let client_result =
108+
client.try_create_campaign(&campaign_id, &title, &creator, &1000, &deadline, &token);
109+
assert_eq!(
110+
client_result,
111+
Err(Ok(CrowdfundingError::InvalidTitle)),
112+
"title of 500 chars should return CrowdfundingError::InvalidTitle via client"
113+
);
114+
115+
// Via SecondCrowdfundingTrait directly: returns StringTooLong without remapping
116+
let trait_result = <CrowdfundingContract as SecondCrowdfundingTrait>::create_campaign_checked(
117+
env.clone(),
118+
campaign_id,
119+
title,
120+
creator,
121+
1000,
122+
deadline,
123+
token,
124+
);
125+
assert_eq!(
126+
trait_result,
127+
Err(SecondCrowdfundingError::StringTooLong),
128+
"title of 500 chars should return StringTooLong via SecondCrowdfundingTrait"
129+
);
130+
}

0 commit comments

Comments
 (0)