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
4 changes: 0 additions & 4 deletions contracts/admin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,6 @@ pub fn create_proposal(env: &Env, creator: Address, description: String) -> u64
.instance()
.set(&AdminKey::Proposal(id), &proposal);
extend_instance_ttl(env);
extend_storage_ttl_for_key(env, &AdminKey::Proposal(id));
id
}

Expand Down Expand Up @@ -199,7 +198,6 @@ pub fn approve_proposal(env: &Env, admin: Address, proposal_id: u64) {
.instance()
.set(&AdminKey::Proposal(proposal_id), &proposal);
extend_instance_ttl(env);
extend_storage_ttl_for_key(env, &AdminKey::Proposal(proposal_id));
}

pub fn is_proposal_ready(env: &Env, proposal_id: u64) -> bool {
Expand All @@ -209,7 +207,6 @@ pub fn is_proposal_ready(env: &Env, proposal_id: u64) -> bool {
.get(&AdminKey::Proposal(proposal_id))
.expect("proposal not found");
extend_instance_ttl(env);
extend_storage_ttl_for_key(env, &AdminKey::Proposal(proposal_id));
proposal.approvals.len() >= get_threshold(env)
}

Expand All @@ -232,7 +229,6 @@ pub fn mark_executed(env: &Env, proposal_id: u64) {
.instance()
.set(&AdminKey::Proposal(proposal_id), &proposal);
extend_instance_ttl(env);
extend_storage_ttl_for_key(env, &AdminKey::Proposal(proposal_id));
}

#[cfg(test)]
Expand Down
277 changes: 276 additions & 1 deletion contracts/rate-limit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

#![no_std]

use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String};
use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, Env, String};

#[derive(Clone)]
#[contracttype]
Expand Down Expand Up @@ -107,6 +107,69 @@ impl BcForgeRateLimit {
env.storage().instance().set(key, state);
}

fn emit_global_rate_limit_exceeded(
env: &Env,
operation_type: &String,
current_count: u64,
limit: u64,
window_seconds: u64,
) {
env.events().publish(
(symbol_short!("rl_gexcd"),),
(operation_type.clone(), current_count, limit, window_seconds),
);
}

fn emit_address_rate_limit_exceeded(
env: &Env,
address: &Address,
operation_type: &String,
current_count: u64,
limit: u64,
window_seconds: u64,
) {
env.events().publish(
(symbol_short!("rl_aexcd"),),
(
address.clone(),
operation_type.clone(),
current_count,
limit,
window_seconds,
),
);
}

fn emit_global_rate_limit_set(
env: &Env,
operation_type: &String,
limit: u64,
window_seconds: u64,
) {
env.events().publish(
(symbol_short!("rl_gset"),),
(operation_type.clone(), limit, window_seconds),
);
}

fn emit_address_rate_limit_set(
env: &Env,
address: &Address,
operation_type: &String,
limit: u64,
window_seconds: u64,
) {
env.events().publish(
(symbol_short!("rl_aset"),),
(
address.clone(),
operation_type.clone(),
limit,
window_seconds,
),
);
}

/// Check if the operation is allowed based on rate limits
/// Returns true if allowed, false if rate limited
pub fn internal_check_rate_limit(
Expand All @@ -130,6 +193,13 @@ impl BcForgeRateLimit {
);

if global_state.count >= global_config.limit {
Self::emit_global_rate_limit_exceeded(
env,
operation_type,
global_state.count,
global_config.limit,
global_config.window_seconds,
);
return false;
}

Expand All @@ -154,6 +224,14 @@ impl BcForgeRateLimit {
);

if address_state.count >= address_config.limit {
Self::emit_address_rate_limit_exceeded(
env,
addr,
operation_type,
address_state.count,
address_config.limit,
address_config.window_seconds,
);
return false;
}

Expand Down Expand Up @@ -182,6 +260,7 @@ impl BcForgeRateLimit {
env.storage()
.instance()
.set(&DataKey::GlobalRateLimit(operation_type.clone()), &config);
Self::emit_global_rate_limit_set(env, operation_type, limit, window_seconds);
}

/// Set per-address rate limit for an operation type
Expand All @@ -200,6 +279,7 @@ impl BcForgeRateLimit {
&DataKey::AddressRateLimit(address.clone(), operation_type.clone()),
&config,
);
Self::emit_address_rate_limit_set(env, address, operation_type, limit, window_seconds);
}
}

