[cosmos] Pay fee + mock wormhole for testing (#433)

* added handler

* create binary for vaas

* updating tests

* add fee test

* blah

* simplify

* simplify

* cleanup5

* Add fees to the terra relayer

Co-authored-by: Jayant Krishnamurthy <jkrishnamurthy@jumptrading.com>
Co-authored-by: Ali Behjati <bahjatia@gmail.com>
This commit is contained in:
Jayant Krishnamurthy 2022-12-19 06:44:57 -08:00 committed by GitHub
parent f6ad2d6544
commit 6b29d9704a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 178 additions and 57 deletions

View File

@ -24,8 +24,10 @@ use {
},
cosmwasm_std::{
entry_point,
has_coins,
to_binary,
Binary,
Coin,
Deps,
DepsMut,
Env,
@ -120,13 +122,17 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S
fn update_price_feeds(
mut deps: DepsMut,
env: Env,
_info: MessageInfo,
info: MessageInfo,
data: &Binary,
) -> StdResult<Response> {
let state = config_read(deps.storage).load()?;
let vaa = parse_vaa(deps.branch(), env.block.time.seconds(), data)?;
let fee = Coin::new(state.fee.u128(), state.fee_denom.clone());
if fee.amount.u128() > 0 && !has_coins(info.funds.as_ref(), &fee) {
return Err(PythContractError::InsufficientFee.into());
}
let vaa = parse_vaa(deps.branch(), env.block.time.seconds(), data)?;
verify_vaa_from_data_source(&state, &vaa)?;
let data = &vaa.payload;
@ -139,26 +145,15 @@ fn update_price_feeds(
fn execute_governance_instruction(
mut deps: DepsMut,
env: Env,
info: MessageInfo,
_info: MessageInfo,
data: &Binary,
) -> StdResult<Response> {
let vaa = parse_vaa(deps.branch(), env.block.time.seconds(), data)?;
execute_governance_instruction_from_vaa(deps, env, info, &vaa)
}
/// Helper function to improve testability of governance instructions (so we can unit test without wormhole).
fn execute_governance_instruction_from_vaa(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
vaa: &ParsedVAA,
) -> StdResult<Response> {
let state = config_read(deps.storage).load()?;
// store updates to the config as a result of this action in here.
let mut updated_config: ConfigInfo = state.clone();
verify_vaa_from_governance_source(&state, vaa)?;
verify_vaa_from_governance_source(&state, &vaa)?;
if vaa.sequence <= state.governance_sequence_number {
return Err(PythContractError::OldGovernanceMessage)?;
@ -417,6 +412,8 @@ mod test {
Target,
},
cosmwasm_std::{
coins,
from_binary,
testing::{
mock_dependencies,
mock_env,
@ -426,16 +423,36 @@ mod test {
MockStorage,
},
Addr,
ContractResult,
OwnedDeps,
QuerierResult,
SystemError,
SystemResult,
},
std::time::Duration,
};
/// Default valid time period for testing purposes.
const VALID_TIME_PERIOD: Duration = Duration::from_secs(3 * 60);
const WORMHOLE_ADDR: &str = "Wormhole";
const EMITTER_CHAIN: u16 = 3;
fn default_emitter_addr() -> Vec<u8> {
vec![0, 1, 80]
}
fn default_config_info() -> ConfigInfo {
ConfigInfo {
wormhole_contract: Addr::unchecked(WORMHOLE_ADDR),
data_sources: create_data_sources(default_emitter_addr(), EMITTER_CHAIN),
..create_zero_config_info()
}
}
fn setup_test() -> (OwnedDeps<MockStorage, MockApi, MockQuerier>, Env) {
let mut dependencies = mock_dependencies();
dependencies.querier.update_wasm(handle_wasm_query);
let mut config = config(dependencies.as_mut().storage);
config
.save(&ConfigInfo {
@ -446,6 +463,47 @@ mod test {
(dependencies, mock_env())
}
/// Mock handler for wormhole queries.
/// Warning: the interface for the `VerifyVAA` action is slightly different than the real wormhole contract.
/// In the mock, you pass in a binary-encoded `ParsedVAA`, and that exact vaa will be returned by wormhole.
/// The real contract uses a different binary VAA format (see `ParsedVAA::deserialize`) which includes
/// the guardian signatures.
fn handle_wasm_query(wasm_query: &WasmQuery) -> QuerierResult {
match wasm_query {
WasmQuery::Smart { contract_addr, msg } if *contract_addr == WORMHOLE_ADDR => {
let query_msg = from_binary::<WormholeQueryMsg>(msg);
match query_msg {
Ok(WormholeQueryMsg::VerifyVAA { vaa, .. }) => {
SystemResult::Ok(ContractResult::Ok(vaa))
}
Err(_e) => SystemResult::Err(SystemError::InvalidRequest {
error: "Invalid message".into(),
request: msg.clone(),
}),
_ => SystemResult::Err(SystemError::NoSuchContract {
addr: contract_addr.clone(),
}),
}
}
WasmQuery::Smart { contract_addr, .. } => {
SystemResult::Err(SystemError::NoSuchContract {
addr: contract_addr.clone(),
})
}
WasmQuery::Raw { contract_addr, .. } => {
SystemResult::Err(SystemError::NoSuchContract {
addr: contract_addr.clone(),
})
}
WasmQuery::ContractInfo { contract_addr, .. } => {
SystemResult::Err(SystemError::NoSuchContract {
addr: contract_addr.clone(),
})
}
_ => unreachable!(),
}
}
fn create_zero_vaa() -> ParsedVAA {
ParsedVAA {
version: 0,
@ -462,6 +520,20 @@ mod test {
}
}
fn create_price_update_msg(emitter_address: &[u8], emitter_chain: u16) -> Binary {
let batch_attestation = BatchPriceAttestation {
// TODO: pass these in
price_attestations: vec![],
};
let mut vaa = create_zero_vaa();
vaa.emitter_address = emitter_address.to_vec();
vaa.emitter_chain = emitter_chain;
vaa.payload = batch_attestation.serialize().unwrap();
to_binary(&vaa).unwrap()
}
fn create_zero_config_info() -> ConfigInfo {
ConfigInfo {
owner: Addr::unchecked(String::default()),
@ -512,50 +584,91 @@ mod test {
.unwrap()
}
fn apply_price_update(
config_info: &ConfigInfo,
emitter_address: &[u8],
emitter_chain: u16,
funds: &[Coin],
) -> StdResult<Response> {
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)
}
#[test]
fn test_verify_vaa_sender_ok() {
let config_info = ConfigInfo {
data_sources: create_data_sources(vec![1u8], 3),
..create_zero_config_info()
};
let mut vaa = create_zero_vaa();
vaa.emitter_address = vec![1u8];
vaa.emitter_chain = 3;
assert_eq!(verify_vaa_from_data_source(&config_info, &vaa), Ok(()));
let result = apply_price_update(
&default_config_info(),
default_emitter_addr().as_slice(),
EMITTER_CHAIN,
&[],
);
assert!(result.is_ok());
}
#[test]
fn test_verify_vaa_sender_fail_wrong_emitter_address() {
let config_info = ConfigInfo {
data_sources: create_data_sources(vec![1u8], 3),
..create_zero_config_info()
};
let mut vaa = create_zero_vaa();
vaa.emitter_address = vec![3u8, 4u8];
vaa.emitter_chain = 3;
assert_eq!(
verify_vaa_from_data_source(&config_info, &vaa),
Err(PythContractError::InvalidUpdateEmitter.into())
let emitter_address = [17, 23, 14];
let result = apply_price_update(
&default_config_info(),
emitter_address.as_slice(),
EMITTER_CHAIN,
&[],
);
assert_eq!(result, Err(PythContractError::InvalidUpdateEmitter.into()));
}
#[test]
fn test_verify_vaa_sender_fail_wrong_emitter_chain() {
let config_info = ConfigInfo {
data_sources: create_data_sources(vec![1u8], 3),
..create_zero_config_info()
};
let mut vaa = create_zero_vaa();
vaa.emitter_address = vec![1u8];
vaa.emitter_chain = 2;
assert_eq!(
verify_vaa_from_data_source(&config_info, &vaa),
Err(PythContractError::InvalidUpdateEmitter.into())
let result = apply_price_update(
&default_config_info(),
default_emitter_addr().as_slice(),
EMITTER_CHAIN + 1,
&[],
);
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 = Uint128::new(100);
config_info.fee_denom = "foo".into();
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]
@ -905,13 +1018,14 @@ mod test {
let info = mock_info("123", &[]);
let result = execute_governance_instruction_from_vaa(deps.as_mut(), env, info, vaa);
let result = execute_governance_instruction(deps.as_mut(), env, info, &to_binary(&vaa)?);
result.and_then(|response| config_read(&deps.storage).load().map(|c| (response, c)))
}
fn governance_test_config() -> ConfigInfo {
ConfigInfo {
wormhole_contract: Addr::unchecked(WORMHOLE_ADDR),
governance_source: PythDataSource {
emitter: Binary(vec![1u8, 2u8]),
pyth_emitter_chain: 3,

View File

@ -40,6 +40,10 @@ pub enum PythContractError {
/// The sequence number of the governance message is too old.
#[error("OldGovernanceMessage")]
OldGovernanceMessage,
/// The message did not include a sufficient fee.
#[error("InsufficientFee")]
InsufficientFee,
}
impl From<PythContractError> for StdError {

View File

@ -37,7 +37,7 @@ for (let idx = 0; idx < process.argv.length; ++idx) {
nodeUrl: helpers.envOrErr("TERRA_NODE_URL"),
terraChainId: helpers.envOrErr("TERRA_CHAIN_ID"),
walletPrivateKey: helpers.envOrErr("TERRA_PRIVATE_KEY"),
coin: helpers.envOrErr("TERRA_COIN"),
coinDenom: helpers.envOrErr("TERRA_COIN"),
contractAddress: helpers.envOrErr("TERRA_PYTH_CONTRACT_ADDRESS"),
});
logger.info("Relaying to Terra");

View File

@ -1,5 +1,6 @@
import { fromUint8Array } from "js-base64";
import {
Coin,
LCDClient,
LCDClientConfig,
MnemonicKey,
@ -17,7 +18,7 @@ export class TerraRelay implements Relay {
readonly nodeUrl: string;
readonly terraChainId: string;
readonly walletPrivateKey: string;
readonly coin: string;
readonly coinDenom: string;
readonly contractAddress: string;
readonly lcdConfig: LCDClientConfig;
@ -25,13 +26,13 @@ export class TerraRelay implements Relay {
nodeUrl: string;
terraChainId: string;
walletPrivateKey: string;
coin: string;
coinDenom: string;
contractAddress: string;
}) {
this.nodeUrl = cfg.nodeUrl;
this.terraChainId = cfg.terraChainId;
this.walletPrivateKey = cfg.walletPrivateKey;
this.coin = cfg.coin;
this.coinDenom = cfg.coinDenom;
this.contractAddress = cfg.contractAddress;
this.lcdConfig = {
@ -44,7 +45,7 @@ export class TerraRelay implements Relay {
"], terraChainId: [" +
this.terraChainId +
"], coin: [" +
this.coin +
this.coinDenom +
"], contractAddress: [" +
this.contractAddress +
"]"
@ -75,7 +76,9 @@ export class TerraRelay implements Relay {
update_price_feeds: {
data: Buffer.from(signedVAAs[idx], "hex").toString("base64"),
},
}
},
// TODO: Query the fee before
[new Coin(this.coinDenom, 1)]
);
msgs.push(msg);
@ -97,7 +100,7 @@ export class TerraRelay implements Relay {
const tx = await wallet.createAndSignTx({
msgs: msgs,
memo: "P2T",
feeDenoms: [this.coin],
feeDenoms: [this.coinDenom],
gasPrices,
});
@ -207,13 +210,13 @@ export class TerraRelay implements Relay {
[coins, pagnation] = await lcdClient.bank.balance(wallet.key.accAddress);
logger.debug("wallet query returned: %o", coins);
if (coins) {
let coin = coins.get(this.coin);
let coin = coins.get(this.coinDenom);
if (coin) {
balance = parseInt(coin.toData().amount);
} else {
logger.error(
"failed to query coin balance, coin [" +
this.coin +
this.coinDenom +
"] is not in the wallet, coins: %o",
coins
);