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

feat: band price feeder #195

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
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
Copy link
Collaborator

Choose a reason for hiding this comment

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

Did you mean to move this file or simply copy it?

Copy link
Collaborator

@JakeHartnell JakeHartnell Jul 26, 2024

Choose a reason for hiding this comment

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

I see other files were moved too, did you kill the old remote-price-feed contract? Fine if you did, just make sure it's cleaned up. ❤️

Copy link
Collaborator Author

@trinitys7 trinitys7 Aug 5, 2024

Choose a reason for hiding this comment

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

I just move the schema.rs file into correct folder. I don't remove anything.

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
Loading