Quantillon Protocol

Quantillon Protocol Integration Examples

This document provides practical integration examples for common use cases with the Quantillon Protocol.


Table of Contents

  1. Basic QEURO Operations
  2. Staking and Yield Generation
  3. Governance Participation
  4. Hedging Operations
  5. Advanced Integration Patterns
  6. Error Handling and Recovery

Basic QEURO Operations

Minting QEURO from USDC

const { ethers } = require('ethers');

async function mintQEURO(usdcAmount, slippage = 0.05) {
    // Initialize contracts
    const vault = new ethers.Contract(VAULT_ADDRESS, VAULT_ABI, signer);
    const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, signer);
    
    try {
        // 1. Check vault state
        const isPaused = await vault.paused();
        if (isPaused) {
            throw new Error('Vault is paused');
        }
        
        // 2. Calculate expected output
        const expectedQeuro = await vault.calculateMintAmount(usdcAmount);
        const minQeuroOut = expectedQeuro.mul(100 - slippage * 100).div(100);
        
        // 3. Approve USDC spending
        const approveTx = await usdc.approve(VAULT_ADDRESS, usdcAmount);
        await approveTx.wait();
        
        // 4. Mint QEURO
        const mintTx = await vault.mintQEURO(usdcAmount, minQeuroOut);
        const receipt = await mintTx.wait();
        
        // 5. Parse events
        const mintEvent = receipt.events.find(e => e.event === 'QEUROMinted');
        console.log(`Minted ${mintEvent.args.qeuroAmount} QEURO for ${mintEvent.args.usdcAmount} USDC`);
        
        return receipt;
    } catch (error) {
        console.error('Minting failed:', error.message);
        throw error;
    }
}

// Usage
const usdcAmount = ethers.utils.parseUnits('1000', 6); // 1000 USDC
await mintQEURO(usdcAmount, 0.05); // 5% slippage tolerance

Redeeming QEURO for USDC

async function redeemQEURO(qeuroAmount, slippage = 0.05) {
    const vault = new ethers.Contract(VAULT_ADDRESS, VAULT_ABI, signer);
    const qeuro = new ethers.Contract(QEURO_ADDRESS, QEURO_ABI, signer);
    
    try {
        // 1. Calculate expected output
        const expectedUsdc = await vault.calculateRedeemAmount(qeuroAmount);
        const minUsdcOut = expectedUsdc.mul(100 - slippage * 100).div(100);
        
        // 2. Approve QEURO spending
        const approveTx = await qeuro.approve(VAULT_ADDRESS, qeuroAmount);
        await approveTx.wait();
        
        // 3. Redeem QEURO
        const redeemTx = await vault.redeemQEURO(qeuroAmount, minUsdcOut);
        const receipt = await redeemTx.wait();
        
        // 4. Parse events
        const redeemEvent = receipt.events.find(e => e.event === 'QEURORedeemed');
        console.log(`Redeemed ${redeemEvent.args.qeuroAmount} QEURO for ${redeemEvent.args.usdcAmount} USDC`);
        
        return receipt;
    } catch (error) {
        console.error('Redemption failed:', error.message);
        throw error;
    }
}

Staking and Yield Generation

Staking QEURO in User Pool

async function stakeQEURO(qeuroAmount) {
    const userPool = new ethers.Contract(USER_POOL_ADDRESS, USER_POOL_ABI, signer);
    const qeuro = new ethers.Contract(QEURO_ADDRESS, QEURO_ABI, signer);
    
    try {
        // 1. Check minimum stake amount
        const minStakeAmount = await userPool.MIN_STAKE_AMOUNT();
        if (qeuroAmount.lt(minStakeAmount)) {
            throw new Error(`Amount below minimum stake: ${minStakeAmount}`);
        }
        
        // 2. Check user's QEURO balance
        const balance = await qeuro.balanceOf(signer.address);
        if (balance.lt(qeuroAmount)) {
            throw new Error('Insufficient QEURO balance');
        }
        
        // 3. Approve QEURO spending
        const approveTx = await qeuro.approve(USER_POOL_ADDRESS, qeuroAmount);
        await approveTx.wait();
        
        // 4. Stake QEURO
        const stakeTx = await userPool.stake(qeuroAmount);
        const receipt = await stakeTx.wait();
        
        console.log(`Staked ${qeuroAmount} QEURO successfully`);
        return receipt;
    } catch (error) {
        console.error('Staking failed:', error.message);
        throw error;
    }
}

