فهرست منبع

feat(target_chains/starknet): fee collection (#1527)

* feat(target_chains/starknet): fee collection

* refactor(target_chains/starknet): renames and comments
Pavel Strakhov 1 سال پیش
والد
کامیت
4e630edac0

+ 6 - 0
target_chains/starknet/contracts/Scarb.lock

@@ -1,10 +1,16 @@
 # Code generated by scarb DO NOT EDIT.
 version = 1
 
+[[package]]
+name = "openzeppelin"
+version = "0.10.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.10.0#d77082732daab2690ba50742ea41080eb23299d3"
+
 [[package]]
 name = "pyth"
 version = "0.1.0"
 dependencies = [
+ "openzeppelin",
  "snforge_std",
 ]
 

+ 2 - 0
target_chains/starknet/contracts/Scarb.toml

@@ -5,6 +5,8 @@ edition = "2023_11"
 
 [dependencies]
 starknet = ">=2.5.4"
+openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.10.0" }
 snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.21.0" }
 
 [[target.starknet-contract]]
+build-external-contracts = ["openzeppelin::presets::erc20::ERC20"]

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 7 - 3
target_chains/starknet/contracts/deploy/local_deploy


+ 37 - 0
target_chains/starknet/contracts/src/pyth.cairo

@@ -66,6 +66,7 @@ pub enum UpdatePriceFeedsError {
     Wormhole: super::wormhole::ParseAndVerifyVmError,
     InvalidUpdateData,
     InvalidUpdateDataSource,
+    InsufficientFeeAllowance,
 }
 
 pub impl UpdatePriceFeedsErrorUnwrapWithFelt252<T> of UnwrapWithFelt252<T, UpdatePriceFeedsError> {
@@ -84,6 +85,7 @@ impl UpdatePriceFeedsErrorIntoFelt252 of Into<UpdatePriceFeedsError, felt252> {
             UpdatePriceFeedsError::Wormhole(err) => err.into(),
             UpdatePriceFeedsError::InvalidUpdateData => 'invalid update data',
             UpdatePriceFeedsError::InvalidUpdateDataSource => 'invalid update data source',
+            UpdatePriceFeedsError::InsufficientFeeAllowance => 'insufficient fee allowance',
         }
     }
 }
@@ -128,6 +130,7 @@ mod pyth {
     use pyth::hash::{Hasher, HasherImpl};
     use core::fmt::{Debug, Formatter};
     use pyth::util::{u64_as_i64, u32_as_i32};
+    use openzeppelin::token::erc20::interface::{IERC20CamelDispatcherTrait, IERC20CamelDispatcher};
 
     // Stands for PNAU (Pyth Network Accumulator Update)
     const ACCUMULATOR_MAGIC: u32 = 0x504e4155;
@@ -232,6 +235,22 @@ mod pyth {
         latest_price_info: LegacyMap<u256, PriceInfo>,
     }
 
+    /// Initializes the Pyth contract.
+    ///
+    /// `owner` is the address that will be allowed to call governance methods (it's a placeholder
+    /// until we implement governance properly).
+    ///
+    /// `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
+    /// 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.
+    ///
+    /// `data_sources` is the list of Wormhole data sources accepted by this contract.
     #[constructor]
     fn constructor(
         ref self: ContractState,
@@ -392,6 +411,20 @@ mod pyth {
 
             let num_updates = reader.read_u8().map_err()?;
 
+            let total_fee = get_total_fee(ref self, num_updates);
+            let fee_contract = IERC20CamelDispatcher {
+                contract_address: self.fee_contract_address.read()
+            };
+            let execution_info = get_execution_info().unbox();
+            let caller = execution_info.caller_address;
+            let contract = execution_info.contract_address;
+            if fee_contract.allowance(caller, contract) < total_fee {
+                return Result::Err(UpdatePriceFeedsError::InsufficientFeeAllowance);
+            }
+            if !fee_contract.transferFrom(caller, contract, total_fee) {
+                return Result::Err(UpdatePriceFeedsError::InsufficientFeeAllowance);
+            }
+
             let mut i = 0;
             let mut result = Result::Ok(());
             while i < num_updates {
@@ -468,4 +501,8 @@ mod pyth {
             self.emit(event);
         }
     }
+
+    fn get_total_fee(ref self: ContractState, num_updates: u8) -> u256 {
+        self.single_update_fee.read() * num_updates.into()
+    }
 }

+ 26 - 1
target_chains/starknet/contracts/tests/pyth.cairo

@@ -8,6 +8,7 @@ use pyth::pyth::{
 use pyth::byte_array::{ByteArray, ByteArrayImpl};
 use pyth::util::{array_felt252_to_bytes31, UnwrapWithFelt252};
 use core::starknet::ContractAddress;
+use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait};
 
 fn decode_event(event: @Event) -> PythEvent {
     if *event.keys.at(0) == event_name_hash('PriceFeedUpdate') {
@@ -31,11 +32,13 @@ fn decode_event(event: @Event) -> PythEvent {
 #[test]
 fn update_price_feeds_works() {
     let owner = 'owner'.try_into().unwrap();
+    let user = 'user'.try_into().unwrap();
     let wormhole = super::wormhole::deploy_and_init(owner);
+    let fee_contract = deploy_fee_contract(user);
     let pyth = deploy(
         owner,
         wormhole.contract_address,
-        0x42.try_into().unwrap(),
+        fee_contract.contract_address,
         1000,
         array![
             DataSource {
@@ -45,9 +48,15 @@ fn update_price_feeds_works() {
         ]
     );
 
+    start_prank(CheatTarget::One(fee_contract.contract_address), user.try_into().unwrap());
+    fee_contract.approve(pyth.contract_address, 10000);
+    stop_prank(CheatTarget::One(fee_contract.contract_address));
+
     let mut spy = spy_events(SpyOn::One(pyth.contract_address));
 
+    start_prank(CheatTarget::One(pyth.contract_address), user.try_into().unwrap());
     pyth.update_price_feeds(good_update1()).unwrap_with_felt252();
+    stop_prank(CheatTarget::One(pyth.contract_address));
 
     spy.fetch_events();
     assert!(spy.events.len() == 1);
@@ -100,6 +109,22 @@ fn deploy(
     IPythDispatcher { contract_address }
 }
 
+fn deploy_fee_contract(recipient: ContractAddress) -> IERC20CamelDispatcher {
+    let mut args = array![];
+    let name: core::byte_array::ByteArray = "eth";
+    let symbol: core::byte_array::ByteArray = "eth";
+    (name, symbol, 100000_u256, recipient).serialize(ref args);
+    let contract = declare("ERC20");
+    let contract_address = match contract.deploy(@args) {
+        Result::Ok(v) => { v },
+        Result::Err(err) => {
+            panic(err.panic_data);
+            0.try_into().unwrap()
+        },
+    };
+    IERC20CamelDispatcher { contract_address }
+}
+
 // A random update pulled from Hermes.
 fn good_update1() -> ByteArray {
     let bytes = array![

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است