Skip to content

Commit 54cc4b2

Browse files
authored
Merge pull request #1 from lista-dao/audit/v3
V3 Audit
2 parents 35c9b5a + 93c5bd3 commit 54cc4b2

11 files changed

Lines changed: 64 additions & 56 deletions

NOTICE

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
This code is derived from Uniswap V3 (https://github.com/Uniswap/v3-core,
2+
https://github.com/Uniswap/v3-periphery), originally licensed under
3+
Business Source License 1.1, which transitioned to GPL-2.0-or-later
4+
on 2023-04-01.
5+
6+
Portions of src/core/libraries/ (FullMath, UnsafeMath) are derived
7+
from work by Remco Bloemen, licensed under MIT.
8+
9+
Modifications by Lista DAO to the original Uniswap V3 code are licensed
10+
under GPL-2.0-or-later, consistent with the original license terms.
11+
12+
Original files authored by Lista DAO are licensed under MIT.

README.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
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.
31+
- 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.
3132

3233
## Build & test
3334

@@ -40,6 +41,10 @@ forge test
4041

4142
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.
4243

44+
## License
45+
46+
Source is GPL-2.0-or-later (see `NOTICE` for derivation and attribution).
47+
4348
## Dependencies
4449

4550
Git submodules under `lib/`:
77.4 KB
Binary file not shown.
628 KB
Binary file not shown.

foundry.toml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ 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).
13+
# ListaV3Pool is pinned to the same profile so POOL_INIT_CODE_HASH binds to exactly
14+
# one Pool bytecode regardless of which compilation unit pulls it in — otherwise
15+
# Foundry produces two Pool artifacts (default + small) and the hardcoded constant
16+
# would silently match only one of them (per Bailsec audit Issue_06).
1517
additional_compiler_profiles = [
1618
{ name = "small", optimizer_runs = 50 },
1719
]
1820

1921
compilation_restrictions = [
20-
{ paths = "src/core/ListaV3Factory.sol", optimizer_runs = 50 },
2122
{ paths = "src/periphery/NonfungiblePositionManager.sol", optimizer_runs = 50 },
23+
{ paths = "src/core/ListaV3Pool.sol", optimizer_runs = 50 },
2224
]
2325

2426
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

script/Deploy.s.sol

Lines changed: 17 additions & 19 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;
@@ -50,6 +49,7 @@ contract Deploy is Script {
5049
address tokenDescriptor = vm.envOr('TOKEN_DESCRIPTOR', address(0));
5150

5251
require(owner != address(0), 'OWNER=0');
52+
require(proxyAdminOwner != address(0), 'PROXY_ADMIN_OWNER=0');
5353
require(weth9 != address(0), 'WETH9=0');
5454

5555
console.log('--- Lista V3 deploy ---');
@@ -63,46 +63,44 @@ contract Deploy is Script {
6363
vm.startBroadcast();
6464

6565
ProxyAdmin proxyAdmin = new ProxyAdmin();
66-
if (proxyAdminOwner != address(this) && proxyAdminOwner != proxyAdmin.owner()) {
66+
// ProxyAdmin.owner() is set to msg.sender (the broadcaster) in its constructor.
67+
// Transfer only if a different owner was requested.
68+
if (proxyAdminOwner != proxyAdmin.owner()) {
6769
proxyAdmin.transferOwnership(proxyAdminOwner);
6870
}
6971

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);
72+
// Factory: plain deploy. owner = msg.sender (the broadcaster). If a separate
73+
// owner is required, the broadcaster should call factory.setOwner(owner) after.
74+
ListaV3Factory factory = new ListaV3Factory();
75+
if (owner != msg.sender) {
76+
factory.setOwner(owner);
77+
}
7878

7979
// 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);
80+
// future upgrade MUST re-pass the exact same (factory, WETH9) to the new impl.
81+
NonfungiblePositionManager npmImpl = new NonfungiblePositionManager(address(factory), weth9);
8282
npmImpl.initialize(address(0xdead));
8383

8484
bytes memory npmInit = abi.encodeWithSelector(NonfungiblePositionManager.initialize.selector, tokenDescriptor);
8585
TransparentUpgradeableProxy npmProxy =
8686
new TransparentUpgradeableProxy(address(npmImpl), address(proxyAdmin), npmInit);
8787

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

9191
vm.stopBroadcast();
9292

9393
out = Deployment({
9494
proxyAdmin: address(proxyAdmin),
95-
factoryImpl: address(factoryImpl),
96-
factoryProxy: address(factoryProxy),
95+
factory: address(factory),
9796
npmImpl: address(npmImpl),
9897
npmProxy: address(npmProxy),
9998
swapRouter: address(swapRouter)
10099
});
101100

102101
console.log('--- deployed ---');
103102
console.log('ProxyAdmin:', out.proxyAdmin);
104-
console.log('Factory impl:', out.factoryImpl);
105-
console.log('Factory proxy:', out.factoryProxy);
103+
console.log('Factory:', out.factory);
106104
console.log('NPM impl:', out.npmImpl);
107105
console.log('NPM proxy:', out.npmProxy);
108106
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));

src/periphery/NonfungiblePositionManager.sol

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,6 @@ contract NonfungiblePositionManager is
7373
/// @dev The address of the token descriptor contract, which handles generating token URIs for position tokens
7474
address private _tokenDescriptor;
7575

76-
uint256[45] private __gap;
77-
7876
constructor(address _factory, address _WETH9) PeripheryImmutableState(_factory, _WETH9) {}
7977

8078
function initialize(address _tokenDescriptor_) external initializer {

src/periphery/base/ERC721PermitUpgradeable.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ abstract contract ERC721PermitUpgradeable is Initializable, BlockTimestamp, ERC7
2424
/// @dev The hash of the version string used in the permit signature verification
2525
bytes32 private _versionHash;
2626

27+
/// @dev Reserved slots for future state additions to this base. Sized so the base
28+
/// (_nameHash + _versionHash + __gap) occupies 50 slots, so additions here consume
29+
/// from __gap rather than shifting NonfungiblePositionManager's storage layout.
2730
uint256[48] private __gap;
2831

2932
function __ERC721Permit_init(

src/periphery/libraries/NFTSVG.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ library NFTSVG {
4646
function generateSVG(SVGParams memory params) internal pure returns (string memory svg) {
4747
/*
4848
address: "0xe8ab59d3bcde16a29912de83a90eb39628cfc163",
49-
msg: "Forged in SVG for Lista in 2021 by 0xe8ab59d3bcde16a29912de83a90eb39628cfc163",
49+
msg: "Forged in SVG for Uniswap in 2021 by 0xe8ab59d3bcde16a29912de83a90eb39628cfc163",
5050
sig: "0x2df0e99d9cbfec33a705d83f75666d98b22dea7c1af412c584f7d626d83f02875993df740dc87563b9c73378f8462426da572d7989de88079a382ad96c57b68d1b",
5151
version: "2"
5252
*/

0 commit comments

Comments
 (0)