Skip to content

๐Ÿงช How to get a staking pool APY โ€‹

๐Ÿ” Full API Setup Example (JS) โ€‹

js
import { ethers } from "ethers";
import { BigNumber } from "bignumber.js";

// ABI for the Staking Pool contract - contains all necessary function signatures
// prettier-ignore
const STAKING_POOL_ABI = [{"inputs":[],"name":"InvalidInitialization","type":"error"},{"inputs":[],"name":"NotInitializing","type":"error"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"OwnableInvalidOwner","type":"error"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"OwnableUnauthorizedAccount","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"staker","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Claimed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"oldFee","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newFee","type":"uint256"}],"name":"FeeChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint64","name":"version","type":"uint64"}],"name":"Initialized","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":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Reward","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"staker","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Staked","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"staker","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Unstaked","type":"event"},{"inputs":[],"name":"BLOCKS_PER_DAY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"BLOCKS_PER_SECOND","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEFAULT_POOL_FEE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FEE_LOCK_PERIOD","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_POOL_FEE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RATIO_PRECISION","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"UNBONDING_PERIOD","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"active","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"claimIfRequiredAndUnstake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"claimUnstakes","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"staker","type":"address"}],"name":"currentStake","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"fee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getAvailableSelfStake","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getDailyVestingRate","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVestingSchedule","outputs":[{"internalType":"uint256","name":"lockPeriodDays","type":"uint256"},{"internalType":"uint256","name":"vestingDurationDays","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"increaseSelfStake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"initialLockPeriod","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"initialSelfStakeAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract PoolOwnership","name":"_ownershipNFT","type":"address"},{"internalType":"contract MATToken","name":"_token","type":"address"},{"internalType":"uint256","name":"_selfStake","type":"uint256"},{"internalType":"uint256","name":"_lockPeriodInDays","type":"uint256"},{"internalType":"uint256","name":"_vestingDurationInDays","type":"uint256"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"lastFeeChange","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"staker","type":"address"}],"name":"moveStakeToSelfStake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ownershipNFT","outputs":[{"internalType":"contract PoolOwnership","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"staker","type":"address"}],"name":"pendingUnstakes","outputs":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"block","type":"uint256"}],"internalType":"struct StakingPool.Unstake[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"poolCreation","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ratio","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"replenish","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"safeClaimUnstakes","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newFee","type":"uint256"}],"name":"setFee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"stake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"stakers","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"token","outputs":[{"internalType":"contract MATToken","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalStake","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":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"unstake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"vestingDuration","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]

/**
 * Array of staking pool contract addresses on the Matchain network
 * These are the validator pools that participate in the staking mechanism
 */
const stakingPoolAddresses = [
    "0xf9Ff433f14f237CE6643257ca4Cd5AEc355eaEE7",
    "0x256b02b26C67029B2881ab5d493cf8E5068D5Fcc",
    "0xa3BE9646116F834D0f9F1a035fdAd6862b5f0A2A",
    "0xfa38f584f561642Ff0d1AB67b5A175798668A047",
    "0x443D8318224C59c31987EA310033D0DC47A10d88",
    "0xc021A95E3Ad7D4e06745c4A12438A960c33859D7",
    "0xfF5Eb1345aa5d4C948C227742FCd32157539E480",
    "0x0B0381a2C4C3a537C614Afe71334c9F387718922",
    "0x9c4C30d5cD29c6D24CF1A712D93B756cBf5071bA",
    "0x8Aa82b86056af2DD5cdAAB45EEFE500c84d0af82",
    "0x4a47d64B074D730868623d89ec633d16adf53212",
    "0x739110f0233B8598957Af1321920787c13142910",
    "0xC3258AF62e46B502700c34ED7C3128F99f0fC532",
    "0xd899cd3cAFB1c9F6d585C36649592956D3dF5Ef9",
    "0xdB604bE88482cE061FBc2595E619f967dE1A7502",
    "0x96b5F62604729ED5b7219f1b3f15A8359E0a86Fb",
    "0xeEF37E218087Bca2f4c3c6C4c50f3D3B51B2Eb4d",
    "0xb4175a66e28F2C348959872B28AF0d12F891201F",
    "0xDD1904dcf7e0b6DFf60BD9729F626B076999E18f",
    "0x53F91B6aE02c8CDbC22565F7F7A158aDD6F1de6E",
    "0x0De1a88dcF7Fd12C54d9aC0807c990F26BD1C121",
];

// JSON-RPC provider for connecting to the Matchain network
const provider = new ethers.JsonRpcProvider("https://rpc.matchain.io");

/**
 * Interface representing the essential information of a staking pool
 */
interface PoolInfo {
    stakingPool: string;  // Contract address of the staking pool
    active: boolean;      // Whether the pool is currently active
    commission: bigint;   // Commission fee percentage (in basis points)
    totalStake: bigint;   // Total amount of tokens staked in this pool
}

/**
 * Parameters required for APY calculation
 */
interface ICalculateAPYParams {
    totalStakedAmount: BigNumber;  // Total staked amount across all pools
    fee: BigNumber;                // Pool commission fee as a percentage
}

// Maximum number of pools to consider for APY calculation (top pools by stake)
const POOLS_LIMIT = 21;

// Mathematical constants for APY calculation
const ONE = new BigNumber(1);
const ZERO = new BigNumber(0);
const HUNDRED = new BigNumber(100);

// Time constants for APY calculation
const avgSecondsInYear = new BigNumber(31556736);  // Average seconds in a year (365.25 days)
const tokensYear = avgSecondsInYear.div(3).div(2); // Token emission rate calculation

/**
 * Calculates the Annual Percentage Yield (APY) for a staking pool
 * Formula: (tokensPerYear * (1 - fee/100) / totalStakedAmount) * 100
 * 
 * @param totalStakedAmount - Total amount of tokens staked across all pools
 * @param fee - Commission fee percentage of the pool
 * @returns APY as a percentage
 */
const calculateAPY = ({ totalStakedAmount, fee }: ICalculateAPYParams): BigNumber => {
    return tokensYear.multipliedBy(ONE.minus(fee.div(HUNDRED)).div(totalStakedAmount).multipliedBy(HUNDRED));
};

/**
 * Fetches pool information from a staking pool contract
 * 
 * @param stakingPool - Contract address of the staking pool
 * @returns Promise resolving to PoolInfo object
 */
const getPool = async (stakingPool: string): Promise<PoolInfo> => {
    const pool = new ethers.Contract(stakingPool, STAKING_POOL_ABI, provider);
    
    // Fetch pool data in parallel for better performance
    const [active, commission, totalStake] = await Promise.all([
        pool.active(),
        pool.fee(),
        pool.totalStake()
    ]);

    return {
        stakingPool,
        active,
        commission,
        totalStake,
    };
};

/**
 * Main function to calculate APY for all staking pools
 * 
 * Process:
 * 1. Fetch information for all staking pools
 * 2. Sort pools by total stake (descending)
 * 3. Take top POOLS_LIMIT pools
 * 4. Calculate total staked amount across selected pools
 * 5. Calculate APY for each pool based on their commission and total stake
 * 
 * @returns Promise resolving to an object mapping pool addresses to their APY values
 */
const getStakingPoolApys = async (): Promise<{ [stakingPool: string]: BigNumber }> => {
    // Fetch pool information for all staking pools
    const pools: PoolInfo[] = [];
    for (const poolAddress of stakingPoolAddresses) {
        pools.push(await getPool(poolAddress));
    }

    // Sort pools by total stake (highest first) and limit to top pools
    const sortedStakingPoolInfos = pools
        .sort((a, b) => (b.totalStake >= a.totalStake ? 1 : -1))
        .slice(0, POOLS_LIMIT);
    
    // Calculate total staked amount across all selected pools
    const totalStakedAmount = sortedStakingPoolInfos
        .map((v) => v.totalStake)
        .reduce((acc, v) => acc + v, 0n);

    // Calculate APY for each pool
    const stakingPoolAPYs: { [stakingPool: string]: BigNumber } = {};
    sortedStakingPoolInfos.forEach((v) => {
        stakingPoolAPYs[v.stakingPool] = calculateAPY({
            totalStakedAmount: new BigNumber(ethers.formatEther(totalStakedAmount)),
            fee: new BigNumber(ethers.formatUnits(v.commission, 2)),
        });
    });

    console.log(stakingPoolAPYs);
    return stakingPoolAPYs;
};

// Execute the APY calculation
getStakingPoolApys();

// Example output format:
// For example: 19.798509935889813888 => APY: 19.798509935889813888 %
// {
//   '0x96b5F62604729ED5b7219f1b3f15A8359E0a86Fb': 19.798509935889813888,
//   '0xf9Ff433f14f237CE6643257ca4Cd5AEc355eaEE7': 19.798509935889813888,
//   '0x0De1a88dcF7Fd12C54d9aC0807c990F26BD1C121': 19.798509935889813888,
//   '0x8Aa82b86056af2DD5cdAAB45EEFE500c84d0af82': 19.798509935889813888,
//   '0x256b02b26C67029B2881ab5d493cf8E5068D5Fcc': 19.798509935889813888,
//   '0xa3BE9646116F834D0f9F1a035fdAd6862b5f0A2A': 19.798509935889813888,
//   '0xc021A95E3Ad7D4e06745c4A12438A960c33859D7': 19.798509935889813888,
//   '0xC3258AF62e46B502700c34ED7C3128F99f0fC532': 19.798509935889813888,
//   '0x443D8318224C59c31987EA310033D0DC47A10d88': 19.798509935889813888,
//   '0x53F91B6aE02c8CDbC22565F7F7A158aDD6F1de6E': 19.798509935889813888,
//   '0x0B0381a2C4C3a537C614Afe71334c9F387718922': 19.798509935889813888,
//   '0xfF5Eb1345aa5d4C948C227742FCd32157539E480': 19.798509935889813888,
//   '0x9c4C30d5cD29c6D24CF1A712D93B756cBf5071bA': 19.798509935889813888,
//   '0xDD1904dcf7e0b6DFf60BD9729F626B076999E18f': 19.798509935889813888,
//   '0xb4175a66e28F2C348959872B28AF0d12F891201F': 19.798509935889813888,
//   '0xfa38f584f561642Ff0d1AB67b5A175798668A047': 19.798509935889813888,
//   '0xeEF37E218087Bca2f4c3c6C4c50f3D3B51B2Eb4d': 19.798509935889813888,
//   '0xdB604bE88482cE061FBc2595E619f967dE1A7502': 19.798509935889813888,
//   '0xd899cd3cAFB1c9F6d585C36649592956D3dF5Ef9': 19.798509935889813888,
//   '0x4a47d64B074D730868623d89ec633d16adf53212': 19.798509935889813888,
//   '0x739110f0233B8598957Af1321920787c13142910': 19.798509935889813888
// }