Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/blue-scissors-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`Math`: Make `ceilDiv` to revert on 0 division even if the numerator is 0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its actually a panic. But this is already merge so whatever.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the changeset is correct, it reverts with a Panic error.

2 changes: 1 addition & 1 deletion contracts/mocks/token/ERC20Reentrant.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity ^0.8.19;

import "../../token/ERC20/ERC20.sol";
import "../../token/ERC20/extensions/ERC4626.sol";
import "../../utils/Address.sol";

contract ERC20Reentrant is ERC20("TEST", "TST") {
enum Type {
Expand Down
23 changes: 23 additions & 0 deletions contracts/mocks/token/ERC4626LimitsMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import "../../token/ERC20/extensions/ERC4626.sol";

abstract contract ERC4626LimitsMock is ERC4626 {
uint256 _maxDeposit;
uint256 _maxMint;

constructor() {
_maxDeposit = 100 ether;
_maxMint = 100 ether;
}

function maxDeposit(address) public view override returns (uint256) {
return _maxDeposit;
}

function maxMint(address) public view override returns (uint256) {
return _maxMint;
}
}
5 changes: 5 additions & 0 deletions contracts/utils/math/Math.sol
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ library Math {
* of rounding down.
*/
function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
if (b == 0) {
// Guarantee the same behavior as in a regular Solidity division.
return a / b;
}

// (a + b - 1) / b can overflow on addition, so we distribute.
return a == 0 ? 0 : (a - 1) / b + 1;
}
Expand Down
8 changes: 8 additions & 0 deletions test/token/ERC1155/ERC1155.behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,14 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m
);
});

it('reverts when transferring from zero address', async function () {
await expectRevertCustomError(
this.token.$_safeBatchTransferFrom(ZERO_ADDRESS, multiTokenHolder, [firstTokenId], [firstAmount], '0x'),
'ERC1155InvalidSender',
[ZERO_ADDRESS],
);
});

