Skip to content

Commit

Permalink
Merge branch 'pashov-l05' of https://github.com/Uniswap/v4-periphery
Browse files Browse the repository at this point in the history
…into pashov-l05
  • Loading branch information
dianakocsis committed Oct 25, 2024
2 parents 6b1baee + 429bdbc commit bc5b7f3
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_subscribe.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
84348
88168
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_unsubscribe.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
59260
63080
9 changes: 8 additions & 1 deletion src/PositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ contract PositionManager is
_;
}

/// @notice Enforces that the PoolManager is locked.
modifier onlyIfPoolManagerLocked() override {
if (poolManager.isUnlocked()) revert PoolManagerMustBeLocked();
_;
}

function tokenURI(uint256 tokenId) public view override returns (string memory) {
return IPositionDescriptor(tokenDescriptor).tokenURI(this, tokenId);
}
Expand Down Expand Up @@ -444,7 +450,8 @@ 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 {
/// @dev will revert if pool manager is locked
function transferFrom(address from, address to, uint256 id) public virtual override onlyIfPoolManagerLocked {
super.transferFrom(from, to, id);
if (positionInfo[id].hasSubscriber()) _notifyTransfer(id, from, to);
}
Expand Down
11 changes: 10 additions & 1 deletion src/base/Notifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ abstract contract Notifier is INotifier {
/// @param tokenId the tokenId of the position
modifier onlyIfApproved(address caller, uint256 tokenId) virtual;

/// @notice Enforces that the PoolManager is locked.
modifier onlyIfPoolManagerLocked() virtual;

function _setUnsubscribed(uint256 tokenId) internal virtual;

function _setSubscribed(uint256 tokenId) internal virtual;
Expand All @@ -37,6 +40,7 @@ abstract contract Notifier is INotifier {
function subscribe(uint256 tokenId, address newSubscriber, bytes calldata data)
external
payable
onlyIfPoolManagerLocked
onlyIfApproved(msg.sender, tokenId)
{
ISubscriber _subscriber = subscriber[tokenId];
Expand All @@ -56,7 +60,12 @@ abstract contract Notifier is INotifier {
}

/// @inheritdoc INotifier
function unsubscribe(uint256 tokenId) external payable onlyIfApproved(msg.sender, tokenId) {
function unsubscribe(uint256 tokenId)
external
payable
onlyIfPoolManagerLocked
onlyIfApproved(msg.sender, tokenId)
{
_unsubscribe(tokenId);
}

Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/INotifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ interface INotifier {
/// @param data caller-provided data that's forwarded to the subscriber contract
/// @dev Calling subscribe when a position is already subscribed will revert
/// @dev payable so it can be multicalled with NATIVE related actions
/// @dev will revert if pool manager is locked
function subscribe(uint256 tokenId, address newSubscriber, bytes calldata data) external payable;

/// @notice Removes the subscriber from receiving notifications for a respective position
/// @param tokenId the ERC721 tokenId
/// @dev Callers must specify a high gas limit (remaining gas should be higher than unsubscriberGasLimit) such that the subscriber can be notified
/// @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.
/// @dev will revert if pool manager is locked
function unsubscribe(uint256 tokenId) external payable;

/// @notice Returns and determines the maximum allowable gas-used for notifying unsubscribe
Expand Down
3 changes: 3 additions & 0 deletions src/interfaces/IPositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ interface IPositionManager is INotifier, IImmutableState {
error NotApproved(address caller);
/// @notice Thrown when the block.timestamp exceeds the user-provided deadline
error DeadlinePassed(uint256 deadline);
/// @notice Thrown when calling transfer, subscribe, or unsubscribe when the PoolManager is unlocked.
/// @dev This is to prevent hooks from being able to trigger notifications at the same time the position is being modified.
error PoolManagerMustBeLocked();

/// @notice Unlocks Uniswap v4 PoolManager and batches actions for modifying liquidity
/// @dev This is the standard entrypoint for the PositionManager
Expand Down
37 changes: 37 additions & 0 deletions test/mocks/MockReenterHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {BaseTestHooks} from "@uniswap/v4-core/src/test/BaseTestHooks.sol";
import {PositionManager} from "../../src/PositionManager.sol";

contract MockReenterHook is BaseTestHooks {
PositionManager posm;

function beforeAddLiquidity(
address,
PoolKey calldata,
IPoolManager.ModifyLiquidityParams calldata,
bytes calldata functionSelector
) external override returns (bytes4) {
if (functionSelector.length == 0) {
return this.beforeAddLiquidity.selector;
}
(bytes4 selector, address owner, uint256 tokenId) = abi.decode(functionSelector, (bytes4, address, uint256));

if (selector == posm.transferFrom.selector) {
posm.transferFrom(owner, address(this), tokenId);
} else if (selector == posm.subscribe.selector) {
posm.subscribe(tokenId, address(this), "");
} else if (selector == posm.unsubscribe.selector) {
posm.unsubscribe(tokenId);
}
return this.beforeAddLiquidity.selector;
}

function setPosm(PositionManager _posm) external {
posm = _posm;
}
}
81 changes: 81 additions & 0 deletions test/position-managers/PositionManager.notifier.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol";
import {PosmTestSetup} from "../shared/PosmTestSetup.sol";
import {MockSubscriber} from "../mocks/MockSubscriber.sol";
Expand All @@ -20,6 +21,7 @@ import {Actions} from "../../src/libraries/Actions.sol";
import {INotifier} from "../../src/interfaces/INotifier.sol";
import {MockReturnDataSubscriber, MockRevertSubscriber} from "../mocks/MockBadSubscribers.sol";
import {PositionInfoLibrary, PositionInfo} from "../../src/libraries/PositionInfoLibrary.sol";
import {MockReenterHook} from "../mocks/MockReenterHook.sol";

contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot {
using PoolIdLibrary for PoolKey;
Expand All @@ -31,10 +33,13 @@ contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot {
MockReturnDataSubscriber badSubscriber;
PositionConfig config;
MockRevertSubscriber revertSubscriber;
MockReenterHook reenterHook;

address alice = makeAddr("ALICE");
address bob = makeAddr("BOB");

PositionConfig reenterConfig;

function setUp() public {
deployFreshManagerAndRouters();
deployMintAndApprove2Currencies();
Expand All @@ -49,6 +54,17 @@ contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot {
revertSubscriber = new MockRevertSubscriber(lpm);
config = PositionConfig({poolKey: key, tickLower: -300, tickUpper: 300});

// set the reenter hook
MockReenterHook impl = new MockReenterHook();
address hookAddr = payable(address(uint160(Hooks.BEFORE_ADD_LIQUIDITY_FLAG)));
vm.etch(hookAddr, address(impl).code);
reenterHook = MockReenterHook(hookAddr);
reenterHook.setPosm(lpm);

PoolKey memory reenterKey = PoolKey(currency0, currency1, 3000, 60, IHooks(reenterHook));
manager.initialize(reenterKey, SQRT_PRICE_1_1);
reenterConfig = PositionConfig({poolKey: reenterKey, tickLower: -60, tickUpper: 60});

// TODO: Test NATIVE poolKey
}

Expand Down Expand Up @@ -647,4 +663,69 @@ contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot {
assertEq(sub.notifyUnsubscribeCount(), beforeUnsubCount + 1);
}
}

function test_unsubscribe_reverts_PoolManagerMustBeLocked() public {
uint256 tokenId = lpm.nextTokenId();
mint(reenterConfig, 10e18, address(this), ZERO_BYTES);

bytes memory hookData = abi.encode(lpm.unsubscribe.selector, address(this), tokenId);
bytes memory actions = getMintEncoded(reenterConfig, 10e18, address(this), hookData);

// approve hook as it should not revert because it does not have permissions
lpm.approve(address(reenterHook), tokenId);
// subscribe as it should not revert because there is no subscriber
lpm.subscribe(tokenId, address(sub), ZERO_BYTES);

// should revert since the pool manager is unlocked
vm.expectRevert(
abi.encodeWithSelector(
Hooks.Wrap__FailedHookCall.selector,
address(reenterHook),
abi.encodeWithSelector(IPositionManager.PoolManagerMustBeLocked.selector)
)
);
lpm.modifyLiquidities(actions, _deadline);
}

function test_subscribe_reverts_PoolManagerMustBeLocked() public {
uint256 tokenId = lpm.nextTokenId();
mint(reenterConfig, 10e18, address(this), ZERO_BYTES);

bytes memory hookData = abi.encode(lpm.subscribe.selector, address(this), tokenId);
bytes memory actions = getMintEncoded(reenterConfig, 10e18, address(this), hookData);

// approve hook as it should not revert because it does not have permissions
lpm.approve(address(reenterHook), tokenId);

// should revert since the pool manager is unlocked
vm.expectRevert(
abi.encodeWithSelector(
Hooks.Wrap__FailedHookCall.selector,
address(reenterHook),
abi.encodeWithSelector(IPositionManager.PoolManagerMustBeLocked.selector)
)
);
lpm.modifyLiquidities(actions, _deadline);
}

function test_transferFrom_reverts_PoolManagerMustBeLocked() public {
uint256 tokenId = lpm.nextTokenId();
mint(reenterConfig, 10e18, address(this), ZERO_BYTES);

bytes memory hookData = abi.encode(lpm.transferFrom.selector, address(this), tokenId);
bytes memory actions = getMintEncoded(reenterConfig, 10e18, address(this), hookData);

// approve hook as it should not revert because it does not have permissions
lpm.approve(address(reenterHook), tokenId);

// should revert since the pool manager is unlocked
vm.expectRevert(
abi.encodeWithSelector(
Hooks.Wrap__FailedHookCall.selector,
address(reenterHook),
abi.encodeWithSelector(IPositionManager.PoolManagerMustBeLocked.selector)
)
);
lpm.modifyLiquidities(actions, _deadline);
}
}

0 comments on commit bc5b7f3

Please sign in to comment.