Skip to content

Commit e21e090

Browse files
committed
redesign remote price oracle
1 parent a7b7a2d commit e21e090

File tree

13 files changed

+203
-244
lines changed

13 files changed

+203
-244
lines changed

Cargo.lock

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

contracts/consumer/remote-price-feed/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ mt = ["library", "sylvia/mt"]
2020

2121
[dependencies]
2222
mesh-apis = { workspace = true }
23+
mesh-bindings = { workspace = true }
2324

24-
sylvia = { workspace = true }
25+
sylvia = { workspace = true }
2526
cosmwasm-schema = { workspace = true }
2627
cosmwasm-std = { workspace = true }
2728
cosmwasm-storage = { workspace = true }
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
1-
use cosmwasm_std::{Decimal, Response};
1+
use cosmwasm_std::{entry_point, DepsMut, Env, IbcChannel, Response, Timestamp};
22
use cw2::set_contract_version;
33
use cw_storage_plus::Item;
44
use cw_utils::nonpayable;
5+
use mesh_bindings::RemotePriceFeedSudoMsg;
56
use sylvia::types::{InstantiateCtx, QueryCtx};
67
use sylvia::{contract, schemars};
78

89
use mesh_apis::price_feed_api::{self, PriceFeedApi, PriceResponse};
910

1011
use crate::error::ContractError;
12+
use crate::ibc::{make_ibc_packet, AUTH_ENDPOINT};
13+
use crate::msg::AuthorizedEndpoint;
14+
use crate::state::{PriceInfo, TradingPair};
1115

1216
pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");
1317
pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
1418

19+
pub const EPOCH_IN_SECS: u64 = 120;
20+
1521
pub struct RemotePriceFeedContract {
16-
pub native_per_foreign: Item<'static, Decimal>,
22+
pub channel: Item<'static, IbcChannel>,
23+
pub trading_pair: Item<'static, TradingPair>,
24+
pub price_info: Item<'static, PriceInfo>,
25+
pub last_epoch: Item<'static, Timestamp>,
1726
}
1827

