From fe9305e16624cd1ad594da7147ed7909e180c5be Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Mon, 20 Nov 2023 21:37:12 -0500 Subject: [PATCH 01/31] add helper contracts and interfaces for position manager --- contracts/base/BlockTimestamp.sol | 12 +++ contracts/base/ERC721Permit.sol | 77 +++++++++++++++++++ contracts/base/Multicall.sol | 27 +++++++ contracts/base/PeripheryPayments.sol | 40 ++++++++++ contracts/base/PeripheryValidation.sol | 11 +++ contracts/base/SelfPermit.sol | 52 +++++++++++++ contracts/interfaces/IERC721Permit.sol | 27 +++++++ contracts/interfaces/IMulticall.sol | 12 +++ contracts/interfaces/IPeripheryPayments.sol | 20 +++++ contracts/interfaces/ISelfPermit.sol | 56 ++++++++++++++ contracts/interfaces/external/IERC1271.sol | 16 ++++ .../external/IERC20PermitAllowed.sol | 27 +++++++ contracts/libraries/ChainId.sol | 13 ++++ contracts/libraries/TransferHelper.sol | 9 +++ 14 files changed, 399 insertions(+) create mode 100644 contracts/base/BlockTimestamp.sol create mode 100644 contracts/base/ERC721Permit.sol create mode 100644 contracts/base/Multicall.sol create mode 100644 contracts/base/PeripheryPayments.sol create mode 100644 contracts/base/PeripheryValidation.sol create mode 100644 contracts/base/SelfPermit.sol create mode 100644 contracts/interfaces/IERC721Permit.sol create mode 100644 contracts/interfaces/IMulticall.sol create mode 100644 contracts/interfaces/IPeripheryPayments.sol create mode 100644 contracts/interfaces/ISelfPermit.sol create mode 100644 contracts/interfaces/external/IERC1271.sol create mode 100644 contracts/interfaces/external/IERC20PermitAllowed.sol create mode 100644 contracts/libraries/ChainId.sol diff --git a/contracts/base/BlockTimestamp.sol b/contracts/base/BlockTimestamp.sol new file mode 100644 index 00000000..5afe4b4b --- /dev/null +++ b/contracts/base/BlockTimestamp.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +/// @title Function for getting block timestamp +/// @dev Base contract that is overridden for tests +abstract contract BlockTimestamp { + /// @dev Method that exists purely to be overridden for tests + /// @return The current block timestamp + function _blockTimestamp() internal view virtual returns (uint256) { + return block.timestamp; + } +} diff --git a/contracts/base/ERC721Permit.sol b/contracts/base/ERC721Permit.sol new file mode 100644 index 00000000..17541f8c --- /dev/null +++ b/contracts/base/ERC721Permit.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {ChainId} from "../libraries/ChainId.sol"; +import {IERC1271} from "../interfaces/external/IERC1271.sol"; +import {IERC721Permit} from "../interfaces/IERC721Permit.sol"; +import {BlockTimestamp} from "./BlockTimestamp.sol"; + +/// @title ERC721 with permit +/// @notice Nonfungible tokens that support an approve via signature, i.e. permit +abstract contract ERC721Permit is BlockTimestamp, ERC721, IERC721Permit { + /// @dev Gets the current nonce for a token ID and then increments it, returning the original value + function _getAndIncrementNonce(uint256 tokenId) internal virtual returns (uint256); + + /// @dev The hash of the name used in the permit signature verification + bytes32 private immutable nameHash; + + /// @dev The hash of the version string used in the permit signature verification + bytes32 private immutable versionHash; + + /// @notice Computes the nameHash and versionHash + constructor(string memory name_, string memory symbol_, string memory version_) ERC721(name_, symbol_) { + nameHash = keccak256(bytes(name_)); + versionHash = keccak256(bytes(version_)); + } + + /// @inheritdoc IERC721Permit + function DOMAIN_SEPARATOR() public view override returns (bytes32) { + return keccak256( + abi.encode( + // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') + 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, + nameHash, + versionHash, + ChainId.get(), + address(this) + ) + ); + } + + /// @inheritdoc IERC721Permit + /// @dev Value is equal to keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)"); + bytes32 public constant override PERMIT_TYPEHASH = + 0x49ecf333e5b8c95c40fdafc95c1ad136e8914a8fb55e9dc8bb01eaa83a2df9ad; + + /// @inheritdoc IERC721Permit + function permit(address spender, uint256 tokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external + payable + override + { + require(_blockTimestamp() <= deadline, "Permit expired"); + + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256(abi.encode(PERMIT_TYPEHASH, spender, tokenId, _getAndIncrementNonce(tokenId), deadline)) + ) + ); + address owner = ownerOf(tokenId); + require(spender != owner, "ERC721Permit: approval to current owner"); + + if (owner.code.length > 0) { + require(IERC1271(owner).isValidSignature(digest, abi.encodePacked(r, s, v)) == 0x1626ba7e, "Unauthorized"); + } else { + address recoveredAddress = ecrecover(digest, v, r, s); + require(recoveredAddress != address(0), "Invalid signature"); + require(recoveredAddress == owner, "Unauthorized"); + } + + _approve(spender, tokenId); + } +} diff --git a/contracts/base/Multicall.sol b/contracts/base/Multicall.sol new file mode 100644 index 00000000..4abce294 --- /dev/null +++ b/contracts/base/Multicall.sol @@ -0,0 +1,27 @@ +// 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) { + // 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; + } + } +} \ No newline at end of file diff --git a/contracts/base/PeripheryPayments.sol b/contracts/base/PeripheryPayments.sol new file mode 100644 index 00000000..08b54d04 --- /dev/null +++ b/contracts/base/PeripheryPayments.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Minimal} from "@uniswap/v4-core/contracts/interfaces/external/IERC20Minimal.sol"; + +import {IPeripheryPayments} from "../interfaces/IPeripheryPayments.sol"; + +import "../libraries/TransferHelper.sol"; + +abstract contract PeripheryPayments is IPeripheryPayments { + /// @inheritdoc IPeripheryPayments + function sweepToken(address token, uint256 amountMinimum, address recipient) public payable override { + uint256 balanceToken = IERC20(token).balanceOf(address(this)); + require(balanceToken >= amountMinimum, "Insufficient token"); + + if (balanceToken > 0) { + TransferHelper.safeTransfer(IERC20Minimal(token), recipient, balanceToken); + } + } + + /// @inheritdoc IPeripheryPayments + function refundETH() external payable override { + if (address(this).balance > 0) TransferHelper.safeTransferETH(msg.sender, address(this).balance); + } + + /// @param token The token 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(address token, address payer, address recipient, uint256 value) internal { + if (payer == address(this)) { + // pay with tokens already in the contract (for the exact input multihop case) + TransferHelper.safeTransfer(IERC20Minimal(token), recipient, value); + } else { + // pull payment + TransferHelper.safeTransferFrom(IERC20Minimal(token), payer, recipient, value); + } + } +} diff --git a/contracts/base/PeripheryValidation.sol b/contracts/base/PeripheryValidation.sol new file mode 100644 index 00000000..e014477e --- /dev/null +++ b/contracts/base/PeripheryValidation.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {BlockTimestamp} from "./BlockTimestamp.sol"; + +abstract contract PeripheryValidation is BlockTimestamp { + modifier checkDeadline(uint256 deadline) { + require(_blockTimestamp() <= deadline, "Transaction too old"); + _; + } +} diff --git a/contracts/base/SelfPermit.sol b/contracts/base/SelfPermit.sol new file mode 100644 index 00000000..45578be5 --- /dev/null +++ b/contracts/base/SelfPermit.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.5.0; + +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/IERC721Permit.sol b/contracts/interfaces/IERC721Permit.sol new file mode 100644 index 00000000..f4fefa40 --- /dev/null +++ b/contracts/interfaces/IERC721Permit.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +/// @title ERC721 with permit +/// @notice Extension to ERC721 that includes a permit function for signature based approvals +interface IERC721Permit is IERC721 { + /// @notice The permit typehash used in the permit signature + /// @return The typehash for the permit + function PERMIT_TYPEHASH() external pure returns (bytes32); + + /// @notice The domain separator used in the permit signature + /// @return The domain seperator used in encoding of permit signature + function DOMAIN_SEPARATOR() external view returns (bytes32); + + /// @notice Approve of a specific token ID for spending by spender via signature + /// @param spender The account that is being approved + /// @param tokenId The ID of the token that is being approved for spending + /// @param deadline The deadline timestamp by which the call must be mined for the approve to work + /// @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 spender, uint256 tokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external + payable; +} diff --git a/contracts/interfaces/IMulticall.sol b/contracts/interfaces/IMulticall.sol new file mode 100644 index 00000000..b8628b9c --- /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); +} \ No newline at end of file diff --git a/contracts/interfaces/IPeripheryPayments.sol b/contracts/interfaces/IPeripheryPayments.sol new file mode 100644 index 00000000..03b06972 --- /dev/null +++ b/contracts/interfaces/IPeripheryPayments.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +/// @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 Refunds any ETH balance held by this contract to the `msg.sender` + /// @dev Useful for bundling with mint or increase liquidity that uses ether, or exact output swaps + /// that use ether for the input amount + function refundETH() external payable; + + /// @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 token 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(address token, 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; +} diff --git a/contracts/libraries/ChainId.sol b/contracts/libraries/ChainId.sol new file mode 100644 index 00000000..08d0f29a --- /dev/null +++ b/contracts/libraries/ChainId.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +/// @title Function for getting the current chain ID +library ChainId { + /// @dev Gets the current chain ID + /// @return chainId The current chain ID + function get() internal view returns (uint256 chainId) { + assembly { + chainId := chainid() + } + } +} diff --git a/contracts/libraries/TransferHelper.sol b/contracts/libraries/TransferHelper.sol index 5b1833a7..ecd679c5 100644 --- a/contracts/libraries/TransferHelper.sol +++ b/contracts/libraries/TransferHelper.sol @@ -51,4 +51,13 @@ library TransferHelper { address(token).call(abi.encodeWithSelector(IERC20Minimal.transferFrom.selector, from, to, value)); require(success && (data.length == 0 || abi.decode(data, (bool))), "STF"); } + + /// @notice Transfers ETH to the recipient address + /// @dev Fails with `STE` + /// @param to The destination of the transfer + /// @param value The value to be transferred + function safeTransferETH(address to, uint256 value) internal { + (bool success, ) = to.call{value: value}(new bytes(0)); + require(success, 'STE'); + } } From 0b01a0a9d1403eb347d92f711a1f858fda68c0d7 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Mon, 20 Nov 2023 21:43:36 -0500 Subject: [PATCH 02/31] run forge fmt --- contracts/base/Multicall.sol | 2 +- contracts/interfaces/IMulticall.sol | 2 +- contracts/libraries/TransferHelper.sol | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/base/Multicall.sol b/contracts/base/Multicall.sol index 4abce294..5eccb34c 100644 --- a/contracts/base/Multicall.sol +++ b/contracts/base/Multicall.sol @@ -24,4 +24,4 @@ abstract contract Multicall is IMulticall { results[i] = result; } } -} \ No newline at end of file +} diff --git a/contracts/interfaces/IMulticall.sol b/contracts/interfaces/IMulticall.sol index b8628b9c..dfa9db24 100644 --- a/contracts/interfaces/IMulticall.sol +++ b/contracts/interfaces/IMulticall.sol @@ -9,4 +9,4 @@ interface IMulticall { /// @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); -} \ No newline at end of file +} diff --git a/contracts/libraries/TransferHelper.sol b/contracts/libraries/TransferHelper.sol index ecd679c5..21209410 100644 --- a/contracts/libraries/TransferHelper.sol +++ b/contracts/libraries/TransferHelper.sol @@ -57,7 +57,7 @@ library TransferHelper { /// @param to The destination of the transfer /// @param value The value to be transferred function safeTransferETH(address to, uint256 value) internal { - (bool success, ) = to.call{value: value}(new bytes(0)); - require(success, 'STE'); + (bool success,) = to.call{value: value}(new bytes(0)); + require(success, "STE"); } } From 74ad1a2530d1fc1f31185a8af19eb25af22a9aaf Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Tue, 21 Nov 2023 14:34:43 -0500 Subject: [PATCH 03/31] remove custom timestamp getter --- contracts/base/BlockTimestamp.sol | 12 ------------ contracts/base/ERC721Permit.sol | 5 ++--- contracts/base/PeripheryValidation.sol | 6 ++---- 3 files changed, 4 insertions(+), 19 deletions(-) delete mode 100644 contracts/base/BlockTimestamp.sol diff --git a/contracts/base/BlockTimestamp.sol b/contracts/base/BlockTimestamp.sol deleted file mode 100644 index 5afe4b4b..00000000 --- a/contracts/base/BlockTimestamp.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -/// @title Function for getting block timestamp -/// @dev Base contract that is overridden for tests -abstract contract BlockTimestamp { - /// @dev Method that exists purely to be overridden for tests - /// @return The current block timestamp - function _blockTimestamp() internal view virtual returns (uint256) { - return block.timestamp; - } -} diff --git a/contracts/base/ERC721Permit.sol b/contracts/base/ERC721Permit.sol index 17541f8c..015f8394 100644 --- a/contracts/base/ERC721Permit.sol +++ b/contracts/base/ERC721Permit.sol @@ -7,11 +7,10 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {ChainId} from "../libraries/ChainId.sol"; import {IERC1271} from "../interfaces/external/IERC1271.sol"; import {IERC721Permit} from "../interfaces/IERC721Permit.sol"; -import {BlockTimestamp} from "./BlockTimestamp.sol"; /// @title ERC721 with permit /// @notice Nonfungible tokens that support an approve via signature, i.e. permit -abstract contract ERC721Permit is BlockTimestamp, ERC721, IERC721Permit { +abstract contract ERC721Permit is ERC721, IERC721Permit { /// @dev Gets the current nonce for a token ID and then increments it, returning the original value function _getAndIncrementNonce(uint256 tokenId) internal virtual returns (uint256); @@ -52,7 +51,7 @@ abstract contract ERC721Permit is BlockTimestamp, ERC721, IERC721Permit { payable override { - require(_blockTimestamp() <= deadline, "Permit expired"); + require(block.timestamp <= deadline, "Permit expired"); bytes32 digest = keccak256( abi.encodePacked( diff --git a/contracts/base/PeripheryValidation.sol b/contracts/base/PeripheryValidation.sol index e014477e..3f8a8b6d 100644 --- a/contracts/base/PeripheryValidation.sol +++ b/contracts/base/PeripheryValidation.sol @@ -1,11 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {BlockTimestamp} from "./BlockTimestamp.sol"; - -abstract contract PeripheryValidation is BlockTimestamp { +abstract contract PeripheryValidation { modifier checkDeadline(uint256 deadline) { - require(_blockTimestamp() <= deadline, "Transaction too old"); + require(block.timestamp <= deadline, "Transaction too old"); _; } } From 30479c695429acfc7015cea313c4e91d10324779 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Tue, 21 Nov 2023 14:55:06 -0500 Subject: [PATCH 04/31] use custom errors and IER1271.isValidSignature.selector --- contracts/base/ERC721Permit.sol | 19 +++++++++++++------ contracts/base/PeripheryPayments.sol | 4 +++- contracts/base/PeripheryValidation.sol | 4 +++- contracts/libraries/TransferHelper.sol | 10 +++++++--- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/contracts/base/ERC721Permit.sol b/contracts/base/ERC721Permit.sol index 015f8394..f9af9273 100644 --- a/contracts/base/ERC721Permit.sol +++ b/contracts/base/ERC721Permit.sol @@ -1,13 +1,18 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {ChainId} from "../libraries/ChainId.sol"; import {IERC1271} from "../interfaces/external/IERC1271.sol"; import {IERC721Permit} from "../interfaces/IERC721Permit.sol"; +error PermitExpired(); +error InvalidSignature(); +error ApprovalToOwner(); +error Unauthorized(); + /// @title ERC721 with permit /// @notice Nonfungible tokens that support an approve via signature, i.e. permit abstract contract ERC721Permit is ERC721, IERC721Permit { @@ -51,7 +56,7 @@ abstract contract ERC721Permit is ERC721, IERC721Permit { payable override { - require(block.timestamp <= deadline, "Permit expired"); + if (block.timestamp > deadline) revert PermitExpired(); bytes32 digest = keccak256( abi.encodePacked( @@ -61,14 +66,16 @@ abstract contract ERC721Permit is ERC721, IERC721Permit { ) ); address owner = ownerOf(tokenId); - require(spender != owner, "ERC721Permit: approval to current owner"); + if (spender == owner) revert ApprovalToOwner(); if (owner.code.length > 0) { - require(IERC1271(owner).isValidSignature(digest, abi.encodePacked(r, s, v)) == 0x1626ba7e, "Unauthorized"); + if (IERC1271(owner).isValidSignature(digest, abi.encodePacked(r, s, v)) != IERC1271.isValidSignature.selector) { + revert Unauthorized(); + } } else { address recoveredAddress = ecrecover(digest, v, r, s); - require(recoveredAddress != address(0), "Invalid signature"); - require(recoveredAddress == owner, "Unauthorized"); + if (recoveredAddress == address(0)) revert InvalidSignature(); + if (recoveredAddress != owner) revert Unauthorized(); } _approve(spender, tokenId); diff --git a/contracts/base/PeripheryPayments.sol b/contracts/base/PeripheryPayments.sol index 08b54d04..2fa42cd0 100644 --- a/contracts/base/PeripheryPayments.sol +++ b/contracts/base/PeripheryPayments.sol @@ -8,11 +8,13 @@ import {IPeripheryPayments} from "../interfaces/IPeripheryPayments.sol"; import "../libraries/TransferHelper.sol"; +error InsufficientToken(); + abstract contract PeripheryPayments is IPeripheryPayments { /// @inheritdoc IPeripheryPayments function sweepToken(address token, uint256 amountMinimum, address recipient) public payable override { uint256 balanceToken = IERC20(token).balanceOf(address(this)); - require(balanceToken >= amountMinimum, "Insufficient token"); + if (balanceToken < amountMinimum) revert InsufficientToken(); if (balanceToken > 0) { TransferHelper.safeTransfer(IERC20Minimal(token), recipient, balanceToken); diff --git a/contracts/base/PeripheryValidation.sol b/contracts/base/PeripheryValidation.sol index 3f8a8b6d..009c0129 100644 --- a/contracts/base/PeripheryValidation.sol +++ b/contracts/base/PeripheryValidation.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; +error TransactionTooOld(); + abstract contract PeripheryValidation { modifier checkDeadline(uint256 deadline) { - require(block.timestamp <= deadline, "Transaction too old"); + if (block.timestamp > deadline) revert TransactionTooOld(); _; } } diff --git a/contracts/libraries/TransferHelper.sol b/contracts/libraries/TransferHelper.sol index 21209410..f7a27e04 100644 --- a/contracts/libraries/TransferHelper.sol +++ b/contracts/libraries/TransferHelper.sol @@ -3,6 +3,10 @@ pragma solidity ^0.8.15; import {IERC20Minimal} from "@uniswap/v4-core/contracts/interfaces/external/IERC20Minimal.sol"; +error TransferFailed(); +error STF(); +error STE(); + /// @title TransferHelper /// @notice Contains helper methods for interacting with ERC20 tokens that do not consistently return true/false /// @dev implementation from https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeTransferLib.sol#L63 @@ -37,7 +41,7 @@ library TransferHelper { ) } - require(success, "TRANSFER_FAILED"); + if (!success) revert TransferFailed(); } /// @notice Transfers tokens from from to a recipient @@ -49,7 +53,7 @@ library TransferHelper { function safeTransferFrom(IERC20Minimal token, address from, address to, uint256 value) internal { (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(IERC20Minimal.transferFrom.selector, from, to, value)); - require(success && (data.length == 0 || abi.decode(data, (bool))), "STF"); + if (!success || !(data.length == 0 || abi.decode(data, (bool)))) revert STF(); } /// @notice Transfers ETH to the recipient address @@ -58,6 +62,6 @@ library TransferHelper { /// @param value The value to be transferred function safeTransferETH(address to, uint256 value) internal { (bool success,) = to.call{value: value}(new bytes(0)); - require(success, "STE"); + if (!success) revert STE(); } } From 349ddb8c3a71bfdf7501953e7e70045c76644a68 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Tue, 21 Nov 2023 15:02:06 -0500 Subject: [PATCH 05/31] use using .. for syntax for TransferHelper --- contracts/base/ERC721Permit.sol | 5 ++++- contracts/base/PeripheryPayments.sol | 11 +++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/contracts/base/ERC721Permit.sol b/contracts/base/ERC721Permit.sol index f9af9273..707e381b 100644 --- a/contracts/base/ERC721Permit.sol +++ b/contracts/base/ERC721Permit.sol @@ -69,7 +69,10 @@ abstract contract ERC721Permit is ERC721, IERC721Permit { if (spender == owner) revert ApprovalToOwner(); if (owner.code.length > 0) { - if (IERC1271(owner).isValidSignature(digest, abi.encodePacked(r, s, v)) != IERC1271.isValidSignature.selector) { + if ( + IERC1271(owner).isValidSignature(digest, abi.encodePacked(r, s, v)) + != IERC1271.isValidSignature.selector + ) { revert Unauthorized(); } } else { diff --git a/contracts/base/PeripheryPayments.sol b/contracts/base/PeripheryPayments.sol index 2fa42cd0..4714bf42 100644 --- a/contracts/base/PeripheryPayments.sol +++ b/contracts/base/PeripheryPayments.sol @@ -8,6 +8,9 @@ import {IPeripheryPayments} from "../interfaces/IPeripheryPayments.sol"; import "../libraries/TransferHelper.sol"; +using TransferHelper for address; +using TransferHelper for IERC20Minimal; + error InsufficientToken(); abstract contract PeripheryPayments is IPeripheryPayments { @@ -17,13 +20,13 @@ abstract contract PeripheryPayments is IPeripheryPayments { if (balanceToken < amountMinimum) revert InsufficientToken(); if (balanceToken > 0) { - TransferHelper.safeTransfer(IERC20Minimal(token), recipient, balanceToken); + IERC20Minimal(token).safeTransfer(recipient, balanceToken); } } /// @inheritdoc IPeripheryPayments function refundETH() external payable override { - if (address(this).balance > 0) TransferHelper.safeTransferETH(msg.sender, address(this).balance); + if (address(this).balance > 0) msg.sender.safeTransferETH(address(this).balance); } /// @param token The token to pay @@ -33,10 +36,10 @@ abstract contract PeripheryPayments is IPeripheryPayments { function pay(address token, address payer, address recipient, uint256 value) internal { if (payer == address(this)) { // pay with tokens already in the contract (for the exact input multihop case) - TransferHelper.safeTransfer(IERC20Minimal(token), recipient, value); + IERC20Minimal(token).safeTransfer(recipient, value); } else { // pull payment - TransferHelper.safeTransferFrom(IERC20Minimal(token), payer, recipient, value); + IERC20Minimal(token).safeTransferFrom(payer, recipient, value); } } } From 906fb656cbbc52f0d75e323c1ba9ad356ea9e393 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Tue, 21 Nov 2023 15:06:27 -0500 Subject: [PATCH 06/31] add erics custom error handler --- contracts/base/Multicall.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/base/Multicall.sol b/contracts/base/Multicall.sol index 5eccb34c..bd926766 100644 --- a/contracts/base/Multicall.sol +++ b/contracts/base/Multicall.sol @@ -13,6 +13,12 @@ abstract contract Multicall is IMulticall { (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 { From 0152e5c80a7b74e505a148f43537846db3a98f2a Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Tue, 21 Nov 2023 17:35:33 -0500 Subject: [PATCH 07/31] remove ChainId library --- contracts/base/ERC721Permit.sol | 3 +-- contracts/libraries/ChainId.sol | 13 ------------- 2 files changed, 1 insertion(+), 15 deletions(-) delete mode 100644 contracts/libraries/ChainId.sol diff --git a/contracts/base/ERC721Permit.sol b/contracts/base/ERC721Permit.sol index 707e381b..5a0f70eb 100644 --- a/contracts/base/ERC721Permit.sol +++ b/contracts/base/ERC721Permit.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.19; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {ChainId} from "../libraries/ChainId.sol"; import {IERC1271} from "../interfaces/external/IERC1271.sol"; import {IERC721Permit} from "../interfaces/IERC721Permit.sol"; @@ -39,7 +38,7 @@ abstract contract ERC721Permit is ERC721, IERC721Permit { 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, nameHash, versionHash, - ChainId.get(), + block.chainid, address(this) ) ); diff --git a/contracts/libraries/ChainId.sol b/contracts/libraries/ChainId.sol deleted file mode 100644 index 08d0f29a..00000000 --- a/contracts/libraries/ChainId.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -/// @title Function for getting the current chain ID -library ChainId { - /// @dev Gets the current chain ID - /// @return chainId The current chain ID - function get() internal view returns (uint256 chainId) { - assembly { - chainId := chainid() - } - } -} From e5fc9dd51dee8354b7b377988af0ab4b7aeb7923 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Tue, 21 Nov 2023 17:46:04 -0500 Subject: [PATCH 08/31] use currency library to combine sweepETH and sweepToken --- contracts/base/PeripheryPayments.sol | 21 ++++++++------------- contracts/interfaces/IPeripheryPayments.sol | 11 ++++------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/contracts/base/PeripheryPayments.sol b/contracts/base/PeripheryPayments.sol index 4714bf42..ca83c0cc 100644 --- a/contracts/base/PeripheryPayments.sol +++ b/contracts/base/PeripheryPayments.sol @@ -3,11 +3,11 @@ pragma solidity ^0.8.19; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Minimal} from "@uniswap/v4-core/contracts/interfaces/external/IERC20Minimal.sol"; - +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; import {IPeripheryPayments} from "../interfaces/IPeripheryPayments.sol"; +import {TransferHelper} from "../libraries/TransferHelper.sol"; -import "../libraries/TransferHelper.sol"; - +using CurrencyLibrary for Currency; using TransferHelper for address; using TransferHelper for IERC20Minimal; @@ -15,20 +15,15 @@ error InsufficientToken(); abstract contract PeripheryPayments is IPeripheryPayments { /// @inheritdoc IPeripheryPayments - function sweepToken(address token, uint256 amountMinimum, address recipient) public payable override { - uint256 balanceToken = IERC20(token).balanceOf(address(this)); - if (balanceToken < amountMinimum) revert InsufficientToken(); + function sweepToken(Currency currency, uint256 amountMinimum, address recipient) public payable override { + uint256 balanceCurrency = currency.balanceOfSelf(); + if (balanceCurrency < amountMinimum) revert InsufficientToken(); - if (balanceToken > 0) { - IERC20Minimal(token).safeTransfer(recipient, balanceToken); + if (balanceCurrency > 0) { + currency.transfer(recipient, balanceCurrency); } } - /// @inheritdoc IPeripheryPayments - function refundETH() external payable override { - if (address(this).balance > 0) msg.sender.safeTransferETH(address(this).balance); - } - /// @param token The token to pay /// @param payer The entity that must pay /// @param recipient The entity that will receive payment diff --git a/contracts/interfaces/IPeripheryPayments.sol b/contracts/interfaces/IPeripheryPayments.sol index 03b06972..765b980f 100644 --- a/contracts/interfaces/IPeripheryPayments.sol +++ b/contracts/interfaces/IPeripheryPayments.sol @@ -1,20 +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 Refunds any ETH balance held by this contract to the `msg.sender` - /// @dev Useful for bundling with mint or increase liquidity that uses ether, or exact output swaps - /// that use ether for the input amount - function refundETH() external payable; - /// @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 token The contract address of the token which will be transferred to `recipient` + /// @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(address token, uint256 amountMinimum, address recipient) external payable; + function sweepToken(Currency currency, uint256 amountMinimum, address recipient) external payable; } From 54d55fd51b1019d8fb5a045684d4d5cf2c691c86 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Tue, 21 Nov 2023 17:54:24 -0500 Subject: [PATCH 09/31] use Currency library for pay method --- contracts/base/PeripheryPayments.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/base/PeripheryPayments.sol b/contracts/base/PeripheryPayments.sol index ca83c0cc..13416416 100644 --- a/contracts/base/PeripheryPayments.sol +++ b/contracts/base/PeripheryPayments.sol @@ -12,6 +12,7 @@ using TransferHelper for address; using TransferHelper for IERC20Minimal; error InsufficientToken(); +error NativeTokenTransferFrom(); abstract contract PeripheryPayments is IPeripheryPayments { /// @inheritdoc IPeripheryPayments @@ -24,17 +25,18 @@ abstract contract PeripheryPayments is IPeripheryPayments { } } - /// @param token The token to pay + /// @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(address token, address payer, address recipient, uint256 value) internal { + 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) - IERC20Minimal(token).safeTransfer(recipient, value); + currency.transfer(recipient, value); } else { + if (currency.isNative()) revert NativeTokenTransferFrom(); // pull payment - IERC20Minimal(token).safeTransferFrom(payer, recipient, value); + IERC20Minimal(Currency.unwrap(currency)).safeTransferFrom(payer, recipient, value); } } } From a2200ac70208f4a580771c09287ebe88b3135983 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Tue, 21 Nov 2023 18:04:54 -0500 Subject: [PATCH 10/31] use solmate SafeTransferLib instead --- contracts/base/PeripheryPayments.sol | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/contracts/base/PeripheryPayments.sol b/contracts/base/PeripheryPayments.sol index 13416416..cfaffb3e 100644 --- a/contracts/base/PeripheryPayments.sol +++ b/contracts/base/PeripheryPayments.sol @@ -1,15 +1,14 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/contracts/interfaces/external/IERC20Minimal.sol"; +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"; -import {TransferHelper} from "../libraries/TransferHelper.sol"; using CurrencyLibrary for Currency; -using TransferHelper for address; -using TransferHelper for IERC20Minimal; +using SafeTransferLib for address; +using SafeTransferLib for ERC20; error InsufficientToken(); error NativeTokenTransferFrom(); @@ -36,7 +35,7 @@ abstract contract PeripheryPayments is IPeripheryPayments { } else { if (currency.isNative()) revert NativeTokenTransferFrom(); // pull payment - IERC20Minimal(Currency.unwrap(currency)).safeTransferFrom(payer, recipient, value); + ERC20(Currency.unwrap(currency)).safeTransferFrom(payer, recipient, value); } } } From b4f84eeae8a55a5d0329509d97494120c9355747 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Tue, 21 Nov 2023 18:06:43 -0500 Subject: [PATCH 11/31] remove erc721permit for now --- contracts/base/ERC721Permit.sol | 85 -------------------------- contracts/interfaces/IERC721Permit.sol | 27 -------- 2 files changed, 112 deletions(-) delete mode 100644 contracts/base/ERC721Permit.sol delete mode 100644 contracts/interfaces/IERC721Permit.sol diff --git a/contracts/base/ERC721Permit.sol b/contracts/base/ERC721Permit.sol deleted file mode 100644 index 5a0f70eb..00000000 --- a/contracts/base/ERC721Permit.sol +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; - -import {IERC1271} from "../interfaces/external/IERC1271.sol"; -import {IERC721Permit} from "../interfaces/IERC721Permit.sol"; - -error PermitExpired(); -error InvalidSignature(); -error ApprovalToOwner(); -error Unauthorized(); - -/// @title ERC721 with permit -/// @notice Nonfungible tokens that support an approve via signature, i.e. permit -abstract contract ERC721Permit is ERC721, IERC721Permit { - /// @dev Gets the current nonce for a token ID and then increments it, returning the original value - function _getAndIncrementNonce(uint256 tokenId) internal virtual returns (uint256); - - /// @dev The hash of the name used in the permit signature verification - bytes32 private immutable nameHash; - - /// @dev The hash of the version string used in the permit signature verification - bytes32 private immutable versionHash; - - /// @notice Computes the nameHash and versionHash - constructor(string memory name_, string memory symbol_, string memory version_) ERC721(name_, symbol_) { - nameHash = keccak256(bytes(name_)); - versionHash = keccak256(bytes(version_)); - } - - /// @inheritdoc IERC721Permit - function DOMAIN_SEPARATOR() public view override returns (bytes32) { - return keccak256( - abi.encode( - // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') - 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, - nameHash, - versionHash, - block.chainid, - address(this) - ) - ); - } - - /// @inheritdoc IERC721Permit - /// @dev Value is equal to keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)"); - bytes32 public constant override PERMIT_TYPEHASH = - 0x49ecf333e5b8c95c40fdafc95c1ad136e8914a8fb55e9dc8bb01eaa83a2df9ad; - - /// @inheritdoc IERC721Permit - function permit(address spender, uint256 tokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) - external - payable - override - { - if (block.timestamp > deadline) revert PermitExpired(); - - bytes32 digest = keccak256( - abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR(), - keccak256(abi.encode(PERMIT_TYPEHASH, spender, tokenId, _getAndIncrementNonce(tokenId), deadline)) - ) - ); - address owner = ownerOf(tokenId); - if (spender == owner) revert ApprovalToOwner(); - - if (owner.code.length > 0) { - if ( - IERC1271(owner).isValidSignature(digest, abi.encodePacked(r, s, v)) - != IERC1271.isValidSignature.selector - ) { - revert Unauthorized(); - } - } else { - address recoveredAddress = ecrecover(digest, v, r, s); - if (recoveredAddress == address(0)) revert InvalidSignature(); - if (recoveredAddress != owner) revert Unauthorized(); - } - - _approve(spender, tokenId); - } -} diff --git a/contracts/interfaces/IERC721Permit.sol b/contracts/interfaces/IERC721Permit.sol deleted file mode 100644 index f4fefa40..00000000 --- a/contracts/interfaces/IERC721Permit.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; - -/// @title ERC721 with permit -/// @notice Extension to ERC721 that includes a permit function for signature based approvals -interface IERC721Permit is IERC721 { - /// @notice The permit typehash used in the permit signature - /// @return The typehash for the permit - function PERMIT_TYPEHASH() external pure returns (bytes32); - - /// @notice The domain separator used in the permit signature - /// @return The domain seperator used in encoding of permit signature - function DOMAIN_SEPARATOR() external view returns (bytes32); - - /// @notice Approve of a specific token ID for spending by spender via signature - /// @param spender The account that is being approved - /// @param tokenId The ID of the token that is being approved for spending - /// @param deadline The deadline timestamp by which the call must be mined for the approve to work - /// @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 spender, uint256 tokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) - external - payable; -} From ad5c503c89dcd2d55e023e86a8b5d9bba6b20885 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Wed, 22 Nov 2023 12:39:08 -0500 Subject: [PATCH 12/31] revert changes to TransferHelper --- contracts/libraries/TransferHelper.sol | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/contracts/libraries/TransferHelper.sol b/contracts/libraries/TransferHelper.sol index f7a27e04..5b1833a7 100644 --- a/contracts/libraries/TransferHelper.sol +++ b/contracts/libraries/TransferHelper.sol @@ -3,10 +3,6 @@ pragma solidity ^0.8.15; import {IERC20Minimal} from "@uniswap/v4-core/contracts/interfaces/external/IERC20Minimal.sol"; -error TransferFailed(); -error STF(); -error STE(); - /// @title TransferHelper /// @notice Contains helper methods for interacting with ERC20 tokens that do not consistently return true/false /// @dev implementation from https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeTransferLib.sol#L63 @@ -41,7 +37,7 @@ library TransferHelper { ) } - if (!success) revert TransferFailed(); + require(success, "TRANSFER_FAILED"); } /// @notice Transfers tokens from from to a recipient @@ -53,15 +49,6 @@ library TransferHelper { function safeTransferFrom(IERC20Minimal token, address from, address to, uint256 value) internal { (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(IERC20Minimal.transferFrom.selector, from, to, value)); - if (!success || !(data.length == 0 || abi.decode(data, (bool)))) revert STF(); - } - - /// @notice Transfers ETH to the recipient address - /// @dev Fails with `STE` - /// @param to The destination of the transfer - /// @param value The value to be transferred - function safeTransferETH(address to, uint256 value) internal { - (bool success,) = to.call{value: value}(new bytes(0)); - if (!success) revert STE(); + require(success && (data.length == 0 || abi.decode(data, (bool))), "STF"); } } From 04a7a1ddaceaff5891bbfd570b3b32ad0a63d17f Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Wed, 22 Nov 2023 12:55:01 -0500 Subject: [PATCH 13/31] put using .. for in contract header --- contracts/base/PeripheryPayments.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/base/PeripheryPayments.sol b/contracts/base/PeripheryPayments.sol index cfaffb3e..9bce10e8 100644 --- a/contracts/base/PeripheryPayments.sol +++ b/contracts/base/PeripheryPayments.sol @@ -6,14 +6,14 @@ import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Curren import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {IPeripheryPayments} from "../interfaces/IPeripheryPayments.sol"; -using CurrencyLibrary for Currency; -using SafeTransferLib for address; -using SafeTransferLib for ERC20; - error InsufficientToken(); error NativeTokenTransferFrom(); abstract contract PeripheryPayments is IPeripheryPayments { + using CurrencyLibrary for Currency; + using SafeTransferLib for address; + using SafeTransferLib for ERC20; + /// @inheritdoc IPeripheryPayments function sweepToken(Currency currency, uint256 amountMinimum, address recipient) public payable override { uint256 balanceCurrency = currency.balanceOfSelf(); From f9d830e7158970fb5cfb279b069224c846c09b86 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Wed, 22 Nov 2023 12:57:02 -0500 Subject: [PATCH 14/31] move custom errors inside contracts --- contracts/base/PeripheryPayments.sol | 6 +++--- contracts/base/PeripheryValidation.sol | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/base/PeripheryPayments.sol b/contracts/base/PeripheryPayments.sol index 9bce10e8..18deaae8 100644 --- a/contracts/base/PeripheryPayments.sol +++ b/contracts/base/PeripheryPayments.sol @@ -6,14 +6,14 @@ import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Curren import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {IPeripheryPayments} from "../interfaces/IPeripheryPayments.sol"; -error InsufficientToken(); -error NativeTokenTransferFrom(); - 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(); diff --git a/contracts/base/PeripheryValidation.sol b/contracts/base/PeripheryValidation.sol index 009c0129..eaa1b18e 100644 --- a/contracts/base/PeripheryValidation.sol +++ b/contracts/base/PeripheryValidation.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -error TransactionTooOld(); - abstract contract PeripheryValidation { + error TransactionTooOld(); + modifier checkDeadline(uint256 deadline) { if (block.timestamp > deadline) revert TransactionTooOld(); _; From 0d87708360936a312f0d1f4d7938cdd56201d4f4 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Wed, 22 Nov 2023 13:33:37 -0500 Subject: [PATCH 15/31] run format --- contracts/base/PeripheryPayments.sol | 2 +- contracts/base/PeripheryValidation.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/base/PeripheryPayments.sol b/contracts/base/PeripheryPayments.sol index 18deaae8..f272da34 100644 --- a/contracts/base/PeripheryPayments.sol +++ b/contracts/base/PeripheryPayments.sol @@ -12,7 +12,7 @@ abstract contract PeripheryPayments is IPeripheryPayments { using SafeTransferLib for ERC20; error InsufficientToken(); - error NativeTokenTransferFrom(); + error NativeTokenTransferFrom(); /// @inheritdoc IPeripheryPayments function sweepToken(Currency currency, uint256 amountMinimum, address recipient) public payable override { diff --git a/contracts/base/PeripheryValidation.sol b/contracts/base/PeripheryValidation.sol index eaa1b18e..b8ea81d4 100644 --- a/contracts/base/PeripheryValidation.sol +++ b/contracts/base/PeripheryValidation.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.19; abstract contract PeripheryValidation { error TransactionTooOld(); - + modifier checkDeadline(uint256 deadline) { if (block.timestamp > deadline) revert TransactionTooOld(); _; From 02a5cd277a2b8ffed856ffb230d1bb479c2231ff Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Wed, 22 Nov 2023 13:35:01 -0500 Subject: [PATCH 16/31] update version --- contracts/base/SelfPermit.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/base/SelfPermit.sol b/contracts/base/SelfPermit.sol index 45578be5..40449636 100644 --- a/contracts/base/SelfPermit.sol +++ b/contracts/base/SelfPermit.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.5.0; +pragma solidity ^0.8.19; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; From d261d58fc12f33facdba7ed18d6da2ddb38bab08 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Mon, 20 Nov 2023 21:59:58 -0500 Subject: [PATCH 17/31] add v4 position manager contracts --- contracts/NonfungiblePositionManagerV4.sol | 211 ++++++++++++++++++ .../INonfungiblePositionManagerV4.sol | 170 ++++++++++++++ 2 files changed, 381 insertions(+) create mode 100644 contracts/NonfungiblePositionManagerV4.sol create mode 100644 contracts/interfaces/INonfungiblePositionManagerV4.sol diff --git a/contracts/NonfungiblePositionManagerV4.sol b/contracts/NonfungiblePositionManagerV4.sol new file mode 100644 index 00000000..1a437f79 --- /dev/null +++ b/contracts/NonfungiblePositionManagerV4.sol @@ -0,0 +1,211 @@ +// 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 {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 {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; + +import {INonfungiblePositionManagerV4} from "./interfaces/INonfungiblePositionManagerV4.sol"; +import {ERC721Permit} from "./base/ERC721Permit.sol"; +import {PeripheryValidation} from "./base/PeripheryValidation.sol"; +import {PeripheryPayments} from "./base/PeripheryPayments.sol"; +import {SelfPermit} from "./base/SelfPermit.sol"; +import {Multicall} from "./base/Multicall.sol"; + +contract NonfungiblePositionManagerV4 is + INonfungiblePositionManagerV4, + ERC721Permit, + PeripheryValidation, + PeripheryPayments, + SelfPermit, + Multicall +{ + IPoolManager public immutable override poolManager; + + // details about the Uniswap position + struct Position { + // 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 + bytes32 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 => Position) 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; + + // TODO: does it still need WETH address in the constructor here? + // TODO: use ERC721Permit2 here + constructor(IPoolManager _poolManager, address _tokenDescriptor_) + ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V4-POS", "1") + { + poolManager = _poolManager; + _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 + ) + { + Position memory position = _positions[tokenId]; + require(position.poolId != 0, "Invalid token ID"); + PoolKey memory poolKey = _poolIdToPoolKey[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 createAndInitializePoolIfNecessary(PoolKey memory poolkey, uint160 sqrtPriceX96, bytes memory initData) + external + payable + { + // TODO: implement this + } + + /// @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 + } + + modifier isAuthorizedForToken(uint256 tokenId) { + require(_isApprovedOrOwner(msg.sender, tokenId), "Not approved"); + _; + } + + function tokenURI(uint256 tokenId) public view override(ERC721, IERC721Metadata) returns (string memory) { + // 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) { + Position storage position = _positions[tokenId]; + require(position.liquidity == 0 && position.tokensOwed0 == 0 && position.tokensOwed1 == 0, "Not cleared"); + delete _positions[tokenId]; + _burn(tokenId); + } + + function _getAndIncrementNonce(uint256 tokenId) internal override returns (uint256) { + return uint256(_positions[tokenId].nonce++); + } + + /// @inheritdoc IERC721 + function getApproved(uint256 tokenId) public view override(ERC721, IERC721) returns (address) { + require(_exists(tokenId), "ERC721: approved query for nonexistent token"); + + 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); + } + + /// @inheritdoc IERC721Enumerable + function tokenByIndex(uint256 index) external view returns (uint256) { + // TODO: implement this + } + + /// @inheritdoc IERC721Enumerable + function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256) { + // TODO: implement this + } + + /// @inheritdoc IERC721Enumerable + function totalSupply() external view returns (uint256) { + // TODO: implement this + } +} diff --git a/contracts/interfaces/INonfungiblePositionManagerV4.sol b/contracts/interfaces/INonfungiblePositionManagerV4.sol new file mode 100644 index 00000000..1365a542 --- /dev/null +++ b/contracts/interfaces/INonfungiblePositionManagerV4.sol @@ -0,0 +1,170 @@ +// 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 {IERC721Permit} from "./IERC721Permit.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 IPeripheryPayments, IERC721Metadata, IERC721Enumerable, IERC721Permit { + /// @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 Creates a new pool if it does not exist, then initializes if not initialized + /// @dev This method can be bundled with others via IMulticall for the first action (e.g. mint) performed against a pool + /// @param sqrtPriceX96 The initial square root price of the pool as a Q64.96 value + /// @param initData The initial square root price of the pool as a Q64.96 value + function createAndInitializePoolIfNecessary(PoolKey memory poolkey, uint160 sqrtPriceX96, bytes memory initData) + external + payable; + + /// @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 { + address token0; + address token1; + uint24 fee; + int24 tickLower; + int24 tickUpper; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + address recipient; + uint256 deadline; + } + + /// @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; +} From 7f544b19e55fe8acd372d748ba95e9e52b6dba23 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Mon, 20 Nov 2023 22:10:01 -0500 Subject: [PATCH 18/31] add Currency class and fix override error --- contracts/NonfungiblePositionManagerV4.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/NonfungiblePositionManagerV4.sol b/contracts/NonfungiblePositionManagerV4.sol index 1a437f79..b5e1bd52 100644 --- a/contracts/NonfungiblePositionManagerV4.sol +++ b/contracts/NonfungiblePositionManagerV4.sol @@ -8,6 +8,7 @@ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.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 {ERC721Permit} from "./base/ERC721Permit.sol"; @@ -24,7 +25,7 @@ contract NonfungiblePositionManagerV4 is SelfPermit, Multicall { - IPoolManager public immutable override poolManager; + IPoolManager public immutable poolManager; // details about the Uniswap position struct Position { From 7cc8c66f0a245b873697473c69e7b570d9b61b66 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Tue, 21 Nov 2023 09:58:55 -0500 Subject: [PATCH 19/31] remove reference to erc721enumerable from concrete --- contracts/NonfungiblePositionManagerV4.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contracts/NonfungiblePositionManagerV4.sol b/contracts/NonfungiblePositionManagerV4.sol index b5e1bd52..bd432306 100644 --- a/contracts/NonfungiblePositionManagerV4.sol +++ b/contracts/NonfungiblePositionManagerV4.sol @@ -2,7 +2,6 @@ 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 {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"; @@ -195,17 +194,14 @@ contract NonfungiblePositionManagerV4 is emit Approval(ownerOf(tokenId), to, tokenId); } - /// @inheritdoc IERC721Enumerable function tokenByIndex(uint256 index) external view returns (uint256) { // TODO: implement this } - /// @inheritdoc IERC721Enumerable function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256) { // TODO: implement this } - /// @inheritdoc IERC721Enumerable function totalSupply() external view returns (uint256) { // TODO: implement this } From c52f05ab3b5618381032f4c162febb79a26ed1f6 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Tue, 21 Nov 2023 17:02:00 -0500 Subject: [PATCH 20/31] use custom errors --- contracts/NonfungiblePositionManagerV4.sol | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/contracts/NonfungiblePositionManagerV4.sol b/contracts/NonfungiblePositionManagerV4.sol index bd432306..00b99871 100644 --- a/contracts/NonfungiblePositionManagerV4.sol +++ b/contracts/NonfungiblePositionManagerV4.sol @@ -16,6 +16,11 @@ import {PeripheryPayments} from "./base/PeripheryPayments.sol"; import {SelfPermit} from "./base/SelfPermit.sol"; import {Multicall} from "./base/Multicall.sol"; +error InvalidTokenID(); +error NotApproved(); +error NotCleared(); +error NonexistentToken(); + contract NonfungiblePositionManagerV4 is INonfungiblePositionManagerV4, ERC721Permit, @@ -89,7 +94,7 @@ contract NonfungiblePositionManagerV4 is ) { Position memory position = _positions[tokenId]; - require(position.poolId != 0, "Invalid token ID"); + if (position.poolId == 0) revert InvalidTokenID(); PoolKey memory poolKey = _poolIdToPoolKey[position.poolId]; return ( position.nonce, @@ -127,7 +132,7 @@ contract NonfungiblePositionManagerV4 is } modifier isAuthorizedForToken(uint256 tokenId) { - require(_isApprovedOrOwner(msg.sender, tokenId), "Not approved"); + if (!_isApprovedOrOwner(msg.sender, tokenId)) revert NotApproved(); _; } @@ -172,7 +177,7 @@ contract NonfungiblePositionManagerV4 is /// @inheritdoc INonfungiblePositionManagerV4 function burn(uint256 tokenId) external payable override isAuthorizedForToken(tokenId) { Position storage position = _positions[tokenId]; - require(position.liquidity == 0 && position.tokensOwed0 == 0 && position.tokensOwed1 == 0, "Not cleared"); + if (position.liquidity != 0 || position.tokensOwed0 != 0 || position.tokensOwed1 != 0) revert NotCleared(); delete _positions[tokenId]; _burn(tokenId); } @@ -183,7 +188,7 @@ contract NonfungiblePositionManagerV4 is /// @inheritdoc IERC721 function getApproved(uint256 tokenId) public view override(ERC721, IERC721) returns (address) { - require(_exists(tokenId), "ERC721: approved query for nonexistent token"); + if (!_exists(tokenId)) revert NonexistentToken(); return _positions[tokenId].operator; } From 05c9543f3afa344d8a5e8777830acadc8a3dcf16 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Tue, 21 Nov 2023 18:10:50 -0500 Subject: [PATCH 21/31] use ERC721 for now --- contracts/NonfungiblePositionManagerV4.sol | 9 ++------- contracts/interfaces/INonfungiblePositionManagerV4.sol | 3 +-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/contracts/NonfungiblePositionManagerV4.sol b/contracts/NonfungiblePositionManagerV4.sol index 00b99871..53c4ff31 100644 --- a/contracts/NonfungiblePositionManagerV4.sol +++ b/contracts/NonfungiblePositionManagerV4.sol @@ -10,7 +10,6 @@ import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.s import {Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; import {INonfungiblePositionManagerV4} from "./interfaces/INonfungiblePositionManagerV4.sol"; -import {ERC721Permit} from "./base/ERC721Permit.sol"; import {PeripheryValidation} from "./base/PeripheryValidation.sol"; import {PeripheryPayments} from "./base/PeripheryPayments.sol"; import {SelfPermit} from "./base/SelfPermit.sol"; @@ -23,7 +22,7 @@ error NonexistentToken(); contract NonfungiblePositionManagerV4 is INonfungiblePositionManagerV4, - ERC721Permit, + ERC721, PeripheryValidation, PeripheryPayments, SelfPermit, @@ -67,7 +66,7 @@ contract NonfungiblePositionManagerV4 is // TODO: does it still need WETH address in the constructor here? // TODO: use ERC721Permit2 here constructor(IPoolManager _poolManager, address _tokenDescriptor_) - ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V4-POS", "1") + ERC721("Uniswap V4 Positions NFT-V1", "UNI-V4-POS") { poolManager = _poolManager; _tokenDescriptor = _tokenDescriptor_; @@ -182,10 +181,6 @@ contract NonfungiblePositionManagerV4 is _burn(tokenId); } - function _getAndIncrementNonce(uint256 tokenId) internal override returns (uint256) { - return uint256(_positions[tokenId].nonce++); - } - /// @inheritdoc IERC721 function getApproved(uint256 tokenId) public view override(ERC721, IERC721) returns (address) { if (!_exists(tokenId)) revert NonexistentToken(); diff --git a/contracts/interfaces/INonfungiblePositionManagerV4.sol b/contracts/interfaces/INonfungiblePositionManagerV4.sol index 1365a542..d263fc51 100644 --- a/contracts/interfaces/INonfungiblePositionManagerV4.sol +++ b/contracts/interfaces/INonfungiblePositionManagerV4.sol @@ -7,12 +7,11 @@ 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 {IERC721Permit} from "./IERC721Permit.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 IPeripheryPayments, IERC721Metadata, IERC721Enumerable, IERC721Permit { +interface INonfungiblePositionManagerV4 is IPeripheryPayments, 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 From ca01507eb67bde557bd1c4d843e00ab23e785c82 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Mon, 27 Nov 2023 19:54:36 -0500 Subject: [PATCH 22/31] move custom errors inside contract + inherit from ILockCallback --- contracts/NonfungiblePositionManagerV4.sol | 14 +++++++++----- .../interfaces/INonfungiblePositionManagerV4.sol | 3 ++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/contracts/NonfungiblePositionManagerV4.sol b/contracts/NonfungiblePositionManagerV4.sol index 53c4ff31..cd10dd87 100644 --- a/contracts/NonfungiblePositionManagerV4.sol +++ b/contracts/NonfungiblePositionManagerV4.sol @@ -15,11 +15,6 @@ import {PeripheryPayments} from "./base/PeripheryPayments.sol"; import {SelfPermit} from "./base/SelfPermit.sol"; import {Multicall} from "./base/Multicall.sol"; -error InvalidTokenID(); -error NotApproved(); -error NotCleared(); -error NonexistentToken(); - contract NonfungiblePositionManagerV4 is INonfungiblePositionManagerV4, ERC721, @@ -30,6 +25,11 @@ contract NonfungiblePositionManagerV4 is { IPoolManager public immutable poolManager; + error InvalidTokenID(); + error NotApproved(); + error NotCleared(); + error NonexistentToken(); + // details about the Uniswap position struct Position { // the nonce for permits @@ -111,6 +111,10 @@ contract NonfungiblePositionManagerV4 is ); } + function lockAcquired(bytes calldata rawData) external returns (bytes memory) { + // TODO: implement this + } + /// @inheritdoc INonfungiblePositionManagerV4 function createAndInitializePoolIfNecessary(PoolKey memory poolkey, uint160 sqrtPriceX96, bytes memory initData) external diff --git a/contracts/interfaces/INonfungiblePositionManagerV4.sol b/contracts/interfaces/INonfungiblePositionManagerV4.sol index d263fc51..624e84b1 100644 --- a/contracts/interfaces/INonfungiblePositionManagerV4.sol +++ b/contracts/interfaces/INonfungiblePositionManagerV4.sol @@ -3,6 +3,7 @@ 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 {ILockCallback} from "@uniswap/v4-core/contracts/interfaces/callback//ILockCallback.sol"; import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; import {Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; @@ -11,7 +12,7 @@ import {IPeripheryPayments} from "./IPeripheryPayments.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 IPeripheryPayments, IERC721Metadata, IERC721Enumerable { +interface INonfungiblePositionManagerV4 is ILockCallback, IPeripheryPayments, 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 From 4a7d82aeaf222f5e7f1fe269e5efc816a77c4949 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Mon, 27 Nov 2023 20:25:41 -0500 Subject: [PATCH 23/31] use poolkey in mint params --- contracts/interfaces/INonfungiblePositionManagerV4.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/interfaces/INonfungiblePositionManagerV4.sol b/contracts/interfaces/INonfungiblePositionManagerV4.sol index 624e84b1..a167324c 100644 --- a/contracts/interfaces/INonfungiblePositionManagerV4.sol +++ b/contracts/interfaces/INonfungiblePositionManagerV4.sol @@ -76,9 +76,7 @@ interface INonfungiblePositionManagerV4 is ILockCallback, IPeripheryPayments, IE ); struct MintParams { - address token0; - address token1; - uint24 fee; + PoolKey poolKey; int24 tickLower; int24 tickUpper; uint256 amount0Desired; From 6e82a2eb4dac83cda325e100cb21a24590a8a153 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Mon, 27 Nov 2023 21:26:49 -0500 Subject: [PATCH 24/31] rename variables --- contracts/NonfungiblePositionManagerV4.sol | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/contracts/NonfungiblePositionManagerV4.sol b/contracts/NonfungiblePositionManagerV4.sol index cd10dd87..5bb9f239 100644 --- a/contracts/NonfungiblePositionManagerV4.sol +++ b/contracts/NonfungiblePositionManagerV4.sol @@ -5,7 +5,7 @@ import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/I 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 {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"; @@ -23,6 +23,8 @@ contract NonfungiblePositionManagerV4 is SelfPermit, Multicall { + using PoolIdLibrary for PoolKey; + IPoolManager public immutable poolManager; error InvalidTokenID(); @@ -31,13 +33,13 @@ contract NonfungiblePositionManagerV4 is error NonexistentToken(); // details about the Uniswap position - struct 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 - bytes32 poolId; + PoolId poolId; // the tick range of the position int24 tickLower; int24 tickUpper; @@ -55,7 +57,7 @@ contract NonfungiblePositionManagerV4 is mapping(bytes32 => PoolKey) private _poolIdToPoolKey; /// @dev The token ID position data - mapping(uint256 => Position) private _positions; + mapping(uint256 => TokenPosition) private _positions; /// @dev The ID of the next token that will be minted. Skips 0 uint176 private _nextId = 1; @@ -92,9 +94,9 @@ contract NonfungiblePositionManagerV4 is uint128 tokensOwed1 ) { - Position memory position = _positions[tokenId]; - if (position.poolId == 0) revert InvalidTokenID(); - PoolKey memory poolKey = _poolIdToPoolKey[position.poolId]; + 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, @@ -179,7 +181,7 @@ contract NonfungiblePositionManagerV4 is /// @inheritdoc INonfungiblePositionManagerV4 function burn(uint256 tokenId) external payable override isAuthorizedForToken(tokenId) { - Position storage position = _positions[tokenId]; + TokenPosition storage position = _positions[tokenId]; if (position.liquidity != 0 || position.tokensOwed0 != 0 || position.tokensOwed1 != 0) revert NotCleared(); delete _positions[tokenId]; _burn(tokenId); From 316604534e40c59e8a9339e601a843a72571b15f Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Wed, 29 Nov 2023 11:54:21 -0500 Subject: [PATCH 25/31] add periphery immutable state --- contracts/NonfungiblePositionManagerV4.sol | 6 +++--- contracts/base/PeripheryImmutableState.sol | 15 +++++++++++++++ .../interfaces/INonfungiblePositionManagerV4.sol | 9 ++++++++- contracts/interfaces/IPeripheryImmutableState.sol | 11 +++++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 contracts/base/PeripheryImmutableState.sol create mode 100644 contracts/interfaces/IPeripheryImmutableState.sol diff --git a/contracts/NonfungiblePositionManagerV4.sol b/contracts/NonfungiblePositionManagerV4.sol index 5bb9f239..f2f5fb58 100644 --- a/contracts/NonfungiblePositionManagerV4.sol +++ b/contracts/NonfungiblePositionManagerV4.sol @@ -12,12 +12,14 @@ 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 {Multicall} from "./base/Multicall.sol"; contract NonfungiblePositionManagerV4 is INonfungiblePositionManagerV4, ERC721, + PeripheryImmutableState, PeripheryValidation, PeripheryPayments, SelfPermit, @@ -25,8 +27,6 @@ contract NonfungiblePositionManagerV4 is { using PoolIdLibrary for PoolKey; - IPoolManager public immutable poolManager; - error InvalidTokenID(); error NotApproved(); error NotCleared(); @@ -68,9 +68,9 @@ contract NonfungiblePositionManagerV4 is // TODO: does it still need WETH address in the constructor here? // TODO: use ERC721Permit2 here constructor(IPoolManager _poolManager, address _tokenDescriptor_) + PeripheryImmutableState(_poolManager) ERC721("Uniswap V4 Positions NFT-V1", "UNI-V4-POS") { - poolManager = _poolManager; _tokenDescriptor = _tokenDescriptor_; } 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/interfaces/INonfungiblePositionManagerV4.sol b/contracts/interfaces/INonfungiblePositionManagerV4.sol index a167324c..990bbd42 100644 --- a/contracts/interfaces/INonfungiblePositionManagerV4.sol +++ b/contracts/interfaces/INonfungiblePositionManagerV4.sol @@ -8,11 +8,18 @@ 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 ILockCallback, IPeripheryPayments, IERC721Metadata, IERC721Enumerable { +interface INonfungiblePositionManagerV4 is + ILockCallback, + IPeripheryPayments, + 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 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); +} From ad34755eaca373ea11f9804374a76757b9f8a3d4 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Wed, 29 Nov 2023 12:14:13 -0500 Subject: [PATCH 26/31] update position manager skeleton with liquidity management file --- contracts/NonfungiblePositionManagerV4.sol | 7 +----- contracts/base/LiquidityManagement.sol | 22 +++++++++++++++++++ contracts/interfaces/ILiquidityManagement.sol | 20 +++++++++++++++++ .../INonfungiblePositionManagerV4.sol | 16 ++------------ 4 files changed, 45 insertions(+), 20 deletions(-) create mode 100644 contracts/base/LiquidityManagement.sol create mode 100644 contracts/interfaces/ILiquidityManagement.sol diff --git a/contracts/NonfungiblePositionManagerV4.sol b/contracts/NonfungiblePositionManagerV4.sol index f2f5fb58..469334b7 100644 --- a/contracts/NonfungiblePositionManagerV4.sol +++ b/contracts/NonfungiblePositionManagerV4.sol @@ -65,8 +65,6 @@ contract NonfungiblePositionManagerV4 is /// @dev The address of the token descriptor contract, which handles generating token URIs for position tokens address private immutable _tokenDescriptor; - // TODO: does it still need WETH address in the constructor here? - // TODO: use ERC721Permit2 here constructor(IPoolManager _poolManager, address _tokenDescriptor_) PeripheryImmutableState(_poolManager) ERC721("Uniswap V4 Positions NFT-V1", "UNI-V4-POS") @@ -113,10 +111,6 @@ contract NonfungiblePositionManagerV4 is ); } - function lockAcquired(bytes calldata rawData) external returns (bytes memory) { - // TODO: implement this - } - /// @inheritdoc INonfungiblePositionManagerV4 function createAndInitializePoolIfNecessary(PoolKey memory poolkey, uint160 sqrtPriceX96, bytes memory initData) external @@ -134,6 +128,7 @@ contract NonfungiblePositionManagerV4 is returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) { // TODO: implement this + // will do something like return mintEntry(params) } modifier isAuthorizedForToken(uint256 tokenId) { diff --git a/contracts/base/LiquidityManagement.sol b/contracts/base/LiquidityManagement.sol new file mode 100644 index 00000000..019687e6 --- /dev/null +++ b/contracts/base/LiquidityManagement.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; +import {ILockCallback} from "@uniswap/v4-core/contracts/interfaces/callback//ILockCallback.sol"; +import {IPeripheryPayments} from "../interfaces/IPeripheryPayments.sol"; +import {ILiquidityManagement} from "../interfaces/ILiquidityManagement.sol"; +import {PeripheryImmutableState} from "./PeripheryImmutableState.sol"; + +abstract contract LiquidityManagement is ILockCallback, ILiquidityManagement, PeripheryImmutableState { + function mintEntry(MintParams memory params) + internal + returns (uint256 tokenId, uint128 liquidity, BalanceDelta delta) + { + // poolManager.lock call here + } + + function lockAcquired(bytes calldata rawData) external override returns (bytes memory) { + // TODO: handle mint/add liquidity here + return abi.encode(0); + } +} diff --git a/contracts/interfaces/ILiquidityManagement.sol b/contracts/interfaces/ILiquidityManagement.sol new file mode 100644 index 00000000..02c4f2dd --- /dev/null +++ b/contracts/interfaces/ILiquidityManagement.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {ILockCallback} from "@uniswap/v4-core/contracts/interfaces/callback//ILockCallback.sol"; +import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; + +interface ILiquidityManagement is ILockCallback { + struct MintParams { + PoolKey poolKey; + int24 tickLower; + int24 tickUpper; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + address recipient; + uint256 deadline; + bytes hookData; + } +} diff --git a/contracts/interfaces/INonfungiblePositionManagerV4.sol b/contracts/interfaces/INonfungiblePositionManagerV4.sol index 990bbd42..2c40ffac 100644 --- a/contracts/interfaces/INonfungiblePositionManagerV4.sol +++ b/contracts/interfaces/INonfungiblePositionManagerV4.sol @@ -3,18 +3,18 @@ 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 {ILockCallback} from "@uniswap/v4-core/contracts/interfaces/callback//ILockCallback.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 {ILiquidityManagement} from "./ILiquidityManagement.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 - ILockCallback, + ILiquidityManagement, IPeripheryPayments, IPeripheryImmutableState, IERC721Metadata, @@ -82,18 +82,6 @@ interface INonfungiblePositionManagerV4 is uint128 tokensOwed1 ); - struct MintParams { - PoolKey poolKey; - int24 tickLower; - int24 tickUpper; - uint256 amount0Desired; - uint256 amount1Desired; - uint256 amount0Min; - uint256 amount1Min; - address recipient; - uint256 deadline; - } - /// @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. From aad107a915b8d576e9c6304cf22506eeb6456fab Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Wed, 29 Nov 2023 12:36:07 -0500 Subject: [PATCH 27/31] fix inheritance order --- contracts/NonfungiblePositionManagerV4.sol | 2 ++ contracts/base/LiquidityManagement.sol | 2 +- contracts/interfaces/ILiquidityManagement.sol | 26 +++++++++++++++++++ .../INonfungiblePositionManagerV4.sol | 24 ----------------- 4 files changed, 29 insertions(+), 25 deletions(-) diff --git a/contracts/NonfungiblePositionManagerV4.sol b/contracts/NonfungiblePositionManagerV4.sol index 469334b7..8548f95b 100644 --- a/contracts/NonfungiblePositionManagerV4.sol +++ b/contracts/NonfungiblePositionManagerV4.sol @@ -14,6 +14,7 @@ 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 @@ -22,6 +23,7 @@ contract NonfungiblePositionManagerV4 is PeripheryImmutableState, PeripheryValidation, PeripheryPayments, + LiquidityManagement, SelfPermit, Multicall { diff --git a/contracts/base/LiquidityManagement.sol b/contracts/base/LiquidityManagement.sol index 019687e6..85c934ee 100644 --- a/contracts/base/LiquidityManagement.sol +++ b/contracts/base/LiquidityManagement.sol @@ -16,7 +16,7 @@ abstract contract LiquidityManagement is ILockCallback, ILiquidityManagement, Pe } function lockAcquired(bytes calldata rawData) external override returns (bytes memory) { - // TODO: handle mint/add liquidity here + // TODO: handle mint/add/decrease liquidity here return abi.encode(0); } } diff --git a/contracts/interfaces/ILiquidityManagement.sol b/contracts/interfaces/ILiquidityManagement.sol index 02c4f2dd..5c13310e 100644 --- a/contracts/interfaces/ILiquidityManagement.sol +++ b/contracts/interfaces/ILiquidityManagement.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.19; import {ILockCallback} from "@uniswap/v4-core/contracts/interfaces/callback//ILockCallback.sol"; import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +/// @title Liquidity management interface +/// @notice Wrapper around pool manager callbacks interface ILiquidityManagement is ILockCallback { struct MintParams { PoolKey poolKey; @@ -17,4 +19,28 @@ interface ILiquidityManagement is ILockCallback { uint256 deadline; bytes hookData; } + + struct IncreaseLiquidityParams { + uint256 tokenId; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + struct DecreaseLiquidityParams { + uint256 tokenId; + uint128 liquidity; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + struct CollectParams { + uint256 tokenId; + address recipient; + uint128 amount0Max; + uint128 amount1Max; + } } diff --git a/contracts/interfaces/INonfungiblePositionManagerV4.sol b/contracts/interfaces/INonfungiblePositionManagerV4.sol index 2c40ffac..a0d12cde 100644 --- a/contracts/interfaces/INonfungiblePositionManagerV4.sol +++ b/contracts/interfaces/INonfungiblePositionManagerV4.sol @@ -95,15 +95,6 @@ interface INonfungiblePositionManagerV4 is 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, @@ -119,14 +110,6 @@ interface INonfungiblePositionManagerV4 is 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, @@ -140,13 +123,6 @@ interface INonfungiblePositionManagerV4 is 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, From 87e0b2fb2bb83d1a9e9f9fe8e88833d7abb08143 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Wed, 29 Nov 2023 15:17:52 -0500 Subject: [PATCH 28/31] move stuff around --- contracts/NonfungiblePositionManagerV4.sol | 18 +++++++++--------- contracts/base/LiquidityManagement.sol | 19 ++++++++++++++++++- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/contracts/NonfungiblePositionManagerV4.sol b/contracts/NonfungiblePositionManagerV4.sol index 8548f95b..fd74569a 100644 --- a/contracts/NonfungiblePositionManagerV4.sol +++ b/contracts/NonfungiblePositionManagerV4.sol @@ -133,15 +133,6 @@ contract NonfungiblePositionManagerV4 is // will do something like return mintEntry(params) } - 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 INonfungiblePositionManagerV4 function increaseLiquidity(IncreaseLiquidityParams calldata params) external @@ -184,6 +175,15 @@ contract NonfungiblePositionManagerV4 is _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(); diff --git a/contracts/base/LiquidityManagement.sol b/contracts/base/LiquidityManagement.sol index 85c934ee..540a8096 100644 --- a/contracts/base/LiquidityManagement.sol +++ b/contracts/base/LiquidityManagement.sol @@ -1,6 +1,7 @@ // 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 {IPeripheryPayments} from "../interfaces/IPeripheryPayments.sol"; @@ -8,11 +9,27 @@ import {ILiquidityManagement} from "../interfaces/ILiquidityManagement.sol"; import {PeripheryImmutableState} from "./PeripheryImmutableState.sol"; abstract contract LiquidityManagement is ILockCallback, ILiquidityManagement, PeripheryImmutableState { + struct AddLiquidityParams { + PoolKey poolKey; + int24 tickLower; + int24 tickUpper; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + bytes hookData; + } + function mintEntry(MintParams memory params) internal returns (uint256 tokenId, uint128 liquidity, BalanceDelta delta) { - // poolManager.lock call here + // TODO: poolManager.lock call here + } + + /// @notice Add liquidity to an initialized pool + function addLiquidity(AddLiquidityParams memory params) internal returns (uint128 liquidity, BalanceDelta delta) { + // TODO: copy over addLiquidity helper here } function lockAcquired(bytes calldata rawData) external override returns (bytes memory) { From c64e82e108b478ce506418d90e74c115557e51d7 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Wed, 29 Nov 2023 16:27:10 -0500 Subject: [PATCH 29/31] move addLiquidity functions to liquiditymanagement --- contracts/NonfungiblePositionManagerV4.sol | 2 - contracts/base/LiquidityManagement.sol | 88 ++++++++++++++++--- contracts/interfaces/ILiquidityManagement.sol | 39 +------- .../INonfungiblePositionManagerV4.sol | 46 ++++++++-- 4 files changed, 115 insertions(+), 60 deletions(-) diff --git a/contracts/NonfungiblePositionManagerV4.sol b/contracts/NonfungiblePositionManagerV4.sol index fd74569a..3dd2e447 100644 --- a/contracts/NonfungiblePositionManagerV4.sol +++ b/contracts/NonfungiblePositionManagerV4.sol @@ -22,7 +22,6 @@ contract NonfungiblePositionManagerV4 is ERC721, PeripheryImmutableState, PeripheryValidation, - PeripheryPayments, LiquidityManagement, SelfPermit, Multicall @@ -130,7 +129,6 @@ contract NonfungiblePositionManagerV4 is returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) { // TODO: implement this - // will do something like return mintEntry(params) } /// @inheritdoc INonfungiblePositionManagerV4 diff --git a/contracts/base/LiquidityManagement.sol b/contracts/base/LiquidityManagement.sol index 540a8096..90f8c82b 100644 --- a/contracts/base/LiquidityManagement.sol +++ b/contracts/base/LiquidityManagement.sol @@ -3,12 +3,33 @@ 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 {IPeripheryPayments} from "../interfaces/IPeripheryPayments.sol"; -import {ILiquidityManagement} from "../interfaces/ILiquidityManagement.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; + } -abstract contract LiquidityManagement is ILockCallback, ILiquidityManagement, PeripheryImmutableState { struct AddLiquidityParams { PoolKey poolKey; int24 tickLower; @@ -20,20 +41,63 @@ abstract contract LiquidityManagement is ILockCallback, ILiquidityManagement, Pe bytes hookData; } - function mintEntry(MintParams memory params) + /// @notice Add liquidity to an initialized pool + function addLiquidity(AddLiquidityParams memory params) internal - returns (uint256 tokenId, uint128 liquidity, BalanceDelta delta) + returns (uint128 liquidity, uint256 amount0, uint256 amount1) { - // TODO: poolManager.lock call here + (liquidity, amount0, amount1) = abi.decode( + poolManager.lock(abi.encode(CallbackData(CallbackType.AddLiquidity, msg.sender, abi.encode(params)))), + (uint128, uint256, uint256) + ); } - /// @notice Add liquidity to an initialized pool - function addLiquidity(AddLiquidityParams memory params) internal returns (uint128 liquidity, BalanceDelta delta) { - // TODO: copy over addLiquidity helper here + 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 lockAcquired(bytes calldata rawData) external override returns (bytes memory) { - // TODO: handle mint/add/decrease liquidity here + 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 add/decrease liquidity here return abi.encode(0); } } diff --git a/contracts/interfaces/ILiquidityManagement.sol b/contracts/interfaces/ILiquidityManagement.sol index 5c13310e..84833cd2 100644 --- a/contracts/interfaces/ILiquidityManagement.sol +++ b/contracts/interfaces/ILiquidityManagement.sol @@ -6,41 +6,4 @@ import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; /// @title Liquidity management interface /// @notice Wrapper around pool manager callbacks -interface ILiquidityManagement is ILockCallback { - struct MintParams { - PoolKey poolKey; - int24 tickLower; - int24 tickUpper; - uint256 amount0Desired; - uint256 amount1Desired; - uint256 amount0Min; - uint256 amount1Min; - address recipient; - uint256 deadline; - bytes hookData; - } - - struct IncreaseLiquidityParams { - uint256 tokenId; - uint256 amount0Desired; - uint256 amount1Desired; - uint256 amount0Min; - uint256 amount1Min; - uint256 deadline; - } - - struct DecreaseLiquidityParams { - uint256 tokenId; - uint128 liquidity; - uint256 amount0Min; - uint256 amount1Min; - uint256 deadline; - } - - struct CollectParams { - uint256 tokenId; - address recipient; - uint128 amount0Max; - uint128 amount1Max; - } -} +interface ILiquidityManagement is ILockCallback {} diff --git a/contracts/interfaces/INonfungiblePositionManagerV4.sol b/contracts/interfaces/INonfungiblePositionManagerV4.sol index a0d12cde..d7f886d1 100644 --- a/contracts/interfaces/INonfungiblePositionManagerV4.sol +++ b/contracts/interfaces/INonfungiblePositionManagerV4.sol @@ -7,19 +7,12 @@ 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 {ILiquidityManagement} from "./ILiquidityManagement.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 - ILiquidityManagement, - IPeripheryPayments, - IPeripheryImmutableState, - IERC721Metadata, - IERC721Enumerable -{ +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 @@ -82,6 +75,19 @@ interface INonfungiblePositionManagerV4 is 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. @@ -95,6 +101,15 @@ interface INonfungiblePositionManagerV4 is 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, @@ -110,6 +125,14 @@ interface INonfungiblePositionManagerV4 is 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, @@ -123,6 +146,13 @@ interface INonfungiblePositionManagerV4 is 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, From b72452b6f26c754c672cc9fa4ac68513780bdf98 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Wed, 29 Nov 2023 17:48:28 -0500 Subject: [PATCH 30/31] update comment + delete unused file --- contracts/base/LiquidityManagement.sol | 2 +- contracts/interfaces/ILiquidityManagement.sol | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 contracts/interfaces/ILiquidityManagement.sol diff --git a/contracts/base/LiquidityManagement.sol b/contracts/base/LiquidityManagement.sol index 90f8c82b..db75da34 100644 --- a/contracts/base/LiquidityManagement.sol +++ b/contracts/base/LiquidityManagement.sol @@ -97,7 +97,7 @@ abstract contract LiquidityManagement is ILockCallback, PeripheryImmutableState, return abi.encode(liquidity, delta.amount0(), delta.amount1()); } - // TODO: handle add/decrease liquidity here + // TODO: handle decrease liquidity here return abi.encode(0); } } diff --git a/contracts/interfaces/ILiquidityManagement.sol b/contracts/interfaces/ILiquidityManagement.sol deleted file mode 100644 index 84833cd2..00000000 --- a/contracts/interfaces/ILiquidityManagement.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {ILockCallback} from "@uniswap/v4-core/contracts/interfaces/callback//ILockCallback.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; - -/// @title Liquidity management interface -/// @notice Wrapper around pool manager callbacks -interface ILiquidityManagement is ILockCallback {} From 5d8a888251213311d3af5400b1a0c675a77da635 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Wed, 29 Nov 2023 18:05:56 -0500 Subject: [PATCH 31/31] remove stuff for initializing pools for now --- contracts/NonfungiblePositionManagerV4.sol | 8 -------- contracts/interfaces/INonfungiblePositionManagerV4.sol | 8 -------- 2 files changed, 16 deletions(-) diff --git a/contracts/NonfungiblePositionManagerV4.sol b/contracts/NonfungiblePositionManagerV4.sol index 3dd2e447..ca18210d 100644 --- a/contracts/NonfungiblePositionManagerV4.sol +++ b/contracts/NonfungiblePositionManagerV4.sol @@ -112,14 +112,6 @@ contract NonfungiblePositionManagerV4 is ); } - /// @inheritdoc INonfungiblePositionManagerV4 - function createAndInitializePoolIfNecessary(PoolKey memory poolkey, uint160 sqrtPriceX96, bytes memory initData) - external - payable - { - // TODO: implement this - } - /// @inheritdoc INonfungiblePositionManagerV4 function mint(MintParams calldata params) external diff --git a/contracts/interfaces/INonfungiblePositionManagerV4.sol b/contracts/interfaces/INonfungiblePositionManagerV4.sol index d7f886d1..38dafbf5 100644 --- a/contracts/interfaces/INonfungiblePositionManagerV4.sol +++ b/contracts/interfaces/INonfungiblePositionManagerV4.sol @@ -34,14 +34,6 @@ interface INonfungiblePositionManagerV4 is IPeripheryImmutableState, IERC721Meta /// @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 Creates a new pool if it does not exist, then initializes if not initialized - /// @dev This method can be bundled with others via IMulticall for the first action (e.g. mint) performed against a pool - /// @param sqrtPriceX96 The initial square root price of the pool as a Q64.96 value - /// @param initData The initial square root price of the pool as a Q64.96 value - function createAndInitializePoolIfNecessary(PoolKey memory poolkey, uint160 sqrtPriceX96, bytes memory initData) - external - payable; - /// @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