Skip to content

Commit

Permalink
Rename rounding modes and complete with fourth (#4455)
Browse files Browse the repository at this point in the history
Co-authored-by: ernestognw <ernestognw@gmail.com>
  • Loading branch information
frangio and ernestognw authored Jul 13, 2023
1 parent a55af77 commit 84db204
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 181 deletions.
5 changes: 5 additions & 0 deletions .changeset/wild-rockets-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': major
---

`Math`: Renamed members of `Rounding` enum, and added a new rounding mode for "away from zero".
4 changes: 2 additions & 2 deletions contracts/mocks/docs/ERC4626Fees.sol
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ abstract contract ERC4626Fees is ERC4626 {
}

function _feeOnRaw(uint256 assets, uint256 feeBasePoint) private pure returns (uint256) {
return assets.mulDiv(feeBasePoint, 1e5, Math.Rounding.Up);
return assets.mulDiv(feeBasePoint, 1e5, Math.Rounding.Ceil);
}

function _feeOnTotal(uint256 assets, uint256 feeBasePoint) private pure returns (uint256) {
return assets.mulDiv(feeBasePoint, feeBasePoint + 1e5, Math.Rounding.Up);
return assets.mulDiv(feeBasePoint, feeBasePoint + 1e5, Math.Rounding.Ceil);
}
}
14 changes: 7 additions & 7 deletions contracts/token/ERC20/extensions/ERC4626.sol
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,12 @@ abstract contract ERC4626 is ERC20, IERC4626 {

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

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

/** @dev See {IERC4626-maxDeposit}. */
Expand All @@ -139,7 +139,7 @@ abstract contract ERC4626 is ERC20, IERC4626 {

/** @dev See {IERC4626-maxWithdraw}. */
function maxWithdraw(address owner) public view virtual returns (uint256) {
return _convertToAssets(balanceOf(owner), Math.Rounding.Down);
return _convertToAssets(balanceOf(owner), Math.Rounding.Floor);
}

/** @dev See {IERC4626-maxRedeem}. */
Expand All @@ -149,22 +149,22 @@ abstract contract ERC4626 is ERC20, IERC4626 {

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

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

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

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

/** @dev See {IERC4626-deposit}. */
Expand Down
2 changes: 1 addition & 1 deletion contracts/utils/Arrays.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ library Arrays {
uint256 mid = Math.average(low, high);

// Note that mid will always be strictly less than high (i.e. it will be a valid array index)
// because Math.average rounds down (it does integer division with truncation).
// because Math.average rounds towards zero (it does integer division with truncation).
if (unsafeAccess(array, mid).value > element) {
high = mid;
} else {
Expand Down
37 changes: 23 additions & 14 deletions contracts/utils/math/Math.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ library Math {
error MathOverflowedMulDiv();

enum Rounding {
Down, // Toward negative infinity
Up, // Toward infinity
Zero // Toward zero
Floor, // Toward negative infinity
Ceil, // Toward positive infinity
Trunc, // Toward zero
Expand // Away from zero
}

/**
Expand Down Expand Up @@ -100,8 +101,8 @@ library Math {
/**
* @dev Returns the ceiling of the division of two numbers.
*
* This differs from standard division with `/` in that it rounds up instead
* of rounding down.
* This differs from standard division with `/` in that it rounds towards infinity instead
* of rounding towards zero.
*/
function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
if (b == 0) {
Expand Down Expand Up @@ -206,14 +207,15 @@ library Math {
*/
function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) {
uint256 result = mulDiv(x, y, denominator);
if (rounding == Rounding.Up && mulmod(x, y, denominator) > 0) {
if (unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0) {
result += 1;
}
return result;
}

/**
* @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded down.
* @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded
* towards zero.
*
* Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11).
*/
Expand Down Expand Up @@ -256,12 +258,12 @@ library Math {
function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = sqrt(a);
return result + (rounding == Rounding.Up && result * result < a ? 1 : 0);
return result + (unsignedRoundsUp(rounding) && result * result < a ? 1 : 0);
}
}

/**
* @dev Return the log in base 2, rounded down, of a positive value.
* @dev Return the log in base 2 of a positive value rounded towards zero.
* Returns 0 if given 0.
*/
function log2(uint256 value) internal pure returns (uint256) {
Expand Down Expand Up @@ -309,12 +311,12 @@ library Math {
function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = log2(value);
return result + (rounding == Rounding.Up && 1 << result < value ? 1 : 0);
return result + (unsignedRoundsUp(rounding) && 1 << result < value ? 1 : 0);
}
}

/**
* @dev Return the log in base 10, rounded down, of a positive value.
* @dev Return the log in base 10 of a positive value rounded towards zero.
* Returns 0 if given 0.
*/
function log10(uint256 value) internal pure returns (uint256) {
Expand Down Expand Up @@ -358,12 +360,12 @@ library Math {
function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = log10(value);
return result + (rounding == Rounding.Up && 10 ** result < value ? 1 : 0);
return result + (unsignedRoundsUp(rounding) && 10 ** result < value ? 1 : 0);
}
}

/**
* @dev Return the log in base 256, rounded down, of a positive value.
* @dev Return the log in base 256 of a positive value rounded towards zero.
* Returns 0 if given 0.
*
* Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.
Expand Down Expand Up @@ -401,7 +403,14 @@ library Math {
function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = log256(value);
return result + (rounding == Rounding.Up && 1 << (result << 3) < value ? 1 : 0);
return result + (unsignedRoundsUp(rounding) && 1 << (result << 3) < value ? 1 : 0);
}
}

/**
* @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers.
*/
function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) {
return uint8(rounding) % 2 == 1;
}
}
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/erc4626.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ image::erc4626-rate-loglogext.png[More exchange rates in logarithmic scale]

=== The attack

When depositing tokens, the number of shares a user gets is rounded down. This rounding takes away value from the user in favor or the vault (i.e. in favor of all the current share holders). This rounding is often negligible because of the amount at stake. If you deposit 1e9 shares worth of tokens, the rounding will have you lose at most 0.0000001% of your deposit. However if you deposit 10 shares worth of tokens, you could lose 10% of your deposit. Even worse, if you deposit <1 share worth of tokens, then you get 0 shares, and you basically made a donation.
When depositing tokens, the number of shares a user gets is rounded towards zero. This rounding takes away value from the user in favor or the vault (i.e. in favor of all the current share holders). This rounding is often negligible because of the amount at stake. If you deposit 1e9 shares worth of tokens, the rounding will have you lose at most 0.0000001% of your deposit. However if you deposit 10 shares worth of tokens, you could lose 10% of your deposit. Even worse, if you deposit <1 share worth of tokens, then you get 0 shares, and you basically made a donation.

For a given amount of assets, the more shares you receive the safer you are. If you want to limit your losses to at most 1%, you need to receive at least 100 shares.

Expand Down
2 changes: 1 addition & 1 deletion test/helpers/enums.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ module.exports = {
Enum,
ProposalState: Enum('Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'),
VoteType: Enum('Against', 'For', 'Abstain'),
Rounding: Enum('Down', 'Up', 'Zero'),
Rounding: Enum('Floor', 'Ceil', 'Trunc', 'Expand'),
OperationState: Enum('Unset', 'Waiting', 'Ready', 'Done'),
};
6 changes: 3 additions & 3 deletions test/token/ERC20/extensions/ERC4626.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -965,8 +965,8 @@ contract('ERC4626', function (accounts) {
}

// 5. Bob mints 2000 shares (costs 3001 assets)
// NOTE: Bob's assets spent got rounded up
// NOTE: Alices's vault assets got rounded up
// NOTE: Bob's assets spent got rounded towards infinity
// NOTE: Alices's vault assets got rounded towards infinity
{
const { tx } = await this.vault.mint(2000, user2, { from: user2 });
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
Expand Down Expand Up @@ -1056,7 +1056,7 @@ contract('ERC4626', function (accounts) {
}

// 9. Alice withdraws 3643 assets (2000 shares)
// NOTE: Bob's assets have been rounded back up
// NOTE: Bob's assets have been rounded back towards infinity
{
const { tx } = await this.vault.withdraw(3643, user1, user1, { from: user1 });
await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
Expand Down
16 changes: 8 additions & 8 deletions test/utils/math/Math.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ contract MathTest is Test {

// square of result is bigger than input
if (_squareBigger(result, input)) {
assertTrue(rounding == Math.Rounding.Up);
assertTrue(Math.unsignedRoundsUp(rounding));
assertTrue(_squareSmaller(result - 1, input));
}
// square of result is smaller than input
else if (_squareSmaller(result, input)) {
assertFalse(rounding == Math.Rounding.Up);
assertFalse(Math.unsignedRoundsUp(rounding));
assertTrue(_squareBigger(result + 1, input));
}
// input is perfect square
Expand All @@ -63,10 +63,10 @@ contract MathTest is Test {
if (input == 0) {
assertEq(result, 0);
} else if (_powerOf2Bigger(result, input)) {
assertTrue(rounding == Math.Rounding.Up);
assertTrue(Math.unsignedRoundsUp(rounding));
assertTrue(_powerOf2Smaller(result - 1, input));
} else if (_powerOf2Smaller(result, input)) {
assertFalse(rounding == Math.Rounding.Up);
assertFalse(Math.unsignedRoundsUp(rounding));
assertTrue(_powerOf2Bigger(result + 1, input));
} else {
assertEq(2 ** result, input);
Expand All @@ -90,10 +90,10 @@ contract MathTest is Test {
if (input == 0) {
assertEq(result, 0);
} else if (_powerOf10Bigger(result, input)) {
assertTrue(rounding == Math.Rounding.Up);
assertTrue(Math.unsignedRoundsUp(rounding));
assertTrue(_powerOf10Smaller(result - 1, input));
} else if (_powerOf10Smaller(result, input)) {
assertFalse(rounding == Math.Rounding.Up);
assertFalse(Math.unsignedRoundsUp(rounding));
assertTrue(_powerOf10Bigger(result + 1, input));
} else {
assertEq(10 ** result, input);
Expand All @@ -117,10 +117,10 @@ contract MathTest is Test {
if (input == 0) {
assertEq(result, 0);
} else if (_powerOf256Bigger(result, input)) {
assertTrue(rounding == Math.Rounding.Up);
assertTrue(Math.unsignedRoundsUp(rounding));
assertTrue(_powerOf256Smaller(result - 1, input));
} else if (_powerOf256Smaller(result, input)) {
assertFalse(rounding == Math.Rounding.Up);
assertFalse(Math.unsignedRoundsUp(rounding));
assertTrue(_powerOf256Bigger(result + 1, input));
} else {
assertEq(256 ** result, input);
Expand Down
Loading

0 comments on commit 84db204

Please sign in to comment.