From 469f8562ac5ed74de0bb4db43479e50f1d04e484 Mon Sep 17 00:00:00 2001 From: Sara Reynolds <30504811+snreynolds@users.noreply.github.com> Date: Thu, 8 Aug 2024 11:44:48 -0400 Subject: [PATCH] move sub unsub (#287) * move sub unsub * use struct * pass in bytes to setConfigId * ... --- .../PositionManager_mint_native.snap | 2 +- ...anager_mint_nativeWithSweep_withClose.snap | 2 +- ...r_mint_nativeWithSweep_withSettlePair.snap | 2 +- .../PositionManager_mint_onSameTickLower.snap | 2 +- .../PositionManager_mint_onSameTickUpper.snap | 2 +- .../PositionManager_mint_sameRange.snap | 2 +- ...nManager_mint_settleWithBalance_sweep.snap | 2 +- ...anager_mint_warmedPool_differentRange.snap | 2 +- .../PositionManager_mint_withClose.snap | 2 +- .../PositionManager_mint_withSettlePair.snap | 2 +- ...tionManager_multicall_initialize_mint.snap | 2 +- src/PositionManager.sol | 54 +++------ src/base/Notifier.sol | 32 ++++- src/interfaces/INotifier.sol | 1 + src/libraries/PositionConfig.sol | 43 +------ src/libraries/PositionConfigId.sol | 39 +++++++ test/libraries/PositionConfig.t.sol | 109 ++++++++++++------ 17 files changed, 171 insertions(+), 129 deletions(-) create mode 100644 src/libraries/PositionConfigId.sol diff --git a/.forge-snapshots/PositionManager_mint_native.snap b/.forge-snapshots/PositionManager_mint_native.snap index ec5fa61b..e60f75f4 100644 --- a/.forge-snapshots/PositionManager_mint_native.snap +++ b/.forge-snapshots/PositionManager_mint_native.snap @@ -1 +1 @@ -341062 \ No newline at end of file +341067 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap b/.forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap index 0eb07673..b0e3dbe4 100644 --- a/.forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap +++ b/.forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap @@ -1 +1 @@ -349554 \ No newline at end of file +349559 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap b/.forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap index 7cc33796..3d917e21 100644 --- a/.forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap +++ b/.forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap @@ -1 +1 @@ -348856 \ No newline at end of file +348861 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap index dd01e09b..6bcd8770 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap @@ -1 +1 @@ -319044 \ No newline at end of file +319049 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap index ecb6919a..589f7a63 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap @@ -1 +1 @@ -319686 \ No newline at end of file +319691 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_sameRange.snap b/.forge-snapshots/PositionManager_mint_sameRange.snap index 4b788a7b..c1563f13 100644 --- a/.forge-snapshots/PositionManager_mint_sameRange.snap +++ b/.forge-snapshots/PositionManager_mint_sameRange.snap @@ -1 +1 @@ -245268 \ No newline at end of file +245273 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap b/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap index 04ed7a3f..66c788d9 100644 --- a/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap +++ b/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap @@ -1 +1 @@ -375086 \ No newline at end of file +375091 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap index 8ab21745..13864905 100644 --- a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap +++ b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap @@ -1 +1 @@ -325062 \ No newline at end of file +325067 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_withClose.snap b/.forge-snapshots/PositionManager_mint_withClose.snap index 2c29315b..f5a59c25 100644 --- a/.forge-snapshots/PositionManager_mint_withClose.snap +++ b/.forge-snapshots/PositionManager_mint_withClose.snap @@ -1 +1 @@ -376362 \ No newline at end of file +376367 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_withSettlePair.snap b/.forge-snapshots/PositionManager_mint_withSettlePair.snap index a151e59d..1742c9ea 100644 --- a/.forge-snapshots/PositionManager_mint_withSettlePair.snap +++ b/.forge-snapshots/PositionManager_mint_withSettlePair.snap @@ -1 +1 @@ -375502 \ No newline at end of file +375507 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap index ddaa376d..869a05f5 100644 --- a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap +++ b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap @@ -1 +1 @@ -420836 \ No newline at end of file +420841 \ No newline at end of file diff --git a/src/PositionManager.sol b/src/PositionManager.sol index d56f30aa..2f4c9bd9 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -10,7 +10,6 @@ import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; -import {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; import {SafeTransferLib} from "solmate/src/utils/SafeTransferLib.sol"; import {ERC20} from "solmate/src/tokens/ERC20.sol"; import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; @@ -29,6 +28,7 @@ import {CalldataDecoder} from "./libraries/CalldataDecoder.sol"; import {INotifier} from "./interfaces/INotifier.sol"; import {Permit2Forwarder} from "./base/Permit2Forwarder.sol"; import {SlippageCheckLibrary} from "./libraries/SlippageCheck.sol"; +import {PositionConfigId, PositionConfigIdLibrary} from "./libraries/PositionConfigId.sol"; contract PositionManager is IPositionManager, @@ -44,18 +44,24 @@ contract PositionManager is using SafeTransferLib for *; using CurrencyLibrary for Currency; using PoolIdLibrary for PoolKey; - using PositionConfigLibrary for *; + using PositionConfigLibrary for PositionConfig; using StateLibrary for IPoolManager; using TransientStateLibrary for IPoolManager; using SafeCast for uint256; using SafeCast for int256; using CalldataDecoder for bytes; using SlippageCheckLibrary for BalanceDelta; + using PositionConfigIdLibrary for PositionConfigId; /// @dev The ID of the next token that will be minted. Skips 0 uint256 public nextTokenId = 1; - mapping(uint256 tokenId => bytes32 config) private positionConfigs; + mapping(uint256 tokenId => PositionConfigId configId) internal positionConfigs; + + /// @notice an internal getter for PositionConfigId to be used by Notifier + function _positionConfigs(uint256 tokenId) internal view override returns (PositionConfigId storage) { + return positionConfigs[tokenId]; + } constructor(IPoolManager _poolManager, IAllowanceTransfer _permit2) BaseActionsRouter(_poolManager) @@ -75,7 +81,7 @@ contract PositionManager is /// @param tokenId the unique identifier of the ERC721 token /// @dev either msg.sender or _msgSender() is passed in as the caller /// _msgSender() should ONLY be used if this is being called from within the unlockCallback - modifier onlyIfApproved(address caller, uint256 tokenId) { + modifier onlyIfApproved(address caller, uint256 tokenId) override { if (!_isApprovedOrOwner(caller, tokenId)) revert NotApproved(caller); _; } @@ -83,8 +89,8 @@ contract PositionManager is /// @notice Reverts if the hash of the config does not equal the saved hash /// @param tokenId the unique identifier of the ERC721 token /// @param config the PositionConfig to check against - modifier onlyValidConfig(uint256 tokenId, PositionConfig calldata config) { - if (positionConfigs.getConfigId(tokenId) != config.toId()) revert IncorrectPositionConfigForTokenId(tokenId); + modifier onlyValidConfig(uint256 tokenId, PositionConfig calldata config) override { + if (positionConfigs[tokenId].getConfigId() != config.toId()) revert IncorrectPositionConfigForTokenId(tokenId); _; } @@ -107,29 +113,6 @@ contract PositionManager is _executeActionsWithoutUnlock(actions, params); } - /// @inheritdoc INotifier - function subscribe(uint256 tokenId, PositionConfig calldata config, address subscriber, bytes calldata data) - external - payable - onlyIfApproved(msg.sender, tokenId) - onlyValidConfig(tokenId, config) - { - // call to _subscribe will revert if the user already has a sub - positionConfigs.setSubscribe(tokenId); - _subscribe(tokenId, config, subscriber, data); - } - - /// @inheritdoc INotifier - function unsubscribe(uint256 tokenId, PositionConfig calldata config, bytes calldata data) - external - payable - onlyIfApproved(msg.sender, tokenId) - onlyValidConfig(tokenId, config) - { - positionConfigs.setUnsubscribe(tokenId); - _unsubscribe(tokenId, config, data); - } - function msgSender() public view override returns (address) { return _getLocker(); } @@ -260,7 +243,7 @@ contract PositionManager is _modifyLiquidity(config, liquidity.toInt256(), bytes32(tokenId), hookData); // Slippage checks should be done on the principal liquidityDelta which is the liquidityDelta - feesAccrued (liquidityDelta - feesAccrued).validateMaxIn(amount0Max, amount1Max); - positionConfigs.setConfigId(tokenId, config); + positionConfigs[tokenId].setConfigId(config.toId()); emit MintPosition(tokenId, config); } @@ -351,7 +334,7 @@ contract PositionManager is hookData ); - if (positionConfigs.hasSubscriber(uint256(salt))) { + if (positionConfigs[uint256(salt)].hasSubscriber()) { _notifyModifyLiquidity(uint256(salt), config, liquidityChange, feesAccrued); } } @@ -369,7 +352,7 @@ contract PositionManager is /// @dev overrides solmate transferFrom in case a notification to subscribers is needed function transferFrom(address from, address to, uint256 id) public virtual override { super.transferFrom(from, to, id); - if (positionConfigs.hasSubscriber(id)) _notifyTransfer(id, from, to); + if (positionConfigs[id].hasSubscriber()) _notifyTransfer(id, from, to); } /// @inheritdoc IPositionManager @@ -385,11 +368,6 @@ contract PositionManager is /// @inheritdoc IPositionManager function getPositionConfigId(uint256 tokenId) external view returns (bytes32) { - return positionConfigs.getConfigId(tokenId); - } - - /// @inheritdoc INotifier - function hasSubscriber(uint256 tokenId) external view returns (bool) { - return positionConfigs.hasSubscriber(tokenId); + return positionConfigs[tokenId].getConfigId(); } } diff --git a/src/base/Notifier.sol b/src/base/Notifier.sol index 09a542d3..10a501a2 100644 --- a/src/base/Notifier.sol +++ b/src/base/Notifier.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.24; import {ISubscriber} from "../interfaces/ISubscriber.sol"; import {PositionConfig} from "../libraries/PositionConfig.sol"; +import {PositionConfigId, PositionConfigIdLibrary} from "../libraries/PositionConfigId.sol"; import {BipsLibrary} from "../libraries/BipsLibrary.sol"; import {INotifier} from "../interfaces/INotifier.sol"; import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; @@ -12,6 +13,7 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; abstract contract Notifier is INotifier { using BipsLibrary for uint256; using CustomRevert for bytes4; + using PositionConfigIdLibrary for PositionConfigId; error AlreadySubscribed(address subscriber); @@ -27,9 +29,20 @@ abstract contract Notifier is INotifier { mapping(uint256 tokenId => ISubscriber subscriber) public subscriber; - function _subscribe(uint256 tokenId, PositionConfig memory config, address newSubscriber, bytes memory data) - internal + modifier onlyIfApproved(address caller, uint256 tokenId) virtual; + modifier onlyValidConfig(uint256 tokenId, PositionConfig calldata config) virtual; + + function _positionConfigs(uint256 tokenId) internal view virtual returns (PositionConfigId storage); + + /// @inheritdoc INotifier + function subscribe(uint256 tokenId, PositionConfig calldata config, address newSubscriber, bytes calldata data) + external + payable + onlyIfApproved(msg.sender, tokenId) + onlyValidConfig(tokenId, config) { + // will revert below if the user already has a subcriber + _positionConfigs(tokenId).setSubscribe(); ISubscriber _subscriber = subscriber[tokenId]; if (_subscriber != NO_SUBSCRIBER) revert AlreadySubscribed(address(_subscriber)); @@ -46,8 +59,14 @@ abstract contract Notifier is INotifier { emit Subscribed(tokenId, address(newSubscriber)); } - /// @dev Must always allow a user to unsubscribe. In the case of a malicious subscriber, a user can always unsubscribe safely, ensuring liquidity is always modifiable. - function _unsubscribe(uint256 tokenId, PositionConfig memory config, bytes memory data) internal { + /// @inheritdoc INotifier + function unsubscribe(uint256 tokenId, PositionConfig calldata config, bytes calldata data) + external + payable + onlyIfApproved(msg.sender, tokenId) + onlyValidConfig(tokenId, config) + { + _positionConfigs(tokenId).setUnsubscribe(); ISubscriber _subscriber = subscriber[tokenId]; uint256 subscriberGasLimit = block.gaslimit.calculatePortion(BLOCK_LIMIT_BPS); @@ -96,4 +115,9 @@ abstract contract Notifier is INotifier { success := call(gas(), target, 0, add(encodedCall, 0x20), mload(encodedCall), 0, 0) } } + + /// @inheritdoc INotifier + function hasSubscriber(uint256 tokenId) external view returns (bool) { + return _positionConfigs(tokenId).hasSubscriber(); + } } diff --git a/src/interfaces/INotifier.sol b/src/interfaces/INotifier.sol index 4aa05ae6..20c4d7f0 100644 --- a/src/interfaces/INotifier.sol +++ b/src/interfaces/INotifier.sol @@ -28,6 +28,7 @@ interface INotifier { /// @param config the corresponding PositionConfig for the tokenId /// @param data caller-provided data that's forwarded to the subscriber contract /// @dev payable so it can be multicalled with NATIVE related actions + /// @dev Must always allow a user to unsubscribe. In the case of a malicious subscriber, a user can always unsubscribe safely, ensuring liquidity is always modifiable. function unsubscribe(uint256 tokenId, PositionConfig calldata config, bytes calldata data) external payable; /// @notice Returns whether a a position should call out to notify a subscribing contract on modification or transfer diff --git a/src/libraries/PositionConfig.sol b/src/libraries/PositionConfig.sol index 7c5d632a..8f3d6ead 100644 --- a/src/libraries/PositionConfig.sol +++ b/src/libraries/PositionConfig.sol @@ -10,49 +10,8 @@ struct PositionConfig { int24 tickUpper; } -/// @notice Library to get and set the PositionConfigId and subscriber status for a given tokenId +/// @notice Library to calculate the PositionConfigId from the PositionConfig struct library PositionConfigLibrary { - using PositionConfigLibrary for PositionConfig; - - bytes32 constant MASK_UPPER_BIT = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - bytes32 constant DIRTY_UPPER_BIT = 0x8000000000000000000000000000000000000000000000000000000000000000; - - /// @notice returns the truncated hash of the PositionConfig for a given tokenId - function getConfigId(mapping(uint256 => bytes32) storage positionConfigs, uint256 tokenId) - internal - view - returns (bytes32 configId) - { - configId = positionConfigs[tokenId] & MASK_UPPER_BIT; - } - - function setConfigId( - mapping(uint256 => bytes32) storage positionConfigs, - uint256 tokenId, - PositionConfig calldata config - ) internal { - positionConfigs[tokenId] = config.toId(); - } - - function setSubscribe(mapping(uint256 => bytes32) storage positionConfigs, uint256 tokenId) internal { - positionConfigs[tokenId] |= DIRTY_UPPER_BIT; - } - - function setUnsubscribe(mapping(uint256 => bytes32) storage positionConfigs, uint256 tokenId) internal { - positionConfigs[tokenId] &= MASK_UPPER_BIT; - } - - function hasSubscriber(mapping(uint256 => bytes32) storage positionConfigs, uint256 tokenId) - internal - view - returns (bool subscribed) - { - bytes32 _config = positionConfigs[tokenId]; - assembly ("memory-safe") { - subscribed := shr(255, _config) - } - } - function toId(PositionConfig calldata config) internal pure returns (bytes32 id) { // id = keccak256(abi.encodePacked(currency0, currency1, fee, tickSpacing, hooks, tickLower, tickUpper))) >> 1 assembly ("memory-safe") { diff --git a/src/libraries/PositionConfigId.sol b/src/libraries/PositionConfigId.sol new file mode 100644 index 00000000..40127102 --- /dev/null +++ b/src/libraries/PositionConfigId.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +/// @notice A configId is set per tokenId +/// The lower 255 bits are used to store the truncated hash of the corresponding PositionConfig +/// The upper bit is used to signal if the tokenId has a subscriber +struct PositionConfigId { + bytes32 id; +} + +library PositionConfigIdLibrary { + bytes32 constant MASK_UPPER_BIT = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + bytes32 constant DIRTY_UPPER_BIT = 0x8000000000000000000000000000000000000000000000000000000000000000; + + /// @notice returns the truncated hash of the PositionConfig for a given tokenId + function getConfigId(PositionConfigId storage _configId) internal view returns (bytes32 configId) { + configId = _configId.id & MASK_UPPER_BIT; + } + + /// @dev We only set the config on mint, guaranteeing that the most significant bit is unset, so we can just assign the entire 32 bytes to the id. + function setConfigId(PositionConfigId storage _configId, bytes32 configId) internal { + _configId.id = configId; + } + + function setSubscribe(PositionConfigId storage configId) internal { + configId.id |= DIRTY_UPPER_BIT; + } + + function setUnsubscribe(PositionConfigId storage configId) internal { + configId.id &= MASK_UPPER_BIT; + } + + function hasSubscriber(PositionConfigId storage configId) internal view returns (bool subscribed) { + bytes32 _id = configId.id; + assembly ("memory-safe") { + subscribed := shr(255, _id) + } + } +} diff --git a/test/libraries/PositionConfig.t.sol b/test/libraries/PositionConfig.t.sol index cfec1a54..187ee370 100644 --- a/test/libraries/PositionConfig.t.sol +++ b/test/libraries/PositionConfig.t.sol @@ -4,11 +4,13 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; import {PositionConfig, PositionConfigLibrary} from "../../src/libraries/PositionConfig.sol"; +import {PositionConfigId, PositionConfigIdLibrary} from "../../src/libraries/PositionConfigId.sol"; contract PositionConfigTest is Test { - using PositionConfigLibrary for *; + using PositionConfigLibrary for PositionConfig; + using PositionConfigIdLibrary for PositionConfigId; - mapping(uint256 => bytes32) internal testConfigs; + mapping(uint256 => PositionConfigId) internal testConfigs; bytes32 public constant UPPER_BIT_SET = 0x8000000000000000000000000000000000000000000000000000000000000000; @@ -18,88 +20,127 @@ contract PositionConfigTest is Test { } function test_fuzz_setConfigId(uint256 tokenId, PositionConfig calldata config) public { - testConfigs.setConfigId(tokenId, config); + testConfigs[tokenId].setConfigId(config.toId()); bytes32 expectedConfigId = _calculateExpectedId(config); - bytes32 actualConfigId = testConfigs[tokenId]; + bytes32 actualConfigId = testConfigs[tokenId].id; assertEq(expectedConfigId, actualConfigId); } function test_fuzz_getConfigId(uint256 tokenId, PositionConfig calldata config) public { bytes32 expectedId = _calculateExpectedId(config); // set - testConfigs[tokenId] = expectedId; + testConfigs[tokenId] = PositionConfigId({id: expectedId}); - assertEq(expectedId, testConfigs.getConfigId(tokenId)); + assertEq(expectedId, testConfigs[tokenId].getConfigId()); } function test_fuzz_setConfigId_getConfigId(uint256 tokenId, PositionConfig calldata config) public { - testConfigs.setConfigId(tokenId, config); + testConfigs[tokenId].setConfigId(config.toId()); bytes32 expectedId = _calculateExpectedId(config); - assertEq(testConfigs.getConfigId(tokenId), testConfigs[tokenId]); - assertEq(testConfigs.getConfigId(tokenId), expectedId); + assertEq(testConfigs[tokenId].getConfigId(), testConfigs[tokenId].id); + assertEq(testConfigs[tokenId].getConfigId(), expectedId); } function test_fuzz_getConfigId_equal_afterSubscribe(uint256 tokenId, PositionConfig calldata config) public { - testConfigs.setConfigId(tokenId, config); - testConfigs.setSubscribe(tokenId); + testConfigs[tokenId].setConfigId(config.toId()); + testConfigs[tokenId].setSubscribe(); - assertEq(testConfigs.getConfigId(tokenId), config.toId()); + assertEq(testConfigs[tokenId].getConfigId(), config.toId()); } function test_fuzz_setSubscribe(uint256 tokenId) public { - testConfigs.setSubscribe(tokenId); - bytes32 upperBitSet = testConfigs[tokenId]; + testConfigs[tokenId].setSubscribe(); + bytes32 upperBitSet = testConfigs[tokenId].id; assertEq(upperBitSet, UPPER_BIT_SET); } function test_fuzz_setConfigId_setSubscribe(uint256 tokenId, PositionConfig calldata config) public { - testConfigs.setConfigId(tokenId, config); - testConfigs.setSubscribe(tokenId); + testConfigs[tokenId].setConfigId(config.toId()); + testConfigs[tokenId].setSubscribe(); bytes32 expectedConfig = _calculateExpectedId(config) | UPPER_BIT_SET; - bytes32 _config = testConfigs[tokenId]; + bytes32 _config = testConfigs[tokenId].id; assertEq(_config, expectedConfig); } function test_fuzz_setUnsubscribe(uint256 tokenId) public { - testConfigs.setSubscribe(tokenId); - bytes32 _config = testConfigs[tokenId]; + testConfigs[tokenId].setSubscribe(); + bytes32 _config = testConfigs[tokenId].id; assertEq(_config, UPPER_BIT_SET); - testConfigs.setUnsubscribe(tokenId); - _config = testConfigs[tokenId]; + testConfigs[tokenId].setUnsubscribe(); + _config = testConfigs[tokenId].id; assertEq(_config, 0); } function test_hasSubscriber(uint256 tokenId) public { - testConfigs.setSubscribe(tokenId); - assert(testConfigs.hasSubscriber(tokenId)); - testConfigs.setUnsubscribe(tokenId); - assert(!testConfigs.hasSubscriber(tokenId)); + testConfigs[tokenId].setSubscribe(); + assert(testConfigs[tokenId].hasSubscriber()); + testConfigs[tokenId].setUnsubscribe(); + assert(!testConfigs[tokenId].hasSubscriber()); } function test_fuzz_setConfigId_setSubscribe_setUnsubscribe_getConfigId( uint256 tokenId, PositionConfig calldata config ) public { - assertEq(testConfigs.getConfigId(tokenId), 0); + assertEq(testConfigs[tokenId].getConfigId(), 0); - testConfigs.setConfigId(tokenId, config); - assertEq(testConfigs.getConfigId(tokenId), config.toId()); + testConfigs[tokenId].setConfigId(config.toId()); + assertEq(testConfigs[tokenId].getConfigId(), config.toId()); - testConfigs.setSubscribe(tokenId); - assertEq(testConfigs.getConfigId(tokenId), config.toId()); - assertEq(testConfigs.hasSubscriber(tokenId), true); + testConfigs[tokenId].setSubscribe(); + assertEq(testConfigs[tokenId].getConfigId(), config.toId()); + assertEq(testConfigs[tokenId].hasSubscriber(), true); - testConfigs.setUnsubscribe(tokenId); - assertEq(testConfigs.getConfigId(tokenId), config.toId()); - assertEq(testConfigs.hasSubscriber(tokenId), false); + testConfigs[tokenId].setUnsubscribe(); + assertEq(testConfigs[tokenId].getConfigId(), config.toId()); + assertEq(testConfigs[tokenId].hasSubscriber(), false); + } + + function test_fuzz_setSubscribe_twice(uint256 tokenId, PositionConfig calldata config) public { + assertFalse(testConfigs[tokenId].hasSubscriber()); + + testConfigs[tokenId].setSubscribe(); + testConfigs[tokenId].setSubscribe(); + assertTrue(testConfigs[tokenId].hasSubscriber()); + + // It is known behavior that setting the config id just stores the id directly, meaning the upper most bit is unset. + // This is ok because setConfigId will only ever be called on mint. + testConfigs[tokenId].setConfigId(config.toId()); + assertFalse(testConfigs[tokenId].hasSubscriber()); + + testConfigs[tokenId].setSubscribe(); + testConfigs[tokenId].setSubscribe(); + assertTrue(testConfigs[tokenId].hasSubscriber()); + } + + function test_fuzz_setUnsubscribe_twice(uint256 tokenId, PositionConfig calldata config) public { + assertFalse(testConfigs[tokenId].hasSubscriber()); + + testConfigs[tokenId].setUnsubscribe(); + testConfigs[tokenId].setUnsubscribe(); + assertFalse(testConfigs[tokenId].hasSubscriber()); + + testConfigs[tokenId].setConfigId(config.toId()); + assertFalse(testConfigs[tokenId].hasSubscriber()); + + testConfigs[tokenId].setUnsubscribe(); + testConfigs[tokenId].setUnsubscribe(); + assertFalse(testConfigs[tokenId].hasSubscriber()); + + testConfigs[tokenId].setSubscribe(); + assertTrue(testConfigs[tokenId].hasSubscriber()); + + testConfigs[tokenId].setUnsubscribe(); + testConfigs[tokenId].setUnsubscribe(); + assertFalse(testConfigs[tokenId].hasSubscriber()); } function _calculateExpectedId(PositionConfig calldata config) internal pure returns (bytes32 expectedId) {