diff --git a/contracts/contracts/stellar-grants/src/constants.rs b/contracts/contracts/stellar-grants/src/constants.rs index b6578c91..631783b6 100644 --- a/contracts/contracts/stellar-grants/src/constants.rs +++ b/contracts/contracts/stellar-grants/src/constants.rs @@ -59,6 +59,7 @@ pub const MAX_SPLIT_RECIPIENTS: u32 = 10; pub const MAX_CRITERIA_PER_MILESTONE: u32 = 20; pub const MAX_INDEX_ENTRIES: u32 = 10_000; pub const MAX_PUBLIC_REVIEW_COMMENT_LEN: u32 = 500; +pub const MAX_PUBLIC_REVIEWS_PER_MILESTONE: u32 = 50; pub const MAX_ROLLING_WINDOW_SIZE: u32 = 50; pub const MAX_PARAM_HISTORY: u32 = 20; pub const MAX_RUBRIC_WEIGHTS: u32 = 6; diff --git a/contracts/contracts/stellar-grants/src/open_review.rs b/contracts/contracts/stellar-grants/src/open_review.rs index 642ca203..9c6438e3 100644 --- a/contracts/contracts/stellar-grants/src/open_review.rs +++ b/contracts/contracts/stellar-grants/src/open_review.rs @@ -3,7 +3,7 @@ /// on a milestone. Reviews are visible to formal reviewers but do not affect governance votes. use soroban_sdk::{Address, Env, String, Vec}; -use crate::constants::MAX_PUBLIC_REVIEW_COMMENT_LEN; +use crate::constants::{MAX_PUBLIC_REVIEWS_PER_MILESTONE, MAX_PUBLIC_REVIEW_COMMENT_LEN}; use crate::errors::ContractError; use crate::events::Events; use crate::storage::Storage; @@ -56,6 +56,9 @@ pub fn submit_review( if let Some(idx) = found_idx { reviews.set(idx, review.clone()); } else { + if reviews.len() >= MAX_PUBLIC_REVIEWS_PER_MILESTONE { + return Err(ContractError::TooManyPublicReviews); + } reviews.push_back(review.clone()); } @@ -276,4 +279,53 @@ mod tests { let result = submit_review(&env, &reviewer, 1, 0, PublicReviewSignal::Positive, s); assert_eq!(result, Err(ContractError::CommentTooLong)); } + + #[test] + fn submit_review_rejects_when_cap_reached() { + let env = Env::default(); + env.mock_all_auths(); + + let grant_id = 1u64; + let milestone_idx = 0u32; + let mut first_reviewer: Option
= None; + + for _ in 0..MAX_PUBLIC_REVIEWS_PER_MILESTONE { + let reviewer = Address::generate(&env); + if first_reviewer.is_none() { + first_reviewer = Some(reviewer.clone()); + } + submit_review( + &env, + &reviewer, + grant_id, + milestone_idx, + PublicReviewSignal::Positive, + String::from_str(&env, "Good"), + ) + .unwrap(); + } + + // 51st reviewer should be rejected + let extra_reviewer = Address::generate(&env); + let result = submit_review( + &env, + &extra_reviewer, + grant_id, + milestone_idx, + PublicReviewSignal::Positive, + String::from_str(&env, "Extra"), + ); + assert_eq!(result, Err(ContractError::TooManyPublicReviews)); + + // Existing reviewer can still update their review + let update_result = submit_review( + &env, + &first_reviewer.unwrap(), + grant_id, + milestone_idx, + PublicReviewSignal::Neutral, + String::from_str(&env, "Updated comment"), + ); + assert!(update_result.is_ok()); + } }