Uniswap V3 Core Contracts Security Analysis
Uniswap V3 Core Contracts Security Analysis
A. Introduction
The increasing adoption of decentralized finance (DeFi) has underscored the critical need for
rigorous analysis of smart contracts. These contracts, the backbone of many DeFi
applications, govern millions of dollars in value and require meticulous scrutiny to ensure
their security and proper functionality. This report presents a detailed analysis of the
IUniswapV3Pool contract, a core component of the Uniswap V3 decentralized exchange.
Through examination of its Application Binary Interface (ABI), call graph, and inheritance
structure, we aim to provide a comprehensive understanding of its internal workings,
potential vulnerabilities, and overall design efficiency. Our analysis focuses on [mention
specific aspects you analyzed, e.g., the swap function, liquidity management, or fee
collection mechanisms] to highlight key aspects of the contract's architecture and security.
B. Project Information
1. Project Name: Uniswap V3 Core Contracts
2. Project Description:
This project analyzes the security and functionality of core smart contracts within the
Uniswap V3 decentralized exchange. The focus is on understanding the interaction between
key contracts, identifying potential vulnerabilities, and assessing the overall design in terms
of security and efficiency. Specific contracts analyzed include [List the exact names of the
contracts analyzed, e.g., IUniswapV3Pool, NonfungiblePositionManager, Quoter]. This
analysis will involve examining the Application Binary Interface (ABI), generating and
interpreting call graphs and inheritance diagrams, and conducting a manual code review of
critical functions. The goal is to provide a comprehensive assessment of the security posture
of these contracts, considering known smart contract vulnerabilities and best practices.
3. Team Information
● Bui Gia Bao: Role – Lead Analyst, responsible for ABI analysis and Call Graph
generation
● Pham Quang Minh: Role – Security Analyst, responsible for vulnerability assessment
and report writing
● Nguyen Chi Cuong: Role – Code Reviewer, responsible for manual code review and
inheritance diagram analysis
4. Version Information
Page 1
C. Methodology
1. Tools Used:
Solidity Compiler: Used to compile the contract code and generate the ABI (Application
Binary Interface). [Specify the version used, e.g., Solidity v0.8.17].
Slither: A Solidity static analysis framework employed to generate call graphs and identify
potential vulnerabilities such as reentrancy, arithmetic overflow/underflow, and unchecked
return values. [Specify the version used].
Remix IDE: Used as a supporting tool for code review and contract interaction. [Optional:
mention specific plugins used in Remix].
2. Analysis Techniques:
ABI Analysis: The ABI for each contract was generated using the Solidity compiler. This ABI
was then used to understand the interface and functionality of each contract, identifying the
inputs, outputs, and general purpose of each function.
Static Analysis with Slither: Slither was used to analyze the contract code and generate call
graphs for each contract. These call graphs were visually inspected to identify potential
vulnerabilities and understand the flow of execution within and between contracts. Specific
analysis features within Slither, such as checks for reentrancy or arithmetic vulnerabilities,
were used. The results were reviewed for false positives and to ascertain their significance.
Manual Code Review: A manual code review was performed on critical functions within each
contract [specify which functions were reviewed, e.g., swap, mint, burn functions in
IUniswapV3Pool]. This involved a line-by-line inspection of the code to identify potential
vulnerabilities not detected by static analysis, including subtle logic flaws or potential edge
cases.
Inheritance Analysis (if applicable): If the contracts exhibit inheritance, a manual analysis
was performed to understand the inheritance hierarchy, identify overridden functions, and
assess the impact of inheritance on security and code reusability.
3. Limitations
Scope: Our analysis focuses on a specific set of Uniswap V3 core contracts, and it may not
capture vulnerabilities present in other parts of the system.
Static Analysis Limitations: Static analysis tools may not detect all vulnerabilities, especially
those that depend on runtime conditions or external factors.
Code Complexity: The inherent complexity of the Uniswap V3 contracts may limit the depth
of the analysis within the given timeframe.
Page 2
D. Results and Analysis
1. ABI illustrations
UniswapV3PoolDeployer contract:
[
{
"inputs": [],
"name": "parameters",
"outputs": [
{
"internalType": "address",
"name": "factory",
"type": "address"
},
{
"internalType": "address",
"name": "token0",
"type": "address"
},
{
"internalType": "address",
"name": "token1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
}
],
"stateMutability": "view",
Page 3
"type": "function"
}
]
The parameters() function is a view function, meaning it's read-only and doesn't modify the
contract's state. It returns the parameters required for deploying a Uniswap V3 pool:
● factory (address): The address of the Uniswap V3 factory contract. This contract is
responsible for creating and managing pools. The security of this address is critical; if
compromised, it could impact the security of all pools deployed through it.
● token0 (address): The address of the first token in the pool (determined by
lexicographical ordering of addresses). The security and functionality of this token
are implicit dependencies for the pool's security. A vulnerable or malicious token
could compromise the pool.
● token1 (address): The address of the second token in the pool. Similar to token0, its
security and functionality are essential.
● fee (uint24): The trading fee collected by the pool, denominated in hundredths of a
bip (basis points). A misconfigured fee could lead to unexpected financial results or
vulnerabilities.
● tickSpacing (int24): The spacing between ticks, which determines the granularity of
price ranges in the pool's concentrated liquidity mechanism. An incorrect tickSpacing
can negatively impact liquidity management and efficient price discovery.
Function Descriptions:
● constructor(): Initializes the contract. Likely sets the parameters state variable to
default values or initializes it to null. The exact initialization would need to be
examined further.
● parameters(): A getter function. It returns the current values stored in the parameters
state variable. This is a read-only function, and it does not modify the contract's state.
It's crucial for external entities to retrieve the deployment parameters for the pool.
Page 4
deterministic and a security feature that can increase auditing visibility and
prevent accidental pool duplications.
● Readability and Structure: The code is relatively well-structured and easy to read.
The use of a Parameters struct improves code organization. The comments are
clear, helpful, and assist in understanding the function's purpose and logic.
● Use of Comments and Documentation: The code includes concise and informative
comments. The /// @inheritdoc directive also indicates that the parameters variable is
inheriting documentation from an interface, which is good practice for maintaining
consistency. This helps make it clear for developers who might interact with or
integrate with the deployment contract.
Security Analysis
● Lack of Explicit Access Control: The ABI doesn't reveal any access control
mechanisms for the parameters() function. This raises concerns about who can
access and potentially manipulate the returned parameters. If an attacker could
influence the parameters returned by this function, they could create pools with
vulnerabilities or manipulate the pools' properties. Further code inspection is needed
to determine whether there are implicit controls (e.g., using a modifier).
● Reentrancy Vulnerability (Indirect): Although not directly visible in the ABI, there's a
potential reentrancy vulnerability in the deploy() function (which is internal and not
exposed in the ABI). If the deploy() function interacts with external contracts before
securely setting the pool's state, a malicious contract could potentially re-enter the
Page 5
deploy() function, potentially leading to a state manipulation attack. Careful review of
the deploy() function's implementation is required to rule out this risk.
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"indexed": true,
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
}
],
"name": "FeeAmountEnabled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "oldOwner",
"type": "address"
Page 6
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnerChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token0",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "token1",
"type": "address"
},
{
"indexed": true,
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"indexed": false,
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"indexed": false,
"internalType": "address",
"name": "pool",
"type": "address"
Page 7
}
],
"name": "PoolCreated",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenA",
"type": "address"
},
{
"internalType": "address",
"name": "tokenB",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
}
],
"name": "createPool",
"outputs": [
{
"internalType": "address",
"name": "pool",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
Page 8
"name": "tickSpacing",
"type": "int24"
}
],
"name": "enableFeeAmount",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"name": "feeAmountTickSpacing",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint24",
"name": "",
Page 9
"type": "uint24"
}
],
"name": "getPool",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "parameters",
"outputs": [
{
"internalType": "address",
"name": "factory",
"type": "address"
},
{
"internalType": "address",
"name": "token0",
"type": "address"
},
{
"internalType": "address",
Page 10
"name": "token1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_owner",
"type": "address"
}
],
"name": "setOwner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
FeeAmountEnabled(uint24 fee, int24 tickSpacing): Signals that a new fee tier has been
enabled. This is important for monitoring changes in the factory's configuration and for
auditing purposes. Analyzing the emitted events can reveal the evolution of fee structures
over time. It also provides a way to verify that fees are correctly set.
Page 11
PoolCreated(address token0, address token1, uint24 fee, int24 tickSpacing, address pool):
Indicates the creation of a new pool. This event logs essential information about each pool,
allowing for verification and post-deployment analysis. It enables tracking of pool creation
and can be used for identifying patterns or potential misuse of the factory.
Functions:
● constructor(): The constructor initializes the factory owner to the deployer's address,
emits an OwnerChanged event, and sets up three predefined fee amounts (500,
3000, 10000) with their respective tick spacings.
● owner(): A simple getter function returning the current owner of the factory contract.
● getPool(address tokenA, address tokenB, uint24 fee): This getter function returns the
address of a pool given the two tokens and the fee tier.
● createPool(address tokenA, address tokenB, uint24 fee): This core function creates a
new Uniswap V3 pool. It performs the following steps:
1. Input Validation: Checks that the two tokens are different, neither token is the
zero address, and the fee is valid.
3. Fee Validation: Retrieves the tick spacing for the given fee, ensuring it exists.
4. Uniqueness Check: Verifies that no pool exists for the given token pair and
fee.
5. Pool Deployment: Deploys the pool using the deploy function inherited from
UniswapV3PoolDeployer. The factory address is passed as the deployer.
Page 12
Code Style Assessment:
● Readability and Structure: The code is well-structured and readable. The use of
comments and the @inheritdoc tag makes it easy to understand the contract's
purpose and functionality.
Security Analysis
● Input Validation: Thorough validation of all inputs across all functions is mandatory to
prevent the creation of invalid or malicious pools and to prevent unexpected
behavior.
● Error Handling: The contract should handle errors gracefully to prevent unexpected
behavior or the exposure of sensitive information.
● Gas Optimization: Optimizing gas usage is crucial, especially for frequently called
functions like getPool.
NoDelegateCall Contract
Functions:
● constructor(): The constructor initializes the original state variable to the address of
the contract instance. This immutable variable is used for the reentrancy check.
Page 13
● Readability and Structure: The code is concise and easy to understand. The
comments clearly explain the purpose and functionality. The separation of the core
logic into the checkNotDelegateCall function improves readability and reduces code
duplication in inheriting contracts, as the modifier only needs to make a call to the
helper function.
Security Analysis
● Malicious Code Execution: This mitigates the risk that the called contract might
contain malicious code.
● Inheritance: The security benefits only apply to contracts that correctly inherit from
NoDelegateCall and use the noDelegateCall modifier. Incorrect usage of the modifier,
or missing the modifier entirely in the inheriting contract, would completely undermine
the security goals of this contract.
Page 14
● Gas Costs: The noDelegateCall modifier will likely increase the gas cost of the
functions in which it's used. This trade-off between security and efficiency needs to
be considered when deciding on its usage.
Events:
● Initialize(uint160 sqrtPriceX96, int24 tick): This is emitted only once during the pool's
initialization, providing its starting price (sqrtPriceX96) and tick.
Page 15
Functions:
● burn: Removes liquidity. Requires checking for authorization (only the position owner)
and accurate calculation of returned tokens.
● collect: Collects fees. Requires authorization (only the position owner) and accurate
fee calculations.
● collectProtocol: Collects protocol fees. Requires strict access control (likely only the
factory owner).
● flash: Supports flash loans. Extremely high security scrutiny is needed to prevent
exploits. It relies on a callback mechanism, which is a common source of
vulnerabilities.
● initialize: Initializes the pool. This function is usually only called once and needs to
correctly set the initial price and other crucial parameters.
● mint: Adds liquidity to a position. Similar to burn, it needs to accurately calculate the
amounts of tokens required and returned. Authorization is necessary.
● observe: Retrieves historical price and liquidity data. The correctness of this function
is vital for accurate off-chain analytics.
● swap: Executes a token swap. This function is very complex and therefore prone to
exploits:
Page 16
● Readability and Structure: The code is highly complex, making readability
challenging. The use of structs (Slot0, ProtocolFees, ModifyPositionParams,
SwapCache, SwapState, StepComputations) and comments helps manage this.
However, some functions are exceptionally long and would benefit from better
decomposition into smaller, more focused functions.
● Comments and Documentation: The contract has good NatSpec documentation, but
it's not enough to fully clarify some complex internal operations. More comments
within complex calculations would improve comprehension.
● Gas Optimization: The comments indicate various gas optimizations (e.g., using
staticcall, avoiding redundant checks)
Security Assessment
● Reentrancy: The swap, mint, and flash functions are particularly vulnerable to
reentrancy attacks due to their external calls. The lock modifier provides some
protection, but thorough analysis of the interaction with callbacks is critical.
● Price Manipulation: The swap function needs careful scrutiny to ensure that it is
resistant to price manipulation attacks.
● Input Validation: Many require statements to protect against invalid inputs, but an
exhaustive check is needed.
● Callback Attacks: The callback functions are potential attack vectors. Malicious
callbacks could manipulate the contract's state in unexpected ways. The checks
made after the callbacks are important for verifying that the contract's state has not
been unexpectedly changed. These checks are vital and need thorough testing.
Page 17
2. Call Graph Discussion
Overview
The graph depicts a complex ecosystem of smart contracts, libraries, and interfaces
for a contract system, from UniswapV3. It organizes them into clusters (subgraphs),
representing individual contracts, interfaces, or reusable libraries. These are linked by call
relationships, either internal or external, forming a comprehensive blueprint of the system’s
architecture.
Graph
Since the graph is too big, please refer to this link:
[Link]
Graph Structure
● Graph Settings:
○ ratio = "auto", bgcolor = "#2e3e56": Specifies layout and background color.
○ Nodes and edges are styled for a visually distinct display with custom colors
and fonts.
● Node Appearance:
○ Nodes use different shapes, colors, and labels to denote various roles (e.g.,
constructors, methods, library functions).
○ Highlighted in colors such as red-pink for constructor, green for special
methods, and others.
● Edge Appearance:
○ Edges are color-coded:
■ Green for internal calls.
■ White for external calls.
Subgraphs
The graph is divided into clusters (subgraphs), each representing a smart contract, interface,
or library:
1. Contracts:
○ NoDelegateCall:
■ Includes methods like <Constructor> and checkNotDelegateCall.
○ UniswapV3Factory:
■ Implements methods like createPool, setOwner, and deploy.
○ UniswapV3Pool:
■ A key contract with multiple methods for managing liquidity, swaps,
and balances (mint, swap, burn, etc.).
2. Interfaces (iface):
Page 18
○ Represent contract interfaces like IUniswapV3Factory, IUniswapV3Pool, and
others.
○ Define expected methods like owner, feeAmountTickSpacing, and getPool.
3. Libraries (lib):
○ Examples include TickMath, SqrtPriceMath, and FullMath.
○ Provide utility functions for mathematical operations, like getTickAtSqrtRatio,
mulDiv, and addDelta.
4. Supporting Structures:
○ Other clusters represent helper libraries, utility functions, or shared resources
like observations, ticks, and positions.
Relationships
In-depth
1. Contracts
Contracts define the core business logic and operational mechanisms of the protocol. They
contain methods and constructors represented as nodes. Notable contracts include:
a. NoDelegateCall
b. UniswapV3Factory
c. UniswapV3Pool
● Purpose: Core pool logic for managing liquidity, swaps, and fees.
● Methods:
Page 19
○ Constructor: Initializes the pool with configuration.
○ mint, burn, swap: Core operations for liquidity providers and traders.
○ flash: Executes flash loans.
○ initialize: Configures initial pool state.
○ _modifyPosition, _updatePosition: Internal methods for managing liquidity
positions.
○ observe, snapshotCumulativesInside: Time-weighted average price (TWAP)
and cumulative data management.
○ balance0, balance1: Tracks token balances in the pool.
○ checkTicks: Ensures tick boundaries are valid.
2. Interfaces
Interfaces define standard methods that contracts are expected to implement. They
represent dependencies or externally called contracts. Examples include:
a. IUniswapV3Factory
b. IUniswapV3Pool
● Includes core pool methods like mint, swap, burn, and TWAP-related methods
(observe, snapshotCumulativesInside).
c. IUniswapV3PoolState
d. IERC20Minimal
● Represents ERC20 token interactions with functions like transfer, approve, and
transferFrom.
3. Libraries
Libraries provide reusable logic for calculations, validations, or transformations. These are
stateless and serve as utility modules.
a. TickMath
b. SqrtPriceMath
Page 20
● Calculates changes in token amounts and prices during swaps.
● Functions include:
○ getNextSqrtPriceFromInput and getNextSqrtPriceFromOutput: Predicts new
prices based on trades.
○ getAmount0Delta and getAmount1Delta: Computes token amounts for swaps.
c. FullMath
d. UnsafeMath
e. Other Libraries:
4. Supporting Structures
Observations
Ticks
Positions
Balances
Page 21
Call Relationships
● Internal Calls (green): Represent function calls within the same contract or library.
● External Calls (white): Represent cross-contract or interface method calls.
● Examples:
○ [Link] -> [Link] (Internal).
○ [Link] -> [Link] (External).
Graph:
[Link]
Page 22
UniswapV3Factory leverages the deployment logic to create new instances of
UniswapV3Pool.
● UniswapV3Factory → NoDelegateCall: NoDelegateCall is a security mechanism
that prevents function calls from being executed through delegatecall. This is crucial
for protecting sensitive operations and preventing reentrancy attacks.
● UniswapV3Factory → IUniswapV3Factory: IUniswapV3Factory defines the
standard interface that UniswapV3Factory must implement. It specifies the core
functionalities required for factory operations, such as pool deployment and retrieval.
Code Reusability:
Through inheritance Solidity allows developers to reuse existing code, such example
can be seen in the UniswapV3:
Through the usage of Solidity libraries code can be reused in situations where
multiple contracts require the same logic. An example of this is the Tickmath library
used to handle tick-related calculations for pool management which provide a
reusable, standardized way to handle these operations.
Code reusability can bring many benefits such as easier maintenance, scalability and
reduce the coding time.
require([Link] == owner);
owner = _owner;
Page 23
State variable overriding occurs when a derived contract modifies or extends a state
variable that was declared in a parent contract. In Solidity, derived contracts can
redefine state variables or add new ones to extend the contract's functionality, often
for the specific context in which they are used.
By overriding the state variable you allow for customizations in functionality such as
pool-specific parameters or fee structures.
The ability to override is important because it allows for better customization, clear
separation of concerns and also provides flexibility and upgradeability.
Function visibility and access control plays a key role in ensuring the security and
functionality of smart contracts. They help determine which functions can be
accessed and by whom, directly impacting the contract's behavior and safety.
There are 4 different types of function visibility: public, private, internal and external.
These can help ensure no critical private or internal functions are exposed. Access
control is a mechanism that helps control access to functions, enhancing the security
of smart contracts.
There are many benefits to function visibility and access control such as:
● Improving security
● Separating operational control
● Help improve code understanding
● Serves as clear documentation
Modifier Application:
Modifiers in Solidity are reusable code blocks that can change or restrict the behavior
of functions. They play a crucial role in enforcing access control, validating
conditions, and ensuring security within smart contracts.
Modifiers can be inherited from parent contracts and applied to functions in child
contracts. An inheritance diagram can illustrate:
1. Modifier Inheritance:
○ Shows which modifiers are inherited and reused in derived contracts.
○ Helps track the flow of access control logic across multiple contracts.
2. Modifier Overrides:
○ Indicates if modifiers have been overridden or customized in derived
contracts.
○ Ensures that critical security checks are not bypassed or altered
unintentionally.
3. Affected Functions:
○ Highlights which functions are protected by specific modifiers.
○ Allows developers and auditors to verify that sensitive functions are
appropriately secured.
Page 24
Page 25