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 all 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 @@
47059
47186
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 @@
46876
47004
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_burn_nonEmpty.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
129852
130136
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 @@
122773
123058
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_collect.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
149984
150257
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_collect_native.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
141136
141409
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_collect_sameRange.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
149984
150257
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_decreaseLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
115527
115800
Original file line number Diff line number Diff line change
@@ -1 +1 @@
108384
108602
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_decrease_burnEmpty.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
133885
134196
Original file line number Diff line number Diff line change
@@ -1 +1 @@
126624
126935
Original file line number Diff line number Diff line change
@@ -1 +1 @@
128243
128516
Original file line number Diff line number Diff line change
@@ -1 +1 @@
152100
152363
Original file line number Diff line number Diff line change
@@ -1 +1 @@
151341
151604
Original file line number Diff line number Diff line change
@@ -1 +1 @@
133900
134163
Original file line number Diff line number Diff line change
@@ -1 +1 @@
130065
130328
Original file line number Diff line number Diff line change
@@ -1 +1 @@
170759
171022
Original file line number Diff line number Diff line change
@@ -1 +1 @@
140581
140866
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_native.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
336663
336841
1 change: 0 additions & 1 deletion .forge-snapshots/PositionManager_mint_nativeWithSweep.snap

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1 +1 @@
345169
345347
Original file line number Diff line number Diff line change
@@ -1 +1 @@
344710
344888
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_onSameTickLower.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
314645
314823
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_onSameTickUpper.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
315287
315465
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_sameRange.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
240869
241047
Original file line number Diff line number Diff line change
@@ -1 +1 @@
370969
371147
Original file line number Diff line number Diff line change
@@ -1 +1 @@
320663
320841
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_withClose.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
371963
372141
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_withSettlePair.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
371342
371520
Original file line number Diff line number Diff line change
@@ -1 +1 @@
416316
416538
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
85 changes: 72 additions & 13 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 {Permit2Forwarder} from "./base/Permit2Forwarder.sol";
import {SlippageCheckLibrary} from "./libraries/SlippageCheck.sol";

Expand All @@ -36,12 +38,13 @@ contract PositionManager is
DeltaResolver,
ReentrancyLock,
BaseActionsRouter,
Notifier,
Permit2Forwarder
{
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 @@ -51,20 +54,39 @@ contract PositionManager is
/// @dev The ID of the next token that will be minted. Skips 0
uint256 public nextTokenId = 1;

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

constructor(IPoolManager _poolManager, IAllowanceTransfer _permit2)
BaseActionsRouter(_poolManager)
Permit2Forwarder(_permit2)
ERC721Permit("Uniswap V4 Positions NFT", "UNI-V4-POSM", "1")
{}

/// @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 caller 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 caller
/// _msgSender() should ONLY be used if this is being called from within the unlockCallback
modifier onlyIfApproved(address caller, uint256 tokenId) {
if (!_isApprovedOrOwner(caller, tokenId)) revert NotApproved(caller);
_;
}

/// @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 @@ -76,6 +98,29 @@ 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)
{
// call to _subscribe will revert if the user already has a sub
positionConfigs.setSubscribe(tokenId);
snreynolds marked this conversation as resolved.
Show resolved Hide resolved
hensha256 marked this conversation as resolved.
Show resolved Hide resolved
_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 @@ -149,8 +194,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 @@ -164,10 +208,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 All @@ -192,7 +233,7 @@ contract PositionManager is
// _beforeModify is not called here because the tokenId is newly minted
BalanceDelta liquidityDelta = _modifyLiquidity(config, liquidity.toInt256(), bytes32(tokenId), hookData);
liquidityDelta.validateMaxIn(amount0Max, amount1Max);
positionConfigs[tokenId] = config.toId();
positionConfigs.setConfigId(tokenId, config);
}

function _close(Currency currency) internal {
Expand Down Expand Up @@ -231,9 +272,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 @@ -264,6 +303,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 @@ -291,4 +334,20 @@ 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 {
hensha256 marked this conversation as resolved.
Show resolved Hide resolved
super.transferFrom(from, to, id);
if (positionConfigs.hasSubscriber(id)) _notifyTransfer(id, from, to);
hensha256 marked this conversation as resolved.
Show resolved Hide resolved
}

/// @inheritdoc IPositionManager
function getPositionConfigId(uint256 tokenId) external view returns (bytes32) {
Copy link
Contributor

Choose a reason for hiding this comment

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

@inheritdoc and both these 2 functions in interface with natspec

Copy link
Contributor

Choose a reason for hiding this comment

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

oh theyre in with natspec - just inheritdoc!

return positionConfigs.getConfigId(tokenId);
}

/// @inheritdoc INotifier
function hasSubscriber(uint256 tokenId) external view returns (bool) {
return positionConfigs.hasSubscriber(tokenId);
}
}
57 changes: 57 additions & 0 deletions src/base/Notifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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";

/// @notice Notifier is used to opt in to sending updates to external contracts about position modifications or transfers
abstract contract Notifier is INotifier {
snreynolds marked this conversation as resolved.
Show resolved Hide resolved
using GasLimitCalculator for uint256;

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;

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();

try _subscriber.notifyUnsubscribe{gas: subscriberGasLimit}(tokenId, config) {} catch {}
hensha256 marked this conversation as resolved.
Show resolved Hide resolved

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);
}
}
1 change: 1 addition & 0 deletions src/interfaces/IERC721Permit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface IERC721Permit {
/// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s`
/// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s`
/// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v`
/// @dev payable so it can be multicalled with NATIVE related actions
function permit(address spender, uint256 tokenId, uint256 deadline, uint256 nonce, uint8 v, bytes32 r, bytes32 s)
external
payable;
Expand Down
Loading
Loading