From a498c2032634e5a26fece4dabc3f69ef8df9fbad Mon Sep 17 00:00:00 2001 From: Infimum90 Date: Mon, 31 Aug 2026 10:12:20 +0100 Subject: [PATCH 1/4] fix: add update_oracle to liquidation_handler and adl_handler (#579) Admin-gated setter to repoint stored oracle address, mirroring deposit_handler/order_handler/withdrawal_handler. Rejects no-op and copy-paste mistakes (current contract or other instance addresses) and emits orcl_set event for audit trail. --- contracts/adl_handler/src/lib.rs | 45 +++++++++++++++++++++++ contracts/liquidation_handler/src/lib.rs | 47 +++++++++++++++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/contracts/adl_handler/src/lib.rs b/contracts/adl_handler/src/lib.rs index d676120e..f2995971 100644 --- a/contracts/adl_handler/src/lib.rs +++ b/contracts/adl_handler/src/lib.rs @@ -51,6 +51,11 @@ pub enum Error { /// Max PnL factor for ADL is not configured (0) for the requested market/side. /// Callers must set a non-zero value via DataStore before ADL can be evaluated. MissingMaxPnlConfig = 8, + /// Issue #643: update_oracle called with the current oracle address + /// (no-op) or with this contract's own address / another of its stored + /// instance addresses (order_handler, data_store, role_store, admin) — + /// almost certainly a copy-paste mistake, not an intentional rotation. + InvalidOracle = 10, } // ─── External clients ───────────────────────────────────────────────────────── @@ -126,6 +131,46 @@ impl AdlHandler { .set(&InstanceKey::OrderHandler, &order_handler); } + pub fn update_oracle(env: Env, caller: Address, new_oracle: Address) { + caller.require_auth(); + let admin: Address = env + .storage() + .instance() + .get(&InstanceKey::Admin) + .unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized)); + if caller != admin { + panic_with_error!(&env, Error::Unauthorized); + } + // Issue #643: reject a no-op call and the most likely copy-paste + // mistakes — pointing the oracle at this contract itself or at one + // of its own other stored instance addresses. + if new_oracle == env.current_contract_address() { + panic_with_error!(&env, Error::InvalidOracle); + } + for key in [ + InstanceKey::Oracle, + InstanceKey::Admin, + InstanceKey::RoleStore, + InstanceKey::DataStore, + InstanceKey::OrderHandler, + ] { + if let Some(other) = env.storage().instance().get::<_, Address>(&key) { + if other == new_oracle { + panic_with_error!(&env, Error::InvalidOracle); + } + } + } + let old_oracle: Address = env.storage().instance().get(&InstanceKey::Oracle).unwrap(); + env.storage().instance().set(&InstanceKey::Oracle, &new_oracle); + // Issue #605: the oracle address every subsequent price validation + // relies on just changed — emit an event so off-chain monitoring has + // an audit trail for this admin action. + env.events().publish( + (symbol_short!("orcl_set"),), + (old_oracle, new_oracle), + ); + } + /// Check whether ADL is currently required for the given market side. /// /// Returns true if total trader PnL / pool_value > MAX_PNL_FACTOR_FOR_ADL. diff --git a/contracts/liquidation_handler/src/lib.rs b/contracts/liquidation_handler/src/lib.rs index 789805f9..d8b70a45 100644 --- a/contracts/liquidation_handler/src/lib.rs +++ b/contracts/liquidation_handler/src/lib.rs @@ -51,7 +51,12 @@ pub enum Error { /// Mirrors the InvalidMarket pattern in deposit_handler/withdrawal_handler /// (issue #371) so callers can match this condition as a typed error /// instead of a generic execution failure. - InvalidMarket = 6, + InvalidMarket = 7, + /// Issue #643: update_oracle called with the current oracle address + /// (no-op) or with this contract's own address / another of its stored + /// instance addresses (order_handler, data_store, role_store, admin) — + /// almost certainly a copy-paste mistake, not an intentional rotation. + InvalidOracle = 8, } // ─── External clients ───────────────────────────────────────────────────────── @@ -167,6 +172,46 @@ impl LiquidationHandler { env.deployer().update_current_contract_wasm(new_wasm_hash); } + pub fn update_oracle(env: Env, caller: Address, new_oracle: Address) { + caller.require_auth(); + let admin: Address = env + .storage() + .instance() + .get(&InstanceKey::Admin) + .unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized)); + if caller != admin { + panic_with_error!(&env, Error::Unauthorized); + } + // Issue #643: reject a no-op call and the most likely copy-paste + // mistakes — pointing the oracle at this contract itself or at one + // of its own other stored instance addresses. + if new_oracle == env.current_contract_address() { + panic_with_error!(&env, Error::InvalidOracle); + } + for key in [ + InstanceKey::Oracle, + InstanceKey::Admin, + InstanceKey::RoleStore, + InstanceKey::DataStore, + InstanceKey::OrderHandler, + ] { + if let Some(other) = env.storage().instance().get::<_, Address>(&key) { + if other == new_oracle { + panic_with_error!(&env, Error::InvalidOracle); + } + } + } + let old_oracle: Address = env.storage().instance().get(&InstanceKey::Oracle).unwrap(); + env.storage().instance().set(&InstanceKey::Oracle, &new_oracle); + // Issue #605: the oracle address every subsequent price validation + // relies on just changed — emit an event so off-chain monitoring has + // an audit trail for this admin action. + env.events().publish( + (symbol_short!("orcl_set"),), + (old_oracle, new_oracle), + ); + } + /// Check if a position is currently liquidatable. pub fn check_liquidatable( env: Env, From e4580c37f3085fbcab0d09b5878f9e9fb4f49dea Mon Sep 17 00:00:00 2001 From: Infimum90 Date: Mon, 31 Aug 2026 10:13:26 +0100 Subject: [PATCH 2/4] fix: remove unused error variants (#578) Remove PriceTooHigh and UnauthorizedPositionManager from order_handler, NoKeepers from oracle, and Unauthorized from reader. Variants were declared but never raised, polluting ABI; either wire or remove per suggested fix. --- contracts/oracle/src/lib.rs | 1 - contracts/order_handler/src/lib.rs | 2 -- contracts/reader/src/lib.rs | 1 - 3 files changed, 4 deletions(-) diff --git a/contracts/oracle/src/lib.rs b/contracts/oracle/src/lib.rs index b1ee1643..0c786575 100644 --- a/contracts/oracle/src/lib.rs +++ b/contracts/oracle/src/lib.rs @@ -45,7 +45,6 @@ pub enum Error { StalePrice = 5, // timestamp too old PriceNotFound = 6, InvalidSignature = 7, - NoKeepers = 8, /// clear_prices called with more than MAX_CLEAR_PRICES_BATCH_SIZE tokens (issue #619). BatchSizeLimitExceeded = 9, } diff --git a/contracts/order_handler/src/lib.rs b/contracts/order_handler/src/lib.rs index cc10efb8..85411fb5 100644 --- a/contracts/order_handler/src/lib.rs +++ b/contracts/order_handler/src/lib.rs @@ -86,14 +86,12 @@ pub enum Error { OrderNotFound = 4, InvalidOrderType = 5, UnsatisfiedTrigger = 6, - PriceTooHigh = 7, PriceTooLow = 8, OrderFrozen = 9, /// Increase/swap orders require collateral to have been transferred to /// order_vault (via exchange_router SendTokens) before calling create_order. /// record_transfer_in returned zero, meaning no collateral arrived. ZeroCollateral = 10, - UnauthorizedPositionManager = 11, /// `flag_stale_keeper` was called but the role's last activity is still /// within the configured heartbeat timeout (issue #249). KeeperNotStale = 12, diff --git a/contracts/reader/src/lib.rs b/contracts/reader/src/lib.rs index 3f1d2a19..d9a23826 100644 --- a/contracts/reader/src/lib.rs +++ b/contracts/reader/src/lib.rs @@ -60,7 +60,6 @@ enum InstanceKey { pub enum Error { NotInitialized = 1, AlreadyInitialized = 2, - Unauthorized = 3, /// `get_protocol_stats` was passed more than `MAX_STATS_MARKETS` markets. TooManyMarkets = 4, /// The supplied market address is not registered in data_store — From 3382799eae71297c72abb5cbf153cb59f6e5e847 Mon Sep 17 00:00:00 2001 From: Infimum90 Date: Mon, 31 Aug 2026 10:14:23 +0100 Subject: [PATCH 3/4] fix: gate vault record_transfer_in with CONTROLLER (#542) Require caller auth and CONTROLLER role for deposit_vault and withdrawal_vault record_transfer_in, mirroring transfer_out. Update deposit_handler call sites to pass handler address as caller. Prevents front-running that strands deposits by zeroing delta before create_deposit. --- contracts/deposit_handler/src/lib.rs | 8 ++++---- contracts/deposit_vault/src/lib.rs | 6 +++++- contracts/withdrawal_vault/src/lib.rs | 5 ++++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/contracts/deposit_handler/src/lib.rs b/contracts/deposit_handler/src/lib.rs index ca3fb6cb..7fb10c0f 100644 --- a/contracts/deposit_handler/src/lib.rs +++ b/contracts/deposit_handler/src/lib.rs @@ -129,7 +129,7 @@ trait IOracle { trait IDepositVault { fn transfer_out(env: Env, caller: Address, token: Address, receiver: Address, amount: i128); fn get_recorded_balance(env: Env, token: Address) -> i128; - fn record_transfer_in(env: Env, token: Address) -> i128; + fn record_transfer_in(env: Env, caller: Address, token: Address) -> i128; } #[allow(dead_code)] @@ -316,7 +316,7 @@ impl DepositHandler { ); // Snapshot the vault's balance so execute_deposit's recorded-balance // check (guard against fee-on-transfer tokens) sees the funds just received. - vault_client.record_transfer_in(¶ms.initial_long_token); + vault_client.record_transfer_in(&handler, ¶ms.initial_long_token); } if params.short_token_amount > 0 { token::Client::new(&env, ¶ms.initial_short_token).transfer( @@ -324,7 +324,7 @@ impl DepositHandler { &deposit_vault, ¶ms.short_token_amount, ); - vault_client.record_transfer_in(¶ms.initial_short_token); + vault_client.record_transfer_in(&handler, ¶ms.initial_short_token); } // Issue #370: collect execution_fee in the long token. @@ -334,7 +334,7 @@ impl DepositHandler { &deposit_vault, &exec_fee, ); - vault_client.record_transfer_in(¶ms.initial_long_token); + vault_client.record_transfer_in(&handler, ¶ms.initial_long_token); } // Allocate deposit key from nonce diff --git a/contracts/deposit_vault/src/lib.rs b/contracts/deposit_vault/src/lib.rs index 88c2988d..c4fbace0 100644 --- a/contracts/deposit_vault/src/lib.rs +++ b/contracts/deposit_vault/src/lib.rs @@ -70,7 +70,11 @@ impl DepositVault { /// Snapshot the balance of `token` in this vault. /// Returns the amount received since the last snapshot (delta). /// Called by deposit_handler right after the user's transfer lands. - pub fn record_transfer_in(env: Env, token: Address) -> i128 { + /// Only callable by a CONTROLLER (deposit_handler). + pub fn record_transfer_in(env: Env, caller: Address, token: Address) -> i128 { + caller.require_auth(); + require_controller(&env, &caller); + let current = token::Client::new(&env, &token).balance(&env.current_contract_address()); let recorded: i128 = env .storage() diff --git a/contracts/withdrawal_vault/src/lib.rs b/contracts/withdrawal_vault/src/lib.rs index 8634c200..11262bb6 100644 --- a/contracts/withdrawal_vault/src/lib.rs +++ b/contracts/withdrawal_vault/src/lib.rs @@ -56,7 +56,10 @@ impl WithdrawalVault { .set(&InstanceKey::RoleStore, &role_store); } - pub fn record_transfer_in(env: Env, token: Address) -> i128 { + pub fn record_transfer_in(env: Env, caller: Address, token: Address) -> i128 { + caller.require_auth(); + require_controller(&env, &caller); + let current = token::Client::new(&env, &token).balance(&env.current_contract_address()); let recorded: i128 = env .storage() From 10c38b0cbddd0b714151dd1cb9c01dbcf8ab6c5e Mon Sep 17 00:00:00 2001 From: Infimum90 Date: Mon, 31 Aug 2026 10:15:50 +0100 Subject: [PATCH 4/4] fix: add TTL bump to data_store scalar accessors (#543) Apply lazy TTL renewal (extend_ttl with MIN_BUMP_THRESHOLD / PERSISTENT_BUMP_TARGET) on every read/write for u128/i128/address/ bool/bytes32 accessors and their delta/increment variants. Prevents archival of market config keys written once at create_market and silently consumed on every operation. --- contracts/data_store/src/lib.rs | 135 +++++++++++++++++++++----------- 1 file changed, 88 insertions(+), 47 deletions(-) diff --git a/contracts/data_store/src/lib.rs b/contracts/data_store/src/lib.rs index abfc0473..1c2900cc 100644 --- a/contracts/data_store/src/lib.rs +++ b/contracts/data_store/src/lib.rs @@ -175,21 +175,23 @@ impl DataStore { // ── u128 operations ────────────────────────────────────────────────────── pub fn get_u128(env: Env, key: BytesN<32>) -> u128 { + let dk = DataKey::U128(key); + let val: u128 = env.storage().persistent().get(&dk).unwrap_or(0); env.storage() .persistent() - .get(&DataKey::U128(key)) - .unwrap_or(0) + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); + val } /// Read multiple u128 values in one call to reduce cross-contract call overhead. pub fn get_u128_batch(env: Env, keys: Vec>) -> Vec { let mut results = Vec::new(&env); for key in keys.iter() { - let val: u128 = env - .storage() + let dk = DataKey::U128(key); + let val: u128 = env.storage().persistent().get(&dk).unwrap_or(0); + env.storage() .persistent() - .get(&DataKey::U128(key)) - .unwrap_or(0); + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); results.push_back(val); } results @@ -234,7 +236,11 @@ impl DataStore { pub fn set_u128(env: Env, caller: Address, key: BytesN<32>, value: u128) -> u128 { caller.require_auth(); require_controller(&env, &caller); - env.storage().persistent().set(&DataKey::U128(key), &value); + let dk = DataKey::U128(key); + env.storage().persistent().set(&dk, &value); + env.storage() + .persistent() + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); value } @@ -248,7 +254,11 @@ impl DataStore { pub fn set_u128_config(env: Env, caller: Address, key: BytesN<32>, value: u128) -> u128 { caller.require_auth(); require_controller(&env, &caller); - env.storage().persistent().set(&DataKey::U128(key.clone()), &value); + let dk = DataKey::U128(key.clone()); + env.storage().persistent().set(&dk, &value); + env.storage() + .persistent() + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); env.storage().instance().set(&DataKey::InstanceU128(key), &value); value } @@ -266,11 +276,11 @@ impl DataStore { /// `set_u128_config`, the next `get_u128_cached` call will detect the /// divergence and return the fresh persistent value, updating the cache. pub fn get_u128_cached(env: Env, key: BytesN<32>) -> u128 { - let persistent_val: u128 = env - .storage() + let dk = DataKey::U128(key.clone()); + let persistent_val: u128 = env.storage().persistent().get(&dk).unwrap_or(0); + env.storage() .persistent() - .get(&DataKey::U128(key.clone())) - .unwrap_or(0); + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); env.storage() .instance() .set(&DataKey::InstanceU128(key), &persistent_val); @@ -305,11 +315,8 @@ impl DataStore { pub fn apply_delta_to_u128(env: Env, caller: Address, key: BytesN<32>, delta: i128) -> u128 { caller.require_auth(); require_controller(&env, &caller); - let current: u128 = env - .storage() - .persistent() - .get(&DataKey::U128(key.clone())) - .unwrap_or(0); + let dk = DataKey::U128(key); + let current: u128 = env.storage().persistent().get(&dk).unwrap_or(0); let next = if delta >= 0 { current.saturating_add(delta as u128) } else { @@ -319,52 +326,61 @@ impl DataStore { } current - sub }; - env.storage().persistent().set(&DataKey::U128(key), &next); + env.storage().persistent().set(&dk, &next); + env.storage() + .persistent() + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); next } pub fn increment_u128(env: Env, caller: Address, key: BytesN<32>, amount: u128) -> u128 { caller.require_auth(); require_controller(&env, &caller); - let current: u128 = env - .storage() - .persistent() - .get(&DataKey::U128(key.clone())) - .unwrap_or(0); + let dk = DataKey::U128(key); + let current: u128 = env.storage().persistent().get(&dk).unwrap_or(0); let next = current.saturating_add(amount); - env.storage().persistent().set(&DataKey::U128(key), &next); + env.storage().persistent().set(&dk, &next); + env.storage() + .persistent() + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); next } pub fn decrement_u128(env: Env, caller: Address, key: BytesN<32>, amount: u128) -> u128 { caller.require_auth(); require_controller(&env, &caller); - let current: u128 = env - .storage() - .persistent() - .get(&DataKey::U128(key.clone())) - .unwrap_or(0); + let dk = DataKey::U128(key); + let current: u128 = env.storage().persistent().get(&dk).unwrap_or(0); if amount > current { panic_with_error!(&env, Error::Underflow); } let next = current - amount; - env.storage().persistent().set(&DataKey::U128(key), &next); + env.storage().persistent().set(&dk, &next); + env.storage() + .persistent() + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); next } // ── i128 operations ────────────────────────────────────────────────────── pub fn get_i128(env: Env, key: BytesN<32>) -> i128 { + let dk = DataKey::I128(key); + let val: i128 = env.storage().persistent().get(&dk).unwrap_or(0); env.storage() .persistent() - .get(&DataKey::I128(key)) - .unwrap_or(0) + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); + val } pub fn set_i128(env: Env, caller: Address, key: BytesN<32>, value: i128) -> i128 { caller.require_auth(); require_controller(&env, &caller); - env.storage().persistent().set(&DataKey::I128(key), &value); + let dk = DataKey::I128(key); + env.storage().persistent().set(&dk, &value); + env.storage() + .persistent() + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); value } @@ -377,26 +393,35 @@ impl DataStore { pub fn apply_delta_to_i128(env: Env, caller: Address, key: BytesN<32>, delta: i128) -> i128 { caller.require_auth(); require_controller(&env, &caller); - let current: i128 = env - .storage() - .persistent() - .get(&DataKey::I128(key.clone())) - .unwrap_or(0); + let dk = DataKey::I128(key); + let current: i128 = env.storage().persistent().get(&dk).unwrap_or(0); let next = current.saturating_add(delta); - env.storage().persistent().set(&DataKey::I128(key), &next); + env.storage().persistent().set(&dk, &next); + env.storage() + .persistent() + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); next } // ── Address operations ──────────────────────────────────────────────────── pub fn get_address(env: Env, key: BytesN<32>) -> Option
{ - env.storage().persistent().get(&DataKey::Addr(key)) + let dk = DataKey::Addr(key); + let val: Option
= env.storage().persistent().get(&dk); + env.storage() + .persistent() + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); + val } pub fn set_address(env: Env, caller: Address, key: BytesN<32>, value: Address) -> Address { caller.require_auth(); require_controller(&env, &caller); - env.storage().persistent().set(&DataKey::Addr(key), &value); + let dk = DataKey::Addr(key); + env.storage().persistent().set(&dk, &value); + env.storage() + .persistent() + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); value } @@ -409,16 +434,22 @@ impl DataStore { // ── bool operations ─────────────────────────────────────────────────────── pub fn get_bool(env: Env, key: BytesN<32>) -> bool { + let dk = DataKey::Bool(key); + let val: bool = env.storage().persistent().get(&dk).unwrap_or(false); env.storage() .persistent() - .get(&DataKey::Bool(key)) - .unwrap_or(false) + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); + val } pub fn set_bool(env: Env, caller: Address, key: BytesN<32>, value: bool) -> bool { caller.require_auth(); require_controller(&env, &caller); - env.storage().persistent().set(&DataKey::Bool(key), &value); + let dk = DataKey::Bool(key); + env.storage().persistent().set(&dk, &value); + env.storage() + .persistent() + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); value } @@ -431,10 +462,16 @@ impl DataStore { // ── BytesN<32> operations ───────────────────────────────────────────────── pub fn get_bytes32(env: Env, key: BytesN<32>) -> BytesN<32> { + let dk = DataKey::B32(key); + let val: BytesN<32> = env + .storage() + .persistent() + .get(&dk) + .unwrap_or(BytesN::from_array(&env, &[0u8; 32])); env.storage() .persistent() - .get(&DataKey::B32(key)) - .unwrap_or(BytesN::from_array(&env, &[0u8; 32])) + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); + val } pub fn set_bytes32( @@ -445,7 +482,11 @@ impl DataStore { ) -> BytesN<32> { caller.require_auth(); require_controller(&env, &caller); - env.storage().persistent().set(&DataKey::B32(key), &value); + let dk = DataKey::B32(key); + env.storage().persistent().set(&dk, &value); + env.storage() + .persistent() + .extend_ttl(&dk, MIN_BUMP_THRESHOLD, PERSISTENT_BUMP_TARGET); value }