Skip to content

Commit 10c4a02

Browse files
authored
Merge pull request #155 from osmosis-labs/99-osmosis-price-oracle
Osmosis price oracle
2 parents 1477b87 + 112693b commit 10c4a02

File tree

22 files changed

+1343
-37
lines changed

22 files changed

+1343
-37
lines changed

Cargo.lock

+311-35
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = ["packages/*", "contracts/provider/*", "contracts/consumer/*"]
2+
members = ["packages/*", "contracts/provider/*", "contracts/consumer/*", "contracts/osmosis-price-provider"]
33
resolver = "2"
44

55
[workspace.package]
@@ -31,6 +31,7 @@ cosmwasm-storage = "1.3.3"
3131
cw-storage-plus = "1.1.0"
3232
cw-utils = "1.0.1"
3333
cw2 = "1.0.1"
34+
osmosis-std = "0.20.1"
3435
schemars = "0.8.11"
3536
serde = { version = "1.0.152", default-features = false, features = ["derive"] }
3637
thiserror = "1.0.38"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[alias]
2+
wasm = "build --release --lib --target wasm32-unknown-unknown"
3+
unit-test = "test --lib"
4+
schema = "run --bin schema"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
[package]
2+
name = "mesh-remote-price-feed"
3+
description = "Returns exchange rates of assets synchronized with an Osmosis price provider"
4+
version = { workspace = true }
5+
edition = { workspace = true }
6+
license = { workspace = true }
7+
repository = { workspace = true }
8+
9+
[lib]
10+
crate-type = ["cdylib", "rlib"]
11+
12+
[features]
13+
# for more explicit tests, cargo test --features=backtraces
14+
backtraces = ["cosmwasm-std/backtraces"]
15+
# use library feature to disable all instantiate/execute/query exports
16+
library = []
17+
# enables generation of mt utilities
18+
mt = ["library", "sylvia/mt"]
19+
20+
[dependencies]
21+
mesh-apis = { workspace = true }
22+
23+
sylvia = { workspace = true }
24+
cosmwasm-schema = { workspace = true }
25+
cosmwasm-std = { workspace = true }
26+
cosmwasm-storage = { workspace = true }
27+
cw-storage-plus = { workspace = true }
28+
cw2 = { workspace = true }
29+
cw-utils = { workspace = true }
30+
31+
schemars = { workspace = true }
32+
serde = { workspace = true }
33+
thiserror = { workspace = true }
34+
35+
[dev-dependencies]
36+
cw-multi-test = { workspace = true }
37+
test-case = { workspace = true }
38+
derivative = { workspace = true }
39+
anyhow = { workspace = true }
40+
41+
[[bin]]
42+
name = "schema"
43+
doc = false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Remote Price Feed
2+
3+
This implements the [price feed API](../../../packages/apis/src/price_feed_api.rs).
4+
5+
This contract provides exchange rate data by contacting an [Osmosis Price Provider](../osmosis-price-provider).
6+
7+
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.
8+
9+
For this contract to work correctly:
10+
11+
- An IBC connection to [Osmosis Price Provider](../osmosis-price-provider) must be opened.
12+
- A `SudoMsg::EndBlock {}` must be sent to the contract regularly. This will allow the contract to request regular updates of locally stored data.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use cosmwasm_schema::write_api;
2+
3+
use mesh_remote_price_feed::contract::{ContractExecMsg, ContractQueryMsg, InstantiateMsg};
4+
5+
#[cfg(not(tarpaulin_include))]
6+
fn main() {
7+
write_api! {
8+
instantiate: InstantiateMsg,
9+
execute: ContractExecMsg,
10+
query: ContractQueryMsg,
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
use cosmwasm_std::{entry_point, Decimal, DepsMut, Env, IbcChannel, Response, Timestamp};
2+
use cw2::set_contract_version;
3+
use cw_storage_plus::Item;
4+
use cw_utils::nonpayable;
5+
use mesh_apis::price_feed_api::SudoMsg;
6+
use sylvia::types::{InstantiateCtx, QueryCtx};
7+
use sylvia::{contract, schemars};
8+
9+
use mesh_apis::price_feed_api::{self, PriceFeedApi, PriceResponse};
10+
11+
use crate::error::ContractError;
12+
use crate::ibc::{make_ibc_packet, AUTH_ENDPOINT};
13+
use crate::msg::AuthorizedEndpoint;
14+
use crate::price_keeper::PriceKeeper;
15+
use crate::scheduler::{Action, Scheduler};
16+
use crate::state::TradingPair;
17+
18+
pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");
19+
pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
20+
21+
pub struct RemotePriceFeedContract {
22+
pub channel: Item<'static, IbcChannel>,
23+
pub trading_pair: Item<'static, TradingPair>,
24+
pub price_keeper: PriceKeeper,
25+
pub scheduler: Scheduler<Box<dyn Action>>,
26+
}
27+
28+
impl Default for RemotePriceFeedContract {
29+
fn default() -> Self {
30+
Self::new()
31+
}
32+
}
33+
34+
#[cfg_attr(not(feature = "library"), sylvia::entry_points)]
35+
#[contract]
36+
#[error(ContractError)]
37+
#[messages(price_feed_api as PriceFeedApi)]
38+
impl RemotePriceFeedContract {
39+
pub fn new() -> Self {
40+
Self {
41+
channel: Item::new("channel"),
42+
trading_pair: Item::new("tpair"),
43+
price_keeper: PriceKeeper::new(),
44+
// TODO: the indirection can be removed once Sylvia supports
45+
// generics. The constructor can then probably be constant.
46+
//
47+
// Stable existential types would be even better!
48+
// https://github.com/rust-lang/rust/issues/63063
49+
scheduler: Scheduler::new(Box::new(query_twap)),
50+
}
51+
}
52+
53+
#[msg(instantiate)]
54+
pub fn instantiate(
55+
&self,
56+
mut ctx: InstantiateCtx,
57+
trading_pair: TradingPair,
58+
auth_endpoint: AuthorizedEndpoint,
59+
epoch_in_secs: u64,
60+
price_info_ttl_in_secs: u64,
61+
) -> Result<Response, ContractError> {
62+
nonpayable(&ctx.info)?;
63+
64+
set_contract_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
65+
self.trading_pair.save(ctx.deps.storage, &trading_pair)?;
66+
67+
self.price_keeper
68+
.init(&mut ctx.deps, price_info_ttl_in_secs)?;
69+
self.scheduler.init(&mut ctx.deps, epoch_in_secs)?;
70+
71+
AUTH_ENDPOINT.save(ctx.deps.storage, &auth_endpoint)?;
72+
73+
Ok(Response::new())
74+
}
75+
76+
pub(crate) fn update_twap(
77+
&self,
78+
deps: DepsMut,
79+
time: Timestamp,
80+
twap: Decimal,
81+
) -> Result<(), ContractError> {
82+
Ok(self.price_keeper.update(deps, time, twap)?)
83+
}
84+
}
85+
86+
#[contract]
87+
#[messages(price_feed_api as PriceFeedApi)]
88+
impl PriceFeedApi for RemotePriceFeedContract {
89+
type Error = ContractError;
90+
91+
/// Return the price of the foreign token. That is, how many native tokens
92+
/// are needed to buy one foreign token.
93+
#[msg(query)]
94+
fn price(&self, ctx: QueryCtx) -> Result<PriceResponse, Self::Error> {
95+
Ok(self
96+
.price_keeper
97+
.price(ctx.deps, &ctx.env)
98+
.map(|rate| PriceResponse {
99+
native_per_foreign: rate,
100+
})?)
101+
}
102+
}
103+
104+
#[cfg_attr(not(feature = "library"), entry_point)]
105+
pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> Result<Response, ContractError> {
106+
let contract = RemotePriceFeedContract::new();
107+
108+
match msg {
109+
SudoMsg::HandleEpoch {} => contract.scheduler.trigger(deps, &env),
110+
}
111+
}
112+
113+
pub fn query_twap(deps: DepsMut, env: &Env) -> Result<Response, ContractError> {
114+
let contract = RemotePriceFeedContract::new();
115+
let TradingPair {
116+
pool_id,
117+
base_asset,
118+
quote_asset,
119+
} = contract.trading_pair.load(deps.storage)?;
120+
121+
let channel = contract
122+
.channel
123+
.may_load(deps.storage)?
124+
.ok_or(ContractError::IbcChannelNotOpen)?;
125+
126+
let packet = mesh_apis::ibc::RemotePriceFeedPacket::QueryTwap {
127+
pool_id,
128+
base_asset,
129+
quote_asset,
130+
};
131+
let msg = make_ibc_packet(&env.block.time, channel, packet)?;
132+
133+
Ok(Response::new().add_message(msg))
134+
}
135+
136+
#[cfg(test)]
137+
mod tests {
138+
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
139+
140+
use super::*;
141+
142+
#[test]
143+
fn instantiation() {
144+
let mut deps = mock_dependencies();
145+
let env = mock_env();
146+
let info = mock_info("sender", &[]);
147+
let contract = RemotePriceFeedContract::new();
148+
149+
let trading_pair = TradingPair {
150+
pool_id: 1,
151+
base_asset: "base".to_string(),
152+
quote_asset: "quote".to_string(),
153+
};
154+
let auth_endpoint = AuthorizedEndpoint {
155+
connection_id: "connection".to_string(),
156+
port_id: "port".to_string(),
157+
};
158+
159+
contract
160+
.instantiate(
161+
InstantiateCtx {
162+
deps: deps.as_mut(),
163+
env,
164+
info,
165+
},
166+
trading_pair,
167+
auth_endpoint,
168+
10,
169+
50,
170+
)
171+
.unwrap();
172+
}
173+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use cosmwasm_std::StdError;
2+
use cw_utils::PaymentError;
3+
use mesh_apis::ibc::VersionError;
4+
use thiserror::Error;
5+
6+
use crate::price_keeper::PriceKeeperError;
7+
8+
#[derive(Error, Debug)]
9+
pub enum ContractError {
10+
#[error("{0}")]
11+
Std(#[from] StdError),
12+
13+
#[error("{0}")]
14+
Payment(#[from] PaymentError),
15+
16+
#[error("{0}")]
17+
IbcVersion(#[from] VersionError),
18+
19+
#[error("{0}")]
20+
PriceKeeper(#[from] PriceKeeperError),
21+
22+
#[error("Unauthorized")]
23+
Unauthorized,
24+
25+
#[error("Invalid authorized endpoint: {0}")]
26+
InvalidEndpoint(String),
27+
28+
#[error("Contract doesn't have an open IBC channel")]
29+
IbcChannelNotOpen,
30+
31+
#[error("Contract already has an open IBC channel")]
32+
IbcChannelAlreadyOpen,
33+
34+
#[error("You must start the channel handshake on the other side, it doesn't support OpenInit")]
35+
IbcOpenInitDisallowed,
36+
37+
#[error("Contract does not receive packets except for acknowledgements")]
38+
IbcReceiveNotAccepted,
39+
40+
#[error("The oracle hasn't received any price data")]
41+
NoPriceData,
42+
43+
#[error("The oracle's price data is outdated")]
44+
OutdatedPriceData,
45+
}

0 commit comments

Comments
 (0)