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
26 changes: 12 additions & 14 deletions dongle-smartcontract/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ make deploy-testnet
- **Verification**: Request and approve project verification
- **Fee Management**: Configurable fees for operations
- **Access Control**: Owner-based permissions
- **Rate Limiting**: Anti-spam protection for write actions with configurable cooldowns
- **TTL Management**: Automatic and manual Time-To-Live extension for persistent storage

## TTL (Time To Live) Management
Expand Down Expand Up @@ -137,6 +138,11 @@ For complete TTL strategy documentation, see [TTL_STRATEGY.md](../TTL_STRATEGY.m
- `set_fee_config` - Configure fees
- `set_treasury` - Set treasury address

### Rate Limiting

- `get_review_action_cooldown_remaining` - Check remaining cooldown for review actions
- `get_verification_request_cooldown_remaining` - Check remaining cooldown for verification requests

## Development

### Using Makefile
Expand Down Expand Up @@ -171,24 +177,16 @@ cargo clippy
cargo clean
```

## Testing
## Rate Limiting

The contract includes comprehensive tests covering:
The contract implements anti-spam protection through cooldown-based rate limiting:

- Project registration and validation
- Ownership and authorization
- Review submission and updates
- Verification workflow
- Fee management
- Edge cases and error handling
- **Review Actions** (add/update/delete): 60-second cooldown per user
- **Verification Requests**: 300-second (5-minute) cooldown per user

Run tests:
```bash
cargo test
```
Rate limits are enforced per user and apply across all projects. Users can check their remaining cooldown time using the provided query functions.

Run specific test:
```bash
This prevents abuse while maintaining reasonable usability for legitimate users.
cargo test test_register_project_success
```

Expand Down
6 changes: 6 additions & 0 deletions dongle-smartcontract/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ pub const RATING_MIN: u32 = 1;
#[allow(dead_code)]
pub const RATING_MAX: u32 = 5;

/// Rate limiting constants (in seconds)
/// Minimum time between review actions (add/update/delete) per user
pub const REVIEW_ACTION_COOLDOWN: u64 = 60; // 1 minute

/// Minimum time between verification requests per user
pub const VERIFICATION_REQUEST_COOLDOWN: u64 = 300; // 5 minutes
// ── TTL (Time To Live) Constants ──────────────────────────────────────────

/// TTL for critical contract data (admin list, fee config, treasury).
Expand Down
2 changes: 2 additions & 0 deletions dongle-smartcontract/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pub enum ContractError {
CannotRemoveLastAdmin = 17,
/// Admin not found
AdminNotFound = 18,
/// Rate limit exceeded - too many actions in short time
RateLimitExceeded = 19,
/// Invalid project name - empty or whitespace only
InvalidProjectName = 19,
/// Invalid project description - empty or whitespace only
Expand Down
11 changes: 11 additions & 0 deletions dongle-smartcontract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod events;
mod fee_manager;
mod project_registry;
pub mod rating_calculator;
mod rate_limiter;
pub mod review_registry;
pub mod storage_keys;
pub mod storage_manager;
Expand Down Expand Up @@ -196,6 +197,16 @@ impl DongleContract {
FeeManager::get_fee_config(&env)
}

// --- Rate Limiting ---

pub fn review_cooldown_remaining(env: Env, user: Address) -> u64 {
crate::rate_limiter::RateLimiter::get_review_action_cooldown_remaining(&env, &user)
}

pub fn verify_cooldown_remaining(env: Env, user: Address) -> u64 {
crate::rate_limiter::RateLimiter::get_verification_request_cooldown_remaining(&env, &user)
}

// --- TTL Management ---

/// Extend TTL for a specific project and its related data
Expand Down
76 changes: 76 additions & 0 deletions dongle-smartcontract/src/rate_limiter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! Rate limiting module to prevent spam and abuse of write actions.
//!
//! This module implements cooldown-based rate limiting for user actions.
//! Each user has separate cooldowns for different action types.

use crate::constants::{REVIEW_ACTION_COOLDOWN, VERIFICATION_REQUEST_COOLDOWN};
use crate::errors::ContractError;
use crate::storage_keys::StorageKey;
use soroban_sdk::{Address, Env};

/// Rate limiter for user actions
pub struct RateLimiter;

impl RateLimiter {
/// Check and enforce rate limit for review actions (add/update/delete)
pub fn check_review_action_cooldown(env: &Env, user: &Address) -> Result<(), ContractError> {
let last_action_key = StorageKey::UserLastReviewAction(user.clone());
let now = env.ledger().timestamp();

if let Some(last_timestamp) = env.storage().persistent().get::<StorageKey, u64>(&last_action_key) {
if now < last_timestamp + REVIEW_ACTION_COOLDOWN {
return Err(ContractError::RateLimitExceeded);
}
}

// Update the last action timestamp
env.storage().persistent().set(&last_action_key, &now);
Ok(())
}

/// Check and enforce rate limit for verification requests
pub fn check_verification_request_cooldown(env: &Env, user: &Address) -> Result<(), ContractError> {
let last_request_key = StorageKey::UserLastVerificationRequest(user.clone());
let now = env.ledger().timestamp();

if let Some(last_timestamp) = env.storage().persistent().get::<StorageKey, u64>(&last_request_key) {
if now < last_timestamp + VERIFICATION_REQUEST_COOLDOWN {
return Err(ContractError::RateLimitExceeded);
}
}

// Update the last request timestamp
env.storage().persistent().set(&last_request_key, &now);
Ok(())
}

/// Get the remaining cooldown time for review actions (in seconds)
/// Returns 0 if no cooldown is active
pub fn get_review_action_cooldown_remaining(env: &Env, user: &Address) -> u64 {
let last_action_key = StorageKey::UserLastReviewAction(user.clone());
let now = env.ledger().timestamp();

if let Some(last_timestamp) = env.storage().persistent().get::<StorageKey, u64>(&last_action_key) {
let cooldown_end = last_timestamp + REVIEW_ACTION_COOLDOWN;
if now < cooldown_end {
return cooldown_end - now;
}
}
0
}

/// Get the remaining cooldown time for verification requests (in seconds)
/// Returns 0 if no cooldown is active
pub fn get_verification_request_cooldown_remaining(env: &Env, user: &Address) -> u64 {
let last_request_key = StorageKey::UserLastVerificationRequest(user.clone());
let now = env.ledger().timestamp();

if let Some(last_timestamp) = env.storage().persistent().get::<StorageKey, u64>(&last_request_key) {
let cooldown_end = last_timestamp + VERIFICATION_REQUEST_COOLDOWN;
if now < cooldown_end {
return cooldown_end - now;
}
}
0
}
}
10 changes: 10 additions & 0 deletions dongle-smartcontract/src/review_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::constants::{RATING_MAX, RATING_MIN};
use crate::errors::ContractError;
use crate::events::publish_review_event;
use crate::rating_calculator::RatingCalculator;
use crate::rate_limiter::RateLimiter;
use crate::storage_keys::StorageKey;
use crate::storage_manager::StorageManager;
use crate::types::{ProjectStats, Review, ReviewAction};
Expand All @@ -22,6 +23,9 @@ impl ReviewRegistry {
// Validation phase
reviewer.require_auth();

// Check rate limit
RateLimiter::check_review_action_cooldown(env, &reviewer)?;

if !(RATING_MIN..=RATING_MAX).contains(&rating) {
return Err(ContractError::InvalidRating);
}
Expand Down Expand Up @@ -119,6 +123,9 @@ impl ReviewRegistry {
// Validation phase
reviewer.require_auth();

// Check rate limit
RateLimiter::check_review_action_cooldown(env, &reviewer)?;

if !(RATING_MIN..=RATING_MAX).contains(&rating) {
return Err(ContractError::InvalidRating);
}
Expand Down Expand Up @@ -192,6 +199,9 @@ impl ReviewRegistry {
// Validation phase
reviewer.require_auth();

// Check rate limit
RateLimiter::check_review_action_cooldown(env, &reviewer)?;

let review_key = StorageKey::Review(project_id, reviewer.clone());
let existing: Review = env
.storage()
Expand Down
4 changes: 4 additions & 0 deletions dongle-smartcontract/src/storage_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ pub enum StorageKey {
Treasury,
/// List of reviewer addresses for a project (by project_id).
ProjectReviews(u64),
/// Last timestamp of review action by user (for rate limiting).
UserLastReviewAction(Address),
/// Last timestamp of verification request by user (for rate limiting).
UserLastVerificationRequest(Address),
}
15 changes: 15 additions & 0 deletions dongle-smartcontract/src/tests/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,18 @@ pub fn assert_project_state(
assert_eq!(project.owner, *expected_owner);
assert_eq!(project.verification_status, expected_status);
}

/// Get a test environment (convenience function).
pub fn get_test_env() -> Env {
Env::default()
}

/// Get a test admin address.
pub fn get_admin(env: &Env) -> Address {
Address::generate(env)
}

/// Get a test user address.
pub fn get_user(env: &Env) -> Address {
Address::generate(env)
}
1 change: 1 addition & 0 deletions dongle-smartcontract/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

// Existing test modules
mod admin;
mod rate_limiting;
mod error_handling_tests;
mod registration;
mod review;
Expand Down
Loading