Claiming Staking Rewards

async function claimStakingRewards() {
    const userPool = new ethers.Contract(USER_POOL_ADDRESS, USER_POOL_ABI, signer);
    
    try {
        // 1. Check pending rewards
        const userInfo = await userPool.getUserInfo(signer.address);
        const pendingRewards = userInfo.pendingRewards;
        
        if (pendingRewards.eq(0)) {
            console.log('No rewards to claim');
            return null;
        }
        
        // 2. Claim rewards
        const claimTx = await userPool.claimStakingRewards();
        const receipt = await claimTx.wait();
        
        // 3. Parse events
        const claimEvent = receipt.events.find(e => e.event === 'RewardsClaimed');
        console.log(`Claimed ${claimEvent.args.amount} QEURO rewards`);
        
        return receipt;
    } catch (error) {
        console.error('Claiming rewards failed:', error.message);
        throw error;
    }
}

Staking in stQEURO Token

async function stakeInStQEURO(qeuroAmount) {
    const stQeuro = new ethers.Contract(ST_QEURO_ADDRESS, ST_QEURO_ABI, signer);
    const qeuro = new ethers.Contract(QEURO_ADDRESS, QEURO_ABI, signer);
    
    try {
        // 1. Get current exchange rate
        const exchangeRate = await stQeuro.getExchangeRate();
        const expectedStQeuro = qeuroAmount.mul(ethers.utils.parseEther('1')).div(exchangeRate);
        
        // 2. Approve QEURO spending
        const approveTx = await qeuro.approve(ST_QEURO_ADDRESS, qeuroAmount);
        await approveTx.wait();
        
        // 3. Stake QEURO
        const stakeTx = await stQeuro.stake(qeuroAmount);
        const receipt = await stakeTx.wait();
        
        console.log(`Staked ${qeuroAmount} QEURO, received ${expectedStQeuro} stQEURO`);
        return receipt;
    } catch (error) {
        console.error('stQEURO staking failed:', error.message);
        throw error;
    }
}

Governance Participation

Locking QTI for Voting Power

async function lockQTI(amount, lockDuration) {
    const qti = new ethers.Contract(QTI_ADDRESS, QTI_ABI, signer);
    
    try {
        // 1. Check lock duration limits
        const minLockTime = await qti.MIN_LOCK_TIME();
        const maxLockTime = await qti.MAX_LOCK_TIME();
        
        if (lockDuration.lt(minLockTime) || lockDuration.gt(maxLockTime)) {
            throw new Error(`Lock duration must be between ${minLockTime} and ${maxLockTime} seconds`);
        }
        
        // 2. Check QTI balance
        const balance = await qti.balanceOf(signer.address);
        if (balance.lt(amount)) {
            throw new Error('Insufficient QTI balance');
        }
        
        // 3. Lock QTI
        const lockTx = await qti.lock(amount, lockDuration);
        const receipt = await lockTx.wait();
        
        // 4. Parse events
        const lockEvent = receipt.events.find(e => e.event === 'TokensLocked');
        console.log(`Locked ${amount} QTI for ${lockDuration} seconds, received ${lockEvent.args.votingPower} veQTI`);
        
        return receipt;
    } catch (error) {
        console.error('QTI locking failed:', error.message);
        throw error;
    }
}

Creating a Governance Proposal

