diff --git a/Cargo.lock b/Cargo.lock index 85112f3..19e6286 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -322,6 +322,19 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -548,6 +561,19 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + [[package]] name = "diff" version = "0.1.13" @@ -680,6 +706,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "equivalent" version = "1.0.2" @@ -708,6 +740,12 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "ff" version = "0.13.1" @@ -831,6 +869,12 @@ dependencies = [ name = "gasguard-cli" version = "0.1.0" dependencies = [ + "anyhow", + "colored", + "dialoguer", + "gasguard-auto-fix", + "gasguard-engine", + "similar", "walkdir", ] @@ -945,6 +989,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + [[package]] name = "gimli" version = "0.32.3" @@ -1158,6 +1213,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "lock_api" version = "0.4.14" @@ -1448,6 +1509,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.7" @@ -1475,7 +1542,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", ] [[package]] @@ -1602,6 +1669,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1786,6 +1866,12 @@ dependencies = [ "keccak", ] +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "1.3.0" @@ -1821,6 +1907,12 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "slab" version = "0.4.11" @@ -1896,7 +1988,7 @@ dependencies = [ "ed25519-dalek 3.0.0", "elliptic-curve", "generic-array", - "getrandom", + "getrandom 0.2.17", "hex-literal", "hmac", "k256", @@ -2112,6 +2204,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "termtree" version = "0.5.1" @@ -2260,6 +2365,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "upgradeable-example" version = "0.1.0" diff --git a/contracts/security/AdaptiveReentrancyGuard.sol b/contracts/security/AdaptiveReentrancyGuard.sol new file mode 100644 index 0000000..9970299 --- /dev/null +++ b/contracts/security/AdaptiveReentrancyGuard.sol @@ -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(); + } +} diff --git a/test/security/AdaptiveReentrancyGuard.test.ts b/test/security/AdaptiveReentrancyGuard.test.ts new file mode 100644 index 0000000..85478f9 --- /dev/null +++ b/test/security/AdaptiveReentrancyGuard.test.ts @@ -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); + }); + }); +});