|
| 1 | +#![cfg(test)] |
| 2 | + |
| 3 | +//! Access-control tests for the oracle price feed (issue #1111). |
| 4 | +//! |
| 5 | +//! The contract's purpose is to be a trustworthy price source for fiat-pegged |
| 6 | +//! bounties. Before this fix `update_price` checked only that the caller had |
| 7 | +//! signed for itself, never that it was the registered oracle — so these tests |
| 8 | +//! are mostly about proving the door is now shut, from each direction it was |
| 9 | +//! previously open. |
| 10 | +
|
| 11 | +use super::*; |
| 12 | +use soroban_sdk::testutils::Address as _; |
| 13 | + |
| 14 | +fn setup() -> (Env, OracleContractClient<'static>, Address, Address) { |
| 15 | + let env = Env::default(); |
| 16 | + env.mock_all_auths(); |
| 17 | + let id = env.register_contract(None, OracleContract); |
| 18 | + let client = OracleContractClient::new(&env, &id); |
| 19 | + let admin = Address::generate(&env); |
| 20 | + let oracle = Address::generate(&env); |
| 21 | + (env, client, admin, oracle) |
| 22 | +} |
| 23 | + |
| 24 | +fn price(env: &Env, micro_usd: i128) -> PriceData { |
| 25 | + PriceData { |
| 26 | + price_micro_usd: micro_usd, |
| 27 | + timestamp: env.ledger().timestamp(), |
| 28 | + } |
| 29 | +} |
| 30 | + |
| 31 | +// ── The hole itself ────────────────────────────────────────────────────────── |
| 32 | + |
| 33 | +#[test] |
| 34 | +#[should_panic(expected = "Caller is not the registered oracle")] |
| 35 | +fn arbitrary_address_cannot_push_a_price() { |
| 36 | + let (env, client, admin, oracle) = setup(); |
| 37 | + client.initialize(&admin); |
| 38 | + client.set_oracle(&admin, &oracle); |
| 39 | + |
| 40 | + // Before the fix this succeeded: require_auth() proved only that the |
| 41 | + // attacker controlled its own address, which it always does. |
| 42 | + let attacker = Address::generate(&env); |
| 43 | + client.update_price(&attacker, &price(&env, 999_999)); |
| 44 | +} |
| 45 | + |
| 46 | +#[test] |
| 47 | +#[should_panic(expected = "No oracle registered")] |
| 48 | +fn price_cannot_be_set_before_an_oracle_is_registered() { |
| 49 | + let (env, client, admin, _oracle) = setup(); |
| 50 | + client.initialize(&admin); |
| 51 | + |
| 52 | + // The worst case previously: with no LastPrice there is nothing to deviate |
| 53 | + // from, so the first writer could set *any* positive price, and every later |
| 54 | + // update would be anchored to it — the deviation guard would then protect |
| 55 | + // the attacker's number rather than the real one. |
| 56 | + let attacker = Address::generate(&env); |
| 57 | + client.update_price(&attacker, &price(&env, 1)); |
| 58 | +} |
| 59 | + |
| 60 | +#[test] |
| 61 | +fn registered_oracle_can_push_a_price() { |
| 62 | + let (env, client, admin, oracle) = setup(); |
| 63 | + client.initialize(&admin); |
| 64 | + client.set_oracle(&admin, &oracle); |
| 65 | + |
| 66 | + client.update_price(&oracle, &price(&env, 120_000)); |
| 67 | + |
| 68 | + assert_eq!(client.get_price().price_micro_usd, 120_000); |
| 69 | +} |
| 70 | + |
| 71 | +#[test] |
| 72 | +#[should_panic(expected = "Caller is not the registered oracle")] |
| 73 | +fn previous_oracle_cannot_push_after_being_replaced() { |
| 74 | + let (env, client, admin, oracle) = setup(); |
| 75 | + client.initialize(&admin); |
| 76 | + client.set_oracle(&admin, &oracle); |
| 77 | + client.update_price(&oracle, &price(&env, 120_000)); |
| 78 | + |
| 79 | + // Rotating the oracle must actually revoke the old one. |
| 80 | + let new_oracle = Address::generate(&env); |
| 81 | + client.set_oracle(&admin, &new_oracle); |
| 82 | + client.update_price(&oracle, &price(&env, 121_000)); |
| 83 | +} |
| 84 | + |
| 85 | +// ── Admin gating ───────────────────────────────────────────────────────────── |
| 86 | + |
| 87 | +#[test] |
| 88 | +#[should_panic(expected = "Caller is not the admin")] |
| 89 | +fn non_admin_cannot_register_an_oracle() { |
| 90 | + let (env, client, admin, oracle) = setup(); |
| 91 | + client.initialize(&admin); |
| 92 | + |
| 93 | + // Previously set_oracle's `admin` parameter was self-asserted — the caller |
| 94 | + // passed whichever address it controlled and require_auth() was satisfied. |
| 95 | + let impostor = Address::generate(&env); |
| 96 | + client.set_oracle(&impostor, &oracle); |
| 97 | +} |
| 98 | + |
| 99 | +#[test] |
| 100 | +#[should_panic(expected = "Contract not initialized")] |
| 101 | +fn set_oracle_fails_before_initialize() { |
| 102 | + let (_env, client, admin, oracle) = setup(); |
| 103 | + |
| 104 | + // An uninitialized contract must be closed, not open — an absent admin is |
| 105 | + // not "anyone may proceed". |
| 106 | + client.set_oracle(&admin, &oracle); |
| 107 | +} |
| 108 | + |
| 109 | +#[test] |
| 110 | +#[should_panic(expected = "Contract already initialized")] |
| 111 | +fn initialize_is_once_only() { |
| 112 | + let (env, client, admin, _oracle) = setup(); |
| 113 | + client.initialize(&admin); |
| 114 | + |
| 115 | + // Otherwise anyone could re-initialize and take over as admin. |
| 116 | + client.initialize(&Address::generate(&env)); |
| 117 | +} |
| 118 | + |
| 119 | +#[test] |
| 120 | +fn admin_can_rotate_the_oracle() { |
| 121 | + let (env, client, admin, oracle) = setup(); |
| 122 | + client.initialize(&admin); |
| 123 | + client.set_oracle(&admin, &oracle); |
| 124 | + |
| 125 | + let new_oracle = Address::generate(&env); |
| 126 | + client.set_oracle(&admin, &new_oracle); |
| 127 | + |
| 128 | + assert_eq!(client.get_oracle(), Some(new_oracle.clone())); |
| 129 | + client.update_price(&new_oracle, &price(&env, 120_000)); |
| 130 | + assert_eq!(client.get_price().price_micro_usd, 120_000); |
| 131 | +} |
| 132 | + |
| 133 | +// ── Existing guards still apply to the registered oracle ───────────────────── |
| 134 | + |
| 135 | +#[test] |
| 136 | +#[should_panic(expected = "Price must be positive")] |
| 137 | +fn registered_oracle_still_cannot_push_a_non_positive_price() { |
| 138 | + let (env, client, admin, oracle) = setup(); |
| 139 | + client.initialize(&admin); |
| 140 | + client.set_oracle(&admin, &oracle); |
| 141 | + |
| 142 | + client.update_price(&oracle, &price(&env, 0)); |
| 143 | +} |
| 144 | + |
| 145 | +#[test] |
| 146 | +#[should_panic(expected = "Price deviation exceeds allowed threshold")] |
| 147 | +fn registered_oracle_still_cannot_exceed_the_deviation_bound() { |
| 148 | + let (env, client, admin, oracle) = setup(); |
| 149 | + client.initialize(&admin); |
| 150 | + client.set_oracle(&admin, &oracle); |
| 151 | + client.update_price(&oracle, &price(&env, 100_000)); |
| 152 | + |
| 153 | + // +50%, well beyond MAX_PRICE_DEVIATION_BPS (10%). Authentication must not |
| 154 | + // become a bypass for the sanity checks. |
| 155 | + client.update_price(&oracle, &price(&env, 150_000)); |
| 156 | +} |
| 157 | + |
| 158 | +#[test] |
| 159 | +fn deviation_within_bound_is_accepted() { |
| 160 | + let (env, client, admin, oracle) = setup(); |
| 161 | + client.initialize(&admin); |
| 162 | + client.set_oracle(&admin, &oracle); |
| 163 | + client.update_price(&oracle, &price(&env, 100_000)); |
| 164 | + |
| 165 | + // +5%, inside the bound. |
| 166 | + client.update_price(&oracle, &price(&env, 105_000)); |
| 167 | + assert_eq!(client.get_price().price_micro_usd, 105_000); |
| 168 | +} |
0 commit comments