async function createProposal(description, startTime, endTime) {
    const qti = new ethers.Contract(QTI_ADDRESS, QTI_ABI, signer);
    
    try {
        // 1. Check voting power
        const votingPower = await qti.getVotingPower(signer.address);
        const minProposalPower = await qti.MIN_PROPOSAL_POWER();
        
        if (votingPower.lt(minProposalPower)) {
            throw new Error(`Insufficient voting power. Required: ${minProposalPower}, Current: ${votingPower}`);
        }
        
        // 2. Validate time parameters
        const currentTime = Math.floor(Date.now() / 1000);
        if (startTime <= currentTime || endTime <= startTime) {
            throw new Error('Invalid time parameters');
        }
        
        // 3. Create proposal
        const proposalTx = await qti.createProposal(description, startTime, endTime);
        const receipt = await proposalTx.wait();
        
        // 4. Parse events
        const proposalEvent = receipt.events.find(e => e.event === 'ProposalCreated');
        console.log(`Created proposal ${proposalEvent.args.proposalId}: ${description}`);
        
        return proposalEvent.args.proposalId;
    } catch (error) {
        console.error('Proposal creation failed:', error.message);
        throw error;
    }
}

Voting on Proposals

async function voteOnProposal(proposalId, support) {
    const qti = new ethers.Contract(QTI_ADDRESS, QTI_ABI, signer);
    
    try {
        // 1. Check voting power
        const votingPower = await qti.getVotingPower(signer.address);
        if (votingPower.eq(0)) {
            throw new Error('No voting power available');
        }
        
        // 2. Check if already voted
        const hasVoted = await qti.hasVoted(proposalId, signer.address);
        if (hasVoted) {
            throw new Error('Already voted on this proposal');
        }
        
        // 3. Vote
        const voteTx = await qti.vote(proposalId, support);
        const receipt = await voteTx.wait();
        
        // 4. Parse events
        const voteEvent = receipt.events.find(e => e.event === 'VoteCast');
        console.log(`Voted ${support ? 'YES' : 'NO'} on proposal ${proposalId} with ${voteEvent.args.votingPower} voting power`);
        
        return receipt;
    } catch (error) {
        console.error('Voting failed:', error.message);
        throw error;
    }
}

Hedging Operations

Opening a Hedge Position

async function openHedgePosition(marginAmount, leverage) {
    const hedgerPool = new ethers.Contract(HEDGER_POOL_ADDRESS, HEDGER_POOL_ABI, signer);
    const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, signer);
    
    try {
        // 1. Validate leverage
        const maxLeverage = await hedgerPool.maxLeverage();
        if (leverage.lt(1) || leverage.gt(maxLeverage)) {
            throw new Error(`Leverage must be between 1 and ${maxLeverage}`);
        }
        
        // 2. Check USDC balance
        const balance = await usdc.balanceOf(signer.address);
        if (balance.lt(marginAmount)) {
            throw new Error('Insufficient USDC balance');
        }
        
        // 3. Approve USDC spending
        const approveTx = await usdc.approve(HEDGER_POOL_ADDRESS, marginAmount);
        await approveTx.wait();
        
        // 4. Open position
        const openTx = await hedgerPool.enterHedgePosition(marginAmount, leverage);
        const receipt = await openTx.wait();
        
        // 5. Parse events
        const openEvent = receipt.events.find(e => e.event === 'HedgePositionOpened');
        console.log(`Opened position ${openEvent.args.positionId} with ${marginAmount} USDC margin and ${leverage}x leverage`);
        
        return openEvent.args.positionId;
    } catch (error) {
        console.error('Opening position failed:', error.message);
        throw error;
    }
}

Monitoring Position Health

