Skip to content

Commit 0775c80

Browse files
lock by address (Uniswap#181)
* Revert "Revert "rough first pass"" This reverts commit 91efa60. * fix snaps * clean up impl * improve reentrancy tests * add comment * snaps * snaps * cache length * beef up tests a bit * make nonzeroDeltaCount global (Uniswap#241) * make nonzerodeltacount global * clean up storage * explicitly pack Owned storage make _accountDelta more efficient * clean up visibility/getters * name change * address comment * improve storage efficiency * update storage order * snaps * remove incorrect comment * nits * snaps * address comments * snaps * remove parentLockIndex
1 parent bb9bad0 commit 0775c80

31 files changed

+393
-281
lines changed
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
131226
1+
93675
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
185905
1+
150995
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
61036
1+
58509

.forge-snapshots/initialize.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
37613
1+
37658
+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
320400
1+
298837
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
294413
1+
272850

.forge-snapshots/mint.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
313077
1+
291514
+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1151
1+
1151
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2785
1+
2785

.forge-snapshots/simple swap.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
67705
1+
47411
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
161567
1+
122677
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
146274
1+
107384

.forge-snapshots/swap with hooks.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
67680
1+
47386
+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
67705
1+
47411

contracts/PoolManager.sol

+25-41
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {Pool} from "./libraries/Pool.sol";
66
import {SafeCast} from "./libraries/SafeCast.sol";
77
import {Position} from "./libraries/Position.sol";
88
import {Currency, CurrencyLibrary} from "./libraries/CurrencyLibrary.sol";
9+
import {LockDataLibrary} from "./libraries/LockDataLibrary.sol";
910

1011
import {NoDelegateCall} from "./NoDelegateCall.sol";
1112
import {Owned} from "./Owned.sol";
@@ -29,6 +30,7 @@ contract PoolManager is IPoolManager, Owned, NoDelegateCall, ERC1155, IERC1155Re
2930
using Hooks for IHooks;
3031
using Position for mapping(bytes32 => Position.Info);
3132
using CurrencyLibrary for Currency;
33+
using LockDataLibrary for IPoolManager.LockData;
3234
using Fees for uint24;
3335

3436
/// @inheritdoc IPoolManager
@@ -45,18 +47,11 @@ contract PoolManager is IPoolManager, Owned, NoDelegateCall, ERC1155, IERC1155Re
4547
IProtocolFeeController public protocolFeeController;
4648

4749
/// @inheritdoc IPoolManager
48-
address[] public override lockedBy;
50+
IPoolManager.LockData public override lockData;
4951

50-
/// @member nonzeroDeltaCount The number of entries in the currencyDelta mapping that have a non-zero value
51-
/// @member currencyDelta The amount owed to the locker (positive) or owed to the pool (negative) of the currency
52-
struct LockState {
53-
uint256 nonzeroDeltaCount;
54-
mapping(Currency currency => int256) currencyDelta;
55-
}
56-
57-
/// @dev Represents the state of the locker at the given index. Each locker must have net 0 currencies owed before
58-
/// releasing their lock. Note this is private because the nested mappings cannot be exposed as a public variable.
59-
mapping(uint256 index => LockState) private lockStates;
52+
/// @dev Represents the currencies due/owed to each locker.
53+
/// Must all net to zero when the last lock is released.
54+
mapping(address locker => mapping(Currency currency => int256 currencyDelta)) public currencyDelta;
6055

6156
/// @inheritdoc IPoolManager
6257
mapping(Currency currency => uint256) public override reservesOf;
@@ -126,6 +121,11 @@ contract PoolManager is IPoolManager, Owned, NoDelegateCall, ERC1155, IERC1155Re
126121
return pools[id].positions.get(owner, tickLower, tickUpper);
127122
}
128123

124+
/// @inheritdoc IPoolManager
125+
function getLock(uint256 i) external view override returns (address locker) {
126+
return LockDataLibrary.getLock(i);
127+
}
128+
129129
/// @inheritdoc IPoolManager
130130
function initialize(PoolKey memory key, uint160 sqrtPriceX96) external override returns (int24 tick) {
131131
if (key.fee & Fees.STATIC_FEE_MASK >= 1000000) revert FeeTooLarge();
@@ -155,53 +155,37 @@ contract PoolManager is IPoolManager, Owned, NoDelegateCall, ERC1155, IERC1155Re
155155
emit Initialize(id, key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks);
156156
}
157157

158-
/// @inheritdoc IPoolManager
159-
function lockedByLength() external view returns (uint256) {
160-
return lockedBy.length;
161-
}
162-
163-
/// @inheritdoc IPoolManager
164-
function getNonzeroDeltaCount(uint256 id) external view returns (uint256) {
165-
return lockStates[id].nonzeroDeltaCount;
166-
}
167-
168-
/// @inheritdoc IPoolManager
169-
function getCurrencyDelta(uint256 id, Currency currency) external view returns (int256) {
170-
return lockStates[id].currencyDelta[currency];
171-
}
172-
173158
/// @inheritdoc IPoolManager
174159
function lock(bytes calldata data) external override returns (bytes memory result) {
175-
uint256 id = lockedBy.length;
176-
lockedBy.push(msg.sender);
160+
lockData.push(msg.sender);
177161

178162
// the caller does everything in this callback, including paying what they owe via calls to settle
179-
result = ILockCallback(msg.sender).lockAcquired(id, data);
163+
result = ILockCallback(msg.sender).lockAcquired(data);
180164

181-
unchecked {
182-
LockState storage lockState = lockStates[id];
183-
if (lockState.nonzeroDeltaCount != 0) revert CurrencyNotSettled();
165+
if (lockData.length == 1) {
166+
if (lockData.nonzeroDeltaCount != 0) revert CurrencyNotSettled();
167+
delete lockData;
168+
} else {
169+
lockData.pop();
184170
}
185-
186-
lockedBy.pop();
187171
}
188172

189173
function _accountDelta(Currency currency, int128 delta) internal {
190174
if (delta == 0) return;
191175

192-
LockState storage lockState = lockStates[lockedBy.length - 1];
193-
int256 current = lockState.currencyDelta[currency];
194-
176+
address locker = lockData.getActiveLock();
177+
int256 current = currencyDelta[locker][currency];
195178
int256 next = current + delta;
179+
196180
unchecked {
197181
if (next == 0) {
198-
lockState.nonzeroDeltaCount--;
182+
lockData.nonzeroDeltaCount--;
199183
} else if (current == 0) {
200-
lockState.nonzeroDeltaCount++;
184+
lockData.nonzeroDeltaCount++;
201185
}
202186
}
203187

204-
lockState.currencyDelta[currency] = next;
188+
currencyDelta[locker][currency] = next;
205189
}
206190

207191
/// @dev Accumulates a balance change to a map of currency to balance changes
@@ -211,7 +195,7 @@ contract PoolManager is IPoolManager, Owned, NoDelegateCall, ERC1155, IERC1155Re
211195
}
212196

213197
modifier onlyByLocker() {
214-
address locker = lockedBy[lockedBy.length - 1];
198+
address locker = lockData.getActiveLock();
215199
if (msg.sender != locker) revert LockedBy(locker);
216200
_;
217201
}

contracts/interfaces/IPoolManager.sol

+16-13
Original file line numberDiff line numberDiff line change
@@ -142,24 +142,27 @@ interface IPoolManager is IERC1155 {
142142
/// @notice Returns the reserves for a given ERC20 currency
143143
function reservesOf(Currency currency) external view returns (uint256);
144144

145-
/// @notice Initialize the state for a given pool ID
146-
function initialize(PoolKey memory key, uint160 sqrtPriceX96) external returns (int24 tick);
145+
/// @notice Contains data about pool lockers.
146+
struct LockData {
147+
/// @notice The current number of active lockers
148+
uint128 length;
149+
/// @notice The total number of nonzero deltas over all active + completed lockers
150+
uint128 nonzeroDeltaCount;
151+
}
147152

148-
/// @notice Represents the stack of addresses that have locked the pool. Each call to #lock pushes the address onto the stack
149-
/// @param index The index of the locker, also known as the id of the locker
150-
function lockedBy(uint256 index) external view returns (address);
153+
/// @notice Returns the locker in the ith position of the locker queue.
154+
function getLock(uint256 i) external view returns (address locker);
151155

152-
/// @notice Getter for the length of the lockedBy array
153-
function lockedByLength() external view returns (uint256);
156+
/// @notice Returns lock data
157+
function lockData() external view returns (uint128 length, uint128 nonzeroDeltaCount);
154158

155-
/// @notice Returns the count of nonzero deltas for the given locker ID
156-
/// @param id The ID of the locker
157-
function getNonzeroDeltaCount(uint256 id) external view returns (uint256);
159+
/// @notice Initialize the state for a given pool ID
160+
function initialize(PoolKey memory key, uint160 sqrtPriceX96) external returns (int24 tick);
158161

159-
/// @notice Get the current delta for a given currency, and its position in the currencies touched array
160-
/// @param id The ID of the locker
162+
/// @notice Get the current delta for a locker in the given currency
163+
/// @param locker The address of the locker
161164
/// @param currency The currency for which to lookup the delta
162-
function getCurrencyDelta(uint256 id, Currency currency) external view returns (int256);
165+
function currencyDelta(address locker, Currency currency) external view returns (int256);
163166

164167
/// @notice All operations go through this function
165168
/// @param data Any data to pass to the callback, via `ILockCallback(msg.sender).lockCallback(data)`

contracts/interfaces/callback/ILockCallback.sol

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ pragma solidity ^0.8.20;
33

44
interface ILockCallback {
55
/// @notice Called by the pool manager on `msg.sender` when a lock is acquired
6-
/// @param id The id of the lock that was acquired
76
/// @param data The data that was passed to the call to lock
87
/// @return Any data that you want to be returned from the lock call
9-
function lockAcquired(uint256 id, bytes calldata data) external returns (bytes memory);
8+
function lockAcquired(bytes calldata data) external returns (bytes memory);
109
}
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.20;
3+
4+
import {IPoolManager} from "../interfaces/IPoolManager.sol";
5+
6+
/// @dev This library manages a custom storage implementation for a queue
7+
/// that tracks current lockers. The "sentinel" storage slot for this data structure,
8+
/// always passed in as IPoolManager.LockData storage self, stores not just the current
9+
/// length of the queue but also the global count of non-zero deltas across all lockers.
10+
/// The values of the data structure start at OFFSET, and each value is a locker address.
11+
library LockDataLibrary {
12+
uint256 private constant OFFSET = uint256(keccak256("LockData"));
13+
14+
/// @dev Pushes a locker onto the end of the queue, and updates the sentinel storage slot.
15+
function push(IPoolManager.LockData storage self, address locker) internal {
16+
// read current value from the sentinel storage slot
17+
uint128 length = self.length;
18+
unchecked {
19+
uint256 indexToWrite = OFFSET + length; // not in assembly because OFFSET is in the library scope
20+
/// @solidity memory-safe-assembly
21+
assembly {
22+
// in the next storage slot, write the locker
23+
sstore(indexToWrite, locker)
24+
}
25+
// update the sentinel storage slot
26+
self.length = length + 1;
27+
}
28+
}
29+
30+
/// @dev Pops a locker off the end of the queue. Note that no storage gets cleared.
31+
function pop(IPoolManager.LockData storage self) internal {
32+
unchecked {
33+
self.length--;
34+
}
35+
}
36+
37+
function getLock(uint256 i) internal view returns (address locker) {
38+
unchecked {
39+
uint256 position = OFFSET + i; // not in assembly because OFFSET is in the library scope
40+
/// @solidity memory-safe-assembly
41+
assembly {
42+
locker := sload(position)
43+
}
44+
}
45+
}
46+
47+
function getActiveLock(IPoolManager.LockData storage self) internal view returns (address locker) {
48+
return getLock(self.length - 1);
49+
}
50+
}

contracts/test/PoolDonateTest.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ contract PoolDonateTest is ILockCallback {
3838
}
3939
}
4040

41-
function lockAcquired(uint256, bytes calldata rawData) external returns (bytes memory) {
41+
function lockAcquired(bytes calldata rawData) external returns (bytes memory) {
4242
require(msg.sender == address(manager));
4343

4444
CallbackData memory data = abi.decode(rawData, (CallbackData));

contracts/test/PoolLockTest.sol

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {IPoolManager} from "../interfaces/IPoolManager.sol";
55
import {ILockCallback} from "../interfaces/callback/ILockCallback.sol";
66

77
contract PoolLockTest is ILockCallback {
8-
event LockAcquired(uint256 id);
8+
event LockAcquired();
99

1010
IPoolManager manager;
1111

@@ -18,8 +18,8 @@ contract PoolLockTest is ILockCallback {
1818
}
1919

2020
/// @notice Called by the pool manager on `msg.sender` when a lock is acquired
21-
function lockAcquired(uint256 id, bytes calldata) external override returns (bytes memory) {
22-
emit LockAcquired(id);
21+
function lockAcquired(bytes calldata) external override returns (bytes memory) {
22+
emit LockAcquired();
2323
return "";
2424
}
2525
}

contracts/test/PoolManagerReentrancyTest.sol

-56
This file was deleted.

contracts/test/PoolModifyPositionTest.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ contract PoolModifyPositionTest is ILockCallback {
3636
}
3737
}
3838

39-
function lockAcquired(uint256, bytes calldata rawData) external returns (bytes memory) {
39+
function lockAcquired(bytes calldata rawData) external returns (bytes memory) {
4040
require(msg.sender == address(manager));
4141

4242
CallbackData memory data = abi.decode(rawData, (CallbackData));

0 commit comments

Comments
 (0)