Rounding in Favor of the Protocol with Integer Division in Solidity

In Solidity, integer division truncates toward zero, which can be leveraged to ensure rounding favors the protocol in critical calculations. This approach is crucial to prevent value leakage to users and maintain the protocol's economic integrity. While ERC-4626 vaults are a common example, this concept applies broadly to any financial mechanism, such as fee calculations, token distributions, or staking rewards.

Key Considerations

  • Integer Division Behavior: In Solidity, division of two integers (a / b) discards the remainder, rounding down. For example, 5 / 2 results in 2, not 2.5.

  • Favoring the Protocol: Structure calculations to round up when distributing shares or benefits to users and round down when returning assets to ensure the protocol retains value. This prevents users from gaining unintended advantages.

  • Security Implications: Incorrect rounding can allow users to exploit calculations, gaining more assets or shares than intended. Additionally, improper handling of edge cases (e.g., zero values or extreme inputs) may cause unintended reverts, potentially leading to denial-of-service (DoS) issues for other users. Always validate edge cases to prevent such vulnerabilities.

Example: Rounding in OpenZeppelin's ERC-4626 Implementation

The OpenZeppelin ERC-4626 implementation provides a robust example of rounding in favor of the protocol during asset-to-share and share-to-asset conversions. Below is a snippet from the official OpenZeppelin ERC-4626 contract (source) showing the preview functions that handle conversions with explicit rounding directions.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Math} from "../../utils/math/Math.sol";
import {IERC20, IERC20Metadata, ERC20} from "../ERC20.sol";

abstract contract ERC4626 is ERC20, IERC4626 {
    // ... (other contract code omitted for brevity)

    /** @dev See {IERC4626-previewDeposit}. */
    function previewDeposit(uint256 assets) public view virtual returns (uint256) {
        return _convertToShares(assets, Math.Rounding.Floor);
    }

    /** @dev See {IERC4626-previewMint}. */
    function previewMint(uint256 shares) public view virtual returns (uint256) {
        return _convertToAssets(shares, Math.Rounding.Ceil);
    }

    /** @dev See {IERC4626-previewWithdraw}. */
    function previewWithdraw(uint256 assets) public view virtual returns (uint256) {
        return _convertToShares(assets, Math.Rounding.Ceil);
    }

    /** @dev See {IERC4626-previewRedeem}. */
    function previewRedeem(uint256 shares) public view virtual returns (uint256) {
        return _convertToAssets(shares, Math.Rounding.Floor);
    }
}

Explanation

  • Concept Generalization: While this example uses OpenZeppelin's ERC-4626 implementation, the principle of rounding in favor of the protocol applies to any scenario involving division, such as fee calculations, reward distributions, or token minting. For instance, in fee calculations, rounding up the fee ensures the protocol collects slightly more, while in reward distributions, rounding down user rewards preserves protocol value.

  • Rounding in ERC-4626:

    • previewDeposit: Converts assets to shares, rounding down (Math.Rounding.Floor) to issue fewer shares, favoring the protocol by reducing share dilution.

    • previewMint: Calculates assets needed to mint a specific number of shares, rounding up (Math.Rounding.Ceil) to require more assets, ensuring the protocol receives more value.

    • previewWithdraw: Converts assets to shares for withdrawal, rounding up (Math.Rounding.Ceil) to burn more shares, reducing the protocol's liability.

    • previewRedeem: Converts shares to assets, rounding down (Math.Rounding.Floor) to return fewer assets, preserving protocol value.

Last updated