[cosmwasm] Osmosis multiple fee token support (#763)

* osmosis txfee supoort

* add get update fee for denom

* query for tx fee support

* add fee checker

* add comment

* implement osmosis code

* correct err response

* fix bugs

* cargo update

* disable rust feature

* return bool instead of result

* add wormhole comment

* tests added

* address feedback

* improve comments

* cargo update
This commit is contained in:
Dev Kalra 2023-04-14 12:19:57 +05:30 committed by GitHub
parent 42ddfb6466
commit 15060d6a5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1206 additions and 572 deletions

File diff suppressed because it is too large Load Diff

View File

@ -9,12 +9,15 @@ description = "Pyth price receiver"
crate-type = ["cdylib", "rlib"]
[features]
# IMPORTANT: if you want to build for injective, enable the default feature below
# IMPORTANT: if you want to build for injective or osmosis, enable injective default feature for injective
# and osmosis default feature for osmosis
# default=["injective"]
# default=["osmosis"]
backtraces = ["cosmwasm-std/backtraces"]
# use library feature to disable all init/handle/query exports
library = []
injective = ["dep:serde_repr"]
osmosis=["pyth-sdk-cw/osmosis"]
[dependencies]
cosmwasm-std = { version = "1.0.0" }
@ -24,9 +27,7 @@ serde = { version = "1.0.103", default-features = false, features = ["derive"] }
serde_derive = { version = "1.0.103"}
serde_repr = { version="0.1", optional = true}
terraswap = "2.4.0"
wormhole-bridge-terra-2 = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.14.8", features = ["library"] }
thiserror = { version = "1.0.20" }
k256 = { version = "0.9.4", default-features = false, features = ["ecdsa"] }
sha3 = { version = "0.9.1", default-features = false }
generic-array = { version = "0.14.4" }
hex = "0.4.2"
@ -36,6 +37,7 @@ pyth-wormhole-attester-sdk = { path = "../../../../wormhole_attester/sdk/rust" }
pyth-sdk = "0.7.0"
byteorder = "1.4.3"
cosmwasm-schema = "1.1.9"
osmosis-std = "0.15.2"
pyth-sdk-cw = { path = "../../sdk/rust" }
[dev-dependencies]

View File

@ -5,6 +5,8 @@ use crate::injective::{
};
#[cfg(not(feature = "injective"))]
use cosmwasm_std::Empty as MsgWrapper;
#[cfg(feature = "osmosis")]
use osmosis_std::types::osmosis::txfees::v1beta1::TxfeesQuerier;
use {
crate::{
governance::{
@ -32,11 +34,14 @@ use {
ConfigInfo,
PythDataSource,
},
wormhole::{
ParsedVAA,
WormholeQueryMsg,
},
},
cosmwasm_std::{
coin,
entry_point,
has_coins,
to_binary,
Addr,
Binary,
@ -74,10 +79,6 @@ use {
iter::FromIterator,
time::Duration,
},
wormhole::{
msg::QueryMsg as WormholeQueryMsg,
state::ParsedVAA,
},
};
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
@ -156,6 +157,78 @@ pub fn execute(
}
}
#[cfg(not(feature = "osmosis"))]
fn is_fee_sufficient(deps: &Deps, info: MessageInfo, data: &[Binary]) -> StdResult<bool> {
use cosmwasm_std::has_coins;
let state = config_read(deps.storage).load()?;
// For any chain other than osmosis there is only one base denom
// If base denom is present in coins and has enough amount this will return true
// or if the base fee is set to 0
// else it will return false
return Ok(state.fee.amount.u128() == 0
|| has_coins(info.funds.as_ref(), &get_update_fee(deps, data)?));
}
// it only checks for fee denoms other than the base denom
#[cfg(feature = "osmosis")]
fn is_allowed_tx_fees_denom(deps: &Deps, denom: &String) -> bool {
// TxFeesQuerier uses stargate queries which we can't mock as of now.
// The capability has not been implemented in `cosmwasm-std` yet.
// Hence, we are hacking it with a feature flag to be able to write tests.
// FIXME
#[cfg(test)]
if denom == "uion"
|| denom == "ibc/FF3065989E34457F342D4EFB8692406D49D4E2B5C70F725F127862E22CE6BDCD"
{
return true;
}
let querier = TxfeesQuerier::new(&deps.querier);
match querier.denom_pool_id(denom.to_string()) {
Ok(_) => true,
Err(_) => false,
}
}
// TODO: add tests for these
#[cfg(feature = "osmosis")]
fn is_fee_sufficient(deps: &Deps, info: MessageInfo, data: &[Binary]) -> StdResult<bool> {
let state = config_read(deps.storage).load()?;
// how to change this in future
// for given coins verify they are allowed in txfee module
// convert each of them to the base token that is 'uosmo'
// combine all the converted token
// check with `has_coins`
// FIXME: should we accept fee for a single transaction in different tokens?
let mut total_amount = 0u128;
for coin in &info.funds {
if coin.denom != state.fee.denom && !is_allowed_tx_fees_denom(deps, &coin.denom) {
return Err(PythContractError::InvalidFeeDenom {
denom: coin.denom.to_string(),
})?;
}
total_amount = total_amount
.checked_add(coin.amount.u128())
.ok_or(OverflowError::new(
OverflowOperation::Add,
total_amount,
coin.amount,
))?;
}
let base_denom_fee = get_update_fee(deps, data)?;
// NOTE: the base fee denom right now is = denom: 'uosmo', amount: 1, which is almost negligible
// It's not important to convert the price right now. For now
// we are keeping the base fee amount same for each valid denom -> 1
// but this logic will be updated to use spot price for different valid tokens in future
Ok(base_denom_fee.amount.u128() <= total_amount)
}
/// Update the on-chain price feeds given the array of price update VAAs `data`.
/// Each price update VAA must be a valid Wormhole message and sent from an authorized emitter.
///
@ -169,11 +242,8 @@ fn update_price_feeds(
) -> StdResult<Response<MsgWrapper>> {
let state = config_read(deps.storage).load()?;
// Check that a sufficient fee was sent with the message
if state.fee.amount.u128() > 0
&& !has_coins(info.funds.as_ref(), &get_update_fee(&deps.as_ref(), data)?)
{
return Err(PythContractError::InsufficientFee.into());
if !is_fee_sufficient(&deps.as_ref(), info, data)? {
return Err(PythContractError::InsufficientFee)?;
}
let mut num_total_attestations: usize = 0;
@ -504,6 +574,10 @@ fn update_price_feed_if_new(
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::PriceFeed { id } => to_binary(&query_price_feed(&deps, id.as_ref())?),
#[cfg(feature = "osmosis")]
QueryMsg::GetUpdateFeeForDenom { vaas, denom } => {
to_binary(&get_update_fee_for_denom(&deps, &vaas, denom)?)
}
QueryMsg::GetUpdateFee { vaas } => to_binary(&get_update_fee(&deps, &vaas)?),
QueryMsg::GetValidTimePeriod => to_binary(&get_valid_time_period(&deps)?),
}
@ -519,6 +593,7 @@ pub fn query_price_feed(deps: &Deps, feed_id: &[u8]) -> StdResult<PriceFeedRespo
/// Get the fee that a caller must pay in order to submit a price update.
/// The fee depends on both the current contract configuration and the update data `vaas`.
/// The fee is in the denoms as stored in the current configuration
pub fn get_update_fee(deps: &Deps, vaas: &[Binary]) -> StdResult<Coin> {
let config = config_read(deps.storage).load()?;
@ -537,6 +612,39 @@ pub fn get_update_fee(deps: &Deps, vaas: &[Binary]) -> StdResult<Coin> {
))
}
#[cfg(feature = "osmosis")]
/// Osmosis can support multiple tokens for transaction fees
/// This will return update fee for the given denom only if that denom is allowed in Osmosis's txFee module
/// Else it will throw error
pub fn get_update_fee_for_denom(deps: &Deps, vaas: &[Binary], denom: String) -> StdResult<Coin> {
let config = config_read(deps.storage).load()?;
// if the denom is not a base denom it should be an allowed one
if denom != config.fee.denom && !is_allowed_tx_fees_denom(deps, &denom) {
return Err(PythContractError::InvalidFeeDenom { denom })?;
}
// the base fee is set to -> denom = base denom of a chain, amount = 1
// which is very minimal
// for other valid denoms too we are using the base amount as 1
// base amount is multiplied to number of vaas to get the total amount
// this will be change later on to add custom logic using spot price for valid tokens
Ok(coin(
config
.fee
.amount
.u128()
.checked_mul(vaas.len() as u128)
.ok_or(OverflowError::new(
OverflowOperation::Mul,
config.fee.amount,
vaas.len(),
))?,
denom,
))
}
pub fn get_valid_time_period(deps: &Deps) -> StdResult<Duration> {
Ok(config_read(deps.storage).load()?.valid_time_period)
}
@ -808,6 +916,80 @@ mod test {
assert!(res.is_err());
}
#[cfg(not(feature = "osmosis"))]
#[test]
fn test_is_fee_sufficient() {
let mut config_info = default_config_info();
config_info.fee = Coin::new(100, "foo");
let (mut deps, _env) = setup_test();
config(&mut deps.storage).save(&config_info).unwrap();
let mut info = mock_info("123", coins(100, "foo").as_slice());
let data = create_price_update_msg(default_emitter_addr().as_slice(), EMITTER_CHAIN);
// sufficient fee -> true
let result = is_fee_sufficient(&deps.as_ref(), info.clone(), &[data.clone()]);
assert_eq!(result, Ok(true));
// insufficient fee -> false
info.funds = coins(50, "foo");
let result = is_fee_sufficient(&deps.as_ref(), info.clone(), &[data.clone()]);
assert_eq!(result, Ok(false));
// insufficient fee -> false
info.funds = coins(150, "bar");
let result = is_fee_sufficient(&deps.as_ref(), info, &[data]);
assert_eq!(result, Ok(false));
}
#[cfg(feature = "osmosis")]
#[test]
fn test_is_fee_sufficient() {
// setup config with base fee
let base_denom = "foo";
let base_amount = 100;
let mut config_info = default_config_info();
config_info.fee = Coin::new(base_amount, base_denom);
let (mut deps, _env) = setup_test();
config(&mut deps.storage).save(&config_info).unwrap();
// a dummy price data
let data = create_price_update_msg(default_emitter_addr().as_slice(), EMITTER_CHAIN);
// sufficient fee in base denom -> true
let info = mock_info("123", coins(base_amount, base_denom).as_slice());
let result = is_fee_sufficient(&deps.as_ref(), info.clone(), &[data.clone()]);
assert_eq!(result, Ok(true));
// insufficient fee in base denom -> false
let info = mock_info("123", coins(50, base_denom).as_slice());
let result = is_fee_sufficient(&deps.as_ref(), info, &[data.clone()]);
assert_eq!(result, Ok(false));
// valid denoms are 'uion' or 'ibc/FF3065989E34457F342D4EFB8692406D49D4E2B5C70F725F127862E22CE6BDCD'
// a valid denom other than base denom with sufficient fee
let info = mock_info("123", coins(100, "uion").as_slice());
let result = is_fee_sufficient(&deps.as_ref(), info, &[data.clone()]);
assert_eq!(result, Ok(true));
// insufficient fee in valid denom -> false
let info = mock_info("123", coins(50, "uion").as_slice());
let result = is_fee_sufficient(&deps.as_ref(), info, &[data.clone()]);
assert_eq!(result, Ok(false));
// an invalid denom -> Err invalid fee denom
let info = mock_info("123", coins(100, "invalid_denom").as_slice());
let result = is_fee_sufficient(&deps.as_ref(), info, &[data.clone()]);
assert_eq!(
result,
Err(PythContractError::InvalidFeeDenom {
denom: "invalid_denom".to_string(),
}
.into())
);
}
#[test]
fn test_process_batch_attestation_empty_array() {
let (mut deps, env) = setup_test();
@ -935,7 +1117,6 @@ mod test {
let (num_attestations, new_attestations) =
process_batch_attestation(&mut deps.as_mut(), &env, &attestations).unwrap();
let stored_price_feed = price_feed_read_bucket(&deps.storage)
.load(&[0u8; 32])
.unwrap();
@ -987,7 +1168,6 @@ mod test {
let (num_attestations, new_attestations) =
process_batch_attestation(&mut deps.as_mut(), &env, &attestations).unwrap();
let stored_price_feed = price_feed_read_bucket(&deps.storage)
.load(&[0u8; 32])
.unwrap();
@ -1044,44 +1224,6 @@ mod test {
assert_eq!(result, Err(PythContractError::InvalidUpdateEmitter.into()));
}
#[test]
fn test_update_price_feeds_insufficient_fee() {
let mut config_info = default_config_info();
config_info.fee = Coin::new(100, "foo");
let result = apply_price_update(
&config_info,
default_emitter_addr().as_slice(),
EMITTER_CHAIN,
&[],
);
assert_eq!(result, Err(PythContractError::InsufficientFee.into()));
let result = apply_price_update(
&config_info,
default_emitter_addr().as_slice(),
EMITTER_CHAIN,
coins(100, "foo").as_slice(),
);
assert!(result.is_ok());
let result = apply_price_update(
&config_info,
default_emitter_addr().as_slice(),
EMITTER_CHAIN,
coins(99, "foo").as_slice(),
);
assert_eq!(result, Err(PythContractError::InsufficientFee.into()));
let result = apply_price_update(
&config_info,
default_emitter_addr().as_slice(),
EMITTER_CHAIN,
coins(100, "bar").as_slice(),
);
assert_eq!(result, Err(PythContractError::InsufficientFee.into()));
}
#[test]
fn test_update_price_feed_if_new_first_price_ok() {
let (mut deps, env) = setup_test();
@ -1232,6 +1374,118 @@ mod test {
assert!(get_update_fee(&deps.as_ref(), &updates[0..2]).is_err());
}
#[cfg(feature = "osmosis")]
#[test]
fn test_get_update_fee_for_denom() {
let (mut deps, _env) = setup_test();
let base_denom = "test";
config(&mut deps.storage)
.save(&ConfigInfo {
fee: Coin::new(10, base_denom),
..create_zero_config_info()
})
.unwrap();
let updates = vec![Binary::from([1u8]), Binary::from([2u8])];
// test for base denom
assert_eq!(
get_update_fee_for_denom(&deps.as_ref(), &updates[0..0], base_denom.to_string()),
Ok(Coin::new(0, base_denom))
);
assert_eq!(
get_update_fee_for_denom(&deps.as_ref(), &updates[0..1], base_denom.to_string()),
Ok(Coin::new(10, base_denom))
);
assert_eq!(
get_update_fee_for_denom(&deps.as_ref(), &updates[0..2], base_denom.to_string()),
Ok(Coin::new(20, base_denom))
);
// test for valid but not base denom
// valid denoms are 'uion' or 'ibc/FF3065989E34457F342D4EFB8692406D49D4E2B5C70F725F127862E22CE6BDCD'
assert_eq!(
get_update_fee_for_denom(&deps.as_ref(), &updates[0..0], "uion".to_string()),
Ok(Coin::new(0, "uion"))
);
assert_eq!(
get_update_fee_for_denom(&deps.as_ref(), &updates[0..1], "uion".to_string()),
Ok(Coin::new(10, "uion"))
);
assert_eq!(
get_update_fee_for_denom(&deps.as_ref(), &updates[0..2], "uion".to_string()),
Ok(Coin::new(20, "uion"))
);
// test for invalid denom
assert_eq!(
get_update_fee_for_denom(&deps.as_ref(), &updates[0..0], "invalid_denom".to_string()),
Err(PythContractError::InvalidFeeDenom {
denom: "invalid_denom".to_string(),
}
.into())
);
assert_eq!(
get_update_fee_for_denom(&deps.as_ref(), &updates[0..1], "invalid_denom".to_string()),
Err(PythContractError::InvalidFeeDenom {
denom: "invalid_denom".to_string(),
}
.into())
);
assert_eq!(
get_update_fee_for_denom(&deps.as_ref(), &updates[0..2], "invalid_denom".to_string()),
Err(PythContractError::InvalidFeeDenom {
denom: "invalid_denom".to_string(),
}
.into())
);
// check for overflow
let big_fee: u128 = (u128::MAX / 4) * 3;
config(&mut deps.storage)
.save(&ConfigInfo {
fee: Coin::new(big_fee, base_denom),
..create_zero_config_info()
})
.unwrap();
// base denom
assert_eq!(
get_update_fee_for_denom(&deps.as_ref(), &updates[0..1], base_denom.to_string()),
Ok(Coin::new(big_fee, base_denom))
);
assert!(
get_update_fee_for_denom(&deps.as_ref(), &updates[0..2], base_denom.to_string())
.is_err()
);
// valid but not base
assert_eq!(
get_update_fee_for_denom(&deps.as_ref(), &updates[0..1], "uion".to_string()),
Ok(Coin::new(big_fee, "uion"))
);
assert!(
get_update_fee_for_denom(&deps.as_ref(), &updates[0..2], "uion".to_string()).is_err()
);
// invalid
assert_eq!(
get_update_fee_for_denom(&deps.as_ref(), &updates[0..1], "invalid_denom".to_string()),
Err(PythContractError::InvalidFeeDenom {
denom: "invalid_denom".to_string(),
}
.into())
);
assert_eq!(
get_update_fee_for_denom(&deps.as_ref(), &updates[0..2], "invalid_denom".to_string()),
Err(PythContractError::InvalidFeeDenom {
denom: "invalid_denom".to_string(),
}
.into())
);
}
#[test]
fn test_get_valid_time_period() {
let (mut deps, _env) = setup_test();

View File

@ -5,6 +5,7 @@ pub mod contract;
pub mod governance;
pub mod msg;
pub mod state;
pub mod wormhole;
#[cfg(feature = "injective")]
mod injective;

View File

@ -0,0 +1,46 @@
// These types are copied from the Wormhole contract. See the links with each type to see the original code
// The reason to do so was dependency conflict. Wormhole contracts were using a very old version of a dependency
// which is not compatible with the one used by osmosis-sdk. And since we weren't using anything else from
// the Wormhole contract the types are moved here.
use {
cosmwasm_std::Binary,
schemars::JsonSchema,
serde::{
Deserialize,
Serialize,
},
};
type HumanAddr = String;
// This type is copied from
// https://github.com/wormhole-foundation/wormhole/blob/main/cosmwasm/contracts/wormhole/src/state.rs#L75
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
pub struct ParsedVAA {
pub version: u8,
pub guardian_set_index: u32,
pub timestamp: u32,
pub nonce: u32,
pub len_signers: u8,
pub emitter_chain: u16,
pub emitter_address: Vec<u8>,
pub sequence: u64,
pub consistency_level: u8,
pub payload: Vec<u8>,
pub hash: Vec<u8>,
}
// The type is copied from
// https://github.com/wormhole-foundation/wormhole/blob/main/cosmwasm/contracts/wormhole/src/msg.rs#L37
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WormholeQueryMsg {
GuardianSetInfo {},
VerifyVAA { vaa: Binary, block_time: u64 },
GetState {},
QueryAddressHex { address: HumanAddr },
}

View File

@ -11,6 +11,9 @@ keywords = [ "pyth", "oracle", "cosmwasm" ]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
osmosis=[]
[dependencies]
pyth-sdk = "0.7.0"
cosmwasm-std = { version = "1.0.0" }

View File

@ -48,6 +48,10 @@ pub enum PythContractError {
/// The message did not include a sufficient fee.
#[error("InsufficientFee")]
InsufficientFee,
/// The message did not include a sufficient fee.
#[error("InvalidFeeDenom")]
InvalidFeeDenom { denom: String },
}
impl From<PythContractError> for StdError {

View File

@ -39,6 +39,9 @@ pub enum QueryMsg {
PriceFeed { id: PriceIdentifier },
#[returns(Coin)]
GetUpdateFee { vaas: Vec<Binary> },
#[cfg(feature = "osmosis")]
#[returns(Coin)]
GetUpdateFeeForDenom { denom: String, vaas: Vec<Binary> },
#[returns(Duration)]
GetValidTimePeriod,
}
@ -76,6 +79,24 @@ pub fn get_update_fee(
}))
}
#[cfg(feature = "osmosis")]
/// Get the fee required in order to update the on-chain state with the provided
/// `price_update_vaas`.
pub fn get_update_fee_for_denom(
querier: &QuerierWrapper,
contract_addr: Addr,
price_update_vaas: &[Binary],
denom: String,
) -> StdResult<Coin> {
querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: contract_addr.into_string(),
msg: to_binary(&QueryMsg::GetUpdateFeeForDenom {
vaas: price_update_vaas.to_vec(),
denom,
})?,
}))
}
/// Get the default length of time for which a price update remains valid.
pub fn get_valid_time_period(querier: &QuerierWrapper, contract_addr: Addr) -> StdResult<Duration> {
querier.query(&QueryRequest::Wasm(WasmQuery::Smart {

View File

@ -71,6 +71,7 @@ impl MockPyth {
Ok(QueryMsg::GetValidTimePeriod) => {
SystemResult::Ok(to_binary(&self.valid_time_period).into())
}
Ok(QueryMsg::GetUpdateFee { vaas }) => {
let new_amount = self
.fee_per_vaa
@ -80,6 +81,16 @@ impl MockPyth {
.unwrap();
SystemResult::Ok(to_binary(&Coin::new(new_amount, &self.fee_per_vaa.denom)).into())
}
#[cfg(feature = "osmosis")]
Ok(QueryMsg::GetUpdateFeeForDenom { vaas, denom }) => {
let new_amount = self
.fee_per_vaa
.amount
.u128()
.checked_mul(vaas.len() as u128)
.unwrap();
SystemResult::Ok(to_binary(&Coin::new(new_amount, denom)).into())
}
Err(_e) => SystemResult::Err(SystemError::InvalidRequest {
error: "Invalid message".into(),
request: msg.clone(),