function batchTransferWasSuccessful({ operator, from, ids, values }) {
it('debits transferred balances from sender', async function () {
const newBalances = await this.token.balanceOfBatch(new Array(ids.length).fill(from), ids);
Expand Down
74 changes: 74 additions & 0 deletions test/token/ERC20/extensions/ERC4626.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const { expectRevertCustomError } = require('../../../helpers/customError');

const ERC20Decimals = artifacts.require('$ERC20DecimalsMock');
const ERC4626 = artifacts.require('$ERC4626');
const ERC4626LimitsMock = artifacts.require('$ERC4626LimitsMock');
const ERC4626OffsetMock = artifacts.require('$ERC4626OffsetMock');
const ERC4626FeesMock = artifacts.require('$ERC4626FeesMock');
const ERC20ExcessDecimalsMock = artifacts.require('ERC20ExcessDecimalsMock');
Expand Down Expand Up @@ -220,6 +221,49 @@ contract('ERC4626', function (accounts) {
});
});

describe('limits', async function () {
beforeEach(async function () {
this.token = await ERC20Decimals.new(name, symbol, decimals);
this.vault = await ERC4626LimitsMock.new(name + ' Vault', symbol + 'V', this.token.address);
});

it('reverts on deposit() above max deposit', async function () {
const maxDeposit = await this.vault.maxDeposit(holder);
await expectRevertCustomError(this.vault.deposit(maxDeposit.addn(1), recipient), 'ERC4626ExceededMaxDeposit', [
recipient,
maxDeposit.addn(1),
maxDeposit,
]);
});

it('reverts on mint() above max mint', async function () {
const maxMint = await this.vault.maxMint(holder);
await expectRevertCustomError(this.vault.mint(maxMint.addn(1), recipient), 'ERC4626ExceededMaxMint', [
recipient,
maxMint.addn(1),
maxMint,
]);
});

it('reverts on withdraw() above max withdraw', async function () {
const maxWithdraw = await this.vault.maxWithdraw(holder);
await expectRevertCustomError(
this.vault.withdraw(maxWithdraw.addn(1), recipient, holder),
'ERC4626ExceededMaxWithdraw',
[holder, maxWithdraw.addn(1), maxWithdraw],
);
});

it('reverts on redeem() above max redeem', async function () {
const maxRedeem = await this.vault.maxRedeem(holder);
await expectRevertCustomError(
this.vault.redeem(maxRedeem.addn(1), recipient, holder),
'ERC4626ExceededMaxRedeem',
[holder, maxRedeem.addn(1), maxRedeem],
);
});
});

for (const offset of [0, 6, 18].map(web3.utils.toBN)) {
const parseToken = token => web3.utils.toBN(10).pow(decimals).muln(token);
const parseShare = share => web3.utils.toBN(10).pow(decimals.add(offset)).muln(share);
Expand Down Expand Up @@ -849,6 +893,9 @@ contract('ERC4626', function (accounts) {
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('0');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2000');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0');
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
'2000',
);
expect(await this.vault.totalSupply()).to.be.bignumber.equal('2000');
expect(await this.vault.totalAssets()).to.be.bignumber.equal('2000');
}
Expand All @@ -872,6 +919,9 @@ contract('ERC4626', function (accounts) {
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2000');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('4000');
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
'6000',
);
expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000');
expect(await this.vault.totalAssets()).to.be.bignumber.equal('6000');
}
Expand All @@ -883,6 +933,9 @@ contract('ERC4626', function (accounts) {
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2999'); // used to be 3000, but virtual assets/shares captures part of the yield
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('5999'); // used to be 6000, but virtual assets/shares captures part of the yield
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
'6000',
);
expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000');
expect(await this.vault.totalAssets()).to.be.bignumber.equal('9000');

Expand All @@ -904,6 +957,9 @@ contract('ERC4626', function (accounts) {
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('4999');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('6000');
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
'7333',
);
expect(await this.vault.totalSupply()).to.be.bignumber.equal('7333');
expect(await this.vault.totalAssets()).to.be.bignumber.equal('11000');
}
Expand All @@ -928,6 +984,9 @@ contract('ERC4626', function (accounts) {
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('4999'); // used to be 5000
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('9000');
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
'9333',
);
expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333');
expect(await this.vault.totalAssets()).to.be.bignumber.equal('14000'); // used to be 14001
}
Expand All @@ -940,6 +999,9 @@ contract('ERC4626', function (accounts) {
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('6070'); // used to be 6071
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10928'); // used to be 10929
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
'9333',
);
expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333');
expect(await this.vault.totalAssets()).to.be.bignumber.equal('17000'); // used to be 17001

Expand All @@ -961,6 +1023,9 @@ contract('ERC4626', function (accounts) {
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3643');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10929');
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
'8000',
);
expect(await this.vault.totalSupply()).to.be.bignumber.equal('8000');
expect(await this.vault.totalAssets()).to.be.bignumber.equal('14573');
}
Expand All @@ -983,6 +1048,9 @@ contract('ERC4626', function (accounts) {
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3643');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8000');
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
'6392',
);
expect(await this.vault.totalSupply()).to.be.bignumber.equal('6392');
expect(await this.vault.totalAssets()).to.be.bignumber.equal('11644');
}
Expand All @@ -1006,6 +1074,9 @@ contract('ERC4626', function (accounts) {
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8000'); // used to be 8001
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
'4392',
);
expect(await this.vault.totalSupply()).to.be.bignumber.equal('4392');
expect(await this.vault.totalAssets()).to.be.bignumber.equal('8001');
}
Expand All @@ -1028,6 +1099,9 @@ contract('ERC4626', function (accounts) {
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('0');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0');
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0');
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
'0',
);
expect(await this.vault.totalSupply()).to.be.bignumber.equal('0');
expect(await this.vault.totalAssets()).to.be.bignumber.equal('1'); // used to be 0
}
Expand Down
11 changes: 10 additions & 1 deletion test/token/ERC721/extensions/ERC721Burnable.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const { expectRevertCustomError } = require('../../../helpers/customError');
const ERC721Burnable = artifacts.require('$ERC721Burnable');

contract('ERC721Burnable', function (accounts) {
const [owner, approved] = accounts;
const [owner, approved, another] = accounts;

const firstTokenId = new BN(1);
const secondTokenId = new BN(2);
Expand Down Expand Up @@ -61,6 +61,15 @@ contract('ERC721Burnable', function (accounts) {
});
});

describe('when there is no previous approval burned', function () {
it('reverts', async function () {
await expectRevertCustomError(this.token.burn(tokenId, { from: another }), 'ERC721InsufficientApproval', [
another,
tokenId,
]);
});
});

describe('when the given token ID was not tracked by this contract', function () {
it('reverts', async function () {
await expectRevertCustomError(this.token.burn(unknownTokenId, { from: owner }), 'ERC721NonexistentToken', [
Expand Down
19 changes: 19 additions & 0 deletions test/token/ERC721/extensions/ERC721Consecutive.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ contract('ERC721Consecutive', function (accounts) {
expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(balance));
}
});

it('reverts on consecutive minting to the zero address', async function () {
await expectRevertCustomError(
ERC721ConsecutiveMock.new(name, symbol, offset, delegates, [ZERO_ADDRESS], [10]),
'ERC721InvalidReceiver',
[ZERO_ADDRESS],
);
});
});

describe('minting after construction', function () {
Expand Down Expand Up @@ -172,6 +180,17 @@ contract('ERC721Consecutive', function (accounts) {
expect(await this.token.$_exists(tokenId)).to.be.equal(true);
expect(await this.token.ownerOf(tokenId), user2);
});

it('reverts burning batches of size != 1', async function () {
const tokenId = batches[0].amount + offset;
const receiver = batches[0].receiver;

await expectRevertCustomError(
this.token.$_afterTokenTransfer(receiver, ZERO_ADDRESS, tokenId, 2),
'ERC721ForbiddenBatchBurn',
[],
);
});
});
});
}
Expand Down
18 changes: 18 additions & 0 deletions test/utils/math/Math.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const { MAX_UINT256 } = constants;
const { Rounding } = require('../../helpers/enums.js');
const { expectRevertCustomError } = require('../../helpers/customError.js');

