Skip to content

Commit 2477534

Browse files
authored
Change behavior of ceilDiv(0, 0) and improve test coverage (#4348)
1 parent ac5480e commit 2477534

File tree

10 files changed

+178
-2
lines changed

10 files changed

+178
-2
lines changed

.changeset/blue-scissors-design.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`Math`: Make `ceilDiv` to revert on 0 division even if the numerator is 0

contracts/mocks/token/ERC20Reentrant.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
pragma solidity ^0.8.19;
33

44
import "../../token/ERC20/ERC20.sol";
5-
import "../../token/ERC20/extensions/ERC4626.sol";
5+
import "../../utils/Address.sol";
66

77
contract ERC20Reentrant is ERC20("TEST", "TST") {
88
enum Type {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.19;
4+
5+
import "../../token/ERC20/extensions/ERC4626.sol";
6+
7+
abstract contract ERC4626LimitsMock is ERC4626 {
8+
uint256 _maxDeposit;
9+
uint256 _maxMint;
10+
11+
constructor() {
12+
_maxDeposit = 100 ether;
13+
_maxMint = 100 ether;
14+
}
15+
16+
function maxDeposit(address) public view override returns (uint256) {
17+
return _maxDeposit;
18+
}
19+
20+
function maxMint(address) public view override returns (uint256) {
21+
return _maxMint;
22+
}
23+
}

contracts/utils/math/Math.sol

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ library Math {
114114
* of rounding down.
115115
*/
116116
function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
117+
if (b == 0) {
118+
// Guarantee the same behavior as in a regular Solidity division.
119+
return a / b;
120+
}
121+
117122
// (a + b - 1) / b can overflow on addition, so we distribute.
118123
return a == 0 ? 0 : (a - 1) / b + 1;
119124
}

test/token/ERC1155/ERC1155.behavior.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,14 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m
479479
);
480480
});
481481

