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
Choose optimal price ranges
Concentrate liquidity around current price for higher fee earnings. Use the ShibaSwap interface to visualize price ranges and liquidity distribution.
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.
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.
Collect fees regularly
Collect accumulated fees to compound your earnings. Regular fee collection helps maximize your returns from liquidity provision.
Risk Considerations
Risk : Token price changes can result in losses compared to holding.Mitigation :
Choose stable pairs or similar tokens
Use narrow price ranges for higher fee earnings
Monitor and rebalance positions
Risk : High gas costs can eat into profits from small positions.Mitigation :
Batch operations when possible
Use appropriate gas optimization techniques
Consider gas costs when choosing position size
Risk : Potential bugs or vulnerabilities in contracts.Mitigation :
Use audited contracts
Start with small amounts
Monitor for security updates