Skip to main content

Liquidity Pool Integration Guide

Overview​

This guide provides comprehensive integration examples for incorporating MCAP's Liquidity Cache system into gaming platforms, DeFi applications, and other Web3 projects. The LiquidityCache contract provides both liquidity pool functionality and staking mechanisms.

Quick Start Integration​

Prerequisites​

npm install ethers @mcap/sdk
# or
yarn add ethers @mcap/sdk

Basic Setup​

import { ethers } from 'ethers';
import { LiquidityCacheABI } from '@mcap/contracts';

// Contract configuration
const LIQUIDITY_CACHE_ADDRESS = "0x..."; // Your deployed contract address
const UNDERLYING_TOKEN_ADDRESS = "0x..."; // ERC20 token address

// Initialize provider and signer
const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
const signer = provider.getSigner();

// Contract instances
const liquidityCache = new ethers.Contract(
LIQUIDITY_CACHE_ADDRESS,
LiquidityCacheABI,
signer
);

const underlyingToken = new ethers.Contract(
UNDERLYING_TOKEN_ADDRESS,
['function approve(address spender, uint256 amount) external returns (bool)',
'function balanceOf(address account) external view returns (uint256)',
'function allowance(address owner, address spender) external view returns (uint256)'],
signer
);

Gaming Platform Integration​

For Game Developers​

1. Adding Game Profits to Pool​

class GameProfitManager {
constructor(liquidityCacheAddress, tokenAddress) {
this.liquidityCache = new ethers.Contract(
liquidityCacheAddress,
LiquidityCacheABI,
signer
);
this.token = new ethers.Contract(tokenAddress, ERC20_ABI, signer);
}

async addGameProfits(profitAmount) {
try {
// 1. Check current allowance
const currentAllowance = await this.token.allowance(
await signer.getAddress(),
this.liquidityCache.address
);

// 2. Approve if necessary
if (currentAllowance.lt(profitAmount)) {
const approveTx = await this.token.approve(
this.liquidityCache.address,
ethers.constants.MaxUint256 // Approve max for gas efficiency
);
await approveTx.wait();
}

// 3. Add assets to the pool
const addAssetsTx = await this.liquidityCache.addAssets(
profitAmount,
await signer.getAddress()
);

const receipt = await addAssetsTx.wait();
console.log('Game profits added:', receipt.transactionHash);

return receipt;
} catch (error) {
console.error('Error adding game profits:', error);
throw error;
}
}

async getPoolStats() {
const totalAssets = await this.liquidityCache.totalAssets();
const totalShares = await this.liquidityCache.totalSupply();
const sharePrice = totalShares.gt(0)
? totalAssets.div(totalShares)
: ethers.constants.Zero;

return {
totalAssets,
totalShares,
sharePrice
};
}
}

2. Processing Player Payouts​

class PayoutManager {
constructor(liquidityCacheAddress) {
this.liquidityCache = new ethers.Contract(
liquidityCacheAddress,
LiquidityCacheABI,
signer
);
}

async processPayout(playerAddress, payoutAmount) {
try {
// Verify sufficient liquidity
const totalAssets = await this.liquidityCache.totalAssets();
if (totalAssets.lt(payoutAmount)) {
throw new Error('Insufficient liquidity in pool');
}

// Process payout (requires PAYOUT_ROLE)
const payoutTx = await this.liquidityCache.removeAssets(
payoutAmount,
playerAddress
);

const receipt = await payoutTx.wait();
console.log('Payout processed:', receipt.transactionHash);

return receipt;
} catch (error) {
console.error('Error processing payout:', error);
throw error;
}
}

async batchPayouts(payouts) {
// Process multiple payouts efficiently
const promises = payouts.map(({ address, amount }) =>
this.processPayout(address, amount)
);

return Promise.allSettled(promises);
}
}

Automated Game Integration​

class AutomatedGameIntegration {
constructor(config) {
this.liquidityCache = new ethers.Contract(
config.liquidityCacheAddress,
LiquidityCacheABI,
config.signer
);
this.profitManager = new GameProfitManager(
config.liquidityCacheAddress,
config.tokenAddress
);
this.payoutManager = new PayoutManager(config.liquidityCacheAddress);
}

async handleGameResult(gameId, players, results) {
try {
let totalProfits = ethers.constants.Zero;
const payouts = [];

// Calculate profits and payouts
for (let i = 0; i < players.length; i++) {
const player = players[i];
const result = results[i];

if (result.won) {
payouts.push({
address: player.address,
amount: result.winAmount
});
} else {
totalProfits = totalProfits.add(result.lossAmount);
}
}

// Add profits to pool
if (totalProfits.gt(0)) {
await this.profitManager.addGameProfits(totalProfits);
}

// Process payouts
if (payouts.length > 0) {
await this.payoutManager.batchPayouts(payouts);
}

console.log(`Game ${gameId} processed: ${totalProfits} profits, ${payouts.length} payouts`);
} catch (error) {
console.error(`Error processing game ${gameId}:`, error);
throw error;
}
}
}

