Liquidity Provision in Perpetual DEXs: Market Making and LP Strategies
Attract and retain liquidity for perpetual DEXs. Learn market maker incentives, LP reward structures, and passive liquidity provision strategies.
Liquidity is the lifeblood of any perpetual DEX. Without deep, consistent liquidity, traders face high slippage, wide spreads, and poor execution—driving them to competitors. Attracting and retaining liquidity providers (LPs) requires sophisticated incentive structures, risk management, and competitive returns.
This guide explores liquidity provision mechanisms for perpetual DEXs, from professional market making to passive LP strategies.
The Liquidity Challenge
Why Perpetual DEXs Need Deep Liquidity:
- Low slippage for large trades
- Tight bid-ask spreads
- Efficient price discovery
- Reduced liquidation cascades
Unique Challenges:
- Impermanent loss from leveraged positions
- One-sided exposure during trends
- Funding rate complexity
- Oracle dependency
Professional Market Maker Programs
Market Maker Registration
contract MarketMakerProgram {
struct MarketMaker {
address mmAddress;
bool isApproved;
uint256 maxExposure;
uint256 currentExposure;
uint256 performanceScore;
uint256 rebateTier;
uint256 stakedAmount;
uint256 lastActivity;
}
mapping(address => MarketMaker) public marketMakers;
mapping(address => bool) public approvedMarketMakers;
uint256 public constant MIN_STAKE = 100000e18; // $100k
uint256 public constant APPROVAL_THRESHOLD = 5; // 5 of 9 multisig
event MarketMakerApproved(address indexed mm, uint256 maxExposure);
event MarketMakerRemoved(address indexed mm, string reason);
function applyAsMarketMaker(
uint256 maxExposure,
bytes calldata credentials
) external returns (bytes32 applicationId) {
require(maxExposure >= MIN_STAKE * 10, "Exposure too small");
applicationId = keccak256(abi.encodePacked(
msg.sender,
block.timestamp,
maxExposure
));
// Store application for review
pendingApplications[applicationId] = MMApplication({
applicant: msg.sender,
maxExposure: maxExposure,
credentials: credentials,
approvals: 0,
status: ApplicationStatus.PENDING
});
emit ApplicationSubmitted(applicationId, msg.sender);
}
function approveMarketMaker(bytes32 applicationId) external onlyMultisig {
MMApplication storage app = pendingApplications[applicationId];
require(app.status == ApplicationStatus.PENDING, "Invalid status");
app.approvals++;
if (app.approvals >= APPROVAL_THRESHOLD) {
app.status = ApplicationStatus.APPROVED;
marketMakers[app.applicant] = MarketMaker({
mmAddress: app.applicant,
isApproved: true,
maxExposure: app.maxExposure,
currentExposure: 0,
performanceScore: 0,
rebateTier: 1,
stakedAmount: 0,
lastActivity: block.timestamp
});
approvedMarketMakers[app.applicant] = true;
emit MarketMakerApproved(app.applicant, app.maxExposure);
}
}
function stakeAsMarketMaker(uint256 amount) external onlyApprovedMM {
MarketMaker storage mm = marketMakers[msg.sender];
collateralToken.transferFrom(msg.sender, address(this), amount);
mm.stakedAmount += amount;
// Increase max exposure based on stake
mm.maxExposure = mm.stakedAmount * 10; // 10x leverage on stake
emit StakeIncreased(msg.sender, amount);
}
}
Market Maker Rebate Structure
contract MarketMakerRebates {
struct RebateTier {
uint256 minVolume24h;
uint256 makerRebateBps; // Rebate on maker orders
uint256 spreadImprovementReq; // Required spread improvement
uint256 uptimeRequirement; // Required uptime %
}
RebateTier[] public rebateTiers;
struct MMPerformance {
address mm;
uint256 volume24h;
uint256 makerVolume24h;
uint256 avgSpread;
uint256 uptime;
uint256 currentTier;
uint256 pendingRebates;
}
mapping(address => MMPerformance) public mmPerformance;
constructor() {
// Tier 1: Basic
rebateTiers.push(RebateTier({
minVolume24h: 1000000e18, // $1M
makerRebateBps: 2, // 0.02%
spreadImprovementReq: 10, // 10% better than base
uptimeRequirement: 80 // 80%
}));
// Tier 2: Active
rebateTiers.push(RebateTier({
minVolume24h: 5000000e18, // $5M
makerRebateBps: 3, // 0.03%
spreadImprovementReq: 20,
uptimeRequirement: 85
}));
// Tier 3: Professional
rebateTiers.push(RebateTier({
minVolume24h: 20000000e18, // $20M
makerRebateBps: 5, // 0.05%
spreadImprovementReq: 30,
uptimeRequirement: 90
}));
// Tier 4: Elite
rebateTiers.push(RebateTier({
minVolume24h: 100000000e18, // $100M
makerRebateBps: 8, // 0.08%
spreadImprovementReq: 40,
uptimeRequirement: 95
}));
}
function recordTrade(
address mm,
bool isMaker,
uint256 volume,
uint256 spread
) external {
MMPerformance storage perf = mmPerformance[mm];
if (isMaker) {
perf.makerVolume24h += volume;
}
perf.volume24h += volume;
// Update rolling average spread
perf.avgSpread = ((perf.avgSpread * 99) + spread) / 100;
}
function calculateRebateTier(address mm) public view returns (uint256) {
MMPerformance memory perf = mmPerformance[mm];
for (uint i = rebateTiers.length - 1; i >= 0; i--) {
RebateTier memory tier = rebateTiers[i];
if (perf.makerVolume24h >= tier.minVolume24h &&
perf.uptime >= tier.uptimeRequirement) {
return i;
}
}
return 0;
}
function claimRebates() external {
MMPerformance storage perf = mmPerformance[msg.sender];
uint256 rebateAmount = perf.pendingRebates;
require(rebateAmount > 0, "No rebates to claim");
perf.pendingRebates = 0;
collateralToken.transfer(msg.sender, rebateAmount);
emit RebatesClaimed(msg.sender, rebateAmount);
}
function distributeRebatesDaily() external {
address[] memory mms = getActiveMarketMakers();
for (uint i = 0; i < mms.length; i++) {
MMPerformance storage perf = mmPerformance[mms[i]];
uint256 tier = calculateRebateTier(mms[i]);
uint256 rebateRate = rebateTiers[tier].makerRebateBps;
uint256 rebate = (perf.makerVolume24h * rebateRate) / 10000;
perf.pendingRebates += rebate;
// Reset daily metrics
perf.volume24h = 0;
perf.makerVolume24h = 0;
}
}
}
Passive Liquidity Pools
vAMM Liquidity Provision
contract vAMMLiquidityPool {
struct LPPosition {
address lp;
uint256 liquidityShares;
uint256 entryTimestamp;
uint256 initialVirtualPrice;
int256 fundingEarned;
uint256 feesEarned;
}
mapping(address => LPPosition) public lpPositions;
uint256 public totalLiquidityShares;
uint256 public totalRealLiquidity;
// Virtual pool state
uint256 public virtualBaseReserve;
uint256 public virtualQuoteReserve;
uint256 public constantK;
uint256 public accumulatedFundingPerShare;
uint256 public accumulatedFeesPerShare;
function addLiquidity(uint256 amount) external {
require(amount >= MIN_LIQUIDITY, "Below minimum");
collateralToken.transferFrom(msg.sender, address(this), amount);
uint256 shares = totalLiquidityShares == 0
? amount
: (amount * totalLiquidityShares) / totalRealLiquidity;
LPPosition storage position = lpPositions[msg.sender];
if (position.lp == address(0)) {
// New LP
position.lp = msg.sender;
position.entryTimestamp = block.timestamp;
position.initialVirtualPrice = getVirtualPrice();
} else {
// Existing LP - claim pending rewards first
_claimRewards(msg.sender);
}
position.liquidityShares += shares;
totalLiquidityShares += shares;
totalRealLiquidity += amount;
// Add to virtual reserves
virtualQuoteReserve += amount;
virtualBaseReserve = constantK / virtualQuoteReserve;
emit LiquidityAdded(msg.sender, amount, shares);
}
function removeLiquidity(uint256 shares) external {
LPPosition storage position = lpPositions[msg.sender];
require(position.liquidityShares >= shares, "Insufficient shares");
// Claim all pending rewards
_claimRewards(msg.sender);
// Calculate amount to return
uint256 amount = (shares * totalRealLiquidity) / totalLiquidityShares;
// Apply IL protection
uint256 currentVirtualPrice = getVirtualPrice();
uint256 priceRatio = (currentVirtualPrice * 1e18) / position.initialVirtualPrice;
// If price moved against LP, apply protection
if (priceRatio < 1e18) {
uint256 il = amount - ((amount * priceRatio) / 1e18);
uint256 protectedAmount = getInsuranceCover(il);
amount += protectedAmount;
}
position.liquidityShares -= shares;
totalLiquidityShares -= shares;
totalRealLiquidity -= amount;
// Remove from virtual reserves
virtualQuoteReserve -= amount;
virtualBaseReserve = constantK / virtualQuoteReserve;
collateralToken.transfer(msg.sender, amount);
emit LiquidityRemoved(msg.sender, amount, shares);
}
function _claimRewards(address lp) internal {
LPPosition storage position = lpPositions[lp];
// Calculate funding rewards
uint256 fundingShare = (position.liquidityShares * accumulatedFundingPerShare) / 1e18
- uint256(position.fundingEarned);
position.fundingEarned += int256(fundingShare);
// Calculate trading fee rewards
uint256 feeShare = (position.liquidityShares * accumulatedFeesPerShare) / 1e18
- position.feesEarned;
position.feesEarned += feeShare;
// Transfer rewards
uint256 totalRewards = fundingShare + feeShare;
if (totalRewards > 0) {
collateralToken.transfer(lp, totalRewards);
}
}
function distributeFunding(int256 fundingPayment) external {
// Funding paid by traders is distributed to LPs
if (fundingPayment > 0) {
uint256 fundingPerShare = (uint256(fundingPayment) * 1e18) / totalLiquidityShares;
accumulatedFundingPerShare += fundingPerShare;
}
}
function distributeFees(uint256 fees) external {
uint256 feesPerShare = (fees * 1e18) / totalLiquidityShares;
accumulatedFeesPerShare += feesPerShare;
}
}
Impermanent Loss Protection
contract ILProtection {
uint256 public constant PROTECTION_PERIOD = 30 days;
uint256 public constant MAX_PROTECTION = 5000; // 50% of IL covered
struct ProtectedPosition {
uint256 entryTimestamp;
uint256 entryPrice;
uint256 size;
uint256 protectionUsed;
}
mapping(address => ProtectedPosition) public protectedPositions;
uint256 public insuranceFund;
function calculateIL(
uint256 entryPrice,
uint256 exitPrice,
uint256 size
) public pure returns (uint256) {
// IL = 2 * sqrt(price_ratio) / (1 + price_ratio) - 1
uint256 priceRatio = (exitPrice * 1e18) / entryPrice;
uint256 sqrtRatio = sqrt(priceRatio);
uint256 il = (2 * sqrtRatio * 1e18) / (1e18 + priceRatio) - 1e18;
return (size * uint256(-int256(il))) / 1e18;
}
function claimILProtection(address lp) external {
ProtectedPosition storage pos = protectedPositions[lp];
require(
block.timestamp >= pos.entryTimestamp + PROTECTION_PERIOD,
"Protection period not over"
);
uint256 currentPrice = getCurrentPrice();
uint256 il = calculateIL(pos.entryPrice, currentPrice, pos.size);
uint256 protectionAmount = (il * MAX_PROTECTION) / 10000;
protectionAmount = protectionAmount - pos.protectionUsed;
require(protectionAmount <= insuranceFund, "Insufficient insurance");
insuranceFund -= protectionAmount;
pos.protectionUsed += protectionAmount;
collateralToken.transfer(lp, protectionAmount);
emit ILProtectionClaimed(lp, protectionAmount);
}
function sqrt(uint256 x) internal pure returns (uint256) {
if (x == 0) return 0;
uint256 z = (x + 1) / 2;
uint256 y = x;
while (z < y) {
y = z;
z = (x / z + z) / 2;
}
return y;
}
}
Dynamic Incentive Adjustment
contract DynamicLiquidityIncentives {
struct LiquidityMetrics {
uint256 totalLiquidity;
uint256 utilizationRate;
uint256 avgSpread;
uint256 volume24h;
}
LiquidityMetrics public currentMetrics;
// Target metrics
uint256 public targetLiquidity = 10000000e18; // $10M
uint256 public targetSpread = 50; // 0.5%
uint256 public targetUtilization = 7000; // 70%
// Incentive parameters
uint256 public baseEmissionRate = 1000e18; // Tokens per day
uint256 public currentEmissionRate;
function adjustIncentives() external {
// Calculate incentive multiplier based on metrics vs targets
uint256 liquidityRatio = (currentMetrics.totalLiquidity * 10000) / targetLiquidity;
uint256 spreadRatio = currentMetrics.avgSpread > targetSpread
? 15000 // Spread too wide, increase incentives
: (targetSpread * 10000) / currentMetrics.avgSpread;
// If liquidity is low or spread is wide, increase incentives
if (liquidityRatio < 8000 || currentMetrics.avgSpread > targetSpread * 2) {
// Boost incentives by up to 3x
currentEmissionRate = baseEmissionRate * 3;
} else if (liquidityRatio > 12000 && currentMetrics.avgSpread < targetSpread / 2) {
// Over-liquidity, can reduce incentives
currentEmissionRate = baseEmissionRate / 2;
} else {
// Normal range
currentEmissionRate = baseEmissionRate;
}
emit IncentiveAdjusted(currentEmissionRate);
}
function calculateLPRewards(address lp) external view returns (uint256) {
LPPosition memory position = getLPPosition(lp);
uint256 timeStaked = block.timestamp - position.entryTime;
// Base rewards based on share of pool
uint256 baseReward = (position.shares * currentEmissionRate * timeStaked) /
(totalShares * 1 days);
// Multiplier for long-term LPs
uint256 timeMultiplier;
if (timeStaked < 7 days) {
timeMultiplier = 1e18;
} else if (timeStaked < 30 days) {
timeMultiplier = 12e17; // 1.2x
} else if (timeStaked < 90 days) {
timeMultiplier = 15e17; // 1.5x
} else {
timeMultiplier = 2e18; // 2x
}
return (baseReward * timeMultiplier) / 1e18;
}
}
Bootstrapping Liquidity
contract LiquidityBootstrap {
uint256 public bootstrapPhaseEnd;
uint256 public initialLiquidityTarget = 5000000e18; // $5M
uint256 public currentBootstrappedLiquidity;
struct BootstrapIncentive {
uint256 bonusMultiplier;
uint256 lockPeriod;
uint256 earlyBirdAllocation;
}
BootstrapIncentive public bootstrapConfig;
function startBootstrapPhase() external onlyGovernance {
bootstrapPhaseEnd = block.timestamp + 30 days;
bootstrapConfig = BootstrapIncentive({
bonusMultiplier: 3, // 3x rewards
lockPeriod: 90 days, // 3 month lock
earlyBirdAllocation: 1000000e18 // 1M tokens
});
}
function bootstrapDeposit(uint256 amount) external {
require(block.timestamp < bootstrapPhaseEnd, "Bootstrap ended");
require(
currentBootstrappedLiquidity < initialLiquidityTarget,
"Target reached"
);
// Transfer collateral
collateralToken.transferFrom(msg.sender, address(this), amount);
// Mint bonus tokens
uint256 bonusTokens = (amount * bootstrapConfig.bonusMultiplier);
require(
bonusTokens <= bootstrapConfig.earlyBirdAllocation,
"Insufficient early bird allocation"
);
bootstrapConfig.earlyBirdAllocation -= bonusTokens;
// Lock liquidity
lockedLiquidity[msg.sender] = LockedLiquidity({
amount: amount,
unlockTime: block.timestamp + bootstrapConfig.lockPeriod,
bonusTokens: bonusTokens
});
currentBootstrappedLiquidity += amount;
emit BootstrapDeposit(msg.sender, amount, bonusTokens);
}
function unlockBootstrapLiquidity() external {
LockedLiquidity storage lock = lockedLiquidity[msg.sender];
require(block.timestamp >= lock.unlockTime, "Still locked");
// Return principal
collateralToken.transfer(msg.sender, lock.amount);
// Transfer bonus tokens
rewardToken.transfer(msg.sender, lock.bonusTokens);
delete lockedLiquidity[msg.sender];
emit BootstrapUnlocked(msg.sender, lock.amount);
}
}
Best Practices
1. Competitive Returns
- Monitor competitor LP yields
- Adjust incentives dynamically
- Offer IL protection
2. Risk Management
- Cap single LP exposure
- Insurance funds for tail risk
- Gradual liquidity addition
3. Transparent Reporting
- Real-time LP dashboards
- Historical performance data
- Clear fee structures
4. Retention Strategies
- Time-weighted rewards
- Loyalty programs
- Governance participation rights
5. Emergency Procedures
- Pause deposits during volatility
- Emergency withdrawal mechanisms
- Graduated fee adjustments
Conclusion
Successful liquidity provision programs balance:
- Attracting LPs: Competitive yields, IL protection, bonuses
- Retaining LPs: Loyalty rewards, governance rights, long-term incentives
- Risk Management: Insurance funds, exposure limits, circuit breakers
- Sustainability: Dynamic incentives, protocol revenue sharing
The perpetual DEXs with the deepest liquidity will be those that treat LPs as partners—not just capital sources—and build sustainable, mutually beneficial relationships.