Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Osmosis price oracle #155

Merged
merged 18 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from 12 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
347 changes: 312 additions & 35 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["packages/*", "contracts/provider/*", "contracts/consumer/*"]
members = ["packages/*", "contracts/provider/*", "contracts/consumer/*", "contracts/osmosis-price-provider"]
resolver = "2"

[workspace.package]
Expand Down Expand Up @@ -31,6 +31,7 @@ cosmwasm-storage = "1.3.3"
cw-storage-plus = "1.1.0"
cw-utils = "1.0.1"
cw2 = "1.0.1"
osmosis-std = "0.20.1"
schemars = "0.8.11"
serde = { version = "1.0.152", default-features = false, features = ["derive"] }
thiserror = "1.0.38"
Expand Down
4 changes: 4 additions & 0 deletions contracts/consumer/remote-price-feed/.cargo/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[alias]
wasm = "build --release --lib --target wasm32-unknown-unknown"
unit-test = "test --lib"
schema = "run --bin schema"
44 changes: 44 additions & 0 deletions contracts/consumer/remote-price-feed/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[package]
name = "mesh-remote-price-feed"
description = "Returns exchange rates of assets synchronized with an Osmosis price provider"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }

[lib]
crate-type = ["cdylib", "rlib"]

[features]
# for more explicit tests, cargo test --features=backtraces
backtraces = ["cosmwasm-std/backtraces"]
# use library feature to disable all instantiate/execute/query exports
library = []
# enables generation of mt utilities
mt = ["library", "sylvia/mt"]

[dependencies]
mesh-apis = { workspace = true }
mesh-bindings = { workspace = true }

sylvia = { workspace = true }
cosmwasm-schema = { workspace = true }
cosmwasm-std = { workspace = true }
cosmwasm-storage = { workspace = true }
cw-storage-plus = { workspace = true }
cw2 = { workspace = true }
cw-utils = { workspace = true }

schemars = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }

[dev-dependencies]
cw-multi-test = { workspace = true }
test-case = { workspace = true }
derivative = { workspace = true }
anyhow = { workspace = true }

[[bin]]
name = "schema"
doc = false
12 changes: 12 additions & 0 deletions contracts/consumer/remote-price-feed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Remote Price Feed

This implements the [price feed API](../../../packages/apis/src/price_feed_api.rs).

This contract provides exchange rate data by contacting an [Osmosis Price Provider](../osmosis-price-provider).

A single trading pair has to be configured on instantiation, along with the IBC endpoint. In case multiple trading pairs need to be synced, multiple contracts must be deployed.

For this contract to work correctly:

- An IBC connection to [Osmosis Price Provider](../osmosis-price-provider) must be opened.
- A`SudoMsg::EndBlock {}` must be sent to the contract regularly. This will allow the contract to request regular updates of locally stored data.
12 changes: 12 additions & 0 deletions contracts/consumer/remote-price-feed/src/bin/schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use cosmwasm_schema::write_api;

use mesh_remote_price_feed::contract::{ContractExecMsg, ContractQueryMsg, InstantiateMsg};

#[cfg(not(tarpaulin_include))]
fn main() {
write_api! {
instantiate: InstantiateMsg,
execute: ContractExecMsg,
query: ContractQueryMsg,
}
}
126 changes: 126 additions & 0 deletions contracts/consumer/remote-price-feed/src/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use cosmwasm_std::{entry_point, DepsMut, Env, IbcChannel, Response, Timestamp};
use cw2::set_contract_version;
use cw_storage_plus::Item;
use cw_utils::nonpayable;
use mesh_bindings::SudoMsg;
use sylvia::types::{InstantiateCtx, QueryCtx};
use sylvia::{contract, schemars};

use mesh_apis::price_feed_api::{self, PriceFeedApi, PriceResponse};

use crate::error::ContractError;
use crate::ibc::{make_ibc_packet, AUTH_ENDPOINT};
use crate::msg::AuthorizedEndpoint;
use crate::state::{PriceInfo, TradingPair};

pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");
pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");

pub struct RemotePriceFeedContract {
pub channel: Item<'static, IbcChannel>,
pub trading_pair: Item<'static, TradingPair>,
pub price_info: Item<'static, PriceInfo>,
pub last_epoch: Item<'static, Timestamp>,
pub epoch_in_secs: Item<'static, u64>,
pub price_info_ttl_in_secs: Item<'static, u64>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Setting a TTL that is slightly larger than the epoch will help with handling delays in the price update process.

Perhaps all this can be derived from a single parameter. Including the IBC timeout as well. But, let's leave that for another iteration.

}

