DeFiFoundrySolidity
16,653 OP
View results
Submission Details
Severity: medium
Invalid

Potential Misuse of `availableWithdrawLimit` Due to Absence of Withdrawal Caps

Summary

The availableWithdrawLimit function in the StrategyOp contract currently lacks defined withdrawal caps, returning the sum of the asset balance and the transmuter’s unexchanged balance without limitations. This design opens the strategy to liquidity-related vulnerabilities, as large withdrawals could disrupt its liquidity and affect optimal functioning. Mitigation includes enforcing static or dynamic withdrawal limits based on strategy performance and risk assessments.


Technical Details

Code Reference

The availableWithdrawLimit function is designed to return the total withdrawable amount:

https://github.com/Cyfrin/2024-12-alchemix/blob/82798f4891e41959eef866bd1d4cb44fc1e26439/src/StrategyOp.sol#L199

function availableWithdrawLimit(
address /*_owner*/
) public view override returns (uint256) {
return asset.balanceOf(address(this)) + transmuter.getUnexchangedBalance(address(this));
}

Workflow Context

  1. Current Implementation:

    • The function calculates the sum of the asset balance and the transmuter’s unexchanged balance.

    • No constraints or caps are enforced on the withdrawal amount.

  2. Issue:

    • Large or frequent withdrawals could deplete the strategy’s liquid reserves, forcing premature or inefficient liquidation of assets.


Exploitation Scenarios

Scenario 1: Large Withdrawals Impacting Liquidity

  1. Setup:

    • The strategy holds 10,000 WETH in total, with 6,000 WETH as unexchanged balance and 4,000 WETH as liquid assets.

  2. Execution:

    • A user initiates a withdrawal of 9,000 WETH, which exceeds the liquid asset reserve.

  3. Impact:

    • The strategy is forced to liquidate unexchanged assets prematurely, potentially incurring slippage or unfavorable market rates.


Scenario 2: Liquidity Crunch During Volatility

  1. Setup:

    • The market becomes volatile, causing a sudden demand for withdrawals.

    • Multiple users simultaneously request large withdrawals.

  2. Execution:

    • The strategy’s liquid reserves are depleted before unexchanged balances can be efficiently converted to liquid assets.

  3. Impact:

    • Delayed withdrawal processing or forced asset sales at unfavorable prices reduce user confidence and yield.


impact Analysis

Severity: Low to Medium

  1. Financial Impact:

    • Forced liquidation at unfavorable rates could lead to yield losses.

    • Reduced liquidity undermines the strategy’s ability to operate efficiently.

  2. Operational Impact:

    • Large withdrawals may delay other user transactions or cause temporary disruptions in functionality.

  3. Reputational Impact:

    • Users may lose trust in the strategy due to delayed withdrawals or reduced performance.


Root Cause Analysis

  1. Absence of Withdrawal Limits:

    • The function allows unlimited withdrawals without considering the strategy’s liquidity constraints.

  2. Lack of Dynamic Constraints:

    • The function does not dynamically adjust withdrawal limits based on real-time liquidity conditions or strategy performance.


Mitigation Recommendations

1. Implement Static Withdrawal Limits

  • Define a maximum allowable withdrawal amount to ensure liquidity:

    uint256 public withdrawalLimit = 1000 ether;
    function availableWithdrawLimit(address /*_owner*/) public view override returns (uint256) {
    uint256 totalAvailable = asset.balanceOf(address(this)) + transmuter.getUnexchangedBalance(address(this));
    return totalAvailable > withdrawalLimit ? withdrawalLimit : totalAvailable;
    }

2. Introduce Dynamic Limits

  • Dynamically adjust the withdrawal limit based on the strategy’s liquidity and market conditions:

    function calculateDynamicLimit() internal view returns (uint256) {
    uint256 currentLiquidity = asset.balanceOf(address(this));
    uint256 riskAdjustedLimit = currentLiquidity * 80 / 100; // Limit to 80% of liquid assets
    return riskAdjustedLimit;
    }
    function availableWithdrawLimit(address /*_owner*/) public view override returns (uint256) {
    uint256 dynamicLimit = calculateDynamicLimit();
    uint256 totalAvailable = asset.balanceOf(address(this)) + transmuter.getUnexchangedBalance(address(this));
    return totalAvailable > dynamicLimit ? dynamicLimit : totalAvailable;
    }

3. Log Withdrawal Requests

  • Enhance transparency by logging large withdrawal requests:

    event WithdrawalRequested(address indexed user, uint256 amountRequested, uint256 limit);
    function availableWithdrawLimit(address _owner) public view override returns (uint256) {
    uint256 totalAvailable = asset.balanceOf(address(this)) + transmuter.getUnexchangedBalance(address(this));
    uint256 limit = calculateDynamicLimit();
    emit WithdrawalRequested(_owner, totalAvailable, limit);
    return totalAvailable > limit ? limit : totalAvailable;
    }

Proof of Concept (PoC)

Step-by-Step Reproduction:

  1. Deploy the contract and initialize the strategy with 10,000 WETH.

  2. Request a withdrawal exceeding the liquid reserve (e.g., 9,000 WETH).

  3. Observe how the strategy is forced to liquidate unexchanged assets or delays processing due to insufficient liquidity.

Updates

Appeal created

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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