1928
#[cfg_attr(not(feature = "library"), sylvia::entry_points)]
@@ -23,17 +32,31 @@ pub struct RemotePriceFeedContract {
2332
impl RemotePriceFeedContract {
2433
pub const fn new() -> Self {
2534
Self {
26-
native_per_foreign: Item::new("price"),
35+
channel: Item::new("channel"),
36+
trading_pair: Item::new("tpair"),
37+
price_info: Item::new("price"),
38+
last_epoch: Item::new("last_epoch"),
2739
}
2840
}
2941

3042
/// Sets up the contract with an initial price.
3143
/// If the owner is not set in the message, it defaults to info.sender.
3244
#[msg(instantiate)]
33-
pub fn instantiate(&self, ctx: InstantiateCtx) -> Result<Response, ContractError> {
45+
pub fn instantiate(
46+
&self,
47+
ctx: InstantiateCtx,
48+
trading_pair: TradingPair,
49+
auth_endpoint: AuthorizedEndpoint,
50+
) -> Result<Response, ContractError> {
3451
nonpayable(&ctx.info)?;
3552

3653
set_contract_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
54+
self.last_epoch
55+
.save(ctx.deps.storage, &Timestamp::from_seconds(0))?;
56+
self.trading_pair.save(ctx.deps.storage, &trading_pair)?;
57+
58+
AUTH_ENDPOINT.save(ctx.deps.storage, &auth_endpoint)?;
59+
3760
Ok(Response::new())
3861
}
3962
}
@@ -47,11 +70,48 @@ impl PriceFeedApi for RemotePriceFeedContract {
4770
/// are needed to buy one foreign token.
4871
#[msg(query)]
4972
fn price(&self, ctx: QueryCtx) -> Result<PriceResponse, Self::Error> {
50-
let native_per_foreign = self.native_per_foreign.may_load(ctx.deps.storage)?;
51-
native_per_foreign
52-
.map(|price| PriceResponse {
53-
native_per_foreign: price,
73+
let price_info = self.price_info.may_load(ctx.deps.storage)?;
74+
price_info
75+
.map(|info| PriceResponse {
76+
native_per_foreign: info.native_per_foreign,
5477
})
5578
.ok_or(ContractError::NoPriceData)
5679
}
5780
}
81+
82+
#[cfg_attr(not(feature = "library"), entry_point)]
83+
pub fn sudo(
84+
deps: DepsMut,
85+
env: Env,
86+
msg: RemotePriceFeedSudoMsg,
87+
) -> Result<Response, ContractError> {
88+
match msg {
89+
RemotePriceFeedSudoMsg::EndBlock {} => {
90+
let contract = RemotePriceFeedContract::new();
91+
let TradingPair {
92+
pool_id,
93+
base_asset,
94+
quote_asset,
95+
} = contract.trading_pair.load(deps.storage)?;
96+
let channel = contract
97+
.channel
98+
.may_load(deps.storage)?
99+
.ok_or(ContractError::IbcChannelNotOpen)?;
100+
101+
let last_epoch = contract.last_epoch.load(deps.storage)?;
102+
let secs_since_last_epoch = env.block.time.seconds() - last_epoch.seconds();
103+
if secs_since_last_epoch >= EPOCH_IN_SECS {
104+
let packet = mesh_apis::ibc::RemotePriceFeedPacket::QueryTwap {
105+
pool_id,
106+
base_asset,
107+
quote_asset,
108+
};
109+
let msg = make_ibc_packet(&env.block.time, channel, packet)?;
110+
111+
Ok(Response::new().add_message(msg))
112+
} else {
113+
Ok(Response::new())
114+
}
115+
}
116+
}
117+
}

contracts/consumer/remote-price-feed/src/error.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,17 @@ pub enum ContractError {
2020
#[error("Invalid authorized endpoint: {0}")]
2121
InvalidEndpoint(String),
2222

23+
#[error("Contract doesn't have an open IBC channel")]
24+
IbcChannelNotOpen,
25+
2326
#[error("Contract already has an open IBC channel")]
2427
IbcChannelAlreadyOpen,
2528

2629
#[error("You must start the channel handshake on the other side, it doesn't support OpenInit")]
2730
IbcOpenInitDisallowed,
2831

29-
#[error("Contract does not accept ack packets")]
30-
IbcAckNotAccepted,
32+
#[error("Contract does not receive packets except for acknowledgements")]
33+
IbcReceiveNotAccepted,
3134

3235
#[error("The oracle hasn't received any price data")]
3336
NoPriceData,

contracts/consumer/remote-price-feed/src/ibc.rs

+46-30
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22
use cosmwasm_std::entry_point;
33

44
use cosmwasm_std::{
5-
from_slice, DepsMut, Env, Ibc3ChannelOpenResponse, IbcBasicResponse, IbcChannel,
6-
IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcChannelOpenResponse,
5+
from_slice, to_binary, DepsMut, Env, Ibc3ChannelOpenResponse, IbcBasicResponse, IbcChannel,
6+
IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcChannelOpenResponse, IbcMsg,
77
IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, IbcTimeout,
8+
Timestamp,
89
};
910
use cw_storage_plus::Item;
10-
use mesh_apis::ibc::{validate_channel_order, PriceFeedProviderPacket, ProtocolVersion};
11+
use mesh_apis::ibc::{
12+
validate_channel_order, PriceFeedProviderAck, ProtocolVersion, RemotePriceFeedPacket,
13+
};
1114

1215
use crate::contract::RemotePriceFeedContract;
1316
use crate::error::ContractError;
1417
use crate::msg::AuthorizedEndpoint;
18+
use crate::state::PriceInfo;
1519

1620
/// This is the maximum version of the Mesh Security protocol that we support
1721
const SUPPORTED_IBC_PROTOCOL_VERSION: &str = "0.11.0";
@@ -20,16 +24,11 @@ const MIN_IBC_PROTOCOL_VERSION: &str = "0.11.0";
2024

2125
// IBC specific state
2226
pub const AUTH_ENDPOINT: Item<AuthorizedEndpoint> = Item::new("auth_endpoint");
23-
pub const IBC_CHANNEL: Item<IbcChannel> = Item::new("ibc_channel");
2427

25-
// If we don't hear anything within 10 minutes, let's abort, for better UX
26-
// This is long enough to allow some clock drift between chains
27-
const DEFAULT_TIMEOUT: u64 = 10 * 60;
28+
const TIMEOUT: u64 = 10 * 60;
2829

29-
pub fn packet_timeout(env: &Env) -> IbcTimeout {
30-
// No idea about their block time, but 24 hours ahead of our view of the clock
31-
// should be decently in the future.
32-
let timeout = env.block.time.plus_seconds(DEFAULT_TIMEOUT);
30+
pub fn packet_timeout(now: &Timestamp) -> IbcTimeout {
31+
let timeout = now.plus_seconds(TIMEOUT);
3332
IbcTimeout::with_timestamp(timeout)
3433
}
3534

@@ -41,7 +40,8 @@ pub fn ibc_channel_open(
4140
msg: IbcChannelOpenMsg,
4241
) -> Result<IbcChannelOpenResponse, ContractError> {
4342
// ensure we have no channel yet
44-
if IBC_CHANNEL.may_load(deps.storage)?.is_some() {
43+
let contract = RemotePriceFeedContract::new();
44+
if contract.channel.may_load(deps.storage)?.is_some() {
4545
return Err(ContractError::IbcChannelAlreadyOpen);
4646
}
4747
// ensure we are called with OpenInit
@@ -83,8 +83,10 @@ pub fn ibc_channel_connect(
8383
_env: Env,
8484
msg: IbcChannelConnectMsg,
8585
) -> Result<IbcBasicResponse, ContractError> {
86+
let contract = RemotePriceFeedContract::new();
87+
8688
// ensure we have no channel yet
87-
if IBC_CHANNEL.may_load(deps.storage)?.is_some() {
89+
if contract.channel.may_load(deps.storage)?.is_some() {
8890
return Err(ContractError::IbcChannelAlreadyOpen);
8991
}
9092
// ensure we are called with OpenConfirm
@@ -94,7 +96,8 @@ pub fn ibc_channel_connect(
9496
};
9597

9698
// Version negotiation over, we can only store the channel
97-
IBC_CHANNEL.save(deps.storage, &channel)?;
99+
let contract = RemotePriceFeedContract::new();
100+
contract.channel.save(deps.storage, &channel)?;
98101

99102
Ok(IbcBasicResponse::default())
100103
}
@@ -110,29 +113,30 @@ pub fn ibc_channel_close(
110113

111114
#[cfg_attr(not(feature = "library"), entry_point)]
112115
pub fn ibc_packet_receive(
113-
deps: DepsMut,
116+
_deps: DepsMut,
114117
_env: Env,
115-
msg: IbcPacketReceiveMsg,
118+
_msg: IbcPacketReceiveMsg,
116119
) -> Result<IbcReceiveResponse, ContractError> {
117-
let packet: PriceFeedProviderPacket = from_slice(&msg.packet.data)?;
118-
let resp = match packet {
119-
PriceFeedProviderPacket::Update { twap } => {
120-
let contract = RemotePriceFeedContract::new();
121-
contract.native_per_foreign.save(deps.storage, &twap)?;
122-
IbcReceiveResponse::new()
123-
}
124-
};
125-
126-
Ok(resp)
120+
Err(ContractError::IbcReceiveNotAccepted)
127121
}
128122

129123
#[cfg_attr(not(feature = "library"), entry_point)]
130124
pub fn ibc_packet_ack(
131-
_deps: DepsMut,
125+
deps: DepsMut,
132126
_env: Env,
133-
_msg: IbcPacketAckMsg,
127+
msg: IbcPacketAckMsg,
134128
) -> Result<IbcBasicResponse, ContractError> {
135-
Err(ContractError::IbcAckNotAccepted)
129+
let ack: PriceFeedProviderAck = from_slice(&msg.acknowledgement.data)?;
130+
let PriceFeedProviderAck::Update { twap } = ack;
131+
let contract = RemotePriceFeedContract::new();
132+
contract.price_info.save(
133+
deps.storage,
134+
&PriceInfo {
135+
native_per_foreign: twap,
136+
},
137+
)?;
138+
139+
Ok(IbcBasicResponse::new())
136140
}
137141

138142
#[cfg_attr(not(feature = "library"), entry_point)]
@@ -141,5 +145,17 @@ pub fn ibc_packet_timeout(
141145
_env: Env,
142146
_msg: IbcPacketTimeoutMsg,
143147
) -> Result<IbcBasicResponse, ContractError> {
144-
Err(ContractError::IbcAckNotAccepted)
148+
Err(ContractError::IbcReceiveNotAccepted)
149+
}
150+
151+
pub(crate) fn make_ibc_packet(
152+
now: &Timestamp,
153+
channel: IbcChannel,
154+
packet: RemotePriceFeedPacket,
155+
) -> Result<IbcMsg, ContractError> {
156+
Ok(IbcMsg::SendPacket {
157+
channel_id: channel.endpoint.channel_id,
158+
data: to_binary(&packet)?,
159+
timeout: packet_timeout(now),
160+
})
145161
}
Original file line numberDiff line numberDiff line change
@@ -1 +1,14 @@
1+
use cosmwasm_schema::cw_serde;
2+
use cosmwasm_std::Decimal;
13

4+
#[cw_serde]
5+
pub struct TradingPair {
6+
pub pool_id: u64,
7+
pub base_asset: String,
8+
pub quote_asset: String,
9+
}
10+
11+
#[cw_serde]
12+
pub struct PriceInfo {
13+
pub native_per_foreign: Decimal,
14+
}

0 commit comments

Comments
 (0)