#[cfg_attr(not(feature = "library"), sylvia::entry_points)]
#[contract]
#[error(ContractError)]
#[messages(price_feed_api as PriceFeedApi)]
impl RemotePriceFeedContract {
pub const fn new() -> Self {
Self {
channel: Item::new("channel"),
trading_pair: Item::new("tpair"),
price_info: Item::new("price"),
last_epoch: Item::new("last_epoch"),
epoch_in_secs: Item::new("epoch"),
price_info_ttl_in_secs: Item::new("price_ttl"),
}
}

#[msg(instantiate)]
pub fn instantiate(
&self,
ctx: InstantiateCtx,
trading_pair: TradingPair,
auth_endpoint: AuthorizedEndpoint,
epoch_in_secs: u64,
price_info_ttl_in_secs: u64,
) -> Result<Response, ContractError> {
nonpayable(&ctx.info)?;

set_contract_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
self.last_epoch
.save(ctx.deps.storage, &Timestamp::from_seconds(0))?;
Copy link
Collaborator

@maurolacy maurolacy Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean 'never'? You can as well save the current block time here.

I think this is better, as it would schedule a price update at the first en block handler call after instantiation, right?

Copy link
Contributor Author

@uint uint Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this is the time of the last update. 0 stands for year 1970 and is pretty much guaranteed to mean "out of date", so that an update is scheduled after instantiation. If we change it to "now", the next update will be at now + epoch_in_secs, so it might introduce an unnecessary lag if anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH if instead of using "end block" we can configure an interval in the native Cosmos module, this whole "epoch length" dance becomes unnecessary here.

Copy link
Collaborator

@maurolacy maurolacy Nov 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but it doesn't hurt either, and allows for different contracts to have different update periods while using the same SDK.

It's also slightly more robust.

self.trading_pair.save(ctx.deps.storage, &trading_pair)?;
self.epoch_in_secs.save(ctx.deps.storage, &epoch_in_secs)?;
self.price_info_ttl_in_secs
.save(ctx.deps.storage, &price_info_ttl_in_secs)?;

AUTH_ENDPOINT.save(ctx.deps.storage, &auth_endpoint)?;

Ok(Response::new())
}
}

#[contract]
#[messages(price_feed_api as PriceFeedApi)]
impl PriceFeedApi for RemotePriceFeedContract {
type Error = ContractError;

/// Return the price of the foreign token. That is, how many native tokens
/// are needed to buy one foreign token.
#[msg(query)]
fn price(&self, ctx: QueryCtx) -> Result<PriceResponse, Self::Error> {
let price_info_ttl = self.price_info_ttl_in_secs.load(ctx.deps.storage)?;
let price_info = self
.price_info
.may_load(ctx.deps.storage)?
.ok_or(ContractError::NoPriceData)?;

if ctx.env.block.time.minus_seconds(price_info_ttl) < price_info.time {
Ok(PriceResponse {
native_per_foreign: price_info.native_per_foreign,
})
} else {
Err(ContractError::OutdatedPriceData)
}
}
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> Result<Response, ContractError> {
match msg {
SudoMsg::EndBlock {} => {
let contract = RemotePriceFeedContract::new();
let TradingPair {
pool_id,
base_asset,
quote_asset,
} = contract.trading_pair.load(deps.storage)?;
let channel = contract
.channel
.may_load(deps.storage)?
.ok_or(ContractError::IbcChannelNotOpen)?;

let last_epoch = contract.last_epoch.load(deps.storage)?;
let epoch_duration = contract.epoch_in_secs.load(deps.storage)?;
let secs_since_last_epoch = env.block.time.seconds() - last_epoch.seconds();
if secs_since_last_epoch >= epoch_duration {
let packet = mesh_apis::ibc::RemotePriceFeedPacket::QueryTwap {
pool_id,
base_asset,
quote_asset,
};
let msg = make_ibc_packet(&env.block.time, channel, packet)?;

Ok(Response::new().add_message(msg))
} else {
Ok(Response::new())
}
}
}
}
40 changes: 40 additions & 0 deletions contracts/consumer/remote-price-feed/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use cosmwasm_std::StdError;
use cw_utils::PaymentError;
use mesh_apis::ibc::VersionError;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),

#[error("{0}")]
Payment(#[from] PaymentError),

#[error("{0}")]
IbcVersion(#[from] VersionError),

#[error("Unauthorized")]
Unauthorized,

#[error("Invalid authorized endpoint: {0}")]
InvalidEndpoint(String),

#[error("Contract doesn't have an open IBC channel")]
IbcChannelNotOpen,

#[error("Contract already has an open IBC channel")]
IbcChannelAlreadyOpen,

#[error("You must start the channel handshake on the other side, it doesn't support OpenInit")]
IbcOpenInitDisallowed,

#[error("Contract does not receive packets except for acknowledgements")]
IbcReceiveNotAccepted,

#[error("The oracle hasn't received any price data")]
NoPriceData,

#[error("The oracle's price data is outdated")]
OutdatedPriceData,
}
Loading