Fix .gitignore: stop tracking ignored files

This commit is contained in:
5t4l1n
2025-07-27 10:39:02 +05:30
parent b42747e9a3
commit 3a87ef0576
625 changed files with 88566 additions and 63 deletions
@@ -0,0 +1,361 @@
const { balance, constants, ether, expectRevert, send, expectEvent } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const Address = artifacts.require('$Address');
const EtherReceiver = artifacts.require('EtherReceiverMock');
const CallReceiverMock = artifacts.require('CallReceiverMock');
contract('Address', function (accounts) {
const [recipient, other] = accounts;
beforeEach(async function () {
this.mock = await Address.new();
});
describe('isContract', function () {
it('returns false for account address', async function () {
expect(await this.mock.$isContract(other)).to.equal(false);
});
it('returns true for contract address', async function () {
expect(await this.mock.$isContract(this.mock.address)).to.equal(true);
});
});
describe('sendValue', function () {
beforeEach(async function () {
this.recipientTracker = await balance.tracker(recipient);
});
context('when sender contract has no funds', function () {
it('sends 0 wei', async function () {
await this.mock.$sendValue(other, 0);
expect(await this.recipientTracker.delta()).to.be.bignumber.equal('0');
});
it('reverts when sending non-zero amounts', async function () {
await expectRevert(this.mock.$sendValue(other, 1), 'Address: insufficient balance');
});
});
context('when sender contract has funds', function () {
const funds = ether('1');
beforeEach(async function () {
await send.ether(other, this.mock.address, funds);
});
it('sends 0 wei', async function () {
await this.mock.$sendValue(recipient, 0);
expect(await this.recipientTracker.delta()).to.be.bignumber.equal('0');
});
it('sends non-zero amounts', async function () {
await this.mock.$sendValue(recipient, funds.subn(1));
expect(await this.recipientTracker.delta()).to.be.bignumber.equal(funds.subn(1));
});
it('sends the whole balance', async function () {
await this.mock.$sendValue(recipient, funds);
expect(await this.recipientTracker.delta()).to.be.bignumber.equal(funds);
expect(await balance.current(this.mock.address)).to.be.bignumber.equal('0');
});
it('reverts when sending more than the balance', async function () {
await expectRevert(this.mock.$sendValue(recipient, funds.addn(1)), 'Address: insufficient balance');
});
context('with contract recipient', function () {
beforeEach(async function () {
this.target = await EtherReceiver.new();
});
it('sends funds', async function () {
const tracker = await balance.tracker(this.target.address);
await this.target.setAcceptEther(true);
await this.mock.$sendValue(this.target.address, funds);
expect(await tracker.delta()).to.be.bignumber.equal(funds);
});
it('reverts on recipient revert', async function () {
await this.target.setAcceptEther(false);
await expectRevert(
this.mock.$sendValue(this.target.address, funds),
'Address: unable to send value, recipient may have reverted',
);
});
});
});
});
describe('functionCall', function () {
beforeEach(async function () {
this.target = await CallReceiverMock.new();
});
context('with valid contract receiver', function () {
it('calls the requested function', async function () {
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
const receipt = await this.mock.$functionCall(this.target.address, abiEncodedCall);
expectEvent(receipt, 'return$functionCall_address_bytes', {
ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']),
});
await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled');
});
it('calls the requested empty return function', async function () {
const abiEncodedCall = this.target.contract.methods.mockFunctionEmptyReturn().encodeABI();
const receipt = await this.mock.$functionCall(this.target.address, abiEncodedCall);
await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled');
});
it('reverts when the called function reverts with no reason', async function () {
const abiEncodedCall = this.target.contract.methods.mockFunctionRevertsNoReason().encodeABI();
await expectRevert(
this.mock.$functionCall(this.target.address, abiEncodedCall),
'Address: low-level call failed',
);
});
it('reverts when the called function reverts, bubbling up the revert reason', async function () {
const abiEncodedCall = this.target.contract.methods.mockFunctionRevertsReason().encodeABI();
await expectRevert(this.mock.$functionCall(this.target.address, abiEncodedCall), 'CallReceiverMock: reverting');
});
it('reverts when the called function runs out of gas', async function () {
const abiEncodedCall = this.target.contract.methods.mockFunctionOutOfGas().encodeABI();
await expectRevert(
this.mock.$functionCall(this.target.address, abiEncodedCall, { gas: '120000' }),
'Address: low-level call failed',
);
});
it('reverts when the called function throws', async function () {
const abiEncodedCall = this.target.contract.methods.mockFunctionThrows().encodeABI();
await expectRevert.unspecified(this.mock.$functionCall(this.target.address, abiEncodedCall));
});
it('bubbles up error message if specified', async function () {
const errorMsg = 'Address: expected error';
await expectRevert(this.mock.$functionCall(this.target.address, '0x12345678', errorMsg), errorMsg);
});
it('reverts when function does not exist', async function () {
const abiEncodedCall = web3.eth.abi.encodeFunctionCall(
{
name: 'mockFunctionDoesNotExist',
type: 'function',
inputs: [],
},
[],
);
await expectRevert(
this.mock.$functionCall(this.target.address, abiEncodedCall),
'Address: low-level call failed',
);
});
});
context('with non-contract receiver', function () {
it('reverts when address is not a contract', async function () {
const [recipient] = accounts;
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
await expectRevert(this.mock.$functionCall(recipient, abiEncodedCall), 'Address: call to non-contract');
});
});
});
describe('functionCallWithValue', function () {
beforeEach(async function () {
this.target = await CallReceiverMock.new();
});
context('with zero value', function () {
it('calls the requested function', async function () {
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
const receipt = await this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, 0);
expectEvent(receipt, 'return$functionCallWithValue_address_bytes_uint256', {
ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']),
});
await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled');
});
});
context('with non-zero value', function () {
const amount = ether('1.2');
it('reverts if insufficient sender balance', async function () {
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
await expectRevert(
this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount),
'Address: insufficient balance for call',
);
});
it('calls the requested function with existing value', async function () {
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
const tracker = await balance.tracker(this.target.address);
await send.ether(other, this.mock.address, amount);
const receipt = await this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount);
expectEvent(receipt, 'return$functionCallWithValue_address_bytes_uint256', {
ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']),
});
await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled');
expect(await tracker.delta()).to.be.bignumber.equal(amount);
});
it('calls the requested function with transaction funds', async function () {
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
const tracker = await balance.tracker(this.target.address);
expect(await balance.current(this.mock.address)).to.be.bignumber.equal('0');
const receipt = await this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount, {
from: other,
value: amount,
});
expectEvent(receipt, 'return$functionCallWithValue_address_bytes_uint256', {
ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']),
});
await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled');
expect(await tracker.delta()).to.be.bignumber.equal(amount);
});
it('reverts when calling non-payable functions', async function () {
const abiEncodedCall = this.target.contract.methods.mockFunctionNonPayable().encodeABI();
await send.ether(other, this.mock.address, amount);
await expectRevert(
this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount),
'Address: low-level call with value failed',
);
});
it('bubbles up error message if specified', async function () {
const errorMsg = 'Address: expected error';
await expectRevert(this.mock.$functionCallWithValue(this.target.address, '0x12345678', 0, errorMsg), errorMsg);
});
});
});
describe('functionStaticCall', function () {
beforeEach(async function () {
this.target = await CallReceiverMock.new();
});
it('calls the requested function', async function () {
const abiEncodedCall = this.target.contract.methods.mockStaticFunction().encodeABI();
expect(await this.mock.$functionStaticCall(this.target.address, abiEncodedCall)).to.be.equal(
web3.eth.abi.encodeParameters(['string'], ['0x1234']),
);
});
it('reverts on a non-static function', async function () {
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
await expectRevert(
this.mock.$functionStaticCall(this.target.address, abiEncodedCall),
'Address: low-level static call failed',
);
});
it('bubbles up revert reason', async function () {
const abiEncodedCall = this.target.contract.methods.mockFunctionRevertsReason().encodeABI();
await expectRevert(
this.mock.$functionStaticCall(this.target.address, abiEncodedCall),
'CallReceiverMock: reverting',
);
});
it('reverts when address is not a contract', async function () {
const [recipient] = accounts;
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
await expectRevert(this.mock.$functionStaticCall(recipient, abiEncodedCall), 'Address: call to non-contract');
});
it('bubbles up error message if specified', async function () {
const errorMsg = 'Address: expected error';
await expectRevert(this.mock.$functionCallWithValue(this.target.address, '0x12345678', 0, errorMsg), errorMsg);
});
});
describe('functionDelegateCall', function () {
beforeEach(async function () {
this.target = await CallReceiverMock.new();
});
it('delegate calls the requested function', async function () {
// pseudorandom values
const slot = '0x93e4c53af435ddf777c3de84bb9a953a777788500e229a468ea1036496ab66a0';
const value = '0x6a465d1c49869f71fb65562bcbd7e08c8044074927f0297127203f2a9924ff5b';
const abiEncodedCall = this.target.contract.methods.mockFunctionWritesStorage(slot, value).encodeABI();
expect(await web3.eth.getStorageAt(this.mock.address, slot)).to.be.equal(constants.ZERO_BYTES32);
expectEvent(
await this.mock.$functionDelegateCall(this.target.address, abiEncodedCall),
'return$functionDelegateCall_address_bytes',
{ ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']) },
);
expect(await web3.eth.getStorageAt(this.mock.address, slot)).to.be.equal(value);
});
it('bubbles up revert reason', async function () {
const abiEncodedCall = this.target.contract.methods.mockFunctionRevertsReason().encodeABI();
await expectRevert(
this.mock.$functionDelegateCall(this.target.address, abiEncodedCall),
'CallReceiverMock: reverting',
);
});
it('reverts when address is not a contract', async function () {
const [recipient] = accounts;
const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI();
await expectRevert(this.mock.$functionDelegateCall(recipient, abiEncodedCall), 'Address: call to non-contract');
});
it('bubbles up error message if specified', async function () {
const errorMsg = 'Address: expected error';
await expectRevert(this.mock.$functionCallWithValue(this.target.address, '0x12345678', 0, errorMsg), errorMsg);
});
});
describe('verifyCallResult', function () {
it('returns returndata on success', async function () {
const returndata = '0x123abc';
expect(await this.mock.$verifyCallResult(true, returndata, '')).to.equal(returndata);
});
it('reverts with return data and error m', async function () {
const errorMsg = 'Address: expected error';
await expectRevert(this.mock.$verifyCallResult(false, '0x', errorMsg), errorMsg);
});
});
});
@@ -0,0 +1,123 @@
require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const AddressArraysMock = artifacts.require('AddressArraysMock');
const Bytes32ArraysMock = artifacts.require('Bytes32ArraysMock');
const Uint256ArraysMock = artifacts.require('Uint256ArraysMock');
contract('Arrays', function () {
describe('findUpperBound', function () {
context('Even number of elements', function () {
const EVEN_ELEMENTS_ARRAY = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
beforeEach(async function () {
this.arrays = await Uint256ArraysMock.new(EVEN_ELEMENTS_ARRAY);
});
it('returns correct index for the basic case', async function () {
expect(await this.arrays.findUpperBound(16)).to.be.bignumber.equal('5');
});
it('returns 0 for the first element', async function () {
expect(await this.arrays.findUpperBound(11)).to.be.bignumber.equal('0');
});
it('returns index of the last element', async function () {
expect(await this.arrays.findUpperBound(20)).to.be.bignumber.equal('9');
});
it('returns first index after last element if searched value is over the upper boundary', async function () {
expect(await this.arrays.findUpperBound(32)).to.be.bignumber.equal('10');
});
it('returns 0 for the element under the lower boundary', async function () {
expect(await this.arrays.findUpperBound(2)).to.be.bignumber.equal('0');
});
});
context('Odd number of elements', function () {
const ODD_ELEMENTS_ARRAY = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21];
beforeEach(async function () {
this.arrays = await Uint256ArraysMock.new(ODD_ELEMENTS_ARRAY);
});
it('returns correct index for the basic case', async function () {
expect(await this.arrays.findUpperBound(16)).to.be.bignumber.equal('5');
});
it('returns 0 for the first element', async function () {
expect(await this.arrays.findUpperBound(11)).to.be.bignumber.equal('0');
});
it('returns index of the last element', async function () {
expect(await this.arrays.findUpperBound(21)).to.be.bignumber.equal('10');
});
it('returns first index after last element if searched value is over the upper boundary', async function () {
expect(await this.arrays.findUpperBound(32)).to.be.bignumber.equal('11');
});
it('returns 0 for the element under the lower boundary', async function () {
expect(await this.arrays.findUpperBound(2)).to.be.bignumber.equal('0');
});
});
context('Array with gap', function () {
const WITH_GAP_ARRAY = [11, 12, 13, 14, 15, 20, 21, 22, 23, 24];
beforeEach(async function () {
this.arrays = await Uint256ArraysMock.new(WITH_GAP_ARRAY);
});
it('returns index of first element in next filled range', async function () {
expect(await this.arrays.findUpperBound(17)).to.be.bignumber.equal('5');
});
});
context('Empty array', function () {
beforeEach(async function () {
this.arrays = await Uint256ArraysMock.new([]);
});
it('always returns 0 for empty array', async function () {
expect(await this.arrays.findUpperBound(10)).to.be.bignumber.equal('0');
});
});
});
describe('unsafeAccess', function () {
for (const { type, artifact, elements } of [
{
type: 'address',
artifact: AddressArraysMock,
elements: Array(10)
.fill()
.map(() => web3.utils.randomHex(20)),
},
{
type: 'bytes32',
artifact: Bytes32ArraysMock,
elements: Array(10)
.fill()
.map(() => web3.utils.randomHex(32)),
},
{
type: 'uint256',
artifact: Uint256ArraysMock,
elements: Array(10)
.fill()
.map(() => web3.utils.randomHex(32)),
},
]) {
it(type, async function () {
const contract = await artifact.new(elements);
for (const i in elements) {
expect(await contract.unsafeAccess(i)).to.be.bignumber.equal(elements[i]);
}
});
}
});
});
@@ -0,0 +1,33 @@
const { expect } = require('chai');
const Base64 = artifacts.require('$Base64');
contract('Strings', function () {
beforeEach(async function () {
this.base64 = await Base64.new();
});
describe('from bytes - base64', function () {
it('converts to base64 encoded string with double padding', async function () {
const TEST_MESSAGE = 'test';
const input = web3.utils.asciiToHex(TEST_MESSAGE);
expect(await this.base64.$encode(input)).to.equal('dGVzdA==');
});
it('converts to base64 encoded string with single padding', async function () {
const TEST_MESSAGE = 'test1';
const input = web3.utils.asciiToHex(TEST_MESSAGE);
expect(await this.base64.$encode(input)).to.equal('dGVzdDE=');
});
it('converts to base64 encoded string without padding', async function () {
const TEST_MESSAGE = 'test12';
const input = web3.utils.asciiToHex(TEST_MESSAGE);
expect(await this.base64.$encode(input)).to.equal('dGVzdDEy');
});
it('empty bytes', async function () {
expect(await this.base64.$encode([])).to.equal('');
});
});
});
@@ -0,0 +1,347 @@
// SPDX-License-Identifier: MIT
// This file was procedurally generated from scripts/generate/templates/Checkpoints.t.js.
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../../contracts/utils/Checkpoints.sol";
import "../../contracts/utils/math/SafeCast.sol";
contract CheckpointsHistoryTest is Test {
using Checkpoints for Checkpoints.History;
// Maximum gap between keys used during the fuzzing tests: the `_prepareKeys` function with make sure that
// key#n+1 is in the [key#n, key#n + _KEY_MAX_GAP] range.
uint8 internal constant _KEY_MAX_GAP = 64;
Checkpoints.History internal _ckpts;
// helpers
function _boundUint32(uint32 x, uint32 min, uint32 max) internal view returns (uint32) {
return SafeCast.toUint32(bound(uint256(x), uint256(min), uint256(max)));
}
function _prepareKeys(uint32[] memory keys, uint32 maxSpread) internal view {
uint32 lastKey = 0;
for (uint256 i = 0; i < keys.length; ++i) {
uint32 key = _boundUint32(keys[i], lastKey, lastKey + maxSpread);
keys[i] = key;
lastKey = key;
}
}
function _assertLatestCheckpoint(bool exist, uint32 key, uint224 value) internal {
(bool _exist, uint32 _key, uint224 _value) = _ckpts.latestCheckpoint();
assertEq(_exist, exist);
assertEq(_key, key);
assertEq(_value, value);
}
// tests
function testPush(uint32[] memory keys, uint224[] memory values, uint32 pastKey) public {
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
// initial state
assertEq(_ckpts.length(), 0);
assertEq(_ckpts.latest(), 0);
_assertLatestCheckpoint(false, 0, 0);
uint256 duplicates = 0;
for (uint256 i = 0; i < keys.length; ++i) {
uint32 key = keys[i];
uint224 value = values[i % values.length];
if (i > 0 && key == keys[i - 1]) ++duplicates;
// push
vm.roll(key);
_ckpts.push(value);
// check length & latest
assertEq(_ckpts.length(), i + 1 - duplicates);
assertEq(_ckpts.latest(), value);
_assertLatestCheckpoint(true, key, value);
}
// Can't push any key in the past
if (keys.length > 0) {
uint32 lastKey = keys[keys.length - 1];
if (lastKey > 0) {
pastKey = _boundUint32(pastKey, 0, lastKey - 1);
vm.roll(pastKey);
vm.expectRevert();
this.push(values[keys.length % values.length]);
}
}
}
// used to test reverts
function push(uint224 value) external {
_ckpts.push(value);
}
function testLookup(uint32[] memory keys, uint224[] memory values, uint32 lookup) public {
vm.assume(keys.length > 0);
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
uint32 lastKey = keys[keys.length - 1];
vm.assume(lastKey > 0);
lookup = _boundUint32(lookup, 0, lastKey - 1);
uint224 upper = 0;
for (uint256 i = 0; i < keys.length; ++i) {
uint32 key = keys[i];
uint224 value = values[i % values.length];
// push
vm.roll(key);
_ckpts.push(value);
// track expected result of lookups
if (key <= lookup) {
upper = value;
}
}
// check lookup
assertEq(_ckpts.getAtBlock(lookup), upper);
assertEq(_ckpts.getAtProbablyRecentBlock(lookup), upper);
vm.expectRevert();
this.getAtBlock(lastKey);
vm.expectRevert();
this.getAtBlock(lastKey + 1);
vm.expectRevert();
this.getAtProbablyRecentBlock(lastKey);
vm.expectRevert();
this.getAtProbablyRecentBlock(lastKey + 1);
}
// used to test reverts
function getAtBlock(uint32 key) external view {
_ckpts.getAtBlock(key);
}
// used to test reverts
function getAtProbablyRecentBlock(uint32 key) external view {
_ckpts.getAtProbablyRecentBlock(key);
}
}
contract CheckpointsTrace224Test is Test {
using Checkpoints for Checkpoints.Trace224;
// Maximum gap between keys used during the fuzzing tests: the `_prepareKeys` function with make sure that
// key#n+1 is in the [key#n, key#n + _KEY_MAX_GAP] range.
uint8 internal constant _KEY_MAX_GAP = 64;
Checkpoints.Trace224 internal _ckpts;
// helpers
function _boundUint32(uint32 x, uint32 min, uint32 max) internal view returns (uint32) {
return SafeCast.toUint32(bound(uint256(x), uint256(min), uint256(max)));
}
function _prepareKeys(uint32[] memory keys, uint32 maxSpread) internal view {
uint32 lastKey = 0;
for (uint256 i = 0; i < keys.length; ++i) {
uint32 key = _boundUint32(keys[i], lastKey, lastKey + maxSpread);
keys[i] = key;
lastKey = key;
}
}
function _assertLatestCheckpoint(bool exist, uint32 key, uint224 value) internal {
(bool _exist, uint32 _key, uint224 _value) = _ckpts.latestCheckpoint();
assertEq(_exist, exist);
assertEq(_key, key);
assertEq(_value, value);
}
// tests
function testPush(uint32[] memory keys, uint224[] memory values, uint32 pastKey) public {
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
// initial state
assertEq(_ckpts.length(), 0);
assertEq(_ckpts.latest(), 0);
_assertLatestCheckpoint(false, 0, 0);
uint256 duplicates = 0;
for (uint256 i = 0; i < keys.length; ++i) {
uint32 key = keys[i];
uint224 value = values[i % values.length];
if (i > 0 && key == keys[i - 1]) ++duplicates;
// push
_ckpts.push(key, value);
// check length & latest
assertEq(_ckpts.length(), i + 1 - duplicates);
assertEq(_ckpts.latest(), value);
_assertLatestCheckpoint(true, key, value);
}
if (keys.length > 0) {
uint32 lastKey = keys[keys.length - 1];
if (lastKey > 0) {
pastKey = _boundUint32(pastKey, 0, lastKey - 1);
vm.expectRevert();
this.push(pastKey, values[keys.length % values.length]);
}
}
}
// used to test reverts
function push(uint32 key, uint224 value) external {
_ckpts.push(key, value);
}
function testLookup(uint32[] memory keys, uint224[] memory values, uint32 lookup) public {
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
uint32 lastKey = keys.length == 0 ? 0 : keys[keys.length - 1];
lookup = _boundUint32(lookup, 0, lastKey + _KEY_MAX_GAP);
uint224 upper = 0;
uint224 lower = 0;
uint32 lowerKey = type(uint32).max;
for (uint256 i = 0; i < keys.length; ++i) {
uint32 key = keys[i];
uint224 value = values[i % values.length];
// push
_ckpts.push(key, value);
// track expected result of lookups
if (key <= lookup) {
upper = value;
}
// find the first key that is not smaller than the lookup key
if (key >= lookup && (i == 0 || keys[i - 1] < lookup)) {
lowerKey = key;
}
if (key == lowerKey) {
lower = value;
}
}
// check lookup
assertEq(_ckpts.lowerLookup(lookup), lower);
assertEq(_ckpts.upperLookup(lookup), upper);
assertEq(_ckpts.upperLookupRecent(lookup), upper);
}
}
contract CheckpointsTrace160Test is Test {
using Checkpoints for Checkpoints.Trace160;
// Maximum gap between keys used during the fuzzing tests: the `_prepareKeys` function with make sure that
// key#n+1 is in the [key#n, key#n + _KEY_MAX_GAP] range.
uint8 internal constant _KEY_MAX_GAP = 64;
Checkpoints.Trace160 internal _ckpts;
// helpers
function _boundUint96(uint96 x, uint96 min, uint96 max) internal view returns (uint96) {
return SafeCast.toUint96(bound(uint256(x), uint256(min), uint256(max)));
}
function _prepareKeys(uint96[] memory keys, uint96 maxSpread) internal view {
uint96 lastKey = 0;
for (uint256 i = 0; i < keys.length; ++i) {
uint96 key = _boundUint96(keys[i], lastKey, lastKey + maxSpread);
keys[i] = key;
lastKey = key;
}
}
function _assertLatestCheckpoint(bool exist, uint96 key, uint160 value) internal {
(bool _exist, uint96 _key, uint160 _value) = _ckpts.latestCheckpoint();
assertEq(_exist, exist);
assertEq(_key, key);
assertEq(_value, value);
}
// tests
function testPush(uint96[] memory keys, uint160[] memory values, uint96 pastKey) public {
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
// initial state
assertEq(_ckpts.length(), 0);
assertEq(_ckpts.latest(), 0);
_assertLatestCheckpoint(false, 0, 0);
uint256 duplicates = 0;
for (uint256 i = 0; i < keys.length; ++i) {
uint96 key = keys[i];
uint160 value = values[i % values.length];
if (i > 0 && key == keys[i - 1]) ++duplicates;
// push
_ckpts.push(key, value);
// check length & latest
assertEq(_ckpts.length(), i + 1 - duplicates);
assertEq(_ckpts.latest(), value);
_assertLatestCheckpoint(true, key, value);
}
if (keys.length > 0) {
uint96 lastKey = keys[keys.length - 1];
if (lastKey > 0) {
pastKey = _boundUint96(pastKey, 0, lastKey - 1);
vm.expectRevert();
this.push(pastKey, values[keys.length % values.length]);
}
}
}
// used to test reverts
function push(uint96 key, uint160 value) external {
_ckpts.push(key, value);
}
function testLookup(uint96[] memory keys, uint160[] memory values, uint96 lookup) public {
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
uint96 lastKey = keys.length == 0 ? 0 : keys[keys.length - 1];
lookup = _boundUint96(lookup, 0, lastKey + _KEY_MAX_GAP);
uint160 upper = 0;
uint160 lower = 0;
uint96 lowerKey = type(uint96).max;
for (uint256 i = 0; i < keys.length; ++i) {
uint96 key = keys[i];
uint160 value = values[i % values.length];
// push
_ckpts.push(key, value);
// track expected result of lookups
if (key <= lookup) {
upper = value;
}
// find the first key that is not smaller than the lookup key
if (key >= lookup && (i == 0 || keys[i - 1] < lookup)) {
lowerKey = key;
}
if (key == lowerKey) {
lower = value;
}
}
// check lookup
assertEq(_ckpts.lowerLookup(lookup), lower);
assertEq(_ckpts.upperLookup(lookup), upper);
assertEq(_ckpts.upperLookupRecent(lookup), upper);
}
}
@@ -0,0 +1,255 @@
const { expectRevert, time } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const { batchInBlock } = require('../helpers/txpool');
const $Checkpoints = artifacts.require('$Checkpoints');
// The library name may be 'Checkpoints' or 'CheckpointsUpgradeable'
const libraryName = $Checkpoints._json.contractName.replace(/^\$/, '');
const traceLengths = [160, 224];
const first = array => (array.length ? array[0] : undefined);
const last = array => (array.length ? array[array.length - 1] : undefined);
contract('Checkpoints', function () {
beforeEach(async function () {
this.mock = await $Checkpoints.new();
this.methods = { trace: {} };
this.methods.history = {
latest: (...args) => this.mock.methods[`$latest_${libraryName}_History(uint256)`](0, ...args),
latestCheckpoint: (...args) => this.mock.methods[`$latestCheckpoint_${libraryName}_History(uint256)`](0, ...args),
length: (...args) => this.mock.methods[`$length_${libraryName}_History(uint256)`](0, ...args),
push: (...args) => this.mock.methods['$push(uint256,uint256)'](0, ...args),
getAtBlock: (...args) => this.mock.$getAtBlock(0, ...args),
getAtRecentBlock: (...args) => this.mock.$getAtProbablyRecentBlock(0, ...args),
};
for (const length of traceLengths) {
this.methods.trace[length] = {
latest: (...args) => this.mock.methods[`$latest_${libraryName}_Trace${length}(uint256)`](0, ...args),
latestCheckpoint: (...args) =>
this.mock.methods[`$latestCheckpoint_${libraryName}_Trace${length}(uint256)`](0, ...args),
length: (...args) => this.mock.methods[`$length_${libraryName}_Trace${length}(uint256)`](0, ...args),
push: (...args) => this.mock.methods[`$push(uint256,uint${256 - length},uint${length})`](0, ...args),
lowerLookup: (...args) => this.mock.methods[`$lowerLookup(uint256,uint${256 - length})`](0, ...args),
upperLookup: (...args) => this.mock.methods[`$upperLookup(uint256,uint${256 - length})`](0, ...args),
upperLookupRecent: (...args) =>
this.mock.methods[`$upperLookupRecent(uint256,uint${256 - length})`](0, ...args),
};
}
});
describe('History checkpoints', function () {
describe('without checkpoints', function () {
it('returns zero as latest value', async function () {
expect(await this.methods.history.latest()).to.be.bignumber.equal('0');
const ckpt = await this.methods.history.latestCheckpoint();
expect(ckpt[0]).to.be.equal(false);
expect(ckpt[1]).to.be.bignumber.equal('0');
expect(ckpt[2]).to.be.bignumber.equal('0');
});
it('returns zero as past value', async function () {
await time.advanceBlock();
expect(await this.methods.history.getAtBlock((await web3.eth.getBlockNumber()) - 1)).to.be.bignumber.equal('0');
expect(
await this.methods.history.getAtRecentBlock((await web3.eth.getBlockNumber()) - 1),
).to.be.bignumber.equal('0');
});
});
describe('with checkpoints', function () {
beforeEach('pushing checkpoints', async function () {
this.tx1 = await this.methods.history.push(1);
this.tx2 = await this.methods.history.push(2);
await time.advanceBlock();
this.tx3 = await this.methods.history.push(3);
await time.advanceBlock();
await time.advanceBlock();
});
it('returns latest value', async function () {
expect(await this.methods.history.latest()).to.be.bignumber.equal('3');
const ckpt = await this.methods.history.latestCheckpoint();
expect(ckpt[0]).to.be.equal(true);
expect(ckpt[1]).to.be.bignumber.equal(web3.utils.toBN(this.tx3.receipt.blockNumber));
expect(ckpt[2]).to.be.bignumber.equal(web3.utils.toBN('3'));
});
for (const getAtBlockVariant of ['getAtBlock', 'getAtRecentBlock']) {
describe(`lookup: ${getAtBlockVariant}`, function () {
it('returns past values', async function () {
expect(
await this.methods.history[getAtBlockVariant](this.tx1.receipt.blockNumber - 1),
).to.be.bignumber.equal('0');
expect(await this.methods.history[getAtBlockVariant](this.tx1.receipt.blockNumber)).to.be.bignumber.equal(
'1',
);
expect(await this.methods.history[getAtBlockVariant](this.tx2.receipt.blockNumber)).to.be.bignumber.equal(
'2',
);
// Block with no new checkpoints
expect(
await this.methods.history[getAtBlockVariant](this.tx2.receipt.blockNumber + 1),
).to.be.bignumber.equal('2');
expect(await this.methods.history[getAtBlockVariant](this.tx3.receipt.blockNumber)).to.be.bignumber.equal(
'3',
);
expect(
await this.methods.history[getAtBlockVariant](this.tx3.receipt.blockNumber + 1),
).to.be.bignumber.equal('3');
});
it('reverts if block number >= current block', async function () {
await expectRevert(
this.methods.history[getAtBlockVariant](await web3.eth.getBlockNumber()),
'Checkpoints: block not yet mined',
);
await expectRevert(
this.methods.history[getAtBlockVariant]((await web3.eth.getBlockNumber()) + 1),
'Checkpoints: block not yet mined',
);
});
});
}
it('multiple checkpoints in the same block', async function () {
const lengthBefore = await this.methods.history.length();
await batchInBlock([
() => this.methods.history.push(8, { gas: 100000 }),
() => this.methods.history.push(9, { gas: 100000 }),
() => this.methods.history.push(10, { gas: 100000 }),
]);
expect(await this.methods.history.length()).to.be.bignumber.equal(lengthBefore.addn(1));
expect(await this.methods.history.latest()).to.be.bignumber.equal('10');
});
it('more than 5 checkpoints', async function () {
for (let i = 4; i <= 6; i++) {
await this.methods.history.push(i);
}
expect(await this.methods.history.length()).to.be.bignumber.equal('6');
const block = await web3.eth.getBlockNumber();
// recent
expect(await this.methods.history.getAtRecentBlock(block - 1)).to.be.bignumber.equal('5');
// non-recent
expect(await this.methods.history.getAtRecentBlock(block - 9)).to.be.bignumber.equal('0');
});
});
});
for (const length of traceLengths) {
describe(`Trace${length}`, function () {
describe('without checkpoints', function () {
it('returns zero as latest value', async function () {
expect(await this.methods.trace[length].latest()).to.be.bignumber.equal('0');
const ckpt = await this.methods.trace[length].latestCheckpoint();
expect(ckpt[0]).to.be.equal(false);
expect(ckpt[1]).to.be.bignumber.equal('0');
expect(ckpt[2]).to.be.bignumber.equal('0');
});
it('lookup returns 0', async function () {
expect(await this.methods.trace[length].lowerLookup(0)).to.be.bignumber.equal('0');
expect(await this.methods.trace[length].upperLookup(0)).to.be.bignumber.equal('0');
expect(await this.methods.trace[length].upperLookupRecent(0)).to.be.bignumber.equal('0');
});
});
describe('with checkpoints', function () {
beforeEach('pushing checkpoints', async function () {
this.checkpoints = [
{ key: '2', value: '17' },
{ key: '3', value: '42' },
{ key: '5', value: '101' },
{ key: '7', value: '23' },
{ key: '11', value: '99' },
];
for (const { key, value } of this.checkpoints) {
await this.methods.trace[length].push(key, value);
}
});
it('length', async function () {
expect(await this.methods.trace[length].length()).to.be.bignumber.equal(this.checkpoints.length.toString());
});
it('returns latest value', async function () {
expect(await this.methods.trace[length].latest()).to.be.bignumber.equal(last(this.checkpoints).value);
const ckpt = await this.methods.trace[length].latestCheckpoint();
expect(ckpt[0]).to.be.equal(true);
expect(ckpt[1]).to.be.bignumber.equal(last(this.checkpoints).key);
expect(ckpt[2]).to.be.bignumber.equal(last(this.checkpoints).value);
});
it('cannot push values in the past', async function () {
await expectRevert(
this.methods.trace[length].push(last(this.checkpoints).key - 1, '0'),
'Checkpoint: decreasing keys',
);
});
it('can update last value', async function () {
const newValue = '42';
// check length before the update
expect(await this.methods.trace[length].length()).to.be.bignumber.equal(this.checkpoints.length.toString());
// update last key
await this.methods.trace[length].push(last(this.checkpoints).key, newValue);
expect(await this.methods.trace[length].latest()).to.be.bignumber.equal(newValue);
// check that length did not change
expect(await this.methods.trace[length].length()).to.be.bignumber.equal(this.checkpoints.length.toString());
});
it('lower lookup', async function () {
for (let i = 0; i < 14; ++i) {
const value = first(this.checkpoints.filter(x => i <= x.key))?.value || '0';
expect(await this.methods.trace[length].lowerLookup(i)).to.be.bignumber.equal(value);
}
});
it('upper lookup & upperLookupRecent', async function () {
for (let i = 0; i < 14; ++i) {
const value = last(this.checkpoints.filter(x => i >= x.key))?.value || '0';
expect(await this.methods.trace[length].upperLookup(i)).to.be.bignumber.equal(value);
expect(await this.methods.trace[length].upperLookupRecent(i)).to.be.bignumber.equal(value);
}
});
it('upperLookupRecent with more than 5 checkpoints', async function () {
const moreCheckpoints = [
{ key: '12', value: '22' },
{ key: '13', value: '131' },
{ key: '17', value: '45' },
{ key: '19', value: '31452' },
{ key: '21', value: '0' },
];
const allCheckpoints = [].concat(this.checkpoints, moreCheckpoints);
for (const { key, value } of moreCheckpoints) {
await this.methods.trace[length].push(key, value);
}
for (let i = 0; i < 25; ++i) {
const value = last(allCheckpoints.filter(x => i >= x.key))?.value || '0';
expect(await this.methods.trace[length].upperLookup(i)).to.be.bignumber.equal(value);
expect(await this.methods.trace[length].upperLookupRecent(i)).to.be.bignumber.equal(value);
}
});
});
});
}
});
@@ -0,0 +1,42 @@
const { BN, expectEvent } = require('@openzeppelin/test-helpers');
const ContextMock = artifacts.require('ContextMock');
function shouldBehaveLikeRegularContext(sender) {
describe('msgSender', function () {
it('returns the transaction sender when called from an EOA', async function () {
const receipt = await this.context.msgSender({ from: sender });
expectEvent(receipt, 'Sender', { sender });
});
it('returns the transaction sender when from another contract', async function () {
const { tx } = await this.caller.callSender(this.context.address, { from: sender });
await expectEvent.inTransaction(tx, ContextMock, 'Sender', { sender: this.caller.address });
});
});
describe('msgData', function () {
const integerValue = new BN('42');
const stringValue = 'OpenZeppelin';
let callData;
beforeEach(async function () {
callData = this.context.contract.methods.msgData(integerValue.toString(), stringValue).encodeABI();
});
it('returns the transaction data when called from an EOA', async function () {
const receipt = await this.context.msgData(integerValue, stringValue);
expectEvent(receipt, 'Data', { data: callData, integerValue, stringValue });
});
it('returns the transaction sender when from another contract', async function () {
const { tx } = await this.caller.callData(this.context.address, integerValue, stringValue);
await expectEvent.inTransaction(tx, ContextMock, 'Data', { data: callData, integerValue, stringValue });
});
});
}
module.exports = {
shouldBehaveLikeRegularContext,
};
@@ -0,0 +1,17 @@
require('@openzeppelin/test-helpers');
const ContextMock = artifacts.require('ContextMock');
const ContextMockCaller = artifacts.require('ContextMockCaller');
const { shouldBehaveLikeRegularContext } = require('./Context.behavior');
contract('Context', function (accounts) {
const [sender] = accounts;
beforeEach(async function () {
this.context = await ContextMock.new();
this.caller = await ContextMockCaller.new();
});
shouldBehaveLikeRegularContext(sender);
});
@@ -0,0 +1,84 @@
const { expectRevert } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const Counters = artifacts.require('$Counters');
contract('Counters', function () {
beforeEach(async function () {
this.counter = await Counters.new();
});
it('starts at zero', async function () {
expect(await this.counter.$current(0)).to.be.bignumber.equal('0');
});
describe('increment', function () {
context('starting from 0', function () {
it('increments the current value by one', async function () {
await this.counter.$increment(0);
expect(await this.counter.$current(0)).to.be.bignumber.equal('1');
});
it('can be called multiple times', async function () {
await this.counter.$increment(0);
await this.counter.$increment(0);
await this.counter.$increment(0);
expect(await this.counter.$current(0)).to.be.bignumber.equal('3');
});
});
});
describe('decrement', function () {
beforeEach(async function () {
await this.counter.$increment(0);
expect(await this.counter.$current(0)).to.be.bignumber.equal('1');
});
context('starting from 1', function () {
it('decrements the current value by one', async function () {
await this.counter.$decrement(0);
expect(await this.counter.$current(0)).to.be.bignumber.equal('0');
});
it('reverts if the current value is 0', async function () {
await this.counter.$decrement(0);
await expectRevert(this.counter.$decrement(0), 'Counter: decrement overflow');
});
});
context('after incremented to 3', function () {
it('can be called multiple times', async function () {
await this.counter.$increment(0);
await this.counter.$increment(0);
expect(await this.counter.$current(0)).to.be.bignumber.equal('3');
await this.counter.$decrement(0);
await this.counter.$decrement(0);
await this.counter.$decrement(0);
expect(await this.counter.$current(0)).to.be.bignumber.equal('0');
});
});
});
describe('reset', function () {
context('null counter', function () {
it('does not throw', async function () {
await this.counter.$reset(0);
expect(await this.counter.$current(0)).to.be.bignumber.equal('0');
});
});
context('non null counter', function () {
beforeEach(async function () {
await this.counter.$increment(0);
expect(await this.counter.$current(0)).to.be.bignumber.equal('1');
});
it('reset to 0', async function () {
await this.counter.$reset(0);
expect(await this.counter.$current(0)).to.be.bignumber.equal('0');
});
});
});
});
@@ -0,0 +1,89 @@
const { balance, ether, expectEvent, expectRevert, send } = require('@openzeppelin/test-helpers');
const { computeCreate2Address } = require('../helpers/create2');
const { expect } = require('chai');
const Create2 = artifacts.require('$Create2');
const VestingWallet = artifacts.require('VestingWallet');
const ERC1820Implementer = artifacts.require('$ERC1820Implementer');
contract('Create2', function (accounts) {
const [deployerAccount, other] = accounts;
const salt = 'salt message';
const saltHex = web3.utils.soliditySha3(salt);
const encodedParams = web3.eth.abi.encodeParameters(['address', 'uint64', 'uint64'], [other, 0, 0]).slice(2);
const constructorByteCode = `${VestingWallet.bytecode}${encodedParams}`;
beforeEach(async function () {
this.factory = await Create2.new();
});
describe('computeAddress', function () {
it('computes the correct contract address', async function () {
const onChainComputed = await this.factory.$computeAddress(saltHex, web3.utils.keccak256(constructorByteCode));
const offChainComputed = computeCreate2Address(saltHex, constructorByteCode, this.factory.address);
expect(onChainComputed).to.equal(offChainComputed);
});
it('computes the correct contract address with deployer', async function () {
const onChainComputed = await this.factory.$computeAddress(
saltHex,
web3.utils.keccak256(constructorByteCode),
deployerAccount,
);
const offChainComputed = computeCreate2Address(saltHex, constructorByteCode, deployerAccount);
expect(onChainComputed).to.equal(offChainComputed);
});
});
describe('deploy', function () {
it('deploys a ERC1820Implementer from inline assembly code', async function () {
const offChainComputed = computeCreate2Address(saltHex, ERC1820Implementer.bytecode, this.factory.address);
expectEvent(await this.factory.$deploy(0, saltHex, ERC1820Implementer.bytecode), 'return$deploy', {
addr: offChainComputed,
});
expect(ERC1820Implementer.bytecode).to.include((await web3.eth.getCode(offChainComputed)).slice(2));
});
it('deploys a contract with constructor arguments', async function () {
const offChainComputed = computeCreate2Address(saltHex, constructorByteCode, this.factory.address);
expectEvent(await this.factory.$deploy(0, saltHex, constructorByteCode), 'return$deploy', {
addr: offChainComputed,
});
expect(await VestingWallet.at(offChainComputed).then(instance => instance.beneficiary())).to.be.equal(other);
});
it('deploys a contract with funds deposited in the factory', async function () {
const deposit = ether('2');
await send.ether(deployerAccount, this.factory.address, deposit);
expect(await balance.current(this.factory.address)).to.be.bignumber.equal(deposit);
const offChainComputed = computeCreate2Address(saltHex, constructorByteCode, this.factory.address);
expectEvent(await this.factory.$deploy(deposit, saltHex, constructorByteCode), 'return$deploy', {
addr: offChainComputed,
});
expect(await balance.current(offChainComputed)).to.be.bignumber.equal(deposit);
});
it('fails deploying a contract in an existent address', async function () {
expectEvent(await this.factory.$deploy(0, saltHex, constructorByteCode), 'return$deploy');
await expectRevert(this.factory.$deploy(0, saltHex, constructorByteCode), 'Create2: Failed on deploy');
});
it('fails deploying a contract if the bytecode length is zero', async function () {
await expectRevert(this.factory.$deploy(0, saltHex, '0x'), 'Create2: bytecode length is zero');
});
it('fails deploying a contract if factory contract does not have sufficient balance', async function () {
await expectRevert(this.factory.$deploy(1, saltHex, constructorByteCode), 'Create2: insufficient balance');
});
});
});
@@ -0,0 +1,68 @@
const { BN, expectRevert } = require('@openzeppelin/test-helpers');
const ERC20MulticallMock = artifacts.require('$ERC20MulticallMock');
contract('Multicall', function (accounts) {
const [deployer, alice, bob] = accounts;
const amount = 12000;
beforeEach(async function () {
this.multicallToken = await ERC20MulticallMock.new('name', 'symbol');
await this.multicallToken.$_mint(deployer, amount);
});
it('batches function calls', async function () {
expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0'));
expect(await this.multicallToken.balanceOf(bob)).to.be.bignumber.equal(new BN('0'));
await this.multicallToken.multicall(
[
this.multicallToken.contract.methods.transfer(alice, amount / 2).encodeABI(),
this.multicallToken.contract.methods.transfer(bob, amount / 3).encodeABI(),
],
{ from: deployer },
);
expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN(amount / 2));
expect(await this.multicallToken.balanceOf(bob)).to.be.bignumber.equal(new BN(amount / 3));
});
it('returns an array with the result of each call', async function () {
const MulticallTest = artifacts.require('MulticallTest');
const multicallTest = await MulticallTest.new({ from: deployer });
await this.multicallToken.transfer(multicallTest.address, amount, { from: deployer });
expect(await this.multicallToken.balanceOf(multicallTest.address)).to.be.bignumber.equal(new BN(amount));
const recipients = [alice, bob];
const amounts = [amount / 2, amount / 3].map(n => new BN(n));
await multicallTest.checkReturnValues(this.multicallToken.address, recipients, amounts);
});
it('reverts previous calls', async function () {
expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0'));
const call = this.multicallToken.multicall(
[
this.multicallToken.contract.methods.transfer(alice, amount).encodeABI(),
this.multicallToken.contract.methods.transfer(bob, amount).encodeABI(),
],
{ from: deployer },
);
await expectRevert(call, 'ERC20: transfer amount exceeds balance');
expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0'));
});
it('bubbles up revert reasons', async function () {
const call = this.multicallToken.multicall(
[
this.multicallToken.contract.methods.transfer(alice, amount).encodeABI(),
this.multicallToken.contract.methods.transfer(bob, amount).encodeABI(),
],
{ from: deployer },
);
await expectRevert(call, 'ERC20: transfer amount exceeds balance');
});
});
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../../contracts/utils/ShortStrings.sol";
contract ShortStringsTest is Test {
string _fallback;
function testRoundtripShort(string memory input) external {
vm.assume(_isShort(input));
ShortString short = ShortStrings.toShortString(input);
string memory output = ShortStrings.toString(short);
assertEq(input, output);
}
function testRoundtripWithFallback(string memory input, string memory fallbackInitial) external {
_fallback = fallbackInitial; // Make sure that the initial value has no effect
ShortString short = ShortStrings.toShortStringWithFallback(input, _fallback);
string memory output = ShortStrings.toStringWithFallback(short, _fallback);
assertEq(input, output);
}
function testRevertLong(string memory input) external {
vm.assume(!_isShort(input));
vm.expectRevert(abi.encodeWithSelector(ShortStrings.StringTooLong.selector, input));
this.toShortString(input);
}
function testLengthShort(string memory input) external {
vm.assume(_isShort(input));
uint256 inputLength = bytes(input).length;
ShortString short = ShortStrings.toShortString(input);
uint256 shortLength = ShortStrings.byteLength(short);
assertEq(inputLength, shortLength);
}
function testLengthWithFallback(string memory input, string memory fallbackInitial) external {
_fallback = fallbackInitial;
uint256 inputLength = bytes(input).length;
ShortString short = ShortStrings.toShortStringWithFallback(input, _fallback);
uint256 shortLength = ShortStrings.byteLengthWithFallback(short, _fallback);
assertEq(inputLength, shortLength);
}
function toShortString(string memory input) external pure returns (ShortString) {
return ShortStrings.toShortString(input);
}
function _isShort(string memory input) internal pure returns (bool) {
return bytes(input).length < 32;
}
}
@@ -0,0 +1,55 @@
const { expect } = require('chai');
const { expectRevertCustomError } = require('../helpers/customError');
const ShortStrings = artifacts.require('$ShortStrings');
function length(sstr) {
return parseInt(sstr.slice(64), 16);
}
function decode(sstr) {
return web3.utils.toUtf8(sstr).slice(0, length(sstr));
}
contract('ShortStrings', function () {
before(async function () {
this.mock = await ShortStrings.new();
});
for (const str of [0, 1, 16, 31, 32, 64, 1024].map(length => 'a'.repeat(length))) {
describe(`with string length ${str.length}`, function () {
it('encode / decode', async function () {
if (str.length < 32) {
const encoded = await this.mock.$toShortString(str);
expect(decode(encoded)).to.be.equal(str);
const length = await this.mock.$byteLength(encoded);
expect(length.toNumber()).to.be.equal(str.length);
const decoded = await this.mock.$toString(encoded);
expect(decoded).to.be.equal(str);
} else {
await expectRevertCustomError(this.mock.$toShortString(str), `StringTooLong("${str}")`);
}
});
it('set / get with fallback', async function () {
const { logs } = await this.mock.$toShortStringWithFallback(str, 0);
const { ret0 } = logs.find(({ event }) => event == 'return$toShortStringWithFallback').args;
const promise = this.mock.$toString(ret0);
if (str.length < 32) {
expect(await promise).to.be.equal(str);
} else {
await expectRevertCustomError(promise, 'InvalidShortString()');
}
const length = await this.mock.$byteLengthWithFallback(ret0, 0);
expect(length.toNumber()).to.be.equal(str.length);
const recovered = await this.mock.$toStringWithFallback(ret0, 0);
expect(recovered).to.be.equal(str);
});
});
}
});
@@ -0,0 +1,210 @@
const { constants, BN } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const StorageSlotMock = artifacts.require('StorageSlotMock');
const slot = web3.utils.keccak256('some.storage.slot');
const otherSlot = web3.utils.keccak256('some.other.storage.slot');
contract('StorageSlot', function (accounts) {
beforeEach(async function () {
this.store = await StorageSlotMock.new();
});
describe('boolean storage slot', function () {
beforeEach(async function () {
this.value = true;
});
it('set', async function () {
await this.store.setBoolean(slot, this.value);
});
describe('get', function () {
beforeEach(async function () {
await this.store.setBoolean(slot, this.value);
});
it('from right slot', async function () {
expect(await this.store.getBoolean(slot)).to.be.equal(this.value);
});
it('from other slot', async function () {
expect(await this.store.getBoolean(otherSlot)).to.be.equal(false);
});
});
});
describe('address storage slot', function () {
beforeEach(async function () {
this.value = accounts[1];
});
it('set', async function () {
await this.store.setAddress(slot, this.value);
});
describe('get', function () {
beforeEach(async function () {
await this.store.setAddress(slot, this.value);
});
it('from right slot', async function () {
expect(await this.store.getAddress(slot)).to.be.equal(this.value);
});
it('from other slot', async function () {
expect(await this.store.getAddress(otherSlot)).to.be.equal(constants.ZERO_ADDRESS);
});
});
});
describe('bytes32 storage slot', function () {
beforeEach(async function () {
this.value = web3.utils.keccak256('some byte32 value');
});
it('set', async function () {
await this.store.setBytes32(slot, this.value);
});
describe('get', function () {
beforeEach(async function () {
await this.store.setBytes32(slot, this.value);
});
it('from right slot', async function () {
expect(await this.store.getBytes32(slot)).to.be.equal(this.value);
});
it('from other slot', async function () {
expect(await this.store.getBytes32(otherSlot)).to.be.equal(constants.ZERO_BYTES32);
});
});
});
describe('uint256 storage slot', function () {
beforeEach(async function () {
this.value = new BN(1742);
});
it('set', async function () {
await this.store.setUint256(slot, this.value);
});
describe('get', function () {
beforeEach(async function () {
await this.store.setUint256(slot, this.value);
});
it('from right slot', async function () {
expect(await this.store.getUint256(slot)).to.be.bignumber.equal(this.value);
});
it('from other slot', async function () {
expect(await this.store.getUint256(otherSlot)).to.be.bignumber.equal('0');
});
});
});
describe('string storage slot', function () {
beforeEach(async function () {
this.value = 'lorem ipsum';
});
it('set', async function () {
await this.store.setString(slot, this.value);
});
describe('get', function () {
beforeEach(async function () {
await this.store.setString(slot, this.value);
});
it('from right slot', async function () {
expect(await this.store.getString(slot)).to.be.equal(this.value);
});
it('from other slot', async function () {
expect(await this.store.getString(otherSlot)).to.be.equal('');
});
});
});
describe('string storage pointer', function () {
beforeEach(async function () {
this.value = 'lorem ipsum';
});
it('set', async function () {
await this.store.setStringStorage(slot, this.value);
});
describe('get', function () {
beforeEach(async function () {
await this.store.setStringStorage(slot, this.value);
});
it('from right slot', async function () {
expect(await this.store.stringMap(slot)).to.be.equal(this.value);
expect(await this.store.getStringStorage(slot)).to.be.equal(this.value);
});
it('from other slot', async function () {
expect(await this.store.stringMap(otherSlot)).to.be.equal('');
expect(await this.store.getStringStorage(otherSlot)).to.be.equal('');
});
});
});
describe('bytes storage slot', function () {
beforeEach(async function () {
this.value = web3.utils.randomHex(128);
});
it('set', async function () {
await this.store.setBytes(slot, this.value);
});
describe('get', function () {
beforeEach(async function () {
await this.store.setBytes(slot, this.value);
});
it('from right slot', async function () {
expect(await this.store.getBytes(slot)).to.be.equal(this.value);
});
it('from other slot', async function () {
expect(await this.store.getBytes(otherSlot)).to.be.equal(null);
});
});
});
describe('bytes storage pointer', function () {
beforeEach(async function () {
this.value = web3.utils.randomHex(128);
});
it('set', async function () {
await this.store.setBytesStorage(slot, this.value);
});
describe('get', function () {
beforeEach(async function () {
await this.store.setBytesStorage(slot, this.value);
});
it('from right slot', async function () {
expect(await this.store.bytesMap(slot)).to.be.equal(this.value);
expect(await this.store.getBytesStorage(slot)).to.be.equal(this.value);
});
it('from other slot', async function () {
expect(await this.store.bytesMap(otherSlot)).to.be.equal(null);
expect(await this.store.getBytesStorage(otherSlot)).to.be.equal(null);
});
});
});
});
@@ -0,0 +1,150 @@
const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const Strings = artifacts.require('$Strings');
contract('Strings', function () {
before(async function () {
this.strings = await Strings.new();
});
describe('toString', function () {
const values = [
'0',
'7',
'10',
'99',
'100',
'101',
'123',
'4132',
'12345',
'1234567',
'1234567890',
'123456789012345',
'12345678901234567890',
'123456789012345678901234567890',
'1234567890123456789012345678901234567890',
'12345678901234567890123456789012345678901234567890',
'123456789012345678901234567890123456789012345678901234567890',
'1234567890123456789012345678901234567890123456789012345678901234567890',
];
describe('uint256', function () {
it('converts MAX_UINT256', async function () {
const value = constants.MAX_UINT256;
expect(await this.strings.methods['$toString(uint256)'](value)).to.equal(value.toString(10));
});
for (const value of values) {
it(`converts ${value}`, async function () {
expect(await this.strings.methods['$toString(uint256)'](value)).to.equal(value);
});
}
});
describe('int256', function () {
it('converts MAX_INT256', async function () {
const value = constants.MAX_INT256;
expect(await this.strings.methods['$toString(int256)'](value)).to.equal(value.toString(10));
});
it('converts MIN_INT256', async function () {
const value = constants.MIN_INT256;
expect(await this.strings.methods['$toString(int256)'](value)).to.equal(value.toString(10));
});
for (const value of values) {
it(`convert ${value}`, async function () {
expect(await this.strings.methods['$toString(int256)'](value)).to.equal(value);
});
it(`convert negative ${value}`, async function () {
const negated = new BN(value).neg();
expect(await this.strings.methods['$toString(int256)'](negated)).to.equal(negated.toString(10));
});
}
});
});
describe('toHexString', function () {
it('converts 0', async function () {
expect(await this.strings.methods['$toHexString(uint256)'](0)).to.equal('0x00');
});
it('converts a positive number', async function () {
expect(await this.strings.methods['$toHexString(uint256)'](0x4132)).to.equal('0x4132');
});
it('converts MAX_UINT256', async function () {
expect(await this.strings.methods['$toHexString(uint256)'](constants.MAX_UINT256)).to.equal(
web3.utils.toHex(constants.MAX_UINT256),
);
});
});
describe('toHexString fixed', function () {
it('converts a positive number (long)', async function () {
expect(await this.strings.methods['$toHexString(uint256,uint256)'](0x4132, 32)).to.equal(
'0x0000000000000000000000000000000000000000000000000000000000004132',
);
});
it('converts a positive number (short)', async function () {
await expectRevert(
this.strings.methods['$toHexString(uint256,uint256)'](0x4132, 1),
'Strings: hex length insufficient',
);
});
it('converts MAX_UINT256', async function () {
expect(await this.strings.methods['$toHexString(uint256,uint256)'](constants.MAX_UINT256, 32)).to.equal(
web3.utils.toHex(constants.MAX_UINT256),
);
});
});
describe('toHexString address', function () {
it('converts a random address', async function () {
const addr = '0xa9036907dccae6a1e0033479b12e837e5cf5a02f';
expect(await this.strings.methods['$toHexString(address)'](addr)).to.equal(addr);
});
it('converts an address with leading zeros', async function () {
const addr = '0x0000e0ca771e21bd00057f54a68c30d400000000';
expect(await this.strings.methods['$toHexString(address)'](addr)).to.equal(addr);
});
});
describe('equal', function () {
it('compares two empty strings', async function () {
expect(await this.strings.methods['$equal(string,string)']('', '')).to.equal(true);
});
it('compares two equal strings', async function () {
expect(await this.strings.methods['$equal(string,string)']('a', 'a')).to.equal(true);
});
it('compares two different strings', async function () {
expect(await this.strings.methods['$equal(string,string)']('a', 'b')).to.equal(false);
});
it('compares two different strings of different lengths', async function () {
expect(await this.strings.methods['$equal(string,string)']('a', 'aa')).to.equal(false);
expect(await this.strings.methods['$equal(string,string)']('aa', 'a')).to.equal(false);
});
it('compares two different large strings', async function () {
const str1 = 'a'.repeat(201);
const str2 = 'a'.repeat(200) + 'b';
expect(await this.strings.methods['$equal(string,string)'](str1, str2)).to.equal(false);
});
it('compares two equal large strings', async function () {
const str1 = 'a'.repeat(201);
const str2 = 'a'.repeat(201);
expect(await this.strings.methods['$equal(string,string)'](str1, str2)).to.equal(true);
});
});
});
@@ -0,0 +1,55 @@
const { BN, time } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const TimersBlockNumberImpl = artifacts.require('TimersBlockNumberImpl');
contract('TimersBlockNumber', function () {
beforeEach(async function () {
this.instance = await TimersBlockNumberImpl.new();
this.now = await web3.eth.getBlock('latest').then(({ number }) => number);
});
it('unset', async function () {
expect(await this.instance.getDeadline()).to.be.bignumber.equal('0');
expect(await this.instance.isUnset()).to.be.equal(true);
expect(await this.instance.isStarted()).to.be.equal(false);
expect(await this.instance.isPending()).to.be.equal(false);
expect(await this.instance.isExpired()).to.be.equal(false);
});
it('pending', async function () {
await this.instance.setDeadline(this.now + 3);
expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now + 3));
expect(await this.instance.isUnset()).to.be.equal(false);
expect(await this.instance.isStarted()).to.be.equal(true);
expect(await this.instance.isPending()).to.be.equal(true);
expect(await this.instance.isExpired()).to.be.equal(false);
});
it('expired', async function () {
await this.instance.setDeadline(this.now - 3);
expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now - 3));
expect(await this.instance.isUnset()).to.be.equal(false);
expect(await this.instance.isStarted()).to.be.equal(true);
expect(await this.instance.isPending()).to.be.equal(false);
expect(await this.instance.isExpired()).to.be.equal(true);
});
it('reset', async function () {
await this.instance.reset();
expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(0));
expect(await this.instance.isUnset()).to.be.equal(true);
expect(await this.instance.isStarted()).to.be.equal(false);
expect(await this.instance.isPending()).to.be.equal(false);
expect(await this.instance.isExpired()).to.be.equal(false);
});
it('fast forward', async function () {
await this.instance.setDeadline(this.now + 3);
expect(await this.instance.isPending()).to.be.equal(true);
expect(await this.instance.isExpired()).to.be.equal(false);
await time.advanceBlockTo(this.now + 3);
expect(await this.instance.isPending()).to.be.equal(false);
expect(await this.instance.isExpired()).to.be.equal(true);
});
});
@@ -0,0 +1,55 @@
const { BN, time } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const TimersTimestampImpl = artifacts.require('TimersTimestampImpl');
contract('TimersTimestamp', function () {
beforeEach(async function () {
this.instance = await TimersTimestampImpl.new();
this.now = await web3.eth.getBlock('latest').then(({ timestamp }) => timestamp);
});
it('unset', async function () {
expect(await this.instance.getDeadline()).to.be.bignumber.equal('0');
expect(await this.instance.isUnset()).to.be.equal(true);
expect(await this.instance.isStarted()).to.be.equal(false);
expect(await this.instance.isPending()).to.be.equal(false);
expect(await this.instance.isExpired()).to.be.equal(false);
});
it('pending', async function () {
await this.instance.setDeadline(this.now + 100);
expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now + 100));
expect(await this.instance.isUnset()).to.be.equal(false);
expect(await this.instance.isStarted()).to.be.equal(true);
expect(await this.instance.isPending()).to.be.equal(true);
expect(await this.instance.isExpired()).to.be.equal(false);
});
it('expired', async function () {
await this.instance.setDeadline(this.now - 100);
expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now - 100));
expect(await this.instance.isUnset()).to.be.equal(false);
expect(await this.instance.isStarted()).to.be.equal(true);
expect(await this.instance.isPending()).to.be.equal(false);
expect(await this.instance.isExpired()).to.be.equal(true);
});
it('reset', async function () {
await this.instance.reset();
expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(0));
expect(await this.instance.isUnset()).to.be.equal(true);
expect(await this.instance.isStarted()).to.be.equal(false);
expect(await this.instance.isPending()).to.be.equal(false);
expect(await this.instance.isExpired()).to.be.equal(false);
});
it('fast forward', async function () {
await this.instance.setDeadline(this.now + 100);
expect(await this.instance.isPending()).to.be.equal(true);
expect(await this.instance.isExpired()).to.be.equal(false);
await time.increaseTo(this.now + 100);
expect(await this.instance.isPending()).to.be.equal(false);
expect(await this.instance.isExpired()).to.be.equal(true);
});
});
@@ -0,0 +1,260 @@
const { expectRevert } = require('@openzeppelin/test-helpers');
const { toEthSignedMessageHash, toDataWithIntendedValidatorHash } = require('../../helpers/sign');
const { expect } = require('chai');
const ECDSA = artifacts.require('$ECDSA');
const TEST_MESSAGE = web3.utils.sha3('OpenZeppelin');
const WRONG_MESSAGE = web3.utils.sha3('Nope');
const NON_HASH_MESSAGE = '0x' + Buffer.from('abcd').toString('hex');
const RANDOM_ADDRESS = web3.utils.toChecksumAddress(web3.utils.randomHex(20));
function to2098Format(signature) {
const long = web3.utils.hexToBytes(signature);
if (long.length !== 65) {
throw new Error('invalid signature length (expected long format)');
}
if (long[32] >> 7 === 1) {
throw new Error("invalid signature 's' value");
}
const short = long.slice(0, 64);
short[32] |= long[64] % 27 << 7; // set the first bit of the 32nd byte to the v parity bit
return web3.utils.bytesToHex(short);
}
function split(signature) {
const raw = web3.utils.hexToBytes(signature);
switch (raw.length) {
case 64:
return [
web3.utils.bytesToHex(raw.slice(0, 32)), // r
web3.utils.bytesToHex(raw.slice(32, 64)), // vs
];
case 65:
return [
raw[64], // v
web3.utils.bytesToHex(raw.slice(0, 32)), // r
web3.utils.bytesToHex(raw.slice(32, 64)), // s
];
default:
expect.fail('Invalid signature length, cannot split');
}
}
contract('ECDSA', function (accounts) {
const [other] = accounts;
beforeEach(async function () {
this.ecdsa = await ECDSA.new();
});
context('recover with invalid signature', function () {
it('with short signature', async function () {
await expectRevert(this.ecdsa.$recover(TEST_MESSAGE, '0x1234'), 'ECDSA: invalid signature length');
});
it('with long signature', async function () {
await expectRevert(
// eslint-disable-next-line max-len
this.ecdsa.$recover(
TEST_MESSAGE,
'0x01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789',
),
'ECDSA: invalid signature length',
);
});
});
context('recover with valid signature', function () {
context('using web3.eth.sign', function () {
it('returns signer address with correct signature', async function () {
// Create the signature
const signature = await web3.eth.sign(TEST_MESSAGE, other);
// Recover the signer address from the generated message and signature.
expect(await this.ecdsa.$recover(toEthSignedMessageHash(TEST_MESSAGE), signature)).to.equal(other);
});
it('returns signer address with correct signature for arbitrary length message', async function () {
// Create the signature
const signature = await web3.eth.sign(NON_HASH_MESSAGE, other);
// Recover the signer address from the generated message and signature.
expect(await this.ecdsa.$recover(toEthSignedMessageHash(NON_HASH_MESSAGE), signature)).to.equal(other);
});
it('returns a different address', async function () {
const signature = await web3.eth.sign(TEST_MESSAGE, other);
expect(await this.ecdsa.$recover(WRONG_MESSAGE, signature)).to.not.equal(other);
});
it('reverts with invalid signature', async function () {
// eslint-disable-next-line max-len
const signature =
'0x332ce75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e01c';
await expectRevert(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSA: invalid signature');
});
});
context('with v=27 signature', function () {
// Signature generated outside ganache with method web3.eth.sign(signer, message)
const signer = '0x2cc1166f6212628A0deEf2B33BEFB2187D35b86c';
// eslint-disable-next-line max-len
const signatureWithoutV =
'0x5d99b6f7f6d1f73d1a26497f2b1c89b24c0993913f86e9a2d02cd69887d9c94f3c880358579d811b21dd1b7fd9bb01c1d81d10e69f0384e675c32b39643be892';
it('works with correct v value', async function () {
const v = '1b'; // 27 = 1b.
const signature = signatureWithoutV + v;
expect(await this.ecdsa.$recover(TEST_MESSAGE, signature)).to.equal(signer);
expect(
await this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)),
).to.equal(signer);
expect(
await this.ecdsa.methods['$recover(bytes32,bytes32,bytes32)'](
TEST_MESSAGE,
...split(to2098Format(signature)),
),
).to.equal(signer);
});
it('rejects incorrect v value', async function () {
const v = '1c'; // 28 = 1c.
const signature = signatureWithoutV + v;
expect(await this.ecdsa.$recover(TEST_MESSAGE, signature)).to.not.equal(signer);
expect(
await this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)),
).to.not.equal(signer);
expect(
await this.ecdsa.methods['$recover(bytes32,bytes32,bytes32)'](
TEST_MESSAGE,
...split(to2098Format(signature)),
),
).to.not.equal(signer);
});
it('reverts wrong v values', async function () {
for (const v of ['00', '01']) {
const signature = signatureWithoutV + v;
await expectRevert(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSA: invalid signature');
await expectRevert(
this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)),
'ECDSA: invalid signature',
);
}
});
it('rejects short EIP2098 format', async function () {
const v = '1b'; // 27 = 1b.
const signature = signatureWithoutV + v;
await expectRevert(
this.ecdsa.$recover(TEST_MESSAGE, to2098Format(signature)),
'ECDSA: invalid signature length',
);
});
});
context('with v=28 signature', function () {
const signer = '0x1E318623aB09Fe6de3C9b8672098464Aeda9100E';
// eslint-disable-next-line max-len
const signatureWithoutV =
'0x331fe75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e0';
it('works with correct v value', async function () {
const v = '1c'; // 28 = 1c.
const signature = signatureWithoutV + v;
expect(await this.ecdsa.$recover(TEST_MESSAGE, signature)).to.equal(signer);
expect(
await this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)),
).to.equal(signer);
expect(
await this.ecdsa.methods['$recover(bytes32,bytes32,bytes32)'](
TEST_MESSAGE,
...split(to2098Format(signature)),
),
).to.equal(signer);
});
it('rejects incorrect v value', async function () {
const v = '1b'; // 27 = 1b.
const signature = signatureWithoutV + v;
expect(await this.ecdsa.$recover(TEST_MESSAGE, signature)).to.not.equal(signer);
expect(
await this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)),
).to.not.equal(signer);
expect(
await this.ecdsa.methods['$recover(bytes32,bytes32,bytes32)'](
TEST_MESSAGE,
...split(to2098Format(signature)),
),
).to.not.equal(signer);
});
it('reverts invalid v values', async function () {
for (const v of ['00', '01']) {
const signature = signatureWithoutV + v;
await expectRevert(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSA: invalid signature');
await expectRevert(
this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)),
'ECDSA: invalid signature',
);
}
});
it('rejects short EIP2098 format', async function () {
const v = '1c'; // 27 = 1b.
const signature = signatureWithoutV + v;
await expectRevert(
this.ecdsa.$recover(TEST_MESSAGE, to2098Format(signature)),
'ECDSA: invalid signature length',
);
});
});
it('reverts with high-s value signature', async function () {
const message = '0xb94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9';
// eslint-disable-next-line max-len
const highSSignature =
'0xe742ff452d41413616a5bf43fe15dd88294e983d3d36206c2712f39083d638bde0a0fc89be718fbc1033e1d30d78be1c68081562ed2e97af876f286f3453231d1b';
await expectRevert(this.ecdsa.$recover(message, highSSignature), "ECDSA: invalid signature 's' value");
await expectRevert(
this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(highSSignature)),
"ECDSA: invalid signature 's' value",
);
expect(() => to2098Format(highSSignature)).to.throw("invalid signature 's' value");
});
});
context('toEthSignedMessageHash', function () {
it('prefixes bytes32 data correctly', async function () {
expect(await this.ecdsa.methods['$toEthSignedMessageHash(bytes32)'](TEST_MESSAGE)).to.equal(
toEthSignedMessageHash(TEST_MESSAGE),
);
});
it('prefixes dynamic length data correctly', async function () {
expect(await this.ecdsa.methods['$toEthSignedMessageHash(bytes)'](NON_HASH_MESSAGE)).to.equal(
toEthSignedMessageHash(NON_HASH_MESSAGE),
);
});
});
context('toDataWithIntendedValidatorHash', function () {
it('returns the hash correctly', async function () {
expect(
await this.ecdsa.methods['$toDataWithIntendedValidatorHash(address,bytes)'](RANDOM_ADDRESS, NON_HASH_MESSAGE),
).to.equal(toDataWithIntendedValidatorHash(RANDOM_ADDRESS, NON_HASH_MESSAGE));
});
});
});
@@ -0,0 +1,103 @@
const ethSigUtil = require('eth-sig-util');
const Wallet = require('ethereumjs-wallet').default;
const { getDomain, domainType, domainSeparator, hashTypedData } = require('../../helpers/eip712');
const { getChainId } = require('../../helpers/chainid');
const { mapValues } = require('../../helpers/map-values');
const EIP712Verifier = artifacts.require('$EIP712Verifier');
const Clones = artifacts.require('$Clones');
contract('EIP712', function (accounts) {
const [mailTo] = accounts;
const shortName = 'A Name';
const shortVersion = '1';
const longName = 'A'.repeat(40);
const longVersion = 'B'.repeat(40);
const cases = [
['short', shortName, shortVersion],
['long', longName, longVersion],
];
for (const [shortOrLong, name, version] of cases) {
describe(`with ${shortOrLong} name and version`, function () {
beforeEach('deploying', async function () {
this.eip712 = await EIP712Verifier.new(name, version);
this.domain = {
name,
version,
chainId: await getChainId(),
verifyingContract: this.eip712.address,
};
this.domainType = domainType(this.domain);
});
describe('domain separator', function () {
it('is internally available', async function () {
const expected = await domainSeparator(this.domain);
expect(await this.eip712.$_domainSeparatorV4()).to.equal(expected);
});
it("can be rebuilt using EIP-5267's eip712Domain", async function () {
const rebuildDomain = await getDomain(this.eip712);
expect(mapValues(rebuildDomain, String)).to.be.deep.equal(mapValues(this.domain, String));
});
if (shortOrLong === 'short') {
// Long strings are in storage, and the proxy will not be properly initialized unless
// the upgradeable contract variant is used and the initializer is invoked.
it('adjusts when behind proxy', async function () {
const factory = await Clones.new();
const cloneReceipt = await factory.$clone(this.eip712.address);
const cloneAddress = cloneReceipt.logs.find(({ event }) => event === 'return$clone').args.instance;
const clone = new EIP712Verifier(cloneAddress);
const cloneDomain = { ...this.domain, verifyingContract: clone.address };
const reportedDomain = await getDomain(clone);
expect(mapValues(reportedDomain, String)).to.be.deep.equal(mapValues(cloneDomain, String));
const expectedSeparator = await domainSeparator(cloneDomain);
expect(await clone.$_domainSeparatorV4()).to.equal(expectedSeparator);
});
}
});
it('hash digest', async function () {
const structhash = web3.utils.randomHex(32);
expect(await this.eip712.$_hashTypedDataV4(structhash)).to.be.equal(hashTypedData(this.domain, structhash));
});
it('digest', async function () {
const message = {
to: mailTo,
contents: 'very interesting',
};
const data = {
types: {
EIP712Domain: this.domainType,
Mail: [
{ name: 'to', type: 'address' },
{ name: 'contents', type: 'string' },
],
},
domain: this.domain,
primaryType: 'Mail',
message,
};
const wallet = Wallet.generate();
const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data });
await this.eip712.verify(signature, wallet.getAddressString(), message.to, message.contents);
});
});
}
});
@@ -0,0 +1,180 @@
require('@openzeppelin/test-helpers');
const { expectRevert } = require('@openzeppelin/test-helpers');
const { MerkleTree } = require('merkletreejs');
const keccak256 = require('keccak256');
const { expect } = require('chai');
const MerkleProof = artifacts.require('$MerkleProof');
contract('MerkleProof', function () {
beforeEach(async function () {
this.merkleProof = await MerkleProof.new();
});
describe('verify', function () {
it('returns true for a valid Merkle proof', async function () {
const elements = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split('');
const merkleTree = new MerkleTree(elements, keccak256, { hashLeaves: true, sortPairs: true });
const root = merkleTree.getHexRoot();
const leaf = keccak256(elements[0]);
const proof = merkleTree.getHexProof(leaf);
expect(await this.merkleProof.$verify(proof, root, leaf)).to.equal(true);
expect(await this.merkleProof.$verifyCalldata(proof, root, leaf)).to.equal(true);
// For demonstration, it is also possible to create valid proofs for certain 64-byte values *not* in elements:
const noSuchLeaf = keccak256(
Buffer.concat([keccak256(elements[0]), keccak256(elements[1])].sort(Buffer.compare)),
);
expect(await this.merkleProof.$verify(proof.slice(1), root, noSuchLeaf)).to.equal(true);
expect(await this.merkleProof.$verifyCalldata(proof.slice(1), root, noSuchLeaf)).to.equal(true);
});
it('returns false for an invalid Merkle proof', async function () {
const correctElements = ['a', 'b', 'c'];
const correctMerkleTree = new MerkleTree(correctElements, keccak256, { hashLeaves: true, sortPairs: true });
const correctRoot = correctMerkleTree.getHexRoot();
const correctLeaf = keccak256(correctElements[0]);
const badElements = ['d', 'e', 'f'];
const badMerkleTree = new MerkleTree(badElements);
const badProof = badMerkleTree.getHexProof(badElements[0]);
expect(await this.merkleProof.$verify(badProof, correctRoot, correctLeaf)).to.equal(false);
expect(await this.merkleProof.$verifyCalldata(badProof, correctRoot, correctLeaf)).to.equal(false);
});
it('returns false for a Merkle proof of invalid length', async function () {
const elements = ['a', 'b', 'c'];
const merkleTree = new MerkleTree(elements, keccak256, { hashLeaves: true, sortPairs: true });
const root = merkleTree.getHexRoot();
const leaf = keccak256(elements[0]);
const proof = merkleTree.getHexProof(leaf);
const badProof = proof.slice(0, proof.length - 5);
expect(await this.merkleProof.$verify(badProof, root, leaf)).to.equal(false);
expect(await this.merkleProof.$verifyCalldata(badProof, root, leaf)).to.equal(false);
});
});
describe('multiProofVerify', function () {
it('returns true for a valid Merkle multi proof', async function () {
const leaves = ['a', 'b', 'c', 'd', 'e', 'f'].map(keccak256).sort(Buffer.compare);
const merkleTree = new MerkleTree(leaves, keccak256, { sort: true });
const root = merkleTree.getRoot();
const proofLeaves = ['b', 'f', 'd'].map(keccak256).sort(Buffer.compare);
const proof = merkleTree.getMultiProof(proofLeaves);
const proofFlags = merkleTree.getProofFlags(proofLeaves, proof);
expect(await this.merkleProof.$multiProofVerify(proof, proofFlags, root, proofLeaves)).to.equal(true);
expect(await this.merkleProof.$multiProofVerifyCalldata(proof, proofFlags, root, proofLeaves)).to.equal(true);
});
it('returns false for an invalid Merkle multi proof', async function () {
const leaves = ['a', 'b', 'c', 'd', 'e', 'f'].map(keccak256).sort(Buffer.compare);
const merkleTree = new MerkleTree(leaves, keccak256, { sort: true });
const root = merkleTree.getRoot();
const badProofLeaves = ['g', 'h', 'i'].map(keccak256).sort(Buffer.compare);
const badMerkleTree = new MerkleTree(badProofLeaves);
const badProof = badMerkleTree.getMultiProof(badProofLeaves);
const badProofFlags = badMerkleTree.getProofFlags(badProofLeaves, badProof);
expect(await this.merkleProof.$multiProofVerify(badProof, badProofFlags, root, badProofLeaves)).to.equal(false);
expect(await this.merkleProof.$multiProofVerifyCalldata(badProof, badProofFlags, root, badProofLeaves)).to.equal(
false,
);
});
it('revert with invalid multi proof #1', async function () {
const fill = Buffer.alloc(32); // This could be anything, we are reconstructing a fake branch
const leaves = ['a', 'b', 'c', 'd'].map(keccak256).sort(Buffer.compare);
const badLeaf = keccak256('e');
const merkleTree = new MerkleTree(leaves, keccak256, { sort: true });
const root = merkleTree.getRoot();
await expectRevert(
this.merkleProof.$multiProofVerify(
[leaves[1], fill, merkleTree.layers[1][1]],
[false, false, false],
root,
[leaves[0], badLeaf], // A, E
),
'MerkleProof: invalid multiproof',
);
await expectRevert(
this.merkleProof.$multiProofVerifyCalldata(
[leaves[1], fill, merkleTree.layers[1][1]],
[false, false, false],
root,
[leaves[0], badLeaf], // A, E
),
'MerkleProof: invalid multiproof',
);
});
it('revert with invalid multi proof #2', async function () {
const fill = Buffer.alloc(32); // This could be anything, we are reconstructing a fake branch
const leaves = ['a', 'b', 'c', 'd'].map(keccak256).sort(Buffer.compare);
const badLeaf = keccak256('e');
const merkleTree = new MerkleTree(leaves, keccak256, { sort: true });
const root = merkleTree.getRoot();
await expectRevert(
this.merkleProof.$multiProofVerify(
[leaves[1], fill, merkleTree.layers[1][1]],
[false, false, false, false],
root,
[badLeaf, leaves[0]], // A, E
),
'reverted with panic code 0x32',
);
await expectRevert(
this.merkleProof.$multiProofVerifyCalldata(
[leaves[1], fill, merkleTree.layers[1][1]],
[false, false, false, false],
root,
[badLeaf, leaves[0]], // A, E
),
'reverted with panic code 0x32',
);
});
it('limit case: works for tree containing a single leaf', async function () {
const leaves = ['a'].map(keccak256).sort(Buffer.compare);
const merkleTree = new MerkleTree(leaves, keccak256, { sort: true });
const root = merkleTree.getRoot();
const proofLeaves = ['a'].map(keccak256).sort(Buffer.compare);
const proof = merkleTree.getMultiProof(proofLeaves);
const proofFlags = merkleTree.getProofFlags(proofLeaves, proof);
expect(await this.merkleProof.$multiProofVerify(proof, proofFlags, root, proofLeaves)).to.equal(true);
expect(await this.merkleProof.$multiProofVerifyCalldata(proof, proofFlags, root, proofLeaves)).to.equal(true);
});
it('limit case: can prove empty leaves', async function () {
const leaves = ['a', 'b', 'c', 'd'].map(keccak256).sort(Buffer.compare);
const merkleTree = new MerkleTree(leaves, keccak256, { sort: true });
const root = merkleTree.getRoot();
expect(await this.merkleProof.$multiProofVerify([root], [], root, [])).to.equal(true);
expect(await this.merkleProof.$multiProofVerifyCalldata([root], [], root, [])).to.equal(true);
});
});
});
@@ -0,0 +1,87 @@
const { toEthSignedMessageHash } = require('../../helpers/sign');
const { expect } = require('chai');
const SignatureChecker = artifacts.require('$SignatureChecker');
const ERC1271WalletMock = artifacts.require('ERC1271WalletMock');
const ERC1271MaliciousMock = artifacts.require('ERC1271MaliciousMock');
const TEST_MESSAGE = web3.utils.sha3('OpenZeppelin');
const WRONG_MESSAGE = web3.utils.sha3('Nope');
contract('SignatureChecker (ERC1271)', function (accounts) {
const [signer, other] = accounts;
before('deploying', async function () {
this.signaturechecker = await SignatureChecker.new();
this.wallet = await ERC1271WalletMock.new(signer);
this.malicious = await ERC1271MaliciousMock.new();
this.signature = await web3.eth.sign(TEST_MESSAGE, signer);
});
context('EOA account', function () {
it('with matching signer and signature', async function () {
expect(
await this.signaturechecker.$isValidSignatureNow(signer, toEthSignedMessageHash(TEST_MESSAGE), this.signature),
).to.equal(true);
});
it('with invalid signer', async function () {
expect(
await this.signaturechecker.$isValidSignatureNow(other, toEthSignedMessageHash(TEST_MESSAGE), this.signature),
).to.equal(false);
});
it('with invalid signature', async function () {
expect(
await this.signaturechecker.$isValidSignatureNow(signer, toEthSignedMessageHash(WRONG_MESSAGE), this.signature),
).to.equal(false);
});
});
context('ERC1271 wallet', function () {
for (const signature of ['isValidERC1271SignatureNow', 'isValidSignatureNow']) {
context(signature, function () {
it('with matching signer and signature', async function () {
expect(
await this.signaturechecker[`$${signature}`](
this.wallet.address,
toEthSignedMessageHash(TEST_MESSAGE),
this.signature,
),
).to.equal(true);
});
it('with invalid signer', async function () {
expect(
await this.signaturechecker[`$${signature}`](
this.signaturechecker.address,
toEthSignedMessageHash(TEST_MESSAGE),
this.signature,
),
).to.equal(false);
});
it('with invalid signature', async function () {
expect(
await this.signaturechecker[`$${signature}`](
this.wallet.address,
toEthSignedMessageHash(WRONG_MESSAGE),
this.signature,
),
).to.equal(false);
});
it('with malicious wallet', async function () {
expect(
await this.signaturechecker[`$${signature}`](
this.malicious.address,
toEthSignedMessageHash(TEST_MESSAGE),
this.signature,
),
).to.equal(false);
});
});
}
});
});
@@ -0,0 +1,37 @@
const { ether, expectRevert } = require('@openzeppelin/test-helpers');
const { shouldBehaveLikeEscrow } = require('./Escrow.behavior');
const ConditionalEscrowMock = artifacts.require('ConditionalEscrowMock');
contract('ConditionalEscrow', function (accounts) {
const [owner, payee, ...otherAccounts] = accounts;
beforeEach(async function () {
this.escrow = await ConditionalEscrowMock.new({ from: owner });
});
context('when withdrawal is allowed', function () {
beforeEach(async function () {
await Promise.all(otherAccounts.map(payee => this.escrow.setAllowed(payee, true)));
});
shouldBehaveLikeEscrow(owner, otherAccounts);
});
context('when withdrawal is disallowed', function () {
const amount = ether('23');
beforeEach(async function () {
await this.escrow.setAllowed(payee, false);
});
it('reverts on withdrawals', async function () {
await this.escrow.deposit(payee, { from: owner, value: amount });
await expectRevert(
this.escrow.withdraw(payee, { from: owner }),
'ConditionalEscrow: payee is not allowed to withdraw',
);
});
});
});
@@ -0,0 +1,90 @@
const { balance, ether, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
function shouldBehaveLikeEscrow(owner, [payee1, payee2]) {
const amount = ether('42');
describe('as an escrow', function () {
describe('deposits', function () {
it('can accept a single deposit', async function () {
await this.escrow.deposit(payee1, { from: owner, value: amount });
expect(await balance.current(this.escrow.address)).to.be.bignumber.equal(amount);
expect(await this.escrow.depositsOf(payee1)).to.be.bignumber.equal(amount);
});
it('can accept an empty deposit', async function () {
await this.escrow.deposit(payee1, { from: owner, value: 0 });
});
it('only the owner can deposit', async function () {
await expectRevert(this.escrow.deposit(payee1, { from: payee2 }), 'Ownable: caller is not the owner');
});
it('emits a deposited event', async function () {
const receipt = await this.escrow.deposit(payee1, { from: owner, value: amount });
expectEvent(receipt, 'Deposited', {
payee: payee1,
weiAmount: amount,
});
});
it('can add multiple deposits on a single account', async function () {
await this.escrow.deposit(payee1, { from: owner, value: amount });
await this.escrow.deposit(payee1, { from: owner, value: amount.muln(2) });
expect(await balance.current(this.escrow.address)).to.be.bignumber.equal(amount.muln(3));
expect(await this.escrow.depositsOf(payee1)).to.be.bignumber.equal(amount.muln(3));
});
it('can track deposits to multiple accounts', async function () {
await this.escrow.deposit(payee1, { from: owner, value: amount });
await this.escrow.deposit(payee2, { from: owner, value: amount.muln(2) });
expect(await balance.current(this.escrow.address)).to.be.bignumber.equal(amount.muln(3));
expect(await this.escrow.depositsOf(payee1)).to.be.bignumber.equal(amount);
expect(await this.escrow.depositsOf(payee2)).to.be.bignumber.equal(amount.muln(2));
});
});
describe('withdrawals', async function () {
it('can withdraw payments', async function () {
const balanceTracker = await balance.tracker(payee1);
await this.escrow.deposit(payee1, { from: owner, value: amount });
await this.escrow.withdraw(payee1, { from: owner });
expect(await balanceTracker.delta()).to.be.bignumber.equal(amount);
expect(await balance.current(this.escrow.address)).to.be.bignumber.equal('0');
expect(await this.escrow.depositsOf(payee1)).to.be.bignumber.equal('0');
});
it('can do an empty withdrawal', async function () {
await this.escrow.withdraw(payee1, { from: owner });
});
it('only the owner can withdraw', async function () {
await expectRevert(this.escrow.withdraw(payee1, { from: payee1 }), 'Ownable: caller is not the owner');
});
it('emits a withdrawn event', async function () {
await this.escrow.deposit(payee1, { from: owner, value: amount });
const receipt = await this.escrow.withdraw(payee1, { from: owner });
expectEvent(receipt, 'Withdrawn', {
payee: payee1,
weiAmount: amount,
});
});
});
});
}
module.exports = {
shouldBehaveLikeEscrow,
};
@@ -0,0 +1,14 @@
require('@openzeppelin/test-helpers');
const { shouldBehaveLikeEscrow } = require('./Escrow.behavior');
const Escrow = artifacts.require('Escrow');
contract('Escrow', function (accounts) {
const [owner, ...otherAccounts] = accounts;
beforeEach(async function () {
this.escrow = await Escrow.new({ from: owner });
});
shouldBehaveLikeEscrow(owner, otherAccounts);
});
@@ -0,0 +1,143 @@
const { balance, constants, ether, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
const { ZERO_ADDRESS } = constants;
const { expect } = require('chai');
const RefundEscrow = artifacts.require('RefundEscrow');
contract('RefundEscrow', function (accounts) {
const [owner, beneficiary, refundee1, refundee2] = accounts;
const amount = ether('54');
const refundees = [refundee1, refundee2];
it('requires a non-null beneficiary', async function () {
await expectRevert(
RefundEscrow.new(ZERO_ADDRESS, { from: owner }),
'RefundEscrow: beneficiary is the zero address',
);
});
context('once deployed', function () {
beforeEach(async function () {
this.escrow = await RefundEscrow.new(beneficiary, { from: owner });
});
context('active state', function () {
it('has beneficiary and state', async function () {
expect(await this.escrow.beneficiary()).to.equal(beneficiary);
expect(await this.escrow.state()).to.be.bignumber.equal('0');
});
it('accepts deposits', async function () {
await this.escrow.deposit(refundee1, { from: owner, value: amount });
expect(await this.escrow.depositsOf(refundee1)).to.be.bignumber.equal(amount);
});
it('does not refund refundees', async function () {
await this.escrow.deposit(refundee1, { from: owner, value: amount });
await expectRevert(this.escrow.withdraw(refundee1), 'ConditionalEscrow: payee is not allowed to withdraw');
});
it('does not allow beneficiary withdrawal', async function () {
await this.escrow.deposit(refundee1, { from: owner, value: amount });
await expectRevert(
this.escrow.beneficiaryWithdraw(),
'RefundEscrow: beneficiary can only withdraw while closed',
);
});
});
it('only the owner can enter closed state', async function () {
await expectRevert(this.escrow.close({ from: beneficiary }), 'Ownable: caller is not the owner');
const receipt = await this.escrow.close({ from: owner });
expectEvent(receipt, 'RefundsClosed');
});
context('closed state', function () {
beforeEach(async function () {
await Promise.all(refundees.map(refundee => this.escrow.deposit(refundee, { from: owner, value: amount })));
await this.escrow.close({ from: owner });
});
it('rejects deposits', async function () {
await expectRevert(
this.escrow.deposit(refundee1, { from: owner, value: amount }),
'RefundEscrow: can only deposit while active',
);
});
it('does not refund refundees', async function () {
await expectRevert(this.escrow.withdraw(refundee1), 'ConditionalEscrow: payee is not allowed to withdraw');
});
it('allows beneficiary withdrawal', async function () {
const balanceTracker = await balance.tracker(beneficiary);
await this.escrow.beneficiaryWithdraw();
expect(await balanceTracker.delta()).to.be.bignumber.equal(amount.muln(refundees.length));
});
it('prevents entering the refund state', async function () {
await expectRevert(
this.escrow.enableRefunds({ from: owner }),
'RefundEscrow: can only enable refunds while active',
);
});
it('prevents re-entering the closed state', async function () {
await expectRevert(this.escrow.close({ from: owner }), 'RefundEscrow: can only close while active');
});
});
it('only the owner can enter refund state', async function () {
await expectRevert(this.escrow.enableRefunds({ from: beneficiary }), 'Ownable: caller is not the owner');
const receipt = await this.escrow.enableRefunds({ from: owner });
expectEvent(receipt, 'RefundsEnabled');
});
context('refund state', function () {
beforeEach(async function () {
await Promise.all(refundees.map(refundee => this.escrow.deposit(refundee, { from: owner, value: amount })));
await this.escrow.enableRefunds({ from: owner });
});
it('rejects deposits', async function () {
await expectRevert(
this.escrow.deposit(refundee1, { from: owner, value: amount }),
'RefundEscrow: can only deposit while active',
);
});
it('refunds refundees', async function () {
for (const refundee of [refundee1, refundee2]) {
const balanceTracker = await balance.tracker(refundee);
await this.escrow.withdraw(refundee, { from: owner });
expect(await balanceTracker.delta()).to.be.bignumber.equal(amount);
}
});
it('does not allow beneficiary withdrawal', async function () {
await expectRevert(
this.escrow.beneficiaryWithdraw(),
'RefundEscrow: beneficiary can only withdraw while closed',
);
});
it('prevents entering the closed state', async function () {
await expectRevert(this.escrow.close({ from: owner }), 'RefundEscrow: can only close while active');
});
it('prevents re-entering the refund state', async function () {
await expectRevert(
this.escrow.enableRefunds({ from: owner }),
'RefundEscrow: can only enable refunds while active',
);
});
});
});
});
@@ -0,0 +1,11 @@
const { shouldSupportInterfaces } = require('./SupportsInterface.behavior');
const ERC165 = artifacts.require('$ERC165');
contract('ERC165', function () {
beforeEach(async function () {
this.mock = await ERC165.new();
});
shouldSupportInterfaces(['ERC165']);
});
@@ -0,0 +1,302 @@
require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const ERC165Checker = artifacts.require('$ERC165Checker');
const ERC165Storage = artifacts.require('$ERC165Storage');
const ERC165MissingData = artifacts.require('ERC165MissingData');
const ERC165MaliciousData = artifacts.require('ERC165MaliciousData');
const ERC165NotSupported = artifacts.require('ERC165NotSupported');
const ERC165ReturnBombMock = artifacts.require('ERC165ReturnBombMock');
const DUMMY_ID = '0xdeadbeef';
const DUMMY_ID_2 = '0xcafebabe';
const DUMMY_ID_3 = '0xdecafbad';
const DUMMY_UNSUPPORTED_ID = '0xbaddcafe';
const DUMMY_UNSUPPORTED_ID_2 = '0xbaadcafe';
const DUMMY_ACCOUNT = '0x1111111111111111111111111111111111111111';
contract('ERC165Checker', function () {
beforeEach(async function () {
this.mock = await ERC165Checker.new();
});
context('ERC165 missing return data', function () {
beforeEach(async function () {
this.target = await ERC165MissingData.new();
});
it('does not support ERC165', async function () {
const supported = await this.mock.$supportsERC165(this.target.address);
expect(supported).to.equal(false);
});
it('does not support mock interface via supportsInterface', async function () {
const supported = await this.mock.$supportsInterface(this.target.address, DUMMY_ID);
expect(supported).to.equal(false);
});
it('does not support mock interface via supportsAllInterfaces', async function () {
const supported = await this.mock.$supportsAllInterfaces(this.target.address, [DUMMY_ID]);
expect(supported).to.equal(false);
});
it('does not support mock interface via getSupportedInterfaces', async function () {
const supported = await this.mock.$getSupportedInterfaces(this.target.address, [DUMMY_ID]);
expect(supported.length).to.equal(1);
expect(supported[0]).to.equal(false);
});
it('does not support mock interface via supportsERC165InterfaceUnchecked', async function () {
const supported = await this.mock.$supportsERC165InterfaceUnchecked(this.target.address, DUMMY_ID);
expect(supported).to.equal(false);
});
});
context('ERC165 malicious return data', function () {
beforeEach(async function () {
this.target = await ERC165MaliciousData.new();
});
it('does not support ERC165', async function () {
const supported = await this.mock.$supportsERC165(this.target.address);
expect(supported).to.equal(false);
});
it('does not support mock interface via supportsInterface', async function () {
const supported = await this.mock.$supportsInterface(this.target.address, DUMMY_ID);
expect(supported).to.equal(false);
});
it('does not support mock interface via supportsAllInterfaces', async function () {
const supported = await this.mock.$supportsAllInterfaces(this.target.address, [DUMMY_ID]);
expect(supported).to.equal(false);
});
it('does not support mock interface via getSupportedInterfaces', async function () {
const supported = await this.mock.$getSupportedInterfaces(this.target.address, [DUMMY_ID]);
expect(supported.length).to.equal(1);
expect(supported[0]).to.equal(false);
});
it('does not support mock interface via supportsERC165InterfaceUnchecked', async function () {
const supported = await this.mock.$supportsERC165InterfaceUnchecked(this.target.address, DUMMY_ID);
expect(supported).to.equal(true);
});
});
context('ERC165 not supported', function () {
beforeEach(async function () {
this.target = await ERC165NotSupported.new();
});
it('does not support ERC165', async function () {
const supported = await this.mock.$supportsERC165(this.target.address);
expect(supported).to.equal(false);
});
it('does not support mock interface via supportsInterface', async function () {
const supported = await this.mock.$supportsInterface(this.target.address, DUMMY_ID);
expect(supported).to.equal(false);
});
it('does not support mock interface via supportsAllInterfaces', async function () {
const supported = await this.mock.$supportsAllInterfaces(this.target.address, [DUMMY_ID]);
expect(supported).to.equal(false);
});
it('does not support mock interface via getSupportedInterfaces', async function () {
const supported = await this.mock.$getSupportedInterfaces(this.target.address, [DUMMY_ID]);
expect(supported.length).to.equal(1);
expect(supported[0]).to.equal(false);
});
it('does not support mock interface via supportsERC165InterfaceUnchecked', async function () {
const supported = await this.mock.$supportsERC165InterfaceUnchecked(this.target.address, DUMMY_ID);
expect(supported).to.equal(false);
});
});
context('ERC165 supported', function () {
beforeEach(async function () {
this.target = await ERC165Storage.new();
});
it('supports ERC165', async function () {
const supported = await this.mock.$supportsERC165(this.target.address);
expect(supported).to.equal(true);
});
it('does not support mock interface via supportsInterface', async function () {
const supported = await this.mock.$supportsInterface(this.target.address, DUMMY_ID);
expect(supported).to.equal(false);
});
it('does not support mock interface via supportsAllInterfaces', async function () {
const supported = await this.mock.$supportsAllInterfaces(this.target.address, [DUMMY_ID]);
expect(supported).to.equal(false);
});
it('does not support mock interface via getSupportedInterfaces', async function () {
const supported = await this.mock.$getSupportedInterfaces(this.target.address, [DUMMY_ID]);
expect(supported.length).to.equal(1);
expect(supported[0]).to.equal(false);
});
it('does not support mock interface via supportsERC165InterfaceUnchecked', async function () {
const supported = await this.mock.$supportsERC165InterfaceUnchecked(this.target.address, DUMMY_ID);
expect(supported).to.equal(false);
});
});
context('ERC165 and single interface supported', function () {
beforeEach(async function () {
this.target = await ERC165Storage.new();
await this.target.$_registerInterface(DUMMY_ID);
});
it('supports ERC165', async function () {
const supported = await this.mock.$supportsERC165(this.target.address);
expect(supported).to.equal(true);
});
it('supports mock interface via supportsInterface', async function () {
const supported = await this.mock.$supportsInterface(this.target.address, DUMMY_ID);
expect(supported).to.equal(true);
});
it('supports mock interface via supportsAllInterfaces', async function () {
const supported = await this.mock.$supportsAllInterfaces(this.target.address, [DUMMY_ID]);
expect(supported).to.equal(true);
});
it('supports mock interface via getSupportedInterfaces', async function () {
const supported = await this.mock.$getSupportedInterfaces(this.target.address, [DUMMY_ID]);
expect(supported.length).to.equal(1);
expect(supported[0]).to.equal(true);
});
it('supports mock interface via supportsERC165InterfaceUnchecked', async function () {
const supported = await this.mock.$supportsERC165InterfaceUnchecked(this.target.address, DUMMY_ID);
expect(supported).to.equal(true);
});
});
context('ERC165 and many interfaces supported', function () {
beforeEach(async function () {
this.supportedInterfaces = [DUMMY_ID, DUMMY_ID_2, DUMMY_ID_3];
this.target = await ERC165Storage.new();
await Promise.all(this.supportedInterfaces.map(interfaceId => this.target.$_registerInterface(interfaceId)));
});
it('supports ERC165', async function () {
const supported = await this.mock.$supportsERC165(this.target.address);
expect(supported).to.equal(true);
});
it('supports each interfaceId via supportsInterface', async function () {
for (const interfaceId of this.supportedInterfaces) {
const supported = await this.mock.$supportsInterface(this.target.address, interfaceId);
expect(supported).to.equal(true);
}
});
it('supports all interfaceIds via supportsAllInterfaces', async function () {
const supported = await this.mock.$supportsAllInterfaces(this.target.address, this.supportedInterfaces);
expect(supported).to.equal(true);
});
it('supports none of the interfaces queried via supportsAllInterfaces', async function () {
const interfaceIdsToTest = [DUMMY_UNSUPPORTED_ID, DUMMY_UNSUPPORTED_ID_2];
const supported = await this.mock.$supportsAllInterfaces(this.target.address, interfaceIdsToTest);
expect(supported).to.equal(false);
});
it('supports not all of the interfaces queried via supportsAllInterfaces', async function () {
const interfaceIdsToTest = [...this.supportedInterfaces, DUMMY_UNSUPPORTED_ID];
const supported = await this.mock.$supportsAllInterfaces(this.target.address, interfaceIdsToTest);
expect(supported).to.equal(false);
});
it('supports all interfaceIds via getSupportedInterfaces', async function () {
const supported = await this.mock.$getSupportedInterfaces(this.target.address, this.supportedInterfaces);
expect(supported.length).to.equal(3);
expect(supported[0]).to.equal(true);
expect(supported[1]).to.equal(true);
expect(supported[2]).to.equal(true);
});
it('supports none of the interfaces queried via getSupportedInterfaces', async function () {
const interfaceIdsToTest = [DUMMY_UNSUPPORTED_ID, DUMMY_UNSUPPORTED_ID_2];
const supported = await this.mock.$getSupportedInterfaces(this.target.address, interfaceIdsToTest);
expect(supported.length).to.equal(2);
expect(supported[0]).to.equal(false);
expect(supported[1]).to.equal(false);
});
it('supports not all of the interfaces queried via getSupportedInterfaces', async function () {
const interfaceIdsToTest = [...this.supportedInterfaces, DUMMY_UNSUPPORTED_ID];
const supported = await this.mock.$getSupportedInterfaces(this.target.address, interfaceIdsToTest);
expect(supported.length).to.equal(4);
expect(supported[0]).to.equal(true);
expect(supported[1]).to.equal(true);
expect(supported[2]).to.equal(true);
expect(supported[3]).to.equal(false);
});
it('supports each interfaceId via supportsERC165InterfaceUnchecked', async function () {
for (const interfaceId of this.supportedInterfaces) {
const supported = await this.mock.$supportsERC165InterfaceUnchecked(this.target.address, interfaceId);
expect(supported).to.equal(true);
}
});
});
context('account address does not support ERC165', function () {
it('does not support ERC165', async function () {
const supported = await this.mock.$supportsERC165(DUMMY_ACCOUNT);
expect(supported).to.equal(false);
});
it('does not support mock interface via supportsInterface', async function () {
const supported = await this.mock.$supportsInterface(DUMMY_ACCOUNT, DUMMY_ID);
expect(supported).to.equal(false);
});
it('does not support mock interface via supportsAllInterfaces', async function () {
const supported = await this.mock.$supportsAllInterfaces(DUMMY_ACCOUNT, [DUMMY_ID]);
expect(supported).to.equal(false);
});
it('does not support mock interface via getSupportedInterfaces', async function () {
const supported = await this.mock.$getSupportedInterfaces(DUMMY_ACCOUNT, [DUMMY_ID]);
expect(supported.length).to.equal(1);
expect(supported[0]).to.equal(false);
});
it('does not support mock interface via supportsERC165InterfaceUnchecked', async function () {
const supported = await this.mock.$supportsERC165InterfaceUnchecked(DUMMY_ACCOUNT, DUMMY_ID);
expect(supported).to.equal(false);
});
});
it('Return bomb resistance', async function () {
this.target = await ERC165ReturnBombMock.new();
const tx1 = await this.mock.$supportsInterface.sendTransaction(this.target.address, DUMMY_ID);
expect(tx1.receipt.gasUsed).to.be.lessThan(120000); // 3*30k + 21k + some margin
const tx2 = await this.mock.$getSupportedInterfaces.sendTransaction(this.target.address, [
DUMMY_ID,
DUMMY_ID_2,
DUMMY_ID_3,
DUMMY_UNSUPPORTED_ID,
DUMMY_UNSUPPORTED_ID_2,
]);
expect(tx2.receipt.gasUsed).to.be.lessThan(250000); // (2+5)*30k + 21k + some margin
});
});
@@ -0,0 +1,23 @@
const { expectRevert } = require('@openzeppelin/test-helpers');
const { shouldSupportInterfaces } = require('./SupportsInterface.behavior');
const ERC165Storage = artifacts.require('$ERC165Storage');
contract('ERC165Storage', function () {
beforeEach(async function () {
this.mock = await ERC165Storage.new();
});
it('register interface', async function () {
expect(await this.mock.supportsInterface('0x00000001')).to.be.equal(false);
await this.mock.$_registerInterface('0x00000001');
expect(await this.mock.supportsInterface('0x00000001')).to.be.equal(true);
});
it('does not allow 0xffffffff', async function () {
await expectRevert(this.mock.$_registerInterface('0xffffffff'), 'ERC165: invalid interface id');
});
shouldSupportInterfaces(['ERC165']);
});
@@ -0,0 +1,71 @@
const { expectRevert, singletons } = require('@openzeppelin/test-helpers');
const { bufferToHex, keccakFromString } = require('ethereumjs-util');
const { expect } = require('chai');
const ERC1820Implementer = artifacts.require('$ERC1820Implementer');
contract('ERC1820Implementer', function (accounts) {
const [registryFunder, implementee, other] = accounts;
const ERC1820_ACCEPT_MAGIC = bufferToHex(keccakFromString('ERC1820_ACCEPT_MAGIC'));
beforeEach(async function () {
this.implementer = await ERC1820Implementer.new();
this.registry = await singletons.ERC1820Registry(registryFunder);
this.interfaceA = bufferToHex(keccakFromString('interfaceA'));
this.interfaceB = bufferToHex(keccakFromString('interfaceB'));
});
context('with no registered interfaces', function () {
it('returns false when interface implementation is queried', async function () {
expect(await this.implementer.canImplementInterfaceForAddress(this.interfaceA, implementee)).to.not.equal(
ERC1820_ACCEPT_MAGIC,
);
});
it('reverts when attempting to set as implementer in the registry', async function () {
await expectRevert(
this.registry.setInterfaceImplementer(implementee, this.interfaceA, this.implementer.address, {
from: implementee,
}),
'Does not implement the interface',
);
});
});
context('with registered interfaces', function () {
beforeEach(async function () {
await this.implementer.$_registerInterfaceForAddress(this.interfaceA, implementee);
});
it('returns true when interface implementation is queried', async function () {
expect(await this.implementer.canImplementInterfaceForAddress(this.interfaceA, implementee)).to.equal(
ERC1820_ACCEPT_MAGIC,
);
});
it('returns false when interface implementation for non-supported interfaces is queried', async function () {
expect(await this.implementer.canImplementInterfaceForAddress(this.interfaceB, implementee)).to.not.equal(
ERC1820_ACCEPT_MAGIC,
);
});
it('returns false when interface implementation for non-supported addresses is queried', async function () {
expect(await this.implementer.canImplementInterfaceForAddress(this.interfaceA, other)).to.not.equal(
ERC1820_ACCEPT_MAGIC,
);
});
it('can be set as an implementer for supported interfaces in the registry', async function () {
await this.registry.setInterfaceImplementer(implementee, this.interfaceA, this.implementer.address, {
from: implementee,
});
expect(await this.registry.getInterfaceImplementer(implementee, this.interfaceA)).to.equal(
this.implementer.address,
);
});
});
});
@@ -0,0 +1,146 @@
const { makeInterfaceId } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const INTERFACES = {
ERC165: ['supportsInterface(bytes4)'],
ERC721: [
'balanceOf(address)',
'ownerOf(uint256)',
'approve(address,uint256)',
'getApproved(uint256)',
'setApprovalForAll(address,bool)',
'isApprovedForAll(address,address)',
'transferFrom(address,address,uint256)',
'safeTransferFrom(address,address,uint256)',
'safeTransferFrom(address,address,uint256,bytes)',
],
ERC721Enumerable: ['totalSupply()', 'tokenOfOwnerByIndex(address,uint256)', 'tokenByIndex(uint256)'],
ERC721Metadata: ['name()', 'symbol()', 'tokenURI(uint256)'],
ERC1155: [
'balanceOf(address,uint256)',
'balanceOfBatch(address[],uint256[])',
'setApprovalForAll(address,bool)',
'isApprovedForAll(address,address)',
'safeTransferFrom(address,address,uint256,uint256,bytes)',
'safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)',
],
ERC1155Receiver: [
'onERC1155Received(address,address,uint256,uint256,bytes)',
'onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)',
],
AccessControl: [
'hasRole(bytes32,address)',
'getRoleAdmin(bytes32)',
'grantRole(bytes32,address)',
'revokeRole(bytes32,address)',
'renounceRole(bytes32,address)',
],
AccessControlEnumerable: ['getRoleMember(bytes32,uint256)', 'getRoleMemberCount(bytes32)'],
AccessControlDefaultAdminRules: [
'defaultAdminDelay()',
'pendingDefaultAdminDelay()',
'defaultAdmin()',
'pendingDefaultAdmin()',
'defaultAdminDelayIncreaseWait()',
'changeDefaultAdminDelay(uint48)',
'rollbackDefaultAdminDelay()',
'beginDefaultAdminTransfer(address)',
'acceptDefaultAdminTransfer()',
'cancelDefaultAdminTransfer()',
],
Governor: [
'name()',
'version()',
'COUNTING_MODE()',
'hashProposal(address[],uint256[],bytes[],bytes32)',
'state(uint256)',
'proposalSnapshot(uint256)',
'proposalDeadline(uint256)',
'votingDelay()',
'votingPeriod()',
'quorum(uint256)',
'getVotes(address,uint256)',
'hasVoted(uint256,address)',
'propose(address[],uint256[],bytes[],string)',
'execute(address[],uint256[],bytes[],bytes32)',
'castVote(uint256,uint8)',
'castVoteWithReason(uint256,uint8,string)',
'castVoteBySig(uint256,uint8,uint8,bytes32,bytes32)',
],
GovernorWithParams: [
'name()',
'version()',
'COUNTING_MODE()',
'hashProposal(address[],uint256[],bytes[],bytes32)',
'state(uint256)',
'proposalSnapshot(uint256)',
'proposalDeadline(uint256)',
'votingDelay()',
'votingPeriod()',
'quorum(uint256)',
'getVotes(address,uint256)',
'getVotesWithParams(address,uint256,bytes)',
'hasVoted(uint256,address)',
'propose(address[],uint256[],bytes[],string)',
'execute(address[],uint256[],bytes[],bytes32)',
'castVote(uint256,uint8)',
'castVoteWithReason(uint256,uint8,string)',
'castVoteWithReasonAndParams(uint256,uint8,string,bytes)',
'castVoteBySig(uint256,uint8,uint8,bytes32,bytes32)',
'castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32)',
],
GovernorCancel: ['proposalProposer(uint256)', 'cancel(address[],uint256[],bytes[],bytes32)'],
GovernorTimelock: ['timelock()', 'proposalEta(uint256)', 'queue(address[],uint256[],bytes[],bytes32)'],
ERC2981: ['royaltyInfo(uint256,uint256)'],
};
const INTERFACE_IDS = {};
const FN_SIGNATURES = {};
for (const k of Object.getOwnPropertyNames(INTERFACES)) {
INTERFACE_IDS[k] = makeInterfaceId.ERC165(INTERFACES[k]);
for (const fnName of INTERFACES[k]) {
// the interface id of a single function is equivalent to its function signature
FN_SIGNATURES[fnName] = makeInterfaceId.ERC165([fnName]);
}
}
function shouldSupportInterfaces(interfaces = []) {
describe('ERC165', function () {
beforeEach(function () {
this.contractUnderTest = this.mock || this.token || this.holder || this.accessControl;
});
it('supportsInterface uses less than 30k gas', async function () {
for (const k of interfaces) {
const interfaceId = INTERFACE_IDS[k] ?? k;
expect(await this.contractUnderTest.supportsInterface.estimateGas(interfaceId)).to.be.lte(30000);
}
});
it('all interfaces are reported as supported', async function () {
for (const k of interfaces) {
const interfaceId = INTERFACE_IDS[k] ?? k;
expect(await this.contractUnderTest.supportsInterface(interfaceId)).to.equal(true, `does not support ${k}`);
}
});
it('all interface functions are in ABI', async function () {
for (const k of interfaces) {
// skip interfaces for which we don't have a function list
if (INTERFACES[k] === undefined) continue;
for (const fnName of INTERFACES[k]) {
const fnSig = FN_SIGNATURES[fnName];
expect(this.contractUnderTest.abi.filter(fn => fn.signature === fnSig).length).to.equal(
1,
`did not find ${fnName}`,
);
}
}
});
});
}
module.exports = {
shouldSupportInterfaces,
};
@@ -0,0 +1,217 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../../../contracts/utils/math/Math.sol";
import "../../../contracts/utils/math/SafeMath.sol";
contract MathTest is Test {
// CEILDIV
function testCeilDiv(uint256 a, uint256 b) public {
vm.assume(b > 0);
uint256 result = Math.ceilDiv(a, b);
if (result == 0) {
assertEq(a, 0);
} else {
uint256 maxdiv = UINT256_MAX / b;
bool overflow = maxdiv * b < a;
assertTrue(a > b * (result - 1));
assertTrue(overflow ? result == maxdiv + 1 : a <= b * result);
}
}
// SQRT
function testSqrt(uint256 input, uint8 r) public {
Math.Rounding rounding = _asRounding(r);
uint256 result = Math.sqrt(input, rounding);
// square of result is bigger than input
if (_squareBigger(result, input)) {
assertTrue(rounding == Math.Rounding.Up);
assertTrue(_squareSmaller(result - 1, input));
}
// square of result is smaller than input
else if (_squareSmaller(result, input)) {
assertFalse(rounding == Math.Rounding.Up);
assertTrue(_squareBigger(result + 1, input));
}
// input is perfect square
else {
assertEq(result * result, input);
}
}
function _squareBigger(uint256 value, uint256 ref) private pure returns (bool) {
(bool noOverflow, uint256 square) = SafeMath.tryMul(value, value);
return !noOverflow || square > ref;
}
function _squareSmaller(uint256 value, uint256 ref) private pure returns (bool) {
return value * value < ref;
}
// LOG2
function testLog2(uint256 input, uint8 r) public {
Math.Rounding rounding = _asRounding(r);
uint256 result = Math.log2(input, rounding);
if (input == 0) {
assertEq(result, 0);
} else if (_powerOf2Bigger(result, input)) {
assertTrue(rounding == Math.Rounding.Up);
assertTrue(_powerOf2Smaller(result - 1, input));
} else if (_powerOf2Smaller(result, input)) {
assertFalse(rounding == Math.Rounding.Up);
assertTrue(_powerOf2Bigger(result + 1, input));
} else {
assertEq(2 ** result, input);
}
}
function _powerOf2Bigger(uint256 value, uint256 ref) private pure returns (bool) {
return value >= 256 || 2 ** value > ref; // 2**256 overflows uint256
}
function _powerOf2Smaller(uint256 value, uint256 ref) private pure returns (bool) {
return 2 ** value < ref;
}
// LOG10
function testLog10(uint256 input, uint8 r) public {
Math.Rounding rounding = _asRounding(r);
uint256 result = Math.log10(input, rounding);
if (input == 0) {
assertEq(result, 0);
} else if (_powerOf10Bigger(result, input)) {
assertTrue(rounding == Math.Rounding.Up);
assertTrue(_powerOf10Smaller(result - 1, input));
} else if (_powerOf10Smaller(result, input)) {
assertFalse(rounding == Math.Rounding.Up);
assertTrue(_powerOf10Bigger(result + 1, input));
} else {
assertEq(10 ** result, input);
}
}
function _powerOf10Bigger(uint256 value, uint256 ref) private pure returns (bool) {
return value >= 78 || 10 ** value > ref; // 10**78 overflows uint256
}
function _powerOf10Smaller(uint256 value, uint256 ref) private pure returns (bool) {
return 10 ** value < ref;
}
// LOG256
function testLog256(uint256 input, uint8 r) public {
Math.Rounding rounding = _asRounding(r);
uint256 result = Math.log256(input, rounding);
if (input == 0) {
assertEq(result, 0);
} else if (_powerOf256Bigger(result, input)) {
assertTrue(rounding == Math.Rounding.Up);
assertTrue(_powerOf256Smaller(result - 1, input));
} else if (_powerOf256Smaller(result, input)) {
assertFalse(rounding == Math.Rounding.Up);
assertTrue(_powerOf256Bigger(result + 1, input));
} else {
assertEq(256 ** result, input);
}
}
function _powerOf256Bigger(uint256 value, uint256 ref) private pure returns (bool) {
return value >= 32 || 256 ** value > ref; // 256**32 overflows uint256
}
function _powerOf256Smaller(uint256 value, uint256 ref) private pure returns (bool) {
return 256 ** value < ref;
}
// MULDIV
function testMulDiv(uint256 x, uint256 y, uint256 d) public {
// Full precision for x * y
(uint256 xyHi, uint256 xyLo) = _mulHighLow(x, y);
// Assume result won't overflow (see {testMulDivDomain})
// This also checks that `d` is positive
vm.assume(xyHi < d);
// Perform muldiv
uint256 q = Math.mulDiv(x, y, d);
// Full precision for q * d
(uint256 qdHi, uint256 qdLo) = _mulHighLow(q, d);
// Add remainder of x * y / d (computed as rem = (x * y % d))
(uint256 qdRemLo, uint256 c) = _addCarry(qdLo, _mulmod(x, y, d));
uint256 qdRemHi = qdHi + c;
// Full precision check that x * y = q * d + rem
assertEq(xyHi, qdRemHi);
assertEq(xyLo, qdRemLo);
}
function testMulDivDomain(uint256 x, uint256 y, uint256 d) public {
(uint256 xyHi, ) = _mulHighLow(x, y);
// Violate {testMulDiv} assumption (covers d is 0 and result overflow)
vm.assume(xyHi >= d);
// we are outside the scope of {testMulDiv}, we expect muldiv to revert
try this.muldiv(x, y, d) returns (uint256) {
fail();
} catch {}
}
// External call
function muldiv(uint256 x, uint256 y, uint256 d) external pure returns (uint256) {
return Math.mulDiv(x, y, d);
}
// Helpers
function _asRounding(uint8 r) private pure returns (Math.Rounding) {
vm.assume(r < uint8(type(Math.Rounding).max));
return Math.Rounding(r);
}
function _mulmod(uint256 x, uint256 y, uint256 z) private pure returns (uint256 r) {
assembly {
r := mulmod(x, y, z)
}
}
function _mulHighLow(uint256 x, uint256 y) private pure returns (uint256 high, uint256 low) {
(uint256 x0, uint256 x1) = (x & type(uint128).max, x >> 128);
(uint256 y0, uint256 y1) = (y & type(uint128).max, y >> 128);
// Karatsuba algorithm
// https://en.wikipedia.org/wiki/Karatsuba_algorithm
uint256 z2 = x1 * y1;
uint256 z1a = x1 * y0;
uint256 z1b = x0 * y1;
uint256 z0 = x0 * y0;
uint256 carry = ((z1a & type(uint128).max) + (z1b & type(uint128).max) + (z0 >> 128)) >> 128;
high = z2 + (z1a >> 128) + (z1b >> 128) + carry;
unchecked {
low = x * y;
}
}
function _addCarry(uint256 x, uint256 y) private pure returns (uint256 res, uint256 carry) {
unchecked {
res = x + y;
}
carry = res < x ? 1 : 0;
}
}
@@ -0,0 +1,289 @@
const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const { MAX_UINT256 } = constants;
const { Rounding } = require('../../helpers/enums.js');
const Math = artifacts.require('$Math');
contract('Math', function () {
const min = new BN('1234');
const max = new BN('5678');
const MAX_UINT256_SUB1 = MAX_UINT256.sub(new BN('1'));
const MAX_UINT256_SUB2 = MAX_UINT256.sub(new BN('2'));
beforeEach(async function () {
this.math = await Math.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 two odd numbers', async function () {
const a = new BN('57417');
const b = new BN('95431');
expect(await this.math.$average(a, b)).to.be.bignumber.equal(bnAverage(a, b));
});
it('is correctly calculated with two even numbers', async function () {
const a = new BN('42304');
const b = new BN('84346');
expect(await this.math.$average(a, b)).to.be.bignumber.equal(bnAverage(a, b));
});
it('is correctly calculated with one even and one odd number', async function () {
const a = new BN('57417');
const b = new BN('84346');
expect(await this.math.$average(a, b)).to.be.bignumber.equal(bnAverage(a, b));
});
it('is correctly calculated with two max uint256 numbers', async function () {
const a = MAX_UINT256;
expect(await this.math.$average(a, a)).to.be.bignumber.equal(bnAverage(a, a));
});
});
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);
});
});
describe('muldiv', function () {
it('divide by 0', async function () {
await expectRevert.unspecified(this.math.$mulDiv(1, 1, 0, Rounding.Down));
});
describe('does round down', async function () {
it('small values', async function () {
expect(await this.math.$mulDiv('3', '4', '5', Rounding.Down)).to.be.bignumber.equal('2');
expect(await this.math.$mulDiv('3', '5', '5', Rounding.Down)).to.be.bignumber.equal('3');
});
it('large values', async function () {
expect(
await this.math.$mulDiv(new BN('42'), MAX_UINT256_SUB1, MAX_UINT256, Rounding.Down),
).to.be.bignumber.equal(new BN('41'));
expect(await this.math.$mulDiv(new BN('17'), MAX_UINT256, MAX_UINT256, Rounding.Down)).to.be.bignumber.equal(
new BN('17'),
);
expect(
await this.math.$mulDiv(MAX_UINT256_SUB1, MAX_UINT256_SUB1, MAX_UINT256, Rounding.Down),
).to.be.bignumber.equal(MAX_UINT256_SUB2);
expect(
await this.math.$mulDiv(MAX_UINT256, MAX_UINT256_SUB1, MAX_UINT256, Rounding.Down),
).to.be.bignumber.equal(MAX_UINT256_SUB1);
expect(await this.math.$mulDiv(MAX_UINT256, MAX_UINT256, MAX_UINT256, Rounding.Down)).to.be.bignumber.equal(
MAX_UINT256,
);
});
});
describe('does round up', async function () {
it('small values', async function () {
expect(await this.math.$mulDiv('3', '4', '5', Rounding.Up)).to.be.bignumber.equal('3');
expect(await this.math.$mulDiv('3', '5', '5', Rounding.Up)).to.be.bignumber.equal('3');
});
it('large values', async function () {
expect(await this.math.$mulDiv(new BN('42'), MAX_UINT256_SUB1, MAX_UINT256, Rounding.Up)).to.be.bignumber.equal(
new BN('42'),
);
expect(await this.math.$mulDiv(new BN('17'), MAX_UINT256, MAX_UINT256, Rounding.Up)).to.be.bignumber.equal(
new BN('17'),
);
expect(
await this.math.$mulDiv(MAX_UINT256_SUB1, MAX_UINT256_SUB1, MAX_UINT256, Rounding.Up),
).to.be.bignumber.equal(MAX_UINT256_SUB1);
expect(await this.math.$mulDiv(MAX_UINT256, MAX_UINT256_SUB1, MAX_UINT256, Rounding.Up)).to.be.bignumber.equal(
MAX_UINT256_SUB1,
);
expect(await this.math.$mulDiv(MAX_UINT256, MAX_UINT256, MAX_UINT256, Rounding.Up)).to.be.bignumber.equal(
MAX_UINT256,
);
});
});
});
describe('sqrt', function () {
it('rounds down', async function () {
expect(await this.math.$sqrt('0', Rounding.Down)).to.be.bignumber.equal('0');
expect(await this.math.$sqrt('1', Rounding.Down)).to.be.bignumber.equal('1');
expect(await this.math.$sqrt('2', Rounding.Down)).to.be.bignumber.equal('1');
expect(await this.math.$sqrt('3', Rounding.Down)).to.be.bignumber.equal('1');
expect(await this.math.$sqrt('4', Rounding.Down)).to.be.bignumber.equal('2');
expect(await this.math.$sqrt('144', Rounding.Down)).to.be.bignumber.equal('12');
expect(await this.math.$sqrt('999999', Rounding.Down)).to.be.bignumber.equal('999');
expect(await this.math.$sqrt('1000000', Rounding.Down)).to.be.bignumber.equal('1000');
expect(await this.math.$sqrt('1000001', Rounding.Down)).to.be.bignumber.equal('1000');
expect(await this.math.$sqrt('1002000', Rounding.Down)).to.be.bignumber.equal('1000');
expect(await this.math.$sqrt('1002001', Rounding.Down)).to.be.bignumber.equal('1001');
expect(await this.math.$sqrt(MAX_UINT256, Rounding.Down)).to.be.bignumber.equal(
'340282366920938463463374607431768211455',
);
});
it('rounds up', async function () {
expect(await this.math.$sqrt('0', Rounding.Up)).to.be.bignumber.equal('0');
expect(await this.math.$sqrt('1', Rounding.Up)).to.be.bignumber.equal('1');
expect(await this.math.$sqrt('2', Rounding.Up)).to.be.bignumber.equal('2');
expect(await this.math.$sqrt('3', Rounding.Up)).to.be.bignumber.equal('2');
expect(await this.math.$sqrt('4', Rounding.Up)).to.be.bignumber.equal('2');
expect(await this.math.$sqrt('144', Rounding.Up)).to.be.bignumber.equal('12');
expect(await this.math.$sqrt('999999', Rounding.Up)).to.be.bignumber.equal('1000');
expect(await this.math.$sqrt('1000000', Rounding.Up)).to.be.bignumber.equal('1000');
expect(await this.math.$sqrt('1000001', Rounding.Up)).to.be.bignumber.equal('1001');
expect(await this.math.$sqrt('1002000', Rounding.Up)).to.be.bignumber.equal('1001');
expect(await this.math.$sqrt('1002001', Rounding.Up)).to.be.bignumber.equal('1001');
expect(await this.math.$sqrt(MAX_UINT256, Rounding.Up)).to.be.bignumber.equal(
'340282366920938463463374607431768211456',
);
});
});
describe('log', function () {
describe('log2', function () {
it('rounds down', async function () {
// For some reason calling .$log2() directly fails
expect(await this.math.methods['$log2(uint256,uint8)']('0', Rounding.Down)).to.be.bignumber.equal('0');
expect(await this.math.methods['$log2(uint256,uint8)']('1', Rounding.Down)).to.be.bignumber.equal('0');
expect(await this.math.methods['$log2(uint256,uint8)']('2', Rounding.Down)).to.be.bignumber.equal('1');
expect(await this.math.methods['$log2(uint256,uint8)']('3', Rounding.Down)).to.be.bignumber.equal('1');
expect(await this.math.methods['$log2(uint256,uint8)']('4', Rounding.Down)).to.be.bignumber.equal('2');
expect(await this.math.methods['$log2(uint256,uint8)']('5', Rounding.Down)).to.be.bignumber.equal('2');
expect(await this.math.methods['$log2(uint256,uint8)']('6', Rounding.Down)).to.be.bignumber.equal('2');
expect(await this.math.methods['$log2(uint256,uint8)']('7', Rounding.Down)).to.be.bignumber.equal('2');
expect(await this.math.methods['$log2(uint256,uint8)']('8', Rounding.Down)).to.be.bignumber.equal('3');
expect(await this.math.methods['$log2(uint256,uint8)']('9', Rounding.Down)).to.be.bignumber.equal('3');
expect(await this.math.methods['$log2(uint256,uint8)'](MAX_UINT256, Rounding.Down)).to.be.bignumber.equal(
'255',
);
});
it('rounds up', async function () {
// For some reason calling .$log2() directly fails
expect(await this.math.methods['$log2(uint256,uint8)']('0', Rounding.Up)).to.be.bignumber.equal('0');
expect(await this.math.methods['$log2(uint256,uint8)']('1', Rounding.Up)).to.be.bignumber.equal('0');
expect(await this.math.methods['$log2(uint256,uint8)']('2', Rounding.Up)).to.be.bignumber.equal('1');
expect(await this.math.methods['$log2(uint256,uint8)']('3', Rounding.Up)).to.be.bignumber.equal('2');
expect(await this.math.methods['$log2(uint256,uint8)']('4', Rounding.Up)).to.be.bignumber.equal('2');
expect(await this.math.methods['$log2(uint256,uint8)']('5', Rounding.Up)).to.be.bignumber.equal('3');
expect(await this.math.methods['$log2(uint256,uint8)']('6', Rounding.Up)).to.be.bignumber.equal('3');
expect(await this.math.methods['$log2(uint256,uint8)']('7', Rounding.Up)).to.be.bignumber.equal('3');
expect(await this.math.methods['$log2(uint256,uint8)']('8', Rounding.Up)).to.be.bignumber.equal('3');
expect(await this.math.methods['$log2(uint256,uint8)']('9', Rounding.Up)).to.be.bignumber.equal('4');
expect(await this.math.methods['$log2(uint256,uint8)'](MAX_UINT256, Rounding.Up)).to.be.bignumber.equal('256');
});
});
describe('log10', function () {
it('rounds down', async function () {
expect(await this.math.$log10('0', Rounding.Down)).to.be.bignumber.equal('0');
expect(await this.math.$log10('1', Rounding.Down)).to.be.bignumber.equal('0');
expect(await this.math.$log10('2', Rounding.Down)).to.be.bignumber.equal('0');
expect(await this.math.$log10('9', Rounding.Down)).to.be.bignumber.equal('0');
expect(await this.math.$log10('10', Rounding.Down)).to.be.bignumber.equal('1');
expect(await this.math.$log10('11', Rounding.Down)).to.be.bignumber.equal('1');
expect(await this.math.$log10('99', Rounding.Down)).to.be.bignumber.equal('1');
expect(await this.math.$log10('100', Rounding.Down)).to.be.bignumber.equal('2');
expect(await this.math.$log10('101', Rounding.Down)).to.be.bignumber.equal('2');
expect(await this.math.$log10('999', Rounding.Down)).to.be.bignumber.equal('2');
expect(await this.math.$log10('1000', Rounding.Down)).to.be.bignumber.equal('3');
expect(await this.math.$log10('1001', Rounding.Down)).to.be.bignumber.equal('3');
expect(await this.math.$log10(MAX_UINT256, Rounding.Down)).to.be.bignumber.equal('77');
});
it('rounds up', async function () {
expect(await this.math.$log10('0', Rounding.Up)).to.be.bignumber.equal('0');
expect(await this.math.$log10('1', Rounding.Up)).to.be.bignumber.equal('0');
expect(await this.math.$log10('2', Rounding.Up)).to.be.bignumber.equal('1');
expect(await this.math.$log10('9', Rounding.Up)).to.be.bignumber.equal('1');
expect(await this.math.$log10('10', Rounding.Up)).to.be.bignumber.equal('1');
expect(await this.math.$log10('11', Rounding.Up)).to.be.bignumber.equal('2');
expect(await this.math.$log10('99', Rounding.Up)).to.be.bignumber.equal('2');
expect(await this.math.$log10('100', Rounding.Up)).to.be.bignumber.equal('2');
expect(await this.math.$log10('101', Rounding.Up)).to.be.bignumber.equal('3');
expect(await this.math.$log10('999', Rounding.Up)).to.be.bignumber.equal('3');
expect(await this.math.$log10('1000', Rounding.Up)).to.be.bignumber.equal('3');
expect(await this.math.$log10('1001', Rounding.Up)).to.be.bignumber.equal('4');
expect(await this.math.$log10(MAX_UINT256, Rounding.Up)).to.be.bignumber.equal('78');
});
});
describe('log256', function () {
it('rounds down', async function () {
expect(await this.math.$log256('0', Rounding.Down)).to.be.bignumber.equal('0');
expect(await this.math.$log256('1', Rounding.Down)).to.be.bignumber.equal('0');
expect(await this.math.$log256('2', Rounding.Down)).to.be.bignumber.equal('0');
expect(await this.math.$log256('255', Rounding.Down)).to.be.bignumber.equal('0');
expect(await this.math.$log256('256', Rounding.Down)).to.be.bignumber.equal('1');
expect(await this.math.$log256('257', Rounding.Down)).to.be.bignumber.equal('1');
expect(await this.math.$log256('65535', Rounding.Down)).to.be.bignumber.equal('1');
expect(await this.math.$log256('65536', Rounding.Down)).to.be.bignumber.equal('2');
expect(await this.math.$log256('65537', Rounding.Down)).to.be.bignumber.equal('2');
expect(await this.math.$log256(MAX_UINT256, Rounding.Down)).to.be.bignumber.equal('31');
});
it('rounds up', async function () {
expect(await this.math.$log256('0', Rounding.Up)).to.be.bignumber.equal('0');
expect(await this.math.$log256('1', Rounding.Up)).to.be.bignumber.equal('0');
expect(await this.math.$log256('2', Rounding.Up)).to.be.bignumber.equal('1');
expect(await this.math.$log256('255', Rounding.Up)).to.be.bignumber.equal('1');
expect(await this.math.$log256('256', Rounding.Up)).to.be.bignumber.equal('1');
expect(await this.math.$log256('257', Rounding.Up)).to.be.bignumber.equal('2');
expect(await this.math.$log256('65535', Rounding.Up)).to.be.bignumber.equal('2');
expect(await this.math.$log256('65536', Rounding.Up)).to.be.bignumber.equal('2');
expect(await this.math.$log256('65537', Rounding.Up)).to.be.bignumber.equal('3');
expect(await this.math.$log256(MAX_UINT256, Rounding.Up)).to.be.bignumber.equal('32');
});
});
});
});
@@ -0,0 +1,152 @@
const { BN, expectRevert } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const { range } = require('../../../scripts/helpers');
const SafeCast = artifacts.require('$SafeCast');
contract('SafeCast', async function () {
beforeEach(async function () {
this.safeCast = await SafeCast.new();
});
function testToUint(bits) {
describe(`toUint${bits}`, () => {
const maxValue = new BN('2').pow(new BN(bits)).subn(1);
it('downcasts 0', async function () {
expect(await this.safeCast[`$toUint${bits}`](0)).to.be.bignumber.equal('0');
});
it('downcasts 1', async function () {
expect(await this.safeCast[`$toUint${bits}`](1)).to.be.bignumber.equal('1');
});
it(`downcasts 2^${bits} - 1 (${maxValue})`, async function () {
expect(await this.safeCast[`$toUint${bits}`](maxValue)).to.be.bignumber.equal(maxValue);
});
it(`reverts when downcasting 2^${bits} (${maxValue.addn(1)})`, async function () {
await expectRevert(
this.safeCast[`$toUint${bits}`](maxValue.addn(1)),
`SafeCast: value doesn't fit in ${bits} bits`,
);
});
it(`reverts when downcasting 2^${bits} + 1 (${maxValue.addn(2)})`, async function () {
await expectRevert(
this.safeCast[`$toUint${bits}`](maxValue.addn(2)),
`SafeCast: value doesn't fit in ${bits} bits`,
);
});
});
}
range(8, 256, 8).forEach(bits => testToUint(bits));
describe('toUint256', () => {
const maxInt256 = new BN('2').pow(new BN(255)).subn(1);
const minInt256 = new BN('2').pow(new BN(255)).neg();
it('casts 0', async function () {
expect(await this.safeCast.$toUint256(0)).to.be.bignumber.equal('0');
});
it('casts 1', async function () {
expect(await this.safeCast.$toUint256(1)).to.be.bignumber.equal('1');
});
it(`casts INT256_MAX (${maxInt256})`, async function () {
expect(await this.safeCast.$toUint256(maxInt256)).to.be.bignumber.equal(maxInt256);
});
it('reverts when casting -1', async function () {
await expectRevert(this.safeCast.$toUint256(-1), 'SafeCast: value must be positive');
});
it(`reverts when casting INT256_MIN (${minInt256})`, async function () {
await expectRevert(this.safeCast.$toUint256(minInt256), 'SafeCast: value must be positive');
});
});
function testToInt(bits) {
describe(`toInt${bits}`, () => {
const minValue = new BN('-2').pow(new BN(bits - 1));
const maxValue = new BN('2').pow(new BN(bits - 1)).subn(1);
it('downcasts 0', async function () {
expect(await this.safeCast[`$toInt${bits}`](0)).to.be.bignumber.equal('0');
});
it('downcasts 1', async function () {
expect(await this.safeCast[`$toInt${bits}`](1)).to.be.bignumber.equal('1');
});
it('downcasts -1', async function () {
expect(await this.safeCast[`$toInt${bits}`](-1)).to.be.bignumber.equal('-1');
});
it(`downcasts -2^${bits - 1} (${minValue})`, async function () {
expect(await this.safeCast[`$toInt${bits}`](minValue)).to.be.bignumber.equal(minValue);
});
it(`downcasts 2^${bits - 1} - 1 (${maxValue})`, async function () {
expect(await this.safeCast[`$toInt${bits}`](maxValue)).to.be.bignumber.equal(maxValue);
});
it(`reverts when downcasting -2^${bits - 1} - 1 (${minValue.subn(1)})`, async function () {
await expectRevert(
this.safeCast[`$toInt${bits}`](minValue.subn(1)),
`SafeCast: value doesn't fit in ${bits} bits`,
);
});
it(`reverts when downcasting -2^${bits - 1} - 2 (${minValue.subn(2)})`, async function () {
await expectRevert(
this.safeCast[`$toInt${bits}`](minValue.subn(2)),
`SafeCast: value doesn't fit in ${bits} bits`,
);
});
it(`reverts when downcasting 2^${bits - 1} (${maxValue.addn(1)})`, async function () {
await expectRevert(
this.safeCast[`$toInt${bits}`](maxValue.addn(1)),
`SafeCast: value doesn't fit in ${bits} bits`,
);
});
it(`reverts when downcasting 2^${bits - 1} + 1 (${maxValue.addn(2)})`, async function () {
await expectRevert(
this.safeCast[`$toInt${bits}`](maxValue.addn(2)),
`SafeCast: value doesn't fit in ${bits} bits`,
);
});
});
}
range(8, 256, 8).forEach(bits => testToInt(bits));
describe('toInt256', () => {
const maxUint256 = new BN('2').pow(new BN(256)).subn(1);
const maxInt256 = new BN('2').pow(new BN(255)).subn(1);
it('casts 0', async function () {
expect(await this.safeCast.$toInt256(0)).to.be.bignumber.equal('0');
});
it('casts 1', async function () {
expect(await this.safeCast.$toInt256(1)).to.be.bignumber.equal('1');
});
it(`casts INT256_MAX (${maxInt256})`, async function () {
expect(await this.safeCast.$toInt256(maxInt256)).to.be.bignumber.equal(maxInt256);
});
it(`reverts when casting INT256_MAX + 1 (${maxInt256.addn(1)})`, async function () {
await expectRevert(this.safeCast.$toInt256(maxInt256.addn(1)), "SafeCast: value doesn't fit in an int256");
});
it(`reverts when casting UINT256_MAX (${maxUint256})`, async function () {
await expectRevert(this.safeCast.$toInt256(maxUint256), "SafeCast: value doesn't fit in an int256");
});
});
});
@@ -0,0 +1,433 @@
const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers');
const { MAX_UINT256 } = constants;
const { expect } = require('chai');
const SafeMath = artifacts.require('$SafeMath');
const SafeMathMemoryCheck = artifacts.require('$SafeMathMemoryCheck');
function expectStruct(value, expected) {
for (const key in expected) {
if (BN.isBN(value[key])) {
expect(value[key]).to.be.bignumber.equal(expected[key]);
} else {
expect(value[key]).to.be.equal(expected[key]);
}
}
}
contract('SafeMath', function () {
beforeEach(async function () {
this.safeMath = await SafeMath.new();
});
async function testCommutative(fn, lhs, rhs, expected, ...extra) {
expect(await fn(lhs, rhs, ...extra)).to.be.bignumber.equal(expected);
expect(await fn(rhs, lhs, ...extra)).to.be.bignumber.equal(expected);
}
async function testFailsCommutative(fn, lhs, rhs, reason, ...extra) {
if (reason === undefined) {
await expectRevert.unspecified(fn(lhs, rhs, ...extra));
await expectRevert.unspecified(fn(rhs, lhs, ...extra));
} else {
await expectRevert(fn(lhs, rhs, ...extra), reason);
await expectRevert(fn(rhs, lhs, ...extra), reason);
}
}
async function testCommutativeIterable(fn, lhs, rhs, expected, ...extra) {
expectStruct(await fn(lhs, rhs, ...extra), expected);
expectStruct(await fn(rhs, lhs, ...extra), expected);
}
describe('with flag', function () {
describe('add', function () {
it('adds correctly', async function () {
const a = new BN('5678');
const b = new BN('1234');
testCommutativeIterable(this.safeMath.$tryAdd, a, b, [true, a.add(b)]);
});
it('reverts on addition overflow', async function () {
const a = MAX_UINT256;
const b = new BN('1');
testCommutativeIterable(this.safeMath.$tryAdd, a, b, [false, '0']);
});
});
describe('sub', function () {
it('subtracts correctly', async function () {
const a = new BN('5678');
const b = new BN('1234');
expectStruct(await this.safeMath.$trySub(a, b), [true, a.sub(b)]);
});
it('reverts if subtraction result would be negative', async function () {
const a = new BN('1234');
const b = new BN('5678');
expectStruct(await this.safeMath.$trySub(a, b), [false, '0']);
});
});
describe('mul', function () {
it('multiplies correctly', async function () {
const a = new BN('1234');
const b = new BN('5678');
testCommutativeIterable(this.safeMath.$tryMul, a, b, [true, a.mul(b)]);
});
it('multiplies by zero correctly', async function () {
const a = new BN('0');
const b = new BN('5678');
testCommutativeIterable(this.safeMath.$tryMul, a, b, [true, a.mul(b)]);
});
it('reverts on multiplication overflow', async function () {
const a = MAX_UINT256;
const b = new BN('2');
testCommutativeIterable(this.safeMath.$tryMul, a, b, [false, '0']);
});
});
describe('div', function () {
it('divides correctly', async function () {
const a = new BN('5678');
const b = new BN('5678');
expectStruct(await this.safeMath.$tryDiv(a, b), [true, a.div(b)]);
});
it('divides zero correctly', async function () {
const a = new BN('0');
const b = new BN('5678');
expectStruct(await this.safeMath.$tryDiv(a, b), [true, a.div(b)]);
});
it('returns complete number result on non-even division', async function () {
const a = new BN('7000');
const b = new BN('5678');
expectStruct(await this.safeMath.$tryDiv(a, b), [true, a.div(b)]);
});
it('reverts on division by zero', async function () {
const a = new BN('5678');
const b = new BN('0');
expectStruct(await this.safeMath.$tryDiv(a, b), [false, '0']);
});
});
describe('mod', function () {
describe('modulos correctly', async function () {
it('when the dividend is smaller than the divisor', async function () {
const a = new BN('284');
const b = new BN('5678');
expectStruct(await this.safeMath.$tryMod(a, b), [true, a.mod(b)]);
});
it('when the dividend is equal to the divisor', async function () {
const a = new BN('5678');
const b = new BN('5678');
expectStruct(await this.safeMath.$tryMod(a, b), [true, a.mod(b)]);
});
it('when the dividend is larger than the divisor', async function () {
const a = new BN('7000');
const b = new BN('5678');
expectStruct(await this.safeMath.$tryMod(a, b), [true, a.mod(b)]);
});
it('when the dividend is a multiple of the divisor', async function () {
const a = new BN('17034'); // 17034 == 5678 * 3
const b = new BN('5678');
expectStruct(await this.safeMath.$tryMod(a, b), [true, a.mod(b)]);
});
});
it('reverts with a 0 divisor', async function () {
const a = new BN('5678');
const b = new BN('0');
expectStruct(await this.safeMath.$tryMod(a, b), [false, '0']);
});
});
});
describe('with default revert message', function () {
describe('add', function () {
it('adds correctly', async function () {
const a = new BN('5678');
const b = new BN('1234');
await testCommutative(this.safeMath.$add, a, b, a.add(b));
});
it('reverts on addition overflow', async function () {
const a = MAX_UINT256;
const b = new BN('1');
await testFailsCommutative(this.safeMath.$add, a, b, undefined);
});
});
describe('sub', function () {
it('subtracts correctly', async function () {
const a = new BN('5678');
const b = new BN('1234');
expect(await this.safeMath.$sub(a, b)).to.be.bignumber.equal(a.sub(b));
});
it('reverts if subtraction result would be negative', async function () {
const a = new BN('1234');
const b = new BN('5678');
await expectRevert.unspecified(this.safeMath.$sub(a, b));
});
});
describe('mul', function () {
it('multiplies correctly', async function () {
const a = new BN('1234');
const b = new BN('5678');
await testCommutative(this.safeMath.$mul, a, b, a.mul(b));
});
it('multiplies by zero correctly', async function () {
const a = new BN('0');
const b = new BN('5678');
await testCommutative(this.safeMath.$mul, a, b, '0');
});
it('reverts on multiplication overflow', async function () {
const a = MAX_UINT256;
const b = new BN('2');
await testFailsCommutative(this.safeMath.$mul, a, b, undefined);
});
});
describe('div', function () {
it('divides correctly', async function () {
const a = new BN('5678');
const b = new BN('5678');
expect(await this.safeMath.$div(a, b)).to.be.bignumber.equal(a.div(b));
});
it('divides zero correctly', async function () {
const a = new BN('0');
const b = new BN('5678');
expect(await this.safeMath.$div(a, b)).to.be.bignumber.equal('0');
});
it('returns complete number result on non-even division', async function () {
const a = new BN('7000');
const b = new BN('5678');
expect(await this.safeMath.$div(a, b)).to.be.bignumber.equal('1');
});
it('reverts on division by zero', async function () {
const a = new BN('5678');
const b = new BN('0');
await expectRevert.unspecified(this.safeMath.$div(a, b));
});
});
describe('mod', function () {
describe('modulos correctly', async function () {
it('when the dividend is smaller than the divisor', async function () {
const a = new BN('284');
const b = new BN('5678');
expect(await this.safeMath.$mod(a, b)).to.be.bignumber.equal(a.mod(b));
});
it('when the dividend is equal to the divisor', async function () {
const a = new BN('5678');
const b = new BN('5678');
expect(await this.safeMath.$mod(a, b)).to.be.bignumber.equal(a.mod(b));
});
it('when the dividend is larger than the divisor', async function () {
const a = new BN('7000');
const b = new BN('5678');
expect(await this.safeMath.$mod(a, b)).to.be.bignumber.equal(a.mod(b));
});
it('when the dividend is a multiple of the divisor', async function () {
const a = new BN('17034'); // 17034 == 5678 * 3
const b = new BN('5678');
expect(await this.safeMath.$mod(a, b)).to.be.bignumber.equal(a.mod(b));
});
});
it('reverts with a 0 divisor', async function () {
const a = new BN('5678');
const b = new BN('0');
await expectRevert.unspecified(this.safeMath.$mod(a, b));
});
});
});
describe('with custom revert message', function () {
describe('sub', function () {
it('subtracts correctly', async function () {
const a = new BN('5678');
const b = new BN('1234');
expect(
await this.safeMath.methods['$sub(uint256,uint256,string)'](a, b, 'MyErrorMessage'),
).to.be.bignumber.equal(a.sub(b));
});
it('reverts if subtraction result would be negative', async function () {
const a = new BN('1234');
const b = new BN('5678');
await expectRevert(
this.safeMath.methods['$sub(uint256,uint256,string)'](a, b, 'MyErrorMessage'),
'MyErrorMessage',
);
});
});
describe('div', function () {
it('divides correctly', async function () {
const a = new BN('5678');
const b = new BN('5678');
expect(
await this.safeMath.methods['$div(uint256,uint256,string)'](a, b, 'MyErrorMessage'),
).to.be.bignumber.equal(a.div(b));
});
it('divides zero correctly', async function () {
const a = new BN('0');
const b = new BN('5678');
expect(
await this.safeMath.methods['$div(uint256,uint256,string)'](a, b, 'MyErrorMessage'),
).to.be.bignumber.equal('0');
});
it('returns complete number result on non-even division', async function () {
const a = new BN('7000');
const b = new BN('5678');
expect(
await this.safeMath.methods['$div(uint256,uint256,string)'](a, b, 'MyErrorMessage'),
).to.be.bignumber.equal('1');
});
it('reverts on division by zero', async function () {
const a = new BN('5678');
const b = new BN('0');
await expectRevert(
this.safeMath.methods['$div(uint256,uint256,string)'](a, b, 'MyErrorMessage'),
'MyErrorMessage',
);
});
});
describe('mod', function () {
describe('modulos correctly', async function () {
it('when the dividend is smaller than the divisor', async function () {
const a = new BN('284');
const b = new BN('5678');
expect(
await this.safeMath.methods['$mod(uint256,uint256,string)'](a, b, 'MyErrorMessage'),
).to.be.bignumber.equal(a.mod(b));
});
it('when the dividend is equal to the divisor', async function () {
const a = new BN('5678');
const b = new BN('5678');
expect(
await this.safeMath.methods['$mod(uint256,uint256,string)'](a, b, 'MyErrorMessage'),
).to.be.bignumber.equal(a.mod(b));
});
it('when the dividend is larger than the divisor', async function () {
const a = new BN('7000');
const b = new BN('5678');
expect(
await this.safeMath.methods['$mod(uint256,uint256,string)'](a, b, 'MyErrorMessage'),
).to.be.bignumber.equal(a.mod(b));
});
it('when the dividend is a multiple of the divisor', async function () {
const a = new BN('17034'); // 17034 == 5678 * 3
const b = new BN('5678');
expect(
await this.safeMath.methods['$mod(uint256,uint256,string)'](a, b, 'MyErrorMessage'),
).to.be.bignumber.equal(a.mod(b));
});
});
it('reverts with a 0 divisor', async function () {
const a = new BN('5678');
const b = new BN('0');
await expectRevert(
this.safeMath.methods['$mod(uint256,uint256,string)'](a, b, 'MyErrorMessage'),
'MyErrorMessage',
);
});
});
});
describe('memory leakage', function () {
beforeEach(async function () {
this.safeMathMemoryCheck = await SafeMathMemoryCheck.new();
});
it('add', async function () {
expect(await this.safeMathMemoryCheck.$addMemoryCheck()).to.be.bignumber.equal('0');
});
it('sub', async function () {
expect(await this.safeMathMemoryCheck.$subMemoryCheck()).to.be.bignumber.equal('0');
});
it('mul', async function () {
expect(await this.safeMathMemoryCheck.$mulMemoryCheck()).to.be.bignumber.equal('0');
});
it('div', async function () {
expect(await this.safeMathMemoryCheck.$divMemoryCheck()).to.be.bignumber.equal('0');
});
it('mod', async function () {
expect(await this.safeMathMemoryCheck.$modMemoryCheck()).to.be.bignumber.equal('0');
});
});
});
@@ -0,0 +1,95 @@
const { BN, constants } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const { MIN_INT256, MAX_INT256 } = constants;
const SignedMath = artifacts.require('$SignedMath');
contract('SignedMath', function () {
const min = new BN('-1234');
const max = new BN('5678');
beforeEach(async function () {
this.math = await SignedMath.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})`,
);
}
}
});
});
describe('abs', function () {
for (const n of [
MIN_INT256,
MIN_INT256.addn(1),
new BN('-1'),
new BN('0'),
new BN('1'),
MAX_INT256.subn(1),
MAX_INT256,
]) {
it(`correctly computes the absolute value of ${n}`, async function () {
expect(await this.math.$abs(n)).to.be.bignumber.equal(n.abs());
});
}
});
});
@@ -0,0 +1,152 @@
const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers');
const { MAX_INT256, MIN_INT256 } = constants;
const { expect } = require('chai');
const SignedSafeMath = artifacts.require('$SignedSafeMath');
contract('SignedSafeMath', function () {
beforeEach(async function () {
this.safeMath = await SignedSafeMath.new();
});
async function testCommutative(fn, lhs, rhs, expected) {
expect(await fn(lhs, rhs)).to.be.bignumber.equal(expected);
expect(await fn(rhs, lhs)).to.be.bignumber.equal(expected);
}
async function testFailsCommutative(fn, lhs, rhs) {
await expectRevert.unspecified(fn(lhs, rhs));
await expectRevert.unspecified(fn(rhs, lhs));
}
describe('add', function () {
it('adds correctly if it does not overflow and the result is positive', async function () {
const a = new BN('1234');
const b = new BN('5678');
await testCommutative(this.safeMath.$add, a, b, a.add(b));
});
it('adds correctly if it does not overflow and the result is negative', async function () {
const a = MAX_INT256;
const b = MIN_INT256;
await testCommutative(this.safeMath.$add, a, b, a.add(b));
});
it('reverts on positive addition overflow', async function () {
const a = MAX_INT256;
const b = new BN('1');
await testFailsCommutative(this.safeMath.$add, a, b);
});
it('reverts on negative addition overflow', async function () {
const a = MIN_INT256;
const b = new BN('-1');
await testFailsCommutative(this.safeMath.$add, a, b);
});
});
describe('sub', function () {
it('subtracts correctly if it does not overflow and the result is positive', async function () {
const a = new BN('5678');
const b = new BN('1234');
const result = await this.safeMath.$sub(a, b);
expect(result).to.be.bignumber.equal(a.sub(b));
});
it('subtracts correctly if it does not overflow and the result is negative', async function () {
const a = new BN('1234');
const b = new BN('5678');
const result = await this.safeMath.$sub(a, b);
expect(result).to.be.bignumber.equal(a.sub(b));
});
it('reverts on positive subtraction overflow', async function () {
const a = MAX_INT256;
const b = new BN('-1');
await expectRevert.unspecified(this.safeMath.$sub(a, b));
});
it('reverts on negative subtraction overflow', async function () {
const a = MIN_INT256;
const b = new BN('1');
await expectRevert.unspecified(this.safeMath.$sub(a, b));
});
});
describe('mul', function () {
it('multiplies correctly', async function () {
const a = new BN('5678');
const b = new BN('-1234');
await testCommutative(this.safeMath.$mul, a, b, a.mul(b));
});
it('multiplies by zero correctly', async function () {
const a = new BN('0');
const b = new BN('5678');
await testCommutative(this.safeMath.$mul, a, b, '0');
});
it('reverts on multiplication overflow, positive operands', async function () {
const a = MAX_INT256;
const b = new BN('2');
await testFailsCommutative(this.safeMath.$mul, a, b);
});
it('reverts when minimum integer is multiplied by -1', async function () {
const a = MIN_INT256;
const b = new BN('-1');
await testFailsCommutative(this.safeMath.$mul, a, b);
});
});
describe('div', function () {
it('divides correctly', async function () {
const a = new BN('-5678');
const b = new BN('5678');
const result = await this.safeMath.$div(a, b);
expect(result).to.be.bignumber.equal(a.div(b));
});
it('divides zero correctly', async function () {
const a = new BN('0');
const b = new BN('5678');
expect(await this.safeMath.$div(a, b)).to.be.bignumber.equal('0');
});
it('returns complete number result on non-even division', async function () {
const a = new BN('7000');
const b = new BN('5678');
expect(await this.safeMath.$div(a, b)).to.be.bignumber.equal('1');
});
it('reverts on division by zero', async function () {
const a = new BN('-5678');
const b = new BN('0');
await expectRevert.unspecified(this.safeMath.$div(a, b));
});
it('reverts on overflow, negative second', async function () {
const a = new BN(MIN_INT256);
const b = new BN('-1');
await expectRevert.unspecified(this.safeMath.$div(a, b));
});
});
});
@@ -0,0 +1,145 @@
const { BN } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const BitMap = artifacts.require('$BitMaps');
contract('BitMap', function () {
const keyA = new BN('7891');
const keyB = new BN('451');
const keyC = new BN('9592328');
beforeEach(async function () {
this.bitmap = await BitMap.new();
});
it('starts empty', async function () {
expect(await this.bitmap.$get(0, keyA)).to.equal(false);
expect(await this.bitmap.$get(0, keyB)).to.equal(false);
expect(await this.bitmap.$get(0, keyC)).to.equal(false);
});
describe('setTo', function () {
it('set a key to true', async function () {
await this.bitmap.$setTo(0, keyA, true);
expect(await this.bitmap.$get(0, keyA)).to.equal(true);
expect(await this.bitmap.$get(0, keyB)).to.equal(false);
expect(await this.bitmap.$get(0, keyC)).to.equal(false);
});
it('set a key to false', async function () {
await this.bitmap.$setTo(0, keyA, true);
await this.bitmap.$setTo(0, keyA, false);
expect(await this.bitmap.$get(0, keyA)).to.equal(false);
expect(await this.bitmap.$get(0, keyB)).to.equal(false);
expect(await this.bitmap.$get(0, keyC)).to.equal(false);
});
it('set several consecutive keys', async function () {
await this.bitmap.$setTo(0, keyA.addn(0), true);
await this.bitmap.$setTo(0, keyA.addn(1), true);
await this.bitmap.$setTo(0, keyA.addn(2), true);
await this.bitmap.$setTo(0, keyA.addn(3), true);
await this.bitmap.$setTo(0, keyA.addn(4), true);
await this.bitmap.$setTo(0, keyA.addn(2), false);
await this.bitmap.$setTo(0, keyA.addn(4), false);
expect(await this.bitmap.$get(0, keyA.addn(0))).to.equal(true);
expect(await this.bitmap.$get(0, keyA.addn(1))).to.equal(true);
expect(await this.bitmap.$get(0, keyA.addn(2))).to.equal(false);
expect(await this.bitmap.$get(0, keyA.addn(3))).to.equal(true);
expect(await this.bitmap.$get(0, keyA.addn(4))).to.equal(false);
});
});
describe('set', function () {
it('adds a key', async function () {
await this.bitmap.$set(0, keyA);
expect(await this.bitmap.$get(0, keyA)).to.equal(true);
expect(await this.bitmap.$get(0, keyB)).to.equal(false);
expect(await this.bitmap.$get(0, keyC)).to.equal(false);
});
it('adds several keys', async function () {
await this.bitmap.$set(0, keyA);
await this.bitmap.$set(0, keyB);
expect(await this.bitmap.$get(0, keyA)).to.equal(true);
expect(await this.bitmap.$get(0, keyB)).to.equal(true);
expect(await this.bitmap.$get(0, keyC)).to.equal(false);
});
it('adds several consecutive keys', async function () {
await this.bitmap.$set(0, keyA.addn(0));
await this.bitmap.$set(0, keyA.addn(1));
await this.bitmap.$set(0, keyA.addn(3));
expect(await this.bitmap.$get(0, keyA.addn(0))).to.equal(true);
expect(await this.bitmap.$get(0, keyA.addn(1))).to.equal(true);
expect(await this.bitmap.$get(0, keyA.addn(2))).to.equal(false);
expect(await this.bitmap.$get(0, keyA.addn(3))).to.equal(true);
expect(await this.bitmap.$get(0, keyA.addn(4))).to.equal(false);
});
});
describe('unset', function () {
it('removes added keys', async function () {
await this.bitmap.$set(0, keyA);
await this.bitmap.$set(0, keyB);
await this.bitmap.$unset(0, keyA);
expect(await this.bitmap.$get(0, keyA)).to.equal(false);
expect(await this.bitmap.$get(0, keyB)).to.equal(true);
expect(await this.bitmap.$get(0, keyC)).to.equal(false);
});
it('removes consecutive added keys', async function () {
await this.bitmap.$set(0, keyA.addn(0));
await this.bitmap.$set(0, keyA.addn(1));
await this.bitmap.$set(0, keyA.addn(3));
await this.bitmap.$unset(0, keyA.addn(1));
expect(await this.bitmap.$get(0, keyA.addn(0))).to.equal(true);
expect(await this.bitmap.$get(0, keyA.addn(1))).to.equal(false);
expect(await this.bitmap.$get(0, keyA.addn(2))).to.equal(false);
expect(await this.bitmap.$get(0, keyA.addn(3))).to.equal(true);
expect(await this.bitmap.$get(0, keyA.addn(4))).to.equal(false);
});
it('adds and removes multiple keys', async function () {
// []
await this.bitmap.$set(0, keyA);
await this.bitmap.$set(0, keyC);
// [A, C]
await this.bitmap.$unset(0, keyA);
await this.bitmap.$unset(0, keyB);
// [C]
await this.bitmap.$set(0, keyB);
// [C, B]
await this.bitmap.$set(0, keyA);
await this.bitmap.$unset(0, keyC);
// [A, B]
await this.bitmap.$set(0, keyA);
await this.bitmap.$set(0, keyB);
// [A, B]
await this.bitmap.$set(0, keyC);
await this.bitmap.$unset(0, keyA);
// [B, C]
await this.bitmap.$set(0, keyA);
await this.bitmap.$unset(0, keyB);
// [A, C]
expect(await this.bitmap.$get(0, keyA)).to.equal(true);
expect(await this.bitmap.$get(0, keyB)).to.equal(false);
expect(await this.bitmap.$get(0, keyC)).to.equal(true);
});
});
});
@@ -0,0 +1,99 @@
const { expectEvent } = require('@openzeppelin/test-helpers');
const { expectRevertCustomError } = require('../../helpers/customError');
const DoubleEndedQueue = artifacts.require('$DoubleEndedQueue');
/** Rebuild the content of the deque as a JS array. */
const getContent = deque =>
deque.$length(0).then(bn =>
Promise.all(
Array(bn.toNumber())
.fill()
.map((_, i) => deque.$at(0, i)),
),
);
contract('DoubleEndedQueue', function () {
const bytesA = '0xdeadbeef'.padEnd(66, '0');
const bytesB = '0x0123456789'.padEnd(66, '0');
const bytesC = '0x42424242'.padEnd(66, '0');
const bytesD = '0x171717'.padEnd(66, '0');
beforeEach(async function () {
this.deque = await DoubleEndedQueue.new();
});
describe('when empty', function () {
it('getters', async function () {
expect(await this.deque.$empty(0)).to.be.equal(true);
expect(await getContent(this.deque)).to.have.ordered.members([]);
});
it('reverts on accesses', async function () {
await expectRevertCustomError(this.deque.$popBack(0), 'Empty()');
await expectRevertCustomError(this.deque.$popFront(0), 'Empty()');
await expectRevertCustomError(this.deque.$back(0), 'Empty()');
await expectRevertCustomError(this.deque.$front(0), 'Empty()');
});
});
describe('when not empty', function () {
beforeEach(async function () {
await this.deque.$pushBack(0, bytesB);
await this.deque.$pushFront(0, bytesA);
await this.deque.$pushBack(0, bytesC);
this.content = [bytesA, bytesB, bytesC];
});
it('getters', async function () {
expect(await this.deque.$empty(0)).to.be.equal(false);
expect(await this.deque.$length(0)).to.be.bignumber.equal(this.content.length.toString());
expect(await this.deque.$front(0)).to.be.equal(this.content[0]);
expect(await this.deque.$back(0)).to.be.equal(this.content[this.content.length - 1]);
expect(await getContent(this.deque)).to.have.ordered.members(this.content);
});
it('out of bounds access', async function () {
await expectRevertCustomError(this.deque.$at(0, this.content.length), 'OutOfBounds()');
});
describe('push', function () {
it('front', async function () {
await this.deque.$pushFront(0, bytesD);
this.content.unshift(bytesD); // add element at the beginning
expect(await getContent(this.deque)).to.have.ordered.members(this.content);
});
it('back', async function () {
await this.deque.$pushBack(0, bytesD);
this.content.push(bytesD); // add element at the end
expect(await getContent(this.deque)).to.have.ordered.members(this.content);
});
});
describe('pop', function () {
it('front', async function () {
const value = this.content.shift(); // remove first element
expectEvent(await this.deque.$popFront(0), 'return$popFront', { value });
expect(await getContent(this.deque)).to.have.ordered.members(this.content);
});
it('back', async function () {
const value = this.content.pop(); // remove last element
expectEvent(await this.deque.$popBack(0), 'return$popBack', { value });
expect(await getContent(this.deque)).to.have.ordered.members(this.content);
});
});
it('clear', async function () {
await this.deque.$clear(0);
expect(await this.deque.$empty(0)).to.be.equal(true);
expect(await getContent(this.deque)).to.have.ordered.members([]);
});
});
});
@@ -0,0 +1,185 @@
const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const zip = require('lodash.zip');
function shouldBehaveLikeMap(keys, values, zeroValue, methods, events) {
const [keyA, keyB, keyC] = keys;
const [valueA, valueB, valueC] = values;
async function expectMembersMatch(map, keys, values) {
expect(keys.length).to.equal(values.length);
await Promise.all(keys.map(async key => expect(await methods.contains(map, key)).to.equal(true)));
expect(await methods.length(map)).to.bignumber.equal(keys.length.toString());
expect((await Promise.all(keys.map(key => methods.get(map, key)))).map(k => k.toString())).to.have.same.members(
values.map(value => value.toString()),
);
// To compare key-value pairs, we zip keys and values, and convert BNs to
// strings to workaround Chai limitations when dealing with nested arrays
expect(
await Promise.all(
[...Array(keys.length).keys()].map(async index => {
const entry = await methods.at(map, index);
return [entry[0].toString(), entry[1].toString()];
}),
),
).to.have.same.deep.members(
zip(
keys.map(k => k.toString()),
values.map(v => v.toString()),
),
);
// This also checks that both arrays have the same length
expect((await methods.keys(map)).map(k => k.toString())).to.have.same.members(keys.map(key => key.toString()));
}
it('starts empty', async function () {
expect(await methods.contains(this.map, keyA)).to.equal(false);
await expectMembersMatch(this.map, [], []);
});
describe('set', function () {
it('adds a key', async function () {
const receipt = await methods.set(this.map, keyA, valueA);
expectEvent(receipt, events.setReturn, { ret0: true });
await expectMembersMatch(this.map, [keyA], [valueA]);
});
it('adds several keys', async function () {
await methods.set(this.map, keyA, valueA);
await methods.set(this.map, keyB, valueB);
await expectMembersMatch(this.map, [keyA, keyB], [valueA, valueB]);
expect(await methods.contains(this.map, keyC)).to.equal(false);
});
it('returns false when adding keys already in the set', async function () {
await methods.set(this.map, keyA, valueA);
const receipt = await methods.set(this.map, keyA, valueA);
expectEvent(receipt, events.setReturn, { ret0: false });
await expectMembersMatch(this.map, [keyA], [valueA]);
});
it('updates values for keys already in the set', async function () {
await methods.set(this.map, keyA, valueA);
await methods.set(this.map, keyA, valueB);
await expectMembersMatch(this.map, [keyA], [valueB]);
});
});
describe('remove', function () {
it('removes added keys', async function () {
await methods.set(this.map, keyA, valueA);
const receipt = await methods.remove(this.map, keyA);
expectEvent(receipt, events.removeReturn, { ret0: true });
expect(await methods.contains(this.map, keyA)).to.equal(false);
await expectMembersMatch(this.map, [], []);
});
it('returns false when removing keys not in the set', async function () {
const receipt = await methods.remove(this.map, keyA);
expectEvent(receipt, events.removeReturn, { ret0: false });
expect(await methods.contains(this.map, keyA)).to.equal(false);
});
it('adds and removes multiple keys', async function () {
// []
await methods.set(this.map, keyA, valueA);
await methods.set(this.map, keyC, valueC);
// [A, C]
await methods.remove(this.map, keyA);
await methods.remove(this.map, keyB);
// [C]
await methods.set(this.map, keyB, valueB);
// [C, B]
await methods.set(this.map, keyA, valueA);
await methods.remove(this.map, keyC);
// [A, B]
await methods.set(this.map, keyA, valueA);
await methods.set(this.map, keyB, valueB);
// [A, B]
await methods.set(this.map, keyC, valueC);
await methods.remove(this.map, keyA);
// [B, C]
await methods.set(this.map, keyA, valueA);
await methods.remove(this.map, keyB);
// [A, C]
await expectMembersMatch(this.map, [keyA, keyC], [valueA, valueC]);
expect(await methods.contains(this.map, keyA)).to.equal(true);
expect(await methods.contains(this.map, keyB)).to.equal(false);
expect(await methods.contains(this.map, keyC)).to.equal(true);
});
});
describe('read', function () {
beforeEach(async function () {
await methods.set(this.map, keyA, valueA);
});
describe('get', function () {
it('existing value', async function () {
expect(await methods.get(this.map, keyA).then(r => r.toString())).to.be.equal(valueA.toString());
});
it('missing value', async function () {
await expectRevert(methods.get(this.map, keyB), 'EnumerableMap: nonexistent key');
});
});
describe('get with message', function () {
it('existing value', async function () {
expect(await methods.getWithMessage(this.map, keyA, 'custom error string').then(r => r.toString())).to.be.equal(
valueA.toString(),
);
});
it('missing value', async function () {
await expectRevert(methods.getWithMessage(this.map, keyB, 'custom error string'), 'custom error string');
});
});
describe('tryGet', function () {
it('existing value', async function () {
const result = await methods.tryGet(this.map, keyA);
expect(result['0']).to.be.equal(true);
expect(result['1'].toString()).to.be.equal(valueA.toString());
});
it('missing value', async function () {
const result = await methods.tryGet(this.map, keyB);
expect(result['0']).to.be.equal(false);
expect(result['1'].toString()).to.be.equal(zeroValue.toString());
});
});
});
}
module.exports = {
shouldBehaveLikeMap,
};
@@ -0,0 +1,154 @@
const { BN, constants } = require('@openzeppelin/test-helpers');
const { mapValues } = require('../../helpers/map-values');
const EnumerableMap = artifacts.require('$EnumerableMap');
const { shouldBehaveLikeMap } = require('./EnumerableMap.behavior');
const getMethods = ms => {
return mapValues(
ms,
m =>
(self, ...args) =>
self.methods[m](0, ...args),
);
};
// Get the name of the library. In the transpiled code it will be EnumerableMapUpgradeable.
const library = EnumerableMap._json.contractName.replace(/^\$/, '');
contract('EnumerableMap', function (accounts) {
const [accountA, accountB, accountC] = accounts;
const keyA = new BN('7891');
const keyB = new BN('451');
const keyC = new BN('9592328');
const bytesA = '0xdeadbeef'.padEnd(66, '0');
const bytesB = '0x0123456789'.padEnd(66, '0');
const bytesC = '0x42424242'.padEnd(66, '0');
beforeEach(async function () {
this.map = await EnumerableMap.new();
});
// AddressToUintMap
describe('AddressToUintMap', function () {
shouldBehaveLikeMap(
[accountA, accountB, accountC],
[keyA, keyB, keyC],
new BN('0'),
getMethods({
set: '$set(uint256,address,uint256)',
get: '$get(uint256,address)',
getWithMessage: '$get(uint256,address,string)',
tryGet: '$tryGet(uint256,address)',
remove: '$remove(uint256,address)',
length: `$length_${library}_AddressToUintMap(uint256)`,
at: `$at_${library}_AddressToUintMap(uint256,uint256)`,
contains: '$contains(uint256,address)',
keys: `$keys_${library}_AddressToUintMap(uint256)`,
}),
{
setReturn: `return$set_${library}_AddressToUintMap_address_uint256`,
removeReturn: `return$remove_${library}_AddressToUintMap_address`,
},
);
});
// UintToAddressMap
describe('UintToAddressMap', function () {
shouldBehaveLikeMap(
[keyA, keyB, keyC],
[accountA, accountB, accountC],
constants.ZERO_ADDRESS,
getMethods({
set: '$set(uint256,uint256,address)',
get: `$get_${library}_UintToAddressMap(uint256,uint256)`,
getWithMessage: `$get_${library}_UintToAddressMap(uint256,uint256,string)`,
tryGet: `$tryGet_${library}_UintToAddressMap(uint256,uint256)`,
remove: `$remove_${library}_UintToAddressMap(uint256,uint256)`,
length: `$length_${library}_UintToAddressMap(uint256)`,
at: `$at_${library}_UintToAddressMap(uint256,uint256)`,
contains: `$contains_${library}_UintToAddressMap(uint256,uint256)`,
keys: `$keys_${library}_UintToAddressMap(uint256)`,
}),
{
setReturn: `return$set_${library}_UintToAddressMap_uint256_address`,
removeReturn: `return$remove_${library}_UintToAddressMap_uint256`,
},
);
});
// Bytes32ToBytes32Map
describe('Bytes32ToBytes32Map', function () {
shouldBehaveLikeMap(
[keyA, keyB, keyC].map(k => '0x' + k.toString(16).padEnd(64, '0')),
[bytesA, bytesB, bytesC],
constants.ZERO_BYTES32,
getMethods({
set: '$set(uint256,bytes32,bytes32)',
get: `$get_${library}_Bytes32ToBytes32Map(uint256,bytes32)`,
getWithMessage: `$get_${library}_Bytes32ToBytes32Map(uint256,bytes32,string)`,
tryGet: `$tryGet_${library}_Bytes32ToBytes32Map(uint256,bytes32)`,
remove: `$remove_${library}_Bytes32ToBytes32Map(uint256,bytes32)`,
length: `$length_${library}_Bytes32ToBytes32Map(uint256)`,
at: `$at_${library}_Bytes32ToBytes32Map(uint256,uint256)`,
contains: `$contains_${library}_Bytes32ToBytes32Map(uint256,bytes32)`,
keys: `$keys_${library}_Bytes32ToBytes32Map(uint256)`,
}),
{
setReturn: `return$set_${library}_Bytes32ToBytes32Map_bytes32_bytes32`,
removeReturn: `return$remove_${library}_Bytes32ToBytes32Map_bytes32`,
},
);
});
// UintToUintMap
describe('UintToUintMap', function () {
shouldBehaveLikeMap(
[keyA, keyB, keyC],
[keyA, keyB, keyC].map(k => k.add(new BN('1332'))),
new BN('0'),
getMethods({
set: '$set(uint256,uint256,uint256)',
get: `$get_${library}_UintToUintMap(uint256,uint256)`,
getWithMessage: `$get_${library}_UintToUintMap(uint256,uint256,string)`,
tryGet: `$tryGet_${library}_UintToUintMap(uint256,uint256)`,
remove: `$remove_${library}_UintToUintMap(uint256,uint256)`,
length: `$length_${library}_UintToUintMap(uint256)`,
at: `$at_${library}_UintToUintMap(uint256,uint256)`,
contains: `$contains_${library}_UintToUintMap(uint256,uint256)`,
keys: `$keys_${library}_UintToUintMap(uint256)`,
}),
{
setReturn: `return$set_${library}_UintToUintMap_uint256_uint256`,
removeReturn: `return$remove_${library}_UintToUintMap_uint256`,
},
);
});
// Bytes32ToUintMap
describe('Bytes32ToUintMap', function () {
shouldBehaveLikeMap(
[bytesA, bytesB, bytesC],
[keyA, keyB, keyC],
new BN('0'),
getMethods({
set: '$set(uint256,bytes32,uint256)',
get: `$get_${library}_Bytes32ToUintMap(uint256,bytes32)`,
getWithMessage: `$get_${library}_Bytes32ToUintMap(uint256,bytes32,string)`,
tryGet: `$tryGet_${library}_Bytes32ToUintMap(uint256,bytes32)`,
remove: `$remove_${library}_Bytes32ToUintMap(uint256,bytes32)`,
length: `$length_${library}_Bytes32ToUintMap(uint256)`,
at: `$at_${library}_Bytes32ToUintMap(uint256,uint256)`,
contains: `$contains_${library}_Bytes32ToUintMap(uint256,bytes32)`,
keys: `$keys_${library}_Bytes32ToUintMap(uint256)`,
}),
{
setReturn: `return$set_${library}_Bytes32ToUintMap_bytes32_uint256`,
removeReturn: `return$remove_${library}_Bytes32ToUintMap_bytes32`,
},
);
});
});
@@ -0,0 +1,129 @@
const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
function shouldBehaveLikeSet(values, methods, events) {
const [valueA, valueB, valueC] = values;
async function expectMembersMatch(set, values) {
const contains = await Promise.all(values.map(value => methods.contains(set, value)));
expect(contains.every(Boolean)).to.be.equal(true);
const length = await methods.length(set);
expect(length).to.bignumber.equal(values.length.toString());
// To compare values we convert to strings to workaround Chai
// limitations when dealing with nested arrays (required for BNs)
const indexedValues = await Promise.all(
Array(values.length)
.fill()
.map((_, index) => methods.at(set, index)),
);
expect(indexedValues.map(v => v.toString())).to.have.same.members(values.map(v => v.toString()));
const returnedValues = await methods.values(set);
expect(returnedValues.map(v => v.toString())).to.have.same.members(values.map(v => v.toString()));
}
it('starts empty', async function () {
expect(await methods.contains(this.set, valueA)).to.equal(false);
await expectMembersMatch(this.set, []);
});
describe('add', function () {
it('adds a value', async function () {
const receipt = await methods.add(this.set, valueA);
expectEvent(receipt, events.addReturn, { ret0: true });
await expectMembersMatch(this.set, [valueA]);
});
it('adds several values', async function () {
await methods.add(this.set, valueA);
await methods.add(this.set, valueB);
await expectMembersMatch(this.set, [valueA, valueB]);
expect(await methods.contains(this.set, valueC)).to.equal(false);
});
it('returns false when adding values already in the set', async function () {
await methods.add(this.set, valueA);
const receipt = await methods.add(this.set, valueA);
expectEvent(receipt, events.addReturn, { ret0: false });
await expectMembersMatch(this.set, [valueA]);
});
});
describe('at', function () {
it('reverts when retrieving non-existent elements', async function () {
await expectRevert.unspecified(methods.at(this.set, 0));
});
});
describe('remove', function () {
it('removes added values', async function () {
await methods.add(this.set, valueA);
const receipt = await methods.remove(this.set, valueA);
expectEvent(receipt, events.removeReturn, { ret0: true });
expect(await methods.contains(this.set, valueA)).to.equal(false);
await expectMembersMatch(this.set, []);
});
it('returns false when removing values not in the set', async function () {
const receipt = await methods.remove(this.set, valueA);
expectEvent(receipt, events.removeReturn, { ret0: false });
expect(await methods.contains(this.set, valueA)).to.equal(false);
});
it('adds and removes multiple values', async function () {
// []
await methods.add(this.set, valueA);
await methods.add(this.set, valueC);
// [A, C]
await methods.remove(this.set, valueA);
await methods.remove(this.set, valueB);
// [C]
await methods.add(this.set, valueB);
// [C, B]
await methods.add(this.set, valueA);
await methods.remove(this.set, valueC);
// [A, B]
await methods.add(this.set, valueA);
await methods.add(this.set, valueB);
// [A, B]
await methods.add(this.set, valueC);
await methods.remove(this.set, valueA);
// [B, C]
await methods.add(this.set, valueA);
await methods.remove(this.set, valueB);
// [A, C]
await expectMembersMatch(this.set, [valueA, valueC]);
expect(await methods.contains(this.set, valueB)).to.equal(false);
});
});
}
module.exports = {
shouldBehaveLikeSet,
};
@@ -0,0 +1,79 @@
const EnumerableSet = artifacts.require('$EnumerableSet');
const { mapValues } = require('../../helpers/map-values');
const { shouldBehaveLikeSet } = require('./EnumerableSet.behavior');
const getMethods = ms => {
return mapValues(
ms,
m =>
(self, ...args) =>
self.methods[m](0, ...args),
);
};
// Get the name of the library. In the transpiled code it will be EnumerableSetUpgradeable.
const library = EnumerableSet._json.contractName.replace(/^\$/, '');
contract('EnumerableSet', function (accounts) {
beforeEach(async function () {
this.set = await EnumerableSet.new();
});
// Bytes32Set
describe('EnumerableBytes32Set', function () {
shouldBehaveLikeSet(
['0xdeadbeef', '0x0123456789', '0x42424242'].map(e => e.padEnd(66, '0')),
getMethods({
add: '$add(uint256,bytes32)',
remove: '$remove(uint256,bytes32)',
contains: '$contains(uint256,bytes32)',
length: `$length_${library}_Bytes32Set(uint256)`,
at: `$at_${library}_Bytes32Set(uint256,uint256)`,
values: `$values_${library}_Bytes32Set(uint256)`,
}),
{
addReturn: `return$add_${library}_Bytes32Set_bytes32`,
removeReturn: `return$remove_${library}_Bytes32Set_bytes32`,
},
);
});
// AddressSet
describe('EnumerableAddressSet', function () {
shouldBehaveLikeSet(
accounts,
getMethods({
add: '$add(uint256,address)',
remove: '$remove(uint256,address)',
contains: '$contains(uint256,address)',
length: `$length_${library}_AddressSet(uint256)`,
at: `$at_${library}_AddressSet(uint256,uint256)`,
values: `$values_${library}_AddressSet(uint256)`,
}),
{
addReturn: `return$add_${library}_AddressSet_address`,
removeReturn: `return$remove_${library}_AddressSet_address`,
},
);
});
// UintSet
describe('EnumerableUintSet', function () {
shouldBehaveLikeSet(
[1234, 5678, 9101112].map(e => web3.utils.toBN(e)),
getMethods({
add: '$add(uint256,uint256)',
remove: '$remove(uint256,uint256)',
contains: '$contains(uint256,uint256)',
length: `$length_${library}_UintSet(uint256)`,
at: `$at_${library}_UintSet(uint256,uint256)`,
values: `$values_${library}_UintSet(uint256)`,
}),
{
addReturn: `return$add_${library}_UintSet_uint256`,
removeReturn: `return$remove_${library}_UintSet_uint256`,
},
);
});
});