Skip to content

Commit 0134b00

Browse files
Amxxarr00ernestognw
authored
Add nonReentrantView modifier (#5800)
Co-authored-by: Arr00 <[email protected]> Co-authored-by: ernestognw <[email protected]>
1 parent 3635d3c commit 0134b00

File tree

7 files changed

+78
-8
lines changed

7 files changed

+78
-8
lines changed

.changeset/quick-pianos-press.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+
`ReentrancyGuard` and `ReentrancyGuardTransient`: Add `nonReentrantView`, a read-only version of the `nonReentrant` modifier.

contracts/mocks/ReentrancyAttack.sol

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@ contract ReentrancyAttack is Context {
99
(bool success, ) = _msgSender().call(data);
1010
require(success, "ReentrancyAttack: failed call");
1111
}
12+
13+
function staticcallSender(bytes calldata data) public view {
14+
(bool success, ) = _msgSender().staticcall(data);
15+
require(success, "ReentrancyAttack: failed call");
16+
}
1217
}

contracts/mocks/ReentrancyMock.sol

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ contract ReentrancyMock is ReentrancyGuard {
1616
_count();
1717
}
1818

19+
function viewCallback() external view nonReentrantView returns (uint256) {
20+
return counter;
21+
}
22+
1923
function countLocalRecursive(uint256 n) public nonReentrant {
2024
if (n > 0) {
2125
_count();
@@ -36,6 +40,11 @@ contract ReentrancyMock is ReentrancyGuard {
3640
attacker.callSender(abi.encodeCall(this.callback, ()));
3741
}
3842

43+
function countAndCallView(ReentrancyAttack attacker) public nonReentrant {
44+
_count();
45+
attacker.staticcallSender(abi.encodeCall(this.viewCallback, ()));
46+
}
47+
3948
function _count() private {
4049
counter += 1;
4150
}

contracts/mocks/ReentrancyTransientMock.sol

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ contract ReentrancyTransientMock is ReentrancyGuardTransient {
1616
_count();
1717
}
1818

19+
function viewCallback() external view nonReentrantView returns (uint256) {
20+
return counter;
21+
}
22+
1923
function countLocalRecursive(uint256 n) public nonReentrant {
2024
if (n > 0) {
2125
_count();
@@ -36,6 +40,11 @@ contract ReentrancyTransientMock is ReentrancyGuardTransient {
3640
attacker.callSender(abi.encodeCall(this.callback, ()));
3741
}
3842

43+
function countAndCallView(ReentrancyAttack attacker) public nonReentrant {
44+
_count();
45+
attacker.staticcallSender(abi.encodeCall(this.viewCallback, ()));
46+
}
47+
3948
function _count() private {
4049
counter += 1;
4150
}

contracts/utils/ReentrancyGuard.sol

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,28 @@ abstract contract ReentrancyGuard {
6161
_nonReentrantAfter();
6262
}
6363

64-
function _nonReentrantBefore() private {
65-
// On the first call to nonReentrant, _status will be NOT_ENTERED
64+
/**
65+
* @dev A `view` only version of {nonReentrant}. Use to block view functions
66+
* from being called, preventing reading from inconsistent contract state.
67+
*
68+
* CAUTION: This is a "view" modifier and does not change the reentrancy
69+
* status. Use it only on view functions. For payable or non-payable functions,
70+
* use the standard {nonReentrant} modifier instead.
71+
*/
72+
modifier nonReentrantView() {
73+
_nonReentrantBeforeView();
74+
_;
75+
}
76+
77+
function _nonReentrantBeforeView() private view {
6678
if (_status == ENTERED) {
6779
revert ReentrancyGuardReentrantCall();
6880
}
81+
}
82+
83+
function _nonReentrantBefore() private {
84+
// On the first call to nonReentrant, _status will be NOT_ENTERED
85+
_nonReentrantBeforeView();
6986

7087
// Any calls to nonReentrant after this point will fail
7188
_status = ENTERED;

contracts/utils/ReentrancyGuardTransient.sol

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,28 @@ abstract contract ReentrancyGuardTransient {
3737
_nonReentrantAfter();
3838
}
3939

40-
function _nonReentrantBefore() private {
41-
// On the first call to nonReentrant, REENTRANCY_GUARD_STORAGE.asBoolean().tload() will be false
40+
/**
41+
* @dev A `view` only version of {nonReentrant}. Use to block view functions
42+
* from being called, preventing reading from inconsistent contract state.
43+
*
44+
* CAUTION: This is a "view" modifier and does not change the reentrancy
45+
* status. Use it only on view functions. For payable or non-payable functions,
46+
* use the standard {nonReentrant} modifier instead.
47+
*/
48+
modifier nonReentrantView() {
49+
_nonReentrantBeforeView();
50+
_;
51+
}
52+
53+
function _nonReentrantBeforeView() private view {
4254
if (_reentrancyGuardEntered()) {
4355
revert ReentrancyGuardReentrantCall();
4456
}
57+
}
58+
59+
function _nonReentrantBefore() private {
60+
// On the first call to nonReentrant, REENTRANCY_GUARD_STORAGE.asBoolean().tload() will be false
61+
_nonReentrantBeforeView();
4562

4663
// Any calls to nonReentrant after this point will fail
4764
REENTRANCY_GUARD_STORAGE.asBoolean().tstore(true);

test/utils/ReentrancyGuard.test.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ for (const variant of ['', 'Transient']) {
77
async function fixture() {
88
const name = `Reentrancy${variant}Mock`;
99
const mock = await ethers.deployContract(name);
10-
return { name, mock };
10+
const attacker = await ethers.deployContract('ReentrancyAttack');
11+
return { name, mock, attacker };
1112
}
1213

1314
beforeEach(async function () {
@@ -20,9 +21,16 @@ for (const variant of ['', 'Transient']) {
2021
expect(await this.mock.counter()).to.equal(1n);
2122
});
2223

23-
it('does not allow remote callback', async function () {
24-
const attacker = await ethers.deployContract('ReentrancyAttack');
25-
await expect(this.mock.countAndCall(attacker)).to.be.revertedWith('ReentrancyAttack: failed call');
24+
it('nonReentrantView function can be called', async function () {
25+
await this.mock.viewCallback();
26+
});
27+
28+
it('does not allow remote callback to nonReentrant function', async function () {
29+
await expect(this.mock.countAndCall(this.attacker)).to.be.revertedWith('ReentrancyAttack: failed call');
30+
});
31+
32+
it('does not allow remote callback to nonReentrantView function', async function () {
33+
await expect(this.mock.countAndCallView(this.attacker)).to.be.revertedWith('ReentrancyAttack: failed call');
2634
});
2735

2836
it('_reentrancyGuardEntered should be true when guarded', async function () {

0 commit comments

Comments
 (0)