Precision Loss During Withdrawals from Vaults Can Block Token Transfers Due to Value < Amount

Many vault systems convert tokens to shares, which represent a proportional ownership of the vault’s assets. When users want to withdraw their assets, the system must first convert the requested amount of tokens back to shares, then calculate how many actual tokens should be transferred. The conversion between tokens and shares is where precision loss can occur.

In Solidity, calculations involving division or multiplication can result in rounding down, especially if the precision between two numbers doesn't align. This results in a smaller number of tokens being available for withdrawal than expected, which can lead to a transaction failure if the system tries to transfer an amount greater than what is available.

Example:

Let’s take a look at a simplified withdrawal function in a vault contract:

function withdraw(address _recipient, uint256 _amount) external override onlyAdmin {
    vault.withdraw(_tokensToShares(_amount));
    address _token = vault.getToken();
    IDetailedERC20(_token).safeTransfer(_recipient, _amount);
}

In this code, the vault converts _amount to the corresponding number of shares via _tokensToShares(_amount). It then calls the vault’s withdraw() function, which internally converts shares back to tokens. After that, the system attempts to transfer _amount of tokens to the recipient.

Here’s where the vulnerability occurs:

  1. Due to precision loss during the conversion process between shares and tokens, the actual number of tokens withdrawn from the vault may be less than the requested _amount.

  2. The system then tries to transfer _amount tokens to the recipient, but if fewer tokens are available (due to the precision loss), the transfer fails and the transaction reverts.

Why Precision Loss Happens

Precision loss in DeFi protocols typically occurs in the following scenarios:

  1. Token-to-Share Conversion: When users deposit tokens into a vault, they are often given shares representing their proportion of the vault’s total assets. However, converting back from shares to tokens involves calculations that may lead to rounding errors.

  2. Integer Division: Solidity does not support floating-point numbers, so when performing division between two integers, the result is rounded down. This means some fractional amounts are lost during calculations.

  3. Different Token Decimal Places: If tokens in the vault have different decimal places, precision loss is even more likely. For example, converting between a token with 18 decimals and one with 8 decimals can cause discrepancies when rounding down.

Example of Precision Loss:

Suppose a user requests to withdraw 1000 tokens from a vault. The vault has a conversion rate of 10 shares per 1 token.

  • The contract calculates how many shares represent 1000 tokens, and it comes out to 100 shares.

  • When the user tries to convert these 100 shares back to tokens, the vault may return 999.99 tokens due to small discrepancies in price-per-share and rounding.

  • However, the contract will attempt to transfer the full 1000 tokens to the user. Since only 999.99 tokens are available, the transfer fails, and the transaction reverts.


Mitigation: Handling Precision Loss to Prevent Transaction Failures

There are several approaches to mitigating precision loss and ensuring that users' withdrawals do not fail due to insufficient token balance.

1. Transfer Only the Available Tokens

Instead of transferring the exact requested amount, the contract should calculate the actual number of tokens available after the withdrawal and transfer that amount. This approach ensures that the transfer will not fail due to precision loss.

Here’s how you can modify the withdrawal function:

function withdraw(address _recipient, uint256 _amount) external override onlyAdmin {
    address _token = vault.getToken();
    
    // Record the token balance before the withdrawal
    uint256 beforeBalance = IDetailedERC20(_token).balanceOf(address(this));
    
    // Withdraw tokens based on share conversion
    vault.withdraw(_tokensToShares(_amount));

    // Calculate the actual amount of tokens withdrawn
    uint256 actualWithdrawn = IDetailedERC20(_token).balanceOf(address(this)) - beforeBalance;
    
    // Transfer the actual number of tokens withdrawn
    IDetailedERC20(_token).safeTransfer(_recipient, actualWithdrawn);
}

This solution ensures that the contract only transfers the number of tokens that were actually withdrawn from the vault, preventing the transaction from failing due to an insufficient token balance.

Conclusion

Precision loss is a common vulnerability in smart contracts, particularly when dealing with vaults, withdrawals, and token-to-share conversions. If not handled correctly, this can lead to failed transactions, preventing users from withdrawing their assets or causing the contract to revert.

Last updated