Skip to content

Commit d577c43

Browse files
razwwclaude
andcommitted
revert: keep ListaV3Factory non-upgradeable
Restores Factory to the upstream Uniswap V3 shape — plain constructor, NoDelegateCall on createPool, no Initializable / __gap / initialize. NPM stays upgradeable behind TransparentUpgradeableProxy. Knock-on changes: - script/Deploy.s.sol: factory deploys directly; setOwner called only if OWNER != msg.sender. Factory impl + proxy + init data removed. - test/periphery/FullFlowTest.t.sol: setUp constructs factory directly. - foundry.toml: factory drops out of compilation_restrictions; the reverted (smaller) code fits at the default optimizer_runs. - README.md: top-line and Contracts section describe factory as non-upgradeable; deployment outline shortened. The License & on-chain bytecode reuse disclaimer is removed since the NoDelegateCall deviation it explained no longer applies. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f8f4410 commit d577c43

5 files changed

Lines changed: 35 additions & 59 deletions

File tree

README.md

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
# Lista V3
22

3-
A Uniswap V3 fork renamed to Lista V3, with the factory and NFT position manager deployed behind upgradeable proxies.
3+
A Uniswap V3 fork renamed to Lista V3, with the NFT position manager deployed behind an upgradeable proxy. Factory, pool, and router behavior tracks upstream Uniswap V3.
44

55
## Contracts
66

77
### Core (`src/core/`)
88

9-
- `ListaV3Factory` — canonical factory. **Upgradeable** via `TransparentUpgradeableProxy`; initializer replaces the constructor and seeds the 500 / 3000 / 10000 fee tiers.
10-
- `ListaV3Pool` — AMM pool, CREATE2-deployed by the factory, **not upgradeable**.
9+
- `ListaV3Factory` — canonical factory. Plain (non-upgradeable) deploy; constructor sets the deployer as owner and seeds the 500 / 3000 / 10000 fee tiers. Keeps `NoDelegateCall` as upstream.
10+
- `ListaV3Pool` — AMM pool, CREATE2-deployed by the factory, not upgradeable.
1111
- `ListaV3PoolDeployer` — base of the factory; writes transient parameters so the pool constructor can read them back, keeping `POOL_INIT_CODE_HASH` constant for off-chain address derivation.
1212

1313
### Periphery (`src/periphery/`)
1414

15-
- `NonfungiblePositionManager` — wraps positions as ERC-721 NFTs. **Hybrid upgradeable**: the ERC-721 / permit stack uses `ERC721PermitUpgradeable` (storage-backed, behind the proxy), while `factory` and `WETH9` remain constructor-set `immutable`s in the implementation bytecode. Every impl upgrade must re-pass the original `(factory, WETH9)` to the new impl's constructor.
15+
- `NonfungiblePositionManager` — wraps positions as ERC-721 NFTs. **Hybrid upgradeable**: the ERC-721 / permit stack uses `ERC721PermitUpgradeable` (storage-backed, behind a `TransparentUpgradeableProxy`), while `factory` and `WETH9` remain constructor-set `immutable`s in the implementation bytecode. Every impl upgrade must re-pass the original `(factory, WETH9)` to the new impl's constructor.
1616
- `SwapRouter`, `V3Migrator`, `Quoter`, `QuoterV2`, `NonfungibleTokenPositionDescriptor`, `PairFlash` — deployed normally (non-upgradeable).
1717

1818
## Deployment outline
1919

20-
1. Deploy `ListaV3Factory` impl (no constructor args).
21-
2. Deploy a shared `ProxyAdmin`.
22-
3. Deploy `TransparentUpgradeableProxy(factoryImpl, proxyAdmin, abi.encodeWithSelector(ListaV3Factory.initialize.selector, owner))`. Treat the proxy address as *the* factory from here on.
23-
4. Deploy `NonfungiblePositionManager` impl with `(factoryProxy, WETH9)`.
24-
5. Deploy `TransparentUpgradeableProxy(npmImpl, proxyAdmin, abi.encodeWithSelector(NonfungiblePositionManager.initialize.selector, tokenDescriptor))`.
25-
6. Deploy `SwapRouter`, `V3Migrator`, etc. against the factory proxy address.
20+
1. Deploy `ListaV3Factory` directly. The deployer becomes owner; call `setOwner` afterwards if a different owner is required.
21+
2. Deploy a `ProxyAdmin` for the NPM proxy (and any future upgradeable contracts).
22+
3. Deploy `NonfungiblePositionManager` impl with `(factory, WETH9)`.
23+
4. Deploy `TransparentUpgradeableProxy(npmImpl, proxyAdmin, abi.encodeWithSelector(NonfungiblePositionManager.initialize.selector, tokenDescriptor))`.
24+
5. Deploy `SwapRouter`, `V3Migrator`, etc. against the factory address.
2625

2726
Operational notes:
2827

