Automated Market Makers are deceptively simple at the surface: two token reserves, one curve equation, a handful of state-mutating functions. That simplicity is load-bearing. Every economic guarantee the protocol makes — fair pricing, solvency, fee correctness — derives from the invariant holding across every state transition. The moment a single code path allows the invariant to be satisfied incorrectly, or bypassed entirely, the entire pricing model collapses.
This article walks through every major attack surface in modern AMM design: from the arithmetic foundations of the constant product formula, through reserve manipulation, Uniswap V4’s new hook lifecycle, concentrated liquidity tick edge cases, LP token inflation, fee accounting drift, flashswap callback exploitation, and the expanded risk surface introduced by custom pool extensions. Solidity examples are included throughout. The article closes with an opinionated audit checklist.
1. The Constant Product Invariant and How Violations Manifest
AMMs like Uniswap and Balancer rely on mathematical invariants — such as the constant product model x·y = k — to set prices and manage liquidity pools. The invariant is not a suggestion; it is an axiom that every swap, liquidity addition, and removal must preserve. The invariant defines the pricing curve, and swaps must start and end on the same curve — using a “new k” destroys the AMM logic entirely.
In practice, k is allowed to increase (due to fees accruing to the pool) but must never decrease after a swap. A violation means the pool has given out more value than it received.
The Canonical Check
// Uniswap V2-style invariant check (simplified)
function swap(
uint amount0Out,
uint amount1Out,
address to,
bytes calldata data
) external nonReentrant {
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
// Optimistic transfer
if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);
// Callback for flash swaps
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, "UniswapV2: INSUFFICIENT_INPUT_AMOUNT");
// The invariant check — adjusted for 0.3% fee
uint balance0Adjusted = balance0 * 1000 - amount0In * 3;
uint balance1Adjusted = balance1 * 1000 - amount1In * 3;
// k must not decrease
require(
balance0Adjusted * balance1Adjusted >= uint(_reserve0) * uint(_reserve1) * (1000**2),
"UniswapV2: K"
);
_update(balance0, balance1, _reserve0, _reserve1);
}
The Uranium Finance Fork Disaster
In the original Uniswap V2 code, the algorithm relies on a multiplier of 1000 for balance adjustments, ensuring the integrity of the K=XY invariant. However, during the fork to Uranium Finance, developers erroneously increased this multiplier to 10000 in two instances within the swap() function but failed to update the corresponding check at the end of the function.
The result was catastrophic: the post-swap invariant check used a different scalar than the pre-swap balance adjustment, meaning the check could pass even when the pool had been substantially drained. The invariant was being “checked” against a mathematically inconsistent standard.
Rounding Errors as Invariant Violations
Rounding errors in fixed-point arithmetic can silently break invariants. In protocols employing a stable curve formula (x³y+y³x >= k), a critical vulnerability exists due to rounding errors in the calculation of the invariant k. Specifically, the _k function encounters a rounding error in the calculation of variable _a, leading to its nullification when x * y < 1e18. If the value of x * y is less than or equal to 1e18, a rounding error occurs, which subsequently results in an incorrect and successful validation of the product constants within the swap() function, triggering unauthorized draining of the pool.
// VULNERABLE: Stable curve _k with rounding footgun
function _k(uint x, uint y) internal pure returns (uint) {
uint _a = (x * y) / 1e18; // ROUNDS TO ZERO if x*y < 1e18
uint _b = (x * x) / 1e18 + (y * y) / 1e18;
return _a * _b; // returns 0 — invariant check always passes
}
// SAFE: Use higher precision intermediate
function _kSafe(uint x, uint y) internal pure returns (uint) {
// Work in 1e36 space before dividing
uint _a = Math.mulDiv(x, y, 1e18);
require(_a > 0, "Insufficient liquidity for stable curve");
uint _b = Math.mulDiv(x, x, 1e18) + Math.mulDiv(y, y, 1e18);
return _a * _b;
}
2. Price Manipulation via Reserve Manipulation
Using the instantaneous reserve ratio as a trusted oracle is dangerous because an attacker can temporarily move reserves — for example with a flash loan — and manipulate the spot price. The attack is straightforward: borrow a large amount of token A, swap it into the pool to skew the A/B ratio dramatically, read the now-manipulated price as an oracle, exploit a downstream lending or liquidation protocol, then swap back and repay the flash loan — all within one transaction.
Why Spot Price Oracles Are Unsafe
// VULNERABLE: Using spot reserves as a price oracle
function getPrice() external view returns (uint) {
(uint112 reserve0, uint112 reserve1,) = IUniswapV2Pair(pool).getReserves();
return (reserve1 * 1e18) / reserve0; // Manipulable in a single tx
}
// SAFE: Use a TWAP accumulator
function getTWAP(address pool, uint32 twapInterval) external view returns (uint) {
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = twapInterval;
secondsAgos[1] = 0;
(int56[] memory tickCumulatives,) =
IUniswapV3Pool(pool).observe(secondsAgos);
int56 tickCumulativeDelta = tickCumulatives[1] - tickCumulatives[0];
int24 arithmeticMeanTick = int24(tickCumulativeDelta / int32(twapInterval));
return TickMath.getSqrtRatioAtTick(arithmeticMeanTick);
}
Averaging prices over time (TWAP) makes manipulation harder but is not invulnerable to powerful ordering or sustained attacks. A well-resourced attacker with block-proposer access can sustain an elevated price for multiple blocks. The TWAP window length must be calibrated to the TVL and liquidity depth of the pool being used as a source.
Direct Reserve Manipulation: The sync() Backdoor
Many Uniswap V2 forks expose a sync() function that forces the internal reserve state to match the contract’s actual token balance. This is normally used to recover from fee-on-transfer token edge cases. However, if an attacker can call sync() after directly transferring tokens into the pool contract — before a legitimate user reads reserves — they can prime the pool to report a false price.
// Attack vector: direct transfer + sync
// Step 1: attacker directly transfers 1M TokenA to pool
// Step 2: attacker calls pool.sync()
// => reserve0 now reads 1M + original, reserve1 unchanged
// => spot price of TokenB inflated by ~1M/original factor
// Step 3: attacker's downstream contract reads getReserves() — sees stale data
// Step 4: attacker swaps back to restore reserves, pocketing the oracle-derived profit
The defense is to never use getReserves() — even through a trusted aggregator — as an atomic price oracle in the same transaction.
3. The donate() Function Surface in Uniswap V4
Uniswap V4 introduces a donate() pathway that lets external callers credit token amounts directly to in-range liquidity providers, bypassing the swap curve entirely. A beforeDonate hook is called before a donation is made to a pool, and an afterDonate hook is called after. Donate hooks provide a way to customize the behavior of token donations to liquidity providers.
The donate surface opens a specific class of vulnerabilities:
Fee Growth Manipulation via Donation
A simpler example is when business logic determines it is not desirable to allow donations. Without activating the beforeDonate() hook to always revert on donations, calls to PoolManager::donate would succeed in increasing the fee growth.
This is a subtle but serious issue. Fee growth accumulators are used to calculate the uncollected fees owed to individual LP positions. By inflating fee growth through donations, an attacker who holds an LP position can claim fees that were never legitimately generated by swap activity:
// VULNERABLE Hook: Does not implement beforeDonate
// An attacker can call PoolManager.donate() on this pool
// to artificially inflate feeGrowthGlobal0X128 / feeGrowthGlobal1X128,
// giving their LP position an unearned fee windfall.
contract VulnerableHook is BaseHook {
// Missing: beforeDonate that reverts if donations are not intended
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeSwap: true,
afterSwap: false,
beforeDonate: false, // ← gap in the permission surface
afterDonate: false,
// ... other permissions
});
}
}
// SAFE: Explicitly gate donate if your pool design does not intend it
contract SecureHook is BaseHook {
function beforeDonate(
address,
PoolKey calldata,
uint256,
uint256,
bytes calldata
) external override returns (bytes4) {
revert DonationsNotAllowed();
}
}
Cross-Pool State Confusion via Shared Hooks
Uniswap V4’s PoolManager allows multiple liquidity pools to reference the same Hook, as there are no built-in restrictions preventing a Hook from being attached to multiple pools. The initialize() function in PoolManager.sol registers a Hook for a given liquidity pool, but it does not enforce exclusivity, meaning the same Hook can be attached to multiple pools.
If a hook maintains per-pool state using a poolId mapping but that mapping is keyed incorrectly, a donation or swap into Pool A can corrupt the accounting for Pool B.
4. Concentrated Liquidity Tick Manipulation
The main idea of Uniswap V3 was to give liquidity providers the ability to place their assets more precisely, targeting a desirable price range. To achieve this, the entire price range is divided into parts called “ticks,” and within the swap process, the price “crosses” multiple price ticks using only the liquidity belonging to the current tick.
This architecture introduces a family of tick-boundary attacks that do not exist in full-range AMMs.
Just-In-Time (JIT) Liquidity
Concentrated liquidity supports liquidity provision within custom price ranges. However, this design introduces a new type of MEV source called Just-in-Time (JIT) liquidity attack, where the adversary mints and burns a liquidity position right before and after a sizable swap.
Just before the swap is executed, the attacker adds a very large amount of liquidity within an extremely narrow tick range right around the current price. The large swap executes almost entirely against the attacker’s liquidity, earning them the vast majority of the trading fee. Immediately after the swap, in the same block, the attacker removes their liquidity. This allows them to collect substantial fees with near-zero risk and minimal capital duration, extracting value that would have otherwise gone to passive, long-term LPs.
Over a span of 20 months, researchers identified 36,671 such attacks, which collectively generated profits of 7,498 ETH. JIT liquidity attacks essentially represent a whales’ game, predominantly controlled by a select few bots.
Tick Traversal Direction Bug
Logic flow issues are associated with incorrect handling of ticks in CLMMs, which may result in double liquidity addition. In Uniswap’s nextInitializedTickWithinOneWord function, the protocol correctly handles directional tick searching by adjusting the starting position based on the search direction — when searching downward (zeroForOne = true), it starts from tickNext - 1. However, in affected protocols’ _findOverlappingPositions function, the implementation incorrectly sets the next tick as nextInitializedTick + 1 regardless of direction when zeroForOne is false. This creates a critical flaw: when tick spacing is 1, the algorithm attempts to search from nextInitializedTick + 1, but since the next initialized tick is actually at nextInitializedTick, the search effectively skips over all valid positions.
// VULNERABLE: Direction-agnostic tick traversal
function _findNextTick(int24 currentTick, bool zeroForOne) internal view returns (int24) {
// BUG: Always adds 1 regardless of direction
return nextInitializedTick + 1;
}
// SAFE: Mirror Uniswap's directional logic exactly
function _findNextTick(int24 currentTick, bool zeroForOne) internal view returns (int24) {
if (zeroForOne) {
// Searching downward: exclude the current tick
return nextInitializedTickBelow(currentTick - 1);
} else {
// Searching upward: start from current position
return nextInitializedTickAbove(currentTick);
}
}
Spot Price Manipulation in Thin Tick Ranges
While the TWAP oracle itself is more robust, the spot price of a V3 pool is easier to manipulate than in V2. Because liquidity is concentrated, the amount of capital required to move the price across a tick boundary is proportional to the liquidity in that tick — not the total liquidity in the pool. A pool with most liquidity concentrated far from the current price is extremely cheap to spot-manipulate at the current tick.
5. LP Token Inflation Attacks
LP token inflation attacks target the share-minting arithmetic at pool initialization. The most discussed pitfall is the inflation attack, sometimes called the donation attack or first-depositor attack. The attack works against the share-pricing math at low totalSupply.
Mechanics
The attacker is the first depositor into a fresh pool. They deposit 1 wei of the underlying asset and receive 1 share. They then directly transfer (not deposit, bypassing the share-mint logic) a large amount of the underlying asset into the pool. totalAssets() now reads, say, 10,000 USDC, while totalSupply remains at 1 share. The share-mint formula (assets × totalSupply / totalAssets) gives a subsequent victim depositor 5,000 × 1 / 10,000 = 0 shares, rounded down. The attacker still holds the only share, now backed by 15,000 USDC, and can redeem the entire balance.
Uniswap V2’s MINIMUM_LIQUIDITY mechanism, which burns the first small batch of LP tokens, is a deliberate defense against front-running the initial liquidity provider. Some forks have removed this logic to simplify the code, inadvertently exposing the first LP to having their initial deposit stolen.
// VULNERABLE: No minimum liquidity protection
function mint(address to) external returns (uint liquidity) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0 - _reserve0;
uint amount1 = balance1 - _reserve1;
uint _totalSupply = totalSupply;
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0 * amount1); // No MINIMUM_LIQUIDITY burn!
} else {
liquidity = Math.min(
amount0 * _totalSupply / _reserve0,
amount1 * _totalSupply / _reserve1
);
}
_mint(to, liquidity);
}
// SAFE: Burn MINIMUM_LIQUIDITY to address(0) on first mint
uint public constant MINIMUM_LIQUIDITY = 1000;
function mint(address to) external returns (uint liquidity) {
// ... balance calculations ...
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
_mint(address(0), MINIMUM_LIQUIDITY); // Permanent lock prevents inflation
}
// ...
}
The virtual offset pattern from OpenZeppelin provides an alternative: the vault tracks an internal offset that prevents zero-share rounding for small deposits. Share-based Uniswap hook protocols face critical rounding vulnerabilities where attackers exploit integer arithmetic precision loss to systematically drain funds.
6. Fee Accounting Vulnerabilities
Fee accounting in AMMs is a subtle discipline. Both under-collection and over-collection are exploitable — the former by LPs gaming the fee accrual windows, the latter by attackers draining unearned fees.
Growth Accumulator Overflow
Uniswap V3 tracks fees through two global accumulators: feeGrowthGlobal0X128 and feeGrowthGlobal1X128. These are 256-bit unsigned integers denominated in Q128.128 fixed-point format. They are designed to overflow gracefully — subtraction of two Q128 accumulators correctly yields the delta even across overflow boundaries. The bug appears when a fork uses SafeMath on these accumulators:
// VULNERABLE: SafeMath blocks intentional overflow in fee accumulators
// In Uniswap V3, feeGrowthGlobal overflows gracefully by design.
// Using SafeMath here causes a revert when fees accumulate past 2^256-1.
// This kills the pool permanently — a slow-acting DoS.
feeGrowthGlobal0X128 = feeGrowthGlobal0X128.add(
FullMath.mulDiv(paid0, FixedPoint128.Q128, _liquidity)
); // .add() will revert at overflow
// SAFE: Use unchecked arithmetic or direct assignment for accumulators
unchecked {
feeGrowthGlobal0X128 += FullMath.mulDiv(paid0, FixedPoint128.Q128, _liquidity);
}
Protocol Fee Skimming
Protocols that route fees through a separate accounting ledger rather than directly adjusting k create a discrepancy window. If the fee-splitting logic runs after the invariant check, a manipulated transaction can pass the K check while routing more than the intended fee to the protocol address:
// VULNERABLE: Fee accounting after invariant check
function swap(...) external {
// Invariant check passes here
require(balance0Adj * balance1Adj >= uint(reserve0) * uint(reserve1) * 1e6, "K");
// Protocol fee split computed AFTER check — skimmable
uint protocolFee0 = amount0In * protocolFeeBps / 10000;
protocolFeeAccrued0 += protocolFee0; // This value is not constrained by K
}
// SAFE: Include protocol fee in the invariant-adjusted balance
uint balance0AdjustedForProtocol = balance0 * FEE_DENOM
- amount0In * (lpFeeBps + protocolFeeBps);
require(balance0AdjustedForProtocol * balance1AdjustedForProtocol >= ...);
7. Flashswap Callback Security
A flash swap lets a caller receive tokens from the pool and must repay or restore the invariant within the same atomic transaction. This enables powerful composability — arbitrage, liquidations — but also facilitates cheap, transaction-level price manipulation and multi-step exploits if other protocols trust manipulable on-chain prices.
Unlike traditional financial markets where participants provide assets before receiving returns, AMMs leverage atomic transaction guarantees to operate on an inverted model: pools optimistically release tokens first, then expect repayment within the same transaction. This “withdraw now, pay later” mechanism requires the implementation of callback functions, called by protocols to trigger repayment.
Missing Caller Validation
The most critical flashswap bug is an unvalidated callback:
// VULNERABLE: No caller validation in uniswapV2Call
contract FlashBorrower {
function uniswapV2Call(
address sender,
uint amount0,
uint amount1,
bytes calldata data
) external {
// BUG: Anyone can call this function directly.
// Attacker calls uniswapV2Call with crafted data
// to drain approved tokens or trigger unintended logic.
(address target, bytes memory payload) = abi.decode(data, (address, bytes));
(bool ok,) = target.call(payload);
require(ok);
}
}
// SAFE: Validate the caller is the legitimate pool
contract SecureFlashBorrower {
address immutable POOL;
address immutable TOKEN0;
address immutable TOKEN1;
constructor(address pool, address t0, address t1) {
POOL = pool; TOKEN0 = t0; TOKEN1 = t1;
}
function uniswapV2Call(
address sender,
uint amount0,
uint amount1,
bytes calldata data
) external {
// Validate the caller is the expected pool contract
require(msg.sender == POOL, "Unauthorized caller");
// Validate the initiator of the flash swap was this contract
require(sender == address(this), "Unauthorized sender");
// ... business logic ...
// Repay with fee
uint fee0 = (amount0 * 3) / 997 + 1;
IERC20(TOKEN0).transfer(POOL, amount0 + fee0);
}
}
Read-Only Reentrancy via Flash Swaps
Although a removeLiquidity function may be protected by the nonReentrant modifier, which prevents reentrant calls, the vulnerability can still exist because of read-only reentrancy. External contracts could invoke getReserves() and interfere with the process, especially if they utilize ERC-777 tokens with a callback function. During a flash swap callback, the pool’s reserves have been reduced (tokens sent out) but not yet updated (the _update call happens after callback return). Any external protocol that reads getReserves() during this window observes a stale and manipulated state.
Malicious actors could utilize an external contract with an ERC-777 callback function and subsequently call getReserves() during the execution of the removeLiquidity function. This allows obtaining incorrect reserve values.
8. Custom Pool Hooks and Extensions: The V4 Risk Surface
Uniswap V4 Hooks offer unprecedented flexibility for developers, enabling custom logic within liquidity pools and swaps. This opens the door to innovative features like dynamic fees, custom AMM strategies, and tighter integrations with other DeFi protocols. However, this flexibility dramatically expands the potential attack surface.
V4 shatters the assumption that auditing the core protocol is sufficient. The new Singleton PoolManager delegates execution to arbitrary hook contracts at 14 different lifecycle points — before/after initialize, swap, add/remove liquidity, and donate. Hooks can modify accounting deltas, take custody of assets, and inject custom logic into every pool operation.
Missing Access Control: The Most Common Critical
The vulnerability in the LimitOrderHook contract arises due to the lack of access control in the beforeSwap() and afterSwap() hook callback functions. The executeOrder() function, which is designed to be executed only by the LimitOrderHook contract after a legitimate swap, can currently be called by any user. This allows orders to be executed or cleared arbitrarily, completely undermining the protocol’s limit order mechanism.
It is expected that only PoolManager will call Hook functions. However, in many implementations there is no restriction on the caller. The absence of access control allows anyone to execute the Hook’s logic, potentially leading to critical vulnerabilities. To prevent this vulnerability, all Hooks’ access should be restricted to the PoolManager.
// VULNERABLE: No caller restriction
contract InsecureHook is BaseHook {
function beforeSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData
) external override returns (bytes4, BeforeSwapDelta, uint24) {
// BUG: msg.sender is not validated — anyone can call this
_updateOracleState(key, params);
return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}
}
// SAFE: Restrict all hook callbacks to PoolManager
abstract contract AccessControlledHook is BaseHook {
modifier onlyPoolManager() {
require(msg.sender == address(poolManager), "Hook: caller is not PoolManager");
_;
}
function beforeSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData
) external override onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) {
// safe to proceed
}
}
Malicious Pool Key Injection
Uniswap V4 does not restrict who can create new liquidity pools, or which hook address to use in a new liquidity pool. If a hook is not restricted to a specific pool or set of pools, an attacker could deploy a malicious pool with fake tokens and use/abuse the hook through attack vectors such as reentrancy or manipulation of the internal accounting.
Certora’s 2024 review of the Doppler protocol uncovered a critical finding that details a variation of this vulnerability. The victim contract, intended to coordinate all ecosystem hooks and associated contracts, can be drained by specification of a malicious pool key due to insufficient input validation of the hook and currency addresses.
// SAFE: Validate pool key on every hook call
mapping(PoolId => bool) public authorizedPools;
function beforeSwap(
address,
PoolKey calldata key,
IPoolManager.SwapParams calldata,
bytes calldata
) external override onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) {
PoolId id = key.toId();
require(authorizedPools[id], "Hook: unauthorized pool");
// ...
}
Hook Permission Bitmap Mismatch
The mechanism that Uniswap V4 employs to determine the permission of the hooks involves examining the least significant bits of the deployed hook address. Once a hook is deployed, the permissions cannot be changed.
V4 hook permissions are encoded in the least significant bits of the hook’s deployment address. A mismatch between encoded permissions and implemented functions creates two failure modes: a permission set with the function not implemented causes calls to the hook to revert, causing DoS for all pool operations that trigger that hook point; a function implemented with the permission not set causes the hook’s logic to be silently skipped, creating a false sense of security.
Reentrancy in V4 Hook Callbacks
V4’s architecture reintroduces reentrancy risk that previous Uniswap versions largely eliminated. Hooks perform external calls during pool operations, creating reentry points. Native token support introduces additional reentrancy risks during msg.value handling and settlement operations. Attackers can exploit reentrancy to manipulate pool or hook state, especially in systems with custom accounting logic that depends on token balances. Implement comprehensive reentrancy guards and follow checks-effects-interactions patterns.
Custom AMM Logic Breaking the Invariant
A key aspect of the design space unlocked by custom accounting is the ability to override the swap logic of the underlying concentrated liquidity model. While this pioneering feature allows for innovative AMM designs to be built on top of the primitive Uniswap V4 hook architecture, it greatly increases the attack surface and increases the potential for subtle arithmetic bugs that can completely break the core functionality. In some audited instances it was possible to execute free swaps — providing zero input tokens but receiving a non-zero amount of output tokens.
Research exploring security risks in Uniswap V4’s hook mechanism discovered that 36% of the hooks studied contained at least one vulnerability that could lead to loss of funds or denial of service, demonstrating that hook security remains an active area of concern for the ecosystem.
AMM Security Audit Checklist
Price oracle and manipulation
- No AMM spot price (
slot0,getReserves) is used as a price oracle without a TWAP - TWAP window is long enough given pool liquidity depth and the value at stake
- Pool observation cardinality is initialized before any TWAP is consumed
- Oracle is validated for staleness and zero price before use
Constant product and reserve integrity
- The k-invariant (
x * y ≥ k) is maintained after every swap, including fees - Reserve state is updated atomically before any external call
- Fee-on-transfer tokens are handled with balance-diff accounting, not amount assumptions
- Direct token donations cannot break share-price or reserve invariants
Slippage and sandwich protection
- All swap functions accept a
minAmountOutor equivalent slippage guard - Deadline parameter is validated and enforced on every swap
- Liquidity operations accept
minAmount0andminAmount1guards
MEV and JIT liquidity
- The protocol acknowledges JIT liquidity risk and has documented it
- Fee tiers are appropriate for the expected volatility of the traded asset
- If hooks are used, the hook’s own fee logic cannot be front-run to drain LPs
Uniswap V4 hooks (if applicable)
- Hook’s custom accounting overrides are reviewed for free-swap and drain vectors
- Hook callbacks follow CEI — no external calls before pool state is settled
- Hook permissions are minimally scoped
- Hook has been audited independently from pool logic