Automated Market Makers (AMMs) look simple from the outside.
A user deposits two tokens into a liquidity pool. Another user swaps one token for another. Liquidity providers earn trading fees.
Underneath that simple interface, however, an AMM is maintaining a set of financial invariants under adversarial conditions.
Every swap changes reserves.
Every liquidity operation changes the ownership of the pool.
Every fee changes the relationship between assets and LP shares.
And if the AMM is used as a price source by another protocol, the pool can become part of an entirely different attack.
That is why AMM security requires more than checking whether swap() works correctly.
A proper AMM security audit needs to examine the pricing model, accounting, token interactions, oracle assumptions, liquidity operations, and the ways an attacker can combine multiple functions in a single transaction.
This matters because the broader smart contract threat landscape remains serious. OWASP’s 2025 Smart Contract Top 10 ranked price oracle manipulation #2, logic errors #3, reentrancy #5, and flash-loan attacks #7. Its incident dataset attributed about $1.42 billion in losses across 149 documented Web3 incidents in 2024.
For AMMs, several of these categories overlap.
A pricing bug can become an oracle attack.
A rounding error can become an accounting exploit.
A flash loan can make a small pricing weakness profitable.
And a reentrancy bug can turn an otherwise correct accounting system into a pool-draining vulnerability.
An Automated Market Maker allows users to trade assets against liquidity held in smart contracts rather than relying on a traditional order book.
One of the best-known AMM designs is the constant-product model used by Uniswap v2.
The basic invariant is:
x * y = k
where:
x = reserve of token Ay = reserve of token Bk = constant productIf a trader wants to buy token B using token A, the reserves change while the pool attempts to preserve the invariant, subject to trading fees.
The Uniswap v2 design uses this constant-product model and also introduced a time-weighted price oracle and flash swaps.
The important security observation is this:
The AMM’s reserves are not just balances. They are part of the protocol’s pricing and accounting state.
If an attacker can manipulate that state in a way the protocol does not expect, the consequences can extend far beyond a bad swap.
An AMM usually has several interacting components:
┌──────────────┐
│ Factory │
└──────┬───────┘
│
▼
┌──────────────┐
│ Pool / Pair │
└──────┬───────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
Swap Liquidity Fees
│ │
└────┬─────┘
▼
Reserves
│
▼
Pricing
A vulnerability in one part can affect another.
For example:
Reserve manipulation
↓
Incorrect price
↓
Incorrect LP valuation
↓
Incorrect collateral valuation
↓
Protocol loss
This is why an AMM audit should not treat every function as an isolated piece of code.
The important question is:
What assumptions does the entire system make about pool state?
The first step in an AMM security audit is understanding the mathematical invariant.
For a constant-product AMM:
x * y = k
But the actual implementation may include:
So the real invariant may be more complicated than the formula shown in the documentation.
Consider:
uint256 amountOut =
reserveOut * amountIn /
(reserveIn + amountIn);
This looks reasonable.
But an audit needs to ask:
amountIn == 0?reserveIn == 0?amountOut >= reserveOut?A mathematically correct formula can still be implemented incorrectly.
One of the most important questions in an AMM audit is:
Are stored reserves always consistent with the actual token balances?
A pool might maintain:
uint256 reserve0;
uint256 reserve1;
while the actual balances are:
token0.balanceOf(address(this));
token1.balanceOf(address(this));
These values do not necessarily have to be identical at every moment.
For example, a protocol may intentionally support:
actual balance > recorded reserve
for donations or fee mechanisms.
That means the auditor needs to understand exactly when reserves are updated.
Potentially dangerous patterns include:
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
being used without considering external balance changes.
The opposite problem can also occur:
recorded reserve
≠
actual token balance
If another function trusts the wrong value, the discrepancy may become exploitable.
skim() and sync()-Style LogicAMMs sometimes expose functions that reconcile token balances and internal reserves.
Conceptually:
function sync() external {
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
}
or:
function skim(address to) external {
// transfer excess balances
}
These functions need careful review.
Ask:
sync() lock a manipulated state into the pool?A function that looks like simple bookkeeping can become security-critical when the reserve values are later used for pricing.
A classic AMM price is derived from reserves:
price ≈ reserveTokenB / reserveTokenA
That is useful for trading.
It is not automatically safe as an oracle.
Suppose a pool contains:
1,000 ETH
1,000,000 USDC
The implied price is roughly:
1 ETH = 1,000 USDC
An attacker can temporarily trade a large amount through a low-liquidity pool.
The reserves change.
The implied price changes.
If another protocol immediately reads that price, it may consume a manipulated value.
OWASP identifies price oracle manipulation as a major smart contract risk, specifically warning that attackers can manipulate DEX-based pricing and cause protocols to make incorrect financial decisions.
This is one of the most important AMM security issues.
Flash loans allow an attacker to temporarily obtain a large amount of capital without providing conventional collateral.
The attacker can perform:
Flash loan
↓
Large swap
↓
Manipulate AMM reserves
↓
Trigger vulnerable protocol
↓
Profit
↓
Reverse swap
↓
Repay loan
Everything can happen inside one transaction.
OWASP specifically describes flash loans as an attack amplifier that can be combined with oracle manipulation, reentrancy, and flawed business logic.
For an AMM audit, the relevant question is therefore not:
“Can someone manipulate the pool?”
It is:
“Can someone manipulate the pool cheaply enough to profit from another protocol?”
That distinction matters.
A common response to spot-price manipulation is to use a Time-Weighted Average Price.
Conceptually:
TWAP =
sum(price × time) / total time
The idea is that manipulating a price for a few seconds should not immediately change the long-term average.
Uniswap v2 introduced a time-weighted oracle that accumulates relative prices at block boundaries. Uniswap’s documentation notes that the cost of manipulating a TWAP generally increases with both pool liquidity and the length of the averaging period.
But TWAP implementation still needs to be audited.
Check:
A one-minute TWAP over a thin pool is a very different security model from a long-window TWAP over a highly liquid market.
Users should generally specify acceptable execution bounds.
For example:
function swap(
uint256 amountIn,
uint256 minAmountOut
) external;
The contract should ensure:
actualAmountOut >= minAmountOut
Without this protection, a transaction sitting in the mempool can become vulnerable to adverse price movement.
A simplified attack looks like:
Victim transaction
↓
Attacker buys
↓
Victim receives worse price
↓
Attacker sells
This is commonly associated with sandwich attacks.
Uniswap’s own security documentation highlights the risk of transactions interacting directly with pools without appropriate price protections and explains why routers provide important safety checks.
An AMM should therefore distinguish between:
Pool-level pricing
and:
User-level execution protection
They are not the same thing.
amountOut CalculationsA large part of an AMM audit is mathematical.
For example:
amountOut =
reserveOut * amountIn /
(reserveIn + amountIn);
A security review should test:
amountIn = 0
amountIn = 1
amountIn = max
reserveIn = 0
reserveOut = 0
very large reserves
very small reserves
Also test the fee calculation.
For example:
amountInWithFee =
amountIn * 997;
amountOut =
reserveOut * amountInWithFee /
(reserveIn * 1000 + amountInWithFee);
Questions include:
amountOut exceed reserves?Arithmetic errors are particularly dangerous in financial contracts because a tiny per-transaction discrepancy can become significant when repeated.
Consider:
uint256 shares =
assets * totalSupply /
totalAssets;
The division rounds down.
Usually that is acceptable.
But if an attacker can repeat the operation many times, rounding can potentially accumulate into meaningful value.
This is especially important for:
The audit should determine:
Who benefits from rounding?
and:
Can the operation be repeated?
An individual rounding error may be negligible.
A repeatable rounding error can be a vulnerability.
The first liquidity provider can be a special case.
Suppose:
totalSupply = 0
How many LP tokens should the first provider receive?
A naive implementation might calculate:
shares =
amount0 * totalSupply /
reserve0;
which is undefined or meaningless when the pool is empty.
Many AMMs therefore have special initialization logic.
This logic needs to be reviewed for:
The first liquidity provider often operates under different assumptions from later liquidity providers.
Do not treat the initial deposit as an ordinary deposit.
A particularly interesting AMM and vault security issue occurs when tokens can be transferred directly to the pool without going through the expected deposit function.
For example:
Attacker
│
│ direct transfer
▼
Pool
Now:
token.balanceOf(pool)
may be greater than:
recordedReserve
Depending on the implementation, this difference can affect:
sync()The auditor should always test:
deposit()
direct transfer
sync()
deposit()
withdraw()
swap()
as a sequence.
A vulnerability may only appear after these operations are combined.
Modern AMMs frequently interact with external contracts during swaps.
For example:
token.transfer(to, amount);
IHook(to).afterSwap(...);
or:
callback(msg.sender);
This creates a potential reentrancy boundary.
An attacker may attempt:
swap()
↓
callback()
↓
swap()
↓
callback()
↓
...
The key question is whether the pool’s state is updated before control leaves the contract.
OWASP ranks reentrancy among the major smart contract vulnerabilities and notes that external calls made before state updates can allow attackers to reenter vulnerable functions.
For an AMM, review reentrancy across:
swapmintburnflashDo not only search for nonReentrant.
Understand the state machine.
Uniswap v2’s flash swaps allow a user to receive tokens before paying for them, provided the required assets are returned by the end of the transaction.
A simplified flow looks like:
Pool
│
│ send tokens
▼
Attacker contract
│
│ execute arbitrary logic
▼
Pool callback
│
│ verify repayment
▼
Pool
The callback is therefore an important trust boundary.
Check:
Flash functionality is powerful because it intentionally gives users temporary control of pool assets.
That makes callback validation critical.
AMMs generate value from fees.
A typical flow is:
Trader
↓
Swap
↓
Trading fee
├── LPs
└── Protocol
Review:
Suppose:
uint256 fee = amountIn * feeRate / 10_000;
Questions:
Is feeRate bounded?
Can governance set feeRate = 100%?
Can feeRate overflow?
Does the trader pay the fee once or twice?
Who receives the rounding remainder?
Administrative fee parameters are part of the security model too.
Not all ERC-20 tokens use 18 decimals.
Consider:
USDC = 6 decimals
WETH = 18 decimals
If an AMM or external pricing contract assumes:
1 token unit = 1e18
without normalizing decimals, prices can be off by orders of magnitude.
This can affect:
Every AMM audit should explicitly document the decimal assumptions for each supported token.
Consider:
token.transferFrom(
msg.sender,
address(this),
amount
);
The contract may assume:
received = amount
But a fee-on-transfer token may result in:
received < amount
If the AMM calculates reserves using the requested amount rather than the actual received amount, accounting can become inconsistent.
For example:
User sends: 100 tokens
Pool receives: 98 tokens
Contract assumes: 100 tokens
That two-token difference has to go somewhere.
If the protocol does not account for it correctly, the invariant may no longer represent reality.
Rebasing tokens introduce another unusual situation.
The pool’s token balance may change without a normal transfer() call.
For example:
Before rebase:
Pool = 1,000 tokens
After positive rebase:
Pool = 1,050 tokens
No trader performed a swap.
No liquidity provider deposited anything.
But the balance changed.
If the AMM assumes balances only change through explicit protocol functions, this can break accounting assumptions.
Therefore, supported token types should be explicitly defined.
Traditional constant-product AMMs distribute liquidity across the entire price curve.
Concentrated liquidity systems such as Uniswap v3 allow LPs to provide liquidity within selected price ranges.
That introduces additional state:
Liquidity
Ticks
Positions
Price
sqrtPriceX96
Tick boundaries
Fee growth
The security model becomes considerably more complicated.
An audit should examine:
sqrtPriceX96Uniswap’s v3 core contracts themselves underwent multiple security reviews, including reviews involving Echidna end-to-end testing and Manticore symbolic execution.
That is a good illustration of how much more complex concentrated-liquidity AMMs become compared with a simple constant-product pool.
sqrtPriceX96 and Fixed-Point MathIf your AMM uses concentrated liquidity, fixed-point arithmetic becomes a major part of the audit.
For example:
sqrtPriceX96
represents a square-root price using fixed-point precision.
Small mistakes in:
can produce significant pricing differences.
The auditor should test extreme values:
minimum price
maximum price
minimum liquidity
maximum liquidity
tick boundaries
large swaps
tiny swaps
and verify that calculations remain within expected bounds.
For concentrated liquidity, liquidity changes when price crosses ticks.
Conceptually:
Tick -100
│
▼
Liquidity A
│
▼
Tick 0
│
▼
Liquidity B
│
▼
Tick +100
Potential bugs include:
These bugs may not appear during ordinary swaps.
They often appear when the price moves exactly across a boundary.
Therefore, tests should deliberately target:
tick - 1
tick
tick + 1
rather than testing only random prices.
AMMs should enforce reasonable bounds.
For example:
require(amountIn > 0);
require(amountOut <= reserveOut);
and, depending on the design:
require(
amountOut >= minAmountOut
);
Also consider maximum values.
An extremely large swap can:
An AMM should define what happens when someone attempts to trade a substantial percentage of the pool.
LP withdrawal is another critical area.
Suppose a provider owns:
10% of total LP shares
They should receive approximately their proportional claim on pool assets, subject to fees and the AMM’s design.
Review:
burn()
withdraw()
removeLiquidity()
collect()
Check:
A useful invariant is:
User's withdrawn assets
≤
User's proportional entitlement
unless the protocol explicitly allows otherwise.
If the protocol itself owns LP positions, additional risks appear.
For example:
Protocol
↓
AMM LP
↓
Pool
Now protocol accounting may depend on:
If the protocol uses its LP position as collateral or treasury value, manipulating the underlying pool can have consequences beyond the AMM itself.
This is another reason AMM spot prices should not automatically be treated as trustworthy financial oracles.
An AMM may contain privileged functions such as:
setFee()
setFeeRecipient()
setOracle()
pause()
setHook()
setTreasury()
upgrade()
Each one should be reviewed.
Questions include:
OWASP’s 2025 ranking puts access-control vulnerabilities at #1, making privileged configuration one of the first areas worth reviewing.
A pool may be secure as an exchange but unsafe as an oracle.
These are different properties.
For example:
AMM
↓
Price
↓
Lending Protocol
↓
Collateral Value
↓
Borrowing
If the lending protocol trusts a manipulable AMM price, the attacker does not necessarily need to exploit the AMM itself.
They only need to manipulate the AMM enough to influence the downstream protocol.
Uniswap’s own documentation warns that directly reading a pool’s runtime price can expose integrations to manipulation and sandwich attacks.
Therefore:
“The AMM works correctly” does not mean “the AMM is safe to use as an oracle.”
One of the biggest mistakes in DeFi testing is testing functions independently.
For example:
swap()
may be completely safe.
sync()
may be completely safe.
addLiquidity()
may be completely safe.
But:
swap()
→ direct transfer
→ sync()
→ addLiquidity()
→ withdraw()
may produce an unexpected state.
This is why AMM security testing should focus on state transitions.
Useful attack sequences include:
flash loan
→ swap
→ manipulate reserves
→ call dependent protocol
→ reverse swap
→ repay
and:
donation
→ sync
→ deposit
→ withdraw
and:
swap
→ callback
→ reenter
→ swap
The interaction between individually safe functions is often where the interesting bugs are.
A strong AMM audit starts by writing down the properties the system must preserve.
Examples:
Recorded reserves
must remain consistent with the protocol's accounting model.
Trader cannot receive more assets than allowed
by the pricing formula and fee model.
LP ownership corresponds to the underlying pool entitlement.
Fees cannot be created or lost through rounding.
Only authorized roles can modify critical parameters.
A temporary pool imbalance cannot create an
unbounded trusted price for downstream protocols.
These invariants can then be tested with fuzzing and invariant testing.
A practical AMM testing stack can include:
Unit Tests
+
Fuzz Testing
+
Invariant Testing
+
Static Analysis
+
Symbolic Execution
+
Manual Review
Tools such as Slither, Echidna, and Manticore are commonly used in smart contract security workflows. Ethereum’s security documentation recommends extensive testing and lists these tools among useful options.
For an AMM, fuzzing should target values such as:
amountIn
amountOut
reserve0
reserve1
liquidity
fee
tick
sqrtPriceX96
deadline
and especially their boundary conditions.
Before deploying an AMM, review the following.
sync() logic reviewedskim() logic reviewedamountOut verifiedsqrtPriceX96 calculations checkedA useful AMM audit should go beyond a list of generic findings.
It should answer questions such as:
Can pool reserves be manipulated?
Can an attacker extract value through rounding?
Can a flash loan amplify a pricing weakness?
Can an attacker manipulate an oracle?
Can an LP receive more assets than they should?
Can a callback reenter the pool?
Can a token's non-standard behavior break accounting?
Can an attacker manipulate the first liquidity position?
Can governance configure an unsafe fee?
Can a downstream protocol incorrectly trust the pool price?
These questions are much closer to the real economic attack surface than simply checking whether the contract uses ReentrancyGuard.
AMM security is fundamentally about protecting economic invariants under adversarial conditions.
The code can be syntactically correct.
The swap formula can look correct.
The unit tests can pass.
And the protocol can still be exploitable.
The most important attack surfaces are usually connected:
AMM
│
├── Reserves
│
├── Pricing
│
├── LP accounting
│
├── Fees
│
├── Callbacks
│
└── Oracle
│
▼
Other DeFi Protocols
A weakness in any one of these areas can become much more serious when combined with flash loans, external callbacks, or downstream protocol integrations.
This is also why established AMMs such as Uniswap have subjected their core contracts to extensive security review. Uniswap v2 underwent formal verification and code review, while Uniswap v3 received multiple security reviews, including testing with Echidna and symbolic execution with Manticore.
For developers building an AMM, DEX, liquidity pool, or protocol that depends on AMM pricing, the safest approach is to treat the pool as a financial system rather than just another Solidity contract.
Verify the invariant.
Verify the accounting.
Verify the price.
Try to manipulate the state.
And most importantly:
Test what happens when an attacker controls liquidity, transaction ordering, callbacks, and temporary capital at the same time.
That is where many of the most interesting AMM vulnerabilities begin.
UNCX's token vesting contract lost roughly $87.7K when an attacker exploited a share-accounting flaw in release(), topUpVesting(), and cancel(). We break down the root cause, the exact attack sequence, and what builders should take away from it — without the jargon.
Autheo’s THEO token lost 92% after attackers used a Hyperlane TrustedRelayerIsm misconfiguration to mint 19 million unbacked tokens on Base and extract roughly $1.76 million. Here’s how the attack worked and why single-key security models remain dangerous for cross-chain assets.
Uniswap v4 hooks explained: the PoolManager singleton, flash accounting, and what the Cork and Bunni exploits teach us about hook security. A PandaSecurity deep dive.
An attacker forced a Uniswap V4 vault into a zero-liquidity state, redeemed one minimal share unit, and withdrew roughly 14,838.47 DAI. Panda Security breaks down the transaction, vulnerable code, root cause, and practical fixes.