29-
- The Factory/NPM implementations should have their initializers consumed post-deploy (e.g. `initialize(0xdead)` / `initialize(0xdead, 0xdead)`) to close the Parity-style impl-takeover window.
30-
- Pool addresses are derived from the factory proxy via `PoolAddress.computeAddress`. If `ListaV3Pool` bytecode is ever changed, `PoolAddress.POOL_INIT_CODE_HASH` must be recomputed — the value in `src/periphery/libraries/PoolAddress.sol` is only valid for the currently-checked-in pool source and build settings.
28+
- The NPM implementation should have its initializer consumed post-deploy (e.g. `npmImpl.initialize(0xdead)`) to close the Parity-style impl-takeover window.
29+
- Every NPM impl upgrade must re-pass the same `(factory, WETH9)` to the new impl's constructor — the values are baked into impl bytecode as immutables. A deploy script that reads `npm.factory()` / `npm.WETH9()` from the existing proxy and forwards them to the new impl's constructor is the safest pattern.
30+
- Pool addresses are derived from the factory via `PoolAddress.computeAddress`. If `ListaV3Pool` bytecode is ever changed, `PoolAddress.POOL_INIT_CODE_HASH` must be recomputed — the value in `src/periphery/libraries/PoolAddress.sol` is only valid for the currently-checked-in pool source and build settings.
3131
- Re-verify `POOL_INIT_CODE_HASH` before deploying to a new chain. The value is machine-deterministic given `bytecode_hash = "none"` in `foundry.toml`, but a different toolchain version, optimizer setting, or solc patch can still shift it. Run `testInitCodeHash` in `test/periphery/FullFlowTest.t.sol` against your build environment as a pre-deploy gate; if it fails, update the constant before deploying or off-chain pool address derivation will silently point at the wrong addresses.
3232

3333
## Build & test
@@ -41,16 +41,10 @@ forge test
4141

4242
The end-to-end flow (pool creation, mint, swap, increase / decrease liquidity, collect, transfer, burn) runs in `test/periphery/FullFlowTest.t.sol`, which also exercises the proxy wiring and asserts NPM initializer state / ERC-165 registrations.
4343

44-
## License & on-chain bytecode reuse
44+
## License
4545

4646
Source is GPL-2.0-or-later (see `NOTICE` for derivation and attribution).
4747

