Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename rounding modes and complete with fourth #4455

Merged
merged 6 commits into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
34 changes: 20 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,11 @@ 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);
}
}

function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) {
frangio marked this conversation as resolved.
Show resolved Hide resolved
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