Add SignedMath with math utilities for signed integers (#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>
This commit is contained in:
rotcivegaf
2022-01-12 16:08:59 -03:00
committed by GitHub
parent dee772a55f
commit 3458c1e854
5 changed files with 131 additions and 0 deletions

View File

@ -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

View File

@ -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);
}
}

View File

@ -27,6 +27,8 @@ Finally, {Create2} contains all necessary utilities to safely use the https://bl
{{Math}}
{{SignedMath}}
{{SafeCast}}
{{SafeMath}}

View File

@ -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));
}
}

View File

@ -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})`);
}
}
});
});
});