#35724 [SC-Low] Users can withdraw collateral even when the admin pauses the contract.
Submitted on Oct 5th 2024 at 00:13:01 UTC by @SeveritySquad for IOP | Swaylend
Report ID: #35724
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/Swaylend/swaylend-monorepo/blob/develop/abis/market_abi/src/abi.sw
Impacts:
Users can interact with some critical functionalities the protocol when paused
Description
Brief/Intro
the admin is allowed to pause the contract or certain collaterals whenever there might be an issue in the system to avoid exploits or for protocol-known reasons, however, users will still be able to withdraw their collateral when the contract markets functionalities are paused.
Vulnerability Details
with the
`pause_collateral_asset()`
`pause()`
The admin is allowed to stop functionalities of the protocol. For instance on the call to supply_collateral() ```sway // ## 3.1 Supply Collateral #[payable, storage(write)] fn supply_collateral() { // Only allow supplying collateral if paused flag is not set require(!storage.pause_config.supply_paused.read(), Error::Paused); // code let asset_id: AssetId = msg_asset_id(); let collateral_configuration = storage.collateral_configurations.get(asset_id).read();//@audit ere require(!collateral_configuration.paused, Error::Paused); // code // Log user supply collateral event log(UserSupplyCollateralEvent { account: caller, asset_id, amount, }); } ``` if ensures that collateral cannot be supplied if the admin pauses the market, however, the opposite is the case for `withdraw_collateral()` ```sway #[payable, storage(write)] fn withdraw_collateral( asset_id: AssetId, amount: u64, price_data_update: PriceDataUpdate, ) { // no checks against withdrawal when the protocol is paused. reentrancy_guard();
// Get the caller's account and calculate the new user and total collateral
let caller = msg_sender().unwrap();
let user_collateral = storage.user_collateral.get((caller, asset_id)).try_read().unwrap_or(0) - amount;
let total_collateral = storage.totals_collateral.get(asset_id).try_read().unwrap_or(0) - amount;
// Update the storage values (total collateral, user collateral)
storage.totals_collateral.insert(asset_id, total_collateral);
storage
.user_collateral
.insert((caller, asset_id), user_collateral);
// Update price data
update_price_feeds_if_necessary_internal(price_data_update);
// Note: no accrue interest, BorrowCollateralFactor < LiquidationCollateralFactor covers small changes
// Check if the user is borrow collateralized
require(is_borrow_collateralized(caller), Error::NotCollateralized);
transfer(caller, asset_id, amount);
// Log user withdraw collateral event
log(UserWithdrawCollateralEvent {
account: caller,
asset_id,
amount,
});
}
```
Mitigation
Apply same checks in `supply_collateral()` for the `withraw_collateral()`
Impact Details
Here, users will be able to bypass the sanctions put by the admin on the market allowing for exploits to continue even tho the contract is paused.
References
https://github.com/Swaylend/swaylend-monorepo/blob/63e7b1163216d1400b9436c8d256f0f0e043e225/contracts/market/src/main.sw#L304C1-L338C6
Proof of Concept
```rust use std::str::FromStr;
use fuels::accounts::ViewOnlyAccount; 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; use crate::utils::{setup, TestData}; use fuels::types::{ContractId, U256}; use market::{CollateralConfiguration, PauseConfiguration}; use market_sdk::get_market_config;
#[tokio::test] async fn test_withdraw_when_paused() { let TestData { admin, admin_account, alice, alice_account, bob, bob_account, market, assets, usdc, oracle, price_feed_ids, publish_time, prices, usdc_contract, uni, uni_contract, .. } = setup().await;
let asset_id = assets["ETH"].asset_id;
let mock_collateral_config = CollateralConfiguration {
asset_id: assets["USDC"].asset_id.into(),
price_feed_id: assets["USDC"].price_feed_id,
decimals: assets["USDC"].decimals.try_into().unwrap(),
borrow_collateral_factor: U256::from(18), // decimals: 18
liquidate_collateral_factor: U256::from(18), // decimals: 18
liquidation_penalty: U256::from(18), // decimals: 18
supply_cap: 10, // decimals: asset decimals
paused: false,
};
let admin_add_collat_res = market
.with_account(&admin)
.await
.unwrap()
.add_collateral_asset(&mock_collateral_config)
.await;
// make sure add_collateral_asset was ok
assert!(admin_add_collat_res.is_ok());
// ==================== Step #1 ====================
// 👛 Wallet: Bob 🧛
// 🤙 Call: supply_collateral
// 💰 Amount: 40.00 UNI ~ $200.00
let bob_mint_amount = parse_units(50 * AMOUNT_COEFFICIENT, uni.decimals);
let bob_supply_amount = parse_units(40 * AMOUNT_COEFFICIENT, uni.decimals);
let bob_mint_log_amount = format!("{} UNI", bob_mint_amount as f64 / SCALE_9);
// print_case_title(1, "Bob", "supply_collateral", bob_mint_log_amount.as_str());
println!("💸 Bob + {bob_mint_log_amount}");
// mint bob an amount to supply
uni_contract
.mint(bob_account, bob_mint_amount)
.await
.unwrap();
let bob_balance = bob.get_asset_balance(&uni.asset_id).await.unwrap();
assert!(bob_balance == bob_mint_amount);
// allow bob to supply collateral
let bob_supply_res = market
.with_account(&bob)
.await
.unwrap()
.supply_collateral(uni.asset_id, bob_supply_amount)
.await;
assert!(bob_supply_res.is_ok());
// makes checks to ensure bob deposit was successful
let bob_user_collateral = market
.get_user_collateral(bob_account, uni.asset_id)
.await
.unwrap()
.value;
assert!(bob_user_collateral == bob_supply_amount);
// allowing admin to pause the contract.
let admin_pause_collat_res = market
.with_account(&admin)
.await
.unwrap()
.pause_collateral_asset(uni.asset_id)
.await;
assert!(admin_pause_collat_res.is_ok());
// get bobs withdrawable balance.
let bob_withdraw_amount = market
.get_user_collateral(bob_account, uni.asset_id)
.await
.unwrap()
.value;
// get the price feed to enable withdraw.
let price_data_update = PriceDataUpdate {
update_fee: 0,
price_feed_ids,
publish_times: vec![publish_time; assets.len()],
update_data: oracle.create_update_data(&prices).await.unwrap(),
};
// bob is not supposed to be able to withdraw when the market is paused, however he can.
let withdraw_collateral_res = market
.with_account(&bob)
.await
.unwrap()
.withdraw_collateral(
&[&oracle.instance],
uni.asset_id,
bob_withdraw_amount.try_into().unwrap(),
&price_data_update,
)
.await;
assert!(withdraw_collateral_res.is_ok());
}
```
Last updated
Was this helpful?