async function monitorPosition(positionId) {
    const hedgerPool = new ethers.Contract(HEDGER_POOL_ADDRESS, HEDGER_POOL_ABI, signer);
    
    try {
        // 1. Get position info
        const positionInfo = await hedgerPool.getPositionInfo(positionId);
        
        // 2. Calculate margin ratio
        const marginRatio = positionInfo.margin.mul(10000).div(positionInfo.positionSize);
        const liquidationThreshold = await hedgerPool.liquidationThreshold();
        
        // 3. Check if position is healthy
        const isHealthy = marginRatio.gt(liquidationThreshold);
        
        console.log(`Position ${positionId}:`);
        console.log(`  Margin: ${ethers.utils.formatUnits(positionInfo.margin, 6)} USDC`);
        console.log(`  Position Size: ${ethers.utils.formatUnits(positionInfo.positionSize, 6)} USDC`);
        console.log(`  Margin Ratio: ${marginRatio.toNumber() / 100}%`);
        console.log(`  Unrealized PnL: ${ethers.utils.formatEther(positionInfo.unrealizedPnL)} QEURO`);
        console.log(`  Status: ${isHealthy ? 'HEALTHY' : 'AT RISK'}`);
        
        return {
            positionInfo,
            marginRatio,
            isHealthy
        };
    } catch (error) {
        console.error('Position monitoring failed:', error.message);
        throw error;
    }
}

Adding Margin to Position

async function addMargin(positionId, additionalMargin) {
    const hedgerPool = new ethers.Contract(HEDGER_POOL_ADDRESS, HEDGER_POOL_ABI, signer);
    const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, signer);
    
    try {
        // 1. Check position ownership
        const positionInfo = await hedgerPool.getPositionInfo(positionId);
        if (positionInfo.hedger !== signer.address) {
            throw new Error('Not the owner of this position');
        }
        
        // 2. Check USDC balance
        const balance = await usdc.balanceOf(signer.address);
        if (balance.lt(additionalMargin)) {
            throw new Error('Insufficient USDC balance');
        }
        
        // 3. Approve USDC spending
        const approveTx = await usdc.approve(HEDGER_POOL_ADDRESS, additionalMargin);
        await approveTx.wait();
        
        // 4. Add margin
        const addMarginTx = await hedgerPool.addMargin(positionId, additionalMargin);
        const receipt = await addMarginTx.wait();
        
        console.log(`Added ${ethers.utils.formatUnits(additionalMargin, 6)} USDC margin to position ${positionId}`);
        return receipt;
    } catch (error) {
        console.error('Adding margin failed:', error.message);
        throw error;
    }
}

Advanced Integration Patterns

Portfolio Management

class QuantillonPortfolio {
    constructor(provider, signer) {
        this.provider = provider;
        this.signer = signer;
        this.vault = new ethers.Contract(VAULT_ADDRESS, VAULT_ABI, signer);
        this.qeuro = new ethers.Contract(QEURO_ADDRESS, QEURO_ABI, signer);
        this.userPool = new ethers.Contract(USER_POOL_ADDRESS, USER_POOL_ABI, signer);
        this.hedgerPool = new ethers.Contract(HEDGER_POOL_ADDRESS, HEDGER_POOL_ABI, signer);
    }
    
    async getPortfolioOverview() {
        const address = this.signer.address;
        
        try {
            const [
                qeuroBalance,
                userInfo,
                vaultMetrics,
                positions
            ] = await Promise.all([
                this.qeuro.balanceOf(address),
                this.userPool.getUserInfo(address),
                this.vault.getVaultMetrics(),
                this.getUserPositions()
            ]);
            
            return {
                balances: {
                    qeuro: ethers.utils.formatEther(qeuroBalance),
                    staked: ethers.utils.formatEther(userInfo.stakedQeuro),
                    deposited: ethers.utils.formatUnits(userInfo.depositedUsdc, 6)
                },
                rewards: {
                    pending: ethers.utils.formatEther(userInfo.pendingRewards),
                    claimed: ethers.utils.formatEther(userInfo.totalRewardsClaimed)
                },
                positions: positions,
                vault: {
                    totalReserves: ethers.utils.formatUnits(vaultMetrics.totalUsdcReserves, 6),
                    totalSupply: ethers.utils.formatEther(vaultMetrics.totalQeuroSupply),
                    collateralizationRatio: vaultMetrics.collateralizationRatio.toNumber() / 100
                }
            };
        } catch (error) {
            console.error('Failed to get portfolio overview:', error.message);
            throw error;
        }
    }
    
