Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

posm: add staking through subscribers #229

Merged
merged 22 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_burn_empty.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
47040
47168
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_burn_empty_native.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
46858
46986
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_burn_nonEmpty.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
129816
130119
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_burn_nonEmpty_native.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
122737
123040
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_collect.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
149962
150258
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_collect_native.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
141114
141410
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_collect_sameRange.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
149962
150258
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_decreaseLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
115505
115801
Original file line number Diff line number Diff line change
@@ -1 +1 @@
108366
108603
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_decrease_burnEmpty.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
133849
134178
Original file line number Diff line number Diff line change
@@ -1 +1 @@
126588
126917
Original file line number Diff line number Diff line change
@@ -1 +1 @@
128221
128517
Original file line number Diff line number Diff line change
@@ -1 +1 @@
152100
152364
Original file line number Diff line number Diff line change
@@ -1 +1 @@
133900
134164
Original file line number Diff line number Diff line change
@@ -1 +1 @@
134581
134845
Original file line number Diff line number Diff line change
@@ -1 +1 @@
170737
171023
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
372012
372160
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_native.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
336712
336860
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_nativeWithSweep.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
345221
345369
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_onSameTickLower.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
314694
314842
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_onSameTickUpper.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
315336
315484
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_sameRange.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
240918
241066
Original file line number Diff line number Diff line change
@@ -1 +1 @@
369926
370074
Original file line number Diff line number Diff line change
@@ -1 +1 @@
320712
320860
Original file line number Diff line number Diff line change
@@ -1 +1 @@
416388
416602
3 changes: 3 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ fuzz_runs = 10_000
[profile.ci]
fuzz_runs = 100_000

[profile.gas]
gas_limit=30_000_000

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
73 changes: 61 additions & 12 deletions src/PositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import {DeltaResolver} from "./base/DeltaResolver.sol";
import {PositionConfig, PositionConfigLibrary} from "./libraries/PositionConfig.sol";
import {BaseActionsRouter} from "./base/BaseActionsRouter.sol";
import {Actions} from "./libraries/Actions.sol";
import {Notifier} from "./base/Notifier.sol";
import {CalldataDecoder} from "./libraries/CalldataDecoder.sol";
import {INotifier} from "./interfaces/INotifier.sol";
import {SlippageCheckLibrary} from "./libraries/SlippageCheck.sol";

contract PositionManager is
Expand All @@ -34,12 +36,13 @@ contract PositionManager is
Multicall,
DeltaResolver,
ReentrancyLock,
BaseActionsRouter
BaseActionsRouter,
Notifier
{
using SafeTransferLib for *;
using CurrencyLibrary for Currency;
using PoolIdLibrary for PoolKey;
using PositionConfigLibrary for PositionConfig;
using PositionConfigLibrary for *;
using StateLibrary for IPoolManager;
using TransientStateLibrary for IPoolManager;
using SafeCast for uint256;
Expand All @@ -50,7 +53,7 @@ contract PositionManager is
uint256 public nextTokenId = 1;

/// @inheritdoc IPositionManager
mapping(uint256 tokenId => bytes32 configId) public positionConfigs;
mapping(uint256 tokenId => bytes32 config) public positionConfigs;

IAllowanceTransfer public immutable permit2;

Expand All @@ -61,11 +64,31 @@ contract PositionManager is
permit2 = _permit2;
}

/// @notice Reverts if the deadline has passed
/// @param deadline The timestamp at which the call is no longer valid, passed in by the caller
modifier checkDeadline(uint256 deadline) {
if (block.timestamp > deadline) revert DeadlinePassed();
_;
}

/// @notice Reverts if the caller is not the owner or approved for the ERC721 token
/// @param sender The address of the caller
/// @param tokenId the unique identifier of the ERC721 token
/// @dev either msg.sender or _msgSender() is passed in as the sender
/// _msgSender() should ONLY be used if this is being called from within the unlockCallback
modifier onlyIfApproved(address sender, uint256 tokenId) {
if (!_isApprovedOrOwner(sender, tokenId)) revert NotApproved(sender);
_;
}

/// @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);
_;
}

/// @param unlockData is an encoding of actions, params, and currencies
/// @param deadline is the timestamp at which the unlockData will no longer be valid
function modifyLiquidities(bytes calldata unlockData, uint256 deadline)
Expand All @@ -77,6 +100,28 @@ contract PositionManager is
_executeActions(unlockData);
}

