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 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
346 changes: 311 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"
43 changes: 43 additions & 0 deletions contracts/consumer/remote-price-feed/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
[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 }

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,
}
}
173 changes: 173 additions & 0 deletions contracts/consumer/remote-price-feed/src/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
use cosmwasm_std::{entry_point, Decimal, DepsMut, Env, IbcChannel, Response, Timestamp};
use cw2::set_contract_version;
use cw_storage_plus::Item;
use cw_utils::nonpayable;
use mesh_apis::price_feed_api::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::price_keeper::PriceKeeper;
use crate::scheduler::{Action, Scheduler};
use crate::state::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_keeper: PriceKeeper,
pub scheduler: Scheduler<Box<dyn Action>>,
}

impl Default for RemotePriceFeedContract {
fn default() -> Self {
Self::new()
}
}

#[cfg_attr(not(feature = "library"), sylvia::entry_points)]
#[contract]
#[error(ContractError)]
#[messages(price_feed_api as PriceFeedApi)]
impl RemotePriceFeedContract {
pub fn new() -> Self {
Self {
channel: Item::new("channel"),
trading_pair: Item::new("tpair"),
price_keeper: PriceKeeper::new(),
// TODO: the indirection can be removed once Sylvia supports
// generics. The constructor can then probably be constant.
//
// Stable existential types would be even better!
// https://github.com/rust-lang/rust/issues/63063
scheduler: Scheduler::new(Box::new(query_twap)),
}
}

#[msg(instantiate)]
pub fn instantiate(
&self,
mut 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.trading_pair.save(ctx.deps.storage, &trading_pair)?;

self.price_keeper
.init(&mut ctx.deps, price_info_ttl_in_secs)?;
self.scheduler.init(&mut ctx.deps, epoch_in_secs)?;

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

Ok(Response::new())
}

pub(crate) fn update_twap(
&self,
deps: DepsMut,
time: Timestamp,
twap: Decimal,
) -> Result<(), ContractError> {
Ok(self.price_keeper.update(deps, time, twap)?)
}
}

#[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> {
Ok(self
.price_keeper
.price(ctx.deps, &ctx.env)
.map(|rate| PriceResponse {
native_per_foreign: rate,
})?)
}
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> Result<Response, ContractError> {
let contract = RemotePriceFeedContract::new();

match msg {
SudoMsg::HandleEpoch {} => contract.scheduler.trigger(deps, &env),
}
}

pub fn query_twap(deps: DepsMut, env: &Env) -> Result<Response, ContractError> {
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 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))
}

#[cfg(test)]
mod tests {
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};

use super::*;

#[test]
fn instantiation() {
let mut deps = mock_dependencies();
let env = mock_env();
let info = mock_info("sender", &[]);
let contract = RemotePriceFeedContract::new();

let trading_pair = TradingPair {
pool_id: 1,
base_asset: "base".to_string(),
quote_asset: "quote".to_string(),
};
let auth_endpoint = AuthorizedEndpoint {
connection_id: "connection".to_string(),
port_id: "port".to_string(),
};

contract
.instantiate(
InstantiateCtx {
deps: deps.as_mut(),
env,
info,
},
trading_pair,
auth_endpoint,
10,
50,
)
.unwrap();
}
}
45 changes: 45 additions & 0 deletions contracts/consumer/remote-price-feed/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use cosmwasm_std::StdError;
use cw_utils::PaymentError;
use mesh_apis::ibc::VersionError;
use thiserror::Error;

use crate::price_keeper::PriceKeeperError;

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

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

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

#[error("{0}")]
PriceKeeper(#[from] PriceKeeperError),

#[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