Browse Source

feat(target_chains/starknet): add remaining pyth contract getters (#1631)

* feat(target_chains/starknet): add price_feed_exists()

* feat(target_chains/starknet): add remaining pyth contract getters

* refactor(target_chains/starknet): rename fee_contract_address to fee_token_address
Pavel Strakhov 1 year ago
parent
commit
07306eeb1a

+ 3 - 3
target_chains/starknet/contracts/deploy/local_deploy

@@ -20,7 +20,7 @@ scarb build
 wormhole_hash=$(starkli declare target/dev/pyth_wormhole.contract_class.json)
 
 # predeployed fee token contract in katana
-fee_contract_address=0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7
+fee_token_address=0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7
 
 # deploying wormhole with mainnet guardians
 
@@ -63,7 +63,7 @@ pyth_hash=$(starkli declare target/dev/pyth_pyth.contract_class.json)
 ${sleep}
 pyth_address=$(starkli deploy "${pyth_hash}" \
     "${wormhole_address}" \
-    "${fee_contract_address}" \
+    "${fee_token_address}" \
     1000 0 `# fee amount` \
     1 `# num_data_sources` \
     26 `# emitter_chain_id` \
@@ -74,7 +74,7 @@ pyth_address=$(starkli deploy "${pyth_hash}" \
 )
 
 ${sleep}
-starkli invoke "${fee_contract_address}" approve "${pyth_address}" 1000 0
+starkli invoke "${fee_token_address}" approve "${pyth_address}" 1000 0
 
 ${sleep}
 starkli invoke --watch --log-traffic "${pyth_address}" update_price_feeds \

+ 65 - 7
target_chains/starknet/contracts/src/pyth.cairo

@@ -1,7 +1,7 @@
 mod errors;
 mod interface;
 mod price_update;
-mod governance;
+pub mod governance;
 
 mod fake_upgrades;
 
@@ -97,7 +97,7 @@ mod pyth {
     #[storage]
     struct Storage {
         wormhole_address: ContractAddress,
-        fee_contract_address: ContractAddress,
+        fee_token_address: ContractAddress,
         single_update_fee: u256,
         data_sources: LegacyMap<usize, DataSource>,
         num_data_sources: usize,
@@ -115,20 +115,20 @@ mod pyth {
     ///
     /// `wormhole_address` is the address of the deployed Wormhole contract implemented in the `wormhole` module.
     ///
-    /// `fee_contract_address` is the address of the ERC20 token used to pay fees to Pyth
+    /// `fee_token_address` is the address of the ERC20 token used to pay fees to Pyth
     /// for price updates. There is no native token on Starknet so an ERC20 contract has to be used.
     /// On Katana, an ETH fee contract is pre-deployed. On Starknet testnet, ETH and STRK fee tokens are
     /// available. Any other ERC20-compatible token can also be used.
     /// In a Starknet Forge testing environment, a fee contract must be deployed manually.
     ///
-    /// `single_update_fee` is the number of tokens of `fee_contract_address` charged for a single price update.
+    /// `single_update_fee` is the number of tokens of `fee_token_address` charged for a single price update.
     ///
     /// `data_sources` is the list of Wormhole data sources accepted by this contract.
     #[constructor]
     fn constructor(
         ref self: ContractState,
         wormhole_address: ContractAddress,
-        fee_contract_address: ContractAddress,
+        fee_token_address: ContractAddress,
         single_update_fee: u256,
         data_sources: Array<DataSource>,
         governance_emitter_chain_id: u16,
@@ -136,7 +136,7 @@ mod pyth {
         governance_initial_sequence: u64,
     ) {
         self.wormhole_address.write(wormhole_address);
-        self.fee_contract_address.write(fee_contract_address);
+        self.fee_token_address.write(fee_token_address);
         self.single_update_fee.write(single_update_fee);
         self.write_data_sources(@data_sources);
         self
@@ -239,6 +239,16 @@ mod pyth {
             Result::Ok(feed)
         }
 
+        fn price_feed_exists(self: @ContractState, price_id: u256) -> bool {
+            let info = self.latest_price_info.read(price_id);
+            info.publish_time != 0
+        }
+
+        fn latest_price_info_publish_time(self: @ContractState, price_id: u256) -> u64 {
+            let info = self.latest_price_info.read(price_id);
+            info.publish_time
+        }
+
         fn update_price_feeds(ref self: ContractState, data: ByteArray) {
             self.update_price_feeds_internal(data, array![], 0, 0, false);
         }
@@ -300,6 +310,54 @@ mod pyth {
                 )
         }
 
+        fn wormhole_address(self: @ContractState) -> ContractAddress {
+            self.wormhole_address.read()
+        }
+
+        fn fee_token_address(self: @ContractState) -> ContractAddress {
+            self.fee_token_address.read()
+        }
+
+        fn get_single_update_fee(self: @ContractState) -> u256 {
+            self.single_update_fee.read()
+        }
+
+        fn valid_data_sources(self: @ContractState) -> Array<DataSource> {
+            let count = self.num_data_sources.read();
+            let mut i = 0;
+            let mut output = array![];
+            while i < count {
+                output.append(self.data_sources.read(i));
+                i += 1;
+            };
+            output
+        }
+
+        fn is_valid_data_source(self: @ContractState, source: DataSource) -> bool {
+            self.is_valid_data_source.read(source)
+        }
+
+        fn governance_data_source(self: @ContractState) -> DataSource {
+            self.governance_data_source.read()
+        }
+
+        fn is_valid_governance_data_source(self: @ContractState, source: DataSource) -> bool {
+            self.governance_data_source.read() == source
+        }
+
+        fn last_executed_governance_sequence(self: @ContractState) -> u64 {
+            self.last_executed_governance_sequence.read()
+        }
+
+        fn governance_data_source_index(self: @ContractState) -> u32 {
+            self.governance_data_source_index.read()
+        }
+
+        fn chain_id(self: @ContractState) -> u16 {
+            let wormhole = IWormholeDispatcher { contract_address: self.wormhole_address.read() };
+            wormhole.chain_id()
+        }
+
         fn execute_governance_instruction(ref self: ContractState, data: ByteArray) {
             let wormhole = IWormholeDispatcher { contract_address: self.wormhole_address.read() };
             let vm = wormhole.parse_and_verify_vm(data.clone());
@@ -555,7 +613,7 @@ mod pyth {
             let num_updates = reader.read_u8();
             let total_fee = self.get_total_fee(num_updates);
             let fee_contract = IERC20CamelDispatcher {
-                contract_address: self.fee_contract_address.read()
+                contract_address: self.fee_token_address.read()
             };
             let execution_info = get_execution_info().unbox();
             let caller = execution_info.caller_address;

+ 15 - 0
target_chains/starknet/contracts/src/pyth/interface.cairo

@@ -1,5 +1,6 @@
 use super::{GetPriceUnsafeError, GetPriceNoOlderThanError};
 use pyth::byte_array::ByteArray;
+use core::starknet::ContractAddress;
 
 #[starknet::interface]
 pub trait IPyth<T> {
@@ -15,6 +16,9 @@ pub trait IPyth<T> {
         self: @T, price_id: u256, age: u64
     ) -> Result<PriceFeed, GetPriceNoOlderThanError>;
     fn query_price_feed_unsafe(self: @T, price_id: u256) -> Result<PriceFeed, GetPriceUnsafeError>;
+    fn price_feed_exists(self: @T, price_id: u256) -> bool;
+    fn latest_price_info_publish_time(self: @T, price_id: u256) -> u64;
+
     fn update_price_feeds(ref self: T, data: ByteArray);
     fn update_price_feeds_if_necessary(
         ref self: T, update: ByteArray, required_publish_times: Array<PriceFeedPublishTime>
@@ -30,6 +34,17 @@ pub trait IPyth<T> {
         ref self: T, data: ByteArray, price_ids: Array<u256>, publish_time: u64, max_staleness: u64,
     ) -> Array<PriceFeed>;
     fn get_update_fee(self: @T, data: ByteArray) -> u256;
+    fn wormhole_address(self: @T) -> ContractAddress;
+    fn fee_token_address(self: @T) -> ContractAddress;
+    fn get_single_update_fee(self: @T) -> u256;
+    fn valid_data_sources(self: @T) -> Array<DataSource>;
+    fn is_valid_data_source(self: @T, source: DataSource) -> bool;
+    fn governance_data_source(self: @T) -> DataSource;
+    fn is_valid_governance_data_source(self: @T, source: DataSource) -> bool;
+    fn last_executed_governance_sequence(self: @T) -> u64;
+    fn governance_data_source_index(self: @T) -> u32;
+    fn chain_id(self: @T) -> u16;
+
     fn execute_governance_instruction(ref self: T, data: ByteArray);
     fn version(self: @T) -> felt252;
     fn pyth_upgradable_magic(self: @T) -> u32;

+ 84 - 4
target_chains/starknet/contracts/tests/pyth.cairo

@@ -83,6 +83,66 @@ fn decode_event(mut event: Event) -> PythEvent {
     output
 }
 
+#[test]
+fn test_getters_work() {
+    let user = 'user'.try_into().unwrap();
+    let wormhole = super::wormhole::deploy_with_mainnet_guardians();
+    let fee_contract = deploy_fee_contract(user);
+    let pyth = deploy_default(wormhole.contract_address, fee_contract.contract_address);
+
+    assert!(pyth.wormhole_address() == wormhole.contract_address);
+    assert!(pyth.fee_token_address() == fee_contract.contract_address);
+    assert!(pyth.get_single_update_fee() == 1000);
+    assert!(
+        pyth
+            .valid_data_sources() == array![
+                DataSource {
+                    emitter_chain_id: 26,
+                    emitter_address: 0xe101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71,
+                }
+            ]
+    );
+    assert!(
+        pyth
+            .is_valid_data_source(
+                DataSource {
+                    emitter_chain_id: 26,
+                    emitter_address: 0xe101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71,
+                }
+            )
+    );
+    assert!(
+        !pyth.is_valid_data_source(DataSource { emitter_chain_id: 26, emitter_address: 0xbad, })
+    );
+    assert!(
+        !pyth
+            .is_valid_data_source(
+                DataSource {
+                    emitter_chain_id: 27,
+                    emitter_address: 0xe101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71,
+                }
+            )
+    );
+    assert!(
+        pyth.governance_data_source() == DataSource { emitter_chain_id: 1, emitter_address: 41, }
+    );
+    assert!(
+        pyth
+            .is_valid_governance_data_source(
+                DataSource { emitter_chain_id: 1, emitter_address: 41, }
+            )
+    );
+    assert!(
+        !pyth
+            .is_valid_governance_data_source(
+                DataSource { emitter_chain_id: 1, emitter_address: 42, }
+            )
+    );
+    assert!(pyth.last_executed_governance_sequence() == 0);
+    assert!(pyth.governance_data_source_index() == 0);
+    assert!(pyth.chain_id() == 60051);
+}
+
 #[test]
 fn update_price_feeds_works() {
     let user = 'user'.try_into().unwrap();
@@ -90,6 +150,16 @@ fn update_price_feeds_works() {
     let fee_contract = deploy_fee_contract(user);
     let pyth = deploy_default(wormhole.contract_address, fee_contract.contract_address);
 
+    assert!(
+        !pyth.price_feed_exists(0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43)
+    );
+    assert!(
+        pyth
+            .latest_price_info_publish_time(
+                0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43
+            ) == 0
+    );
+
     let fee = pyth.get_update_fee(data::good_update1());
     assert!(fee == 1000);
 
@@ -144,6 +214,16 @@ fn update_price_feeds_works() {
     assert!(feed.ema_price.conf == 4096812700);
     assert!(feed.ema_price.expo == -8);
     assert!(feed.ema_price.publish_time == 1712589206);
+
+    assert!(
+        pyth.price_feed_exists(0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43)
+    );
+    assert!(
+        pyth
+            .latest_price_info_publish_time(
+                0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43
+            ) == 1712589206
+    );
 }
 
 #[test]
@@ -867,11 +947,11 @@ fn test_upgrade_rejects_wrong_magic() {
 
 
 fn deploy_default(
-    wormhole_address: ContractAddress, fee_contract_address: ContractAddress
+    wormhole_address: ContractAddress, fee_token_address: ContractAddress
 ) -> IPythDispatcher {
     deploy(
         wormhole_address,
-        fee_contract_address,
+        fee_token_address,
         1000,
         array![
             DataSource {
@@ -887,7 +967,7 @@ fn deploy_default(
 
 fn deploy(
     wormhole_address: ContractAddress,
-    fee_contract_address: ContractAddress,
+    fee_token_address: ContractAddress,
     single_update_fee: u256,
     data_sources: Array<DataSource>,
     governance_emitter_chain_id: u16,
@@ -895,7 +975,7 @@ fn deploy(
     governance_initial_sequence: u64,
 ) -> IPythDispatcher {
     let mut args = array![];
-    (wormhole_address, fee_contract_address, single_update_fee).serialize(ref args);
+    (wormhole_address, fee_token_address, single_update_fee).serialize(ref args);
     (data_sources, governance_emitter_chain_id).serialize(ref args);
     (governance_emitter_address, governance_initial_sequence).serialize(ref args);
     let contract = declare("pyth");