diff --git a/src/adaptors/sparkdex/README.md b/src/adaptors/sparkdex/README.md new file mode 100644 index 0000000000..671a69dcdb --- /dev/null +++ b/src/adaptors/sparkdex/README.md @@ -0,0 +1,84 @@ +# SparkDEX Adapter + +This adapter provides yield information for the SPRK token staking protocol on the Flare network. + +## Overview + +SPRK token holders can stake their tokens to receive xSPRK (escrowed tokens). By allocating xSPRK, users can earn dividends from various distributed tokens. + +## Token Addresses + +- **SPRK Token**: `0x657097cC15fdEc9e383dB8628B57eA4a763F2ba0` +- **xSPRK (Escrowed)**: `0xB5Dc569d06be81Eb222a00cEe810c42976981986` +- **Dividends Smart Contract**: `0x710a578356A3Dfa7C207B839D3E244807b2f5AFE` + +## How It Works + +1. **Staking**: Users stake SPRK tokens to receive xSPRK +2. **Allocation**: Users allocate their xSPRK to start earning dividends +3. **Dividends**: Users earn dividends from distributed tokens based on their allocation +4. **Epoch**: Dividends are distributed every 7 days + +## APY Calculation + +The adapter calculates APR (not APY) since this is not an auto-compounding protocol: + +``` +APR = (Current Distribution USD / TVL USD) * (365 days / 7 days) * 100 +``` + +Where: +- **Current Distribution USD**: Total value of tokens being distributed in the current epoch +- **TVL USD**: Total value of allocated xSPRK tokens +- **Epoch Duration**: 7 days + +## Data Sources + +### Current Implementation (Testing) +- Uses mock data for testing purposes +- Simulates distributed tokens and allocations +- Includes fallback price data + +### Production Implementation +- Should fetch data from smart contracts: + - `distributedTokensLength` from dividends SC + - `distributedTokens` array from dividends SC + - `totalAllocation` (xSPRK amount) from dividends SC + - `currentDistribution` for each token from dividends SC + +### Price Data +- Primary: DefiLlama Price API (`https://coins.llama.fi/prices/current/flare:{token}`) +- Fallback: FlareMetrics API +- Mock prices for testing + +## Pool Information + +- **Pool ID**: `0xb5dc569d06be81eb222a00cee810c42976981986-flare` +- **Symbol**: `xSPRK` +- **Project**: `SparkDEX` +- **Chain**: `flare` +- **Underlying Token**: xSPRK +- **Reward Tokens**: All distributed tokens (WFLR, USDC.e, USDT, etc.) + +## Testing + +The adapter includes comprehensive test coverage: +- Field validation +- Data type checks +- APY calculations +- Token validation +- Pool uniqueness + +## Future Improvements + +1. **Smart Contract Integration**: Replace mock data with actual contract calls +2. **Real-time Updates**: Implement real-time data fetching +3. **Historical Data**: Add historical APY tracking +4. **Gas Optimization**: Optimize contract calls for gas efficiency + +## Notes + +- This is not an auto-compounding protocol, so APR is used instead of APY +- TVL is calculated based on allocated xSPRK, not staked SPRK +- The adapter handles cases where price data might be unavailable +- Mock data is used for testing but should be replaced with real data in production diff --git a/src/adaptors/sparkdex/dividends-abi.json b/src/adaptors/sparkdex/dividends-abi.json new file mode 100644 index 0000000000..2a4219afab --- /dev/null +++ b/src/adaptors/sparkdex/dividends-abi.json @@ -0,0 +1,919 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "xSPRKToken_", + "type": "address" + }, + { + "internalType": "uint256", + "name": "startTime_", + "type": "uint256" + }, + { + "internalType": "address", + "name": "weth_", + "type": "address" + }, + { + "internalType": "address", + "name": "owner_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "previousValue", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newValue", + "type": "uint256" + } + ], + "name": "CycleDividendsPercentUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "DistributedTokenDisabled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "DistributedTokenEnabled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "DistributedTokenRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "DividendsAddedToCurrentCycle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "DividendsAddedToPending", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "DividendsCollected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "handler", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "isActive", + "type": "bool" + } + ], + "name": "HandlerSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "previousBalance", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newBalance", + "type": "uint256" + } + ], + "name": "UserUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "CYCLE_DURATION_SECONDS", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DEFAULT_CYCLE_DIVIDENDS_PERCENT", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_CYCLE_DIVIDENDS_PERCENT", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_DISTRIBUTED_TOKENS", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MIN_CYCLE_DIVIDENDS_PERCENT", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "UNIT", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "UNIT_DIV_100", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "addDividendsToCurrentCycle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "addDividendsToPending", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "userAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "allocate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "currentCycleStartTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "cycleDurationSeconds", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "userAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "deallocate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "disableDistributedToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "distributedToken", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "distributedTokensLength", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "dividendsInfo", + "outputs": [ + { + "internalType": "uint256", + "name": "currentDistributionAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "dividendsAmountPerSecond", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentCycleDistributedAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pendingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "distributedAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "accDividendsPerShare", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lastUpdateTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "cycleDividendsPercent", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "distributionDisabled", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "emergencyWithdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "emergencyWithdrawAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "enableDistributedToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "functionCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "withdrawEth", + "type": "bool" + } + ], + "name": "harvestAllDividends", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "bool", + "name": "withdrawEth", + "type": "bool" + } + ], + "name": "harvestDividends", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "isDistributedToken", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isHandler", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "massUpdateDividendsInfo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "nextCycleStartTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "userAddress", + "type": "address" + } + ], + "name": "pendingDividendsAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenToRemove", + "type": "address" + } + ], + "name": "removeTokenFromDistributedTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_handler", + "type": "address" + }, + { + "internalType": "bool", + "name": "_isActive", + "type": "bool" + } + ], + "name": "setHandler", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalAllocation", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "updateCurrentCycleStartTime", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "percent", + "type": "uint256" + } + ], + "name": "updateCycleDividendsPercent", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "updateDividendsInfo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "users", + "outputs": [ + { + "internalType": "uint256", + "name": "pendingDividends", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "rewardDebt", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "usersAllocation", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "weth", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "xSPRKToken", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ] +} diff --git a/src/adaptors/sparkdex/index.js b/src/adaptors/sparkdex/index.js new file mode 100644 index 0000000000..2b99061714 --- /dev/null +++ b/src/adaptors/sparkdex/index.js @@ -0,0 +1,181 @@ +const axios = require('axios'); +const sdk = require("@defillama/sdk"); + +// Token addresses +const SPRK_TOKEN = '0x657097cC15fdEc9e383dB8628B57eA4a763F2ba0'; +const XSPRK_TOKEN = '0xB5Dc569d06be81Eb222a00cEe810c42976981986'; +const DIVIDENDS_SC = '0x710a578356A3Dfa7C207B839D3E244807b2f5AFE'; + +// Epoch duration: 7 days +const EPOCH_DURATION_DAYS = 7; + +// Load ABI from file +const dividendsABI = require('./dividends-abi.json').abi; + +const apy = async () => { + try { + // Get distributed tokens length from smart contract + const distributedTokensLengthResult = await sdk.api.abi.call({ + target: DIVIDENDS_SC, + abi: dividendsABI.find(item => item.name === 'distributedTokensLength'), + chain: "flare", + }); + + if (!distributedTokensLengthResult.output || parseInt(distributedTokensLengthResult.output) === 0) { + return []; + } + + const distributedTokensLength = parseInt(distributedTokensLengthResult.output); + const distributedTokens = []; + + // Get all distributed tokens + for (let i = 0; i < distributedTokensLength; i++) { + try { + const tokenResult = await sdk.api.abi.call({ + target: DIVIDENDS_SC, + abi: dividendsABI.find(item => item.name === 'distributedToken'), + params: [i], + chain: "flare", + }); + + if (tokenResult.output && tokenResult.output !== '0x0000000000000000000000000000000000000000') { + distributedTokens.push(tokenResult.output); + } + } catch (error) { + console.log(`Error getting distributed token at index ${i}:`, error.message); + } + } + + if (distributedTokens.length === 0) { + return []; + } + + // Get total allocation (xSPRK amount) from smart contract + const totalAllocationResult = await sdk.api.abi.call({ + target: DIVIDENDS_SC, + abi: dividendsABI.find(item => item.name === 'totalAllocation'), + chain: "flare", + }); + + if (!totalAllocationResult.output || parseInt(totalAllocationResult.output) === 0) { + return []; + } + + // Get xSPRK decimals + const xSprkDecimalsResult = await sdk.api.abi.call({ + target: XSPRK_TOKEN, + abi: "uint8:decimals", + chain: "flare", + }); + + const xSprkDecimals = parseInt(xSprkDecimalsResult.output); + const totalAllocation = parseInt(totalAllocationResult.output) / Math.pow(10, xSprkDecimals); + + // Get SPRK price from DefiLlama (since SPRK and xSPRK are 1:1) + const sprkPrice = await getTokenPrice(SPRK_TOKEN); + + if (!sprkPrice) { + return []; + } + + // Calculate TVL in USD + const tvlUsd = totalAllocation * sprkPrice; + + // Calculate total current distribution USD amount + let totalCurrentDistributionUsd = 0; + + for (const tokenAddress of distributedTokens) { + try { + // Get dividends info from smart contract + const dividendsInfoResult = await sdk.api.abi.call({ + target: DIVIDENDS_SC, + abi: dividendsABI.find(item => item.name === 'dividendsInfo'), + params: [tokenAddress], + chain: "flare", + }); + + if (dividendsInfoResult.output && dividendsInfoResult.output.length > 0) { + const currentDistributionRaw = dividendsInfoResult.output[0]; // currentDistributionAmount + + if (currentDistributionRaw && parseInt(currentDistributionRaw) > 0) { + // Get token decimals (assuming most tokens have 18 decimals for now) + // In production, you might want to fetch this from each token contract + const tokenDecimals = 18; // Default assumption + const currentDistribution = parseInt(currentDistributionRaw) / Math.pow(10, tokenDecimals); + + if (currentDistribution > 0) { + // Get token price + const tokenPrice = await getTokenPrice(tokenAddress); + + if (tokenPrice) { + // Calculate current distribution USD amount + const distributionUsd = currentDistribution * tokenPrice; + totalCurrentDistributionUsd += distributionUsd; + } + } + } + } + } catch (error) { + console.log(`Error getting dividends info for token ${tokenAddress}:`, error.message); + } + } + + // Calculate APR (not APY since it's not auto-compounding) + // APR = (current distribution USD / TVL USD) * (365 days / epoch duration) * 100 + const apr = totalCurrentDistributionUsd > 0 && tvlUsd > 0 + ? (totalCurrentDistributionUsd / tvlUsd) * (365 / EPOCH_DURATION_DAYS) * 100 + : 0; + + // Create pool object + const pool = { + pool: `${XSPRK_TOKEN}-flare`.toLowerCase(), + symbol: 'xSPRK', + project: 'sparkdex', + chain: 'flare', + tvlUsd, + apyBase: apr, // Using APR since it's not auto-compounding + underlyingTokens: [XSPRK_TOKEN], + rewardTokens: distributedTokens.filter(token => token !== XSPRK_TOKEN), // All distributed tokens except xSPRK + url: 'https://sparkdex.ai/stake', + }; + + return [pool]; + } catch (error) { + console.error('Error in SparkDEX adapter:', error); + return []; + } +}; + +// Helper function to get token price from DefiLlama +async function getTokenPrice(tokenAddress) { + try { + // Use DefiLlama price API + const response = await axios.get(`https://coins.llama.fi/prices/current/flare:${tokenAddress}`); + + if (response.data && response.data.coins && response.data.coins[`flare:${tokenAddress}`]) { + return response.data.coins[`flare:${tokenAddress}`].price; + } + + // Fallback: try to get price from FlareMetrics + try { + const flareMetricsResponse = await axios.get(`https://api.flaremetrics.io/v2/defi/flare/price?token=${tokenAddress}`); + if (flareMetricsResponse.data && flareMetricsResponse.data.price) { + return flareMetricsResponse.data.price; + } + } catch (flareMetricsError) { + console.log(`FlareMetrics price fetch failed for ${tokenAddress}:`, flareMetricsError.message); + } + + // If all else fails, return null + return null; + } catch (error) { + console.error(`Error getting price for token ${tokenAddress}:`, error.message); + return null; + } +} + +module.exports = { + timetravel: false, + apy, + url: 'https://sparkdex.ai/stake', +};