Staking Integration​

User Staking Interface​

class StakingInterface {
constructor(liquidityCacheAddress, tokenAddress) {
this.liquidityCache = new ethers.Contract(
liquidityCacheAddress,
LiquidityCacheABI,
signer
);
this.token = new ethers.Contract(tokenAddress, ERC20_ABI, signer);
}

async stake(amount) {
try {
const userAddress = await signer.getAddress();

// 1. Check balance
const balance = await this.token.balanceOf(userAddress);
if (balance.lt(amount)) {
throw new Error('Insufficient token balance');
}

// 2. Approve tokens
const approveTx = await this.token.approve(
this.liquidityCache.address,
amount
);
await approveTx.wait();

// 3. Stake (deposit)
const stakeTx = await this.liquidityCache.deposit(amount, userAddress);
const receipt = await stakeTx.wait();

// Extract shares received from events
const depositEvent = receipt.events.find(e => e.event === 'Deposit');
const sharesReceived = depositEvent.args.shares;

return {
transactionHash: receipt.transactionHash,
sharesReceived,
amount
};
} catch (error) {
console.error('Staking error:', error);
throw error;
}
}

async unstake(shareAmount) {
try {
const userAddress = await signer.getAddress();

// Check share balance
const shareBalance = await this.liquidityCache.balanceOf(userAddress);
if (shareBalance.lt(shareAmount)) {
throw new Error('Insufficient share balance');
}

// Preview withdrawal
const assetsToReceive = await this.liquidityCache.previewRedeem(shareAmount);

// Unstake (redeem)
const unstakeTx = await this.liquidityCache.redeem(
shareAmount,
userAddress,
userAddress
);
const receipt = await unstakeTx.wait();

return {
transactionHash: receipt.transactionHash,
sharesRedeemed: shareAmount,
assetsReceived: assetsToReceive
};
} catch (error) {
console.error('Unstaking error:', error);
throw error;
}
}

async getStakingPosition(userAddress) {
const shareBalance = await this.liquidityCache.balanceOf(userAddress);
const assetValue = await this.liquidityCache.convertToAssets(shareBalance);

return {
shares: shareBalance,
assetValue,
shareBalance
};
}
}

Portfolio Tracking​

class PortfolioTracker {
constructor(liquidityCacheAddress) {
this.liquidityCache = new ethers.Contract(
liquidityCacheAddress,
LiquidityCacheABI,
provider // Read-only, use provider
);
}

async getUserPortfolio(userAddress) {
const shareBalance = await this.liquidityCache.balanceOf(userAddress);
const currentValue = await this.liquidityCache.convertToAssets(shareBalance);
const totalAssets = await this.liquidityCache.totalAssets();
const totalShares = await this.liquidityCache.totalSupply();

const poolShare = totalShares.gt(0)
? shareBalance.mul(10000).div(totalShares) // Basis points
: ethers.constants.Zero;

return {
shareBalance,
currentValue,
poolShareBasisPoints: poolShare,
poolSharePercentage: poolShare.toNumber() / 100
};
}

async calculateReturns(userAddress, initialDeposits) {
const currentPosition = await this.getUserPortfolio(userAddress);
const totalDeposited = initialDeposits.reduce(
(sum, deposit) => sum.add(deposit.amount),
ethers.constants.Zero
);

const unrealizedGains = currentPosition.currentValue.sub(totalDeposited);
const returnPercentage = totalDeposited.gt(0)
? unrealizedGains.mul(10000).div(totalDeposited) // Basis points
: ethers.constants.Zero;

return {
totalDeposited,
currentValue: currentPosition.currentValue,
unrealizedGains,
returnPercentage: returnPercentage.toNumber() / 100
};
}
}

React Integration Examples​

Staking Component​

import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';

