Skip to content

Commit

Permalink
Merge pull request #195 from decentrio/vuong/band-oracle
Browse files Browse the repository at this point in the history
feat: band price feeder
  • Loading branch information
vuong177 authored Sep 24, 2024
2 parents 9c1a082 + f51b2c6 commit 17e76e7
Show file tree
Hide file tree
Showing 28 changed files with 880 additions and 195 deletions.
403 changes: 247 additions & 156 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ repository = "https://github.com/osmosis-labs/mesh-security"
mesh-apis = { path = "./packages/apis" }
mesh-bindings = { path = "./packages/bindings" }
mesh-burn = { path = "./packages/burn" }
mesh-price-feed = { path = "./packages/price-feed" }
mesh-sync = { path = "./packages/sync" }

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

obi = "0.0.2"
cw-band = "0.1.1"

# dev deps
anyhow = "1"
cw-multi-test = "0.20"
Expand Down
8 changes: 6 additions & 2 deletions codegen/codegen.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ codegen({
dir: './contracts/consumer/converter/schema'
},
{
name: 'RemotePriceFeed',
dir: './contracts/consumer/remote-price-feed/schema'
name: 'OsmosisPriceFeed',
dir: './contracts/consumer/osmosis-price-feed/schema'
},
{
name: 'BandPriceFeed',
dir: './contracts/consumer/band-price-feed/schema'
},
{
name: 'SimplePriceFeed',
Expand Down
47 changes: 47 additions & 0 deletions contracts/consumer/band-price-feed/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[package]
name = "mesh-band-price-feed"
description = "Returns exchange rates of assets fetched from Band Protocol"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[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-price-feed = { workspace = true }

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

schemars = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
obi = { workspace = true }
cw-band = { workspace = true }

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

[[bin]]
name = "schema"
doc = false
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use cosmwasm_schema::write_api;

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

#[cfg(not(tarpaulin_include))]
fn main() {
Expand Down
210 changes: 210 additions & 0 deletions contracts/consumer/band-price-feed/src/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
use cosmwasm_std::{
to_json_binary, Binary, Coin, DepsMut, Env, IbcChannel, IbcMsg, IbcTimeout, Response, Uint64,
};
use cw2::set_contract_version;
use cw_storage_plus::Item;
use cw_utils::nonpayable;
use mesh_apis::price_feed_api::{PriceFeedApi, PriceResponse};

use crate::error::ContractError;
use crate::state::{Config, TradingPair};

use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx, SudoCtx};
use sylvia::{contract, schemars};

use cw_band::{Input, OracleRequestPacketData};
use mesh_price_feed::{Action, PriceKeeper, Scheduler};
use obi::enc::OBIEncode;

// Version info for migration
const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");

pub struct RemotePriceFeedContract {
pub channel: Item<'static, IbcChannel>,
pub config: Item<'static, Config>,
pub trading_pair: Item<'static, TradingPair>,
pub price_keeper: PriceKeeper,
pub scheduler: Scheduler<Box<dyn Action<ContractError>>, ContractError>,
}

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

#[cfg_attr(not(feature = "library"), sylvia::entry_points)]
#[contract]
#[sv::error(ContractError)]
#[sv::messages(mesh_apis::price_feed_api as PriceFeedApi)]
impl RemotePriceFeedContract {
pub fn new() -> Self {
Self {
channel: Item::new("channel"),
config: Item::new("config"),
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(try_request)),
}
}

#[sv::msg(instantiate)]
pub fn instantiate(
&self,
mut ctx: InstantiateCtx,
trading_pair: TradingPair,
client_id: String,
oracle_script_id: Uint64,
ask_count: Uint64,
min_count: Uint64,
fee_limit: Vec<Coin>,
prepare_gas: Uint64,
execute_gas: Uint64,
minimum_sources: u8,
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.config.save(
ctx.deps.storage,
&Config {
client_id,
oracle_script_id,
ask_count,
min_count,
fee_limit,
prepare_gas,
execute_gas,
minimum_sources,
},
)?;
self.price_keeper
.init(&mut ctx.deps, price_info_ttl_in_secs)?;
Ok(Response::new())
}

#[sv::msg(exec)]
pub fn request(&self, ctx: ExecCtx) -> Result<Response, ContractError> {
let ExecCtx { deps, env, info: _ } = ctx;
try_request(deps, &env)
}
}

impl PriceFeedApi for RemotePriceFeedContract {
type Error = ContractError;
// FIXME: make these under a feature flag if we need virtual-staking multitest compatibility
type ExecC = cosmwasm_std::Empty;
type QueryC = cosmwasm_std::Empty;

/// Return the price of the foreign token. That is, how many native tokens
/// are needed to buy one foreign token.
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,
})?)
}

