diff --git a/src/PoolInfoUtils.sol b/src/PoolInfoUtils.sol index beed3f53a..748f9b669 100644 --- a/src/PoolInfoUtils.sol +++ b/src/PoolInfoUtils.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.18; import { Math } from '@openzeppelin/contracts/utils/math/Math.sol'; +import { Multicall } from '@openzeppelin/contracts/utils/Multicall.sol'; import { IPool, IERC20Token } from './interfaces/pool/IPool.sol'; @@ -32,7 +33,7 @@ import { PoolCommons } from './libraries/external/PoolCommons.sol'; * @notice Contract for providing information for any deployed pool. * @dev Pool info is calculated using same helper functions / logic as in `Pool` contracts. */ -contract PoolInfoUtils { +contract PoolInfoUtils is Multicall { /** * @notice Exposes status of a liquidation auction. diff --git a/src/PoolInfoUtilsMulticall.sol b/src/PoolInfoUtilsMulticall.sol index 6d07f4cee..b0ac9fe2f 100644 --- a/src/PoolInfoUtilsMulticall.sol +++ b/src/PoolInfoUtilsMulticall.sol @@ -2,12 +2,27 @@ pragma solidity 0.8.18; +import { Multicall } from '@openzeppelin/contracts/utils/Multicall.sol'; +import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + import { PoolInfoUtils } from "./PoolInfoUtils.sol"; +import { IPool } from "./interfaces/pool/IPool.sol"; +import { IERC20Pool } from "./interfaces/pool/erc20/IERC20Pool.sol"; + contract PoolInfoUtilsMulticall { PoolInfoUtils public immutable poolInfoUtils; + struct PoolLoansInfo { + uint256 poolSize; + uint256 loansCount; + address maxBorrower; + uint256 pendingInflator; + uint256 pendingInterestFactor; + } + struct PoolPriceInfo { uint256 hpb; uint256 hpbIndex; @@ -17,6 +32,12 @@ contract PoolInfoUtilsMulticall { uint256 lupIndex; } + struct PoolRatesAndFees { + uint256 lenderInterestMargin; + uint256 borrowFeeRate; + uint256 depositFeeRate; + } + struct PoolReservesInfo { uint256 reserves; uint256 claimableReserves; @@ -32,13 +53,14 @@ contract PoolInfoUtilsMulticall { uint256 poolTargetUtilization; } - struct BucketInfo { - uint256 price; - uint256 quoteTokens; - uint256 collateral; - uint256 bucketLP; - uint256 scale; - uint256 exchangeRate; + struct PoolBalanceDetails { + uint256 debt; // debtInfo() + uint256 accruedDebt; // debtInfo() + uint256 debtInAuction; // debtInfo() + uint256 t0Debt2ToCollateral; // debtInfo() + uint256 depositUpToIndex; + uint256 quoteTokenBalance; + uint256 collateralTokenBalance; } constructor(PoolInfoUtils poolInfoUtils_) { @@ -46,24 +68,32 @@ contract PoolInfoUtilsMulticall { } /** - * @notice Retrieves PoolPriceInfo, PoolReservesInfo, PoolUtilizationInfo and BucketInfo + * @notice Retrieves PoolLoansInfo, PoolPriceInfo, PoolRatesAndFees, PoolReservesInfo and PoolUtilizationInfo + * @dev This function is used to retrieve pool details available from PoolInfoUtils in a single RPC call for Indexers. * @param ajnaPool_ Address of `Ajna` pool - * @param bucketIndex_ The index of the bucket to retrieve + * @return poolLoansInfo_ Pool loans info struct * @return poolPriceInfo_ Pool price info struct + * @return poolRatesAndFees_ Pool rates and fees struct * @return poolReservesInfo_ Pool reserves info struct * @return poolUtilizationInfo_ Pool utilization info struct - * @return bucketInfo_ Bucket info struct */ - function poolDetailsAndBucketInfo(address ajnaPool_, uint256 bucketIndex_) - external - view - returns( - PoolPriceInfo memory poolPriceInfo_, - PoolReservesInfo memory poolReservesInfo_, - PoolUtilizationInfo memory poolUtilizationInfo_, - BucketInfo memory bucketInfo_ - ) - { + function poolDetailsMulticall(address ajnaPool_) external view returns ( + PoolLoansInfo memory poolLoansInfo_, + PoolPriceInfo memory poolPriceInfo_, + PoolRatesAndFees memory poolRatesAndFees_, + PoolReservesInfo memory poolReservesInfo_, + PoolUtilizationInfo memory poolUtilizationInfo_ + ) { + // retrieve loans info + ( + poolLoansInfo_.poolSize, + poolLoansInfo_.loansCount, + poolLoansInfo_.maxBorrower, + poolLoansInfo_.pendingInflator, + poolLoansInfo_.pendingInterestFactor + ) = poolInfoUtils.poolLoansInfo(ajnaPool_); + + // retrieve prices info ( poolPriceInfo_.hpb, poolPriceInfo_.hpbIndex, @@ -73,6 +103,12 @@ contract PoolInfoUtilsMulticall { poolPriceInfo_.lupIndex ) = poolInfoUtils.poolPricesInfo(ajnaPool_); + // retrieve rates and fees + poolRatesAndFees_.lenderInterestMargin = poolInfoUtils.lenderInterestMargin(ajnaPool_); + poolRatesAndFees_.borrowFeeRate = poolInfoUtils.borrowFeeRate(ajnaPool_); + poolRatesAndFees_.depositFeeRate = poolInfoUtils.unutilizedDepositFeeRate(ajnaPool_); + + // retrieve reserves info ( poolReservesInfo_.reserves, poolReservesInfo_.claimableReserves, @@ -81,21 +117,13 @@ contract PoolInfoUtilsMulticall { poolReservesInfo_.timeRemaining ) = poolInfoUtils.poolReservesInfo(ajnaPool_); + // retrieve utilization info ( poolUtilizationInfo_.poolMinDebtAmount, poolUtilizationInfo_.poolCollateralization, poolUtilizationInfo_.poolActualUtilization, poolUtilizationInfo_.poolTargetUtilization ) = poolInfoUtils.poolUtilizationInfo(ajnaPool_); - - ( - bucketInfo_.price, - bucketInfo_.quoteTokens, - bucketInfo_.collateral, - bucketInfo_.bucketLP, - bucketInfo_.scale, - bucketInfo_.exchangeRate - ) = poolInfoUtils.bucketInfo(ajnaPool_, bucketIndex_); } /** @@ -105,21 +133,60 @@ contract PoolInfoUtilsMulticall { * @return borrowFeeRate Borrow fee rate calculated from the pool interest ra * @return depositFeeRate Deposit fee rate calculated from the pool interest rate */ - function poolRatesAndFees(address ajnaPool_) + function poolRatesAndFeesMulticall(address ajnaPool_) external - view - returns + returns ( uint256 lenderInterestMargin, uint256 borrowFeeRate, uint256 depositFeeRate - ) + ) { lenderInterestMargin = poolInfoUtils.lenderInterestMargin(ajnaPool_); borrowFeeRate = poolInfoUtils.borrowFeeRate(ajnaPool_); depositFeeRate = poolInfoUtils.unutilizedDepositFeeRate(ajnaPool_); } + /** + * @notice Retrieves pool debtInfo, depositUpToIndex, quoteTokenBalance and collateralTokenBalance + * @dev This function is used to retrieve pool balance details in a single RPC call for Indexers. + * @param ajnaPool_ Address of `Ajna` pool + * @param index_ Index of deposit + * @param quoteTokenAddress_ Address of quote token + * @param collateralTokenAddress_ Address of collateral token + * @param isNFT_ Boolean indicating if the pool is an NFT pool + * @return poolBalanceDetails_ Pool balance details struct + */ + function poolBalanceDetails(address ajnaPool_, uint256 index_, address quoteTokenAddress_, address collateralTokenAddress_, bool isNFT_) + external view + returns (PoolBalanceDetails memory poolBalanceDetails_) + { + IPool pool = IPool(ajnaPool_); + + // pool debtInfo + (poolBalanceDetails_.debt, poolBalanceDetails_.accruedDebt, poolBalanceDetails_.debtInAuction, poolBalanceDetails_.t0Debt2ToCollateral) = pool.debtInfo(); + + // depositUpToIndex(index_) + poolBalanceDetails_.depositUpToIndex = pool.depositUpToIndex(index_); + + // get pool quote token balance + uint256 poolQuoteBalance = IERC20(quoteTokenAddress_).balanceOf(ajnaPool_); + uint256 quoteScale = pool.quoteTokenScale(); + // normalize token balance to WAD scale + poolBalanceDetails_.quoteTokenBalance = poolQuoteBalance * quoteScale; + + // get pool collateral token balance + if (isNFT_) { + // convert whole NFT amounts to WAD to match pool accounting + poolBalanceDetails_.collateralTokenBalance = IERC721(collateralTokenAddress_).balanceOf(ajnaPool_) * 10**18; + } else { + // normalize token balance to WAD scale + uint256 collateralScale = IERC20Pool(ajnaPool_).collateralScale(); + uint256 poolCollateralBalance = IERC20(collateralTokenAddress_).balanceOf(ajnaPool_); + poolBalanceDetails_.collateralTokenBalance = poolCollateralBalance * collateralScale; + } + } + /** * @notice Aggregate results from multiple read-only function calls * @param functionSignatures_ Array of signatures of read-only functions to be called diff --git a/tests/forge/unit/ERC20Pool/ERC20PoolInfoUtils.t.sol b/tests/forge/unit/ERC20Pool/ERC20PoolInfoUtils.t.sol index 915805a0f..c2814230a 100644 --- a/tests/forge/unit/ERC20Pool/ERC20PoolInfoUtils.t.sol +++ b/tests/forge/unit/ERC20Pool/ERC20PoolInfoUtils.t.sol @@ -265,16 +265,6 @@ contract ERC20PoolInfoUtilsTest is ERC20HelperContract { ); } - function testPoolInfoUtilsMulticallPoolAndBucketInfo() external { - PoolInfoUtilsMulticall poolUtilsMulticall = new PoolInfoUtilsMulticall(_poolUtils); - - PoolInfoUtilsMulticall.BucketInfo memory bucketInfo; - - (,,, bucketInfo) = poolUtilsMulticall.poolDetailsAndBucketInfo(address(_pool), high); - - assertEq(bucketInfo.bucketLP, 10_000 * 1e18); - } - function testPoolInfoUtilsMulticall() external { PoolInfoUtilsMulticall poolUtilsMulticall = new PoolInfoUtilsMulticall(_poolUtils); @@ -311,4 +301,57 @@ contract ERC20PoolInfoUtilsTest is ERC20HelperContract { assertEq(debt, 21_020.192307692307702000 * 1e18); assertEq(abi.decode(result[1], (uint256)), _poolUtils.htp(address(_pool))); } + + function testPoolInfoUtilsMulticallRatesAndFees() external { + PoolInfoUtilsMulticall poolUtilsMulticall = new PoolInfoUtilsMulticall(_poolUtils); + + (uint256 lim, uint256 bfr, uint256 dfr) = poolUtilsMulticall.poolRatesAndFeesMulticall(address(_pool)); + + assertGe(lim, 0.000136986301369863 * 1e18); + assertGe(bfr, 0.000961538461538462 * 1e18); + assertGe(dfr, 0.000136986301369863 * 1e18); + } + + function testPoolInfoUtilsMulticallPoolDetails() external { + PoolInfoUtilsMulticall poolUtilsMulticall = new PoolInfoUtilsMulticall(_poolUtils); + + ( + PoolInfoUtilsMulticall.PoolLoansInfo memory poolLoansInfo, + PoolInfoUtilsMulticall.PoolPriceInfo memory poolPriceInfo, + PoolInfoUtilsMulticall.PoolRatesAndFees memory poolRatesAndFees, + PoolInfoUtilsMulticall.PoolReservesInfo memory poolReservesInfo, + PoolInfoUtilsMulticall.PoolUtilizationInfo memory poolUtilizationInfo + ) = poolUtilsMulticall.poolDetailsMulticall(address(_pool)); + + assertEq(poolLoansInfo.poolSize, 50_000 * 1e18); + + assertEq(poolPriceInfo.hpb, 3_010.892022197881557845 * 1e18); + assertEq(poolPriceInfo.hpbIndex, 2550); + assertEq(poolPriceInfo.htp, 210.201923076923077020 * 1e18); + assertEq(poolPriceInfo.htpIndex, 3083); + assertEq(poolPriceInfo.lup, 2981.007422784467321543 * 1e18); + assertEq(poolPriceInfo.lupIndex, 2552); + + assertGe(poolRatesAndFees.lenderInterestMargin, 0.000136986301369863 * 1e18); + assertGe(poolRatesAndFees.borrowFeeRate, 0.000961538461538462 * 1e18); + assertGe(poolRatesAndFees.depositFeeRate, 0.000136986301369863 * 1e18); + + assertEq(poolReservesInfo.reserves, 20.192307692307702000 * 1e18); + + assertEq(poolUtilizationInfo.poolMinDebtAmount, 2_102.019230769230770200 * 1e18); + } + + function testPoolInfoUtilsMulticallPoolBalanceDetails() external { + PoolInfoUtilsMulticall poolUtilsMulticall = new PoolInfoUtilsMulticall(_poolUtils); + + uint256 meaningfulIndex = 5000; + address quoteTokenAddress = IPool(_pool).quoteTokenAddress(); + address collateralTokenAddress = IPool(_pool).collateralAddress(); + + PoolInfoUtilsMulticall.PoolBalanceDetails memory poolBalanceDetails = poolUtilsMulticall.poolBalanceDetails(address(_pool), meaningfulIndex, quoteTokenAddress, collateralTokenAddress, false); + + assertEq(poolBalanceDetails.debt, 21_020.192307692307702000 * 1e18); + assertEq(poolBalanceDetails.quoteTokenBalance, 29_000 * 1e18); + assertEq(poolBalanceDetails.collateralTokenBalance, 100 * 1e18); + } }