#35876 [SC-High] Users will lose funds on calls to critical functions if the prices are not updated

Submitted on Oct 11th 2024 at 15:51:15 UTC by @SeveritySquad for IOP | Swaylend

  • Report ID: #35876

  • Report Type: Smart Contract

  • Report severity: High

  • Target: https://github.com/Swaylend/swaylend-monorepo/blob/develop/contracts/market/src/main.sw

  • Impacts:

    • Permanent freezing of funds

Description

Brief/Intro

The internal `update_price_feeds_if_necessary_internal()` transfers the fees for updating the Pyth price-feeds to the oracle, however, the fees will be left stuck in Pyth in scenarios where the prices were never even updated.

Vulnerability Details

The `update_price_feeds_if_necessary_internal()` is called by:

  • `absorb()`.

  • `withdraw_base()`.

  • `withdraw_collateral()`.

Using `withdraw_collateral()` as an example, on the call to it, ```rust #[payable, storage(write)] fn withdraw_collateral( // some code // Update price data //@audit here the price is updated if neccessary. update_price_feeds_if_necessary_internal(price_data_update);

// code for logging } ``` internally the function calls `update_price_feeds_if_necessary_internal()` which further makes a call to Pyth's `update_price_feeds_if_necessary()` function, before the call to Pyth's oracle it ensures that the user sent the amount of fee for the price feeds to be updated and forwards that fee on the call to Pyth's oracle `update_price_feeds_if_necessary()`.

  • https://github.com/Swaylend/swaylend-monorepo/blob/9132747331188b86dd8cbf9a1ca37b811d08dddb/contracts/market/src/main.sw#L1441C1-L1469C2 ```rust #[payable, storage(read)] fn update_price_feeds_if_necessary_internal(price_data_update: PriceDataUpdate) { // some code // check if the payment is sufficient require( msg_amount() >= price_data_update .update_fee && msg_asset_id() == AssetId::base(), Error::InvalidPayment, );

    let oracle = abi(PythCore, contract_id.bits()); oracle .update_price_feeds_if_necessary { asset_id: AssetId::base().bits(), coins: price_data_update.update_fee, }( price_data_update .price_feed_ids, price_data_update .publish_times, price_data_update .update_data, ); }

``` The issue here lies in the fact that Pyth's oracle `update_price_feeds_if_necessary()` which only calls `update_price_feeds()` if the latest publish time is less than the input published time, and fees are only required in `update_price_feeds()` whenever a price feed is updated.

  • https://github.com/pyth-network/pyth-crosschain/blob/9c761626440da0c731d5e41e9b6d31aa5e909bf3/target_chains/fuel/contracts/pyth-contract/src/main.sw#L292C1-L300C10 ```rust while i < price_feed_ids.len() { if latest_publish_time(price_feed_ids.get(i).unwrap()) < publish_times.get(i).unwrap() { update_price_feeds(update_data); return; }

```

  • https://github.com/pyth-network/pyth-crosschain/blob/9c761626440da0c731d5e41e9b6d31aa5e909bf3/target_chains/fuel/contracts/pyth-contract/src/main.sw#L420C2-L421C71

```rust #[storage(read, write), payable] fn update_price_feeds(update_data: Vec<Bytes>) { // code for price feed updates. let required_fee = total_fee(total_number_of_updates, storage.single_update_fee); require(msg_amount() >= required_fee, PythError::InsufficientFee); ```

Impact Details

In scenarios when the price feeds are not updated in Pyth's oracle, the fees for updating those feeds will be left stuck in Pyth with no way to retrieve it, and given the number of transactions and collaterals that would be used the amount the users will lose for price feeds that were never updated will increase exponentially. Also, Pyth fee can increase in the future causing the costs to rise, and concerning this

  • https://github.com/Swaylend/swaylend-monorepo/issues/158

Mitigation

Use the Pyth's `PythInfo::latest_publish_time()` -> https://github.com/pyth-network/pyth-crosschain/blob/9c761626440da0c731d5e41e9b6d31aa5e909bf3/target_chains/fuel/contracts/pyth-contract/src/main.sw#L563C8-L563C27 to get the latest publish times, possibly looping through to check the price feeds that need not be updated and only forwarding to the Pyth oracle the amount of fees required for the feeds that will be updated.

Proof of Concept

Proof of Concept

```bash cargo test local_tests::scenarios::multicall_withdraw_supply::multicall_withdraw_supply_test ``` We update the price feeds, before with call `withdraw_base()` with that same price feed update data, but the fee is still lost to the Pyth oracle ```rust use crate::utils::{print_case_title, setup, TestData}; use fuels::{ accounts::ViewOnlyAccount, programs::{ calls::{CallHandler, CallParameters}, responses::CallResponse, }, types::{transaction::TxPolicies, transaction_builders::VariableOutputPolicy}, }; use market::PriceDataUpdate; use market_sdk::parse_units;

const AMOUNT_COEFFICIENT: u64 = 10u64.pow(0); const SCALE_6: f64 = 10u64.pow(6) as f64; const SCALE_9: f64 = 10u64.pow(9) as f64;

#[tokio::test] async fn multicall_withdraw_supply_test() { let TestData { wallets, alice, alice_account, bob, bob_account, market, usdc, usdc_contract, eth, oracle, price_feed_ids, publish_time, prices, assets, .. } = setup().await;

}

```

Last updated

Was this helpful?