Skip to content

Commit

Permalink
added execute_update_prices() implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrei Zavgorodnii committed Feb 27, 2025
1 parent f93a812 commit 1c6d467
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 52 deletions.
199 changes: 185 additions & 14 deletions src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}
use cw2::set_contract_version;

use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, MigrateMsg};
use crate::state::{Config, CONFIG, LAST_INDEX};
use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};

use crate::state::{Config, CONFIG, LAST_INDEX, PRICE_HISTORY};
use neutron_std::types::slinky::oracle::v1::{GetPriceResponse, OracleQuerier};
use neutron_std::types::slinky::types::v1::CurrencyPair;

// version info for migration info
const CONTRACT_NAME: &str = "crates.io:neutron-oracle-history";
Expand Down Expand Up @@ -36,7 +39,8 @@ pub fn instantiate(

// Initialize the ring buffer pointer for each pair to 0.
for pair in msg.pairs.iter() {
LAST_INDEX.save(deps.storage, &pair, &0)?;
let key = pair_key(pair);
LAST_INDEX.save(deps.storage, &key, &0)?;
}

Ok(Response::new()
Expand All @@ -51,12 +55,128 @@ pub fn instantiate(

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
_deps: DepsMut,
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::UpdatePrices {} => execute_update_prices(deps, env, info),
ExecuteMsg::UpdateConfig {
pairs,
update_period,
max_blocks_old,
history_size,
} => execute_update_config(
deps,
env,
info,
pairs,
update_period,
max_blocks_old,
history_size,
),
}
}

/// Triggered by the authorized Cron module. For each configured pair, this function:
/// 1. Queries the oracle price.
/// 2. Validates that the price is fresh (not too old) and not nil.
/// 3. Writes a new record to the ring buffer (only one record is read/written per pair).
pub fn execute_update_prices(
deps: DepsMut,
env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;

// Only the authorized address (e.g. Cron module) can trigger this.
if info.sender != config.caller {
return Err(ContractError::Unauthorized {});
}

// Enforce the minimal update period.
if env.block.height < config.last_update + config.update_period {
return Err(ContractError::TooSoon {
expected: config.last_update + config.update_period,
});
}

let mut res = Response::new();

// Process each pair.
for pair in config.pairs.iter() {
let key = pair_key(pair);
// Query the oracle for the price.
let oracle_resp = query_oracle_price(&deps.as_ref(), pair)?;

if let Some(price) = oracle_resp.price {
if !validate_price(
oracle_resp.nonce,
env.block.height,
price.block_height,
config.max_blocks_old,
) {
res = res.add_attribute("skip", key.clone());
continue;
}

// Update the ring buffer for this pair.
let pointer = LAST_INDEX.may_load(deps.storage, &key)?.unwrap_or(0);

PRICE_HISTORY.save(deps.storage, (key.as_str(), pointer), &price)?;

// Update the pointer (wrap around using modulo arithmetic).
let new_pointer = (pointer + 1) % config.history_size;
LAST_INDEX.save(deps.storage, &key, &new_pointer)?;

res = res.add_attribute("updated", key.clone());
} else {
// If there's no oracle response, skip to the next item in the loop.
continue;
}
}

// Update the last update block.
config.last_update = env.block.height;
CONFIG.save(deps.storage, &config)?;
Ok(res)
}

/// Allows the authorized address to update configuration parameters.
pub fn execute_update_config(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: ExecuteMsg,
info: MessageInfo,
pairs: Option<Vec<CurrencyPair>>,
update_period: Option<u64>,
max_blocks_old: Option<u64>,
history_size: Option<u64>,
) -> Result<Response, ContractError> {
unimplemented!()
let mut config = CONFIG.load(deps.storage)?;
if info.sender != &config.owner {
return Err(ContractError::Unauthorized {});
}

if let Some(new_pairs) = pairs {
// Reset pointers for new pairs.
for pair in new_pairs.iter() {
let key = pair_key(pair);
LAST_INDEX.save(deps.storage, &key, &0)?;
}
config.pairs = new_pairs;
}
if let Some(up) = update_period {
config.update_period = up;
}
if let Some(mbo) = max_blocks_old {
config.max_blocks_old = mbo;
}
if let Some(hs) = history_size {
config.history_size = hs;
}
CONFIG.save(deps.storage, &config)?;
Ok(Response::new().add_attribute("action", "update_config"))
}

#[cfg_attr(not(feature = "library"), entry_point)]
Expand All @@ -71,12 +191,53 @@ pub fn query(_deps: Deps, _env: Env, _msg: QueryMsg) -> StdResult<Binary> {
unimplemented!()
}

/// ------------------------------------------------------------------------------------------------
/// HELPERS
/// ------------------------------------------------------------------------------------------------
/// Query the oracle price for a given pair.
pub fn query_oracle_price(
deps: &Deps,
pair: &CurrencyPair,
) -> Result<GetPriceResponse, ContractError> {
let querier = OracleQuerier::new(&deps.querier);
let price: GetPriceResponse = querier.get_price(Some(pair.clone()))?;
Ok(price)
}

pub fn validate_price(
nonce: u64,
current_height: u64,
price_height: u64,
max_blocks_old: u64,
) -> bool {
// Check that the price received more than zero updates from validators.
// TODO(zavgorodnii): maybe introduce a parameter for the desired nonce?
if nonce == 0 {
return false;
}

// Check that the price is not stale.
if current_height.saturating_sub(price_height) > max_blocks_old {
return false;
}

true
}

/// Returns a string key for a currency pair.
fn pair_key(pair: &CurrencyPair) -> String {
format!("{}-{}", pair.base, pair.quote)
}

/// ------------------------------------------------------------------------------------------------
/// TESTS
/// ------------------------------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::{
testing::{mock_dependencies, mock_env, message_info},
};
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};

