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
115 changes: 113 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 52 additions & 0 deletions contracts/security/AdaptiveReentrancyGuard.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @title AdaptiveReentrancyGuard
/// @notice Reentrancy guard that uses EIP-1153 transient storage (TSTORE/TLOAD)
/// when deployed in transient mode, falling back to SSTORE-based locking
/// for non-Cancun EVM targets.
/// @dev Mode is selected at construction. Transient mode costs ~100 gas per
/// lock/unlock vs ~5000 gas for storage-based guards.
abstract contract AdaptiveReentrancyGuard {
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;

uint256 private _status;

bool private immutable _transient;

error ReentrantCall();

constructor(bool useTransient) {
_transient = useTransient;
_status = _NOT_ENTERED;
}

modifier nonReentrant() {
if (_transient) {
assembly {
if tload(0) { revert(0, 0) }
tstore(0, 1)
}
_;
assembly { tstore(0, 0) }
} else {
if (_status == _ENTERED) revert ReentrantCall();
_status = _ENTERED;
_;
_status = _NOT_ENTERED;
}
}
}

/// @title AdaptiveReentrancyGuardMock
/// @dev Test-only mock exposing nonReentrant for both modes.
contract AdaptiveReentrancyGuardMock is AdaptiveReentrancyGuard {
constructor(bool useTransient) AdaptiveReentrancyGuard(useTransient) {}

function enter() external nonReentrant {}

function reenter() external nonReentrant {
this.reenter();
}
}
84 changes: 84 additions & 0 deletions test/security/AdaptiveReentrancyGuard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { expect } from "chai";
import { ethers } from "hardhat";
import { Contract, ContractFactory } from "ethers";

describe("AdaptiveReentrancyGuard", () => {
describe("transient mode", () => {
let mock: Contract;

before(async () => {
const factory: ContractFactory = await ethers.getContractFactory(
"AdaptiveReentrancyGuardMock"
);
mock = await factory.deploy(true);
await mock.waitForDeployment();
});

it("should allow first entry", async () => {
await expect(mock.enter()).to.not.be.reverted;
});

it("should reject reentrant call", async () => {
await expect(mock.reenter()).to.be.reverted;
});

it("should allow re-entry after completion", async () => {
await mock.enter();
await expect(mock.enter()).to.not.be.reverted;
});
});

describe("storage mode", () => {
let mock: Contract;

before(async () => {
const factory: ContractFactory = await ethers.getContractFactory(
"AdaptiveReentrancyGuardMock"
);
mock = await factory.deploy(false);
await mock.waitForDeployment();
});

it("should allow first entry", async () => {
await expect(mock.enter()).to.not.be.reverted;
});

it("should reject reentrant call with custom error", async () => {
await expect(mock.reenter()).to.be.revertedWithCustomError(
mock,
"ReentrantCall"
);
});

it("should allow re-entry after completion", async () => {
await mock.enter();
await expect(mock.enter()).to.not.be.reverted;
});
});

describe("gas comparison", () => {
it("should be more gas efficient in transient mode than storage mode", async () => {
const transientFactory: ContractFactory = await ethers.getContractFactory(
"AdaptiveReentrancyGuardMock"
);
const transientMock: Contract = await transientFactory.deploy(true);
await transientMock.waitForDeployment();

const storageFactory: ContractFactory = await ethers.getContractFactory(
"AdaptiveReentrancyGuardMock"
);
const storageMock: Contract = await storageFactory.deploy(false);
await storageMock.waitForDeployment();

const txTransient = await transientMock.enter();
const receiptTransient = await txTransient.wait();
const gasTransient = receiptTransient!.gasUsed;

const txStorage = await storageMock.enter();
const receiptStorage = await txStorage.wait();
const gasStorage = receiptStorage!.gasUsed;

expect(gasTransient).to.be.lessThan(gasStorage);
});
});
});
Loading