DeFiFoundry
60,000 USDC
View results
Submission Details
Severity: medium
Invalid

Blacklisted accounts for some ERC20 token will DOS during funds withdrawal leading to inability to retrieve such tokens.

Summary

Certain ERC20 tokens, such as USDC, have the ability to blacklist addresses, preventing them from sending or receiving tokens. In the current withdrawal implementation, funds are always sent back to msg.sender, which can cause issues if the address is blacklisted, leading to stuck collateral. To mitigate this, the withdrawal function should allow users to specify an alternative receiver address.

Vulnerability Details

When an account is blacklisted by an ERC20 token contract (e.g., USDC), it cannot send or receive tokens. The current withdrawal function in the protocol sends funds directly back to msg.sender. If msg.sender is blacklisted, the transaction will revert, causing the collateral to be stuck in the protocol.

Key Points:

  1. Current Behavior:

    • Withdrawal function sends funds back to msg.sender.

    • If msg.sender is blacklisted, the transaction reverts.

  2. Issue:

    • Blacklisted addresses cannot receive funds.

    • Collateral becomes stuck in the protocol.

  3. Desired Behavior:

    • Allow users to specify a different receiver address.

    • If no receiver address is specified, default to msg.sender.

function withdrawMargin(uint128 tradingAccountId, address collateralType, uint256 amount) external {
@audit>> we can't pass in a address receiver>>>
// fetch storage slot for this collateral's config config
MarginCollateralConfiguration.Data storage marginCollateralConfiguration =
MarginCollateralConfiguration.load(collateralType);
// load existing trading account; reverts for non-existent account
// enforces `msg.sender == owner` so only account owner can withdraw
TradingAccount.Data storage tradingAccount =
TradingAccount.loadExistingAccountAndVerifySender(tradingAccountId);
// convert uint256 -> UD60x18; scales input amount to 18 decimals
UD60x18 amountX18 = marginCollateralConfiguration.convertTokenAmountToUd60x18(amount);
// enforce converted amount > 0
_requireAmountNotZero(amountX18);
// enforces that user has deposited enough collateral of this type to withdraw
_requireEnoughMarginCollateral(tradingAccount, collateralType, amountX18);
// deduct amount from trader's collateral balance
tradingAccount.withdraw(collateralType, amountX18);
// load account required initial margin requirement & unrealized USD profit/loss
// ignores "required maintenance margin" output parameter
(UD60x18 requiredInitialMarginUsdX18,, SD59x18 accountTotalUnrealizedPnlUsdX18) =
tradingAccount.getAccountMarginRequirementUsdAndUnrealizedPnlUsd(0, SD59x18_ZERO);
// get trader's margin balance
SD59x18 marginBalanceUsdX18 = tradingAccount.getMarginBalanceUsd(accountTotalUnrealizedPnlUsdX18);
// check against initial margin requirement as initial margin > maintenance margin
// hence prevent the user from withdrawing all the way to the maintenance margin
// so that they couldn't be liquidated very soon afterwards if their position
// goes against them even a little bit
tradingAccount.validateMarginRequirement(requiredInitialMarginUsdX18, marginBalanceUsdX18, UD60x18_ZERO);
// finally send the tokens
@audit>> transfer only to msg.sender>>> IERC20(collateralType).safeTransfer(msg.sender, amount);
emit LogWithdrawMargin(msg.sender, tradingAccountId, collateralType, amount);
}

Impact

This issue can lead to a denial of service for users with blacklisted addresses, preventing them from withdrawing their collateral. Based on docs -

https://docs.zaros.fi/overview/resources/faq#:~:text=If your trade position has not opened yet yes. If the trade position is opened you need to wait until the end of the trade or close manually for your money to show in your balance and be available for withdrawal.

We should be able to withdraw all funds available in our balance. Not being able to do so for any reason will lead to funds being stuck in contract.

Tools Used

  • Manual Solidity code analysis

  • Review of ERC20 token standards and blacklist functionality

Recommendations

Update Withdrawal Function: Modify the withdrawal function to accept an optional receiver address. If no receiver address is provided, default to msg.sender.

/// @notice Withdraws available margin collateral from the given trading account.
/// @param tradingAccountId The trading account id.
/// @param collateralType The margin collateral address.
/// @param amount The UD60x18 amount of margin collateral to withdraw.
-- function withdrawMargin(uint128 tradingAccountId, address collateralType, uint256 amount) external {
++ function withdrawMargin(uint128 tradingAccountId, address collateralType,address Receiver, uint256 amount) external {
++ if (Receiver==address(0)){
++ Receiver=msg.sender;
++ }
// fetch storage slot for this collateral's config config
MarginCollateralConfiguration.Data storage marginCollateralConfiguration =
MarginCollateralConfiguration.load(collateralType);
// load existing trading account; reverts for non-existent account
// enforces \`msg.sender == owner\` so only account owner can withdraw
TradingAccount.Data storage tradingAccount =
TradingAccount.loadExistingAccountAndVerifySender(tradingAccountId);
// convert uint256 -> UD60x18; scales input amount to 18 decimals
UD60x18 amountX18 = marginCollateralConfiguration.convertTokenAmountToUd60x18(amount);
// enforce converted amount > 0
_requireAmountNotZero(amountX18);
// enforces that user has deposited enough collateral of this type to withdraw
_requireEnoughMarginCollateral(tradingAccount, collateralType, amountX18);
// deduct amount from trader's collateral balance
tradingAccount.withdraw(collateralType, amountX18);
// load account required initial margin requirement & unrealized USD profit/loss
// ignores "required maintenance margin" output parameter
(UD60x18 requiredInitialMarginUsdX18,, SD59x18 accountTotalUnrealizedPnlUsdX18) =
tradingAccount.getAccountMarginRequirementUsdAndUnrealizedPnlUsd(0, SD59x18\_ZERO);
// get trader's margin balance
SD59x18 marginBalanceUsdX18 = tradingAccount.getMarginBalanceUsd(accountTotalUnrealizedPnlUsdX18);
// check against initial margin requirement as initial margin > maintenance margin
// hence prevent the user from withdrawing all the way to the maintenance margin
// so that they couldn't be liquidated very soon afterwards if their position
// goes against them even a little bit
tradingAccount.validateMarginRequirement(requiredInitialMarginUsdX18, marginBalanceUsdX18, UD60x18\_ZERO);
// finally send the tokens
-- IERC20(collateralType).safeTransfer(msg.sender, amount);
++ IERC20(collateralType).safeTransfer(Receiver, amount);
emit LogWithdrawMargin(msg.sender, tradingAccountId, collateralType, amount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.