Providing liquidity to ShibaSwap v2 allows you to earn trading fees while supporting the ecosystem. This guide covers setting up contracts, minting positions, collecting fees, and managing your liquidity.

Overview

ShibaSwap v2 uses concentrated liquidity positions represented as NFTs, similar to Uniswap v3. Each position has a specific price range where it provides liquidity and earns fees.
Liquidity providers earn a portion of trading fees proportional to their share of the pool and the time their liquidity is in range.

Setting Up Your Contract

First, set up a contract to interact with ShibaSwap v2’s liquidity management system:
contracts/LiquidityProvider.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma abicoder v2;

import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

interface INonfungiblePositionManager {
    struct MintParams {
        address token0;
        address token1;
        uint24 fee;
        int24 tickLower;
        int24 tickUpper;
        uint256 amount0Desired;
        uint256 amount1Desired;
        uint256 amount0Min;
        uint256 amount1Min;
        address recipient;
        uint256 deadline;
    }
    
    function mint(MintParams calldata params) external payable returns (
        uint256 tokenId,
        uint128 liquidity,
        uint256 amount0,
        uint256 amount1
    );
    
    function positions(uint256 tokenId) external view returns (
        uint96 nonce,
        address operator,
        address token0,
        address token1,
        uint24 fee,
        int24 tickLower,
        int24 tickUpper,
        uint128 liquidity,
        uint256 feeGrowthInside0LastX128,
        uint256 feeGrowthInside1LastX128,
        uint128 tokensOwed0,
        uint128 tokensOwed1
    );
}

contract LiquidityProvider is IERC721Receiver {
    using SafeERC20 for IERC20;
    
    INonfungiblePositionManager public immutable positionManager;
    
    // Token addresses for ShibaSwap v2
    address public constant SHIB = 0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE;
    address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    
    uint24 public constant poolFee = 3000; // 0.3%
    
    struct Deposit {
        address owner;
        uint128 liquidity;
        address token0;
        address token1;
    }
    
    mapping(uint256 => Deposit) public deposits;
    
    constructor(address _positionManager) {
        positionManager = INonfungiblePositionManager(_positionManager);
    }
    
    function onERC721Received(
        address operator,
        address,
        uint256 tokenId,
        bytes calldata
    ) external override returns (bytes4) {
        _createDeposit(operator, tokenId);
        return this.onERC721Received.selector;
    }
    
    function _createDeposit(address owner, uint256 tokenId) internal {
        (, , address token0, address token1, , , , uint128 liquidity, , , , ) =
            positionManager.positions(tokenId);
        
        deposits[tokenId] = Deposit({
            owner: owner,
            liquidity: liquidity,
            token0: token0,
            token1: token1
        });
    }
}

Minting a New Position

Create a new liquidity position by minting an NFT:
contracts/LiquidityProvider.sol
// ... existing code ...

function mintNewPosition(
    uint256 amount0Desired,
    uint256 amount1Desired,
    int24 tickLower,
    int24 tickUpper
) external returns (
    uint256 tokenId,
    uint128 liquidity,
    uint256 amount0,
    uint256 amount1
) {
    // Transfer tokens from user
    IERC20(SHIB).safeTransferFrom(msg.sender, address(this), amount0Desired);
    IERC20(WETH).safeTransferFrom(msg.sender, address(this), amount1Desired);
    
    // Approve position manager
    IERC20(SHIB).approve(address(positionManager), amount0Desired);
    IERC20(WETH).approve(address(positionManager), amount1Desired);
    
    INonfungiblePositionManager.MintParams memory params =
        INonfungiblePositionManager.MintParams({
            token0: SHIB,
            token1: WETH,
            fee: poolFee,
            tickLower: tickLower,
            tickUpper: tickUpper,
            amount0Desired: amount0Desired,
            amount1Desired: amount1Desired,
            amount0Min: 0,
            amount1Min: 0,
            recipient: address(this),
            deadline: block.timestamp
        });
    
    (tokenId, liquidity, amount0, amount1) = positionManager.mint(params);
    
    // Create deposit record
    _createDeposit(msg.sender, tokenId);
    
    // Refund excess tokens
    if (amount0 < amount0Desired) {
        uint256 refund0 = amount0Desired - amount0;
        IERC20(SHIB).safeTransfer(msg.sender, refund0);
    }
    
    if (amount1 < amount1Desired) {
        uint256 refund1 = amount1Desired - amount1;
        IERC20(WETH).safeTransfer(msg.sender, refund1);
    }
}

