Cosmwasm refactor and preparation for parse price feeds (#895)

* Add more tests for update_fee_amount

Restructure code to split parsing and updating logic so that all
messages can be parsed without being applied

* Fix docker build command for cosmwasm contract

* Refactor is_fee_sufficient tests

Extract out the chain specific part into separate function
Merge test_accumulator_is_fee_sufficient back into is_fee_sufficient

* Rename function and simplify process_batch_attestation tests
This commit is contained in:
Mohammad Amin Khashkhashi Moghaddam 2023-06-22 09:49:05 +01:00 committed by GitHub
parent 339b2f6ce0
commit ef963cdfc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 215 additions and 184 deletions

View File

@ -22,8 +22,8 @@ First, build the contracts within [the current directory](./). You must have Doc
cd ./tools
npm ci
# if you want to build specifically for injective
npm run build-contract -- --injective
# if you want to build specifically for one chain
npm run build-contract -- --[injective|osmosis]
# else a generic cosmwasm contract can be build using
npm run build-contract -- --cosmwasm

View File

@ -60,6 +60,10 @@ use {
WasmMsg,
WasmQuery,
},
pyth_sdk::{
Identifier,
UnixTimestamp,
},
pyth_sdk_cw::{
error::PythContractError,
ExecuteMsg,
@ -146,7 +150,7 @@ pub fn instantiate(
/// *Warning* this function does not verify the emitter of the wormhole message; it only checks
/// that the wormhole signatures are valid. The caller is responsible for checking that the message
/// originates from the expected emitter.
pub fn parse_and_verify_vaa(deps: DepsMut, block_time: u64, data: &Binary) -> StdResult<ParsedVAA> {
pub fn parse_and_verify_vaa(deps: Deps, block_time: u64, data: &Binary) -> StdResult<ParsedVAA> {
let cfg = config_read(deps.storage).load()?;
let vaa: ParsedVAA = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: cfg.wormhole_contract.to_string(),
@ -256,35 +260,11 @@ fn update_price_feeds(
info: MessageInfo,
data: &[Binary],
) -> StdResult<Response<MsgWrapper>> {
let state = config_read(deps.storage).load()?;
if !is_fee_sufficient(&deps.as_ref(), info, data)? {
return Err(PythContractError::InsufficientFee)?;
}
let mut num_total_attestations: usize = 0;
let mut total_new_feeds: Vec<PriceFeed> = vec![];
for datum in data {
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_feed in new_feeds {
total_new_feeds.push(new_feed.to_owned());
}
}
let (num_total_attestations, total_new_feeds) = apply_updates(&mut deps, &env, data)?;
let num_total_new_attestations = total_new_feeds.len();
@ -313,12 +293,12 @@ fn update_price_feeds(
/// The VAA must come from an authorized governance emitter.
/// See [GovernanceInstruction] for descriptions of the supported operations.
fn execute_governance_instruction(
mut deps: DepsMut,
deps: DepsMut,
env: Env,
_info: MessageInfo,
data: &Binary,
) -> StdResult<Response<MsgWrapper>> {
let vaa = parse_and_verify_vaa(deps.branch(), env.block.time.seconds(), data)?;
let vaa = parse_and_verify_vaa(deps.as_ref(), env.block.time.seconds(), data)?;
let state = config_read(deps.storage).load()?;
verify_vaa_from_governance_source(&state, &vaa)?;
@ -358,7 +338,7 @@ fn execute_governance_instruction(
}
AuthorizeGovernanceDataSourceTransfer { claim_vaa } => {
let parsed_claim_vaa =
parse_and_verify_vaa(deps.branch(), env.block.time.seconds(), &claim_vaa)?;
parse_and_verify_vaa(deps.as_ref(), env.block.time.seconds(), &claim_vaa)?;
transfer_governance(&mut updated_config, &state, &parsed_claim_vaa)?
}
SetDataSources { data_sources } => {
@ -500,17 +480,43 @@ fn verify_vaa_from_governance_source(state: &ConfigInfo, vaa: &ParsedVAA) -> Std
Ok(())
}
fn process_accumulator(
fn parse_update(deps: &Deps, env: &Env, data: &Binary) -> StdResult<Vec<PriceFeed>> {
let header = data.get(0..4);
let feeds = if header == Some(PYTHNET_ACCUMULATOR_UPDATE_MAGIC.as_slice()) {
parse_accumulator(deps, env, data)?
} else {
parse_batch_attestation(deps, env, data)?
};
Ok(feeds)
}
fn apply_updates(
deps: &mut DepsMut,
env: &Env,
data: &[u8],
data: &[Binary],
) -> StdResult<(usize, Vec<PriceFeed>)> {
let mut num_total_attestations: usize = 0;
let mut total_new_feeds: Vec<PriceFeed> = vec![];
for datum in data {
let feeds = parse_update(&deps.as_ref(), env, datum)?;
num_total_attestations += feeds.len();
for feed in feeds {
if update_price_feed_if_new(deps, env, feed)? {
total_new_feeds.push(feed);
}
}
}
Ok((num_total_attestations, total_new_feeds))
}
fn parse_accumulator(deps: &Deps, env: &Env, data: &[u8]) -> StdResult<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(),
*deps,
env.block.time.seconds(),
&Binary::from(Vec::from(vaa)),
)?;
@ -523,8 +529,7 @@ fn process_accumulator(
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![];
let mut feeds = vec![];
for update in updates {
let message_vec = Vec::from(update.message);
if !root.check(update.proof, &message_vec) {
@ -551,37 +556,33 @@ fn process_accumulator(
publish_time: price_feed_message.publish_time,
},
);
if update_price_feed_if_new(deps, env, price_feed)? {
new_feeds.push(price_feed);
}
feeds.push(price_feed);
}
_ => return Err(PythContractError::InvalidAccumulatorMessageType)?,
}
}
Ok((update_len, new_feeds))
Ok(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![];
fn parse_batch_attestation(deps: &Deps, env: &Env, data: &Binary) -> StdResult<Vec<PriceFeed>> {
let vaa = parse_and_verify_vaa(*deps, env.block.time.seconds(), data)?;
let state = config_read(deps.storage).load()?;
verify_vaa_from_data_source(&state, &vaa)?;
let data = &vaa.payload;
let batch_attestation = BatchPriceAttestation::deserialize(&data[..])
.map_err(|_| PythContractError::InvalidUpdatePayload)?;
let mut 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_feeds.push(price_feed);
}
feeds.push(price_feed);
}
Ok((batch_attestation.price_attestations.len(), new_feeds))
Ok(feeds)
}
fn create_price_feed_from_price_attestation(price_attestation: &PriceAttestation) -> PriceFeed {
@ -667,6 +668,58 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
}
}
/// This function is not used in the contract yet but mimicks the behavior implemented
/// in the EVM contract. We are yet to finalize how the parsed prices should be consumed
/// in injective as well as other chains.
pub fn parse_price_feed_updates(
deps: DepsMut,
env: Env,
info: MessageInfo,
updates: &[Binary],
price_feeds: Vec<Identifier>,
min_publish_time: UnixTimestamp,
max_publish_time: UnixTimestamp,
) -> StdResult<Response<MsgWrapper>> {
let _config = config_read(deps.storage).load()?;
if !is_fee_sufficient(&deps.as_ref(), info, updates)? {
return Err(PythContractError::InsufficientFee)?;
}
let mut found_feeds = 0;
let mut results: Vec<(Identifier, Option<PriceFeed>)> =
price_feeds.iter().map(|id| (*id, None)).collect();
for datum in updates {
let feeds = parse_update(&deps.as_ref(), &env, datum)?;
for result in results.as_mut_slice() {
if result.1.is_some() {
continue;
}
for feed in feeds.as_slice() {
if feed.get_price_unchecked().publish_time < min_publish_time
|| feed.get_price_unchecked().publish_time > max_publish_time
{
continue;
}
if result.0 == feed.id {
result.1 = Some(*feed);
found_feeds += 1;
break;
}
}
}
}
if found_feeds != price_feeds.len() {
return Err(PythContractError::InvalidUpdatePayload)?;
}
let _unwrapped_feeds = results
.into_iter()
.map(|(_, feed)| feed.unwrap())
.collect::<Vec<PriceFeed>>();
let response = Response::new();
Ok(response.add_attribute("action", "parse_price_feeds"))
}
/// Get the most recent value of the price feed indicated by `feed_id`.
pub fn query_price_feed(deps: &Deps, feed_id: &[u8]) -> StdResult<PriceFeedResponse> {
match price_feed_read_bucket(deps.storage).load(feed_id) {
@ -882,9 +935,13 @@ mod test {
}
}
fn create_price_update_msg(emitter_address: &[u8], emitter_chain: u16) -> Binary {
fn create_batch_price_update_msg(
emitter_address: &[u8],
emitter_chain: u16,
attestations: Vec<PriceAttestation>,
) -> Binary {
let batch_attestation = BatchPriceAttestation {
price_attestations: vec![PriceAttestation::default()],
price_attestations: attestations,
};
let mut vaa = create_zero_vaa();
@ -895,6 +952,17 @@ mod test {
to_binary(&vaa).unwrap()
}
fn create_batch_price_update_msg_from_attestations(
attestations: Vec<PriceAttestation>,
) -> Binary {
create_batch_price_update_msg(
default_emitter_addr().as_slice(),
EMITTER_CHAIN,
attestations,
)
}
fn create_zero_config_info() -> ConfigInfo {
ConfigInfo {
wormhole_contract: Addr::unchecked(String::default()),
@ -946,14 +1014,12 @@ mod test {
config_info: &ConfigInfo,
emitter_address: &[u8],
emitter_chain: u16,
funds: &[Coin],
) -> StdResult<Response<MsgWrapper>> {
attestations: Vec<PriceAttestation>,
) -> StdResult<(usize, Vec<PriceFeed>)> {
let (mut deps, env) = setup_test();
config(&mut deps.storage).save(config_info).unwrap();
let info = mock_info("123", funds);
let msg = create_price_update_msg(emitter_address, emitter_chain);
update_price_feeds(deps.as_mut(), env, info, &[msg])
let msg = create_batch_price_update_msg(emitter_address, emitter_chain, attestations);
apply_updates(&mut deps.as_mut(), &env, &[msg])
}
#[test]
@ -1025,71 +1091,32 @@ 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()]);
fn check_sufficient_fee(deps: &Deps, data: &[Binary]) {
let mut info = mock_info("123", coins(100, "foo").as_slice());
let result = is_fee_sufficient(&deps, info.clone(), &data);
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()]);
info.funds = coins(50, "foo");
let result = is_fee_sufficient(&deps, info.clone(), &data);
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()]);
info.funds = coins(100, "uion");
let result = is_fee_sufficient(&deps, info.clone(), &data);
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()]);
info.funds = coins(50, "uion");
let result = is_fee_sufficient(&deps, info.clone(), &data);
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()]);
info.funds = coins(100, "invalid_denom");
let result = is_fee_sufficient(&deps, info, &data);
assert_eq!(
result,
Err(PythContractError::InvalidFeeDenom {
@ -1099,14 +1126,57 @@ mod test {
);
}
#[cfg(not(feature = "osmosis"))]
fn check_sufficient_fee(deps: &Deps, data: &[Binary]) {
let mut info = mock_info("123", coins(100, "foo").as_slice());
// sufficient fee -> true
let result = is_fee_sufficient(deps, info.clone(), data);
assert_eq!(result, Ok(true));
// insufficient fee -> false
info.funds = coins(50, "foo");
let result = is_fee_sufficient(deps, info.clone(), data);
assert_eq!(result, Ok(false));
// insufficient fee -> false
info.funds = coins(150, "bar");
let result = is_fee_sufficient(deps, info, data);
assert_eq!(result, Ok(false));
}
#[test]
fn test_process_batch_attestation_empty_array() {
let (mut deps, env) = setup_test();
let attestations = BatchPriceAttestation {
price_attestations: vec![],
};
let (num_attestations, new_attestations) =
process_batch_attestation(&mut deps.as_mut(), &env, &attestations).unwrap();
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 data = [create_batch_price_update_msg_from_attestations(vec![
PriceAttestation::default(),
])];
check_sufficient_fee(&deps.as_ref(), &data);
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 data = [create_accumulator_message(
&[feed1, feed2, feed3],
&[feed1],
false,
)];
check_sufficient_fee(&deps.as_ref(), &data)
}
#[test]
fn test_parse_batch_attestation_empty_array() {
let (num_attestations, new_attestations) = apply_price_update(
&default_config_info(),
default_emitter_addr().as_slice(),
EMITTER_CHAIN,
vec![],
)
.unwrap();
assert_eq!(num_attestations, 0);
assert_eq!(new_attestations.len(), 0);
@ -1252,36 +1322,6 @@ mod test {
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();
@ -1307,6 +1347,14 @@ mod test {
false,
);
assert_eq!(get_update_fee_amount(&deps.as_ref(), &[msg]).unwrap(), 500);
let batch_msg =
create_batch_price_update_msg_from_attestations(vec![PriceAttestation::default()]);
let msg = create_accumulator_message(&[feed1, feed2, feed3], &[feed1, feed2, feed3], false);
assert_eq!(
get_update_fee_amount(&deps.as_ref(), &[msg, batch_msg]).unwrap(),
400
);
}
@ -1643,11 +1691,8 @@ mod test {
assert_eq!(ema_price.publish_time, 99);
}
// this is testing the function process_batch_attestation
// process_batch_attestation is calling update_price_feed_if_new
// changes to update_price_feed_if_new might cause this test
#[test]
fn test_process_batch_attestation_status_not_trading() {
fn test_parse_batch_attestation_status_not_trading() {
let (mut deps, env) = setup_test();
let price_attestation = PriceAttestation {
@ -1666,20 +1711,14 @@ mod test {
..Default::default()
};
let attestations = BatchPriceAttestation {
price_attestations: vec![price_attestation],
};
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])
config(&mut deps.storage)
.save(&default_config_info())
.unwrap();
let price = stored_price_feed.get_price_unchecked();
let ema_price = stored_price_feed.get_ema_price_unchecked();
assert_eq!(num_attestations, 1);
assert_eq!(new_attestations.len(), 1);
let msg = create_batch_price_update_msg_from_attestations(vec![price_attestation]);
let feeds = parse_batch_attestation(&deps.as_ref(), &env, &msg).unwrap();
assert_eq!(feeds.len(), 1);
let price = feeds[0].get_price_unchecked();
let ema_price = feeds[0].get_ema_price_unchecked();
// for price
assert_eq!(price.price, 99);
@ -1694,11 +1733,8 @@ mod test {
assert_eq!(ema_price.publish_time, 99);
}
// this is testing the function process_batch_attestation
// process_batch_attestation is calling update_price_feed_if_new
// changes to update_price_feed_if_new might affect this test
#[test]
fn test_process_batch_attestation_status_trading() {
fn test_parse_batch_attestation_status_trading() {
let (mut deps, env) = setup_test();
let price_attestation = PriceAttestation {
@ -1717,20 +1753,14 @@ mod test {
..Default::default()
};
let attestations = BatchPriceAttestation {
price_attestations: vec![price_attestation],
};
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])
config(&mut deps.storage)
.save(&default_config_info())
.unwrap();
let price = stored_price_feed.get_price_unchecked();
let ema_price = stored_price_feed.get_ema_price_unchecked();
assert_eq!(num_attestations, 1);
assert_eq!(new_attestations.len(), 1);
let msg = create_batch_price_update_msg_from_attestations(vec![price_attestation]);
let feeds = parse_batch_attestation(&deps.as_ref(), &env, &msg).unwrap();
assert_eq!(feeds.len(), 1);
let price = feeds[0].get_price_unchecked();
let ema_price = feeds[0].get_ema_price_unchecked();
// for price
assert_eq!(price.price, 100);
@ -1751,7 +1781,7 @@ mod test {
&default_config_info(),
default_emitter_addr().as_slice(),
EMITTER_CHAIN,
&[],
vec![PriceAttestation::default()],
);
assert!(result.is_ok());
}
@ -1763,7 +1793,7 @@ mod test {
&default_config_info(),
emitter_address.as_slice(),
EMITTER_CHAIN,
&[],
vec![PriceAttestation::default()],
);
assert_eq!(result, Err(PythContractError::InvalidUpdateEmitter.into()));
}
@ -1774,7 +1804,7 @@ mod test {
&default_config_info(),
default_emitter_addr().as_slice(),
EMITTER_CHAIN + 1,
&[],
vec![PriceAttestation::default()],
);
assert_eq!(result, Err(PythContractError::InvalidUpdateEmitter.into()));
}

View File

@ -80,6 +80,7 @@ function build() {
const buildCommand = `
docker run --rm -v "$(cd ..; pwd)":/code \
-v "$(cd ../../../pythnet; pwd)":/pythnet \
-v "$(cd ../../../wormhole_attester; pwd)":/wormhole_attester \
--mount type=volume,source="$(basename "$(cd ..; pwd)")_cache",target=/code/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \