Skip to content

Commit 2a2a925

Browse files
author
TSxo
committed
feat: Add Mutex mixin
1 parent dd65d2c commit 2a2a925

6 files changed

Lines changed: 298 additions & 19 deletions

File tree

.github/workflows/test.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ env:
1010

1111
jobs:
1212
check:
13-
strategy:
14-
fail-fast: true
15-
1613
name: Foundry project
1714
runs-on: ubuntu-latest
1815
steps:

README.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,24 @@ A suite of opinionated, optimized smart contract modules.
77
The smart contracts are located in the `src` directory.
88

99
```ml
10-
auth
11-
├── IOwned.sol
12-
├── Owned.sol
13-
└── managed
14-
├── AuthManaged.sol
15-
├── AuthManager.sol
16-
├── IAuthManaged.sol
17-
├── IAuthManager.sol
18-
└── IAuthority.sol
10+
.
11+
├── auth
12+
│   ├── IOwned.sol
13+
│   ├── Owned.sol
14+
│   └── managed
15+
│   ├── AuthManaged.sol
16+
│   ├── AuthManager.sol
17+
│   ├── IAuthManaged.sol
18+
│   ├── IAuthManager.sol
19+
│   └── IAuthority.sol
20+
├── mixins
21+
│   └── Mutex.sol
1922
```
2023

2124
## Work in Progress
2225

23-
This project is currently under active development. New modules and features will
24-
be added to enhance functionality and performance. While the core auth modules are
25-
available for testing and experimentation, the complete suite will be published
26-
once enough modules are integrated and thoroughly vetted.
26+
This project is currently under development. New modules and features will
27+
be added to enhance functionality and performance.
2728

2829
Contributions and suggestions are welcomed.
2930

src/auth/managed/AuthManager.sol

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ import { IAuthority } from "./IAuthority.sol";
1010
///
1111
/// @author TSxo
1212
///
13-
/// @notice A role-based access control system for managing permissions on smart
14-
/// contracts.
15-
///
1613
/// @dev This contract provides an efficient and flexible solution for managing
1714
/// the permissions of a system.
1815
///

src/mixins/Mutex.sol

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
/// @title Mutex
6+
///
7+
/// @author TSxo
8+
///
9+
/// @dev Provides mechanisms to guard against reentrancy attacks.
10+
///
11+
/// # Guards
12+
///
13+
/// Exposes two modifiers: `lock` for write operations and `whenUnlocked` for
14+
/// reads.
15+
///
16+
/// The `lock` modifier acquires a lock before function execution and releases
17+
/// it afterward. While locked, no other function using `lock` or `whenUnlocked`
18+
/// can be called.
19+
///
20+
/// The `whenUnlocked` modifier ensures no lock is held during calls to view
21+
/// functions, preventing read-level reentrancy.
22+
///
23+
/// Functions guarded by these modifiers cannot directly call each other. To
24+
/// navigate this issue, extract reusable logic into modifier-free internal or
25+
/// private functions and compose calls to them from a single guarded entry point.
26+
///
27+
/// For the rationale behind `1` and `2` representing locked states, see:
28+
/// - https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/ReentrancyGuard.sol
29+
/// - https://eips.ethereum.org/EIPS/eip-2200
30+
///
31+
/// # Upgrade Compatible
32+
///
33+
/// This contract utilizes ERC-7201 namespaced storage, making it compatible with
34+
/// upgradeable contract architectures. Note, it does not enforce any restrictions
35+
/// against reinitialization, leaving such protection mechanisms up to the
36+
/// inheriting contract.
37+
///
38+
/// # Disclaimer
39+
///
40+
/// This contract prioritizes an opinionated balance between optimization and
41+
/// readability. **It was not designed with user safety in mind** and contains
42+
/// minimal safety checks. It is experimental software and is provided **as-is**,
43+
/// without any warranties or guarantees of functionality, security, or fitness
44+
/// for any particular purpose.
45+
///
46+
/// There are implicit invariants this contract expects to hold. Users and
47+
/// developers integrating this contract **do so at their own risk** and are
48+
/// responsible for thoroughly reviewing the code before use.
49+
///
50+
/// The author assumes **no liability** for any loss, damage, or unintended
51+
/// behavior resulting from the use, deployment, or interaction with this contract.
52+
///
53+
/// # Acknowledgements
54+
///
55+
/// Heavy inspiration is taken from:
56+
/// - Open Zeppelin;
57+
/// - Solmate; and
58+
/// - Solady.
59+
///
60+
/// Thank you.
61+
abstract contract Mutex {
62+
// -------------------------------------------------------------------------
63+
// Type Declarations
64+
65+
/// @custom:storage-location erc7201:libsol.storage.Mutex
66+
struct MutexStorage {
67+
uint256 locked;
68+
}
69+
70+
// -------------------------------------------------------------------------
71+
// State
72+
73+
/// @dev keccak256(abi.encode(uint256(keccak256("libsol.storage.Mutex")) - 1)) & ~bytes32(uint256(0xff))
74+
bytes32 private constant STORAGE = 0x4772547a0c85096864cfb8fb79e76bca1fc87ca09848b27c5507e4697fcfd100;
75+
76+
/// @dev Represents the unlocked state.
77+
uint256 private constant UNLOCKED = 1;
78+
79+
/// @dev Represents the locked state.
80+
uint256 private constant LOCKED = 2;
81+
82+
// -------------------------------------------------------------------------
83+
// Errors
84+
85+
/// @notice Raised when entering a guarded function after a lock has been
86+
/// acquired.
87+
error Mutex__Locked();
88+
89+
// -------------------------------------------------------------------------
90+
// Modifiers
91+
92+
/// @dev Guards against reentrancy attacks by acquiring a lock before function
93+
/// execution and releasing it afterward.
94+
modifier lock() virtual {
95+
_acquireLock();
96+
_;
97+
_releaseLock();
98+
}
99+
100+
/// @dev Guards against read-only reentrancy attacks by ensuring no write
101+
/// lock is held.
102+
///
103+
/// Important: Does not acquire a lock. See the `lock()` modifier.
104+
modifier whenUnlocked() virtual {
105+
_assertUnlocked();
106+
_;
107+
}
108+
109+
// -------------------------------------------------------------------------
110+
// Functions - Init
111+
112+
/// @notice Initializes the contract by setting the locked status to `UNLOCKED`.
113+
function _initializeMutex() internal virtual {
114+
assembly ("memory-safe") {
115+
sstore(STORAGE, UNLOCKED)
116+
}
117+
}
118+
119+
// -------------------------------------------------------------------------
120+
// Functions - Internal
121+
122+
/// @notice Acquires the lock. Once acquired, no reentrant calls can be made
123+
/// to functions that use the `lock` or `whenUnlocked` modifiers.
124+
function _acquireLock() internal virtual {
125+
_assertUnlocked();
126+
127+
assembly ("memory-safe") {
128+
sstore(STORAGE, LOCKED)
129+
}
130+
}
131+
132+
/// @notice Releases the lock.
133+
function _releaseLock() internal virtual {
134+
assembly ("memory-safe") {
135+
sstore(STORAGE, UNLOCKED)
136+
}
137+
}
138+
139+
/// @notice Returns whether the lock is currently acquired.
140+
///
141+
/// @return result True if the lock is acquired, false otherwise.
142+
function _isLocked() internal view virtual returns (bool result) {
143+
assembly ("memory-safe") {
144+
result := eq(sload(STORAGE), LOCKED)
145+
}
146+
}
147+
148+
/// @notice Asserts that the lock has not been acquired.
149+
function _assertUnlocked() internal view virtual {
150+
assembly ("memory-safe") {
151+
if eq(sload(STORAGE), LOCKED) {
152+
mstore(0x00, 0x02c73c37) // `Mutex__Locked()`
153+
revert(0x1c, 0x04)
154+
}
155+
}
156+
}
157+
}

src/mocks/mixins/MutexImpl.sol

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import { Mutex } from "@tsxo/libsol/mixins/Mutex.sol";
6+
7+
contract MutexImpl is Mutex {
8+
uint256 private _count;
9+
10+
constructor() {
11+
_initializeMutex();
12+
}
13+
14+
function increment() public lock {
15+
_count++;
16+
}
17+
18+
function count() public view whenUnlocked returns (uint256) {
19+
return _count;
20+
}
21+
22+
function unguardedToGuarded() external {
23+
increment();
24+
}
25+
26+
function guardedToGuarded() external lock {
27+
increment();
28+
}
29+
30+
function unguardedToGuardedRead() external view {
31+
count();
32+
}
33+
34+
function guardedToGuardedRead() external lock {
35+
count();
36+
}
37+
38+
function unguardedToExternal(MutexAttack attacker) external {
39+
attacker.execute(this.increment.selector);
40+
}
41+
42+
function guardedToExternal(MutexAttack attacker) external lock {
43+
attacker.execute(this.increment.selector);
44+
}
45+
46+
function isLocked() external view returns (bool) {
47+
return _isLocked();
48+
}
49+
}
50+
51+
contract MutexAttack {
52+
error Failed();
53+
54+
function execute(bytes4 selector) external {
55+
(bool success,) = msg.sender.call(abi.encodeWithSelector(selector));
56+
57+
if (!success) revert Failed();
58+
}
59+
}

test/mixins/Mutex.t.sol

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
pragma solidity 0.8.20;
2+
3+
import { Test } from "forge-std/Test.sol";
4+
import { Mutex } from "@tsxo/libsol/mixins/Mutex.sol";
5+
import { MutexImpl, MutexAttack } from "@tsxo/libsol/mocks/mixins/MutexImpl.sol";
6+
7+
contract MutextTest is Test {
8+
// -------------------------------------------------------------------------
9+
// State
10+
11+
MutexImpl mutex;
12+
MutexAttack attacker;
13+
14+
// -------------------------------------------------------------------------
15+
// Set Up
16+
17+
function setUp() public {
18+
mutex = new MutexImpl();
19+
attacker = new MutexAttack();
20+
}
21+
22+
// -------------------------------------------------------------------------
23+
// Test - Initialization
24+
25+
function test_IsUnlockedByDefault() public view {
26+
assertFalse(mutex.isLocked());
27+
}
28+
29+
// -------------------------------------------------------------------------
30+
// Test - Guards
31+
32+
function test_UnguardedCanCallGuarded() public {
33+
assertEq(mutex.count(), 0);
34+
mutex.unguardedToGuarded();
35+
assertEq(mutex.count(), 1);
36+
}
37+
38+
function test_GuardedCannotCallGuarded() public {
39+
bytes4 err = Mutex.Mutex__Locked.selector;
40+
vm.expectRevert(err);
41+
42+
mutex.guardedToGuarded();
43+
}
44+
45+
function test_UnguardedCanCallGuardedRead() public view {
46+
mutex.unguardedToGuardedRead();
47+
}
48+
49+
function test_GuardedCannotCallGuardedRead() public {
50+
bytes4 err = Mutex.Mutex__Locked.selector;
51+
vm.expectRevert(err);
52+
53+
mutex.guardedToGuardedRead();
54+
}
55+
56+
function test_UnguardedExternalCallCanReenter() public {
57+
assertEq(mutex.count(), 0);
58+
mutex.unguardedToExternal(attacker);
59+
assertEq(mutex.count(), 1);
60+
}
61+
62+
function test_GuardedExternalCallPreventsReentrancy() public {
63+
bytes4 err = MutexAttack.Failed.selector;
64+
vm.expectRevert(err);
65+
66+
mutex.guardedToExternal(attacker);
67+
}
68+
}

0 commit comments

Comments
 (0)