Top 5 Smart Contract Security Bugs (Jan 2025): Issues in Protocols Interacting with Uniswap V3 Liquidity & Cross-Chain Swaps
Back to Blog

Top 5 Smart Contract Security Bugs (Jan 2025): Issues in Protocols Interacting with Uniswap V3 Liquidity & Cross-Chain Swaps

0x59dA (CD Security)
Security
February 24, 2025
4 min read

Welcome to the first post in our new monthly series, where we share five of the most intriguing findings from recent audits. In each article, you’ll discover common pitfalls, unique vulnerabilities, and practical fixes. Our goal is not only to help make protocols safer but also to educate more teams and auditors so we can all work together toward a safer ecosystem.

This article identifies key issues in different protocols interacting with Uniswap V3 liquidity positions and cross-chain swaps, including withdrawal limitations, reward distribution inefficiencies, and slippage vulnerabilities. We outline these problems and provide solutions to improve protocol security.

Issue 1: Lack of Withdrawal Mechanism for Deposited Uni V3 NFT Liquidity Positions

Impact: Medium
Likelihood: High

Description

The protocol generates yield for stakers by locking Uniswap V3 liquidity position NFTs. There is a function that lets the NFT owner add more liquidity to the position:

mapping(uint256 => address) public nftOwners;

function depositNFT(uint256 tokenId) external {
uniV3PositionNft.safeTransferFrom(msg.sender, address(this), tokenId);
nftOwners[tokenId] = msg.sender;
}

function addLiquidity(uint256 tokenId, uint256 amount0, uint256 amount1) external {
require(nftOwners[tokenId] == msg.sender, "Not the NFT owner");
// Logic to add liquidity
}

However, there is no function that lets the owner decrease position liquidity or reclaim the NFT. Without a withdrawal mechanism, deposited liquidity positions become permanently locked, restricting liquidity management and repositioning.

Recommendation

Introduce a withdrawal function that allows owners to retrieve their liquidity positions:

function withdrawNFT(uint256 tokenId) external {
require(ownerOf(tokenId) == msg.sender, "Not the NFT owner");
nft.safeTransferFrom(address(this), msg.sender, tokenId);
}

Also consider allowing partial liquidity decreases for flexibility.


Issue 2: Undistributed Rewards May Be Stuck in the Contract if No Stakers Exist in a Cycle

Impact: Medium
Likelihood: Medium

Description

StakeVault receives rewards from a generator contract and distributes them across pools in 30-day cycles. When no stakers exist in a cycle, rewards allocated for that cycle become permanently stuck, as the mechanism doesn’t forward them to future cycles.

Recommendation

Add a function to sweep or carry forward unclaimed rewards:

function sweepUnclaimedRewards(uint256 cycleId) external onlyAdmin {
if (cycleShares[cycleId] != 0) revert("Cycle has stakers");

uint256 unclaimed = cycleRewards[cycleId];
require(unclaimed > 0, "No unclaimed rewards");
cycleRewards[currentCycleId] += unclaimed;
delete cycleRewards[cycleId];
}

This ensures rewards from empty cycles are never permanently locked.


Issue 3: Addressing Allocation Point Misconfiguration in Liquidity Manager

Impact: High
Likelihood: Medium

Description

The LiquidityFarmManager manages Uniswap V3 liquidity farms and distributes incentives based on liquidity and time. A flaw in registerFarm allows farms to be registered with zero allocation points, leaving lastRewardTime set to zero. Once allocation points are later added, rewards from past periods are incorrectly accumulated, leading to distorted emissions.

Recommendation

Enforce non-zero allocation points for all new farms:

function registerFarm(AddFarmParams calldata params) external restricted {
require(params.allocPoints > 0, "Allocation points must be greater than zero");
// Continue registration logic...
}

This ensures fair and consistent reward distribution.


Issue 4: Missing Slippage on Add Liquidity Function Can Lead to Stuck Funds

Impact: Medium
Likelihood: Medium

Description

When users add liquidity to existing Uniswap V3 positions, slippage parameters (amount0Min and amount1Min) are hardcoded to zero:

INonfungiblePositionManager.IncreaseLiquidityParams memory params = INonfungiblePositionManager.IncreaseLiquidityParams({
tokenId: tokenId,
amount0Desired: amountAdd0,
amount1Desired: amountAdd1,
amount0Min: 0, // <= Vulnerable
amount1Min: 0, // <= Vulnerable
deadline: block.timestamp
});

Without slippage protection, MEV bots can front-run users, executing transactions at manipulated prices and causing stuck or lost funds.

Recommendation

Allow users to specify minimum amounts instead of using zero:

amount0Min: userProvidedMin0,
amount1Min: userProvidedMin1,

Following Uniswap’s documentation, slippage limits are crucial to prevent manipulation.


Issue 5: Swap Doesn’t Correctly Check for Slippage

Impact: Low
Likelihood: High

Description

In the protocol’s cross-chain swap flow, a fee is deducted after the slippage check on the source chain. This means the actual bridged amount can be smaller than the user’s specified minimum — violating slippage protection.

Recommendation

Perform the slippage check after applying the fee:

sourceAmountOut -= sourceAmountOut * feeBps / 10000;
require(sourceAmountOut >= minAmountOutSrc, "Slippage exceeded");

This ensures user-defined thresholds remain accurate even after fees.


Outro

We highlighted five critical issues affecting Uniswap V3 integration and cross-chain swaps. Implementing these fixes — including withdrawal mechanisms, slippage protection, and better allocation validation — will enhance both security and user experience.

Stay tuned for next month’s article, where we’ll share more real-world vulnerabilities and best practices for securing the Web3 ecosystem.

Until then, stay secure and keep building! 🚀

Don’t forget to follow CD Security on Twitter, as well as the author chrisdior.eth, for daily Web3 insights and security tips.