/// @inheritdoc INotifier
function subscribe(uint256 tokenId, PositionConfig calldata config, address subscriber)
external
payable
onlyIfApproved(msg.sender, tokenId)
onlyValidConfig(tokenId, config)
{
positionConfigs.setSubscribe(tokenId);
_subscribe(tokenId, config, subscriber);
}

/// @inheritdoc INotifier
function unsubscribe(uint256 tokenId, PositionConfig calldata config)
external
payable
onlyIfApproved(msg.sender, tokenId)
onlyValidConfig(tokenId, config)
{
positionConfigs.setUnsubscribe(tokenId);
_unsubscribe(tokenId, config);
}

function _handleAction(uint256 action, bytes calldata params) internal virtual override {
if (action == Actions.INCREASE_LIQUIDITY) {
(
Expand Down Expand Up @@ -144,8 +189,7 @@ contract PositionManager is
uint128 amount0Max,
uint128 amount1Max,
bytes calldata hookData
) internal {
if (positionConfigs[tokenId] != config.toId()) revert IncorrectPositionConfigForTokenId(tokenId);
) internal onlyValidConfig(tokenId, config) {
// Note: The tokenId is used as the salt for this position, so every minted position has unique storage in the pool manager.
BalanceDelta liquidityDelta = _modifyLiquidity(config, liquidity.toInt256(), bytes32(tokenId), hookData);
liquidityDelta.validateMaxInNegative(amount0Max, amount1Max);
Expand All @@ -159,10 +203,7 @@ contract PositionManager is
uint128 amount0Min,
uint128 amount1Min,
bytes calldata hookData
) internal {
if (!_isApprovedOrOwner(_msgSender(), tokenId)) revert NotApproved(_msgSender());
if (positionConfigs[tokenId] != config.toId()) revert IncorrectPositionConfigForTokenId(tokenId);

) internal onlyIfApproved(_msgSender(), tokenId) onlyValidConfig(tokenId, config) {
// Note: the tokenId is used as the salt.
BalanceDelta liquidityDelta = _modifyLiquidity(config, -(liquidity.toInt256()), bytes32(tokenId), hookData);
liquidityDelta.validateMinOut(amount0Min, amount1Min);
Expand Down Expand Up @@ -217,9 +258,7 @@ contract PositionManager is
uint128 amount0Min,
uint128 amount1Min,
bytes calldata hookData
) internal {
if (!_isApprovedOrOwner(_msgSender(), tokenId)) revert NotApproved(_msgSender());
if (positionConfigs[tokenId] != config.toId()) revert IncorrectPositionConfigForTokenId(tokenId);
) internal onlyIfApproved(_msgSender(), tokenId) onlyValidConfig(tokenId, config) {
uint256 liquidity = uint256(_getPositionLiquidity(config, tokenId));

BalanceDelta liquidityDelta;
Expand Down Expand Up @@ -250,6 +289,10 @@ contract PositionManager is
}),
hookData
);

if (positionConfigs.hasSubscriber(uint256(salt))) {
_notifyModifyLiquidity(uint256(salt), config, liquidityChange);
}
}

function _getPositionLiquidity(PositionConfig calldata config, uint256 tokenId)
Expand Down Expand Up @@ -277,4 +320,10 @@ contract PositionManager is
permit2.transferFrom(payer, address(poolManager), uint160(amount), Currency.unwrap(currency));
}
}

/// @dev overrides solmate transferFrom in case a notification to subscribers is needed
function transferFrom(address from, address to, uint256 id) public override {
super.transferFrom(from, to, id);
if (positionConfigs.hasSubscriber(id)) _notifyTransfer(id, from, to);
}
}
61 changes: 61 additions & 0 deletions src/base/Notifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

import {ISubscriber} from "../interfaces/ISubscriber.sol";
import {PositionConfig} from "../libraries/PositionConfig.sol";
import {GasLimitCalculator} from "../libraries/GasLimitCalculator.sol";

import "../interfaces/INotifier.sol";

