Skip to content

Commit 17e76e7

Browse files
authored
Merge pull request #195 from decentrio/vuong/band-oracle
feat: band price feeder
2 parents 9c1a082 + f51b2c6 commit 17e76e7

File tree

28 files changed

+880
-195
lines changed

28 files changed

+880
-195
lines changed

Cargo.lock

Lines changed: 247 additions & 156 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ repository = "https://github.com/osmosis-labs/mesh-security"
1717
mesh-apis = { path = "./packages/apis" }
1818
mesh-bindings = { path = "./packages/bindings" }
1919
mesh-burn = { path = "./packages/burn" }
20+
mesh-price-feed = { path = "./packages/price-feed" }
2021
mesh-sync = { path = "./packages/sync" }
2122

2223
mesh-vault = { path = "./contracts/provider/vault" }
@@ -42,6 +43,9 @@ thiserror = "1.0.59"
4243
semver = "1.0.22"
4344
itertools = "0.12.1"
4445

46+
obi = "0.0.2"
47+
cw-band = "0.1.1"
48+
4549
# dev deps
4650
anyhow = "1"
4751
cw-multi-test = "0.20"

codegen/codegen.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ codegen({
2323
dir: './contracts/consumer/converter/schema'
2424
},
2525
{
26-
name: 'RemotePriceFeed',
27-
dir: './contracts/consumer/remote-price-feed/schema'
26+
name: 'OsmosisPriceFeed',
27+
dir: './contracts/consumer/osmosis-price-feed/schema'
28+
},
29+
{
30+
name: 'BandPriceFeed',
31+
dir: './contracts/consumer/band-price-feed/schema'
2832
},
2933
{
3034
name: 'SimplePriceFeed',
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
[package]
2+
name = "mesh-band-price-feed"
3+
description = "Returns exchange rates of assets fetched from Band Protocol"
4+
version = { workspace = true }
5+
edition = { workspace = true }
6+
license = { workspace = true }
7+
repository = { workspace = true }
8+
9+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
10+
[lib]
11+
crate-type = ["cdylib", "rlib"]
12+
13+
[features]
14+
# for more explicit tests, cargo test --features=backtraces
15+
backtraces = ["cosmwasm-std/backtraces"]
16+
# use library feature to disable all instantiate/execute/query exports
17+
library = []
18+
# enables generation of mt utilities
19+
mt = ["library", "sylvia/mt"]
20+
21+
22+
[dependencies]
23+
mesh-apis = { workspace = true }
24+
mesh-price-feed = { workspace = true }
25+
26+
sylvia = { workspace = true }
27+
cosmwasm-schema = { workspace = true }
28+
cosmwasm-std = { workspace = true }
29+
cw-storage-plus = { workspace = true }
30+
cw2 = { workspace = true }
31+
cw-utils = { workspace = true }
32+
33+
schemars = { workspace = true }
34+
serde = { workspace = true }
35+
thiserror = { workspace = true }
36+
obi = { workspace = true }
37+
cw-band = { workspace = true }
38+
39+
[dev-dependencies]
40+
cw-multi-test = { workspace = true }
41+
test-case = { workspace = true }
42+
derivative = { workspace = true }
43+
anyhow = { workspace = true }
44+
45+
[[bin]]
46+
name = "schema"
47+
doc = false

contracts/consumer/remote-price-feed/src/bin/schema.rs renamed to contracts/consumer/band-price-feed/src/bin/schema.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use cosmwasm_schema::write_api;
22

3-
use mesh_remote_price_feed::contract::sv::{ContractExecMsg, ContractQueryMsg, InstantiateMsg};
3+
use mesh_band_price_feed::contract::sv::{ContractExecMsg, ContractQueryMsg, InstantiateMsg};
44

55
#[cfg(not(tarpaulin_include))]
66
fn main() {
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
use cosmwasm_std::{
2+
to_json_binary, Binary, Coin, DepsMut, Env, IbcChannel, IbcMsg, IbcTimeout, Response, Uint64,
3+
};
4+
use cw2::set_contract_version;
5+
use cw_storage_plus::Item;
6+
use cw_utils::nonpayable;
7+
use mesh_apis::price_feed_api::{PriceFeedApi, PriceResponse};
8+
9+
use crate::error::ContractError;
10+
use crate::state::{Config, TradingPair};
11+
12+
use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx, SudoCtx};
13+
use sylvia::{contract, schemars};
14+
15+
use cw_band::{Input, OracleRequestPacketData};
16+
use mesh_price_feed::{Action, PriceKeeper, Scheduler};
17+
use obi::enc::OBIEncode;
18+
19+
// Version info for migration
20+
const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");
21+
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
22+
23+
pub struct RemotePriceFeedContract {
24+
pub channel: Item<'static, IbcChannel>,
25+
pub config: Item<'static, Config>,
26+
pub trading_pair: Item<'static, TradingPair>,
27+
pub price_keeper: PriceKeeper,
28+
pub scheduler: Scheduler<Box<dyn Action<ContractError>>, ContractError>,
29+
}
30+
31+
impl Default for RemotePriceFeedContract {
32+
fn default() -> Self {
33+
Self::new()
34+
}
35+
}
36+
37+
#[cfg_attr(not(feature = "library"), sylvia::entry_points)]
38+
#[contract]
39+
#[sv::error(ContractError)]
40+
#[sv::messages(mesh_apis::price_feed_api as PriceFeedApi)]
41+
impl RemotePriceFeedContract {
42+
pub fn new() -> Self {
43+
Self {
44+
channel: Item::new("channel"),
45+
config: Item::new("config"),
46+
trading_pair: Item::new("tpair"),
47+
price_keeper: PriceKeeper::new(),
48+
// TODO: the indirection can be removed once Sylvia supports
49+
// generics. The constructor can then probably be constant.
50+
//
51+
// Stable existential types would be even better!
52+
// https://github.com/rust-lang/rust/issues/63063
53+
scheduler: Scheduler::new(Box::new(try_request)),
54+
}
55+
}
56+
57+
#[sv::msg(instantiate)]
58+
pub fn instantiate(
59+
&self,
60+
mut ctx: InstantiateCtx,
61+
trading_pair: TradingPair,
62+
client_id: String,
63+
oracle_script_id: Uint64,
64+
ask_count: Uint64,
65+
min_count: Uint64,
66+
fee_limit: Vec<Coin>,
67+
prepare_gas: Uint64,
68+
execute_gas: Uint64,
69+
minimum_sources: u8,
70+
price_info_ttl_in_secs: u64,
71+
) -> Result<Response, ContractError> {
72+
nonpayable(&ctx.info)?;
73+
74+
set_contract_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
75+
self.trading_pair.save(ctx.deps.storage, &trading_pair)?;
76+
self.config.save(
77+
ctx.deps.storage,
78+
&Config {
79+
client_id,
80+
oracle_script_id,
81+
ask_count,
82+
min_count,
83+
fee_limit,
84+
prepare_gas,
85+
execute_gas,
86+
minimum_sources,
87+
},
88+
)?;
89+
self.price_keeper
90+
.init(&mut ctx.deps, price_info_ttl_in_secs)?;
91+
Ok(Response::new())
92+
}
93+
94+
#[sv::msg(exec)]
95+
pub fn request(&self, ctx: ExecCtx) -> Result<Response, ContractError> {
96+
let ExecCtx { deps, env, info: _ } = ctx;
97+
try_request(deps, &env)
98+
}
99+
}
100+
101+
impl PriceFeedApi for RemotePriceFeedContract {
102+
type Error = ContractError;
103+
// FIXME: make these under a feature flag if we need virtual-staking multitest compatibility
104+
type ExecC = cosmwasm_std::Empty;
105+
type QueryC = cosmwasm_std::Empty;
106+
107+
/// Return the price of the foreign token. That is, how many native tokens
108+
/// are needed to buy one foreign token.
109+
fn price(&self, ctx: QueryCtx) -> Result<PriceResponse, Self::Error> {
110+
Ok(self
111+
.price_keeper
112+
.price(ctx.deps, &ctx.env)
113+
.map(|rate| PriceResponse {
114+
native_per_foreign: rate,
115+
})?)
116+
}
117+
118+
fn handle_epoch(&self, ctx: SudoCtx) -> Result<Response, Self::Error> {
119+
self.scheduler.trigger(ctx.deps, &ctx.env)
120+
}
121+
}
122+
123+
// TODO: Possible features
124+
// - Request fee + Bounty logic to prevent request spam and incentivize relayer
125+
// - Whitelist who can call update price
126+
pub fn try_request(deps: DepsMut, env: &Env) -> Result<Response, ContractError> {
127+
let contract = RemotePriceFeedContract::new();
128+
let TradingPair {
129+
base_asset,
130+
quote_asset,
131+
} = contract.trading_pair.load(deps.storage)?;
132+
let config = contract.config.load(deps.storage)?;
133+
let channel = contract
134+
.channel
135+
.may_load(deps.storage)?
136+
.ok_or(ContractError::IbcChannelNotOpen)?;
137+
138+
let raw_calldata = Input {
139+
symbols: vec![base_asset, quote_asset],
140+
minimum_sources: config.minimum_sources,
141+
}
142+
.try_to_vec()
143+
.map(Binary)
144+
.map_err(|err| ContractError::CustomError {
145+
val: err.to_string(),
146+
})?;
147+
148+
let packet = OracleRequestPacketData {
149+
client_id: config.client_id,
150+
oracle_script_id: config.oracle_script_id,
151+
calldata: raw_calldata,
152+
ask_count: config.ask_count,
153+
min_count: config.min_count,
154+
prepare_gas: config.prepare_gas,
155+
execute_gas: config.execute_gas,
156+
fee_limit: config.fee_limit,
157+
};
158+
159+
Ok(Response::new().add_message(IbcMsg::SendPacket {
160+
channel_id: channel.endpoint.channel_id,
161+
data: to_json_binary(&packet)?,
162+
timeout: IbcTimeout::with_timestamp(env.block.time.plus_seconds(60)),
163+
}))
164+
}
165+
166+
#[cfg(test)]
167+
mod tests {
168+
use cosmwasm_std::{
169+
testing::{mock_dependencies, mock_env, mock_info},
170+
Uint128, Uint64,
171+
};
172+
173+
use super::*;
174+
175+
#[test]
176+
fn instantiation() {
177+
let mut deps = mock_dependencies();
178+
let env = mock_env();
179+
let info = mock_info("sender", &[]);
180+
let contract = RemotePriceFeedContract::new();
181+
182+
let trading_pair = TradingPair {
183+
base_asset: "base".to_string(),
184+
quote_asset: "quote".to_string(),
185+
};
186+
187+
contract
188+
.instantiate(
189+
InstantiateCtx {
190+
deps: deps.as_mut(),
191+
env,
192+
info,
193+
},
194+
trading_pair,
195+
"07-tendermint-0".to_string(),
196+
Uint64::new(1),
197+
Uint64::new(10),
198+
Uint64::new(50),
199+
vec![Coin {
200+
denom: "uband".to_string(),
201+
amount: Uint128::new(1),
202+
}],
203+
Uint64::new(100000),
204+
Uint64::new(200000),
205+
1,
206+
60,
207+
)
208+
.unwrap();
209+
}
210+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use cosmwasm_std::StdError;
2+
use cw_utils::PaymentError;
3+
use thiserror::Error;
4+
5+
use mesh_price_feed::PriceKeeperError;
6+
7+
/// Never is a placeholder to ensure we don't return any errors
8+
#[derive(Error, Debug)]
9+
pub enum Never {}
10+
11+
#[derive(Error, Debug, PartialEq)]
12+
pub enum ContractError {
13+
#[error("{0}")]
14+
Std(#[from] StdError),
15+
16+
#[error("{0}")]
17+
Payment(#[from] PaymentError),
18+
19+
#[error("{0}")]
20+
PriceKeeper(#[from] PriceKeeperError),
21+
22+
#[error("Unauthorized")]
23+
Unauthorized,
24+
25+
#[error("Request didn't suceess")]
26+
RequestNotSuccess {},
27+
28+
#[error("Only supports channel with ibc version bandchain-1, got {version}")]
29+
InvalidIbcVersion { version: String },
30+
31+
#[error("Only supports unordered channel")]
32+
OnlyUnorderedChannel {},
33+
34+
#[error("The provided IBC channel is not open")]
35+
IbcChannelNotOpen,
36+
37+
#[error("Contract already has an open IBC channel")]
38+
IbcChannelAlreadyOpen,
39+
40+
#[error("You must start the channel handshake on the other side, it doesn't support OpenInit")]
41+
IbcOpenInitDisallowed,
42+
43+
#[error("Contract does not receive packets ack")]
44+
IbcAckNotAccepted,
45+
46+
#[error("Contract does not receive packets timeout")]
47+
IbcTimeoutNotAccepted,
48+
49+
#[error("Response packet should only contains 2 symbols")]
50+
InvalidResponsePacket,
51+
52+
#[error("Symbol must be base denom or quote denom")]
53+
SymbolsNotMatch,
54+
55+
#[error("Invalid price, must be greater than 0.0")]
56+
InvalidPrice,
57+
58+
#[error("Custom Error val: {val:?}")]
59+
CustomError { val: String },
60+
// Add any other custom errors you like here.
61+
// Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details.
62+
}

0 commit comments

Comments
 (0)