Expand Down Expand Up @@ -249,3 +329,198 @@ impl BcForgeRateLimit {
)
}
}

#[cfg(test)]
mod test {
use super::*;
use soroban_sdk::testutils::Address as _;
use soroban_sdk::testutils::Events as _;
use soroban_sdk::testutils::Ledger as _;
use soroban_sdk::{symbol_short, TryIntoVal, Val, Vec};

fn setup_contract(env: &Env) -> (BcForgeRateLimitClient<'_>, Address) {
let contract_id = env.register(BcForgeRateLimit, ());
let client = BcForgeRateLimitClient::new(env, &contract_id);
(client, contract_id)
}

fn find_event(
events: &soroban_sdk::Vec<(Address, soroban_sdk::Vec<Val>, Val)>,
env: &Env,
symbol: soroban_sdk::Symbol,
) -> soroban_sdk::Vec<(Address, soroban_sdk::Vec<Val>, Val)> {
let mut result: soroban_sdk::Vec<(Address, soroban_sdk::Vec<Val>, Val)> = Vec::new(env);
for i in 0..events.len() {
let event = events.get(i).unwrap();
let topic0: soroban_sdk::Symbol = event.1.get(0).unwrap().try_into_val(env).unwrap();
if topic0 == symbol {
result.push_back(event);
}
}
result
}

#[test]
fn test_global_rate_limit_set_event() {
let env = Env::default();
env.mock_all_auths();
let (client, _contract_id) = setup_contract(&env);
let op = String::from_str(&env, "mint");

client.set_global_rate_limit(&op, &5, &3600);

let events = env.events().all();
let set_events = find_event(&events, &env, symbol_short!("rl_gset"));

assert_eq!(set_events.len(), 1, "expected one rl_gset event");
let (_emitter, _topics, data) = set_events.get(0).unwrap();

let data_vec: soroban_sdk::Vec<Val> = data.try_into_val(&env).unwrap();
assert_eq!(data_vec.len(), 3, "data should have 3 elements");
let op_from_event: String = data_vec.get(0).unwrap().try_into_val(&env).unwrap();
assert_eq!(op_from_event, op);
let limit: u64 = data_vec.get(1).unwrap().try_into_val(&env).unwrap();
assert_eq!(limit, 5);
let window: u64 = data_vec.get(2).unwrap().try_into_val(&env).unwrap();
assert_eq!(window, 3600);
}

#[test]
fn test_address_rate_limit_set_event() {
let env = Env::default();
env.mock_all_auths();
let (client, _contract_id) = setup_contract(&env);
let addr = Address::generate(&env);
let op = String::from_str(&env, "transfer");

client.set_address_rate_limit(&addr, &op, &3, &600);

let events = env.events().all();
let set_events = find_event(&events, &env, symbol_short!("rl_aset"));

assert_eq!(set_events.len(), 1, "expected one rl_aset event");
let (_emitter, _topics, data) = set_events.get(0).unwrap();

let data_vec: soroban_sdk::Vec<Val> = data.try_into_val(&env).unwrap();
assert_eq!(data_vec.len(), 4, "data should have 4 elements");
let addr_from_event: Address = data_vec.get(0).unwrap().try_into_val(&env).unwrap();
assert_eq!(addr_from_event, addr);
let op_from_event: String = data_vec.get(1).unwrap().try_into_val(&env).unwrap();
assert_eq!(op_from_event, op);
let limit: u64 = data_vec.get(2).unwrap().try_into_val(&env).unwrap();
assert_eq!(limit, 3);
let window: u64 = data_vec.get(3).unwrap().try_into_val(&env).unwrap();
assert_eq!(window, 600);
}

#[test]
fn test_global_rate_limit_exceeded_event() {
let env = Env::default();
env.mock_all_auths();
let (client, _contract_id) = setup_contract(&env);
let op = String::from_str(&env, "mint");

client.set_global_rate_limit(&op, &2, &1000);

assert!(client.check_rate_limit(&None, &op, &0));
assert!(client.check_rate_limit(&None, &op, &0));
assert!(!client.check_rate_limit(&None, &op, &0));

let events = env.events().all();
let excd_events = find_event(&events, &env, symbol_short!("rl_gexcd"));

assert_eq!(excd_events.len(), 1, "expected one rl_gexcd event");
let (_emitter, _topics, data) = excd_events.get(0).unwrap();

let data_vec: soroban_sdk::Vec<Val> = data.try_into_val(&env).unwrap();
assert_eq!(data_vec.len(), 4, "data should have 4 elements");
let op_from_event: String = data_vec.get(0).unwrap().try_into_val(&env).unwrap();
assert_eq!(op_from_event, op);
let count: u64 = data_vec.get(1).unwrap().try_into_val(&env).unwrap();
assert_eq!(count, 2);
let limit: u64 = data_vec.get(2).unwrap().try_into_val(&env).unwrap();
assert_eq!(limit, 2);
let window: u64 = data_vec.get(3).unwrap().try_into_val(&env).unwrap();
assert_eq!(window, 1000);
}

#[test]
fn test_address_rate_limit_exceeded_event() {
let env = Env::default();
env.mock_all_auths();
let (client, _contract_id) = setup_contract(&env);
let addr = Address::generate(&env);
let op = String::from_str(&env, "transfer");

client.set_address_rate_limit(&addr, &op, &1, &500);

assert!(client.check_rate_limit(&Some(addr.clone()), &op, &0));
assert!(!client.check_rate_limit(&Some(addr.clone()), &op, &0));

let events = env.events().all();
let excd_events = find_event(&events, &env, symbol_short!("rl_aexcd"));

assert_eq!(excd_events.len(), 1, "expected one rl_aexcd event");
let (_emitter, _topics, data) = excd_events.get(0).unwrap();

let data_vec: soroban_sdk::Vec<Val> = data.try_into_val(&env).unwrap();
assert_eq!(data_vec.len(), 5, "data should have 5 elements");
let addr_from_event: Address = data_vec.get(0).unwrap().try_into_val(&env).unwrap();
assert_eq!(addr_from_event, addr);
let op_from_event: String = data_vec.get(1).unwrap().try_into_val(&env).unwrap();
assert_eq!(op_from_event, op);
let count: u64 = data_vec.get(2).unwrap().try_into_val(&env).unwrap();
assert_eq!(count, 1);
let limit: u64 = data_vec.get(3).unwrap().try_into_val(&env).unwrap();
assert_eq!(limit, 1);
let window: u64 = data_vec.get(4).unwrap().try_into_val(&env).unwrap();
assert_eq!(window, 500);
}

#[test]
fn test_rate_limit_exceeded_limit_one() {
let env = Env::default();
env.mock_all_auths();
let (client, _contract_id) = setup_contract(&env);
let op = String::from_str(&env, "mint");

client.set_global_rate_limit(&op, &1, &1000);

assert!(client.check_rate_limit(&None, &op, &0));
assert!(!client.check_rate_limit(&None, &op, &0));

let events = env.events().all();
let excd_events = find_event(&events, &env, symbol_short!("rl_gexcd"));
assert_eq!(
excd_events.len(),
1,
"expected rl_gexcd for global limit exceeded"
);
}

#[test]
fn test_rate_limit_window_reset() {
let env = Env::default();
env.mock_all_auths();
let (client, _contract_id) = setup_contract(&env);
let op = String::from_str(&env, "mint");

env.ledger().set_timestamp(1000);

client.set_global_rate_limit(&op, &1, &100);

assert!(client.check_rate_limit(&None, &op, &0));
assert!(!client.check_rate_limit(&None, &op, &0));

let events = env.events().all();
let excd = find_event(&events, &env, symbol_short!("rl_gexcd"));
assert_eq!(excd.len(), 1, "rl_gexcd emitted when limit exceeded");

env.ledger().set_timestamp(2000);

assert!(
client.check_rate_limit(&None, &op, &0),
"window reset should allow operation"
);
}
}
Loading