Description
// reviewer_pool.rs:112-157
pub fn accept_request(env: &Env, reviewer: &Address, grant_id: u64) -> Result<(), ContractError> {
reviewer.require_auth();
let request = Storage::get_reviewer_request(env, grant_id, reviewer).ok_or(ContractError::InvalidState)?;
if request.status != ReviewerRequestStatus::Pending { return Err(ContractError::InvalidState); }
if env.ledger().timestamp() > request.expires_at { return Err(ContractError::InvalidState); }
let profile = Storage::get_reviewer_profile(env, reviewer).ok_or(ContractError::InvalidState)?;
if profile.availability != ReviewerAvailability::Available { return Err(ContractError::InvalidState); }
let mut grant = Storage::get_grant_v(env, grant_id);
grant.reviewers.push_back(reviewer.clone());
Storage::set_grant(env, grant_id, &grant);
...
}
This pushes reviewer onto grant.reviewers with no whitelist check and no cap check. Compare with the two other places grant.reviewers gains an entry:
internal_grant_create (in lib.rs) enforces whitelist::is_allowed(env, &r, &WhitelistScope::GlobalReviewer) and rejects once reviewers.len() > protocol_cfg.max_reviewers.
multi_grant::add_reviewer_to_grant explicitly re-checks the whitelist and dedups via .contains().
accept_request has neither check, and there's also no dedup: request_reviewer can be called again for the same (grant_id, reviewer) pair (it unconditionally overwrites the stored request back to Pending), and accept_request never checks grant.reviewers.contains(reviewer) before pushing — so the same reviewer can be accepted multiple times, growing grant.reviewers past protocol_cfg.max_reviewers, an invariant enforced everywhere else at grant creation but silently bypassable here.
Exploit: An admin sets WhitelistScope::GlobalReviewer to Restricted specifically to control who can review grants (as enforced by internal_grant_create/multi_grant::add_reviewer_to_grant). A non-whitelisted reviewer registers via reviewer_register, the grant owner calls reviewer_request for them (or the reviewer requests to review, depending on the direction of request_reviewer), and the reviewer calls reviewer_accept_request — bypassing the whitelist entirely via a different code path than the ones that check it.
Technical Requirements
Files to update
contracts/contracts/stellar-grants/src/reviewer_pool.rs (accept_request, lines 112-157)
Fix direction
pub fn accept_request(env: &Env, reviewer: &Address, grant_id: u64) -> Result<(), ContractError> {
reviewer.require_auth();
let request = Storage::get_reviewer_request(env, grant_id, reviewer).ok_or(ContractError::InvalidState)?;
if request.status != ReviewerRequestStatus::Pending { return Err(ContractError::InvalidState); }
if env.ledger().timestamp() > request.expires_at { return Err(ContractError::InvalidState); }
let profile = Storage::get_reviewer_profile(env, reviewer).ok_or(ContractError::InvalidState)?;
if profile.availability != ReviewerAvailability::Available { return Err(ContractError::InvalidState); }
if !crate::whitelist::is_allowed(env, reviewer, &WhitelistScope::GlobalReviewer) {
return Err(ContractError::Unauthorized);
}
let mut grant = Storage::get_grant_v(env, grant_id);
if grant.reviewers.contains(reviewer.clone()) {
return Err(ContractError::AlreadyRegistered);
}
let protocol_cfg = Storage::get_protocol_config(env);
if grant.reviewers.len() >= protocol_cfg.max_reviewers {
return Err(ContractError::InvalidInput);
}
grant.reviewers.push_back(reviewer.clone());
Storage::set_grant(env, grant_id, &grant);
// ... rest unchanged
}
Adjust the exact whitelist/config accessor calls to match whatever helper functions internal_grant_create/multi_grant::add_reviewer_to_grant already use, for consistency.
Acceptance Criteria
- A non-whitelisted reviewer (when
GlobalReviewer scope is Restricted) cannot become a grant reviewer via accept_request.
accept_request cannot push the same reviewer onto grant.reviewers twice, and cannot exceed protocol_cfg.max_reviewers.
- A test confirms all three invariants (whitelist, dedup, cap) are enforced through this path specifically.
cargo test passes.
Estimated Effort
Beginner: 4 hours
Intermediate: 2 hours
Expert: 1.5 hours
How to work this issue
- Read
contracts/ContributionGuide.md for the contribution workflow.
- Comment on the issue to claim it before starting.
- Branch:
fix/issue-928-reviewer-pool-accept-request-whitelist.
- Run
cargo fmt, cargo clippy -- -D warnings, cargo test before opening your PR.
- Use a Conventional Commit message, e.g.
security: enforce whitelist, dedup, and max-reviewers cap in accept_request.
Before you start
If you find this project interesting, please consider starring the repository on GitHub. It helps the project gain visibility and supports the Drips Wave program that rewards contributors for merged fixes like this one.
Description
This pushes
reviewerontogrant.reviewerswith no whitelist check and no cap check. Compare with the two other placesgrant.reviewersgains an entry:internal_grant_create(inlib.rs) enforceswhitelist::is_allowed(env, &r, &WhitelistScope::GlobalReviewer)and rejects oncereviewers.len() > protocol_cfg.max_reviewers.multi_grant::add_reviewer_to_grantexplicitly re-checks the whitelist and dedups via.contains().accept_requesthas neither check, and there's also no dedup:request_reviewercan be called again for the same(grant_id, reviewer)pair (it unconditionally overwrites the stored request back toPending), andaccept_requestnever checksgrant.reviewers.contains(reviewer)before pushing — so the same reviewer can be accepted multiple times, growinggrant.reviewerspastprotocol_cfg.max_reviewers, an invariant enforced everywhere else at grant creation but silently bypassable here.Exploit: An admin sets
WhitelistScope::GlobalReviewertoRestrictedspecifically to control who can review grants (as enforced byinternal_grant_create/multi_grant::add_reviewer_to_grant). A non-whitelisted reviewer registers viareviewer_register, the grant owner callsreviewer_requestfor them (or the reviewer requests to review, depending on the direction ofrequest_reviewer), and the reviewer callsreviewer_accept_request— bypassing the whitelist entirely via a different code path than the ones that check it.Technical Requirements
Files to update
contracts/contracts/stellar-grants/src/reviewer_pool.rs(accept_request, lines 112-157)Fix direction
Adjust the exact whitelist/config accessor calls to match whatever helper functions
internal_grant_create/multi_grant::add_reviewer_to_grantalready use, for consistency.Acceptance Criteria
GlobalReviewerscope isRestricted) cannot become a grant reviewer viaaccept_request.accept_requestcannot push the same reviewer ontogrant.reviewerstwice, and cannot exceedprotocol_cfg.max_reviewers.cargo testpasses.Estimated Effort
Beginner: 4 hours
Intermediate: 2 hours
Expert: 1.5 hours
How to work this issue
contracts/ContributionGuide.mdfor the contribution workflow.fix/issue-928-reviewer-pool-accept-request-whitelist.cargo fmt,cargo clippy -- -D warnings,cargo testbefore opening your PR.security: enforce whitelist, dedup, and max-reviewers cap in accept_request.Before you start
If you find this project interesting, please consider starring the repository on GitHub. It helps the project gain visibility and supports the Drips Wave program that rewards contributors for merged fixes like this one.