Skip to content

Commit

Permalink
posm: add staking through subscribers (#229)
Browse files Browse the repository at this point in the history
* compiling

* compiling, gas snaps added

* add unit tests for PositionConfigLibrary

* subscriber tests

* pr comments

* use gas limit calcualtor

* test return data

* use solidity

* comments

* natspec & more tests

* payable

* pr comments

* pr comments

* inheritdoc
  • Loading branch information
snreynolds authored Aug 2, 2024
1 parent a9e463d commit e764aec
Show file tree
Hide file tree
Showing 45 changed files with 828 additions and 57 deletions.
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);
_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 {
super.transferFrom(from, to, id);
if (positionConfigs.hasSubscriber(id)) _notifyTransfer(id, from, to);
}

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

uint256 subscriberGasLimit = BLOCK_LIMIT_BPS.toGasLimit();

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

0 comments on commit e764aec

Please sign in to comment.