const StakingComponent = ({ liquidityCacheAddress, tokenAddress }) => {
const [stakeAmount, setStakeAmount] = useState('');
const [userPosition, setUserPosition] = useState(null);
const [loading, setLoading] = useState(false);

const stakingInterface = new StakingInterface(liquidityCacheAddress, tokenAddress);
const portfolioTracker = new PortfolioTracker(liquidityCacheAddress);

useEffect(() => {
loadUserPosition();
}, []);

const loadUserPosition = async () => {
try {
const address = await signer.getAddress();
const position = await portfolioTracker.getUserPortfolio(address);
setUserPosition(position);
} catch (error) {
console.error('Error loading position:', error);
}
};

const handleStake = async () => {
if (!stakeAmount) return;

setLoading(true);
try {
const amount = ethers.utils.parseUnits(stakeAmount, 18);
const result = await stakingInterface.stake(amount);

console.log('Stake successful:', result);
await loadUserPosition(); // Refresh position
setStakeAmount('');
} catch (error) {
console.error('Stake failed:', error);
}
setLoading(false);
};

const handleUnstake = async (shareAmount) => {
setLoading(true);
try {
const result = await stakingInterface.unstake(shareAmount);

console.log('Unstake successful:', result);
await loadUserPosition(); // Refresh position
} catch (error) {
console.error('Unstake failed:', error);
}
setLoading(false);
};

return (
<div className="staking-component">
<h2>Liquidity Pool Staking</h2>

{userPosition && (
<div className="position-info">
<h3>Your Position</h3>
<p>Shares: {ethers.utils.formatUnits(userPosition.shareBalance, 18)}</p>
<p>Value: {ethers.utils.formatUnits(userPosition.currentValue, 18)} tokens</p>
<p>Pool Share: {userPosition.poolSharePercentage.toFixed(4)}%</p>
</div>
)}

<div className="stake-form">
<h3>Stake Tokens</h3>
<input
type="number"
value={stakeAmount}
onChange={(e) => setStakeAmount(e.target.value)}
placeholder="Amount to stake"
disabled={loading}
/>
<button onClick={handleStake} disabled={loading || !stakeAmount}>
{loading ? 'Staking...' : 'Stake'}
</button>
</div>

{userPosition && userPosition.shareBalance.gt(0) && (
<div className="unstake-form">
<h3>Unstake</h3>
<button
onClick={() => handleUnstake(userPosition.shareBalance)}
disabled={loading}
>
{loading ? 'Unstaking...' : 'Unstake All'}
</button>
</div>
)}
</div>
);
};

export default StakingComponent;

Pool Statistics Dashboard​

import React, { useState, useEffect } from 'react';

const PoolDashboard = ({ liquidityCacheAddress }) => {
const [poolStats, setPoolStats] = useState(null);
const [recentActivity, setRecentActivity] = useState([]);

useEffect(() => {
loadPoolStats();
loadRecentActivity();

// Set up polling for real-time updates
const interval = setInterval(() => {
loadPoolStats();
loadRecentActivity();
}, 30000); // Update every 30 seconds

return () => clearInterval(interval);
}, []);

const loadPoolStats = async () => {
try {
const liquidityCache = new ethers.Contract(
liquidityCacheAddress,
LiquidityCacheABI,
provider
);

const totalAssets = await liquidityCache.totalAssets();
const totalShares = await liquidityCache.totalSupply();
const sharePrice = totalShares.gt(0)
? totalAssets.div(totalShares)
: ethers.constants.Zero;

setPoolStats({
totalAssets: ethers.utils.formatUnits(totalAssets, 18),
totalShares: ethers.utils.formatUnits(totalShares, 18),
sharePrice: ethers.utils.formatUnits(sharePrice, 18)
});
} catch (error) {
console.error('Error loading pool stats:', error);
}
};

const loadRecentActivity = async () => {
try {
const liquidityCache = new ethers.Contract(
liquidityCacheAddress,
LiquidityCacheABI,
provider
);

// Get recent events
const currentBlock = await provider.getBlockNumber();
const fromBlock = currentBlock - 10000; // Last ~10k blocks

const [assetsAddedEvents, assetsRemovedEvents, depositEvents] = await Promise.all([
liquidityCache.queryFilter('AssetsAdded', fromBlock),
liquidityCache.queryFilter('AssetsRemoved', fromBlock),
liquidityCache.queryFilter('Deposit', fromBlock)
]);

const allEvents = [...assetsAddedEvents, ...assetsRemovedEvents, ...depositEvents]
.sort((a, b) => b.blockNumber - a.blockNumber)
.slice(0, 10); // Most recent 10 events

setRecentActivity(allEvents);
} catch (error) {
console.error('Error loading recent activity:', error);
}
};

return (
<div className="pool-dashboard">
<h2>Pool Statistics</h2>

{poolStats && (
<div className="stats-grid">
<div className="stat-card">
<h3>Total Pool Value</h3>
<p>{parseFloat(poolStats.totalAssets).toLocaleString()} tokens</p>
</div>

<div className="stat-card">
<h3>Total Shares</h3>
<p>{parseFloat(poolStats.totalShares).toLocaleString()}</p>
</div>

<div className="stat-card">
<h3>Share Price</h3>
<p>{parseFloat(poolStats.sharePrice).toFixed(6)} tokens</p>
</div>
</div>
)}

<div className="recent-activity">
<h3>Recent Activity</h3>
{recentActivity.length > 0 ? (
<ul>
{recentActivity.map((event, index) => (
<li key={index}>
<strong>{event.event}</strong> - Block {event.blockNumber}
{/* Add more event details as needed */}
</li>
))}
</ul>
) : (
<p>No recent activity</p>
)}
</div>
</div>
);
};

