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

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

0x59dA (CD Security)
February 24, 2025
17 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.