abstract contract Notifier is INotifier {
using GasLimitCalculator for uint256;

error SubscriberCannotBeNotified();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subscriber address?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually we dont even throw this anymore, removed

error AlreadySubscribed(address subscriber);

event Subscribed(uint256 tokenId, address subscriber);
event Unsubscribed(uint256 tokenId, address subscriber);

ISubscriber private constant NO_SUBSCRIBER = ISubscriber(address(0));

// a percentage of the block.gaslimit denoted in BPS, used as the gas limit for subscriber calls
// 100 bps is 1%
// at 30M gas, the limit is 300K
uint256 private constant BLOCK_LIMIT_BPS = 100;

mapping(uint256 tokenId => ISubscriber subscriber) public subscriber;

constructor() {}

function _subscribe(uint256 tokenId, PositionConfig memory config, address newSubscriber) internal {
ISubscriber _subscriber = subscriber[tokenId];

if (_subscriber != NO_SUBSCRIBER) revert AlreadySubscribed(address(_subscriber));
subscriber[tokenId] = ISubscriber(newSubscriber);

ISubscriber(newSubscriber).notifySubscribe(tokenId, config);
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) internal {
ISubscriber _subscriber = subscriber[tokenId];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it revert if they arent already subscribed? I believe currently if you call _unsubscribe and youre not subscribed it would succeed? Because contract calls to EOAs (like address(0)) succeed. Maybe add a test to check

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it reverts in the test I wrote with no code changes


uint256 subscriberGasLimit = BLOCK_LIMIT_BPS.toGasLimit();

bytes memory data = abi.encodeWithSelector(ISubscriber.notifyUnsubscribe.selector, tokenId, config);

try _subscriber.notifyUnsubscribe{gas: subscriberGasLimit}(tokenId, config) {} catch {}

delete subscriber[tokenId];
emit Unsubscribed(tokenId, address(_subscriber));
}

function _notifyModifyLiquidity(uint256 tokenId, PositionConfig memory config, int256 liquidityChange) internal {
subscriber[tokenId].notifyModifyLiquidity(tokenId, config, liquidityChange);
}

function _notifyTransfer(uint256 tokenId, address previousOwner, address newOwner) internal {
subscriber[tokenId].notifyTransfer(tokenId, previousOwner, newOwner);
}
}
18 changes: 18 additions & 0 deletions src/interfaces/INotifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import {PositionConfig} from "../libraries/PositionConfig.sol";

/// @notice This interface is used to opt in to sending updates to external contracts about position modifications or transfers
interface INotifier {
/// @notice Enables the subscriber to receive notifications for a respective position
/// @param tokenId the ERC721 tokenId
/// @param config the corresponding PositionConfig for the tokenId
/// @param subscriber the address to notify
function subscribe(uint256 tokenId, PositionConfig calldata config, address subscriber) external payable;

/// @notice Removes the subscriber from receiving notifications for a respective position
/// @param tokenId the ERC721 tokenId
/// @param config the corresponding PositionConfig for the tokenId
function unsubscribe(uint256 tokenId, PositionConfig calldata config) external payable;
}
8 changes: 6 additions & 2 deletions src/interfaces/IPositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ pragma solidity ^0.8.24;

import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";

interface IPositionManager {
import {INotifier} from "./INotifier.sol";

interface IPositionManager is INotifier {
error NotApproved(address caller);
error DeadlinePassed();
error IncorrectPositionConfigForTokenId(uint256 tokenId);

/// @notice Maps the ERC721 tokenId to a configId, which is a keccak256 hash of the position's pool key, and range (tickLower, tickUpper)
/// Enforces that a minted ERC721 token is tied to one range on one pool.
/// @param tokenId the ERC721 tokenId, assigned at mint
/// @return configId the hash of the position's poolkey, tickLower, and tickUpper
/// @return configId a truncated hash of the position's poolkey, tickLower, and tickUpper and a reserved upper bit for the isSubscribed flag
/// @dev the highest bit of the configId is used to signal if the position is subscribed
/// and the lower bits contain the truncated hash of the PositionConfig
function positionConfigs(uint256 tokenId) external view returns (bytes32 configId);

/// @notice Batches many liquidity modification calls to pool manager
Expand Down
11 changes: 11 additions & 0 deletions src/interfaces/ISubscriber.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import {PositionConfig} from "../libraries/PositionConfig.sol";

interface ISubscriber {
function notifySubscribe(uint256 tokenId, PositionConfig memory config) external;
function notifyUnsubscribe(uint256 tokenId, PositionConfig memory config) external;
function notifyModifyLiquidity(uint256 tokenId, PositionConfig memory config, int256 liquidityChange) external;
function notifyTransfer(uint256 tokenId, address previousOwner, address newOwner) external;
}
Loading
Loading