482+
it('reverts when transferring from zero address', async function () {
483+
await expectRevertCustomError(
484+
this.token.$_safeBatchTransferFrom(ZERO_ADDRESS, multiTokenHolder, [firstTokenId], [firstAmount], '0x'),
485+
'ERC1155InvalidSender',
486+
[ZERO_ADDRESS],
487+
);
488+
});
489+
482490
function batchTransferWasSuccessful({ operator, from, ids, values }) {
483491
it('debits transferred balances from sender', async function () {
484492
const newBalances = await this.token.balanceOfBatch(new Array(ids.length).fill(from), ids);

test/token/ERC20/extensions/ERC4626.test.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const { expectRevertCustomError } = require('../../../helpers/customError');
66

77
const ERC20Decimals = artifacts.require('$ERC20DecimalsMock');
88
const ERC4626 = artifacts.require('$ERC4626');
9+
const ERC4626LimitsMock = artifacts.require('$ERC4626LimitsMock');
910
const ERC4626OffsetMock = artifacts.require('$ERC4626OffsetMock');
1011
const ERC4626FeesMock = artifacts.require('$ERC4626FeesMock');
1112
const ERC20ExcessDecimalsMock = artifacts.require('ERC20ExcessDecimalsMock');
@@ -220,6 +221,49 @@ contract('ERC4626', function (accounts) {
220221
});
221222
});
222223

224+
describe('limits', async function () {
225+
beforeEach(async function () {
226+
this.token = await ERC20Decimals.new(name, symbol, decimals);
227+
this.vault = await ERC4626LimitsMock.new(name + ' Vault', symbol + 'V', this.token.address);
228+
});
229+
230+
it('reverts on deposit() above max deposit', async function () {
231+
const maxDeposit = await this.vault.maxDeposit(holder);
232+
await expectRevertCustomError(this.vault.deposit(maxDeposit.addn(1), recipient), 'ERC4626ExceededMaxDeposit', [
233+
recipient,
234+
maxDeposit.addn(1),
235+
maxDeposit,
236+
]);
237+
});
238+
239+
it('reverts on mint() above max mint', async function () {
240+
const maxMint = await this.vault.maxMint(holder);
241+
await expectRevertCustomError(this.vault.mint(maxMint.addn(1), recipient), 'ERC4626ExceededMaxMint', [
242+
recipient,
243+
maxMint.addn(1),
244+
maxMint,
245+
]);
246+
});
247+
248+
it('reverts on withdraw() above max withdraw', async function () {
249+
const maxWithdraw = await this.vault.maxWithdraw(holder);
250+
await expectRevertCustomError(
251+
this.vault.withdraw(maxWithdraw.addn(1), recipient, holder),
252+
'ERC4626ExceededMaxWithdraw',
253+
[holder, maxWithdraw.addn(1), maxWithdraw],
254+
);
255+
});
256+
257+
it('reverts on redeem() above max redeem', async function () {
258+
const maxRedeem = await this.vault.maxRedeem(holder);
259+
await expectRevertCustomError(
260+
this.vault.redeem(maxRedeem.addn(1), recipient, holder),
261+
'ERC4626ExceededMaxRedeem',
262+
[holder, maxRedeem.addn(1), maxRedeem],
263+
);
264+
});
265+
});
266+
223267
for (const offset of [0, 6, 18].map(web3.utils.toBN)) {
224268
const parseToken = token => web3.utils.toBN(10).pow(decimals).muln(token);
225269
const parseShare = share => web3.utils.toBN(10).pow(decimals.add(offset)).muln(share);
@@ -849,6 +893,9 @@ contract('ERC4626', function (accounts) {
849893
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('0');
850894
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2000');
851895
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0');
896+
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
897+
'2000',
898+
);
852899
expect(await this.vault.totalSupply()).to.be.bignumber.equal('2000');
853900
expect(await this.vault.totalAssets()).to.be.bignumber.equal('2000');
854901
}
@@ -872,6 +919,9 @@ contract('ERC4626', function (accounts) {
872919
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
873920
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2000');
874921
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('4000');
922+
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
923+
'6000',
924+
);
875925
expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000');
876926
expect(await this.vault.totalAssets()).to.be.bignumber.equal('6000');
877927
}
@@ -883,6 +933,9 @@ contract('ERC4626', function (accounts) {
883933
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
884934
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
885935
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
936+
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
937+
'6000',
938+
);
886939
expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000');
887940
expect(await this.vault.totalAssets()).to.be.bignumber.equal('9000');
888941

@@ -904,6 +957,9 @@ contract('ERC4626', function (accounts) {
904957
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
905958
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('4999');
906959
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('6000');
960+
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
961+
'7333',
962+
);
907963
expect(await this.vault.totalSupply()).to.be.bignumber.equal('7333');
908964
expect(await this.vault.totalAssets()).to.be.bignumber.equal('11000');
909965
}
@@ -928,6 +984,9 @@ contract('ERC4626', function (accounts) {
928984
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
929985
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('4999'); // used to be 5000
930986
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('9000');
987+
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
988+
'9333',
989+
);
931990
expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333');
932991
expect(await this.vault.totalAssets()).to.be.bignumber.equal('14000'); // used to be 14001
933992
}
@@ -940,6 +999,9 @@ contract('ERC4626', function (accounts) {
940999
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
9411000
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('6070'); // used to be 6071
9421001
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10928'); // used to be 10929
1002+
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
1003+
'9333',
1004+
);
9431005
expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333');
9441006
expect(await this.vault.totalAssets()).to.be.bignumber.equal('17000'); // used to be 17001
9451007

@@ -961,6 +1023,9 @@ contract('ERC4626', function (accounts) {
9611023
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
9621024
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3643');
9631025
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10929');
1026+
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
1027+
'8000',
1028+
);
9641029
expect(await this.vault.totalSupply()).to.be.bignumber.equal('8000');
9651030
expect(await this.vault.totalAssets()).to.be.bignumber.equal('14573');
9661031
}
@@ -983,6 +1048,9 @@ contract('ERC4626', function (accounts) {
9831048
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392');
9841049
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3643');
9851050
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8000');
1051+
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
1052+
'6392',
1053+
);
9861054
expect(await this.vault.totalSupply()).to.be.bignumber.equal('6392');
9871055
expect(await this.vault.totalAssets()).to.be.bignumber.equal('11644');
9881056
}
@@ -1006,6 +1074,9 @@ contract('ERC4626', function (accounts) {
10061074
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392');
10071075
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0');
10081076
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8000'); // used to be 8001
1077+
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
1078+
'4392',
1079+
);
10091080
expect(await this.vault.totalSupply()).to.be.bignumber.equal('4392');
10101081
expect(await this.vault.totalAssets()).to.be.bignumber.equal('8001');
10111082
}
@@ -1028,6 +1099,9 @@ contract('ERC4626', function (accounts) {
10281099
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('0');
10291100
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0');
10301101
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0');
1102+
expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal(
1103+
'0',
1104+
);
10311105
expect(await this.vault.totalSupply()).to.be.bignumber.equal('0');
10321106
expect(await this.vault.totalAssets()).to.be.bignumber.equal('1'); // used to be 0
10331107
}

test/token/ERC721/extensions/ERC721Burnable.test.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const { expectRevertCustomError } = require('../../../helpers/customError');
66
const ERC721Burnable = artifacts.require('$ERC721Burnable');
77

88
contract('ERC721Burnable', function (accounts) {
9-
const [owner, approved] = accounts;
9+
const [owner, approved, another] = accounts;
1010

1111
const firstTokenId = new BN(1);
1212
const secondTokenId = new BN(2);
@@ -61,6 +61,15 @@ contract('ERC721Burnable', function (accounts) {
6161
});
6262
});
6363

64+
describe('when there is no previous approval burned', function () {
65+
it('reverts', async function () {
66+
await expectRevertCustomError(this.token.burn(tokenId, { from: another }), 'ERC721InsufficientApproval', [
67+
another,
68+
tokenId,
69+
]);
70+
});
71+
});
72+
6473
describe('when the given token ID was not tracked by this contract', function () {
6574
it('reverts', async function () {
6675
await expectRevertCustomError(this.token.burn(unknownTokenId, { from: owner }), 'ERC721NonexistentToken', [

test/token/ERC721/extensions/ERC721Consecutive.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ contract('ERC721Consecutive', function (accounts) {
8585
expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(balance));
8686
}
8787
});
88+
89+
it('reverts on consecutive minting to the zero address', async function () {
90+
await expectRevertCustomError(
91+
ERC721ConsecutiveMock.new(name, symbol, offset, delegates, [ZERO_ADDRESS], [10]),
92+
'ERC721InvalidReceiver',
93+
[ZERO_ADDRESS],
94+
);
95+
});
8896
});
8997

9098
describe('minting after construction', function () {
@@ -172,6 +180,17 @@ contract('ERC721Consecutive', function (accounts) {
172180
expect(await this.token.$_exists(tokenId)).to.be.equal(true);
173181
expect(await this.token.ownerOf(tokenId), user2);
174182
});
183+
184+
it('reverts burning batches of size != 1', async function () {
185+
const tokenId = batches[0].amount + offset;
186+
const receiver = batches[0].receiver;
187+
188+
await expectRevertCustomError(
189+
this.token.$_afterTokenTransfer(receiver, ZERO_ADDRESS, tokenId, 2),
190+
'ERC721ForbiddenBatchBurn',
191+
[],
192+
);
193+
});
175194
});
176195
});
177196
}

test/utils/math/Math.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers');
22
const { expect } = require('chai');
33
const { MAX_UINT256 } = constants;
44
const { Rounding } = require('../../helpers/enums.js');
5+
const { expectRevertCustomError } = require('../../helpers/customError.js');
56

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

@@ -204,6 +205,19 @@ contract('Math', function () {
204205
});
205206

206207
describe('ceilDiv', function () {
208+
it('reverts on zero division', async function () {
209+
const a = new BN('2');
210+
const b = new BN('0');
211+
// It's unspecified because it's a low level 0 division error
212+
await expectRevert.unspecified(this.math.$ceilDiv(a, b));
213+
});
214+
215+
it('does not round up a zero result', async function () {
216+
const a = new BN('0');
217+
const b = new BN('2');
218+
expect(await this.math.$ceilDiv(a, b)).to.be.bignumber.equal('0');
219+
});
220+
207221
it('does not round up on exact division', async function () {
208222
const a = new BN('10');
209223
const b = new BN('5');
@@ -233,6 +247,10 @@ contract('Math', function () {
233247
await expectRevert.unspecified(this.math.$mulDiv(1, 1, 0, Rounding.Down));
234248
});
235249

250+
it('reverts with result higher than 2 ^ 256', async function () {
251+
await expectRevertCustomError(this.math.$mulDiv(5, MAX_UINT256, 2, Rounding.Down), 'MathOverflowedMulDiv', []);
252+
});
253+
236254
describe('does round down', async function () {
237255
it('small values', async function () {
238256
expect(await this.math.$mulDiv('3', '4', '5', Rounding.Down)).to.be.bignumber.equal('2');

test/utils/structs/Checkpoints.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const { expect } = require('chai');
44

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

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

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

3739
describe('without checkpoints', function () {
40+
it('at zero reverts', async function () {
41+
// Reverts with array out of bound access, which is unspecified
42+
await expectRevert.unspecified(this.methods.at(0));
43+
});
44+
3845
it('returns zero as latest value', async function () {
3946
expect(await this.methods.latest()).to.be.bignumber.equal('0');
4047

@@ -65,6 +72,14 @@ contract('Checkpoints', function () {
6572
}
6673
});
6774

75+
it('at keys', async function () {
76+
for (const [index, { key, value }] of this.checkpoints.entries()) {
77+
const at = await this.methods.at(index);
78+
expect(at._value).to.be.bignumber.equal(value);
79+
expect(at._key).to.be.bignumber.equal(key);
80+
}
81+
});
82+
6883
it('length', async function () {
6984
expect(await this.methods.length()).to.be.bignumber.equal(this.checkpoints.length.toString());
7085
});

0 commit comments

Comments
 (0)