diff --git a/pythnet/pythnet_sdk/src/error.rs b/pythnet/pythnet_sdk/src/error.rs index b4152ad9fb..c82302ab27 100644 --- a/pythnet/pythnet_sdk/src/error.rs +++ b/pythnet/pythnet_sdk/src/error.rs @@ -7,6 +7,9 @@ pub enum Error { #[error("Invalid Version")] InvalidVersion, + + #[error("Deserialization error")] + DeserializationError, } #[macro_export] diff --git a/pythnet/pythnet_sdk/src/wire.rs b/pythnet/pythnet_sdk/src/wire.rs index d81ebcde48..5b198b0960 100644 --- a/pythnet/pythnet_sdk/src/wire.rs +++ b/pythnet/pythnet_sdk/src/wire.rs @@ -57,7 +57,7 @@ pub mod v1 { major_version: u8, minor_version: u8, trailing: Vec, - proof: Proof, + pub proof: Proof, } impl AccumulatorUpdateData { @@ -72,7 +72,8 @@ pub mod v1 { } pub fn try_from_slice(bytes: &[u8]) -> Result { - let message = from_slice::(bytes).unwrap(); + let message = from_slice::(bytes) + .map_err(|_| Error::DeserializationError)?; require!( &message.magic[..] == PYTHNET_ACCUMULATOR_UPDATE_MAGIC, Error::InvalidMagic @@ -109,8 +110,16 @@ pub mod v1 { pub const ACCUMULATOR_UPDATE_WORMHOLE_VERIFICATION_MAGIC: &[u8; 4] = b"AUWV"; impl WormholeMessage { + pub fn new(payload: WormholePayload) -> Self { + Self { + magic: *ACCUMULATOR_UPDATE_WORMHOLE_VERIFICATION_MAGIC, + payload, + } + } + pub fn try_from_bytes(bytes: impl AsRef<[u8]>) -> Result { - let message = from_slice::(bytes.as_ref()).unwrap(); + let message = from_slice::(bytes.as_ref()) + .map_err(|_| Error::DeserializationError)?; require!( &message.magic[..] == ACCUMULATOR_UPDATE_WORMHOLE_VERIFICATION_MAGIC, Error::InvalidMagic diff --git a/target_chains/cosmwasm/Cargo.lock b/target_chains/cosmwasm/Cargo.lock index 8fd785eaeb..0a2b337ebf 100644 --- a/target_chains/cosmwasm/Cargo.lock +++ b/target_chains/cosmwasm/Cargo.lock @@ -83,6 +83,15 @@ dependencies = [ "crunchy 0.1.6", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -187,6 +196,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + [[package]] name = "byteorder" version = "1.4.3" @@ -765,6 +794,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fast-math" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2465292146cdfc2011350fe3b1c616ac83cf0faeedb33463ba1c332ed8948d66" +dependencies = [ + "ieee754", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -902,6 +940,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "ieee754" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9007da9cacbd3e6343da136e98b0d2df013f553d35bdec8b518f07bea768e19c" + [[package]] name = "indexmap" version = "1.9.3" @@ -971,9 +1015,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" dependencies = [ "cpufeatures", ] @@ -1099,6 +1143,40 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7843ec2de400bcbc6a6328c958dc38e5359da6e93e72e37bc5246bf1ae776389" +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" +dependencies = [ + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1109,6 +1187,29 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -1331,12 +1432,13 @@ dependencies = [ "pyth-sdk 0.7.0", "pyth-sdk-cw", "pyth-wormhole-attester-sdk", + "pythnet-sdk", "schemars", "serde", "serde_derive", "serde_json", "serde_repr", - "sha3", + "sha3 0.9.1", "terraswap", "thiserror", ] @@ -1386,6 +1488,23 @@ dependencies = [ "serde", ] +[[package]] +name = "pythnet-sdk" +version = "1.13.6" +dependencies = [ + "bincode", + "borsh", + "bytemuck", + "byteorder", + "fast-math", + "hex", + "rustc_version", + "serde", + "sha3 0.10.8", + "slow_primes", + "thiserror", +] + [[package]] name = "quote" version = "1.0.26" @@ -1521,6 +1640,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.37.11" @@ -1597,6 +1725,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + [[package]] name = "serde" version = "1.0.160" @@ -1713,6 +1847,16 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.6", + "keccak", +] + [[package]] name = "signature" version = "1.6.4" @@ -1729,6 +1873,15 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "slow_primes" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58267dd2fbaa6dceecba9e3e106d2d90a2b02497c0e8b01b8759beccf5113938" +dependencies = [ + "num", +] + [[package]] name = "smallvec" version = "1.10.0" diff --git a/target_chains/cosmwasm/contracts/pyth/Cargo.toml b/target_chains/cosmwasm/contracts/pyth/Cargo.toml index 0d00ab4fc1..90a45310fe 100644 --- a/target_chains/cosmwasm/contracts/pyth/Cargo.toml +++ b/target_chains/cosmwasm/contracts/pyth/Cargo.toml @@ -39,6 +39,7 @@ byteorder = "1.4.3" cosmwasm-schema = "1.1.9" osmosis-std = "0.15.2" pyth-sdk-cw = { path = "../../sdk/rust" } +pythnet-sdk = { path = "../../../../pythnet/pythnet_sdk" } [dev-dependencies] cosmwasm-vm = { version = "1.0.0", default-features = false } diff --git a/target_chains/cosmwasm/contracts/pyth/src/contract.rs b/target_chains/cosmwasm/contracts/pyth/src/contract.rs index 082b0ef36d..781fc1296f 100644 --- a/target_chains/cosmwasm/contracts/pyth/src/contract.rs +++ b/target_chains/cosmwasm/contracts/pyth/src/contract.rs @@ -39,6 +39,7 @@ use { WormholeQueryMsg, }, }, + byteorder::BigEndian, cosmwasm_std::{ coin, entry_point, @@ -73,6 +74,21 @@ use { PriceAttestation, PriceStatus, }, + pythnet_sdk::{ + accumulators::merkle::MerkleRoot, + hashers::keccak256_160::Keccak160, + messages::Message, + wire::{ + from_slice, + v1::{ + AccumulatorUpdateData, + Proof, + WormholeMessage, + WormholePayload, + PYTHNET_ACCUMULATOR_UPDATE_MAGIC, + }, + }, + }, std::{ collections::HashSet, convert::TryFrom, @@ -247,32 +263,36 @@ fn update_price_feeds( } let mut num_total_attestations: usize = 0; - let mut total_new_attestations: Vec = vec![]; + let mut total_new_feeds: Vec = vec![]; for datum in data { - let vaa = parse_and_verify_vaa(deps.branch(), env.block.time.seconds(), datum)?; - verify_vaa_from_data_source(&state, &vaa)?; - - let data = &vaa.payload; - let batch_attestation = BatchPriceAttestation::deserialize(&data[..]) - .map_err(|_| PythContractError::InvalidUpdatePayload)?; - - let (num_attestations, new_attestations) = - process_batch_attestation(&mut deps, &env, &batch_attestation)?; + let header = datum.get(0..4); + let (num_attestations, new_feeds) = + if header == Some(PYTHNET_ACCUMULATOR_UPDATE_MAGIC.as_slice()) { + process_accumulator(&mut deps, &env, datum)? + } else { + let vaa = parse_and_verify_vaa(deps.branch(), env.block.time.seconds(), datum)?; + verify_vaa_from_data_source(&state, &vaa)?; + + let data = &vaa.payload; + let batch_attestation = BatchPriceAttestation::deserialize(&data[..]) + .map_err(|_| PythContractError::InvalidUpdatePayload)?; + + process_batch_attestation(&mut deps, &env, &batch_attestation)? + }; num_total_attestations += num_attestations; - for new_attestation in new_attestations { - total_new_attestations.push(new_attestation.to_owned()); + for new_feed in new_feeds { + total_new_feeds.push(new_feed.to_owned()); } } - let num_total_new_attestations = total_new_attestations.len(); + let num_total_new_attestations = total_new_feeds.len(); let response = Response::new(); #[cfg(feature = "injective")] { - let inj_message = - create_relay_pyth_prices_msg(env.contract.address, total_new_attestations); + let inj_message = create_relay_pyth_prices_msg(env.contract.address, total_new_feeds); Ok(response .add_message(inj_message) .add_attribute("action", "update_price_feeds") @@ -480,24 +500,88 @@ fn verify_vaa_from_governance_source(state: &ConfigInfo, vaa: &ParsedVAA) -> Std Ok(()) } +fn process_accumulator( + deps: &mut DepsMut, + env: &Env, + data: &[u8], +) -> StdResult<(usize, Vec)> { + let update_data = AccumulatorUpdateData::try_from_slice(data) + .map_err(|_| PythContractError::InvalidAccumulatorPayload)?; + match update_data.proof { + Proof::WormholeMerkle { vaa, updates } => { + let parsed_vaa = parse_and_verify_vaa( + deps.branch(), + env.block.time.seconds(), + &Binary::from(Vec::from(vaa)), + )?; + let state = config_read(deps.storage).load()?; + verify_vaa_from_data_source(&state, &parsed_vaa)?; + + let msg = WormholeMessage::try_from_bytes(parsed_vaa.payload) + .map_err(|_| PythContractError::InvalidWormholeMessage)?; + + let root: MerkleRoot = MerkleRoot::new(match msg.payload { + WormholePayload::Merkle(merkle_root) => merkle_root.root, + }); + let update_len = updates.len(); + let mut new_feeds = vec![]; + for update in updates { + let message_vec = Vec::from(update.message); + if !root.check(update.proof, &message_vec) { + return Err(PythContractError::InvalidMerkleProof)?; + } + + let msg = from_slice::(&message_vec) + .map_err(|_| PythContractError::InvalidAccumulatorMessage)?; + + match msg { + Message::PriceFeedMessage(price_feed_message) => { + let price_feed = PriceFeed::new( + PriceIdentifier::new(price_feed_message.id), + Price { + price: price_feed_message.price, + conf: price_feed_message.conf, + expo: price_feed_message.exponent, + publish_time: price_feed_message.publish_time, + }, + Price { + price: price_feed_message.ema_price, + conf: price_feed_message.ema_conf, + expo: price_feed_message.exponent, + publish_time: price_feed_message.publish_time, + }, + ); + + if update_price_feed_if_new(deps, env, price_feed)? { + new_feeds.push(price_feed); + } + } + _ => return Err(PythContractError::InvalidAccumulatorMessageType)?, + } + } + Ok((update_len, new_feeds)) + } + } +} + /// Update the on-chain storage for any new price updates provided in `batch_attestation`. -fn process_batch_attestation<'a>( +fn process_batch_attestation( deps: &mut DepsMut, env: &Env, - batch_attestation: &'a BatchPriceAttestation, -) -> StdResult<(usize, Vec<&'a PriceAttestation>)> { - let mut new_attestations = vec![]; + batch_attestation: &BatchPriceAttestation, +) -> StdResult<(usize, Vec)> { + let mut new_feeds = vec![]; // Update prices for price_attestation in batch_attestation.price_attestations.iter() { let price_feed = create_price_feed_from_price_attestation(price_attestation); if update_price_feed_if_new(deps, env, price_feed)? { - new_attestations.push(price_attestation); + new_feeds.push(price_feed); } } - Ok((batch_attestation.price_attestations.len(), new_attestations)) + Ok((batch_attestation.price_attestations.len(), new_feeds)) } fn create_price_feed_from_price_attestation(price_attestation: &PriceAttestation) -> PriceFeed { @@ -591,25 +675,43 @@ pub fn query_price_feed(deps: &Deps, feed_id: &[u8]) -> StdResult StdResult { + let config = config_read(deps.storage).load()?; + + let mut total_updates: u128 = 0; + for datum in vaas { + let header = datum.get(0..4); + if header == Some(PYTHNET_ACCUMULATOR_UPDATE_MAGIC.as_slice()) { + let update_data = AccumulatorUpdateData::try_from_slice(datum) + .map_err(|_| PythContractError::InvalidAccumulatorPayload)?; + match update_data.proof { + Proof::WormholeMerkle { vaa: _, updates } => { + total_updates += updates.len() as u128; + } + } + } else { + total_updates += 1; + } + } + + Ok(config + .fee + .amount + .u128() + .checked_mul(total_updates) + .ok_or(OverflowError::new( + OverflowOperation::Mul, + config.fee.amount, + total_updates, + ))?) +} + /// 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 { let config = config_read(deps.storage).load()?; - - Ok(coin( - config - .fee - .amount - .u128() - .checked_mul(vaas.len() as u128) - .ok_or(OverflowError::new( - OverflowOperation::Mul, - config.fee.amount, - vaas.len(), - ))?, - config.fee.denom, - )) + Ok(coin(get_update_fee_amount(deps, vaas)?, config.fee.denom)) } #[cfg(feature = "osmosis")] @@ -630,19 +732,7 @@ pub fn get_update_fee_for_denom(deps: &Deps, vaas: &[Binary], denom: String) -> // 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, - )) + Ok(coin(get_update_fee_amount(deps, vaas)?, denom)) } pub fn get_valid_time_period(deps: &Deps) -> StdResult { @@ -675,6 +765,7 @@ mod test { ContractResult, OwnedDeps, QuerierResult, + StdError, SystemError, SystemResult, Uint128, @@ -682,6 +773,24 @@ mod test { pyth_sdk::UnixTimestamp, pyth_sdk_cw::PriceIdentifier, pyth_wormhole_attester_sdk::PriceAttestation, + pythnet_sdk::{ + accumulators::{ + merkle::MerkleTree, + Accumulator, + }, + messages::{ + PriceFeedMessage, + TwapMessage, + }, + wire::{ + to_vec, + v1::{ + MerklePriceUpdate, + WormholeMerkleRoot, + }, + PrefixedVec, + }, + }, std::time::Duration, }; @@ -1003,6 +1112,452 @@ mod test { assert_eq!(new_attestations.len(), 0); } + fn create_dummy_price_feed_message(value: i64) -> Message { + let mut dummy_id = [0; 32]; + dummy_id[0] = value as u8; + let msg = PriceFeedMessage { + id: dummy_id, + price: value, + conf: value as u64, + exponent: value as i32, + publish_time: value, + prev_publish_time: value, + ema_price: value, + ema_conf: value as u64, + }; + Message::PriceFeedMessage(msg) + } + + fn create_accumulator_message_from_updates( + price_updates: Vec, + tree: MerkleTree, + corrupt_wormhole_message: bool, + emitter_address: Vec, + emitter_chain: u16, + ) -> Binary { + let mut root_hash = [0u8; 20]; + root_hash.copy_from_slice(&to_vec::<_, BigEndian>(&tree.root).unwrap()[..20]); + let wormhole_message = WormholeMessage::new(WormholePayload::Merkle(WormholeMerkleRoot { + slot: 0, + ring_size: 0, + root: root_hash, + })); + + let mut vaa = create_zero_vaa(); + vaa.emitter_address = emitter_address; + vaa.emitter_chain = emitter_chain; + vaa.payload = to_vec::<_, BigEndian>(&wormhole_message).unwrap(); + if corrupt_wormhole_message { + vaa.payload[0] = 0; + } + + let vaa_binary = to_binary(&vaa).unwrap(); + let accumulator_update_data = AccumulatorUpdateData::new(Proof::WormholeMerkle { + vaa: PrefixedVec::from(vaa_binary.to_vec()), + updates: price_updates, + }); + + Binary::from(to_vec::<_, BigEndian>(&accumulator_update_data).unwrap()) + } + + fn create_accumulator_message( + all_feeds: &[Message], + updates: &[Message], + corrupt_wormhole_message: bool, + ) -> Binary { + let all_feeds_bytes: Vec<_> = all_feeds + .iter() + .map(|f| to_vec::<_, BigEndian>(f).unwrap()) + .collect(); + let all_feeds_bytes_refs: Vec<_> = all_feeds_bytes.iter().map(|f| f.as_ref()).collect(); + let tree = MerkleTree::::new(all_feeds_bytes_refs.as_slice()).unwrap(); + let mut price_updates: Vec = vec![]; + for update in updates { + let proof = tree + .prove(&to_vec::<_, BigEndian>(update).unwrap()) + .unwrap(); + price_updates.push(MerklePriceUpdate { + message: PrefixedVec::from(to_vec::<_, BigEndian>(update).unwrap()), + proof, + }); + } + create_accumulator_message_from_updates( + price_updates, + tree, + corrupt_wormhole_message, + default_emitter_addr(), + EMITTER_CHAIN, + ) + } + + + fn check_price_match(deps: &OwnedDeps, msg: &Message) { + match msg { + Message::PriceFeedMessage(feed_msg) => { + let feed = price_feed_read_bucket(&deps.storage) + .load(&feed_msg.id) + .unwrap(); + let price = feed.get_price_unchecked(); + let ema_price = feed.get_ema_price_unchecked(); + assert_eq!(price.price, feed_msg.price); + assert_eq!(price.conf, feed_msg.conf); + assert_eq!(price.expo, feed_msg.exponent); + assert_eq!(price.publish_time, feed_msg.publish_time); + + assert_eq!(ema_price.price, feed_msg.ema_price); + assert_eq!(ema_price.conf, feed_msg.ema_conf); + assert_eq!(ema_price.expo, feed_msg.exponent); + assert_eq!(ema_price.publish_time, feed_msg.publish_time); + } + _ => assert!(false, "invalid message type"), + }; + } + + fn test_accumulator_wrong_source(emitter_address: Vec, emitter_chain: u16) { + let (mut deps, env) = setup_test(); + config(&mut deps.storage) + .save(&default_config_info()) + .unwrap(); + + let feed1 = create_dummy_price_feed_message(100); + let feed1_bytes = to_vec::<_, BigEndian>(&feed1).unwrap(); + let tree = MerkleTree::::new(&[feed1_bytes.as_slice()]).unwrap(); + let mut price_updates: Vec = vec![]; + let proof1 = tree.prove(&feed1_bytes).unwrap(); + price_updates.push(MerklePriceUpdate { + message: PrefixedVec::from(feed1_bytes), + proof: proof1, + }); + let msg = create_accumulator_message_from_updates( + price_updates, + tree, + false, + emitter_address, + emitter_chain, + ); + let info = mock_info("123", &[]); + let result = update_price_feeds(deps.as_mut(), env, info, &[msg]); + assert!(result.is_err()); + assert_eq!(result, Err(PythContractError::InvalidUpdateEmitter.into())); + } + + #[test] + fn test_accumulator_verify_vaa_sender_fail_wrong_emitter_address() { + let emitter_address = [17, 23, 14]; + test_accumulator_wrong_source(emitter_address.to_vec(), EMITTER_CHAIN); + } + + #[test] + fn test_accumulator_verify_vaa_sender_fail_wrong_emitter_chain() { + test_accumulator_wrong_source(default_emitter_addr(), EMITTER_CHAIN + 1); + } + + #[test] + fn test_accumulator_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 feed1 = create_dummy_price_feed_message(100); + let feed2 = create_dummy_price_feed_message(200); + let feed3 = create_dummy_price_feed_message(300); + let msg = create_accumulator_message(&[feed1, feed2, feed3], &[feed1, feed3], false); + let data = &[msg]; + let mut info = mock_info("123", coins(200, "foo").as_slice()); + // sufficient fee -> true + let result = is_fee_sufficient(&deps.as_ref(), info.clone(), data); + assert_eq!(result, Ok(true)); + + // insufficient fee -> false + info.funds = coins(100, "foo"); + let result = is_fee_sufficient(&deps.as_ref(), info.clone(), data); + assert_eq!(result, Ok(false)); + + // insufficient fee -> false + info.funds = coins(300, "bar"); + let result = is_fee_sufficient(&deps.as_ref(), info, data); + assert_eq!(result, Ok(false)); + } + + #[test] + fn test_accumulator_get_update_fee_amount() { + 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 feed1 = create_dummy_price_feed_message(100); + let feed2 = create_dummy_price_feed_message(200); + let feed3 = create_dummy_price_feed_message(300); + + let msg = create_accumulator_message(&[feed1, feed2, feed3], &[feed1, feed3], false); + assert_eq!(get_update_fee_amount(&deps.as_ref(), &[msg]).unwrap(), 200); + + let msg = create_accumulator_message(&[feed1, feed2, feed3], &[feed1], false); + assert_eq!(get_update_fee_amount(&deps.as_ref(), &[msg]).unwrap(), 100); + + let msg = create_accumulator_message( + &[feed1, feed2, feed3], + &[feed1, feed2, feed3, feed1, feed3], + false, + ); + assert_eq!(get_update_fee_amount(&deps.as_ref(), &[msg]).unwrap(), 500); + } + + + #[test] + fn test_accumulator_message_single_update() { + let (mut deps, env) = setup_test(); + config(&mut deps.storage) + .save(&default_config_info()) + .unwrap(); + + let feed1 = create_dummy_price_feed_message(100); + let feed2 = create_dummy_price_feed_message(200); + let msg = create_accumulator_message(&[feed1, feed2], &[feed1], false); + let info = mock_info("123", &[]); + let result = update_price_feeds(deps.as_mut(), env, info, &[msg]); + assert!(result.is_ok()); + check_price_match(&deps, &feed1); + } + + #[test] + fn test_accumulator_message_multi_update_many_feeds() { + let (mut deps, env) = setup_test(); + config(&mut deps.storage) + .save(&default_config_info()) + .unwrap(); + let mut all_feeds: Vec = vec![]; + for i in 0..10000 { + all_feeds.push(create_dummy_price_feed_message(i)); + } + let msg = create_accumulator_message(&all_feeds, &all_feeds[100..110], false); + let info = mock_info("123", &[]); + let result = update_price_feeds(deps.as_mut(), env, info, &[msg]); + assert!(result.is_ok()); + for i in 100..110 { + check_price_match(&deps, &all_feeds[i]); + } + } + + fn as_mut_price_feed(msg: &mut Message) -> &mut PriceFeedMessage { + match msg { + Message::PriceFeedMessage(ref mut price_feed) => price_feed, + _ => { + panic!("unexpected message type"); + } + } + } + + #[test] + fn test_accumulator_multi_message_multi_update() { + let (mut deps, env) = setup_test(); + config(&mut deps.storage) + .save(&default_config_info()) + .unwrap(); + let mut feed1 = create_dummy_price_feed_message(100); + let mut feed2 = create_dummy_price_feed_message(200); + let mut feed3 = create_dummy_price_feed_message(300); + let msg = create_accumulator_message(&[feed1, feed2, feed3], &[feed1, feed2, feed3], false); + as_mut_price_feed(&mut feed1).publish_time += 1; + as_mut_price_feed(&mut feed2).publish_time += 1; + as_mut_price_feed(&mut feed3).publish_time += 1; + as_mut_price_feed(&mut feed1).price *= 2; + as_mut_price_feed(&mut feed2).price *= 2; + as_mut_price_feed(&mut feed3).price *= 2; + let msg2 = + create_accumulator_message(&[feed1, feed2, feed3], &[feed1, feed2, feed3], false); + let info = mock_info("123", &[]); + let result = update_price_feeds(deps.as_mut(), env, info, &[msg, msg2]); + + assert!(result.is_ok()); + check_price_match(&deps, &feed1); + check_price_match(&deps, &feed2); + check_price_match(&deps, &feed3); + } + + #[test] + fn test_accumulator_multi_update_out_of_order() { + let (mut deps, env) = setup_test(); + config(&mut deps.storage) + .save(&default_config_info()) + .unwrap(); + let feed1 = create_dummy_price_feed_message(100); + let mut feed2 = create_dummy_price_feed_message(100); + let feed3 = create_dummy_price_feed_message(300); + as_mut_price_feed(&mut feed2).publish_time -= 1; + as_mut_price_feed(&mut feed2).price *= 2; + let msg = create_accumulator_message(&[feed1, feed2, feed3], &[feed1, feed2, feed3], false); + let info = mock_info("123", &[]); + let result = update_price_feeds(deps.as_mut(), env, info, &[msg]); + + assert!(result.is_ok()); + check_price_match(&deps, &feed1); + check_price_match(&deps, &feed3); + } + + #[test] + fn test_accumulator_multi_message_multi_update_out_of_order() { + let (mut deps, env) = setup_test(); + config(&mut deps.storage) + .save(&default_config_info()) + .unwrap(); + let feed1 = create_dummy_price_feed_message(100); + let mut feed2 = create_dummy_price_feed_message(100); + let feed3 = create_dummy_price_feed_message(300); + as_mut_price_feed(&mut feed2).publish_time -= 1; + as_mut_price_feed(&mut feed2).price *= 2; + let msg = create_accumulator_message(&[feed1, feed2, feed3], &[feed1, feed3], false); + + let msg2 = create_accumulator_message(&[feed1, feed2, feed3], &[feed2, feed3], false); + let info = mock_info("123", &[]); + let result = update_price_feeds(deps.as_mut(), env, info, &[msg, msg2]); + + assert!(result.is_ok()); + check_price_match(&deps, &feed1); + check_price_match(&deps, &feed3); + } + + #[test] + fn test_invalid_accumulator_update() { + let (mut deps, env) = setup_test(); + config(&mut deps.storage) + .save(&default_config_info()) + .unwrap(); + + let feed1 = create_dummy_price_feed_message(100); + let mut msg = create_accumulator_message(&[feed1], &[feed1], false); + msg.0[5] = 3; + let info = mock_info("123", &[]); + let result = update_price_feeds(deps.as_mut(), env, info, &[msg]); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + StdError::from(PythContractError::InvalidAccumulatorPayload) + ); + } + + #[test] + fn test_invalid_wormhole_message() { + let (mut deps, env) = setup_test(); + config(&mut deps.storage) + .save(&default_config_info()) + .unwrap(); + + let feed1 = create_dummy_price_feed_message(100); + let msg = create_accumulator_message(&[feed1], &[feed1], true); + let info = mock_info("123", &[]); + let result = update_price_feeds(deps.as_mut(), env, info, &[msg]); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + StdError::from(PythContractError::InvalidWormholeMessage) + ); + } + + #[test] + fn test_invalid_accumulator_message_type() { + let (mut deps, env) = setup_test(); + config(&mut deps.storage) + .save(&default_config_info()) + .unwrap(); + // Although Twap Message is a valid message but it won't get stored on-chain via + // `update_price_feeds` and (will be) used in other methods + let feed1 = Message::TwapMessage(TwapMessage { + id: [0; 32], + cumulative_price: 0, + cumulative_conf: 0, + num_down_slots: 0, + exponent: 0, + publish_time: 0, + prev_publish_time: 0, + publish_slot: 0, + }); + let msg = create_accumulator_message(&[feed1], &[feed1], false); + let info = mock_info("123", &[]); + let result = update_price_feeds(deps.as_mut(), env, info, &[msg]); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + StdError::from(PythContractError::InvalidAccumulatorMessageType) + ); + } + + #[test] + fn test_invalid_proof() { + let (mut deps, env) = setup_test(); + config(&mut deps.storage) + .save(&default_config_info()) + .unwrap(); + + let feed1 = create_dummy_price_feed_message(100); + let feed2 = create_dummy_price_feed_message(200); + let feed1_bytes = to_vec::<_, BigEndian>(&feed1).unwrap(); + let feed2_bytes = to_vec::<_, BigEndian>(&feed2).unwrap(); + let tree = MerkleTree::::new(&[feed1_bytes.as_slice()]).unwrap(); + let mut price_updates: Vec = vec![]; + + let proof1 = tree.prove(&feed1_bytes).unwrap(); + price_updates.push(MerklePriceUpdate { + // proof1 is valid for feed1, but not feed2 + message: PrefixedVec::from(feed2_bytes), + proof: proof1, + }); + let msg = create_accumulator_message_from_updates( + price_updates, + tree, + false, + default_emitter_addr(), + EMITTER_CHAIN, + ); + let info = mock_info("123", &[]); + let result = update_price_feeds(deps.as_mut(), env, info, &[msg]); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + StdError::from(PythContractError::InvalidMerkleProof) + ); + } + + #[test] + fn test_invalid_message() { + let (mut deps, env) = setup_test(); + config(&mut deps.storage) + .save(&default_config_info()) + .unwrap(); + + let feed1 = create_dummy_price_feed_message(100); + let mut feed1_bytes = to_vec::<_, BigEndian>(&feed1).unwrap(); + feed1_bytes.pop(); + let tree = MerkleTree::::new(&[feed1_bytes.as_slice()]).unwrap(); + let mut price_updates: Vec = vec![]; + + let proof1 = tree.prove(&feed1_bytes).unwrap(); + price_updates.push(MerklePriceUpdate { + message: PrefixedVec::from(feed1_bytes), + proof: proof1, + }); + let msg = create_accumulator_message_from_updates( + price_updates, + tree, + false, + default_emitter_addr(), + EMITTER_CHAIN, + ); + let info = mock_info("123", &[]); + let result = update_price_feeds(deps.as_mut(), env, info, &[msg]); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + StdError::from(PythContractError::InvalidAccumulatorMessage) + ); + } + #[test] fn test_create_price_feed_from_price_attestation_status_trading() { let price_attestation = PriceAttestation { diff --git a/target_chains/cosmwasm/contracts/pyth/src/injective.rs b/target_chains/cosmwasm/contracts/pyth/src/injective.rs index 46e52984f2..65d6c335c4 100644 --- a/target_chains/cosmwasm/contracts/pyth/src/injective.rs +++ b/target_chains/cosmwasm/contracts/pyth/src/injective.rs @@ -4,7 +4,7 @@ use { CosmosMsg, CustomMsg, }, - pyth_wormhole_attester_sdk::PriceAttestation, + pyth_sdk_cw::PriceFeed, schemars::JsonSchema, serde::{ Deserialize, @@ -24,17 +24,19 @@ pub struct InjectivePriceAttestation { pub publish_time: i64, } -impl From<&PriceAttestation> for InjectivePriceAttestation { - fn from(pa: &PriceAttestation) -> Self { +impl From<&PriceFeed> for InjectivePriceAttestation { + fn from(pa: &PriceFeed) -> Self { + let price = pa.get_price_unchecked(); + let ema_price = pa.get_ema_price_unchecked(); InjectivePriceAttestation { - price_id: pa.price_id.to_hex(), - price: pa.price, - conf: pa.conf, - expo: pa.expo, - ema_price: pa.ema_price, - ema_conf: pa.ema_conf, - ema_expo: pa.expo, - publish_time: pa.publish_time, + price_id: pa.id.to_hex(), + price: price.price, + conf: price.conf, + expo: price.expo, + ema_price: ema_price.price, + ema_conf: ema_price.conf, + ema_expo: ema_price.expo, + publish_time: price.publish_time, } } } @@ -58,13 +60,13 @@ pub struct InjectiveMsgWrapper { pub fn create_relay_pyth_prices_msg( sender: Addr, - price_attestations: Vec, + price_feeds: Vec, ) -> CosmosMsg { InjectiveMsgWrapper { route: "oracle".to_string(), msg_data: InjectiveMsg::RelayPythPrices { sender, - price_attestations: price_attestations + price_attestations: price_feeds .iter() .map(InjectivePriceAttestation::from) .collect(), diff --git a/target_chains/cosmwasm/sdk/rust/src/error.rs b/target_chains/cosmwasm/sdk/rust/src/error.rs index c59efc8203..92a783dc30 100644 --- a/target_chains/cosmwasm/sdk/rust/src/error.rs +++ b/target_chains/cosmwasm/sdk/rust/src/error.rs @@ -52,6 +52,26 @@ pub enum PythContractError { /// The message did not include a sufficient fee. #[error("InvalidFeeDenom")] InvalidFeeDenom { denom: String }, + + /// Message starts with accumulator magic but is not parsable + #[error("InvalidAccumulatorPayload")] + InvalidAccumulatorPayload, + + /// Message type is not supported yet + #[error("InvalidAccumulatorMessageType")] + InvalidAccumulatorMessageType, + + /// Accumulator message can not be parsed + #[error("InvalidAccumulatorMessage")] + InvalidAccumulatorMessage, + + /// Wormhole message inside the accumulator payload can not be parsed + #[error("InvalidWormholeMessage")] + InvalidWormholeMessage, + + /// Merkle proof is invalid + #[error("InvalidMerkleProof")] + InvalidMerkleProof, } impl From for StdError {