瀏覽代碼

[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>
Jayant Krishnamurthy 2 年之前
父節點
當前提交
6b29d9704a

+ 161 - 47
cosmwasm/contracts/pyth/src/contract.rs

@@ -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()
     }
 
-    #[test]
-    fn test_verify_vaa_sender_ok() {
-        let config_info = ConfigInfo {
-            data_sources: create_data_sources(vec![1u8], 3),
-            ..create_zero_config_info()
-        };
+    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 mut vaa = create_zero_vaa();
-        vaa.emitter_address = vec![1u8];
-        vaa.emitter_chain = 3;
+        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)
+    }
 
-        assert_eq!(verify_vaa_from_data_source(&config_info, &vaa), Ok(()));
+    #[test]
+    fn test_verify_vaa_sender_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 result = apply_price_update(
+            &default_config_info(),
+            default_emitter_addr().as_slice(),
+            EMITTER_CHAIN + 1,
+            &[],
+        );
+        assert_eq!(result, Err(PythContractError::InvalidUpdateEmitter.into()));
+    }
 
-        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())
+    #[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,

+ 4 - 0
cosmwasm/contracts/pyth/src/error.rs

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

+ 1 - 1
third_party/pyth/p2w-relay/src/index.ts

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

+ 11 - 8
third_party/pyth/p2w-relay/src/relay/terra.ts

@@ -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
           );