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
10 changes: 10 additions & 0 deletions acbu_oracle/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,16 @@ impl OracleContract {
}
}

if sources.len() > 0 {
let min_sigs: u32 = env
.storage()
.instance()
.get(&DATA_KEY.min_signatures)
.unwrap();
let required = min_sigs.max(MIN_ORACLE_SOURCE_FEEDS);
if sources.len() < required {
env.panic_with_error(OracleError::InsufficientOracleSources);
}
Comment thread
hartz0 marked this conversation as resolved.
let min_sigs: u32 = env
.storage()
.instance()
Expand Down
139 changes: 139 additions & 0 deletions acbu_oracle/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -699,3 +699,142 @@ fn test_rate_not_initialized_before_first_submission() {
let result = client.try_get_acbu_usd_rate_with_timestamp();
assert!(result.is_err(), "get_acbu_usd_rate_with_timestamp should fail before first submission");
}

// ─── Oracle quorum / InsufficientOracleSources tests (#319) ──────────────────

/// Submitting sources.len() > 0 but below max(min_signatures, MIN_ORACLE_SOURCE_FEEDS=3)
/// must panic with InsufficientOracleSources (#7009).
#[test]
#[should_panic]
Comment thread
hartz0 marked this conversation as resolved.
fn test_update_rate_too_few_sources_panics() {
let env = Env::default();
env.mock_all_auths();
env.ledger().with_mut(|l| l.timestamp = 1_000_000);

let admin = Address::generate(&env);
let validator = Address::generate(&env);
let mut validators = Vec::new(&env);
validators.push_back(validator.clone());

let ngn = CurrencyCode::new(&env, "NGN");
let mut currencies = Vec::new(&env);
currencies.push_back(ngn.clone());
let mut basket_weights = Map::new(&env);
basket_weights.set(ngn.clone(), 10_000i128);

let contract_id = env.register_contract(None, OracleContract);
let client = OracleContractClient::new(&env, &contract_id);
// min_signatures = 1, but MIN_ORACLE_SOURCE_FEEDS = 3, so required = max(1,3) = 3
client.initialize(&admin, &validators, &1u32, &currencies, &basket_weights);

let mut two_sources = Vec::new(&env);
two_sources.push_back(1_000_000i128);
two_sources.push_back(1_000_000i128);
// 2 sources < 3 required → must panic
client.update_rate(&validator, &ngn, &1_000_000i128, &two_sources, &env.ledger().timestamp());
}

/// Submitting exactly MIN_ORACLE_SOURCE_FEEDS sources must succeed.
#[test]
fn test_update_rate_exactly_min_sources_succeeds() {
let env = Env::default();
env.mock_all_auths();
env.ledger().with_mut(|l| l.timestamp = 1_000_000);

let admin = Address::generate(&env);
let validator = Address::generate(&env);
let mut validators = Vec::new(&env);
validators.push_back(validator.clone());

let ngn = CurrencyCode::new(&env, "NGN");
let mut currencies = Vec::new(&env);
currencies.push_back(ngn.clone());
let mut basket_weights = Map::new(&env);
basket_weights.set(ngn.clone(), 10_000i128);

let contract_id = env.register_contract(None, OracleContract);
let client = OracleContractClient::new(&env, &contract_id);
client.initialize(&admin, &validators, &1u32, &currencies, &basket_weights);

// Exactly 3 sources (MIN_ORACLE_SOURCE_FEEDS) — must not panic.
let mut three_sources = Vec::new(&env);
three_sources.push_back(1_000_000i128);
three_sources.push_back(1_001_000i128);
three_sources.push_back(999_000i128);
client.update_rate(&validator, &ngn, &1_000_000i128, &three_sources, &env.ledger().timestamp());

let stored = client.get_rate(&ngn);
// median of [999_000, 1_000_000, 1_001_000] = 1_000_000
assert_eq!(stored, 1_000_000, "median of 3 sources should be 1_000_000");
}

/// Submitting zero sources must still be accepted (bypass path — rate param is used directly).
#[test]
fn test_update_rate_zero_sources_bypasses_quorum_check() {
let env = Env::default();
env.mock_all_auths();
env.ledger().with_mut(|l| l.timestamp = 1_000_000);

let admin = Address::generate(&env);
let validator = Address::generate(&env);
let mut validators = Vec::new(&env);
validators.push_back(validator.clone());

let ngn = CurrencyCode::new(&env, "NGN");
let mut currencies = Vec::new(&env);
currencies.push_back(ngn.clone());
let mut basket_weights = Map::new(&env);
basket_weights.set(ngn.clone(), 10_000i128);

let contract_id = env.register_contract(None, OracleContract);
let client = OracleContractClient::new(&env, &contract_id);
client.initialize(&admin, &validators, &1u32, &currencies, &basket_weights);

// Empty sources: quorum guard is skipped entirely; rate param is stored as-is.
let empty_sources = Vec::new(&env);
let submitted_rate = 1_234_567i128;
client.update_rate(&validator, &ngn, &submitted_rate, &empty_sources, &env.ledger().timestamp());

assert_eq!(client.get_rate(&ngn), submitted_rate, "empty sources should store the rate param directly");
}

/// When min_signatures > MIN_ORACLE_SOURCE_FEEDS the larger threshold applies.
#[test]
#[should_panic]
fn test_update_rate_min_sigs_larger_than_min_feeds_is_enforced() {
let env = Env::default();
env.mock_all_auths();
env.ledger().with_mut(|l| l.timestamp = 1_000_000);

let admin = Address::generate(&env);
let v1 = Address::generate(&env);
let v2 = Address::generate(&env);
let v3 = Address::generate(&env);
let v4 = Address::generate(&env);
let v5 = Address::generate(&env);

let mut validators = Vec::new(&env);
validators.push_back(v1.clone());
validators.push_back(v2.clone());
validators.push_back(v3.clone());
validators.push_back(v4.clone());
validators.push_back(v5.clone());

let ngn = CurrencyCode::new(&env, "NGN");
let mut currencies = Vec::new(&env);
currencies.push_back(ngn.clone());
let mut basket_weights = Map::new(&env);
basket_weights.set(ngn.clone(), 10_000i128);

let contract_id = env.register_contract(None, OracleContract);
let client = OracleContractClient::new(&env, &contract_id);
// min_signatures = 5 > MIN_ORACLE_SOURCE_FEEDS = 3, so required = 5
client.initialize(&admin, &validators, &5u32, &currencies, &basket_weights);

// Only 3 sources — below min_signatures=5 threshold → must panic
let mut three_sources = Vec::new(&env);
three_sources.push_back(1_000_000i128);
three_sources.push_back(1_001_000i128);
three_sources.push_back(999_000i128);
client.update_rate(&v1, &ngn, &1_000_000i128, &three_sources, &env.ledger().timestamp());
}
1 change: 1 addition & 0 deletions acbu_reserve_tracker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub enum ReserveTrackerError {
AdminTimelockNotElapsed = 8005,
NoPendingAdminToCancel = 8006,
Unauthorized = 8007,
DuplicateCurrency = 8008,
AttestationNotFound = 8008,
InvalidMerkleProof = 8009,
InvalidCustodian = 8010,
Expand Down
2 changes: 1 addition & 1 deletion acbu_reserve_tracker/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -743,4 +743,4 @@ fn test_get_latest_attestation_before_submit_panics() {
result.is_err(),
"get_latest_attestation must panic when no attestation has been submitted"
);
}
}
Loading