diff --git a/contracts/NonfungiblePositionManagerV4.sol b/contracts/NonfungiblePositionManagerV4.sol new file mode 100644 index 00000000..ca18210d --- /dev/null +++ b/contracts/NonfungiblePositionManagerV4.sol @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; +import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; + +import {INonfungiblePositionManagerV4} from "./interfaces/INonfungiblePositionManagerV4.sol"; +import {PeripheryValidation} from "./base/PeripheryValidation.sol"; +import {PeripheryPayments} from "./base/PeripheryPayments.sol"; +import {PeripheryImmutableState} from "./base/PeripheryImmutableState.sol"; +import {SelfPermit} from "./base/SelfPermit.sol"; +import {LiquidityManagement} from "./base/LiquidityManagement.sol"; +import {Multicall} from "./base/Multicall.sol"; + +contract NonfungiblePositionManagerV4 is + INonfungiblePositionManagerV4, + ERC721, + PeripheryImmutableState, + PeripheryValidation, + LiquidityManagement, + SelfPermit, + Multicall +{ + using PoolIdLibrary for PoolKey; + + error InvalidTokenID(); + error NotApproved(); + error NotCleared(); + error NonexistentToken(); + + // details about the Uniswap position + struct TokenPosition { + // the nonce for permits + uint96 nonce; + // the address that is approved for spending this token + address operator; + // the hashed poolKey of the pool with which this token is connected + PoolId poolId; + // the tick range of the position + int24 tickLower; + int24 tickUpper; + // the liquidity of the position + uint128 liquidity; + // the fee growth of the aggregate position as of the last action on the individual position + uint256 feeGrowthInside0LastX128; + uint256 feeGrowthInside1LastX128; + // how many uncollected tokens are owed to the position, as of the last computation + uint128 tokensOwed0; + uint128 tokensOwed1; + } + + /// @dev Pool keys by poolIds + mapping(bytes32 => PoolKey) private _poolIdToPoolKey; + + /// @dev The token ID position data + mapping(uint256 => TokenPosition) private _positions; + + /// @dev The ID of the next token that will be minted. Skips 0 + uint176 private _nextId = 1; + + /// @dev The address of the token descriptor contract, which handles generating token URIs for position tokens + address private immutable _tokenDescriptor; + + constructor(IPoolManager _poolManager, address _tokenDescriptor_) + PeripheryImmutableState(_poolManager) + ERC721("Uniswap V4 Positions NFT-V1", "UNI-V4-POS") + { + _tokenDescriptor = _tokenDescriptor_; + } + + /// @inheritdoc INonfungiblePositionManagerV4 + function positions(uint256 tokenId) + external + view + override + returns ( + uint96 nonce, + address operator, + Currency currency0, + Currency currency1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ) + { + TokenPosition memory position = _positions[tokenId]; + if (PoolId.unwrap(position.poolId) == 0) revert InvalidTokenID(); + PoolKey memory poolKey = _poolIdToPoolKey[PoolId.unwrap(position.poolId)]; + return ( + position.nonce, + position.operator, + poolKey.currency0, + poolKey.currency1, + poolKey.fee, + position.tickLower, + position.tickUpper, + position.liquidity, + position.feeGrowthInside0LastX128, + position.feeGrowthInside1LastX128, + position.tokensOwed0, + position.tokensOwed1 + ); + } + + /// @inheritdoc INonfungiblePositionManagerV4 + function mint(MintParams calldata params) + external + payable + override + checkDeadline(params.deadline) + returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) + { + // TODO: implement this + } + + /// @inheritdoc INonfungiblePositionManagerV4 + function increaseLiquidity(IncreaseLiquidityParams calldata params) + external + payable + override + checkDeadline(params.deadline) + returns (uint128 liquidity, uint256 amount0, uint256 amount1) + { + // TODO: implement this + } + + /// @inheritdoc INonfungiblePositionManagerV4 + function decreaseLiquidity(DecreaseLiquidityParams calldata params) + external + payable + override + isAuthorizedForToken(params.tokenId) + checkDeadline(params.deadline) + returns (uint256 amount0, uint256 amount1) + { + // TODO: implement this + } + + /// @inheritdoc INonfungiblePositionManagerV4 + function collect(CollectParams calldata params) + external + payable + override + isAuthorizedForToken(params.tokenId) + returns (uint256 amount0, uint256 amount1) + { + // TODO: implement this + } + + /// @inheritdoc INonfungiblePositionManagerV4 + function burn(uint256 tokenId) external payable override isAuthorizedForToken(tokenId) { + TokenPosition storage position = _positions[tokenId]; + if (position.liquidity != 0 || position.tokensOwed0 != 0 || position.tokensOwed1 != 0) revert NotCleared(); + delete _positions[tokenId]; + _burn(tokenId); + } + + modifier isAuthorizedForToken(uint256 tokenId) { + if (!_isApprovedOrOwner(msg.sender, tokenId)) revert NotApproved(); + _; + } + + function tokenURI(uint256 tokenId) public view override(ERC721, IERC721Metadata) returns (string memory) { + // TODO: implement this + } + + /// @inheritdoc IERC721 + function getApproved(uint256 tokenId) public view override(ERC721, IERC721) returns (address) { + if (!_exists(tokenId)) revert NonexistentToken(); + + return _positions[tokenId].operator; + } + + /// @dev Overrides _approve to use the operator in the position, which is packed with the position permit nonce + function _approve(address to, uint256 tokenId) internal override(ERC721) { + _positions[tokenId].operator = to; + emit Approval(ownerOf(tokenId), to, tokenId); + } + + function tokenByIndex(uint256 index) external view returns (uint256) { + // TODO: implement this + } + + function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256) { + // TODO: implement this + } + + function totalSupply() external view returns (uint256) { + // TODO: implement this + } +} diff --git a/contracts/base/LiquidityManagement.sol b/contracts/base/LiquidityManagement.sol new file mode 100644 index 00000000..db75da34 --- /dev/null +++ b/contracts/base/LiquidityManagement.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; +import {ILockCallback} from "@uniswap/v4-core/contracts/interfaces/callback/ILockCallback.sol"; +import {TickMath} from "@uniswap/v4-core/contracts/libraries/TickMath.sol"; +import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; +import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; + +import {LiquidityAmounts} from "../libraries/LiquidityAmounts.sol"; +import {PeripheryImmutableState} from "./PeripheryImmutableState.sol"; +import {PeripheryPayments} from "./PeripheryPayments.sol"; + +/// @title Liquidity management functions +/// @notice Internal functions for safely managing liquidity in Uniswap V4 +abstract contract LiquidityManagement is ILockCallback, PeripheryImmutableState, PeripheryPayments { + using CurrencyLibrary for Currency; + using PoolIdLibrary for PoolKey; + + error PriceSlippage(); + + enum CallbackType {AddLiquidity} + + struct CallbackData { + CallbackType callbackType; + address sender; + bytes params; + } + + struct AddLiquidityParams { + PoolKey poolKey; + int24 tickLower; + int24 tickUpper; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + bytes hookData; + } + + /// @notice Add liquidity to an initialized pool + function addLiquidity(AddLiquidityParams memory params) + internal + returns (uint128 liquidity, uint256 amount0, uint256 amount1) + { + (liquidity, amount0, amount1) = abi.decode( + poolManager.lock(abi.encode(CallbackData(CallbackType.AddLiquidity, msg.sender, abi.encode(params)))), + (uint128, uint256, uint256) + ); + } + + function addLiquidityCallback(AddLiquidityParams memory params) + internal + returns (uint128 liquidity, BalanceDelta delta) + { + (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(params.poolKey.toId()); + uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(params.tickLower); + uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(params.tickUpper); + liquidity = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, params.amount0Desired, params.amount1Desired + ); + delta = poolManager.modifyPosition( + params.poolKey, + IPoolManager.ModifyPositionParams(params.tickLower, params.tickUpper, int256(int128(liquidity))), + params.hookData + ); + if ( + uint256(int256(delta.amount0())) < params.amount0Min || uint256(int256(delta.amount1())) < params.amount1Min + ) revert PriceSlippage(); + } + + function settleDeltas(address from, PoolKey memory poolKey, BalanceDelta delta) internal { + if (delta.amount0() > 0) { + pay(poolKey.currency0, from, address(poolManager), uint256(int256(delta.amount0()))); + poolManager.settle(poolKey.currency0); + } else if (delta.amount0() < 0) { + poolManager.take(poolKey.currency0, address(this), uint128(-delta.amount0())); + } + + if (delta.amount1() > 0) { + pay(poolKey.currency0, from, address(poolManager), uint256(int256(delta.amount1()))); + poolManager.settle(poolKey.currency1); + } else if (delta.amount1() < 0) { + poolManager.take(poolKey.currency1, address(this), uint128(-delta.amount1())); + } + } + + function lockAcquired(bytes calldata data) external override returns (bytes memory) { + CallbackData memory callbackData = abi.decode(data, (CallbackData)); + if (callbackData.callbackType == CallbackType.AddLiquidity) { + AddLiquidityParams memory params = abi.decode(callbackData.params, (AddLiquidityParams)); + (uint128 liquidity, BalanceDelta delta) = addLiquidityCallback(params); + settleDeltas(callbackData.sender, params.poolKey, delta); + return abi.encode(liquidity, delta.amount0(), delta.amount1()); + } + + // TODO: handle decrease liquidity here + return abi.encode(0); + } +} diff --git a/contracts/base/Multicall.sol b/contracts/base/Multicall.sol new file mode 100644 index 00000000..bd926766 --- /dev/null +++ b/contracts/base/Multicall.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {IMulticall} from "../interfaces/IMulticall.sol"; + +/// @title Multicall +/// @notice Enables calling multiple methods in a single call to the contract +abstract contract Multicall is IMulticall { + /// @inheritdoc IMulticall + function multicall(bytes[] calldata data) public payable override returns (bytes[] memory results) { + results = new bytes[](data.length); + for (uint256 i = 0; i < data.length; i++) { + (bool success, bytes memory result) = address(this).delegatecall(data[i]); + + if (!success) { + // handle custom errors + if (result.length == 4) { + assembly { + revert(add(result, 0x20), mload(result)) + } + } + // Next 5 lines from https://ethereum.stackexchange.com/a/83577 + if (result.length < 68) revert(); + assembly { + result := add(result, 0x04) + } + revert(abi.decode(result, (string))); + } + + results[i] = result; + } + } +} diff --git a/contracts/base/PeripheryImmutableState.sol b/contracts/base/PeripheryImmutableState.sol new file mode 100644 index 00000000..c5a6d284 --- /dev/null +++ b/contracts/base/PeripheryImmutableState.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import {IPeripheryImmutableState} from "../interfaces/IPeripheryImmutableState.sol"; + +/// @title Immutable state +/// @notice Immutable state used by periphery contracts +abstract contract PeripheryImmutableState is IPeripheryImmutableState { + IPoolManager public immutable override poolManager; + + constructor(IPoolManager _poolManager) { + poolManager = _poolManager; + } +} diff --git a/contracts/base/PeripheryPayments.sol b/contracts/base/PeripheryPayments.sol new file mode 100644 index 00000000..f272da34 --- /dev/null +++ b/contracts/base/PeripheryPayments.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {IPeripheryPayments} from "../interfaces/IPeripheryPayments.sol"; + +abstract contract PeripheryPayments is IPeripheryPayments { + using CurrencyLibrary for Currency; + using SafeTransferLib for address; + using SafeTransferLib for ERC20; + + error InsufficientToken(); + error NativeTokenTransferFrom(); + + /// @inheritdoc IPeripheryPayments + function sweepToken(Currency currency, uint256 amountMinimum, address recipient) public payable override { + uint256 balanceCurrency = currency.balanceOfSelf(); + if (balanceCurrency < amountMinimum) revert InsufficientToken(); + + if (balanceCurrency > 0) { + currency.transfer(recipient, balanceCurrency); + } + } + + /// @param currency The currency to pay + /// @param payer The entity that must pay + /// @param recipient The entity that will receive payment + /// @param value The amount to pay + function pay(Currency currency, address payer, address recipient, uint256 value) internal { + if (payer == address(this)) { + // pay with tokens already in the contract (for the exact input multihop case) + currency.transfer(recipient, value); + } else { + if (currency.isNative()) revert NativeTokenTransferFrom(); + // pull payment + ERC20(Currency.unwrap(currency)).safeTransferFrom(payer, recipient, value); + } + } +} diff --git a/contracts/base/PeripheryValidation.sol b/contracts/base/PeripheryValidation.sol new file mode 100644 index 00000000..b8ea81d4 --- /dev/null +++ b/contracts/base/PeripheryValidation.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +abstract contract PeripheryValidation { + error TransactionTooOld(); + + modifier checkDeadline(uint256 deadline) { + if (block.timestamp > deadline) revert TransactionTooOld(); + _; + } +} diff --git a/contracts/base/SelfPermit.sol b/contracts/base/SelfPermit.sol new file mode 100644 index 00000000..40449636 --- /dev/null +++ b/contracts/base/SelfPermit.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; + +import {IERC20PermitAllowed} from "../interfaces/external/IERC20PermitAllowed.sol"; +import {ISelfPermit} from "../interfaces/ISelfPermit.sol"; + +/// @title Self Permit +/// @notice Functionality to call permit on any EIP-2612-compliant token for use in the route +/// @dev These functions are expected to be embedded in multicalls to allow EOAs to approve a contract and call a function +/// that requires an approval in a single transaction. +abstract contract SelfPermit is ISelfPermit { + /// @inheritdoc ISelfPermit + function selfPermit(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + public + payable + override + { + IERC20Permit(token).permit(msg.sender, address(this), value, deadline, v, r, s); + } + + /// @inheritdoc ISelfPermit + function selfPermitIfNecessary(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external + payable + override + { + if (IERC20(token).allowance(msg.sender, address(this)) < value) selfPermit(token, value, deadline, v, r, s); + } + + /// @inheritdoc ISelfPermit + function selfPermitAllowed(address token, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) + public + payable + override + { + IERC20PermitAllowed(token).permit(msg.sender, address(this), nonce, expiry, true, v, r, s); + } + + /// @inheritdoc ISelfPermit + function selfPermitAllowedIfNecessary(address token, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) + external + payable + override + { + if (IERC20(token).allowance(msg.sender, address(this)) < type(uint256).max) { + selfPermitAllowed(token, nonce, expiry, v, r, s); + } + } +} diff --git a/contracts/interfaces/IMulticall.sol b/contracts/interfaces/IMulticall.sol new file mode 100644 index 00000000..dfa9db24 --- /dev/null +++ b/contracts/interfaces/IMulticall.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +/// @title Multicall interface +/// @notice Enables calling multiple methods in a single call to the contract +interface IMulticall { + /// @notice Call multiple functions in the current contract and return the data from all of them if they all succeed + /// @dev The `msg.value` should not be trusted for any method callable from multicall. + /// @param data The encoded function data for each of the calls to make to this contract + /// @return results The results from each of the calls passed in via data + function multicall(bytes[] calldata data) external payable returns (bytes[] memory results); +} diff --git a/contracts/interfaces/INonfungiblePositionManagerV4.sol b/contracts/interfaces/INonfungiblePositionManagerV4.sol new file mode 100644 index 00000000..38dafbf5 --- /dev/null +++ b/contracts/interfaces/INonfungiblePositionManagerV4.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import {IERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; +import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; + +import {IPeripheryPayments} from "./IPeripheryPayments.sol"; +import {IPeripheryImmutableState} from "./IPeripheryImmutableState.sol"; + +/// @title Non-fungible token for positions +/// @notice Wraps Uniswap V4 positions in a non-fungible token interface which allows for them to be transferred +/// and authorized. +interface INonfungiblePositionManagerV4 is IPeripheryImmutableState, IERC721Metadata, IERC721Enumerable { + /// @notice Emitted when liquidity is increased for a position NFT + /// @dev Also emitted when a token is minted + /// @param tokenId The ID of the token for which liquidity was increased + /// @param liquidity The amount by which liquidity for the NFT position was increased + /// @param amount0 The amount of token0 that was paid for the increase in liquidity + /// @param amount1 The amount of token1 that was paid for the increase in liquidity + event IncreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); + /// @notice Emitted when liquidity is decreased for a position NFT + /// @param tokenId The ID of the token for which liquidity was decreased + /// @param liquidity The amount by which liquidity for the NFT position was decreased + /// @param amount0 The amount of token0 that was accounted for the decrease in liquidity + /// @param amount1 The amount of token1 that was accounted for the decrease in liquidity + event DecreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); + /// @notice Emitted when tokens are collected for a position NFT + /// @dev The amounts reported may not be exactly equivalent to the amounts transferred, due to rounding behavior + /// @param tokenId The ID of the token for which underlying tokens were collected + /// @param recipient The address of the account that received the collected tokens + /// @param amount0 The amount of token0 owed to the position that was collected + /// @param amount1 The amount of token1 owed to the position that was collected + event Collect(uint256 indexed tokenId, address recipient, uint256 amount0, uint256 amount1); + + /// @notice Returns the position information associated with a given token ID. + /// @dev Throws if the token ID is not valid. + /// @param tokenId The ID of the token that represents the position + /// @return nonce The nonce for permits + /// @return operator The address that is approved for spending + /// @return currency0 The address of the currency0 for a specific pool + /// @return currency1 The address of the currency1 for a specific pool + /// @return fee The fee associated with the pool + /// @return tickLower The lower end of the tick range for the position + /// @return tickUpper The higher end of the tick range for the position + /// @return liquidity The liquidity of the position + /// @return feeGrowthInside0LastX128 The fee growth of token0 as of the last action on the individual position + /// @return feeGrowthInside1LastX128 The fee growth of token1 as of the last action on the individual position + /// @return tokensOwed0 The uncollected amount of token0 owed to the position as of the last computation + /// @return tokensOwed1 The uncollected amount of token1 owed to the position as of the last computation + function positions(uint256 tokenId) + external + view + returns ( + uint96 nonce, + address operator, + Currency currency0, + Currency currency1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ); + + struct MintParams { + PoolKey poolKey; + int24 tickLower; + int24 tickUpper; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + address recipient; + uint256 deadline; + bytes hookData; + } + + /// @notice Creates a new position wrapped in a NFT + /// @dev Call this when the pool does exist and is initialized. Note that if the pool is created but not initialized + /// a method does not exist, i.e. the pool is assumed to be initialized. + /// @param params The params necessary to mint a position, encoded as `MintParams` in calldata + /// @return tokenId The ID of the token that represents the minted position + /// @return liquidity The amount of liquidity for this position + /// @return amount0 The amount of token0 + /// @return amount1 The amount of token1 + function mint(MintParams calldata params) + external + payable + returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); + + struct IncreaseLiquidityParams { + uint256 tokenId; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + /// @notice Increases the amount of liquidity in a position, with tokens paid by the `msg.sender` + /// @param params tokenId The ID of the token for which liquidity is being increased, + /// amount0Desired The desired amount of token0 to be spent, + /// amount1Desired The desired amount of token1 to be spent, + /// amount0Min The minimum amount of token0 to spend, which serves as a slippage check, + /// amount1Min The minimum amount of token1 to spend, which serves as a slippage check, + /// deadline The time by which the transaction must be included to effect the change + /// @return liquidity The new liquidity amount as a result of the increase + /// @return amount0 The amount of token0 to acheive resulting liquidity + /// @return amount1 The amount of token1 to acheive resulting liquidity + function increaseLiquidity(IncreaseLiquidityParams calldata params) + external + payable + returns (uint128 liquidity, uint256 amount0, uint256 amount1); + + struct DecreaseLiquidityParams { + uint256 tokenId; + uint128 liquidity; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + /// @notice Decreases the amount of liquidity in a position and accounts it to the position + /// @param params tokenId The ID of the token for which liquidity is being decreased, + /// amount The amount by which liquidity will be decreased, + /// amount0Min The minimum amount of token0 that should be accounted for the burned liquidity, + /// amount1Min The minimum amount of token1 that should be accounted for the burned liquidity, + /// deadline The time by which the transaction must be included to effect the change + /// @return amount0 The amount of token0 accounted to the position's tokens owed + /// @return amount1 The amount of token1 accounted to the position's tokens owed + function decreaseLiquidity(DecreaseLiquidityParams calldata params) + external + payable + returns (uint256 amount0, uint256 amount1); + + struct CollectParams { + uint256 tokenId; + address recipient; + uint128 amount0Max; + uint128 amount1Max; + } + + /// @notice Collects up to a maximum amount of fees owed to a specific position to the recipient + /// @param params tokenId The ID of the NFT for which tokens are being collected, + /// recipient The account that should receive the tokens, + /// amount0Max The maximum amount of token0 to collect, + /// amount1Max The maximum amount of token1 to collect + /// @return amount0 The amount of fees collected in token0 + /// @return amount1 The amount of fees collected in token1 + function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1); + + /// @notice Burns a token ID, which deletes it from the NFT contract. The token must have 0 liquidity and all tokens + /// must be collected first. + /// @param tokenId The ID of the token that is being burned + function burn(uint256 tokenId) external payable; +} diff --git a/contracts/interfaces/IPeripheryImmutableState.sol b/contracts/interfaces/IPeripheryImmutableState.sol new file mode 100644 index 00000000..9797b721 --- /dev/null +++ b/contracts/interfaces/IPeripheryImmutableState.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; + +/// @title Immutable state +/// @notice Functions that return immutable state +interface IPeripheryImmutableState { + /// @return Returns the pool manager + function poolManager() external view returns (IPoolManager); +} diff --git a/contracts/interfaces/IPeripheryPayments.sol b/contracts/interfaces/IPeripheryPayments.sol new file mode 100644 index 00000000..765b980f --- /dev/null +++ b/contracts/interfaces/IPeripheryPayments.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; + +/// @title Periphery Payments +/// @notice Functions to ease deposits and withdrawals of ETH +interface IPeripheryPayments { + // TODO: figure out if we still need unwrapWETH9 from v3? + + /// @notice Transfers the full amount of a token held by this contract to recipient + /// @dev The amountMinimum parameter prevents malicious contracts from stealing the token from users + /// @param currency The contract address of the token which will be transferred to `recipient` + /// @param amountMinimum The minimum amount of token required for a transfer + /// @param recipient The destination address of the token + function sweepToken(Currency currency, uint256 amountMinimum, address recipient) external payable; +} diff --git a/contracts/interfaces/ISelfPermit.sol b/contracts/interfaces/ISelfPermit.sol new file mode 100644 index 00000000..cb2445f5 --- /dev/null +++ b/contracts/interfaces/ISelfPermit.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.5.0; + +/// @title Self Permit +/// @notice Functionality to call permit on any EIP-2612-compliant token for use in the route +interface ISelfPermit { + /// @notice Permits this contract to spend a given token from `msg.sender` + /// @dev The `owner` is always msg.sender and the `spender` is always address(this). + /// @param token The address of the token spent + /// @param value The amount that can be spent of token + /// @param deadline A timestamp, the current blocktime must be less than or equal to this timestamp + /// @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` + function selfPermit(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external + payable; + + /// @notice Permits this contract to spend a given token from `msg.sender` + /// @dev The `owner` is always msg.sender and the `spender` is always address(this). + /// Can be used instead of #selfPermit to prevent calls from failing due to a frontrun of a call to #selfPermit + /// @param token The address of the token spent + /// @param value The amount that can be spent of token + /// @param deadline A timestamp, the current blocktime must be less than or equal to this timestamp + /// @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` + function selfPermitIfNecessary(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external + payable; + + /// @notice Permits this contract to spend the sender's tokens for permit signatures that have the `allowed` parameter + /// @dev The `owner` is always msg.sender and the `spender` is always address(this) + /// @param token The address of the token spent + /// @param nonce The current nonce of the owner + /// @param expiry The timestamp at which the permit is no longer valid + /// @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` + function selfPermitAllowed(address token, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) + external + payable; + + /// @notice Permits this contract to spend the sender's tokens for permit signatures that have the `allowed` parameter + /// @dev The `owner` is always msg.sender and the `spender` is always address(this) + /// Can be used instead of #selfPermitAllowed to prevent calls from failing due to a frontrun of a call to #selfPermitAllowed. + /// @param token The address of the token spent + /// @param nonce The current nonce of the owner + /// @param expiry The timestamp at which the permit is no longer valid + /// @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` + function selfPermitAllowedIfNecessary(address token, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) + external + payable; +} diff --git a/contracts/interfaces/external/IERC1271.sol b/contracts/interfaces/external/IERC1271.sol new file mode 100644 index 00000000..dcb30cb8 --- /dev/null +++ b/contracts/interfaces/external/IERC1271.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.5.0; + +/// @title Interface for verifying contract-based account signatures +/// @notice Interface that verifies provided signature for the data +/// @dev Interface defined by EIP-1271 +interface IERC1271 { + /// @notice Returns whether the provided signature is valid for the provided data + /// @dev MUST return the bytes4 magic value 0x1626ba7e when function passes. + /// MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5). + /// MUST allow external calls. + /// @param hash Hash of the data to be signed + /// @param signature Signature byte array associated with _data + /// @return magicValue The bytes4 magic value 0x1626ba7e + function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue); +} diff --git a/contracts/interfaces/external/IERC20PermitAllowed.sol b/contracts/interfaces/external/IERC20PermitAllowed.sol new file mode 100644 index 00000000..7f2cf657 --- /dev/null +++ b/contracts/interfaces/external/IERC20PermitAllowed.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.5.0; + +/// @title Interface for permit +/// @notice Interface used by DAI/CHAI for permit +interface IERC20PermitAllowed { + /// @notice Approve the spender to spend some tokens via the holder signature + /// @dev This is the permit interface used by DAI and CHAI + /// @param holder The address of the token holder, the token owner + /// @param spender The address of the token spender + /// @param nonce The holder's nonce, increases at each call to permit + /// @param expiry The timestamp at which the permit is no longer valid + /// @param allowed Boolean that sets approval amount, true for type(uint256).max and false for 0 + /// @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` + function permit( + address holder, + address spender, + uint256 nonce, + uint256 expiry, + bool allowed, + uint8 v, + bytes32 r, + bytes32 s + ) external; +}