fn handle_epoch(&self, ctx: SudoCtx) -> Result<Response, Self::Error> {
self.scheduler.trigger(ctx.deps, &ctx.env)
}
}

// TODO: Possible features
// - Request fee + Bounty logic to prevent request spam and incentivize relayer
// - Whitelist who can call update price
pub fn try_request(deps: DepsMut, env: &Env) -> Result<Response, ContractError> {
let contract = RemotePriceFeedContract::new();
let TradingPair {
base_asset,
quote_asset,
} = contract.trading_pair.load(deps.storage)?;
let config = contract.config.load(deps.storage)?;
let channel = contract
.channel
.may_load(deps.storage)?
.ok_or(ContractError::IbcChannelNotOpen)?;

let raw_calldata = Input {
symbols: vec![base_asset, quote_asset],
minimum_sources: config.minimum_sources,
}
.try_to_vec()
.map(Binary)
.map_err(|err| ContractError::CustomError {
val: err.to_string(),
})?;

let packet = OracleRequestPacketData {
client_id: config.client_id,
oracle_script_id: config.oracle_script_id,
calldata: raw_calldata,
ask_count: config.ask_count,
min_count: config.min_count,
prepare_gas: config.prepare_gas,
execute_gas: config.execute_gas,
fee_limit: config.fee_limit,
};

Ok(Response::new().add_message(IbcMsg::SendPacket {
channel_id: channel.endpoint.channel_id,
data: to_json_binary(&packet)?,
timeout: IbcTimeout::with_timestamp(env.block.time.plus_seconds(60)),
}))
}

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

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 {
base_asset: "base".to_string(),
quote_asset: "quote".to_string(),
};

contract
.instantiate(
InstantiateCtx {
deps: deps.as_mut(),
env,
info,
},
trading_pair,
"07-tendermint-0".to_string(),
Uint64::new(1),
Uint64::new(10),
Uint64::new(50),
vec![Coin {
denom: "uband".to_string(),
amount: Uint128::new(1),
}],
Uint64::new(100000),
Uint64::new(200000),
1,
60,
)
.unwrap();
}
}
62 changes: 62 additions & 0 deletions contracts/consumer/band-price-feed/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use cosmwasm_std::StdError;
use cw_utils::PaymentError;
use thiserror::Error;

use mesh_price_feed::PriceKeeperError;

/// Never is a placeholder to ensure we don't return any errors
#[derive(Error, Debug)]
pub enum Never {}

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

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

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

#[error("Unauthorized")]
Unauthorized,

#[error("Request didn't suceess")]
RequestNotSuccess {},

#[error("Only supports channel with ibc version bandchain-1, got {version}")]
InvalidIbcVersion { version: String },

#[error("Only supports unordered channel")]
OnlyUnorderedChannel {},

#[error("The provided IBC channel is not open")]
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 ack")]
IbcAckNotAccepted,

#[error("Contract does not receive packets timeout")]
IbcTimeoutNotAccepted,

#[error("Response packet should only contains 2 symbols")]
InvalidResponsePacket,

#[error("Symbol must be base denom or quote denom")]
SymbolsNotMatch,

#[error("Invalid price, must be greater than 0.0")]
InvalidPrice,

#[error("Custom Error val: {val:?}")]
CustomError { val: String },
// Add any other custom errors you like here.
// Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details.
}
Loading

0 comments on commit 17e76e7

Please sign in to comment.