Collecting Fees

Collect accumulated trading fees from your position:
contracts/LiquidityProvider.sol
// ... existing code ...

interface INonfungiblePositionManager {
    // ... existing interface ...
    
    struct CollectParams {
        uint256 tokenId;
        address recipient;
        uint128 amount0Max;
        uint128 amount1Max;
    }
    
    function collect(CollectParams calldata params) external payable returns (
        uint256 amount0,
        uint256 amount1
    );
}

function collectFees(uint256 tokenId) external returns (uint256 amount0, uint256 amount1) {
    require(deposits[tokenId].owner == msg.sender, "Not position owner");
    
    INonfungiblePositionManager.CollectParams memory params =
        INonfungiblePositionManager.CollectParams({
            tokenId: tokenId,
            recipient: msg.sender,
            amount0Max: type(uint128).max,
            amount1Max: type(uint128).max
        });
    
    (amount0, amount1) = positionManager.collect(params);
}

Decreasing Liquidity

Remove liquidity from your position:
contracts/LiquidityProvider.sol
// ... existing code ...

interface INonfungiblePositionManager {
    // ... existing interface ...
    
    struct DecreaseLiquidityParams {
        uint256 tokenId;
        uint128 liquidity;
        uint256 amount0Min;
        uint256 amount1Min;
        uint256 deadline;
    }
    
    function decreaseLiquidity(DecreaseLiquidityParams calldata params) external payable returns (
        uint256 amount0,
        uint256 amount1
    );
}

function decreaseLiquidity(
    uint256 tokenId,
    uint128 liquidity,
    uint256 amount0Min,
    uint256 amount1Min
) external returns (uint256 amount0, uint256 amount1) {
    require(deposits[tokenId].owner == msg.sender, "Not position owner");
    
    INonfungiblePositionManager.DecreaseLiquidityParams memory params =
        INonfungiblePositionManager.DecreaseLiquidityParams({
            tokenId: tokenId,
            liquidity: liquidity,
            amount0Min: amount0Min,
            amount1Min: amount1Min,
            deadline: block.timestamp
        });
    
    (amount0, amount1) = positionManager.decreaseLiquidity(params);
    
    // Update deposit record
    deposits[tokenId].liquidity -= liquidity;
}

Increasing Liquidity

Add more liquidity to an existing position:
contracts/LiquidityProvider.sol
// ... existing code ...

interface INonfungiblePositionManager {
    // ... existing interface ...
    
    struct IncreaseLiquidityParams {
        uint256 tokenId;
        uint256 amount0Desired;
        uint256 amount1Desired;
        uint256 amount0Min;
        uint256 amount1Min;
        uint256 deadline;
    }
    
    function increaseLiquidity(IncreaseLiquidityParams calldata params) external payable returns (
        uint128 liquidity,
        uint256 amount0,
        uint256 amount1
    );
}

function increaseLiquidity(
    uint256 tokenId,
    uint256 amount0Desired,
    uint256 amount1Desired,
    uint256 amount0Min,
    uint256 amount1Min
) external returns (uint128 liquidity, uint256 amount0, uint256 amount1) {
    require(deposits[tokenId].owner == msg.sender, "Not position owner");
    
    // Transfer tokens from user
    IERC20(SHIB).safeTransferFrom(msg.sender, address(this), amount0Desired);
    IERC20(WETH).safeTransferFrom(msg.sender, address(this), amount1Desired);
    
    // Approve position manager
    IERC20(SHIB).approve(address(positionManager), amount0Desired);
    IERC20(WETH).approve(address(positionManager), amount1Desired);
    
    INonfungiblePositionManager.IncreaseLiquidityParams memory params =
        INonfungiblePositionManager.IncreaseLiquidityParams({
            tokenId: tokenId,
            amount0Desired: amount0Desired,
            amount1Desired: amount1Desired,
            amount0Min: amount0Min,
            amount1Min: amount1Min,
            deadline: block.timestamp
        });
    
    (liquidity, amount0, amount1) = positionManager.increaseLiquidity(params);
    
    // Update deposit record
    deposits[tokenId].liquidity += liquidity;
    
    // Refund excess tokens
    if (amount0 < amount0Desired) {
        uint256 refund0 = amount0Desired - amount0;
        IERC20(SHIB).safeTransfer(msg.sender, refund0);
    }
    
    if (amount1 < amount1Desired) {
        uint256 refund1 = amount1Desired - amount1;
        IERC20(WETH).safeTransfer(msg.sender, refund1);
    }
}