    async getUserPositions() {
        // Implementation to get user's hedge positions
        // This would require tracking position IDs or using events
        return [];
    }
    
    async optimizeYield() {
        try {
            const userPoolAPY = await this.userPool.getStakingAPY();
            const hedgerPoolAPY = await this.hedgerPool.getHedgingAPY();
            
            console.log(`User Pool APY: ${userPoolAPY.toNumber() / 100}%`);
            console.log(`Hedger Pool APY: ${hedgerPoolAPY.toNumber() / 100}%`);
            
            if (userPoolAPY.gt(hedgerPoolAPY)) {
                console.log('Recommendation: Stake in User Pool for higher yield');
            } else {
                console.log('Recommendation: Consider hedging for higher yield');
            }
        } catch (error) {
            console.error('Yield optimization failed:', error.message);
            throw error;
        }
    }
}

// Usage
const portfolio = new QuantillonPortfolio(provider, signer);
const overview = await portfolio.getPortfolioOverview();
console.log('Portfolio Overview:', overview);
await portfolio.optimizeYield();

Automated Yield Management

class YieldManager {
    constructor(provider, signer) {
        this.provider = provider;
        this.signer = signer;
        this.yieldShift = new ethers.Contract(YIELD_SHIFT_ADDRESS, YIELD_SHIFT_ABI, signer);
        this.aaveVault = new ethers.Contract(AAVE_VAULT_ADDRESS, AAVE_VAULT_ABI, signer);
    }
    
    async checkAndDistributeYield() {
        try {
            // 1. Check if yield distribution is needed
            const poolMetrics = await this.yieldShift.getPoolMetrics();
            const targetRatio = await this.yieldShift.targetPoolRatio();
            
            const currentRatio = poolMetrics.userPoolSize.mul(10000).div(poolMetrics.hedgerPoolSize);
            const deviation = currentRatio.sub(targetRatio).abs();
            const threshold = await this.yieldShift.rebalanceThreshold();
            
            if (deviation.gt(threshold)) {
                console.log('Pool ratio deviation detected, distributing yield...');
                await this.yieldShift.distributeYield();
            }
        } catch (error) {
            console.error('Yield distribution failed:', error.message);
            throw error;
        }
    }
    
    async harvestAaveYield() {
        try {
            const yieldAmount = await this.aaveVault.harvestAaveYield();
            console.log(`Harvested ${ethers.utils.formatEther(yieldAmount)} QEURO from Aave`);
            return yieldAmount;
        } catch (error) {
            console.error('Aave yield harvest failed:', error.message);
            throw error;
        }
    }
    
    async autoRebalance() {
        try {
            const [rebalanced, newAllocation, expectedYield] = await this.aaveVault.autoRebalance();
            
            if (rebalanced) {
                console.log(`Rebalanced to ${newAllocation.toNumber() / 100}% allocation`);
                console.log(`Expected yield: ${ethers.utils.formatEther(expectedYield)} QEURO`);
            } else {
                console.log('No rebalancing needed');
            }
            
            return { rebalanced, newAllocation, expectedYield };
        } catch (error) {
            console.error('Auto rebalancing failed:', error.message);
            throw error;
        }
    }
}

Error Handling and Recovery

Comprehensive Error Handling

