Skip to content

Commit b6e0791

Browse files
Amxxernestognw
andauthored
Transient version of ReentrancyGuard (#4988)
Co-authored-by: ernestognw <[email protected]>
1 parent 8a7a9c5 commit b6e0791

File tree

6 files changed

+164
-42
lines changed

6 files changed

+164
-42
lines changed

.changeset/witty-chicken-smile.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+
`ReentrancyGuardTransient`: Added a variant of `ReentrancyGuard` that uses transient storage.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {ReentrancyGuardTransient} from "../utils/ReentrancyGuardTransient.sol";
6+
import {ReentrancyAttack} from "./ReentrancyAttack.sol";
7+
8+
contract ReentrancyTransientMock is ReentrancyGuardTransient {
9+
uint256 public counter;
10+
11+
constructor() {
12+
counter = 0;
13+
}
14+
15+
function callback() external nonReentrant {
16+
_count();
17+
}
18+
19+
function countLocalRecursive(uint256 n) public nonReentrant {
20+
if (n > 0) {
21+
_count();
22+
countLocalRecursive(n - 1);
23+
}
24+
}
25+
26+
function countThisRecursive(uint256 n) public nonReentrant {
27+
if (n > 0) {
28+
_count();
29+
(bool success, ) = address(this).call(abi.encodeCall(this.countThisRecursive, (n - 1)));
30+
require(success, "ReentrancyTransientMock: failed call");
31+
}
32+
}
33+
34+
function countAndCall(ReentrancyAttack attacker) public nonReentrant {
35+
_count();
36+
attacker.callSender(abi.encodeCall(this.callback, ()));
37+
}
38+
39+
function _count() private {
40+
counter += 1;
41+
}
42+
43+
function guardedCheckEntered() public nonReentrant {
44+
require(_reentrancyGuardEntered());
45+
}
46+
47+
function unguardedCheckNotEntered() public view {
48+
require(!_reentrancyGuardEntered());
49+
}
50+
}

contracts/utils/README.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
1313
* {MerkleProof}: Functions for verifying https://en.wikipedia.org/wiki/Merkle_tree[Merkle Tree] proofs.
1414
* {EIP712}: Contract with functions to allow processing signed typed structure data according to https://eips.ethereum.org/EIPS/eip-712[EIP-712].
1515
* {ReentrancyGuard}: A modifier that can prevent reentrancy during certain functions.
16+
* {ReentrancyGuardTransient}: Variant of {ReentrancyGuard} that uses transient storage (https://eips.ethereum.org/EIPS/eip-1153[EIP-1153]).
1617
* {Pausable}: A common emergency response mechanism that can pause functionality while a remediation is pending.
1718
* {Nonces}: Utility for tracking and verifying address nonces that only increment.
1819
* {ERC165, ERC165Checker}: Utilities for inspecting interfaces supported by contracts.
@@ -65,6 +66,8 @@ Because Solidity does not support generic types, {EnumerableMap} and {Enumerable
6566

6667
{{ReentrancyGuard}}
6768

69+
{{ReentrancyGuardTransient}}
70+
6871
{{Pausable}}
6972

7073
{{Nonces}}

contracts/utils/ReentrancyGuard.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ pragma solidity ^0.8.20;
1515
* those functions `private`, and then adding `external` `nonReentrant` entry
1616
* points to them.
1717
*
18+
* TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at,
19+
* consider using {ReentrancyGuardTransient} instead.
20+
*
1821
* TIP: If you would like to learn more about reentrancy and alternative ways
1922
* to protect against it, check out our blog post
2023
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {StorageSlot} from "./StorageSlot.sol";
6+
7+
/**
8+
* @dev Variant of {ReentrancyGuard} that uses transient storage.
9+
*
10+
* NOTE: This variant only works on networks where EIP-1153 is available.
11+
*/
12+
abstract contract ReentrancyGuardTransient {
13+
using StorageSlot for *;
14+
15+
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ReentrancyGuard")) - 1)) & ~bytes32(uint256(0xff))
16+
bytes32 private constant REENTRANCY_GUARD_STORAGE =
17+
0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00;
18+
19+
/**
20+
* @dev Unauthorized reentrant call.
21+
*/
22+
error ReentrancyGuardReentrantCall();
23+
24+
/**
25+
* @dev Prevents a contract from calling itself, directly or indirectly.
26+
* Calling a `nonReentrant` function from another `nonReentrant`
27+
* function is not supported. It is possible to prevent this from happening
28+
* by making the `nonReentrant` function external, and making it call a
29+
* `private` function that does the actual work.
30+
*/
31+
modifier nonReentrant() {
32+
_nonReentrantBefore();
33+
_;
34+
_nonReentrantAfter();
35+
}
36+
37+
function _nonReentrantBefore() private {
38+
// On the first call to nonReentrant, _status will be NOT_ENTERED
39+
if (_reentrancyGuardEntered()) {
40+
revert ReentrancyGuardReentrantCall();
41+
}
42+
43+
// Any calls to nonReentrant after this point will fail
44+
REENTRANCY_GUARD_STORAGE.asBoolean().tstore(true);
45+
}
46+
47+
function _nonReentrantAfter() private {
48+
REENTRANCY_GUARD_STORAGE.asBoolean().tstore(false);
49+
}
50+
51+
/**
52+
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
53+
* `nonReentrant` function in the call stack.
54+
*/
55+
function _reentrancyGuardEntered() internal view returns (bool) {
56+
return REENTRANCY_GUARD_STORAGE.asBoolean().tload();
57+
}
58+
}

test/utils/ReentrancyGuard.test.js

Lines changed: 45 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,49 @@ const { ethers } = require('hardhat');
22
const { expect } = require('chai');
33
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
44

5-
async function fixture() {
6-
const mock = await ethers.deployContract('ReentrancyMock');
7-
return { mock };
8-
}
9-
10-
describe('ReentrancyGuard', function () {
11-
beforeEach(async function () {
12-
Object.assign(this, await loadFixture(fixture));
13-
});
14-
15-
it('nonReentrant function can be called', async function () {
16-
expect(await this.mock.counter()).to.equal(0n);
17-
await this.mock.callback();
18-
expect(await this.mock.counter()).to.equal(1n);
19-
});
20-
21-
it('does not allow remote callback', async function () {
22-
const attacker = await ethers.deployContract('ReentrancyAttack');
23-
await expect(this.mock.countAndCall(attacker)).to.be.revertedWith('ReentrancyAttack: failed call');
24-
});
25-
26-
it('_reentrancyGuardEntered should be true when guarded', async function () {
27-
await this.mock.guardedCheckEntered();
5+
for (const variant of ['', 'Transient']) {
6+
describe(`Reentrancy${variant}Guard`, function () {
7+
async function fixture() {
8+
const name = `Reentrancy${variant}Mock`;
9+
const mock = await ethers.deployContract(name);
10+
return { name, mock };
11+
}
12+
13+
beforeEach(async function () {
14+
Object.assign(this, await loadFixture(fixture));
15+
});
16+
17+
it('nonReentrant function can be called', async function () {
18+
expect(await this.mock.counter()).to.equal(0n);
19+
await this.mock.callback();
20+
expect(await this.mock.counter()).to.equal(1n);
21+
});
22+
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');
26+
});
27+
28+
it('_reentrancyGuardEntered should be true when guarded', async function () {
29+
await this.mock.guardedCheckEntered();
30+
});
31+
32+
it('_reentrancyGuardEntered should be false when unguarded', async function () {
33+
await this.mock.unguardedCheckNotEntered();
34+
});
35+
36+
// The following are more side-effects than intended behavior:
37+
// I put them here as documentation, and to monitor any changes
38+
// in the side-effects.
39+
it('does not allow local recursion', async function () {
40+
await expect(this.mock.countLocalRecursive(10n)).to.be.revertedWithCustomError(
41+
this.mock,
42+
'ReentrancyGuardReentrantCall',
43+
);
44+
});
45+
46+
it('does not allow indirect local recursion', async function () {
47+
await expect(this.mock.countThisRecursive(10n)).to.be.revertedWith(`${this.name}: failed call`);
48+
});
2849
});
29-
30-
it('_reentrancyGuardEntered should be false when unguarded', async function () {
31-
await this.mock.unguardedCheckNotEntered();
32-
});
33-
34-
// The following are more side-effects than intended behavior:
35-
// I put them here as documentation, and to monitor any changes
36-
// in the side-effects.
37-
it('does not allow local recursion', async function () {
38-
await expect(this.mock.countLocalRecursive(10n)).to.be.revertedWithCustomError(
39-
this.mock,
40-
'ReentrancyGuardReentrantCall',
41-
);
42-
});
43-
44-
it('does not allow indirect local recursion', async function () {
45-
await expect(this.mock.countThisRecursive(10n)).to.be.revertedWith('ReentrancyMock: failed call');
46-
});
47-
});
50+
}

0 commit comments

Comments
 (0)