From 7c754d066507ef2b1f1901289355dc2a1910d85a Mon Sep 17 00:00:00 2001 From: Nicholas Rodrigues Lordello Date: Thu, 20 May 2021 15:53:31 +0200 Subject: [PATCH] Add ceiling division operation to the `Math.sol` library (#2681) Co-authored-by: Hadrien Croubois Co-authored-by: Francisco Giordano --- CHANGELOG.md | 3 ++- contracts/mocks/MathMock.sol | 4 ++++ contracts/utils/math/Math.sol | 11 +++++++++++ test/utils/math/Math.test.js | 29 +++++++++++++++++++++++++++-- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 209e27507..3b034767f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,12 @@ ## Unreleased -* `ERC20Votes`: add a new extension of the `ERC20` token with support for voting snapshots and delegation. This extension is compatible with Compound's `Comp` token interface. ([#2632](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2632)) + * `ERC20Votes`: add a new extension of the `ERC20` token with support for voting snapshots and delegation. This extension is compatible with Compound's `Comp` token interface. ([#2632](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2632)) * Enumerables: Improve gas cost of removal in `EnumerableSet` and `EnumerableMap`. * Enumerables: Improve gas cost of lookup in `EnumerableSet` and `EnumerableMap`. * `Counter`: add a reset method. ([#2678](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2678)) * Tokens: Wrap definitely safe subtractions in `unchecked` blocks. + * `Math`: Add a `ceilDiv` method for performing ceiling division. ## 4.1.0 (2021-04-29) diff --git a/contracts/mocks/MathMock.sol b/contracts/mocks/MathMock.sol index 34bf6b6d4..c651b6bb1 100644 --- a/contracts/mocks/MathMock.sol +++ b/contracts/mocks/MathMock.sol @@ -16,4 +16,8 @@ contract MathMock { function average(uint256 a, uint256 b) public pure returns (uint256) { return Math.average(a, b); } + + function ceilDiv(uint256 a, uint256 b) public pure returns (uint256) { + return Math.ceilDiv(a, b); + } } diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index e7023c40c..3979e0e1e 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -28,4 +28,15 @@ library Math { // (a + b) / 2 can overflow, so we distribute. return (a / 2) + (b / 2) + ((a % 2 + b % 2) / 2); } + + /** + * @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. + */ + function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b - 1) / b can overflow on addition, so we distribute. + return a / b + (a % b == 0 ? 0 : 1); + } } diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 4cf9de7ce..31d3e17ac 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -1,6 +1,6 @@ -const { BN } = require('@openzeppelin/test-helpers'); - +const { BN, constants } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { MAX_UINT256 } = constants; const MathMock = artifacts.require('MathMock'); @@ -55,4 +55,29 @@ contract('Math', function (accounts) { expect(await this.math.average(a, b)).to.be.bignumber.equal(bnAverage(a, b)); }); }); + + describe('ceilDiv', function () { + it('does not round up on exact division', async function () { + const a = new BN('10'); + const b = new BN('5'); + expect(await this.math.ceilDiv(a, b)).to.be.bignumber.equal('2'); + }); + + it('rounds up on division with remainders', async function () { + const a = new BN('42'); + const b = new BN('13'); + expect(await this.math.ceilDiv(a, b)).to.be.bignumber.equal('4'); + }); + + it('does not overflow', async function () { + const b = new BN('2'); + const result = new BN('1').shln(255); + expect(await this.math.ceilDiv(MAX_UINT256, b)).to.be.bignumber.equal(result); + }); + + it('correctly computes max uint256 divided by 1', async function () { + const b = new BN('1'); + expect(await this.math.ceilDiv(MAX_UINT256, b)).to.be.bignumber.equal(MAX_UINT256); + }); + }); });