export default PoolDashboard;

Advanced Integration Patterns​

Event Monitoring Service​

class LiquidityPoolMonitor {
constructor(liquidityCacheAddress, provider) {
this.liquidityCache = new ethers.Contract(
liquidityCacheAddress,
LiquidityCacheABI,
provider
);
this.eventHandlers = new Map();
}

// Set up event listeners
startMonitoring() {
// Monitor asset additions (game profits)
this.liquidityCache.on('AssetsAdded', (amount, sender, event) => {
this.handleEvent('AssetsAdded', { amount, sender, event });
});

// Monitor asset removals (payouts)
this.liquidityCache.on('AssetsRemoved', (amount, receiver, event) => {
this.handleEvent('AssetsRemoved', { amount, receiver, event });
});

// Monitor deposits (staking)
this.liquidityCache.on('Deposit', (sender, owner, assets, shares, event) => {
this.handleEvent('Deposit', { sender, owner, assets, shares, event });
});

// Monitor withdrawals (unstaking)
this.liquidityCache.on('Withdraw', (sender, receiver, owner, assets, shares, event) => {
this.handleEvent('Withdraw', { sender, receiver, owner, assets, shares, event });
});
}

stopMonitoring() {
this.liquidityCache.removeAllListeners();
}

// Register event handlers
onEvent(eventName, handler) {
if (!this.eventHandlers.has(eventName)) {
this.eventHandlers.set(eventName, []);
}
this.eventHandlers.get(eventName).push(handler);
}

handleEvent(eventName, eventData) {
const handlers = this.eventHandlers.get(eventName) || [];
handlers.forEach(handler => {
try {
handler(eventData);
} catch (error) {
console.error(`Error in ${eventName} handler:`, error);
}
});
}
}

// Usage example
const monitor = new LiquidityPoolMonitor(LIQUIDITY_CACHE_ADDRESS, provider);

monitor.onEvent('AssetsAdded', (data) => {
console.log('Game profits added:', ethers.utils.formatUnits(data.amount, 18));
// Update UI, send notifications, etc.
});

monitor.onEvent('Deposit', (data) => {
console.log('New stake:', ethers.utils.formatUnits(data.assets, 18));
// Update user portfolio, recalculate pool stats, etc.
});

monitor.startMonitoring();

Multi-Pool Manager​

class MultiPoolManager {
constructor(poolConfigs) {
this.pools = new Map();

poolConfigs.forEach(config => {
this.pools.set(config.name, {
contract: new ethers.Contract(
config.address,
LiquidityCacheABI,
config.signer || provider
),
token: config.tokenAddress,
...config
});
});
}

async getPoolComparison() {
const comparisons = [];

for (const [name, pool] of this.pools) {
try {
const totalAssets = await pool.contract.totalAssets();
const totalShares = await pool.contract.totalSupply();
const sharePrice = totalShares.gt(0)
? totalAssets.div(totalShares)
: ethers.constants.Zero;

comparisons.push({
name,
totalAssets: ethers.utils.formatUnits(totalAssets, 18),
totalShares: ethers.utils.formatUnits(totalShares, 18),
sharePrice: ethers.utils.formatUnits(sharePrice, 18),
address: pool.contract.address
});
} catch (error) {
console.error(`Error getting stats for pool ${name}:`, error);
}
}

return comparisons;
}

async findBestYieldPool() {
const comparison = await this.getPoolComparison();
return comparison.reduce((best, current) =>
parseFloat(current.sharePrice) > parseFloat(best.sharePrice) ? current : best
);
}

async distributeStakeAcrossPools(totalAmount, strategy = 'equal') {
const poolNames = Array.from(this.pools.keys());

if (strategy === 'equal') {
const amountPerPool = totalAmount.div(poolNames.length);
const distributions = poolNames.map(name => ({
pool: name,
amount: amountPerPool
}));

return distributions;
}

// Add more distribution strategies as needed
throw new Error(`Unknown distribution strategy: ${strategy}`);
}
}

