Vulnerability from Small Deposits Being Rounded Down to Zero Shares in Smart Contracts

In certain smart contract systems, users who make small deposits can end up with zero shares due to rounding errors in share calculations. This typically occurs when the deposit amount is too small in comparison to the total supply and total assets in the system, causing the calculated shares to round down to zero. As a result, users lose their entire deposit without receiving any ownership or compensation.

This tutorial will walk you through how this vulnerability arises, how small deposits can be rounded down to zero, and how to mitigate the issue.

The Vulnerability: Small Deposits Rounding Down to Zero Shares

In decentralized finance (DeFi) systems, when users deposit assets, they receive a proportional amount of shares representing their ownership of the underlying assets. These shares are often calculated using integer division, which rounds down in Solidity. If the user deposits a small amount of assets, the calculation may result in zero shares due to rounding down.

This problem can lead to a situation where users effectively lose their entire deposit, as they receive zero shares in return for their contribution.

Example of the Vulnerability:

Let’s examine a deposit function that calculates the number of shares a user should receive:

function deposit(uint256 assets, address receiver)
    public
    virtual
    override
    returns (uint256)
{
    if (assets > maxDeposit(receiver)) revert MaxError(assets);

    uint256 shares = _previewDeposit(assets);
    _deposit(_msgSender(), receiver, assets, shares);

    return shares;
}

The problem arises in the _previewDeposit function, which calculates the number of shares using integer division:

function _convertToShares(uint256 assets, Math.Rounding rounding)
    internal
    view
    virtual
    override
    returns (uint256 shares)
{
    uint256 _totalSupply = totalSupply();
    uint256 _totalAssets = totalAssets();

    return (assets == 0 || _totalSupply == 0 || _totalAssets == 0)
        ? assets
        : assets.mulDiv(_totalSupply, _totalAssets, rounding);
}

Here’s an example where the rounding issue occurs:

  • Assume the total supply of shares (_totalSupply) is 100,000 and the total assets (_totalAssets) are 100,000,000.

  • A user deposits 999 assets.

The calculation for shares becomes:

shares = 999 * 100,000 / 100,000,000 = 0.999

Since Solidity rounds down, the user will receive zero shares for their deposit, despite having deposited assets.


Impact: Users Lose Their Deposited Funds

  1. Complete Loss of Funds: When the calculation rounds down to zero shares, the user’s entire deposit is lost. They receive no shares, effectively losing their investment without getting anything in return.

  2. Unrecoverable Funds: Since the shares represent ownership, users cannot reclaim their deposits, leading to a permanent loss of funds.

  3. User Dissatisfaction: This type of vulnerability can severely damage user trust in the protocol. Users may be discouraged from participating if they believe there is a risk of losing their deposit.


Mitigation: Preventing Small Deposits from Being Rounded Down to Zero

To avoid this vulnerability, developers should ensure that small deposits are correctly handled, and users always receive at least one share. The following are effective mitigation strategies.

1. Revert Transactions that Result in Zero Shares

The simplest way to prevent this issue is to revert any transaction where the calculated shares are zero. This ensures that users do not lose their deposit if the share calculation would round down to zero.

Here’s how you can modify the deposit function:

function deposit(uint256 assets, address receiver)
    public
    virtual
    override
    returns (uint256)
{
    if (assets > maxDeposit(receiver)) revert MaxError(assets);

    uint256 shares = _previewDeposit(assets);
    
    // Revert if the share calculation results in zero shares
    if (shares == 0) revert ZeroSharesError();

    _deposit(_msgSender(), receiver, assets, shares);

    return shares;
}

By using the mulDivUp function, the calculation rounds up, ensuring that users receive at least one share for their deposit.


Conclusion

Rounding errors in share calculations can cause users to lose their entire investment when small deposits are rounded down to zero shares. This issue arises when the numerator is not a multiple of the denominator, causing the calculation to round down and mint zero shares for the user.

To mitigate this risk:

  1. Revert transactions that would result in zero shares being minted.

  2. Use rounding-up arithmetic to ensure that users always receive at least one share.

By implementing these strategies, developers can ensure that their protocols handle deposits fairly and prevent users from losing their investments due to rounding errors.

Last updated