Implement accumulator updates for cosmwasm (#880)

* Implement accumulator updates for cosmwasm

* Update fee calculation logic for accumulator messages

The fee for accumulator messages is base fee times the number of messages
but the logic remains the same for the batch method
This commit is contained in:
Mohammad Amin Khashkhashi Moghaddam 2023-06-13 12:59:29 +02:00 committed by GitHub
parent 3721dd2c57
commit 211bf02450
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 809 additions and 66 deletions

View File

@ -7,6 +7,9 @@ pub enum Error {
#[error("Invalid Version")]
InvalidVersion,
#[error("Deserialization error")]
DeserializationError,
}
#[macro_export]

View File

@ -57,7 +57,7 @@ pub mod v1 {
major_version: u8,
minor_version: u8,
trailing: Vec<u8>,
proof: Proof,
pub proof: Proof,
}
impl AccumulatorUpdateData {
@ -72,7 +72,8 @@ pub mod v1 {
}
pub fn try_from_slice(bytes: &[u8]) -> Result<Self, Error> {
let message = from_slice::<byteorder::BE, Self>(bytes).unwrap();
let message = from_slice::<byteorder::BE, Self>(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<Self, Error> {
let message = from_slice::<byteorder::BE, Self>(bytes.as_ref()).unwrap();
let message = from_slice::<byteorder::BE, Self>(bytes.as_ref())
.map_err(|_| Error::DeserializationError)?;
require!(
&message.magic[..] == ACCUMULATOR_UPDATE_WORMHOLE_VERIFICATION_MAGIC,
Error::InvalidMagic

View File

@ -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"

View File

@ -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 }

View File

@ -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<PriceAttestation> = vec![];
let mut total_new_feeds: Vec<PriceFeed> = 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 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)?;
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)?;
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(())
}
/// Update the on-chain storage for any new price updates provided in `batch_attestation`.
fn process_batch_attestation<'a>(
fn process_accumulator(
deps: &mut DepsMut,
env: &Env,
batch_attestation: &'a BatchPriceAttestation,
) -> StdResult<(usize, Vec<&'a PriceAttestation>)> {
let mut new_attestations = vec![];
data: &[u8],
) -> StdResult<(usize, Vec<PriceFeed>)> {
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<Keccak160> = 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::<BigEndian, Message>(&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(
deps: &mut DepsMut,
env: &Env,
batch_attestation: &BatchPriceAttestation,
) -> StdResult<(usize, Vec<PriceFeed>)> {
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<PriceFeedRespo
}
}
pub fn get_update_fee_amount(deps: &Deps, vaas: &[Binary]) -> StdResult<u128> {
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<Coin> {
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<Duration> {
@ -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<MerklePriceUpdate>,
tree: MerkleTree<Keccak160>,
corrupt_wormhole_message: bool,
emitter_address: Vec<u8>,
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::<Keccak160>::new(all_feeds_bytes_refs.as_slice()).unwrap();
let mut price_updates: Vec<MerklePriceUpdate> = 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<MockStorage, MockApi, MockQuerier>, 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<u8>, 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::<Keccak160>::new(&[feed1_bytes.as_slice()]).unwrap();
let mut price_updates: Vec<MerklePriceUpdate> = 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<Message> = 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::<Keccak160>::new(&[feed1_bytes.as_slice()]).unwrap();
let mut price_updates: Vec<MerklePriceUpdate> = 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::<Keccak160>::new(&[feed1_bytes.as_slice()]).unwrap();
let mut price_updates: Vec<MerklePriceUpdate> = 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 {

View File

@ -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<PriceAttestation>,
price_feeds: Vec<PriceFeed>,
) -> CosmosMsg<InjectiveMsgWrapper> {
InjectiveMsgWrapper {
route: "oracle".to_string(),
msg_data: InjectiveMsg::RelayPythPrices {
sender,
price_attestations: price_attestations
price_attestations: price_feeds
.iter()
.map(InjectivePriceAttestation::from)
.collect(),

View File

@ -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<PythContractError> for StdError {