const Math = artifacts.require('$Math');

Expand Down Expand Up @@ -204,6 +205,19 @@ contract('Math', function () {
});

describe('ceilDiv', function () {
it('reverts on zero division', async function () {
const a = new BN('2');
const b = new BN('0');
// It's unspecified because it's a low level 0 division error
await expectRevert.unspecified(this.math.$ceilDiv(a, b));
});

it('does not round up a zero result', async function () {
const a = new BN('0');
const b = new BN('2');
expect(await this.math.$ceilDiv(a, b)).to.be.bignumber.equal('0');
});

it('does not round up on exact division', async function () {
const a = new BN('10');
const b = new BN('5');
Expand Down Expand Up @@ -233,6 +247,10 @@ contract('Math', function () {
await expectRevert.unspecified(this.math.$mulDiv(1, 1, 0, Rounding.Down));
});

it('reverts with result higher than 2 ^ 256', async function () {
await expectRevertCustomError(this.math.$mulDiv(5, MAX_UINT256, 2, Rounding.Down), 'MathOverflowedMulDiv', []);
});

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');
Expand Down
15 changes: 15 additions & 0 deletions test/utils/structs/Checkpoints.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { expect } = require('chai');

const { VALUE_SIZES } = require('../../../scripts/generate/templates/Checkpoints.opts.js');
const { expectRevertCustomError } = require('../../helpers/customError.js');
const { expectRevert } = require('@openzeppelin/test-helpers');

const $Checkpoints = artifacts.require('$Checkpoints');

Expand All @@ -22,6 +23,7 @@ contract('Checkpoints', function () {
describe(`Trace${length}`, function () {
beforeEach(async function () {
this.methods = {
at: (...args) => this.mock.methods[`$at_${libraryName}_Trace${length}(uint256,uint32)`](0, ...args),
latest: (...args) => this.mock.methods[`$latest_${libraryName}_Trace${length}(uint256)`](0, ...args),
latestCheckpoint: (...args) =>
this.mock.methods[`$latestCheckpoint_${libraryName}_Trace${length}(uint256)`](0, ...args),
Expand All @@ -35,6 +37,11 @@ contract('Checkpoints', function () {
});

describe('without checkpoints', function () {
it('at zero reverts', async function () {
// Reverts with array out of bound access, which is unspecified
await expectRevert.unspecified(this.methods.at(0));
});

it('returns zero as latest value', async function () {
expect(await this.methods.latest()).to.be.bignumber.equal('0');

Expand Down Expand Up @@ -65,6 +72,14 @@ contract('Checkpoints', function () {
}
});

it('at keys', async function () {
for (const [index, { key, value }] of this.checkpoints.entries()) {
const at = await this.methods.at(index);
expect(at._value).to.be.bignumber.equal(value);
expect(at._key).to.be.bignumber.equal(key);
}
});

it('length', async function () {
expect(await this.methods.length()).to.be.bignumber.equal(this.checkpoints.length.toString());
});
Expand Down