Error Handling and Best Practices​

Robust Error Handling​

class RobustLiquidityIntegration {
constructor(config) {
this.config = config;
this.liquidityCache = new ethers.Contract(
config.liquidityCacheAddress,
LiquidityCacheABI,
config.signer
);
this.maxRetries = config.maxRetries || 3;
this.retryDelay = config.retryDelay || 1000;
}

async executeWithRetry(operation, ...args) {
let lastError;

for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
return await operation(...args);
} catch (error) {
lastError = error;

// Don't retry on certain errors
if (this.isNonRetryableError(error)) {
throw error;
}

if (attempt < this.maxRetries) {
await this.delay(this.retryDelay * attempt);
console.log(`Retry attempt ${attempt} for operation`);
}
}
}

throw lastError;
}

isNonRetryableError(error) {
// Don't retry on these errors
const nonRetryableReasons = [
'insufficient balance',
'insufficient allowance',
'insufficient assets in pool',
'caller is not authorized',
'paused'
];

return nonRetryableReasons.some(reason =>
error.message.toLowerCase().includes(reason)
);
}

async delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

async safeStake(amount) {
return this.executeWithRetry(async () => {
// Pre-flight checks
await this.validateStakePrerequisites(amount);

// Execute stake
const result = await this.stakingInterface.stake(amount);

// Post-execution validation
await this.validateStakeResult(result);

return result;
});
}

async validateStakePrerequisites(amount) {
const userAddress = await this.config.signer.getAddress();

// Check token balance
const balance = await this.token.balanceOf(userAddress);
if (balance.lt(amount)) {
throw new Error('Insufficient token balance');
}

// Check contract is not paused
const isPaused = await this.liquidityCache.paused();
if (isPaused) {
throw new Error('Contract is paused');
}

// Check allowance
const allowance = await this.token.allowance(userAddress, this.liquidityCache.address);
if (allowance.lt(amount)) {
throw new Error('Insufficient allowance');
}
}

async validateStakeResult(result) {
if (!result.transactionHash) {
throw new Error('No transaction hash in result');
}

if (!result.sharesReceived || result.sharesReceived.eq(0)) {
throw new Error('No shares received');
}
}
}

Integration Testing Utilities​

class IntegrationTestUtils {
constructor(liquidityCacheAddress, tokenAddress) {
this.liquidityCache = new ethers.Contract(
liquidityCacheAddress,
LiquidityCacheABI,
provider
);
this.token = new ethers.Contract(tokenAddress, ERC20_ABI, provider);
}

async setupTestEnvironment() {
// Helper to set up test environment
const testAccounts = await ethers.getSigners();
const admin = testAccounts[0];
const user1 = testAccounts[1];
const user2 = testAccounts[2];

return { admin, user1, user2 };
}

async mintTestTokens(userAddress, amount) {
// Helper to mint test tokens (if token contract supports it)
// Implementation depends on your test token contract
}

async simulateGameProfits(amount) {
// Helper to simulate adding game profits
const gameProfitManager = new GameProfitManager(
this.liquidityCache.address,
this.token.address
);

return gameProfitManager.addGameProfits(amount);
}

async getCompletePoolState() {
const totalAssets = await this.liquidityCache.totalAssets();
const totalShares = await this.liquidityCache.totalSupply();
const isPaused = await this.liquidityCache.paused();

return {
totalAssets,
totalShares,
sharePrice: totalShares.gt(0) ? totalAssets.div(totalShares) : ethers.constants.Zero,
isPaused
};
}

async waitForTransaction(txHash, confirmations = 1) {
const tx = await provider.getTransaction(txHash);
return tx.wait(confirmations);
}
}

Production Deployment Checklist​

Pre-Deployment​

  • Contract audited by security firm
  • All integration tests passing
  • Gas optimization analysis completed
  • Role assignments planned and documented
  • Emergency pause procedures documented
  • Monitoring and alerting systems set up

Deployment​

  • Deploy to testnet first
  • Verify contract source code
  • Set up initial roles and permissions
  • Test all major functions
  • Deploy to mainnet
  • Verify mainnet deployment

Post-Deployment​

  • Monitor initial transactions
  • Set up automated monitoring
  • Document all contract addresses
  • Update frontend configurations
  • Announce to users/stakeholders
  • Monitor for any issues

Support​

For additional integration support:

  • Review the technical reference for detailed specifications
  • Check the staking guide for user flow examples
  • Contact the MCAP development team for custom integration needs
  • Join the developer community for questions and discussions