Skip to content

Commit

Permalink
Add SignedMath with math utilities for signed integers (OpenZeppelin#…
Browse files Browse the repository at this point in the history
…2686)

* add contract and tests

* avoid implicit cast

* add test cases

* fix test names

* modify avarage and add tests

* improve signed average formula

* fix lint

* better average formula

* refactor signed average testing

* add doc and changelog entry

* Update contracts/utils/math/SignedMath.sol

Co-authored-by: Francisco Giordano <frangio.1@gmail.com>

* remove ceilDiv

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
  • Loading branch information
3 people authored Jan 12, 2022
1 parent dee772a commit 3458c1e
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* `ERC20`: reduce allowance before triggering transfer. ([#3056](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/#3056))
* `ERC20`: do not update allowance on `transferFrom` when allowance is `type(uint256).max`. ([#3085](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/#3085))
* `ERC777`: do not update allowance on `transferFrom` when allowance is `type(uint256).max`. ([#3085](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/#3085))
* `SignedMath`: a new signed version of the Math library with `max`, `min`, and `average`. ([#2686](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2686))

### Breaking change

Expand Down
19 changes: 19 additions & 0 deletions contracts/mocks/SignedMathMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../utils/math/SignedMath.sol";

contract SignedMathMock {
function max(int256 a, int256 b) public pure returns (int256) {
return SignedMath.max(a, b);
}

function min(int256 a, int256 b) public pure returns (int256) {
return SignedMath.min(a, b);
}

function average(int256 a, int256 b) public pure returns (int256) {
return SignedMath.average(a, b);
}
}
2 changes: 2 additions & 0 deletions contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Finally, {Create2} contains all necessary utilities to safely use the https://bl

{{Math}}

{{SignedMath}}

{{SafeCast}}

{{SafeMath}}
Expand Down
32 changes: 32 additions & 0 deletions contracts/utils/math/SignedMath.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
* @dev Standard signed math utilities missing in the Solidity language.
*/
library SignedMath {
/**
* @dev Returns the largest of two signed numbers.
*/
function max(int256 a, int256 b) internal pure returns (int256) {
return a >= b ? a : b;
}

/**
* @dev Returns the smallest of two signed numbers.
*/
function min(int256 a, int256 b) internal pure returns (int256) {
return a < b ? a : b;
}

/**
* @dev Returns the average of two signed numbers without overflow.
* The result is rounded towards zero.
*/
function average(int256 a, int256 b) internal pure returns (int256) {
// Formula from the book "Hacker's Delight"
int256 x = (a & b) + ((a ^ b) >> 1);
return x + (int256(uint256(x) >> 255) & (a ^ b));
}
}
77 changes: 77 additions & 0 deletions test/utils/math/SignedMath.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const { BN, constants } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const { MIN_INT256, MAX_INT256 } = constants;

const SignedMathMock = artifacts.require('SignedMathMock');

contract('SignedMath', function (accounts) {
const min = new BN('-1234');
const max = new BN('5678');

beforeEach(async function () {
this.math = await SignedMathMock.new();
});

describe('max', function () {
it('is correctly detected in first argument position', async function () {
expect(await this.math.max(max, min)).to.be.bignumber.equal(max);
});

it('is correctly detected in second argument position', async function () {
expect(await this.math.max(min, max)).to.be.bignumber.equal(max);
});
});

describe('min', function () {
it('is correctly detected in first argument position', async function () {
expect(await this.math.min(min, max)).to.be.bignumber.equal(min);
});

it('is correctly detected in second argument position', async function () {
expect(await this.math.min(max, min)).to.be.bignumber.equal(min);
});
});

describe('average', function () {
function bnAverage (a, b) {
return a.add(b).divn(2);
}

it('is correctly calculated with various input', async function () {
const valuesX = [
new BN('0'),
new BN('3'),
new BN('-3'),
new BN('4'),
new BN('-4'),
new BN('57417'),
new BN('-57417'),
new BN('42304'),
new BN('-42304'),
MIN_INT256,
MAX_INT256,
];

const valuesY = [
new BN('0'),
new BN('5'),
new BN('-5'),
new BN('2'),
new BN('-2'),
new BN('57417'),
new BN('-57417'),
new BN('42304'),
new BN('-42304'),
MIN_INT256,
MAX_INT256,
];

for (const x of valuesX) {
for (const y of valuesY) {
expect(await this.math.average(x, y))
.to.be.bignumber.equal(bnAverage(x, y), `Bad result for average(${x}, ${y})`);
}
}
});
});
});

0 comments on commit 3458c1e

Please sign in to comment.