48-
`NoDelegateCall` has been removed from `ListaV3Factory` — its `address(this) == original` invariant is incompatible with the proxy pattern, since every legitimate call from `TransparentUpgradeableProxy` is itself a `delegatecall`. `ListaV3Pool` is unproxied and keeps the modifier.
49-
50-
Upstream Uniswap V3 added `NoDelegateCall` as a license-agnostic deterrent against on-chain bytecode reuse — the original PR comment frames the intent as ["Prevents circumventing the license, GPL or otherwise"](https://github.com/Uniswap/v3-core/pull/327#issuecomment-813462722). Without it, a third party can point their own proxy at our deployed factory implementation and run a parallel AMM on Lista's compiled logic. Pools they create that way are state-isolated — CREATE2 addresses derive from their proxy, not Lista's — and cannot interact with Lista pools.
51-
52-
This does not change GPL obligations: source modification and redistribution remain governed by GPL-2.0-or-later. The legal status of `delegatecall`-into-licensed-bytecode is itself unsettled; `NoDelegateCall` was a technical deterrent, not a settled legal interpretation. We accept the tradeoff as the cost of TUP-fronted upgradeability.
53-
5448
## Dependencies
5549

5650
Git submodules under `lib/`:

foundry.toml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,13 @@ optimizer_runs = 500
88
# deterministic across machines / lib changes (matching Uniswap V3's production config).
99
bytecode_hash = "none"
1010

11-
# Factory and NonfungiblePositionManager exceed EIP-170 at the default run count.
12-
# Compile them with fewer runs (smaller runtime bytecode, slightly more gas per call).
13-
# Same profile for both so Foundry can resolve Deploy.s.sol (which imports both) as
14-
# a single compilation unit.
11+
# NonfungiblePositionManager exceeds EIP-170 at the default run count, so compile
12+
# it with fewer runs (smaller runtime bytecode, slightly more gas per call).
1513
additional_compiler_profiles = [
1614
{ name = "small", optimizer_runs = 50 },
1715
]
1816

1917
compilation_restrictions = [
20-
{ paths = "src/core/ListaV3Factory.sol", optimizer_runs = 50 },
2118
{ paths = "src/periphery/NonfungiblePositionManager.sol", optimizer_runs = 50 },
2219
]
2320

script/Deploy.s.sol

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ import {TransparentUpgradeableProxy} from 'lib/openzeppelin-contracts/contracts/
3131
contract Deploy is Script {
3232
struct Deployment {
3333
address proxyAdmin;
34-
address factoryImpl;
35-
address factoryProxy;
34+
address factory;
3635
address npmImpl;
3736
address npmProxy;
3837
address swapRouter;
@@ -67,42 +66,38 @@ contract Deploy is Script {
6766
proxyAdmin.transferOwnership(proxyAdminOwner);
6867
}
6968

70-
// Factory: impl + proxy
71-
ListaV3Factory factoryImpl = new ListaV3Factory();
72-
// Consume the impl's initializer so it can't be hijacked on-chain.
73-
factoryImpl.initialize(address(0xdead));
74-
75-
bytes memory factoryInit = abi.encodeWithSelector(ListaV3Factory.initialize.selector, owner);
76-
TransparentUpgradeableProxy factoryProxy =
77-
new TransparentUpgradeableProxy(address(factoryImpl), address(proxyAdmin), factoryInit);
69+
// Factory: plain deploy. owner = msg.sender (the broadcaster). If a separate
70+
// owner is required, the broadcaster should call factory.setOwner(owner) after.
71+
ListaV3Factory factory = new ListaV3Factory();
72+
if (owner != address(0) && owner != msg.sender) {
73+
factory.setOwner(owner);
74+
}
7875

7976
// NPM: impl + proxy. factory/WETH9 are constructor immutables on the impl; every
80-
// future upgrade MUST re-pass the exact same (factoryProxy, WETH9) to the new impl.
81-
NonfungiblePositionManager npmImpl = new NonfungiblePositionManager(address(factoryProxy), weth9);
77+
// future upgrade MUST re-pass the exact same (factory, WETH9) to the new impl.
78+
NonfungiblePositionManager npmImpl = new NonfungiblePositionManager(address(factory), weth9);
8279
npmImpl.initialize(address(0xdead));
8380

8481
bytes memory npmInit = abi.encodeWithSelector(NonfungiblePositionManager.initialize.selector, tokenDescriptor);
8582
TransparentUpgradeableProxy npmProxy =
8683
new TransparentUpgradeableProxy(address(npmImpl), address(proxyAdmin), npmInit);
8784

88-
// SwapRouter is not upgradeable; it's a plain deploy against the factory proxy.
89-
SwapRouter swapRouter = new SwapRouter(address(factoryProxy), weth9);
85+
// SwapRouter is not upgradeable; plain deploy against the factory.
86+
SwapRouter swapRouter = new SwapRouter(address(factory), weth9);
9087

9188
vm.stopBroadcast();
9289

9390
out = Deployment({
9491
proxyAdmin: address(proxyAdmin),
95-
factoryImpl: address(factoryImpl),
96-
factoryProxy: address(factoryProxy),
92+
factory: address(factory),
9793
npmImpl: address(npmImpl),
9894
npmProxy: address(npmProxy),
9995
swapRouter: address(swapRouter)
10096
});
10197

10298
console.log('--- deployed ---');
10399
console.log('ProxyAdmin:', out.proxyAdmin);
104-
console.log('Factory impl:', out.factoryImpl);
105-
console.log('Factory proxy:', out.factoryProxy);
100+
console.log('Factory:', out.factory);
106101
console.log('NPM impl:', out.npmImpl);
107102
console.log('NPM proxy:', out.npmProxy);
108103
console.log('SwapRouter:', out.swapRouter);

src/core/ListaV3Factory.sol

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
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-
64
import './interfaces/IListaV3Factory.sol';
75

86
import './ListaV3PoolDeployer.sol';
7+
import './NoDelegateCall.sol';
98

109
import './ListaV3Pool.sol';
1110

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

@@ -21,13 +19,9 @@ contract ListaV3Factory is IListaV3Factory, ListaV3PoolDeployer, Initializable {
2119
/// @inheritdoc IListaV3Factory
2220
mapping(address => mapping(address => mapping(uint24 => address))) public override getPool;
2321

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_);
22+
constructor() {
23+
owner = msg.sender;
24+
emit OwnerChanged(address(0), msg.sender);
3125

3226
feeAmountTickSpacing[500] = 10;
3327
emit FeeAmountEnabled(500, 10);
@@ -42,7 +36,7 @@ contract ListaV3Factory is IListaV3Factory, ListaV3PoolDeployer, Initializable {
4236
address tokenA,
4337
address tokenB,
4438
uint24 fee
45-
) external override returns (address pool) {
39+
) external override noDelegateCall returns (address pool) {
4640
require(tokenA != tokenB);
4741
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
4842
require(token0 != address(0));

test/periphery/FullFlowTest.t.sol

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,7 @@ contract FullFlowTest is Test {
9797
weth = new WETH9Mock();
9898
proxyAdmin = new ProxyAdmin();
9999

100-
ListaV3Factory factoryImpl = new ListaV3Factory();
101-
bytes memory factoryInit = abi.encodeWithSelector(ListaV3Factory.initialize.selector, address(this));
102-
TransparentUpgradeableProxy factoryProxy =
103-
new TransparentUpgradeableProxy(address(factoryImpl), address(proxyAdmin), factoryInit);
104-
factory = ListaV3Factory(address(factoryProxy));
100+
factory = new ListaV3Factory();
105101

106102
NonfungiblePositionManager npmImpl = new NonfungiblePositionManager(address(factory), address(weth));
107103
bytes memory npmInit = abi.encodeWithSelector(NonfungiblePositionManager.initialize.selector, address(0));

0 commit comments

Comments
 (0)