Skip to content

Commit 9c92bae

Browse files
razwwclaude
andcommitted
feat: make Factory and NPM proxy-upgradeable
Factory is now Initializable (constructor → initialize(owner_), drops NoDelegateCall). NPM uses a hybrid upgrade pattern: ERC721 / permit state lives behind the proxy via ERC721PermitUpgradeable, while factory / WETH9 stay as impl-bytecode immutables set by the constructor. FullFlowTest deploys both via TransparentUpgradeableProxy and asserts initializer state. POOL_INIT_CODE_HASH refreshed for the metadata-hash change from adding the new lib. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5d1ca6b commit 9c92bae

9 files changed

Lines changed: 272 additions & 32 deletions

foundry.lock

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
"rev": "04695aecbd4d17dddfd55de766d10e3805d6f42f"
1212
}
1313
},
14+
"lib/openzeppelin-contracts-upgradeable": {
15+
"tag": {
16+
"name": "v3.4.2-solc-0.7",
17+
"rev": "e0683346f70db930bd39551046b12449ea68f2dc"
18+
}
19+
},
1420
"lib/solidity-lib": {
1521
"rev": "c01640b0f0f1d8a85cba8de378cc48469fcfd9a6"
1622
}

src/core/ListaV3Factory.sol

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
// SPDX-License-Identifier: GPL-2.0-or-later
22
pragma solidity =0.7.6;
33

4+
import 'lib/openzeppelin-contracts-upgradeable/contracts/proxy/Initializable.sol';
5+
46
import './interfaces/IListaV3Factory.sol';
57

68
import './ListaV3PoolDeployer.sol';
7-
import './NoDelegateCall.sol';
89

910
import './ListaV3Pool.sol';
1011