class QuantillonErrorHandler {
    static handleError(error) {
        const errorMessage = error.message.toLowerCase();
        
        if (errorMessage.includes('insufficient balance')) {
            return {
                type: 'INSUFFICIENT_BALANCE',
                message: 'Insufficient token balance for this operation',
                action: 'Check your token balance and try again'
            };
        } else if (errorMessage.includes('insufficient allowance')) {
            return {
                type: 'INSUFFICIENT_ALLOWANCE',
                message: 'Token allowance is insufficient',
                action: 'Approve token spending before calling this function'
            };
        } else if (errorMessage.includes('stale price')) {
            return {
                type: 'STALE_PRICE',
                message: 'Oracle price is stale',
                action: 'Wait for oracle to update or contact support'
            };
        } else if (errorMessage.includes('contract paused')) {
            return {
                type: 'CONTRACT_PAUSED',
                message: 'Contract is currently paused',
                action: 'Wait for contract to be unpaused'
            };
        } else if (errorMessage.includes('unauthorized')) {
            return {
                type: 'UNAUTHORIZED',
                message: 'Unauthorized access',
                action: 'Check your permissions and try again'
            };
        } else {
            return {
                type: 'UNKNOWN',
                message: error.message,
                action: 'Contact support if the issue persists'
            };
        }
    }
    
    static async retryOperation(operation, maxRetries = 3, delay = 1000) {
        for (let i = 0; i < maxRetries; i++) {
            try {
                return await operation();
            } catch (error) {
                const errorInfo = this.handleError(error);
                
                if (i === maxRetries - 1) {
                    throw new Error(`${errorInfo.type}: ${errorInfo.message}. ${errorInfo.action}`);
                }
                
                console.log(`Attempt ${i + 1} failed: ${errorInfo.message}. Retrying in ${delay}ms...`);
                await new Promise(resolve => setTimeout(resolve, delay));
                delay *= 2; // Exponential backoff
            }
        }
    }
}

// Usage
try {
    await QuantillonErrorHandler.retryOperation(async () => {
        return await vault.mintQEURO(usdcAmount, minQeuroOut);
    });
} catch (error) {
    console.error('Operation failed after retries:', error.message);
}

Transaction Monitoring

class TransactionMonitor {
    static async waitForConfirmation(tx, confirmations = 1) {
        try {
            console.log(`Transaction submitted: ${tx.hash}`);
            const receipt = await tx.wait(confirmations);
            console.log(`Transaction confirmed in block ${receipt.blockNumber}`);
            return receipt;
        } catch (error) {
            console.error(`Transaction failed: ${error.message}`);
            throw error;
        }
    }
    
    static async monitorGasPrice(provider, maxGasPrice) {
        const gasPrice = await provider.getGasPrice();
        if (gasPrice.gt(maxGasPrice)) {
            console.warn(`Gas price ${ethers.utils.formatUnits(gasPrice, 'gwei')} Gwei exceeds maximum ${ethers.utils.formatUnits(maxGasPrice, 'gwei')} Gwei`);
            return false;
        }
        return true;
    }
    
    static async estimateGasWithBuffer(contract, method, params, buffer = 1.2) {
        try {
            const gasEstimate = await contract.estimateGas[method](...params);
            return gasEstimate.mul(Math.floor(buffer * 100)).div(100);
        } catch (error) {
            console.error('Gas estimation failed:', error.message);
            throw error;
        }
    }
}

Best Practices

1. Always Check Contract State

// Check if contract is paused before any operation
const isPaused = await contract.paused();
if (isPaused) {
    throw new Error('Contract is paused');
}

2. Use Slippage Protection

// Always use slippage protection for swaps
const slippage = 0.05; // 5%
const minOutput = expectedOutput.mul(100 - slippage * 100).div(100);

3. Implement Proper Error Handling

// Use try-catch blocks and handle specific errors
try {
    await contract.function();
} catch (error) {
    const errorInfo = QuantillonErrorHandler.handleError(error);
    console.error(`${errorInfo.type}: ${errorInfo.message}`);
}

4. Monitor Events

// Listen for important events
contract.on('EventName', (param1, param2) => {
    console.log('Event received:', param1, param2);
});

5. Gas Optimization

// Estimate gas and add buffer
const gasEstimate = await contract.estimateGas.function(params);
const gasLimit = gasEstimate.mul(120).div(100); // 20% buffer

This integration examples guide is maintained by Quantillon Labs and updated regularly.