JavaScript Integration

Here’s how to interact with your liquidity contract:
scripts/liquidity-management.js
const { ethers } = require("hardhat");

async function manageLiquidity() {
    const [signer] = await ethers.getSigners();
    
    // Contract addresses
    const LIQUIDITY_PROVIDER_ADDRESS = "YOUR_DEPLOYED_CONTRACT_ADDRESS";
    const SHIB_ADDRESS = "0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE";
    const WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
    
    // Contract instance
    const liquidityProvider = new ethers.Contract(LIQUIDITY_PROVIDER_ADDRESS, [
        "function mintNewPosition(uint256 amount0Desired, uint256 amount1Desired, int24 tickLower, int24 tickUpper) external returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)",
        "function collectFees(uint256 tokenId) external returns (uint256 amount0, uint256 amount1)",
        "function decreaseLiquidity(uint256 tokenId, uint128 liquidity, uint256 amount0Min, uint256 amount1Min) external returns (uint256 amount0, uint256 amount1)",
        "function increaseLiquidity(uint256 tokenId, uint256 amount0Desired, uint256 amount1Desired, uint256 amount0Min, uint256 amount1Min) external returns (uint128 liquidity, uint256 amount0, uint256 amount1)"
    ], signer);
    
    // Mint new position
    const amount0Desired = ethers.utils.parseEther("1000000"); // 1M SHIB
    const amount1Desired = ethers.utils.parseEther("1"); // 1 WETH
    const tickLower = -1000; // Price range lower bound
    const tickUpper = 1000;  // Price range upper bound
    
    try {
        // Approve tokens
        const shibToken = new ethers.Contract(SHIB_ADDRESS, [
            "function approve(address spender, uint256 amount) external returns (bool)"
        ], signer);
        
        const wethToken = new ethers.Contract(WETH_ADDRESS, [
            "function approve(address spender, uint256 amount) external returns (bool)"
        ], signer);
        
        await shibToken.approve(LIQUIDITY_PROVIDER_ADDRESS, amount0Desired);
        await wethToken.approve(LIQUIDITY_PROVIDER_ADDRESS, amount1Desired);
        
        // Mint position
        const tx = await liquidityProvider.mintNewPosition(
            amount0Desired,
            amount1Desired,
            tickLower,
            tickUpper
        );
        
        const receipt = await tx.wait();
        console.log("Position minted successfully!");
        
        // Extract tokenId from events (you'll need to parse the event)
        const tokenId = 1; // This would come from the mint event
        
        // Collect fees (after some time)
        const fees = await liquidityProvider.collectFees(tokenId);
        console.log("Fees collected:", ethers.utils.formatEther(fees[0]), "SHIB,", ethers.utils.formatEther(fees[1]), "WETH");
        
    } catch (error) {
        console.error("Error:", error.message);
    }
}

manageLiquidity();

Best Practices

1

Choose optimal price ranges

Concentrate liquidity around current price for higher fee earnings.
Use the ShibaSwap interface to visualize price ranges and liquidity distribution.
2

Monitor your positions

Regularly check if your liquidity is still in range and earning fees.
Out-of-range positions don’t earn fees and may suffer impermanent loss.
3

Set appropriate slippage

Use minimum amounts to protect against price movements during transactions.
Higher slippage tolerance increases success rate but may result in worse pricing.
4

Collect fees regularly

Collect accumulated fees to compound your earnings.
Regular fee collection helps maximize your returns from liquidity provision.

Risk Considerations