1112
/// @title Canonical Lista V3 factory
1213
/// @notice Deploys Lista V3 pools and manages ownership and control over pool protocol fees
13-
contract ListaV3Factory is IListaV3Factory, ListaV3PoolDeployer, NoDelegateCall {
14+
/// @dev Deployed behind a TransparentUpgradeableProxy; initialize() replaces the constructor.
15+
contract ListaV3Factory is IListaV3Factory, ListaV3PoolDeployer, Initializable {
1416
/// @inheritdoc IListaV3Factory
1517
address public override owner;
1618

@@ -19,9 +21,13 @@ contract ListaV3Factory is IListaV3Factory, ListaV3PoolDeployer, NoDelegateCall
1921
/// @inheritdoc IListaV3Factory
2022
mapping(address => mapping(address => mapping(uint24 => address))) public override getPool;
2123

22-
constructor() {
23-
owner = msg.sender;
24-
emit OwnerChanged(address(0), msg.sender);
24+
/// @dev Reserved for future layout additions behind the proxy.
25+
uint256[50] private __gap;
26+
27+
function initialize(address owner_) external initializer {
28+
require(owner_ != address(0));
29+
owner = owner_;
30+
emit OwnerChanged(address(0), owner_);
2531

2632
feeAmountTickSpacing[500] = 10;
2733
emit FeeAmountEnabled(500, 10);
@@ -36,7 +42,7 @@ contract ListaV3Factory is IListaV3Factory, ListaV3PoolDeployer, NoDelegateCall
3642
address tokenA,
3743
address tokenB,
3844
uint24 fee
39-
) external override noDelegateCall returns (address pool) {
45+
) external override returns (address pool) {
4046
require(tokenA != tokenB);
4147
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
4248
require(token0 != address(0));

src/periphery/NonfungiblePositionManager.sol

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
pragma solidity =0.7.6;
33
pragma abicoder v2;
44

5+
import 'lib/openzeppelin-contracts-upgradeable/contracts/token/ERC721/ERC721Upgradeable.sol';
6+
import 'lib/openzeppelin-contracts-upgradeable/contracts/token/ERC721/IERC721MetadataUpgradeable.sol';
7+
import 'lib/openzeppelin-contracts-upgradeable/contracts/token/ERC721/IERC721Upgradeable.sol';
8+
59
import 'src/core/interfaces/IListaV3Pool.sol';
610
import 'src/core/libraries/FixedPoint128.sol';
711
import 'src/core/libraries/FullMath.sol';
@@ -13,17 +17,18 @@ import './libraries/PoolAddress.sol';
1317
import './base/LiquidityManagement.sol';
1418
import './base/PeripheryImmutableState.sol';
1519
import './base/Multicall.sol';
16-
import './base/ERC721Permit.sol';
20+
import './base/ERC721PermitUpgradeable.sol';
1721
import './base/PeripheryValidation.sol';
1822
import './base/SelfPermit.sol';
1923
import './base/PoolInitializer.sol';
2024

2125
/// @title NFT positions
2226
/// @notice Wraps Lista V3 positions in the ERC721 non-fungible token interface
27+
/// @dev Deployed behind a TransparentUpgradeableProxy; initialize() replaces the constructor.
2328
contract NonfungiblePositionManager is
2429
INonfungiblePositionManager,
2530
Multicall,
26-
ERC721Permit,
31+
ERC721PermitUpgradeable,
2732
PeripheryImmutableState,
2833
PoolInitializer,
2934
LiquidityManagement,
@@ -60,20 +65,23 @@ contract NonfungiblePositionManager is
6065
/// @dev The token ID position data
6166
mapping(uint256 => Position) private _positions;
6267

63-
/// @dev The ID of the next token that will be minted. Skips 0
64-
uint176 private _nextId = 1;
65-
/// @dev The ID of the next pool that is used for the first time. Skips 0
66-
uint80 private _nextPoolId = 1;
68+
/// @dev The ID of the next token that will be minted. Skips 0. Set in initialize().
69+
uint176 private _nextId;
70+
/// @dev The ID of the next pool that is used for the first time. Skips 0. Set in initialize().
71+
uint80 private _nextPoolId;
6772

6873
/// @dev The address of the token descriptor contract, which handles generating token URIs for position tokens
69-
address private immutable _tokenDescriptor;
74+
address private _tokenDescriptor;
75+
76+
uint256[45] private __gap;
7077

71-
constructor(
72-
address _factory,
73-
address _WETH9,
74-
address _tokenDescriptor_
75-
) ERC721Permit('Lista V3 Positions NFT-V1', 'LIS-V3-POS', '1') PeripheryImmutableState(_factory, _WETH9) {
78+
constructor(address _factory, address _WETH9) PeripheryImmutableState(_factory, _WETH9) {}
79+
80+
function initialize(address _tokenDescriptor_) external initializer {
81+
__ERC721Permit_init('Lista V3 Positions NFT-V1', 'LIS-V3-POS', '1');
7682
_tokenDescriptor = _tokenDescriptor_;
83+
_nextId = 1;
84+
_nextPoolId = 1;
7785
}
7886

7987
/// @inheritdoc INonfungiblePositionManager
@@ -186,7 +194,12 @@ contract NonfungiblePositionManager is
186194
_;
187195
}
188196

189-
function tokenURI(uint256 tokenId) public view override(ERC721, IERC721Metadata) returns (string memory) {
197+
function tokenURI(uint256 tokenId)
198+
public
199+
view
200+
override(ERC721Upgradeable, IERC721MetadataUpgradeable)
201+
returns (string memory)
202+
{
190203
require(_exists(tokenId));
191204
return INonfungibleTokenPositionDescriptor(_tokenDescriptor).tokenURI(this, tokenId);
192205
}
@@ -385,15 +398,20 @@ contract NonfungiblePositionManager is
385398
return uint256(_positions[tokenId].nonce++);
386399
}
387400

388-
/// @inheritdoc IERC721
389-
function getApproved(uint256 tokenId) public view override(ERC721, IERC721) returns (address) {
401+
/// @inheritdoc IERC721Upgradeable
402+
function getApproved(uint256 tokenId)
403+
public
404+
view
405+
override(ERC721Upgradeable, IERC721Upgradeable)
406+
returns (address)
407+
{
390408
require(_exists(tokenId), 'ERC721: approved query for nonexistent token');
391409

392410
return _positions[tokenId].operator;
393411
}
394412

395413
/// @dev Overrides _approve to use the operator in the position, which is packed with the position permit nonce
396-
function _approve(address to, uint256 tokenId) internal override(ERC721) {
414+
function _approve(address to, uint256 tokenId) internal override(ERC721Upgradeable) {
397415
_positions[tokenId].operator = to;
398416
emit Approval(ownerOf(tokenId), to, tokenId);
399417
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity =0.7.6;
3+
4+
import 'lib/openzeppelin-contracts-upgradeable/contracts/proxy/Initializable.sol';
5+
import 'lib/openzeppelin-contracts-upgradeable/contracts/token/ERC721/ERC721Upgradeable.sol';
6+
import 'lib/openzeppelin-contracts-upgradeable/contracts/utils/AddressUpgradeable.sol';
7+
8+
import '../libraries/ChainId.sol';
9+
import '../interfaces/external/IERC1271.sol';
10+
import '../interfaces/IERC721PermitUpgradeable.sol';
11+
import './BlockTimestamp.sol';
12+
13+
/// @title Upgradeable ERC721 with permit
14+
/// @notice Nonfungible tokens that support an approve via signature, i.e. permit.
15+
/// @dev Drop-in upgradeable equivalent of ERC721Permit. `nameHash` / `versionHash` are
16+
/// stored rather than immutable so they survive being deployed behind a proxy.
17+
abstract contract ERC721PermitUpgradeable is Initializable, BlockTimestamp, ERC721Upgradeable, IERC721PermitUpgradeable {
18+
/// @dev Gets the current nonce for a token ID and then increments it, returning the original value
19+
function _getAndIncrementNonce(uint256 tokenId) internal virtual returns (uint256);
20+
21+
/// @dev The hash of the name used in the permit signature verification
22+
bytes32 private _nameHash;
23+
24+
/// @dev The hash of the version string used in the permit signature verification
25+
bytes32 private _versionHash;
26+
27+
uint256[48] private __gap;
28+
29+
function __ERC721Permit_init(
30+
string memory name_,
31+
string memory symbol_,
32+
string memory version_
33+
) internal initializer {
34+
__ERC721_init(name_, symbol_);
35+
__ERC721Permit_init_unchained(name_, version_);
36+
}
37+
38+
function __ERC721Permit_init_unchained(string memory name_, string memory version_) internal initializer {
39+
_nameHash = keccak256(bytes(name_));
40+
_versionHash = keccak256(bytes(version_));
41+
}
42+
43+
/// @inheritdoc IERC721PermitUpgradeable
44+
function DOMAIN_SEPARATOR() public view override returns (bytes32) {
45+
return
46+
keccak256(
47+
abi.encode(
48+
// keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')
49+
0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f,
50+
_nameHash,
51+
_versionHash,
52+
ChainId.get(),
53+
address(this)
54+
)
55+
);
56+
}
57+
58+
/// @inheritdoc IERC721PermitUpgradeable
59+
/// @dev Value is equal to keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)");
60+
bytes32 public constant override PERMIT_TYPEHASH =
61+
0x49ecf333e5b8c95c40fdafc95c1ad136e8914a8fb55e9dc8bb01eaa83a2df9ad;
62+
63+
/// @inheritdoc IERC721PermitUpgradeable
64+
function permit(
65+
address spender,
66+
uint256 tokenId,
67+
uint256 deadline,
68+
uint8 v,
69+
bytes32 r,
70+
bytes32 s
71+
) external payable override {
72+
require(_blockTimestamp() <= deadline, 'Permit expired');
73+
74+
bytes32 digest =
75+
keccak256(
76+
abi.encodePacked(
77+
'\x19\x01',
78+
DOMAIN_SEPARATOR(),
79+
keccak256(abi.encode(PERMIT_TYPEHASH, spender, tokenId, _getAndIncrementNonce(tokenId), deadline))
80+
)
81+
);
82+
address owner = ownerOf(tokenId);
83+
require(spender != owner, 'ERC721Permit: approval to current owner');
84+
85+
if (AddressUpgradeable.isContract(owner)) {
86+
require(IERC1271(owner).isValidSignature(digest, abi.encodePacked(r, s, v)) == 0x1626ba7e, 'Unauthorized');
87+
} else {
88+
address recoveredAddress = ecrecover(digest, v, r, s);
89+
require(recoveredAddress != address(0), 'Invalid signature');
90+
require(recoveredAddress == owner, 'Unauthorized');
91+
}
92+
93+
_approve(spender, tokenId);
94+
}
95+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity >=0.7.5;
3+
4+
import 'lib/openzeppelin-contracts-upgradeable/contracts/token/ERC721/IERC721Upgradeable.sol';
5+
6+
/// @title ERC721 with permit (upgradeable variant)
7+
/// @notice Extension to ERC721Upgradeable that includes a permit function for signature based approvals
8+
interface IERC721PermitUpgradeable is IERC721Upgradeable {
9+
/// @notice The permit typehash used in the permit signature
10+
/// @return The typehash for the permit
11+
function PERMIT_TYPEHASH() external pure returns (bytes32);
12+
13+
/// @notice The domain separator used in the permit signature
14+
/// @return The domain seperator used in encoding of permit signature
15+
function DOMAIN_SEPARATOR() external view returns (bytes32);
16+
17+
/// @notice Approve of a specific token ID for spending by spender via signature
18+
/// @param spender The account that is being approved
19+
/// @param tokenId The ID of the token that is being approved for spending
20+
/// @param deadline The deadline timestamp by which the call must be mined for the approve to work
21+
/// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s`
22+
/// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s`
23+
/// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v`
24+
function permit(
25+
address spender,
26+
uint256 tokenId,
27+
uint256 deadline,
28+
uint8 v,
29+
bytes32 r,
30+
bytes32 s
31+
) external payable;
32+
}

src/periphery/interfaces/INonfungiblePositionManager.sol

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
pragma solidity >=0.7.5;
33
pragma abicoder v2;
44

5-
import 'lib/openzeppelin-contracts/contracts/token/ERC721/IERC721Metadata.sol';
6-
import 'lib/openzeppelin-contracts/contracts/token/ERC721/IERC721Enumerable.sol';
5+
import 'lib/openzeppelin-contracts-upgradeable/contracts/token/ERC721/IERC721MetadataUpgradeable.sol';
6+
import 'lib/openzeppelin-contracts-upgradeable/contracts/token/ERC721/IERC721EnumerableUpgradeable.sol';
77

88
import './IPoolInitializer.sol';
9-
import './IERC721Permit.sol';
9+
import './IERC721PermitUpgradeable.sol';
1010
import './IPeripheryPayments.sol';
1111
import './IPeripheryImmutableState.sol';
1212

@@ -17,9 +17,9 @@ interface INonfungiblePositionManager is
1717
IPoolInitializer,
1818
IPeripheryPayments,
1919
IPeripheryImmutableState,
20-
IERC721Metadata,
21-
IERC721Enumerable,
22-
IERC721Permit
20+
IERC721MetadataUpgradeable,
21+
IERC721EnumerableUpgradeable,
22+
IERC721PermitUpgradeable
2323
{
2424
/// @notice Emitted when liquidity is increased for a position NFT
2525
/// @dev Also emitted when a token is minted

src/periphery/libraries/PoolAddress.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pragma solidity >=0.5.0;
44
/// @title Provides functions for deriving a pool address from the factory, tokens, and the fee
55
library PoolAddress {
66
// Post-rename init code hash of ListaV3Pool; must match keccak256(type(ListaV3Pool).creationCode).
7-
bytes32 internal constant POOL_INIT_CODE_HASH = 0x6c6332dbf3a9a0174451c95243ab532dbf6cfa5c66118e61ca86ce769cdc7900;
7+
bytes32 internal constant POOL_INIT_CODE_HASH = 0x810bcfa2272833e7170faadd4e1478e6c08783b7a576c7ab4a48e11e679eeaad;
88

99
/// @notice The identifying key of the pool
1010
struct PoolKey {

0 commit comments

Comments
 (0)