#[test]
fn test_instantiate_success() {
Expand All @@ -89,14 +250,23 @@ mod tests {
let instantiate_msg = InstantiateMsg {
owner: deps.api.addr_make("owner_addr").into_string(),
caller: deps.api.addr_make("caller_addr").into_string(),
pairs: vec!["pair1".to_string(), "pair2".to_string()],
pairs: vec![
CurrencyPair {
base: "untrn".to_string(),
quote: "usd".to_string(),
},
CurrencyPair {
base: "uatom".to_string(),
quote: "usd".to_string(),
},
],
update_period: 10,
max_blocks_old: 100,
history_size: 5,
};

// Call the instantiate function
let res = instantiate(deps.as_mut(), env.clone(), info, instantiate_msg.clone())
instantiate(deps.as_mut(), env.clone(), info, instantiate_msg.clone())
.expect("contract initialization should succeed");

// Verify that the stored config matches the input data
Expand All @@ -111,10 +281,11 @@ mod tests {

// Verify that ring buffer pointers are initialized for each pair with 0
for pair in &instantiate_msg.pairs {
let key = pair_key(pair);
let idx = LAST_INDEX
.load(&deps.storage, pair)
.load(&deps.storage, &key)
.expect("pair index must be saved");
assert_eq!(idx, 0);
}
}
}
}
27 changes: 0 additions & 27 deletions src/helpers.rs

This file was deleted.

1 change: 0 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
pub mod contract;
mod error;
pub mod helpers;
pub mod msg;
pub mod state;

Expand Down
13 changes: 7 additions & 6 deletions src/msg.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use neutron_std::types::slinky::oracle::v1::QuotePrice;
use crate::state::Config;
use cosmwasm_schema::{cw_serde, QueryResponses};
use crate::state::{Config};
use neutron_std::types::slinky::oracle::v1::QuotePrice;
use neutron_std::types::slinky::types::v1::CurrencyPair;

#[cw_serde]
pub struct InstantiateMsg {
Expand All @@ -9,7 +10,7 @@ pub struct InstantiateMsg {
/// The authorized address (e.g. Cron module) that can trigger price updates.
pub caller: String,
/// The list of currency pairs to track.
pub pairs: Vec<String>,
pub pairs: Vec<CurrencyPair>,
/// Minimal period (in blocks) between updates.
pub update_period: u64,
/// Maximum allowed block age for a price to be "fresh".
Expand All @@ -25,7 +26,7 @@ pub enum ExecuteMsg {
/// Update the configuration (only callable by the authorized address).
UpdateConfig {
/// Optionally update the list of currency pairs.
pairs: Option<Vec<String>>,
pairs: Option<Vec<CurrencyPair>>,
/// Optionally update the update period (in blocks).
update_period: Option<u64>,
/// Optionally update the maximum allowed block age.
Expand All @@ -45,15 +46,15 @@ pub enum QueryMsg {
#[returns(HistoryResponse)]
History {
/// The list of currency pairs to query.
pairs: Vec<String>,
pairs: Vec<CurrencyPair>,
},
}

/// The query response for history queries.
#[cw_serde]
pub struct HistoryResponse {
/// A list of tuples containing the currency pair and its ordered list of price records.
pub histories: Vec<(String, Vec<QuotePrice>)>,
pub histories: Vec<(CurrencyPair, Vec<QuotePrice>)>,
}

/// The query response for the config query.
Expand Down
8 changes: 4 additions & 4 deletions src/state.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use cosmwasm_std::Addr;
use cw_storage_plus::{Item, Map};
use neutron_std::types::slinky::oracle::v1::QuotePrice;
use neutron_std::types::slinky::types::v1::CurrencyPair;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use neutron_std::types::slinky::oracle::v1::QuotePrice;
use cw_storage_plus::{Item, Map};

/// Contract configuration.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
Expand All @@ -12,7 +13,7 @@ pub struct Config {
/// The authorized address (e.g. Cron module) allowed to trigger updates.
pub caller: Addr,
/// The list of currency pairs for which we want to store prices.
pub pairs: Vec<String>,
pub pairs: Vec<CurrencyPair>,
/// Minimal period (in blocks) between price updates.
pub update_period: u64,
/// Maximum number of blocks a price can be old to be considered fresh.
Expand All @@ -32,4 +33,3 @@ pub const LAST_INDEX: Map<&str, u64> = Map::new("last_index");
/// Ring-buffer storage for price records for each pair.
/// The key is a tuple: (pair identifier, index within the ring buffer).
pub const PRICE_HISTORY: Map<(&str, u64), QuotePrice> = Map::new("price_history");

0 comments on commit 1c6d467

Please sign in to comment.