diff --git a/contracts/incentives-v2/DistributionManagerV2.sol b/contracts/incentives-v2/DistributionManagerV2.sol index 8cc2d493..672796b0 100644 --- a/contracts/incentives-v2/DistributionManagerV2.sol +++ b/contracts/incentives-v2/DistributionManagerV2.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.10; import {IAaveDistributionManagerV2} from './interfaces/IAaveDistributionManagerV2.sol'; import {DistributionTypesV2} from './libraries/DistributionTypesV2.sol'; +import {IERC20Detailed} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20Detailed.sol'; /** * @title DistributionManagerV2 @@ -20,13 +21,12 @@ abstract contract DistributionManagerV2 is IAaveDistributionManagerV2 { struct AssetData { mapping(address => RewardData) rewards; address[] availableRewards; + uint8 decimals; } // manager of incentives address public immutable EMISSION_MANAGER; - uint8 public constant PRECISION = 18; - // asset => AssetData mapping(address => AssetData) internal _assets; @@ -148,6 +148,8 @@ abstract contract DistributionManagerV2 is IAaveDistributionManagerV2 { **/ function _configureAssets(DistributionTypesV2.RewardsConfigInput[] memory rewardsInput) internal { for (uint256 i = 0; i < rewardsInput.length; i++) { + _assets[rewardsInput[i].asset].decimals = IERC20Detailed(rewardsInput[i].asset).decimals(); + RewardData storage rewardConfig = _assets[rewardsInput[i].asset].rewards[ rewardsInput[i].reward ]; @@ -168,7 +170,8 @@ abstract contract DistributionManagerV2 is IAaveDistributionManagerV2 { rewardsInput[i].asset, rewardsInput[i].reward, rewardConfig, - rewardsInput[i].totalSupply + rewardsInput[i].totalSupply, + _assets[rewardsInput[i].asset].decimals ); // Configure emission and distribution end of the reward per asset @@ -189,14 +192,16 @@ abstract contract DistributionManagerV2 is IAaveDistributionManagerV2 { * @param asset The address of the asset being updated * @param reward The address of the reward being updated * @param rewardConfig Storage pointer to the distribution's reward config - * @param totalSupply Current total of staked _assets for this distribution + * @param totalSupply Current total of underlying assets for this distribution + * @param decimals The decimals of the underlying asset * @return The new distribution index **/ function _updateAssetStateInternal( address asset, address reward, RewardData storage rewardConfig, - uint256 totalSupply + uint256 totalSupply, + uint8 decimals ) internal returns (uint256) { uint256 oldIndex = rewardConfig.index; @@ -209,7 +214,8 @@ abstract contract DistributionManagerV2 is IAaveDistributionManagerV2 { rewardConfig.emissionPerSecond, rewardConfig.lastUpdateTimestamp, rewardConfig.distributionEnd, - totalSupply + totalSupply, + decimals ); if (newIndex != oldIndex) { @@ -245,11 +251,17 @@ abstract contract DistributionManagerV2 is IAaveDistributionManagerV2 { uint256 userIndex = rewardData.usersIndex[user]; uint256 accruedRewards = 0; - uint256 newIndex = _updateAssetStateInternal(asset, reward, rewardData, totalSupply); + uint256 newIndex = _updateAssetStateInternal( + asset, + reward, + rewardData, + totalSupply, + _assets[asset].decimals + ); if (userIndex != newIndex) { if (userBalance != 0) { - accruedRewards = _getRewards(userBalance, newIndex, userIndex); + accruedRewards = _getRewards(userBalance, newIndex, userIndex, _assets[asset].decimals); } rewardData.usersIndex[user] = newIndex; @@ -259,6 +271,13 @@ abstract contract DistributionManagerV2 is IAaveDistributionManagerV2 { return accruedRewards; } + /** + * @dev Iterates and updates all rewards of an asset that belongs to an user + * @param asset The address of the reference asset of the distribution + * @param user The user address + * @param userBalance The current user asset balance + * @param totalSupply Total supply of the asset + **/ function _updateUserRewardsPerAssetInternal( address asset, address user, @@ -370,31 +389,34 @@ abstract contract DistributionManagerV2 is IAaveDistributionManagerV2 { DistributionTypesV2.UserAssetStatsInput memory stake ) internal view returns (uint256) { RewardData storage rewardData = _assets[stake.underlyingAsset].rewards[reward]; - + uint8 assetDecimals = _assets[stake.underlyingAsset].decimals; uint256 assetIndex = _getAssetIndex( rewardData.index, rewardData.emissionPerSecond, rewardData.lastUpdateTimestamp, rewardData.distributionEnd, - stake.totalSupply + stake.totalSupply, + assetDecimals ); - return _getRewards(stake.userBalance, assetIndex, rewardData.usersIndex[user]); + return _getRewards(stake.userBalance, assetIndex, rewardData.usersIndex[user], assetDecimals); } /** * @dev Internal function for the calculation of user's rewards on a distribution - * @param principalUserBalance Amount staked by the user on a distribution + * @param principalUserBalance Balance of the user asset on a distribution * @param reserveIndex Current index of the distribution * @param userIndex Index stored for the user, representation his staking moment + * @param decimals The decimals of the underlying asset * @return The rewards **/ function _getRewards( uint256 principalUserBalance, uint256 reserveIndex, - uint256 userIndex + uint256 userIndex, + uint8 decimals ) internal pure returns (uint256) { - return (principalUserBalance * (reserveIndex - userIndex)) / 10**uint256(PRECISION); + return (principalUserBalance * (reserveIndex - userIndex)) / 10**decimals; } /** @@ -403,6 +425,7 @@ abstract contract DistributionManagerV2 is IAaveDistributionManagerV2 { * @param emissionPerSecond Representing the total rewards distributed per second per asset unit, on the distribution * @param lastUpdateTimestamp Last moment this distribution was updated * @param totalBalance of tokens considered for the distribution + * @param decimals The decimals of the underlying asset * @return The new index. **/ function _getAssetIndex( @@ -410,7 +433,8 @@ abstract contract DistributionManagerV2 is IAaveDistributionManagerV2 { uint256 emissionPerSecond, uint128 lastUpdateTimestamp, uint256 distributionEnd, - uint256 totalBalance + uint256 totalBalance, + uint8 decimals ) internal view returns (uint256) { if ( emissionPerSecond == 0 || @@ -425,7 +449,7 @@ abstract contract DistributionManagerV2 is IAaveDistributionManagerV2 { ? distributionEnd : block.timestamp; uint256 timeDelta = currentTimestamp - lastUpdateTimestamp; - return (emissionPerSecond * timeDelta * (10**uint256(PRECISION))) / totalBalance + currentIndex; + return (emissionPerSecond * timeDelta * (10**decimals)) / totalBalance + currentIndex; } /** @@ -439,4 +463,9 @@ abstract contract DistributionManagerV2 is IAaveDistributionManagerV2 { view virtual returns (DistributionTypesV2.UserAssetStatsInput[] memory userState); + + /// @inheritdoc IAaveDistributionManagerV2 + function getAssetDecimals(address asset) external view returns (uint8) { + return _assets[asset].decimals; + } } diff --git a/contracts/incentives-v2/interfaces/IAaveDistributionManagerV2.sol b/contracts/incentives-v2/interfaces/IAaveDistributionManagerV2.sol index d4536daf..f54d9e62 100644 --- a/contracts/incentives-v2/interfaces/IAaveDistributionManagerV2.sol +++ b/contracts/incentives-v2/interfaces/IAaveDistributionManagerV2.sol @@ -118,8 +118,9 @@ interface IAaveDistributionManagerV2 { returns (address[] memory, uint256[] memory); /** - * @dev Returns the precision to calculate the distribution delta - * @return The precision of the calculation + * @dev Returns the decimals of an asset to calculate the distribution delta + * @param asset The address to retrieve decimals saved at storage + * @return The decimals of an underlying asset */ - function PRECISION() external view returns (uint8); + function getAssetDecimals(address asset) external view returns (uint8); } diff --git a/contracts/misc/UiIncentiveDataProviderV3.sol b/contracts/misc/UiIncentiveDataProviderV3.sol index dfb2f4b8..4956e229 100644 --- a/contracts/misc/UiIncentiveDataProviderV3.sol +++ b/contracts/misc/UiIncentiveDataProviderV3.sol @@ -2,18 +2,19 @@ pragma solidity 0.8.10; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; -import {IAaveIncentivesController} from '@aave/core-v3/contracts/interfaces/IAaveIncentivesController.sol'; -import {IUiIncentiveDataProvider} from './interfaces/IUiIncentiveDataProvider.sol'; +import {IAaveIncentivesControllerV2} from '../incentives-v2/interfaces/IAaveIncentivesControllerV2.sol'; +import {IUiIncentiveDataProviderV3} from './interfaces/IUiIncentiveDataProviderV3.sol'; import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; import {IncentivizedERC20} from '@aave/core-v3/contracts/protocol/tokenization/IncentivizedERC20.sol'; import {UserConfiguration} from '@aave/core-v3/contracts/protocol/libraries/configuration/UserConfiguration.sol'; import {DataTypes} from '@aave/core-v3/contracts/protocol/libraries/types/DataTypes.sol'; import {IERC20Detailed} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20Detailed.sol'; +import {IEACAggregatorProxy} from './interfaces/IEACAggregatorProxy.sol'; -contract UiIncentiveDataProviderV3 is IUiIncentiveDataProvider { +contract UiIncentiveDataProviderV3 is IUiIncentiveDataProviderV3 { using UserConfiguration for DataTypes.UserConfigurationMap; - constructor() public {} + constructor() {} function getFullReservesIncentiveData(IPoolAddressesProvider provider, address user) external @@ -42,162 +43,174 @@ contract UiIncentiveDataProviderV3 is IUiIncentiveDataProvider { address[] memory reserves = lendingPool.getReservesList(); AggregatedReserveIncentiveData[] memory reservesIncentiveData = new AggregatedReserveIncentiveData[](reserves.length); - + // Iterate through the reserves to get all the information from the (a/s/v) Tokens for (uint256 i = 0; i < reserves.length; i++) { AggregatedReserveIncentiveData memory reserveIncentiveData = reservesIncentiveData[i]; reserveIncentiveData.underlyingAsset = reserves[i]; DataTypes.ReserveData memory baseData = lendingPool.getReserveData(reserves[i]); - try IncentivizedERC20(baseData.aTokenAddress).getIncentivesController() returns ( - IAaveIncentivesController aTokenIncentiveController - ) { - if (address(aTokenIncentiveController) != address(0)) { - address aRewardToken = aTokenIncentiveController.REWARD_TOKEN(); - - try aTokenIncentiveController.getAssetData(baseData.aTokenAddress) returns ( - uint256 aTokenIncentivesIndex, - uint256 aEmissionPerSecond, - uint256 aIncentivesLastUpdateTimestamp - ) { - reserveIncentiveData.aIncentiveData = IncentiveData( - aEmissionPerSecond, - aIncentivesLastUpdateTimestamp, - aTokenIncentivesIndex, - aTokenIncentiveController.DISTRIBUTION_END(), - baseData.aTokenAddress, - aRewardToken, - address(aTokenIncentiveController), - IERC20Detailed(aRewardToken).decimals(), - aTokenIncentiveController.PRECISION() - ); - } catch ( - bytes memory /*lowLevelData*/ - ) { - ( - uint256 aEmissionPerSecond, - uint256 aIncentivesLastUpdateTimestamp, - uint256 aTokenIncentivesIndex - ) = aTokenIncentiveController.assets(baseData.aTokenAddress); - - reserveIncentiveData.aIncentiveData = IncentiveData( - aEmissionPerSecond, - aIncentivesLastUpdateTimestamp, - aTokenIncentivesIndex, - aTokenIncentiveController.DISTRIBUTION_END(), - baseData.aTokenAddress, - aRewardToken, - address(aTokenIncentiveController), - IERC20Detailed(aRewardToken).decimals(), - aTokenIncentiveController.PRECISION() - ); - } + // Get aTokens rewards information + // TODO: check that this is deployed correctly on contract and remove casting + IAaveIncentivesControllerV2 aTokenIncentiveController = IAaveIncentivesControllerV2( + address(IncentivizedERC20(baseData.aTokenAddress).getIncentivesController()) + ); + RewardInfo[] memory aRewardsInformation; + if (address(aTokenIncentiveController) != address(0)) { + address[] memory aTokenRewardAddresses = aTokenIncentiveController.getRewardsByAsset( + baseData.aTokenAddress + ); + + aRewardsInformation = new RewardInfo[](aTokenRewardAddresses.length); + for (uint256 j = 0; j < aTokenRewardAddresses.length; ++j) { + RewardInfo memory rewardInformation; + rewardInformation.rewardTokenAddress = aTokenRewardAddresses[j]; + + ( + rewardInformation.tokenIncentivesIndex, + rewardInformation.emissionPerSecond, + rewardInformation.incentivesLastUpdateTimestamp, + rewardInformation.emissionEndTimestamp + ) = aTokenIncentiveController.getRewardsData( + baseData.aTokenAddress, + rewardInformation.rewardTokenAddress + ); + + rewardInformation.precision = aTokenIncentiveController.getAssetDecimals(baseData.aTokenAddress); + rewardInformation.rewardTokenDecimals = IERC20Detailed( + rewardInformation.rewardTokenAddress + ).decimals(); + rewardInformation.rewardTokenSymbol = IERC20Detailed(rewardInformation.rewardTokenAddress) + .symbol(); + + // Get price of reward token from Chainlink Proxy Oracle + rewardInformation.rewardOracleAddress = aTokenIncentiveController.getRewardOracle( + rewardInformation.rewardTokenAddress + ); + rewardInformation.priceFeedDecimals = IEACAggregatorProxy( + rewardInformation.rewardOracleAddress + ).decimals(); + rewardInformation.rewardPriceFeed = IEACAggregatorProxy( + rewardInformation.rewardOracleAddress + ).latestAnswer(); + + aRewardsInformation[j] = rewardInformation; } - } catch ( - bytes memory /*lowLevelData*/ - ) { - // Will not get here } - try IncentivizedERC20(baseData.stableDebtTokenAddress).getIncentivesController() returns ( - IAaveIncentivesController sTokenIncentiveController - ) { - if (address(sTokenIncentiveController) != address(0)) { - address sRewardToken = sTokenIncentiveController.REWARD_TOKEN(); - try sTokenIncentiveController.getAssetData(baseData.stableDebtTokenAddress) returns ( - uint256 sTokenIncentivesIndex, - uint256 sEmissionPerSecond, - uint256 sIncentivesLastUpdateTimestamp - ) { - reserveIncentiveData.sIncentiveData = IncentiveData( - sEmissionPerSecond, - sIncentivesLastUpdateTimestamp, - sTokenIncentivesIndex, - sTokenIncentiveController.DISTRIBUTION_END(), - baseData.stableDebtTokenAddress, - sRewardToken, - address(sTokenIncentiveController), - IERC20Detailed(sRewardToken).decimals(), - sTokenIncentiveController.PRECISION() - ); - } catch ( - bytes memory /*lowLevelData*/ - ) { - ( - uint256 sEmissionPerSecond, - uint256 sIncentivesLastUpdateTimestamp, - uint256 sTokenIncentivesIndex - ) = sTokenIncentiveController.assets(baseData.stableDebtTokenAddress); - - reserveIncentiveData.sIncentiveData = IncentiveData( - sEmissionPerSecond, - sIncentivesLastUpdateTimestamp, - sTokenIncentivesIndex, - sTokenIncentiveController.DISTRIBUTION_END(), - baseData.stableDebtTokenAddress, - sRewardToken, - address(sTokenIncentiveController), - IERC20Detailed(sRewardToken).decimals(), - sTokenIncentiveController.PRECISION() - ); - } + reserveIncentiveData.aIncentiveData = IncentiveData( + baseData.aTokenAddress, + address(aTokenIncentiveController), + aRewardsInformation + ); + + // Get vTokens rewards information + IAaveIncentivesControllerV2 vTokenIncentiveController = IAaveIncentivesControllerV2( + address(IncentivizedERC20(baseData.variableDebtTokenAddress).getIncentivesController()) + ); + address[] memory vTokenRewardAddresses = vTokenIncentiveController.getRewardsByAsset( + baseData.variableDebtTokenAddress + ); + RewardInfo[] memory vRewardsInformation; + + if (address(vTokenIncentiveController) != address(0)) { + vRewardsInformation = new RewardInfo[](vTokenRewardAddresses.length); + for (uint256 j = 0; j < vTokenRewardAddresses.length; ++j) { + RewardInfo memory rewardInformation; + rewardInformation.rewardTokenAddress = vTokenRewardAddresses[j]; + + ( + rewardInformation.tokenIncentivesIndex, + rewardInformation.emissionPerSecond, + rewardInformation.incentivesLastUpdateTimestamp, + rewardInformation.emissionEndTimestamp + ) = vTokenIncentiveController.getRewardsData( + baseData.variableDebtTokenAddress, + rewardInformation.rewardTokenAddress + ); + + rewardInformation.precision = vTokenIncentiveController.getAssetDecimals(baseData.variableDebtTokenAddress); + rewardInformation.rewardTokenDecimals = IERC20Detailed( + rewardInformation.rewardTokenAddress + ).decimals(); + rewardInformation.rewardTokenSymbol = IERC20Detailed(rewardInformation.rewardTokenAddress) + .symbol(); + + // Get price of reward token from Chainlink Proxy Oracle + rewardInformation.rewardOracleAddress = vTokenIncentiveController.getRewardOracle( + rewardInformation.rewardTokenAddress + ); + rewardInformation.priceFeedDecimals = IEACAggregatorProxy( + rewardInformation.rewardOracleAddress + ).decimals(); + rewardInformation.rewardPriceFeed = IEACAggregatorProxy( + rewardInformation.rewardOracleAddress + ).latestAnswer(); + + vRewardsInformation[j] = rewardInformation; } - } catch ( - bytes memory /*lowLevelData*/ - ) { - // Will not get here } - try IncentivizedERC20(baseData.variableDebtTokenAddress).getIncentivesController() returns ( - IAaveIncentivesController vTokenIncentiveController - ) { - if (address(vTokenIncentiveController) != address(0)) { - address vRewardToken = vTokenIncentiveController.REWARD_TOKEN(); - - try vTokenIncentiveController.getAssetData(baseData.variableDebtTokenAddress) returns ( - uint256 vTokenIncentivesIndex, - uint256 vEmissionPerSecond, - uint256 vIncentivesLastUpdateTimestamp - ) { - reserveIncentiveData.vIncentiveData = IncentiveData( - vEmissionPerSecond, - vIncentivesLastUpdateTimestamp, - vTokenIncentivesIndex, - vTokenIncentiveController.DISTRIBUTION_END(), - baseData.variableDebtTokenAddress, - vRewardToken, - address(vTokenIncentiveController), - IERC20Detailed(vRewardToken).decimals(), - vTokenIncentiveController.PRECISION() - ); - } catch ( - bytes memory /*lowLevelData*/ - ) { - ( - uint256 vEmissionPerSecond, - uint256 vIncentivesLastUpdateTimestamp, - uint256 vTokenIncentivesIndex - ) = vTokenIncentiveController.assets(baseData.variableDebtTokenAddress); - - reserveIncentiveData.vIncentiveData = IncentiveData( - vEmissionPerSecond, - vIncentivesLastUpdateTimestamp, - vTokenIncentivesIndex, - vTokenIncentiveController.DISTRIBUTION_END(), - baseData.variableDebtTokenAddress, - vRewardToken, - address(vTokenIncentiveController), - IERC20Detailed(vRewardToken).decimals(), - vTokenIncentiveController.PRECISION() - ); - } + reserveIncentiveData.vIncentiveData = IncentiveData( + baseData.variableDebtTokenAddress, + address(vTokenIncentiveController), + vRewardsInformation + ); + + // Get sTokens rewards information + IAaveIncentivesControllerV2 sTokenIncentiveController = IAaveIncentivesControllerV2( + address(IncentivizedERC20(baseData.stableDebtTokenAddress).getIncentivesController()) + ); + address[] memory sTokenRewardAddresses = sTokenIncentiveController.getRewardsByAsset( + baseData.stableDebtTokenAddress + ); + RewardInfo[] memory sRewardsInformation; + + if (address(sTokenIncentiveController) != address(0)) { + sRewardsInformation = new RewardInfo[](sTokenRewardAddresses.length); + for (uint256 j = 0; j < sTokenRewardAddresses.length; ++j) { + RewardInfo memory rewardInformation; + rewardInformation.rewardTokenAddress = sTokenRewardAddresses[j]; + + ( + rewardInformation.tokenIncentivesIndex, + rewardInformation.emissionPerSecond, + rewardInformation.incentivesLastUpdateTimestamp, + rewardInformation.emissionEndTimestamp + ) = sTokenIncentiveController.getRewardsData( + baseData.stableDebtTokenAddress, + rewardInformation.rewardTokenAddress + ); + + rewardInformation.precision = sTokenIncentiveController.getAssetDecimals(baseData.stableDebtTokenAddress); + rewardInformation.rewardTokenDecimals = IERC20Detailed( + rewardInformation.rewardTokenAddress + ).decimals(); + rewardInformation.rewardTokenSymbol = IERC20Detailed(rewardInformation.rewardTokenAddress) + .symbol(); + + // Get price of reward token from Chainlink Proxy Oracle + rewardInformation.rewardOracleAddress = sTokenIncentiveController.getRewardOracle( + rewardInformation.rewardTokenAddress + ); + rewardInformation.priceFeedDecimals = IEACAggregatorProxy( + rewardInformation.rewardOracleAddress + ).decimals(); + rewardInformation.rewardPriceFeed = IEACAggregatorProxy( + rewardInformation.rewardOracleAddress + ).latestAnswer(); + + sRewardsInformation[j] = rewardInformation; } - } catch ( - bytes memory /*lowLevelData*/ - ) { - // Will not get here } + + reserveIncentiveData.sIncentiveData = IncentiveData( + baseData.stableDebtTokenAddress, + address(sTokenIncentiveController), + sRewardsInformation + ); } + return (reservesIncentiveData); } @@ -228,79 +241,165 @@ contract UiIncentiveDataProviderV3 is IUiIncentiveDataProvider { // user reserve data userReservesIncentivesData[i].underlyingAsset = reserves[i]; - IUiIncentiveDataProvider.UserIncentiveData memory aUserIncentiveData; + IAaveIncentivesControllerV2 aTokenIncentiveController = IAaveIncentivesControllerV2( + address(IncentivizedERC20(baseData.aTokenAddress).getIncentivesController()) + ); + if (address(aTokenIncentiveController) != address(0)) { + // get all rewards information from the asset + address[] memory aTokenRewardAddresses = aTokenIncentiveController.getRewardsByAsset( + baseData.aTokenAddress + ); + UserRewardInfo[] memory aUserRewardsInformation = new UserRewardInfo[]( + aTokenRewardAddresses.length + ); + for (uint256 j = 0; j < aTokenRewardAddresses.length; ++j) { + UserRewardInfo memory userRewardInformation; + userRewardInformation.rewardTokenAddress = aTokenRewardAddresses[j]; + + userRewardInformation.tokenIncentivesUserIndex = aTokenIncentiveController + .getUserAssetData( + user, + baseData.aTokenAddress, + userRewardInformation.rewardTokenAddress + ); - try IncentivizedERC20(baseData.aTokenAddress).getIncentivesController() returns ( - IAaveIncentivesController aTokenIncentiveController - ) { - if (address(aTokenIncentiveController) != address(0)) { - address aRewardToken = aTokenIncentiveController.REWARD_TOKEN(); - aUserIncentiveData.tokenincentivesUserIndex = aTokenIncentiveController.getUserAssetData( - user, - baseData.aTokenAddress + userRewardInformation.userUnclaimedRewards = aTokenIncentiveController + .getUserUnclaimedRewardsFromStorage(user, userRewardInformation.rewardTokenAddress); + userRewardInformation.rewardTokenDecimals = IERC20Detailed( + userRewardInformation.rewardTokenAddress + ).decimals(); + userRewardInformation.rewardTokenSymbol = IERC20Detailed( + userRewardInformation.rewardTokenAddress + ).symbol(); + + // Get price of reward token from Chainlink Proxy Oracle + userRewardInformation.rewardOracleAddress = aTokenIncentiveController.getRewardOracle( + userRewardInformation.rewardTokenAddress ); - aUserIncentiveData.userUnclaimedRewards = aTokenIncentiveController - .getUserUnclaimedRewards(user); - aUserIncentiveData.tokenAddress = baseData.aTokenAddress; - aUserIncentiveData.rewardTokenAddress = aRewardToken; - aUserIncentiveData.incentiveControllerAddress = address(aTokenIncentiveController); - aUserIncentiveData.rewardTokenDecimals = IERC20Detailed(aRewardToken).decimals(); + userRewardInformation.priceFeedDecimals = IEACAggregatorProxy( + userRewardInformation.rewardOracleAddress + ).decimals(); + userRewardInformation.rewardPriceFeed = IEACAggregatorProxy( + userRewardInformation.rewardOracleAddress + ).latestAnswer(); + + aUserRewardsInformation[j] = userRewardInformation; } - } catch ( - bytes memory /*lowLevelData*/ - ) {} - - userReservesIncentivesData[i].aTokenIncentivesUserData = aUserIncentiveData; - - UserIncentiveData memory vUserIncentiveData; - - try IncentivizedERC20(baseData.variableDebtTokenAddress).getIncentivesController() returns ( - IAaveIncentivesController vTokenIncentiveController - ) { - if (address(vTokenIncentiveController) != address(0)) { - address vRewardToken = vTokenIncentiveController.REWARD_TOKEN(); - vUserIncentiveData.tokenincentivesUserIndex = vTokenIncentiveController.getUserAssetData( - user, - baseData.variableDebtTokenAddress + + userReservesIncentivesData[i].aTokenIncentivesUserData = UserIncentiveData( + baseData.aTokenAddress, + address(aTokenIncentiveController), + aUserRewardsInformation + ); + } + + // variable debt token + IAaveIncentivesControllerV2 vTokenIncentiveController = IAaveIncentivesControllerV2( + address(IncentivizedERC20(baseData.variableDebtTokenAddress).getIncentivesController()) + ); + if (address(vTokenIncentiveController) != address(0)) { + // get all rewards information from the asset + address[] memory vTokenRewardAddresses = vTokenIncentiveController.getRewardsByAsset( + baseData.variableDebtTokenAddress + ); + UserRewardInfo[] memory vUserRewardsInformation = new UserRewardInfo[]( + vTokenRewardAddresses.length + ); + for (uint256 j = 0; j < vTokenRewardAddresses.length; ++j) { + UserRewardInfo memory userRewardInformation; + userRewardInformation.rewardTokenAddress = vTokenRewardAddresses[j]; + + userRewardInformation.tokenIncentivesUserIndex = vTokenIncentiveController + .getUserAssetData( + user, + baseData.variableDebtTokenAddress, + userRewardInformation.rewardTokenAddress + ); + + userRewardInformation.userUnclaimedRewards = vTokenIncentiveController + .getUserUnclaimedRewardsFromStorage(user, userRewardInformation.rewardTokenAddress); + userRewardInformation.rewardTokenDecimals = IERC20Detailed( + userRewardInformation.rewardTokenAddress + ).decimals(); + userRewardInformation.rewardTokenSymbol = IERC20Detailed( + userRewardInformation.rewardTokenAddress + ).symbol(); + + // Get price of reward token from Chainlink Proxy Oracle + userRewardInformation.rewardOracleAddress = vTokenIncentiveController.getRewardOracle( + userRewardInformation.rewardTokenAddress ); - vUserIncentiveData.userUnclaimedRewards = vTokenIncentiveController - .getUserUnclaimedRewards(user); - vUserIncentiveData.tokenAddress = baseData.variableDebtTokenAddress; - vUserIncentiveData.rewardTokenAddress = vRewardToken; - vUserIncentiveData.incentiveControllerAddress = address(vTokenIncentiveController); - vUserIncentiveData.rewardTokenDecimals = IERC20Detailed(vRewardToken).decimals(); + userRewardInformation.priceFeedDecimals = IEACAggregatorProxy( + userRewardInformation.rewardOracleAddress + ).decimals(); + userRewardInformation.rewardPriceFeed = IEACAggregatorProxy( + userRewardInformation.rewardOracleAddress + ).latestAnswer(); + + vUserRewardsInformation[j] = userRewardInformation; } - } catch ( - bytes memory /*lowLevelData*/ - ) {} - - userReservesIncentivesData[i].vTokenIncentivesUserData = vUserIncentiveData; - - UserIncentiveData memory sUserIncentiveData; - - try IncentivizedERC20(baseData.stableDebtTokenAddress).getIncentivesController() returns ( - IAaveIncentivesController sTokenIncentiveController - ) { - if (address(sTokenIncentiveController) != address(0)) { - address sRewardToken = sTokenIncentiveController.REWARD_TOKEN(); - sUserIncentiveData.tokenincentivesUserIndex = sTokenIncentiveController.getUserAssetData( - user, - baseData.stableDebtTokenAddress + + userReservesIncentivesData[i].vTokenIncentivesUserData = UserIncentiveData( + baseData.variableDebtTokenAddress, + address(aTokenIncentiveController), + vUserRewardsInformation + ); + } + + // stable debt toekn + IAaveIncentivesControllerV2 sTokenIncentiveController = IAaveIncentivesControllerV2( + address(IncentivizedERC20(baseData.stableDebtTokenAddress).getIncentivesController()) + ); + if (address(sTokenIncentiveController) != address(0)) { + // get all rewards information from the asset + address[] memory sTokenRewardAddresses = sTokenIncentiveController.getRewardsByAsset( + baseData.stableDebtTokenAddress + ); + UserRewardInfo[] memory sUserRewardsInformation = new UserRewardInfo[]( + sTokenRewardAddresses.length + ); + for (uint256 j = 0; j < sTokenRewardAddresses.length; ++j) { + UserRewardInfo memory userRewardInformation; + userRewardInformation.rewardTokenAddress = sTokenRewardAddresses[j]; + + userRewardInformation.tokenIncentivesUserIndex = sTokenIncentiveController + .getUserAssetData( + user, + baseData.stableDebtTokenAddress, + userRewardInformation.rewardTokenAddress + ); + + userRewardInformation.userUnclaimedRewards = sTokenIncentiveController + .getUserUnclaimedRewardsFromStorage(user, userRewardInformation.rewardTokenAddress); + userRewardInformation.rewardTokenDecimals = IERC20Detailed( + userRewardInformation.rewardTokenAddress + ).decimals(); + userRewardInformation.rewardTokenSymbol = IERC20Detailed( + userRewardInformation.rewardTokenAddress + ).symbol(); + + // Get price of reward token from Chainlink Proxy Oracle + userRewardInformation.rewardOracleAddress = sTokenIncentiveController.getRewardOracle( + userRewardInformation.rewardTokenAddress ); - sUserIncentiveData.userUnclaimedRewards = sTokenIncentiveController - .getUserUnclaimedRewards(user); - sUserIncentiveData.tokenAddress = baseData.stableDebtTokenAddress; - sUserIncentiveData.rewardTokenAddress = sRewardToken; - sUserIncentiveData.incentiveControllerAddress = address(sTokenIncentiveController); - sUserIncentiveData.rewardTokenDecimals = IERC20Detailed(sRewardToken).decimals(); + userRewardInformation.priceFeedDecimals = IEACAggregatorProxy( + userRewardInformation.rewardOracleAddress + ).decimals(); + userRewardInformation.rewardPriceFeed = IEACAggregatorProxy( + userRewardInformation.rewardOracleAddress + ).latestAnswer(); + + sUserRewardsInformation[j] = userRewardInformation; } - } catch ( - bytes memory /*lowLevelData*/ - ) {} - userReservesIncentivesData[i].sTokenIncentivesUserData = sUserIncentiveData; + userReservesIncentivesData[i].sTokenIncentivesUserData = UserIncentiveData( + baseData.stableDebtTokenAddress, + address(aTokenIncentiveController), + sUserRewardsInformation + ); + } } return (userReservesIncentivesData); } -} +} \ No newline at end of file diff --git a/contracts/misc/UiPoolDataProvider.sol b/contracts/misc/UiPoolDataProviderV3.sol similarity index 98% rename from contracts/misc/UiPoolDataProvider.sol rename to contracts/misc/UiPoolDataProviderV3.sol index 59ae64c0..e247739a 100644 --- a/contracts/misc/UiPoolDataProvider.sol +++ b/contracts/misc/UiPoolDataProviderV3.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.10; import {IERC20Detailed} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20Detailed.sol'; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; -import {IUiPoolDataProvider} from './interfaces/IUiPoolDataProvider.sol'; +import {IUiPoolDataProviderV3} from './interfaces/IUiPoolDataProviderV3.sol'; import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; import {IPriceOracleGetter} from '@aave/core-v3/contracts/interfaces/IPriceOracleGetter.sol'; import {IAToken} from '@aave/core-v3/contracts/interfaces/IAToken.sol'; @@ -20,7 +20,7 @@ import {IEACAggregatorProxy} from './interfaces/IEACAggregatorProxy.sol'; import {IERC20DetailedBytes} from './interfaces/IERC20DetailedBytes.sol'; import {AaveProtocolDataProvider} from '@aave/core-v3/contracts/misc/AaveProtocolDataProvider.sol'; -contract UiPoolDataProvider is IUiPoolDataProvider { +contract UiPoolDataProviderV3 is IUiPoolDataProviderV3 { using WadRayMath for uint256; using ReserveConfiguration for DataTypes.ReserveConfigurationMap; using UserConfiguration for DataTypes.UserConfigurationMap; diff --git a/contracts/misc/interfaces/IUiIncentiveDataProvider.sol b/contracts/misc/interfaces/IUiIncentiveDataProviderV3.sol similarity index 77% rename from contracts/misc/interfaces/IUiIncentiveDataProvider.sol rename to contracts/misc/interfaces/IUiIncentiveDataProviderV3.sol index c0685a17..ec3832f3 100644 --- a/contracts/misc/interfaces/IUiIncentiveDataProvider.sol +++ b/contracts/misc/interfaces/IUiIncentiveDataProviderV3.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.10; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; -interface IUiIncentiveDataProvider { +interface IUiIncentiveDataProviderV3 { struct AggregatedReserveIncentiveData { address underlyingAsset; IncentiveData aIncentiveData; @@ -12,15 +12,23 @@ interface IUiIncentiveDataProvider { } struct IncentiveData { + address tokenAddress; + address incentiveControllerAddress; + RewardInfo[] rewardsTokenInformation; + } + + struct RewardInfo { + string rewardTokenSymbol; + address rewardTokenAddress; + address rewardOracleAddress; uint256 emissionPerSecond; uint256 incentivesLastUpdateTimestamp; uint256 tokenIncentivesIndex; uint256 emissionEndTimestamp; - address tokenAddress; - address rewardTokenAddress; - address incentiveControllerAddress; + int256 rewardPriceFeed; uint8 rewardTokenDecimals; uint8 precision; + uint8 priceFeedDecimals; } struct UserReserveIncentiveData { @@ -29,14 +37,23 @@ interface IUiIncentiveDataProvider { UserIncentiveData vTokenIncentivesUserData; UserIncentiveData sTokenIncentivesUserData; } - + struct UserIncentiveData { - uint256 tokenincentivesUserIndex; - uint256 userUnclaimedRewards; address tokenAddress; - address rewardTokenAddress; address incentiveControllerAddress; + UserRewardInfo[] userRewardsInformation; + } + + struct UserRewardInfo { + string rewardTokenSymbol; + address rewardOracleAddress; + address rewardTokenAddress; + uint256 userUnclaimedRewards; + uint256 tokenIncentivesUserIndex; + int256 rewardPriceFeed; + uint8 priceFeedDecimals; uint8 rewardTokenDecimals; + } function getReservesIncentivesData(IPoolAddressesProvider provider) @@ -54,4 +71,4 @@ interface IUiIncentiveDataProvider { external view returns (AggregatedReserveIncentiveData[] memory, UserReserveIncentiveData[] memory); -} +} \ No newline at end of file diff --git a/contracts/misc/interfaces/IUiPoolDataProvider.sol b/contracts/misc/interfaces/IUiPoolDataProviderV3.sol similarity index 98% rename from contracts/misc/interfaces/IUiPoolDataProvider.sol rename to contracts/misc/interfaces/IUiPoolDataProviderV3.sol index 0859d967..c123bbfd 100644 --- a/contracts/misc/interfaces/IUiPoolDataProvider.sol +++ b/contracts/misc/interfaces/IUiPoolDataProviderV3.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.10; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; import {DataTypes} from '@aave/core-v3/contracts/protocol/libraries/types/DataTypes.sol'; -interface IUiPoolDataProvider { +interface IUiPoolDataProviderV3 { struct AggregatedReserveData { address underlyingAsset; string name; diff --git a/contracts/mocks/ATokenMock.sol b/contracts/mocks/ATokenMock.sol index 67e28c8a..a2286505 100644 --- a/contracts/mocks/ATokenMock.sol +++ b/contracts/mocks/ATokenMock.sol @@ -7,6 +7,7 @@ contract ATokenMock { IAaveIncentivesControllerV2 public _aic; uint256 internal _userBalance; uint256 internal _totalSupply; + uint256 internal immutable _decimals; // hack to be able to test event from EI properly event RewardsAccrued(address indexed user, uint256 amount); @@ -16,8 +17,9 @@ contract ATokenMock { event AssetIndexUpdated(address indexed asset, uint256 index); event UserIndexUpdated(address indexed user, address indexed asset, uint256 index); - constructor(IAaveIncentivesControllerV2 aic) { + constructor(IAaveIncentivesControllerV2 aic, uint256 decimals) { _aic = aic; + _decimals = decimals; } function handleActionOnAic( @@ -54,4 +56,8 @@ contract ATokenMock { _userBalance = 0; _totalSupply = 0; } + + function decimals() external view returns (uint256) { + return _decimals; + } } diff --git a/package.json b/package.json index d22c8ae5..bf8f9ee5 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "hardhat:matic": "hardhat --network matic", "compile": "SKIP_LOAD=true hardhat compile", "console:fork": "MAINNET_FORK=true hardhat console", - "test": ". ./setup-test-env.sh && TS_NODE_TRANSPILE_ONLY=1 hardhat test test/__setup.spec.ts ./test/weth-gateway.spec.ts ./test/incentives-v2/**/*.spec.ts", - "test-incentives": ". ./setup-test-env.sh && TS_NODE_TRANSPILE_ONLY=1 hardhat test test/__setup.spec.ts ./test/incentives-v2/**/.spec.ts", + "test": ". ./setup-test-env.sh && TS_NODE_TRANSPILE_ONLY=1 hardhat test test/__setup.spec.ts ./test/weth-gateway.spec.ts ./test/incentives-v2/*.spec.ts ./test/incentives-v2/**/*.spec.ts", + "test-incentives": ". ./setup-test-env.sh && TS_NODE_TRANSPILE_ONLY=1 hardhat test test/__setup.spec.ts ./test/incentives-v2/*.spec.ts ./test/incentives-v2/**/*.spec.ts", "test-strategies": ". ./setup-test-env.sh && TS_NODE_TRANSPILE_ONLY=1 hardhat test test/__setup.spec.ts ./test/incentives-v2/strategies/*.spec.ts", "coverage": ". ./setup-test-env.sh && TS_NODE_TRANSPILE_ONLY=1 COVERAGE=true EMPTY_RUN=true npx hardhat coverage --temp temp-artifacts --testfiles test/emptyrun.coverage.ts && rm -rf coverage.json coverage/ && TS_NODE_TRANSPILE_ONLY=1 COVERAGE=true npx hardhat coverage --temp temp-artifacts --testfiles 'test/**/*.spec.ts'", "clean": "rm -rf artifacts cache", diff --git a/test/helpers/make-suite.ts b/test/helpers/make-suite.ts index 9f95b872..2067fc22 100644 --- a/test/helpers/make-suite.ts +++ b/test/helpers/make-suite.ts @@ -49,6 +49,7 @@ import { waitForTx, MAX_UINT_AMOUNT, TESTNET_PRICE_AGGR_PREFIX, + deployMintableERC20, } from '@aave/deploy-v3'; import { deployATokenMock } from '../incentives-v2/helpers/deploy'; import { parseEther } from 'ethers/lib/utils'; @@ -90,6 +91,7 @@ export interface TestEnv { aDaiMockV2: ATokenMock; aWethMockV2: ATokenMock; aAaveMockV2: ATokenMock; + aEursMockV2: ATokenMock; pullRewardsStrategy: PullRewardsTransferStrategy; stakedTokenStrategy: StakedTokenTransferStrategy; rewardToken: MintableERC20; @@ -134,6 +136,7 @@ const testEnv: TestEnv = { aDaiMockV2: {} as ATokenMock, aWethMockV2: {} as ATokenMock, aAaveMockV2: {} as ATokenMock, + aEursMockV2: {} as ATokenMock, pullRewardsStrategy: {} as PullRewardsTransferStrategy, stakedTokenStrategy: {} as StakedTokenTransferStrategy, rewardToken: {} as MintableERC20, @@ -215,7 +218,20 @@ export async function initializeMakeSuite() { testEnv.weth = await getWETHMocked(wethAddress); testEnv.wethGateway = await getWETHGateway(); - // incentives-v2 setup + // Added extra reward token + await hre.deployments.deploy(`EXTRA${TESTNET_REWARD_TOKEN_PREFIX}`, { + from: await _deployer.getAddress(), + contract: 'MintableERC20', + args: ['EXTRA', 'EXTRA', 18], + log: true, + }); + await hre.deployments.deploy(`EXTRA${TESTNET_PRICE_AGGR_PREFIX}`, { + args: [parseEther('2')], + from: await _deployer.getAddress(), + log: true, + contract: 'MockAggregator', + }); + // Setup Incentives V2 environment const rewardTokens = await getSubTokensByPrefix(TESTNET_REWARD_TOKEN_PREFIX); const incentivesControllerV2 = ((await getIncentivesV2()) as any) as IncentivesControllerV2; testEnv.incentivesControllerV2 = incentivesControllerV2; @@ -225,6 +241,7 @@ export async function initializeMakeSuite() { testEnv.aDaiMockV2 = await deployATokenMock(incentivesControllerV2.address, 'aDaiV2'); testEnv.aWethMockV2 = await deployATokenMock(incentivesControllerV2.address, 'aWethV2'); testEnv.aAaveMockV2 = await deployATokenMock(incentivesControllerV2.address, 'aAaveV2'); + testEnv.aEursMockV2 = await deployATokenMock(incentivesControllerV2.address, 'aEursV2', 2); testEnv.pullRewardsStrategy = (await getPullRewardsStrategy()) as PullRewardsTransferStrategy; testEnv.stakedTokenStrategy = ((await getStakedRewardsStrategy()) as any) as StakedTokenTransferStrategy; testEnv.rewardToken = await getMintableERC20(rewardTokens[0].artifact.address); @@ -246,18 +263,18 @@ export async function initializeMakeSuite() { await waitForTx( await testEnv.aaveToken .connect(rewardsVault.signer) - ['mint(address,uint256)'](rewardsVault.address, parseEther('3000000')) + ['mint(address,uint256)'](rewardsVault.address, parseEther('60000000000')) ); await waitForTx( await testEnv.rewardToken .connect(rewardsVault.signer) - ['mint(address,uint256)'](rewardsVault.address, parseEther('2000000')) + ['mint(address,uint256)'](rewardsVault.address, parseEther('200000000')) ); await waitForTx( await testEnv.aaveToken .connect(rewardsVault.signer) - .transfer(testEnv.stakedTokenStrategy.address, parseEther('1000000')) + .transfer(testEnv.stakedTokenStrategy.address, parseEther('30000000000')) ); } diff --git a/test/incentives-v2/claim-all-rewards-to-self.spec.ts b/test/incentives-v2/claim-all-rewards-to-self.spec.ts index 0ab4933a..c7bbcb05 100644 --- a/test/incentives-v2/claim-all-rewards-to-self.spec.ts +++ b/test/incentives-v2/claim-all-rewards-to-self.spec.ts @@ -4,9 +4,9 @@ import { BigNumber } from 'ethers'; import { waitForTx, getBlockTimestamp, - increaseTime, MAX_UINT_AMOUNT, ERC20__factory, + advanceTimeAndBlock, } from '@aave/deploy-v3'; import { assetDataComparator, @@ -17,6 +17,7 @@ import { getUserIndex } from './helpers/DistributionManagerV2/data-helpers/asset import { parseEther } from '@ethersproject/units'; import Bluebird from 'bluebird'; import { ATokenMock__factory } from '../../types'; +import hre from 'hardhat'; type ScenarioAction = { caseName: string; @@ -73,10 +74,10 @@ const getRewardsBalanceScenarios: ScenarioAction[] = [ }, ]; -makeSuite('Incentives Controller V2 claimRewards tests', (testEnv) => { +makeSuite('Incentives Controller V2 claimRewards to self tests', (testEnv) => { before(async () => { const { rewardTokens, rewardsVault, pullRewardsStrategy } = testEnv; - const rewards = rewardTokens.slice(0, 4); + const rewards = rewardTokens.slice(0, 3); await Bluebird.each(rewards, async (reward, index) => { await reward.connect(rewardsVault.signer)['mint(uint256)'](parseEther('1000000000')); @@ -87,14 +88,16 @@ makeSuite('Incentives Controller V2 claimRewards tests', (testEnv) => { }); for (const { caseName, emissionsPerSecond, zeroBalance } of getRewardsBalanceScenarios) { it(caseName, async () => { - await increaseTime(100); + const { timestamp } = await hre.ethers.provider.getBlock('latest'); + const timePerTest = 31536000; + const distributionEnd = timestamp + timePerTest * getRewardsBalanceScenarios.length; + await advanceTimeAndBlock(timePerTest); const { incentivesControllerV2, aDaiMockV2, aAaveMockV2, aWethMockV2, pullRewardsStrategy, - distributionEnd, rewardTokens, deployer, } = testEnv; @@ -110,7 +113,7 @@ makeSuite('Incentives Controller V2 claimRewards tests', (testEnv) => { const totalSupply = assets.map((_, index) => BigNumber.from(parseEther('100000')).mul(caseName.length).mul(index) ); - const rewards = rewardTokens.slice(0, 4).map(({ address }) => address); + const rewards = rewardTokens.slice(0, 3).map(({ address }) => address); await Bluebird.each(assets, async (asset, index) => { await ATokenMock__factory.connect(asset, deployer.signer).setUserBalanceAndSupply( diff --git a/test/incentives-v2/claim-all-rewards.spec.ts b/test/incentives-v2/claim-all-rewards.spec.ts index 016e3914..37b10968 100644 --- a/test/incentives-v2/claim-all-rewards.spec.ts +++ b/test/incentives-v2/claim-all-rewards.spec.ts @@ -7,6 +7,7 @@ import { increaseTime, MAX_UINT_AMOUNT, ERC20__factory, + advanceTimeAndBlock, } from '@aave/deploy-v3'; import { RANDOM_ADDRESSES } from '../helpers/constants'; import { @@ -15,9 +16,10 @@ import { getRewardsData, } from './helpers/DistributionManagerV2/data-helpers/asset-data'; import { getUserIndex } from './helpers/DistributionManagerV2/data-helpers/asset-user-data'; -import { parseEther } from '@ethersproject/units'; +import { parseEther, parseUnits } from '@ethersproject/units'; import Bluebird from 'bluebird'; import { ATokenMock__factory } from '../../types'; +import hre from 'hardhat'; type ScenarioAction = { caseName: string; @@ -30,55 +32,55 @@ type ScenarioAction = { const getRewardsBalanceScenarios: ScenarioAction[] = [ { caseName: 'Accrued rewards are 0', - emissionsPerSecond: ['0', '0', '0'], - zeroBalance: [false, false, false], + emissionsPerSecond: ['0', '0', '0', '0'], + zeroBalance: [false, false, false, false], }, { caseName: 'Accrued rewards are not 0', - emissionsPerSecond: ['2432424', '4432424', '1234'], - zeroBalance: [false, false, false], + emissionsPerSecond: ['2432424', '4432424', '1234', '124231210000'], + zeroBalance: [false, false, false, false], }, { caseName: 'Some rewards are not 0', - emissionsPerSecond: ['2432424', '0', '1234'], - zeroBalance: [false, false, false], + emissionsPerSecond: ['2432424', '0', '1234', '1242312100000'], + zeroBalance: [false, false, false, false], }, { caseName: 'Some rewards are not 0', - emissionsPerSecond: ['0', '2432424', '1234'], - zeroBalance: [false, false, false], + emissionsPerSecond: ['0', '2432424', '1234', '1242312100000'], + zeroBalance: [false, false, false, false], }, { caseName: 'Some user balances are not 0', - emissionsPerSecond: ['13412', '2432424', '0'], - zeroBalance: [false, true, true], + emissionsPerSecond: ['13412', '2432424', '0', '12423121000000'], + zeroBalance: [false, true, true, true], }, { caseName: 'Some user balances are not 0', - emissionsPerSecond: ['13412', '2432424', '0'], - zeroBalance: [true, false, true], + emissionsPerSecond: ['13412', '2432424', '0', '12423121000000'], + zeroBalance: [true, false, true, true], }, { caseName: 'Should withdraw to another user', - emissionsPerSecond: ['2314', '3331', '421512'], + emissionsPerSecond: ['2314', '3331', '421512', '42152'], to: RANDOM_ADDRESSES[5], - zeroBalance: [false, false, false], + zeroBalance: [false, false, false, false], }, { caseName: 'Should withdraw to another user', - emissionsPerSecond: ['2314', '3331', '421512'], + emissionsPerSecond: ['2314', '3331', '421512', '2123'], to: RANDOM_ADDRESSES[3], - zeroBalance: [false, false, false], + zeroBalance: [false, false, false, false], }, { caseName: 'Should not claim due emissions are zero', - emissionsPerSecond: ['0', '0', '0'], + emissionsPerSecond: ['0', '0', '0', '0'], to: RANDOM_ADDRESSES[3], - zeroBalance: [false, false, false], + zeroBalance: [false, false, false, false], }, ]; -makeSuite('Incentives Controller V2 claimRewards tests', (testEnv) => { +makeSuite('Incentives Controller V2 claimAllRewards tests', (testEnv) => { before(async () => { const { rewardTokens, rewardsVault, pullRewardsStrategy } = testEnv; const rewards = rewardTokens.slice(0, 4); @@ -90,30 +92,43 @@ makeSuite('Incentives Controller V2 claimRewards tests', (testEnv) => { .approve(pullRewardsStrategy.address, MAX_UINT_AMOUNT); }); }); - for (const { caseName, to, emissionsPerSecond, zeroBalance } of getRewardsBalanceScenarios) { + for (const [ + caseIndex, + { caseName, to, emissionsPerSecond, zeroBalance }, + ] of getRewardsBalanceScenarios.entries()) { it(caseName, async () => { - await increaseTime(100); + const { timestamp } = await hre.ethers.provider.getBlock('latest'); + const timePerTest = 31536000; + const distributionEnd = timestamp + timePerTest * getRewardsBalanceScenarios.length; + await advanceTimeAndBlock(timePerTest); const { incentivesControllerV2, aDaiMockV2, aAaveMockV2, aWethMockV2, + aEursMockV2, pullRewardsStrategy, - distributionEnd, rewardTokens, deployer, } = testEnv; const userAddress = await incentivesControllerV2.signer.getAddress(); - const assets = [aDaiMockV2, aAaveMockV2, aWethMockV2].map(({ address }) => address); + const assets = [aDaiMockV2, aAaveMockV2, aWethMockV2, aEursMockV2].map( + ({ address }) => address + ); + const stakedByUser = assets.map((_, index) => zeroBalance[index] ? BigNumber.from('0') - : BigNumber.from(parseEther('20000')).mul(caseName.length).mul(index) + : BigNumber.from(parseUnits('10000', index > 2 ? 2 : 18)) + .mul(index) + .mul(caseIndex) ); const totalSupply = assets.map((_, index) => - BigNumber.from(parseEther('100000')).mul(caseName.length).mul(index) + BigNumber.from(parseUnits('10000', index > 2 ? 2 : 18)) + .mul(index) + .mul(caseIndex) ); const rewards = rewardTokens.slice(0, 4).map(({ address }) => address); @@ -125,7 +140,6 @@ makeSuite('Incentives Controller V2 claimRewards tests', (testEnv) => { }); // update emissionPerSecond in advance to not affect user calculations - await waitForTx( await incentivesControllerV2.configureAssets( emissionsPerSecond.map((emissionPerSecond, index) => ({ @@ -203,14 +217,12 @@ makeSuite('Incentives Controller V2 claimRewards tests', (testEnv) => { const claimedAmounts = await Bluebird.map(destinationAddressBalanceAfter, (balance, index) => balance.sub(destinationAddressBalanceBefore[index]) ); - - await aDaiMockV2.cleanUserState(); - const expectedAccruedRewards = await Bluebird.map(rewards, (_, index) => getRewards( stakedByUser[index], userIndexesAfter[index], - userIndexesBefore[index] + userIndexesBefore[index], + index > 2 ? 2 : 18 ).toString() ); @@ -221,7 +233,8 @@ makeSuite('Incentives Controller V2 claimRewards tests', (testEnv) => { assetDataAfter[i], actionBlockTimestamp, distributionEnd, - {} + {}, + i > 2 ? 2 : 18 ); expect(userIndexesAfter[i].toString()).to.be.equal( diff --git a/test/incentives-v2/claim-rewards-low-decimals.spec.ts b/test/incentives-v2/claim-rewards-low-decimals.spec.ts new file mode 100644 index 00000000..9676125c --- /dev/null +++ b/test/incentives-v2/claim-rewards-low-decimals.spec.ts @@ -0,0 +1,267 @@ +const { expect } = require('chai'); +import { makeSuite } from '../helpers/make-suite'; +import { BigNumber } from 'ethers'; +import { + waitForTx, + getBlockTimestamp, + increaseTime, + MAX_UINT_AMOUNT, + advanceTimeAndBlock, +} from '@aave/deploy-v3'; +import { RANDOM_ADDRESSES } from '../helpers/constants'; +import { comparatorEngine } from './helpers/comparator-engine'; +import { + assetDataComparator, + getRewards, + getRewardsData, +} from './helpers/DistributionManagerV2/data-helpers/asset-data'; +import { getUserIndex } from './helpers/DistributionManagerV2/data-helpers/asset-user-data'; +import hre from 'hardhat'; + +type ScenarioAction = { + caseName: string; + emissionPerSecond?: string; + amountToClaim: string; + to?: string; + toStake?: boolean; +}; + +const getRewardsBalanceScenarios: ScenarioAction[] = [ + { + caseName: 'Accrued rewards are 0, claim 0', + emissionPerSecond: '0', + amountToClaim: '0', + }, + { + caseName: 'Accrued rewards are 0, claim not 0', + emissionPerSecond: '0', + amountToClaim: '100', + }, + { + caseName: 'Accrued rewards are not 0', + emissionPerSecond: '317097919837645865', + amountToClaim: '10', + }, + { + caseName: 'Should allow -1', + emissionPerSecond: '317097919837645865', + amountToClaim: MAX_UINT_AMOUNT, + }, + { + caseName: 'Should withdraw everything if amountToClaim more then rewards balance', + emissionPerSecond: '317097919837645865', + amountToClaim: '1034', + }, + { + caseName: 'Should withdraw to another user', + emissionPerSecond: '317097919837645865', + amountToClaim: '1034', + to: RANDOM_ADDRESSES[5], + }, + { + caseName: 'Should withdraw to another user and stake', + emissionPerSecond: '317097919837645865', + amountToClaim: '1034', + to: RANDOM_ADDRESSES[5], + }, +]; + +makeSuite('Incentives Controller V2 claimRewards with 2 decimals', (testEnv) => { + for (const { + caseName, + amountToClaim: _amountToClaim, + to, + emissionPerSecond, + } of getRewardsBalanceScenarios) { + let amountToClaim = _amountToClaim; + it(caseName, async () => { + const { timestamp } = await hre.ethers.provider.getBlock('latest'); + const timePerTest = 31536000; + const distributionEnd = timestamp + timePerTest * getRewardsBalanceScenarios.length; + await advanceTimeAndBlock(timePerTest); + const { incentivesControllerV2, stakedAave, aEursMockV2, stakedTokenStrategy } = testEnv; + + const userAddress = await incentivesControllerV2.signer.getAddress(); + + const underlyingAsset = aEursMockV2.address; + const stakedByUser = 22 * caseName.length; + const totalSupply = 33 * caseName.length; + const reward = stakedAave.address; + + await aEursMockV2.setUserBalanceAndSupply(stakedByUser, totalSupply); + + // update emissionPerSecond in advance to not affect user calculations + if (emissionPerSecond) { + await waitForTx( + await incentivesControllerV2.configureAssets([ + { + asset: underlyingAsset, + reward, + rewardOracle: testEnv.aavePriceAggregator, + emissionPerSecond, + distributionEnd, + totalSupply, + transferStrategy: stakedTokenStrategy.address, + }, + ]) + ); + } + + const destinationAddress = to || userAddress; + + const destinationAddressBalanceBefore = await stakedAave.balanceOf(destinationAddress); + await aEursMockV2.handleActionOnAic(userAddress, totalSupply, stakedByUser); + + const unclaimedRewardsBefore = await incentivesControllerV2.getUserRewardsBalance( + [underlyingAsset], + userAddress, + reward + ); + const unclaimedRewardsStorageBefore = await incentivesControllerV2.getUserUnclaimedRewardsFromStorage( + userAddress, + reward + ); + + const userIndexBefore = await getUserIndex( + incentivesControllerV2, + userAddress, + underlyingAsset, + reward + ); + const assetDataBefore = ( + await getRewardsData(incentivesControllerV2, [underlyingAsset], [reward]) + )[0]; + + const action = await incentivesControllerV2.claimRewards( + [underlyingAsset], + amountToClaim, + destinationAddress, + reward + ); + const claimRewardsReceipt = await waitForTx(action); + const eventsEmitted = claimRewardsReceipt.events || []; + + const actionBlockTimestamp = await getBlockTimestamp(claimRewardsReceipt.blockNumber); + + const userIndexAfter = await getUserIndex( + incentivesControllerV2, + userAddress, + underlyingAsset, + reward + ); + const assetDataAfter = ( + await getRewardsData(incentivesControllerV2, [underlyingAsset], [reward]) + )[0]; + + const unclaimedRewardsAfter = await incentivesControllerV2.getUserRewardsBalance( + [underlyingAsset], + userAddress, + reward + ); + const unclaimedRewardsStorageAfter = await incentivesControllerV2.getUserUnclaimedRewardsFromStorage( + userAddress, + reward + ); + + const destinationAddressBalanceAfter = await stakedAave.balanceOf(destinationAddress); + + const claimedAmount = destinationAddressBalanceAfter.sub(destinationAddressBalanceBefore); + + // Only calculate expected accrued rewards if unclaimedRewards is below the amount to claim due gas optimization + const expectedAccruedRewards = unclaimedRewardsStorageBefore.lt(amountToClaim) + ? getRewards(stakedByUser, userIndexAfter, userIndexBefore, 2).toString() + : '0'; + + await aEursMockV2.cleanUserState(); + + if (amountToClaim === '0') { + // state should not change + expect(userIndexBefore.toString()).to.be.equal( + userIndexAfter.toString(), + 'userIndexAfter should not change' + ); + expect(unclaimedRewardsBefore.toString()).to.be.equal( + unclaimedRewardsAfter.toString(), + 'unclaimedRewards should not change' + ); + expect(destinationAddressBalanceBefore.toString()).to.be.equal( + destinationAddressBalanceAfter.toString(), + 'destinationAddressBalance should not change' + ); + await comparatorEngine( + ['emissionPerSecond', 'index', 'lastUpdateTimestamp'], + { underlyingAsset, totalSupply }, + assetDataBefore, + assetDataAfter, + actionBlockTimestamp, + {} + ); + expect(eventsEmitted.length).to.be.equal(0, 'no events should be emitted'); + return; + } + + // ------- Distribution Manager tests START ----- + await assetDataComparator( + { underlyingAsset, totalSupply }, + assetDataBefore, + assetDataAfter, + unclaimedRewardsStorageBefore.gte(amountToClaim) + ? Number(assetDataBefore.lastUpdateTimestamp.toString()) + : actionBlockTimestamp, + distributionEnd, + {}, + 2 + ); + expect(userIndexAfter.toString()).to.be.equal( + unclaimedRewardsStorageBefore.gte(amountToClaim) + ? userIndexBefore.toString() + : assetDataAfter.index.toString(), + 'user index are not correctly updated' + ); + if (!assetDataAfter.index.eq(assetDataBefore.index)) { + await expect(action) + .to.emit(incentivesControllerV2, 'AssetIndexUpdated') + .withArgs(assetDataAfter.underlyingAsset, reward, assetDataAfter.index); + await expect(action) + .to.emit(incentivesControllerV2, 'UserIndexUpdated') + .withArgs(userAddress, assetDataAfter.underlyingAsset, reward, assetDataAfter.index); + } + // ------- Distribution Manager tests END ----- + + let unclaimedRewardsCalc = unclaimedRewardsStorageBefore.add(expectedAccruedRewards); + + let expectedClaimedAmount: BigNumber; + if (unclaimedRewardsCalc.lte(amountToClaim)) { + expectedClaimedAmount = unclaimedRewardsCalc; + expect(unclaimedRewardsStorageAfter.toString()).to.be.equal( + '0', + 'unclaimed amount after should go to 0' + ); + } else { + expectedClaimedAmount = BigNumber.from(amountToClaim); + expect(unclaimedRewardsStorageAfter.toString()).to.be.equal( + unclaimedRewardsCalc.sub(amountToClaim).toString(), + 'unclaimed rewards after are wrong' + ); + } + + expect(claimedAmount.toString()).to.be.equal( + expectedClaimedAmount.toString(), + 'claimed amount are wrong' + ); + if (expectedAccruedRewards !== '0') { + await expect(action) + .to.emit(incentivesControllerV2, 'RewardsAccrued') + .withArgs(userAddress, reward, expectedAccruedRewards); + await expect(action) + .to.emit(incentivesControllerV2, 'UserIndexUpdated') + .withArgs(userAddress, assetDataAfter.underlyingAsset, reward, assetDataAfter.index); + } + if (expectedClaimedAmount.gt(0)) { + await expect(action) + .to.emit(incentivesControllerV2, 'RewardsClaimed') + .withArgs(userAddress, reward, destinationAddress, userAddress, expectedClaimedAmount); + } + }); + } +}); diff --git a/test/incentives-v2/claim-rewards-to-self.spec.ts b/test/incentives-v2/claim-rewards-to-self.spec.ts index 7ba05feb..4cb7a909 100644 --- a/test/incentives-v2/claim-rewards-to-self.spec.ts +++ b/test/incentives-v2/claim-rewards-to-self.spec.ts @@ -1,4 +1,10 @@ -import { getBlockTimestamp, increaseTime, waitForTx, MAX_UINT_AMOUNT } from '@aave/deploy-v3'; +import { + getBlockTimestamp, + increaseTime, + waitForTx, + MAX_UINT_AMOUNT, + advanceTimeAndBlock, +} from '@aave/deploy-v3'; import { BigNumber } from 'ethers'; import { makeSuite } from '../helpers/make-suite'; import { comparatorEngine } from './helpers/comparator-engine'; @@ -8,6 +14,7 @@ import { getRewardsData, } from './helpers/DistributionManagerV2/data-helpers/asset-data'; import { getUserIndex } from './helpers/DistributionManagerV2/data-helpers/asset-user-data'; +import hre from 'hardhat'; const { expect } = require('chai'); @@ -53,14 +60,11 @@ makeSuite('AaveIncentivesController claimRewardsToSelf tests', (testEnv) => { } of getRewardsBalanceScenarios) { let amountToClaim = _amountToClaim; it(caseName, async () => { - await increaseTime(100); - const { - incentivesControllerV2, - stakedAave, - aDaiMockV2, - distributionEnd, - stakedTokenStrategy, - } = testEnv; + const { timestamp } = await hre.ethers.provider.getBlock('latest'); + const timePerTest = 31536000; + const distributionEnd = timestamp + timePerTest * getRewardsBalanceScenarios.length; + await advanceTimeAndBlock(timePerTest); + const { incentivesControllerV2, stakedAave, aDaiMockV2, stakedTokenStrategy } = testEnv; const userAddress = await incentivesControllerV2.signer.getAddress(); diff --git a/test/incentives-v2/claim-rewards.spec.ts b/test/incentives-v2/claim-rewards.spec.ts index cba37fac..de6e7dda 100644 --- a/test/incentives-v2/claim-rewards.spec.ts +++ b/test/incentives-v2/claim-rewards.spec.ts @@ -1,7 +1,13 @@ const { expect } = require('chai'); import { makeSuite } from '../helpers/make-suite'; import { BigNumber } from 'ethers'; -import { waitForTx, getBlockTimestamp, increaseTime, MAX_UINT_AMOUNT } from '@aave/deploy-v3'; +import { + waitForTx, + getBlockTimestamp, + increaseTime, + MAX_UINT_AMOUNT, + advanceTimeAndBlock, +} from '@aave/deploy-v3'; import { RANDOM_ADDRESSES } from '../helpers/constants'; import { comparatorEngine } from './helpers/comparator-engine'; import { @@ -10,6 +16,7 @@ import { getRewardsData, } from './helpers/DistributionManagerV2/data-helpers/asset-data'; import { getUserIndex } from './helpers/DistributionManagerV2/data-helpers/asset-user-data'; +import hre from 'hardhat'; type ScenarioAction = { caseName: string; @@ -68,14 +75,11 @@ makeSuite('Incentives Controller V2 claimRewards tests', (testEnv) => { } of getRewardsBalanceScenarios) { let amountToClaim = _amountToClaim; it(caseName, async () => { - await increaseTime(100); - const { - incentivesControllerV2, - stakedAave, - aDaiMockV2, - stakedTokenStrategy, - distributionEnd, - } = testEnv; + const { timestamp } = await hre.ethers.provider.getBlock('latest'); + const timePerTest = 31536000; + const distributionEnd = timestamp + timePerTest * getRewardsBalanceScenarios.length; + await advanceTimeAndBlock(timePerTest); + const { incentivesControllerV2, stakedAave, aDaiMockV2, stakedTokenStrategy } = testEnv; const userAddress = await incentivesControllerV2.signer.getAddress(); diff --git a/test/incentives-v2/helpers/DistributionManagerV2/data-helpers/asset-data.ts b/test/incentives-v2/helpers/DistributionManagerV2/data-helpers/asset-data.ts index 0fe4db67..e46e028e 100644 --- a/test/incentives-v2/helpers/DistributionManagerV2/data-helpers/asset-data.ts +++ b/test/incentives-v2/helpers/DistributionManagerV2/data-helpers/asset-data.ts @@ -109,7 +109,8 @@ export function assetDataComparator< assetConfigAfter: State, actionBlockTimestamp: number, emissionEndTimestamp: number, - compareRules: CompareRules + compareRules: CompareRules, + decimals = 18 ) { return comparatorEngine( ['emissionPerSecond', 'index', 'lastUpdateTimestamp'], @@ -134,7 +135,8 @@ export function assetDataComparator< stateBefore.emissionPerSecond, stateBefore.lastUpdateTimestamp, txTimestamp, - emissionEndTimestamp + emissionEndTimestamp, + decimals ).toString(10); }, }, diff --git a/test/incentives-v2/helpers/deploy.ts b/test/incentives-v2/helpers/deploy.ts index ac933cd2..9e557ebf 100644 --- a/test/incentives-v2/helpers/deploy.ts +++ b/test/incentives-v2/helpers/deploy.ts @@ -5,13 +5,14 @@ declare var hre: HardhatRuntimeEnvironment; export const deployATokenMock = async ( incentivesController: string, - slug: string + slug: string, + decimals = 18 ): Promise => { const { deployer } = await hre.getNamedAccounts(); const artifact = await hre.deployments.deploy(`${slug}-ATokenMock`, { contract: 'ATokenMock', from: deployer, - args: [incentivesController], + args: [incentivesController, decimals], log: true, }); return hre.ethers.getContractAt(artifact.abi, artifact.address);