├── .editorconfig ├── .giatattributes ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .gitlab-ci.yml ├── .prettierignore ├── .prettierrc ├── .solcover.js ├── Dockerfile ├── Dockerfile_test ├── LICENSE.md ├── contracts ├── gov │ └── Executor.sol ├── incentives │ ├── PullRewardsIncentivesController.sol │ ├── StakedTokenIncentivesController.sol │ └── base │ │ ├── BaseIncentivesController.sol │ │ └── DistributionManager.sol ├── interfaces │ ├── IATokenDetailed.sol │ ├── IAaveDistributionManager.sol │ ├── IAaveEcosystemReserveController.sol │ ├── IAaveGovernanceV2.sol │ ├── IAaveIncentivesController.sol │ ├── IGovernancePowerDelegationToken.sol │ ├── ILendingPool.sol │ ├── ILendingPoolAddressesProvider.sol │ ├── ILendingPoolAddressesProviderRegistry.sol │ ├── ILendingPoolConfigurator.sol │ ├── ILendingPoolData.sol │ ├── IProposalIncentivesExecutor.sol │ ├── IScaledBalanceToken.sol │ └── IStakedTokenWithConfig.sol ├── lending-pool │ ├── AToken.sol │ ├── AaveProtocolDataProvider.sol │ └── VariableDebtToken.sol ├── lib │ ├── DistributionTypes.sol │ └── SafeMath.sol ├── mocks │ ├── ATokenMock.sol │ └── StakeMock.sol ├── proposals │ └── ProposalIncentivesExecutor.sol ├── stake │ └── StakedAaveV3.sol └── utils │ ├── Context.sol │ ├── DataTypes.sol │ ├── InitializableAdminUpgradeabilityProxy.sol.sol │ ├── MintableErc20.sol │ ├── PercentageMath.sol │ ├── SelfdestructTransfer.sol │ └── VersionedInitializable.sol ├── docker-compose.test.yml ├── docker-compose.yml ├── hardhat.config.ts ├── helper-hardhat-config.ts ├── helpers ├── constants.ts ├── contracts-accessors.ts ├── contracts-helpers.ts ├── defender-utils.ts ├── etherscan-verification.ts ├── misc-utils.ts ├── tenderly-utils.ts └── types.ts ├── package-lock.json ├── package.json ├── readme.md ├── tasks ├── deployment │ ├── deploy-atoken.ts │ ├── deploy-incentives-impl.ts │ ├── deploy-reserve-implementations.ts │ ├── deploy-variable-debt-token.ts │ └── propose-incentives.ts ├── migrations │ ├── deploy-pull-rewards-incentives.ts │ ├── incentives-deploy-mainnet.ts │ ├── incentives-submit-proposal-mainnet.ts │ ├── incentives-submit-proposal-tenderly.ts │ ├── tenderly-execute-proposal-fork.ts │ └── tenderly-proposal-fork.ts └── misc │ ├── set-DRE.ts │ ├── verify-proposal-etherscan.ts │ └── verify-sc.ts ├── test-fork ├── helpers.ts ├── incentives-skip.spec.ts └── incentivesProposal.spec.ts ├── test-wallets.ts ├── test ├── DistributionManager │ └── data-helpers │ │ ├── asset-data.ts │ │ ├── asset-user-data.ts │ │ └── base-math.ts ├── PullRewardsIncentivesController │ ├── claim-on-behalf.spec.ts │ ├── claim-rewards-to-self.spec.ts │ ├── claim-rewards.spec.ts │ ├── configure-assets.spec.ts │ ├── get-rewards-balance.spec.ts │ ├── handle-action.spec.ts │ ├── initialize.spec.ts │ └── misc.spec.ts ├── StakedIncentivesController │ ├── claim-on-behalf.spec.ts │ ├── claim-rewards-to-self.spec.ts │ ├── claim-rewards.spec.ts │ ├── configure-assets.spec.ts │ ├── get-rewards-balance.spec.ts │ ├── handle-action.spec.ts │ ├── initialize.spec.ts │ └── misc.spec.ts ├── __setup.spec.ts └── helpers │ ├── comparator-engine.ts │ ├── deploy.ts │ ├── make-suite.ts │ └── ray-math │ ├── bignumber.ts │ ├── index.ts │ └── ray-math.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /.giatattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [14.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - uses: actions/cache@v2 24 | with: 25 | path: ~/.npm 26 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 27 | restore-keys: | 28 | ${{ runner.os }}-node- 29 | - name: Install dependencies 30 | run: npm ci 31 | - name: Compile 32 | run: npm run compile 33 | - name: Test 34 | run: npm run test 35 | - name: Coverage 36 | run: npm run coverage 37 | - uses: codecov/codecov-action@v1 38 | with: 39 | fail_ci_if_error: true 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | #Buidler files 3 | cache 4 | artifacts 5 | node_modules 6 | dist/ 7 | build/ 8 | .vscode 9 | .idea 10 | /types 11 | 12 | deployed-contracts.json 13 | 14 | coverage 15 | .coverage_artifacts 16 | .coverage_cache 17 | .coverage_contracts 18 | deployments/ 19 | coverage.json 20 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - checks 3 | - prepare 4 | - publish 5 | 6 | variables: 7 | IMAGE: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA} 8 | 9 | lint: 10 | stage: checks 11 | tags: 12 | - aave-build-runner 13 | before_script: 14 | - docker-compose -p ${CI_JOB_ID} -f docker-compose.test.yml build 15 | script: 16 | - docker-compose -p ${CI_JOB_ID} -f docker-compose.test.yml run contracts-env npm run prettier:check 17 | after_script: 18 | - docker-compose -p ${CI_JOB_ID} -f docker-compose.test.yml run contracts-env npm run ci:clean 19 | - docker-compose -p ${CI_JOB_ID} -f docker-compose.test.yml down 20 | 21 | test: 22 | stage: checks 23 | tags: 24 | - aave-build-runner 25 | before_script: 26 | - docker-compose -p ${CI_JOB_ID} -f docker-compose.test.yml build 27 | script: 28 | - docker-compose -p ${CI_JOB_ID} -f docker-compose.test.yml run contracts-env npm run ci:test 29 | after_script: 30 | - docker-compose -p ${CI_JOB_ID} -f docker-compose.test.yml run contracts-env npm run ci:clean 31 | - docker-compose -p ${CI_JOB_ID} -f docker-compose.test.yml down 32 | only: 33 | - master 34 | - merge_requests 35 | deploy-mainnet-fork: 36 | tags: 37 | - aave-build-runner 38 | stage: checks 39 | before_script: 40 | - docker-compose -p ${CI_JOB_ID} -f docker-compose.test.yml build 41 | script: 42 | - docker-compose -p ${CI_JOB_ID} -f docker-compose.test.yml run contracts-env npm run aave:fork:main 43 | after_script: 44 | - docker-compose -p ${CI_JOB_ID} -f docker-compose.test.yml run contracts-env npm run ci:clean 45 | - docker-compose -p ${CI_JOB_ID} -f docker-compose.test.yml down 46 | only: 47 | - master 48 | - merge_requests 49 | 50 | certora-test: 51 | stage: checks 52 | image: python:latest 53 | before_script: 54 | - apt-get update || apt-get update 55 | - apt-get install -y software-properties-common 56 | - pip3 install certora-cli 57 | - wget https://github.com/ethereum/solidity/releases/download/v0.6.12/solc-static-linux 58 | - chmod +x solc-static-linux 59 | - mv solc-static-linux /usr/bin/solc 60 | - export PATH=$PATH:/usr/bin/solc/solc-static-linux 61 | script: 62 | - certoraRun specs/harness/StableDebtTokenHarness.sol:StableDebtTokenHarness --solc_args "['--optimize']" --verify StableDebtTokenHarness:specs/StableDebtToken.spec --settings -assumeUnwindCond,-b=4 --cache StableDebtToken --cloud 63 | - certoraRun specs/harness/UserConfigurationHarness.sol --verify UserConfigurationHarness:specs/UserConfiguration.spec --solc_args "['--optimize']" --settings -useBitVectorTheory --cache UserConfiguration --cloud 64 | - certoraRun contracts/protocol/tokenization/VariableDebtToken.sol:VariableDebtToken specs/harness/LendingPoolHarnessForVariableDebtToken.sol --solc_args "['--optimize']" --link VariableDebtToken:POOL=LendingPoolHarnessForVariableDebtToken --verify VariableDebtToken:specs/VariableDebtToken.spec --settings -assumeUnwindCond,-useNonLinearArithmetic,-b=4 --cache VariableDebtToken --cloud 65 | only: 66 | - master 67 | - merge_requests 68 | 69 | prepare: 70 | stage: prepare 71 | tags: 72 | - docker-builder 73 | script: 74 | - docker build -t ${IMAGE} . 75 | - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY 76 | - docker push ${IMAGE} 77 | only: 78 | - master 79 | 80 | publish: 81 | image: ${IMAGE} 82 | tags: 83 | - docker 84 | stage: publish 85 | script: 86 | - npm ci 87 | - echo //registry.npmjs.org/:_authToken=${NPM_V2_PACKAGES_TOKEN} > .npmrc 88 | - npm run compile 89 | - ${VERSION} 90 | - npm publish --access public 91 | only: 92 | - master 93 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | artifacts 2 | cache 3 | node_modules 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "trailingComma": "es5", 4 | "semi": true, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "overrides": [ 8 | { 9 | "files": "*.sol", 10 | "options": { 11 | "semi": true, 12 | "printWidth": 100 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: ['./mocks', './interfaces'], 3 | }; 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ethereum/solc:0.6.12 as build-deps 2 | 3 | FROM node:14 4 | COPY --from=build-deps /usr/bin/solc /usr/bin/solc 5 | -------------------------------------------------------------------------------- /Dockerfile_test: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | WORKDIR /app 4 | ADD ./package-lock.json ./package.json /app/ 5 | RUN npm ci 6 | 7 | ADD ./ /app/ 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020 Aave 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU Affero General Public License as 5 | published by the Free Software Foundation, either version 3 of the 6 | License, or any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) 12 | for more details 13 | -------------------------------------------------------------------------------- /contracts/gov/Executor.sol: -------------------------------------------------------------------------------- 1 | import '@aave/governance-v2/contracts/governance/Executor.sol'; 2 | -------------------------------------------------------------------------------- /contracts/incentives/PullRewardsIncentivesController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import {IERC20} from '@aave/aave-stake/contracts/interfaces/IERC20.sol'; 6 | import {SafeERC20} from '@aave/aave-stake/contracts/lib/SafeERC20.sol'; 7 | 8 | import {BaseIncentivesController} from './base/BaseIncentivesController.sol'; 9 | 10 | /** 11 | * @title PullRewardsIncentivesController 12 | * @notice Distributor contract for ERC20 rewards to the Aave protocol participants that pulls ERC20 from external account 13 | * @author Aave 14 | **/ 15 | contract PullRewardsIncentivesController is 16 | BaseIncentivesController 17 | { 18 | using SafeERC20 for IERC20; 19 | 20 | address internal _rewardsVault; 21 | 22 | event RewardsVaultUpdated(address indexed vault); 23 | 24 | constructor(IERC20 rewardToken, address emissionManager) 25 | BaseIncentivesController(rewardToken, emissionManager) 26 | {} 27 | 28 | /** 29 | * @dev Initialize AaveIncentivesController 30 | * @param rewardsVault rewards vault to pull ERC20 funds 31 | **/ 32 | function initialize(address rewardsVault) external initializer { 33 | _rewardsVault = rewardsVault; 34 | emit RewardsVaultUpdated(_rewardsVault); 35 | } 36 | 37 | /** 38 | * @dev returns the current rewards vault contract 39 | * @return address 40 | */ 41 | function getRewardsVault() external view returns (address) { 42 | return _rewardsVault; 43 | } 44 | 45 | /** 46 | * @dev update the rewards vault address, only allowed by the Rewards admin 47 | * @param rewardsVault The address of the rewards vault 48 | **/ 49 | function setRewardsVault(address rewardsVault) external onlyEmissionManager { 50 | _rewardsVault = rewardsVault; 51 | emit RewardsVaultUpdated(rewardsVault); 52 | } 53 | 54 | 55 | /// @inheritdoc BaseIncentivesController 56 | function _transferRewards(address to, uint256 amount) internal override { 57 | IERC20(REWARD_TOKEN).safeTransferFrom(_rewardsVault, to, amount); 58 | } 59 | } -------------------------------------------------------------------------------- /contracts/incentives/StakedTokenIncentivesController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import {SafeERC20} from '@aave/aave-stake/contracts/lib/SafeERC20.sol'; 6 | import {IERC20} from '@aave/aave-stake/contracts/interfaces/IERC20.sol'; 7 | import {BaseIncentivesController} from './base/BaseIncentivesController.sol'; 8 | import {IStakedTokenWithConfig} from '../interfaces/IStakedTokenWithConfig.sol'; 9 | 10 | /** 11 | * @title StakedTokenIncentivesController 12 | * @notice Distributor contract for rewards to the Aave protocol, using a staked token as rewards asset. 13 | * The contract stakes the rewards before redistributing them to the Aave protocol participants. 14 | * The reference staked token implementation is at https://github.com/aave/aave-stake-v2 15 | * @author Aave 16 | **/ 17 | contract StakedTokenIncentivesController is BaseIncentivesController { 18 | using SafeERC20 for IERC20; 19 | 20 | IStakedTokenWithConfig public immutable STAKE_TOKEN; 21 | 22 | constructor(IStakedTokenWithConfig stakeToken, address emissionManager) 23 | BaseIncentivesController(IERC20(address(stakeToken)), emissionManager) 24 | { 25 | STAKE_TOKEN = stakeToken; 26 | } 27 | 28 | /** 29 | * @dev Initialize IStakedTokenIncentivesController 30 | **/ 31 | function initialize() external initializer { 32 | //approves the safety module to allow staking 33 | IERC20(STAKE_TOKEN.STAKED_TOKEN()).safeApprove(address(STAKE_TOKEN), type(uint256).max); 34 | } 35 | 36 | /// @inheritdoc BaseIncentivesController 37 | function _transferRewards(address to, uint256 amount) internal override { 38 | STAKE_TOKEN.stake(to, amount); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/incentives/base/BaseIncentivesController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import {SafeMath} from '../../lib/SafeMath.sol'; 6 | import {DistributionTypes} from '../../lib/DistributionTypes.sol'; 7 | import {VersionedInitializable} from '@aave/aave-stake/contracts/utils/VersionedInitializable.sol'; 8 | import {DistributionManager} from './DistributionManager.sol'; 9 | import {IERC20} from '@aave/aave-stake/contracts/interfaces/IERC20.sol'; 10 | import {IScaledBalanceToken} from '../../interfaces/IScaledBalanceToken.sol'; 11 | import {IAaveIncentivesController} from '../../interfaces/IAaveIncentivesController.sol'; 12 | 13 | /** 14 | * @title BaseIncentivesController 15 | * @notice Abstract contract template to build Distributors contracts for ERC20 rewards to protocol participants 16 | * @author Aave 17 | **/ 18 | abstract contract BaseIncentivesController is 19 | IAaveIncentivesController, 20 | VersionedInitializable, 21 | DistributionManager 22 | { 23 | using SafeMath for uint256; 24 | 25 | uint256 public constant REVISION = 1; 26 | 27 | address public immutable override REWARD_TOKEN; 28 | 29 | mapping(address => uint256) internal _usersUnclaimedRewards; 30 | 31 | // this mapping allows whitelisted addresses to claim on behalf of others 32 | // useful for contracts that hold tokens to be rewarded but don't have any native logic to claim Liquidity Mining rewards 33 | mapping(address => address) internal _authorizedClaimers; 34 | 35 | modifier onlyAuthorizedClaimers(address claimer, address user) { 36 | require(_authorizedClaimers[user] == claimer, 'CLAIMER_UNAUTHORIZED'); 37 | _; 38 | } 39 | 40 | constructor(IERC20 rewardToken, address emissionManager) 41 | DistributionManager(emissionManager) 42 | { 43 | REWARD_TOKEN = address(rewardToken); 44 | } 45 | 46 | /// @inheritdoc IAaveIncentivesController 47 | function configureAssets(address[] calldata assets, uint256[] calldata emissionsPerSecond) 48 | external 49 | override 50 | onlyEmissionManager 51 | { 52 | require(assets.length == emissionsPerSecond.length, 'INVALID_CONFIGURATION'); 53 | 54 | DistributionTypes.AssetConfigInput[] memory assetsConfig = 55 | new DistributionTypes.AssetConfigInput[](assets.length); 56 | 57 | for (uint256 i = 0; i < assets.length; i++) { 58 | require(uint104(emissionsPerSecond[i]) == emissionsPerSecond[i], 'Index overflow at emissionsPerSecond'); 59 | assetsConfig[i].underlyingAsset = assets[i]; 60 | assetsConfig[i].emissionPerSecond = uint104(emissionsPerSecond[i]); 61 | assetsConfig[i].totalStaked = IScaledBalanceToken(assets[i]).scaledTotalSupply(); 62 | } 63 | _configureAssets(assetsConfig); 64 | } 65 | 66 | /// @inheritdoc IAaveIncentivesController 67 | function handleAction( 68 | address user, 69 | uint256 totalSupply, 70 | uint256 userBalance 71 | ) external override { 72 | uint256 accruedRewards = _updateUserAssetInternal(user, msg.sender, userBalance, totalSupply); 73 | if (accruedRewards != 0) { 74 | _usersUnclaimedRewards[user] = _usersUnclaimedRewards[user].add(accruedRewards); 75 | emit RewardsAccrued(user, accruedRewards); 76 | } 77 | } 78 | 79 | /// @inheritdoc IAaveIncentivesController 80 | function getRewardsBalance(address[] calldata assets, address user) 81 | external 82 | view 83 | override 84 | returns (uint256) 85 | { 86 | uint256 unclaimedRewards = _usersUnclaimedRewards[user]; 87 | 88 | DistributionTypes.UserStakeInput[] memory userState = 89 | new DistributionTypes.UserStakeInput[](assets.length); 90 | for (uint256 i = 0; i < assets.length; i++) { 91 | userState[i].underlyingAsset = assets[i]; 92 | (userState[i].stakedByUser, userState[i].totalStaked) = IScaledBalanceToken(assets[i]) 93 | .getScaledUserBalanceAndSupply(user); 94 | } 95 | unclaimedRewards = unclaimedRewards.add(_getUnclaimedRewards(user, userState)); 96 | return unclaimedRewards; 97 | } 98 | 99 | /// @inheritdoc IAaveIncentivesController 100 | function claimRewards( 101 | address[] calldata assets, 102 | uint256 amount, 103 | address to 104 | ) external override returns (uint256) { 105 | require(to != address(0), 'INVALID_TO_ADDRESS'); 106 | return _claimRewards(assets, amount, msg.sender, msg.sender, to); 107 | } 108 | 109 | /// @inheritdoc IAaveIncentivesController 110 | function claimRewardsOnBehalf( 111 | address[] calldata assets, 112 | uint256 amount, 113 | address user, 114 | address to 115 | ) external override onlyAuthorizedClaimers(msg.sender, user) returns (uint256) { 116 | require(user != address(0), 'INVALID_USER_ADDRESS'); 117 | require(to != address(0), 'INVALID_TO_ADDRESS'); 118 | return _claimRewards(assets, amount, msg.sender, user, to); 119 | } 120 | 121 | /// @inheritdoc IAaveIncentivesController 122 | function claimRewardsToSelf(address[] calldata assets, uint256 amount) 123 | external 124 | override 125 | returns (uint256) 126 | { 127 | return _claimRewards(assets, amount, msg.sender, msg.sender, msg.sender); 128 | } 129 | 130 | /// @inheritdoc IAaveIncentivesController 131 | function setClaimer(address user, address caller) external override onlyEmissionManager { 132 | _authorizedClaimers[user] = caller; 133 | emit ClaimerSet(user, caller); 134 | } 135 | 136 | /// @inheritdoc IAaveIncentivesController 137 | function getClaimer(address user) external view override returns (address) { 138 | return _authorizedClaimers[user]; 139 | } 140 | 141 | /// @inheritdoc IAaveIncentivesController 142 | function getUserUnclaimedRewards(address _user) external view override returns (uint256) { 143 | return _usersUnclaimedRewards[_user]; 144 | } 145 | 146 | /** 147 | * @dev returns the revision of the implementation contract 148 | */ 149 | function getRevision() internal pure override returns (uint256) { 150 | return REVISION; 151 | } 152 | 153 | /** 154 | * @dev Claims reward for an user on behalf, on all the assets of the lending pool, accumulating the pending rewards. 155 | * @param amount Amount of rewards to claim 156 | * @param user Address to check and claim rewards 157 | * @param to Address that will be receiving the rewards 158 | * @return Rewards claimed 159 | **/ 160 | function _claimRewards( 161 | address[] calldata assets, 162 | uint256 amount, 163 | address claimer, 164 | address user, 165 | address to 166 | ) internal returns (uint256) { 167 | if (amount == 0) { 168 | return 0; 169 | } 170 | uint256 unclaimedRewards = _usersUnclaimedRewards[user]; 171 | 172 | if (amount > unclaimedRewards) { 173 | DistributionTypes.UserStakeInput[] memory userState = 174 | new DistributionTypes.UserStakeInput[](assets.length); 175 | for (uint256 i = 0; i < assets.length; i++) { 176 | userState[i].underlyingAsset = assets[i]; 177 | (userState[i].stakedByUser, userState[i].totalStaked) = IScaledBalanceToken(assets[i]) 178 | .getScaledUserBalanceAndSupply(user); 179 | } 180 | 181 | uint256 accruedRewards = _claimRewards(user, userState); 182 | if (accruedRewards != 0) { 183 | unclaimedRewards = unclaimedRewards.add(accruedRewards); 184 | emit RewardsAccrued(user, accruedRewards); 185 | } 186 | } 187 | 188 | if (unclaimedRewards == 0) { 189 | return 0; 190 | } 191 | 192 | uint256 amountToClaim = amount > unclaimedRewards ? unclaimedRewards : amount; 193 | _usersUnclaimedRewards[user] = unclaimedRewards - amountToClaim; // Safe due to the previous line 194 | 195 | _transferRewards(to, amountToClaim); 196 | emit RewardsClaimed(user, to, claimer, amountToClaim); 197 | 198 | return amountToClaim; 199 | } 200 | 201 | /** 202 | * @dev Abstract function to transfer rewards to the desired account 203 | * @param to Account address to send the rewards 204 | * @param amount Amount of rewards to transfer 205 | */ 206 | function _transferRewards(address to, uint256 amount) internal virtual; 207 | } 208 | -------------------------------------------------------------------------------- /contracts/interfaces/IATokenDetailed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | 4 | interface IATokenDetailed { 5 | function UNDERLYING_ASSET_ADDRESS() external view returns (address); 6 | } 7 | -------------------------------------------------------------------------------- /contracts/interfaces/IAaveDistributionManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import {DistributionTypes} from '../lib/DistributionTypes.sol'; 6 | 7 | interface IAaveDistributionManager { 8 | 9 | event AssetConfigUpdated(address indexed asset, uint256 emission); 10 | event AssetIndexUpdated(address indexed asset, uint256 index); 11 | event UserIndexUpdated(address indexed user, address indexed asset, uint256 index); 12 | event DistributionEndUpdated(uint256 newDistributionEnd); 13 | 14 | /** 15 | * @dev Sets the end date for the distribution 16 | * @param distributionEnd The end date timestamp 17 | **/ 18 | function setDistributionEnd(uint256 distributionEnd) external; 19 | 20 | /** 21 | * @dev Gets the end date for the distribution 22 | * @return The end of the distribution 23 | **/ 24 | function getDistributionEnd() external view returns (uint256); 25 | 26 | /** 27 | * @dev for backwards compatibility with the previous DistributionManager used 28 | * @return The end of the distribution 29 | **/ 30 | function DISTRIBUTION_END() external view returns(uint256); 31 | 32 | /** 33 | * @dev Returns the data of an user on a distribution 34 | * @param user Address of the user 35 | * @param asset The address of the reference asset of the distribution 36 | * @return The new index 37 | **/ 38 | function getUserAssetData(address user, address asset) external view returns (uint256); 39 | 40 | /** 41 | * @dev Returns the configuration of the distribution for a certain asset 42 | * @param asset The address of the reference asset of the distribution 43 | * @return The asset index, the emission per second and the last updated timestamp 44 | **/ 45 | function getAssetData(address asset) external view returns (uint256, uint256, uint256); 46 | } 47 | -------------------------------------------------------------------------------- /contracts/interfaces/IAaveEcosystemReserveController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | 4 | interface IAaveEcosystemReserveController { 5 | function AAVE_RESERVE_ECOSYSTEM() external view returns (address); 6 | 7 | function approve( 8 | address token, 9 | address recipient, 10 | uint256 amount 11 | ) external; 12 | 13 | function owner() external view returns (address); 14 | 15 | function renounceOwnership() external; 16 | 17 | function transfer( 18 | address token, 19 | address recipient, 20 | uint256 amount 21 | ) external; 22 | 23 | function transferOwnership(address newOwner) external; 24 | } 25 | -------------------------------------------------------------------------------- /contracts/interfaces/IAaveGovernanceV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | 4 | import '@aave/governance-v2/contracts/interfaces/IAaveGovernanceV2.sol'; 5 | -------------------------------------------------------------------------------- /contracts/interfaces/IAaveIncentivesController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | 4 | pragma experimental ABIEncoderV2; 5 | 6 | import {IAaveDistributionManager} from '../interfaces/IAaveDistributionManager.sol'; 7 | 8 | interface IAaveIncentivesController is IAaveDistributionManager { 9 | event RewardsAccrued(address indexed user, uint256 amount); 10 | 11 | event RewardsClaimed( 12 | address indexed user, 13 | address indexed to, 14 | address indexed claimer, 15 | uint256 amount 16 | ); 17 | 18 | event ClaimerSet(address indexed user, address indexed claimer); 19 | 20 | /** 21 | * @dev Whitelists an address to claim the rewards on behalf of another address 22 | * @param user The address of the user 23 | * @param claimer The address of the claimer 24 | */ 25 | function setClaimer(address user, address claimer) external; 26 | 27 | /** 28 | * @dev Returns the whitelisted claimer for a certain address (0x0 if not set) 29 | * @param user The address of the user 30 | * @return The claimer address 31 | */ 32 | function getClaimer(address user) external view returns (address); 33 | 34 | /** 35 | * @dev Configure assets for a certain rewards emission 36 | * @param assets The assets to incentivize 37 | * @param emissionsPerSecond The emission for each asset 38 | */ 39 | function configureAssets(address[] calldata assets, uint256[] calldata emissionsPerSecond) 40 | external; 41 | 42 | /** 43 | * @dev Called by the corresponding asset on any update that affects the rewards distribution 44 | * @param asset The address of the user 45 | * @param userBalance The balance of the user of the asset in the lending pool 46 | * @param totalSupply The total supply of the asset in the lending pool 47 | **/ 48 | function handleAction( 49 | address asset, 50 | uint256 userBalance, 51 | uint256 totalSupply 52 | ) external; 53 | 54 | /** 55 | * @dev Returns the total of rewards of an user, already accrued + not yet accrued 56 | * @param user The address of the user 57 | * @return The rewards 58 | **/ 59 | function getRewardsBalance(address[] calldata assets, address user) 60 | external 61 | view 62 | returns (uint256); 63 | 64 | /** 65 | * @dev Claims reward for an user to the desired address, on all the assets of the lending pool, accumulating the pending rewards 66 | * @param amount Amount of rewards to claim 67 | * @param to Address that will be receiving the rewards 68 | * @return Rewards claimed 69 | **/ 70 | function claimRewards( 71 | address[] calldata assets, 72 | uint256 amount, 73 | address to 74 | ) external returns (uint256); 75 | 76 | /** 77 | * @dev Claims reward for an user on behalf, on all the assets of the lending pool, accumulating the pending rewards. The caller must 78 | * be whitelisted via "allowClaimOnBehalf" function by the RewardsAdmin role manager 79 | * @param amount Amount of rewards to claim 80 | * @param user Address to check and claim rewards 81 | * @param to Address that will be receiving the rewards 82 | * @return Rewards claimed 83 | **/ 84 | function claimRewardsOnBehalf( 85 | address[] calldata assets, 86 | uint256 amount, 87 | address user, 88 | address to 89 | ) external returns (uint256); 90 | 91 | /** 92 | * @dev Claims reward for msg.sender, on all the assets of the lending pool, accumulating the pending rewards 93 | * @param amount Amount of rewards to claim 94 | * @return Rewards claimed 95 | **/ 96 | function claimRewardsToSelf(address[] calldata assets, uint256 amount) external returns (uint256); 97 | 98 | /** 99 | * @dev returns the unclaimed rewards of the user 100 | * @param user the address of the user 101 | * @return the unclaimed user rewards 102 | */ 103 | function getUserUnclaimedRewards(address user) external view returns (uint256); 104 | 105 | /** 106 | * @dev for backward compatibility with previous implementation of the Incentives controller 107 | */ 108 | function REWARD_TOKEN() external view returns (address); 109 | } 110 | -------------------------------------------------------------------------------- /contracts/interfaces/IGovernancePowerDelegationToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | 4 | interface IGovernancePowerDelegationToken { 5 | enum DelegationType {VOTING_POWER, PROPOSITION_POWER} 6 | 7 | /** 8 | * @dev emitted when a user delegates to another 9 | * @param delegator the delegator 10 | * @param delegatee the delegatee 11 | * @param delegationType the type of delegation (VOTING_POWER, PROPOSITION_POWER) 12 | **/ 13 | event DelegateChanged( 14 | address indexed delegator, 15 | address indexed delegatee, 16 | DelegationType delegationType 17 | ); 18 | 19 | /** 20 | * @dev emitted when an action changes the delegated power of a user 21 | * @param user the user which delegated power has changed 22 | * @param amount the amount of delegated power for the user 23 | * @param delegationType the type of delegation (VOTING_POWER, PROPOSITION_POWER) 24 | **/ 25 | event DelegatedPowerChanged(address indexed user, uint256 amount, DelegationType delegationType); 26 | 27 | /** 28 | * @dev delegates the specific power to a delegatee 29 | * @param delegatee the user which delegated power has changed 30 | * @param delegationType the type of delegation (VOTING_POWER, PROPOSITION_POWER) 31 | **/ 32 | function delegateByType(address delegatee, DelegationType delegationType) external virtual; 33 | 34 | /** 35 | * @dev delegates all the powers to a specific user 36 | * @param delegatee the user to which the power will be delegated 37 | **/ 38 | function delegate(address delegatee) external virtual; 39 | 40 | /** 41 | * @dev returns the delegatee of an user 42 | * @param delegator the address of the delegator 43 | **/ 44 | function getDelegateeByType(address delegator, DelegationType delegationType) 45 | external 46 | view 47 | virtual 48 | returns (address); 49 | 50 | /** 51 | * @dev returns the current delegated power of a user. The current power is the 52 | * power delegated at the time of the last snapshot 53 | * @param user the user 54 | **/ 55 | function getPowerCurrent(address user, DelegationType delegationType) 56 | external 57 | view 58 | virtual 59 | returns (uint256); 60 | 61 | /** 62 | * @dev returns the delegated power of a user at a certain block 63 | * @param user the user 64 | **/ 65 | function getPowerAtBlock( 66 | address user, 67 | uint256 blockNumber, 68 | DelegationType delegationType 69 | ) external view virtual returns (uint256); 70 | 71 | /** 72 | * @dev returns the total supply at a certain block number 73 | **/ 74 | function totalSupplyAt(uint256 blockNumber) external view virtual returns (uint256); 75 | } 76 | -------------------------------------------------------------------------------- /contracts/interfaces/ILendingPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity ^0.6.12; 3 | 4 | import '@aave/protocol-v2/contracts/interfaces/ILendingPool.sol'; 5 | -------------------------------------------------------------------------------- /contracts/interfaces/ILendingPoolAddressesProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | 4 | /** 5 | * @title LendingPoolAddressesProvider contract 6 | * @dev Main registry of addresses part of or connected to the protocol, including permissioned roles 7 | * - Acting also as factory of proxies and admin of those, so with right to change its implementations 8 | * - Owned by the Aave Governance 9 | * @author Aave 10 | **/ 11 | interface ILendingPoolAddressesProvider { 12 | event MarketIdSet(string newMarketId); 13 | event LendingPoolUpdated(address indexed newAddress); 14 | event ConfigurationAdminUpdated(address indexed newAddress); 15 | event EmergencyAdminUpdated(address indexed newAddress); 16 | event LendingPoolConfiguratorUpdated(address indexed newAddress); 17 | event LendingPoolCollateralManagerUpdated(address indexed newAddress); 18 | event PriceOracleUpdated(address indexed newAddress); 19 | event LendingRateOracleUpdated(address indexed newAddress); 20 | event ProxyCreated(bytes32 id, address indexed newAddress); 21 | event AddressSet(bytes32 id, address indexed newAddress, bool hasProxy); 22 | 23 | function getMarketId() external view returns (string memory); 24 | 25 | function setMarketId(string calldata marketId) external; 26 | 27 | function setAddress(bytes32 id, address newAddress) external; 28 | 29 | function setAddressAsProxy(bytes32 id, address impl) external; 30 | 31 | function getAddress(bytes32 id) external view returns (address); 32 | 33 | function getLendingPool() external view returns (address); 34 | 35 | function setLendingPoolImpl(address pool) external; 36 | 37 | function getLendingPoolConfigurator() external view returns (address); 38 | 39 | function setLendingPoolConfiguratorImpl(address configurator) external; 40 | 41 | function getLendingPoolCollateralManager() external view returns (address); 42 | 43 | function setLendingPoolCollateralManager(address manager) external; 44 | 45 | function getPoolAdmin() external view returns (address); 46 | 47 | function setPoolAdmin(address admin) external; 48 | 49 | function getEmergencyAdmin() external view returns (address); 50 | 51 | function setEmergencyAdmin(address admin) external; 52 | 53 | function getPriceOracle() external view returns (address); 54 | 55 | function setPriceOracle(address priceOracle) external; 56 | 57 | function getLendingRateOracle() external view returns (address); 58 | 59 | function setLendingRateOracle(address lendingRateOracle) external; 60 | } 61 | -------------------------------------------------------------------------------- /contracts/interfaces/ILendingPoolAddressesProviderRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | 4 | /** 5 | * @title LendingPoolAddressesProviderRegistry contract 6 | * @dev Main registry of LendingPoolAddressesProvider of multiple Aave protocol's markets 7 | * - Used for indexing purposes of Aave protocol's markets 8 | * - The id assigned to a LendingPoolAddressesProvider refers to the market it is connected with, 9 | * for example with `0` for the Aave main market and `1` for the next created 10 | * @author Aave 11 | **/ 12 | interface ILendingPoolAddressesProviderRegistry { 13 | event AddressesProviderRegistered(address indexed newAddress); 14 | event AddressesProviderUnregistered(address indexed newAddress); 15 | 16 | function getAddressesProvidersList() external view returns (address[] memory); 17 | 18 | function getAddressesProviderIdByAddress(address addressesProvider) 19 | external 20 | view 21 | returns (uint256); 22 | 23 | function registerAddressesProvider(address provider, uint256 id) external; 24 | 25 | function unregisterAddressesProvider(address provider) external; 26 | } 27 | -------------------------------------------------------------------------------- /contracts/interfaces/ILendingPoolConfigurator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | 4 | interface ILendingPoolConfigurator { 5 | function updateAToken(address reserve, address implementation) external; 6 | 7 | function updateVariableDebtToken(address reserve, address implementation) external; 8 | } 9 | -------------------------------------------------------------------------------- /contracts/interfaces/ILendingPoolData.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | pragma abicoder v2; 4 | 5 | import {DataTypes} from '../utils/DataTypes.sol'; 6 | 7 | interface ILendingPoolData { 8 | /** 9 | * @dev Returns the state and configuration of the reserve 10 | * @param asset The address of the underlying asset of the reserve 11 | * @return The state of the reserve 12 | **/ 13 | function getReserveData(address asset) external view returns (DataTypes.ReserveData memory); 14 | } 15 | -------------------------------------------------------------------------------- /contracts/interfaces/IProposalIncentivesExecutor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity 0.7.5; 3 | 4 | interface IProposalIncentivesExecutor { 5 | function execute( 6 | address[6] memory aTokenImplementations, 7 | address[6] memory variableDebtImplementation 8 | ) external; 9 | } 10 | -------------------------------------------------------------------------------- /contracts/interfaces/IScaledBalanceToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | 4 | interface IScaledBalanceToken { 5 | /** 6 | * @dev Returns the scaled balance of the user. The scaled balance is the sum of all the 7 | * updated stored balance divided by the reserve's liquidity index at the moment of the update 8 | * @param user The user whose balance is calculated 9 | * @return The scaled balance of the user 10 | **/ 11 | function scaledBalanceOf(address user) external view returns (uint256); 12 | 13 | /** 14 | * @dev Returns the scaled balance of the user and the scaled total supply. 15 | * @param user The address of the user 16 | * @return The scaled balance of the user 17 | * @return The scaled balance and the scaled total supply 18 | **/ 19 | function getScaledUserBalanceAndSupply(address user) external view returns (uint256, uint256); 20 | 21 | /** 22 | * @dev Returns the scaled total supply of the token. Represents sum(debt/index) 23 | * @return The scaled total supply 24 | **/ 25 | function scaledTotalSupply() external view returns (uint256); 26 | } 27 | -------------------------------------------------------------------------------- /contracts/interfaces/IStakedTokenWithConfig.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity 0.7.5; 3 | 4 | import {IStakedToken} from '@aave/aave-stake/contracts/interfaces/IStakedToken.sol'; 5 | 6 | interface IStakedTokenWithConfig is IStakedToken { 7 | function STAKED_TOKEN() external view returns(address); 8 | } -------------------------------------------------------------------------------- /contracts/lending-pool/AToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity ^0.6.12; 3 | 4 | import '@aave/protocol-v2/contracts/protocol/tokenization/AToken.sol'; 5 | -------------------------------------------------------------------------------- /contracts/lending-pool/AaveProtocolDataProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity ^0.6.12; 3 | 4 | import '@aave/protocol-v2/contracts/misc/AaveProtocolDataProvider.sol'; 5 | -------------------------------------------------------------------------------- /contracts/lending-pool/VariableDebtToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity ^0.6.12; 3 | 4 | import '@aave/protocol-v2/contracts/protocol/tokenization/VariableDebtToken.sol'; 5 | -------------------------------------------------------------------------------- /contracts/lib/DistributionTypes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | pragma experimental ABIEncoderV2; 4 | 5 | library DistributionTypes { 6 | struct AssetConfigInput { 7 | uint104 emissionPerSecond; 8 | uint256 totalStaked; 9 | address underlyingAsset; 10 | } 11 | 12 | struct UserStakeInput { 13 | address underlyingAsset; 14 | uint256 stakedByUser; 15 | uint256 totalStaked; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contracts/lib/SafeMath.sol: -------------------------------------------------------------------------------- 1 | 2 | // SPDX-License-Identifier: agpl-3.0 3 | pragma solidity ^0.7.5; 4 | 5 | /// @title Optimized overflow and underflow safe math operations 6 | /// @notice Contains methods for doing math operations that revert on overflow or underflow for minimal gas cost 7 | /// inspired by uniswap V3 8 | library SafeMath { 9 | /// @notice Returns x + y, reverts if sum overflows uint256 10 | /// @param x The augend 11 | /// @param y The addend 12 | /// @return z The sum of x and y 13 | function add(uint256 x, uint256 y) internal pure returns (uint256 z) { 14 | require((z = x + y) >= x); 15 | } 16 | 17 | /// @notice Returns x - y, reverts if underflows 18 | /// @param x The minuend 19 | /// @param y The subtrahend 20 | /// @return z The difference of x and y 21 | function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { 22 | require((z = x - y) <= x); 23 | } 24 | 25 | /// @notice Returns x * y, reverts if overflows 26 | /// @param x The multiplicand 27 | /// @param y The multiplier 28 | /// @return z The product of x and y 29 | function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { 30 | require(x == 0 || (z = x * y) / x == y); 31 | } 32 | 33 | function div(uint256 x, uint256 y) internal pure returns(uint256) { 34 | // no need to check for division by zero - solidity already reverts 35 | return x / y; 36 | } 37 | } -------------------------------------------------------------------------------- /contracts/mocks/ATokenMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import {IAaveIncentivesController} from '../interfaces/IAaveIncentivesController.sol'; 6 | import {DistributionTypes} from '../lib/DistributionTypes.sol'; 7 | import {IAToken} from '@aave/aave-stake/contracts/interfaces/IAToken.sol'; 8 | 9 | contract ATokenMock is IAToken { 10 | IAaveIncentivesController public _aic; 11 | uint256 internal _userBalance; 12 | uint256 internal _totalSupply; 13 | 14 | // hack to be able to test event from EI properly 15 | event RewardsAccrued(address indexed user, uint256 amount); 16 | 17 | // hack to be able to test event from Distribution manager properly 18 | event AssetConfigUpdated(address indexed asset, uint256 emission); 19 | event AssetIndexUpdated(address indexed asset, uint256 index); 20 | event UserIndexUpdated(address indexed user, address indexed asset, uint256 index); 21 | 22 | constructor(IAaveIncentivesController aic) public { 23 | _aic = aic; 24 | } 25 | 26 | function handleActionOnAic( 27 | address user, 28 | uint256 totalSupply, 29 | uint256 userBalance 30 | ) external { 31 | _aic.handleAction(user, totalSupply, userBalance); 32 | } 33 | 34 | function doubleHandleActionOnAic( 35 | address user, 36 | uint256 totalSupply, 37 | uint256 userBalance 38 | ) external { 39 | _aic.handleAction(user, totalSupply, userBalance); 40 | _aic.handleAction(user, totalSupply, userBalance); 41 | } 42 | 43 | function setUserBalanceAndSupply(uint256 userBalance, uint256 totalSupply) public { 44 | _userBalance = userBalance; 45 | _totalSupply = totalSupply; 46 | } 47 | 48 | function getScaledUserBalanceAndSupply(address user) 49 | external 50 | view 51 | override 52 | returns (uint256, uint256) 53 | { 54 | return (_userBalance, _totalSupply); 55 | } 56 | 57 | function scaledTotalSupply() external view returns (uint256) { 58 | return _totalSupply; 59 | } 60 | 61 | function cleanUserState() external { 62 | _userBalance = 0; 63 | _totalSupply = 0; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /contracts/mocks/StakeMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.7.5; 2 | 3 | import {IStakedTokenWithConfig} from '../interfaces/IStakedTokenWithConfig.sol'; 4 | import {IERC20} from '@aave/aave-stake/contracts/interfaces/IERC20.sol'; 5 | 6 | contract StakeMock is IStakedTokenWithConfig { 7 | address public immutable override STAKED_TOKEN; 8 | 9 | constructor(address stakedToken) { 10 | STAKED_TOKEN = stakedToken; 11 | } 12 | 13 | function stake(address to, uint256 amount) external override { 14 | IERC20(STAKED_TOKEN).transferFrom(msg.sender, address(this), amount); 15 | } 16 | 17 | function redeem(address to, uint256 amount) external override {} 18 | 19 | function cooldown() external override {} 20 | 21 | function claimRewards(address to, uint256 amount) external override {} 22 | } 23 | -------------------------------------------------------------------------------- /contracts/proposals/ProposalIncentivesExecutor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity 0.7.5; 3 | pragma abicoder v2; 4 | 5 | import {IERC20} from '@aave/aave-stake/contracts/interfaces/IERC20.sol'; 6 | import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; 7 | import {ILendingPoolConfigurator} from '../interfaces/ILendingPoolConfigurator.sol'; 8 | import {IAaveIncentivesController} from '../interfaces/IAaveIncentivesController.sol'; 9 | import {IAaveEcosystemReserveController} from '../interfaces/IAaveEcosystemReserveController.sol'; 10 | import {IProposalIncentivesExecutor} from '../interfaces/IProposalIncentivesExecutor.sol'; 11 | import {DistributionTypes} from '../lib/DistributionTypes.sol'; 12 | import {DataTypes} from '../utils/DataTypes.sol'; 13 | import {ILendingPoolData} from '../interfaces/ILendingPoolData.sol'; 14 | import {IATokenDetailed} from '../interfaces/IATokenDetailed.sol'; 15 | import {PercentageMath} from '../utils/PercentageMath.sol'; 16 | import {SafeMath} from '../lib/SafeMath.sol'; 17 | 18 | contract ProposalIncentivesExecutor is IProposalIncentivesExecutor { 19 | using SafeMath for uint256; 20 | using PercentageMath for uint256; 21 | 22 | address constant AAVE_TOKEN = 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9; 23 | address constant POOL_CONFIGURATOR = 0x311Bb771e4F8952E6Da169b425E7e92d6Ac45756; 24 | address constant ADDRESSES_PROVIDER = 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5; 25 | address constant LENDING_POOL = 0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9; 26 | address constant ECO_RESERVE_ADDRESS = 0x1E506cbb6721B83B1549fa1558332381Ffa61A93; 27 | address constant INCENTIVES_CONTROLLER_PROXY_ADDRESS = 0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5; 28 | address constant INCENTIVES_CONTROLLER_IMPL_ADDRESS = 0x83D055D382f25e6793099713505c68a5C7535a35; 29 | 30 | uint256 constant DISTRIBUTION_DURATION = 7776000; // 90 days 31 | uint256 constant DISTRIBUTION_AMOUNT = 198000000000000000000000; // 198000 AAVE during 90 days 32 | 33 | function execute( 34 | address[6] memory aTokenImplementations, 35 | address[6] memory variableDebtImplementations 36 | ) external override { 37 | uint256 tokensCounter; 38 | 39 | address[] memory assets = new address[](12); 40 | 41 | // Reserves Order: DAI/GUSD/USDC/USDT/WBTC/WETH 42 | address payable[6] memory reserves = 43 | [ 44 | 0x6B175474E89094C44Da98b954EedeAC495271d0F, 45 | 0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd, 46 | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, 47 | 0xdAC17F958D2ee523a2206206994597C13D831ec7, 48 | 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599, 49 | 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 50 | ]; 51 | 52 | uint256[] memory emissions = new uint256[](12); 53 | 54 | emissions[0] = 1706018518518520; //aDAI 55 | emissions[1] = 1706018518518520; //vDebtDAI 56 | emissions[2] = 92939814814815; //aGUSD 57 | emissions[3] = 92939814814815; //vDebtGUSD 58 | emissions[4] = 5291203703703700; //aUSDC 59 | emissions[5] = 5291203703703700; //vDebtUSDC 60 | emissions[6] = 3293634259259260; //aUSDT 61 | emissions[7] = 3293634259259260; //vDebtUSDT 62 | emissions[8] = 1995659722222220; //aWBTC 63 | emissions[9] = 105034722222222; //vDebtWBTC 64 | emissions[10] = 2464942129629630; //aETH 65 | emissions[11] = 129733796296296; //vDebtWETH 66 | 67 | ILendingPoolConfigurator poolConfigurator = ILendingPoolConfigurator(POOL_CONFIGURATOR); 68 | IAaveIncentivesController incentivesController = 69 | IAaveIncentivesController(INCENTIVES_CONTROLLER_PROXY_ADDRESS); 70 | IAaveEcosystemReserveController ecosystemReserveController = 71 | IAaveEcosystemReserveController(ECO_RESERVE_ADDRESS); 72 | 73 | ILendingPoolAddressesProvider provider = ILendingPoolAddressesProvider(ADDRESSES_PROVIDER); 74 | 75 | //adding the incentives controller proxy to the addresses provider 76 | provider.setAddress(keccak256('INCENTIVES_CONTROLLER'), INCENTIVES_CONTROLLER_PROXY_ADDRESS); 77 | 78 | //updating the implementation of the incentives controller proxy 79 | provider.setAddressAsProxy(keccak256('INCENTIVES_CONTROLLER'), INCENTIVES_CONTROLLER_IMPL_ADDRESS); 80 | 81 | require( 82 | aTokenImplementations.length == variableDebtImplementations.length && 83 | aTokenImplementations.length == reserves.length, 84 | 'ARRAY_LENGTH_MISMATCH' 85 | ); 86 | 87 | // Update each reserve AToken implementation, Debt implementation, and prepare incentives configuration input 88 | for (uint256 x = 0; x < reserves.length; x++) { 89 | require( 90 | IATokenDetailed(aTokenImplementations[x]).UNDERLYING_ASSET_ADDRESS() == reserves[x], 91 | 'AToken underlying does not match' 92 | ); 93 | require( 94 | IATokenDetailed(variableDebtImplementations[x]).UNDERLYING_ASSET_ADDRESS() == reserves[x], 95 | 'Debt Token underlying does not match' 96 | ); 97 | DataTypes.ReserveData memory reserveData = 98 | ILendingPoolData(LENDING_POOL).getReserveData(reserves[x]); 99 | 100 | // Update aToken impl 101 | poolConfigurator.updateAToken(reserves[x], aTokenImplementations[x]); 102 | 103 | // Update variable debt impl 104 | poolConfigurator.updateVariableDebtToken(reserves[x], variableDebtImplementations[x]); 105 | 106 | assets[tokensCounter++] = reserveData.aTokenAddress; 107 | 108 | // Configure variable debt token at incentives controller 109 | assets[tokensCounter++] = reserveData.variableDebtTokenAddress; 110 | 111 | } 112 | // Transfer AAVE funds to the Incentives Controller 113 | ecosystemReserveController.transfer( 114 | AAVE_TOKEN, 115 | INCENTIVES_CONTROLLER_PROXY_ADDRESS, 116 | DISTRIBUTION_AMOUNT 117 | ); 118 | 119 | // Enable incentives in aTokens and Variable Debt tokens 120 | incentivesController.configureAssets(assets, emissions); 121 | 122 | // Sets the end date for the distribution 123 | incentivesController.setDistributionEnd(block.timestamp + DISTRIBUTION_DURATION); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /contracts/stake/StakedAaveV3.sol: -------------------------------------------------------------------------------- 1 | import '@aave/aave-stake/contracts/stake/StakedAaveV3.sol'; 2 | -------------------------------------------------------------------------------- /contracts/utils/Context.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.7.5; 4 | 5 | /** 6 | * @dev From https://github.com/OpenZeppelin/openzeppelin-contracts 7 | * Provides information about the current execution context, including the 8 | * sender of the transaction and its data. While these are generally available 9 | * via msg.sender and msg.data, they should not be accessed in such a direct 10 | * manner, since when dealing with GSN meta-transactions the account sending and 11 | * paying for execution may not be the actual sender (as far as an application 12 | * is concerned). 13 | * 14 | * This contract is only required for intermediate, library-like contracts. 15 | */ 16 | abstract contract Context { 17 | function _msgSender() internal view virtual returns (address payable) { 18 | return msg.sender; 19 | } 20 | 21 | function _msgData() internal view virtual returns (bytes memory) { 22 | this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 23 | return msg.data; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /contracts/utils/DataTypes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | 4 | library DataTypes { 5 | // refer to the whitepaper, section 1.1 basic concepts for a formal description of these properties. 6 | struct ReserveData { 7 | //stores the reserve configuration 8 | ReserveConfigurationMap configuration; 9 | //the liquidity index. Expressed in ray 10 | uint128 liquidityIndex; 11 | //variable borrow index. Expressed in ray 12 | uint128 variableBorrowIndex; 13 | //the current supply rate. Expressed in ray 14 | uint128 currentLiquidityRate; 15 | //the current variable borrow rate. Expressed in ray 16 | uint128 currentVariableBorrowRate; 17 | //the current stable borrow rate. Expressed in ray 18 | uint128 currentStableBorrowRate; 19 | uint40 lastUpdateTimestamp; 20 | //tokens addresses 21 | address aTokenAddress; 22 | address stableDebtTokenAddress; 23 | address variableDebtTokenAddress; 24 | //address of the interest rate strategy 25 | address interestRateStrategyAddress; 26 | //the id of the reserve. Represents the position in the list of the active reserves 27 | uint8 id; 28 | } 29 | 30 | struct ReserveConfigurationMap { 31 | //bit 0-15: LTV 32 | //bit 16-31: Liq. threshold 33 | //bit 32-47: Liq. bonus 34 | //bit 48-55: Decimals 35 | //bit 56: Reserve is active 36 | //bit 57: reserve is frozen 37 | //bit 58: borrowing is enabled 38 | //bit 59: stable rate borrowing enabled 39 | //bit 60-63: reserved 40 | //bit 64-79: reserve factor 41 | uint256 data; 42 | } 43 | 44 | struct UserConfigurationMap { 45 | uint256 data; 46 | } 47 | 48 | enum InterestRateMode {NONE, STABLE, VARIABLE} 49 | } 50 | -------------------------------------------------------------------------------- /contracts/utils/InitializableAdminUpgradeabilityProxy.sol.sol: -------------------------------------------------------------------------------- 1 | import '@aave/aave-stake/contracts/lib/InitializableAdminUpgradeabilityProxy.sol'; 2 | -------------------------------------------------------------------------------- /contracts/utils/MintableErc20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.6.12; 3 | 4 | import {ERC20} from '@aave/protocol-v2/contracts/dependencies/openzeppelin/contracts/ERC20.sol'; 5 | 6 | /** 7 | * @title ERC20Mintable 8 | * @dev ERC20 minting logic 9 | */ 10 | contract MintableErc20 is ERC20 { 11 | constructor(string memory name, string memory symbol) public ERC20(name, symbol) {} 12 | 13 | /** 14 | * @dev Function to mint tokens 15 | * @param value The amount of tokens to mint. 16 | * @return A boolean that indicates if the operation was successful. 17 | */ 18 | function mint(uint256 value) public returns (bool) { 19 | _mint(msg.sender, value); 20 | return true; 21 | } 22 | 23 | /** 24 | * @dev implements a mock permit feature 25 | **/ 26 | function permit( 27 | address owner, 28 | address spender, 29 | uint256 value, 30 | uint256 deadline, 31 | uint8 v, 32 | bytes32 r, 33 | bytes32 s 34 | ) external { 35 | _approve(owner, spender, value); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /contracts/utils/PercentageMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | 4 | /** 5 | * @title PercentageMath library 6 | * @author Aave 7 | * @notice Provides functions to perform percentage calculations 8 | * @dev Percentages are defined by default with 2 decimals of precision (100.00). The precision is indicated by PERCENTAGE_FACTOR 9 | * @dev Operations are rounded half up 10 | **/ 11 | 12 | library PercentageMath { 13 | uint256 constant PERCENTAGE_FACTOR = 1e4; //percentage plus two decimals 14 | uint256 constant HALF_PERCENT = PERCENTAGE_FACTOR / 2; 15 | 16 | /** 17 | * @dev Executes a percentage multiplication 18 | * @param value The value of which the percentage needs to be calculated 19 | * @param percentage The percentage of the value to be calculated 20 | * @return The percentage of value 21 | **/ 22 | function percentMul(uint256 value, uint256 percentage) internal pure returns (uint256) { 23 | if (value == 0 || percentage == 0) { 24 | return 0; 25 | } 26 | 27 | require( 28 | value <= (type(uint256).max - HALF_PERCENT) / percentage, 29 | 'MATH_MULTIPLICATION_OVERFLOW' 30 | ); 31 | 32 | return (value * percentage + HALF_PERCENT) / PERCENTAGE_FACTOR; 33 | } 34 | 35 | /** 36 | * @dev Executes a percentage division 37 | * @param value The value of which the percentage needs to be calculated 38 | * @param percentage The percentage of the value to be calculated 39 | * @return The value divided the percentage 40 | **/ 41 | function percentDiv(uint256 value, uint256 percentage) internal pure returns (uint256) { 42 | require(percentage != 0, 'MATH_DIVISION_BY_ZERO'); 43 | uint256 halfPercentage = percentage / 2; 44 | 45 | require( 46 | value <= (type(uint256).max - halfPercentage) / PERCENTAGE_FACTOR, 47 | 'MATH_MULTIPLICATION_OVERFLOW' 48 | ); 49 | 50 | return (value * PERCENTAGE_FACTOR + halfPercentage) / percentage; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /contracts/utils/SelfdestructTransfer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.6.12; 3 | 4 | contract SelfdestructTransfer { 5 | function destroyAndTransfer(address payable to) external payable { 6 | selfdestruct(to); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /contracts/utils/VersionedInitializable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.7.5; 3 | 4 | /** 5 | * @title VersionedInitializable 6 | * 7 | * @dev Helper contract to support initializer functions. To use it, replace 8 | * the constructor with a function that has the `initializer` modifier. 9 | * WARNING: Unlike constructors, initializer functions must be manually 10 | * invoked. This applies both to deploying an Initializable contract, as well 11 | * as extending an Initializable contract via inheritance. 12 | * WARNING: When used with inheritance, manual care must be taken to not invoke 13 | * a parent initializer twice, or ensure that all initializers are idempotent, 14 | * because this is not dealt with automatically as with constructors. 15 | * 16 | * @author Aave, inspired by the OpenZeppelin Initializable contract 17 | */ 18 | abstract contract VersionedInitializable { 19 | /** 20 | * @dev Indicates that the contract has been initialized. 21 | */ 22 | uint256 internal lastInitializedRevision = 0; 23 | 24 | /** 25 | * @dev Modifier to use in the initializer function of a contract. 26 | */ 27 | modifier initializer() { 28 | uint256 revision = getRevision(); 29 | require(revision > lastInitializedRevision, 'Contract instance has already been initialized'); 30 | 31 | lastInitializedRevision = revision; 32 | 33 | _; 34 | } 35 | 36 | /// @dev returns the revision number of the contract. 37 | /// Needs to be defined in the inherited class as a constant. 38 | function getRevision() internal pure virtual returns (uint256); 39 | 40 | // Reserved storage space to allow for layout changes in the future. 41 | uint256[50] private ______gap; 42 | } 43 | -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | contracts-env: 4 | build: 5 | context: ./ 6 | dockerfile: ./Dockerfile_test 7 | environment: 8 | - MNEMONIC 9 | - ALCHEMY_KEY 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | contracts-env: 5 | env_file: 6 | - .env 7 | build: 8 | context: ./ 9 | working_dir: /src 10 | command: npm run run-env 11 | volumes: 12 | - ./:/src 13 | - $HOME/.tenderly/config.yaml:/root/.tenderly/config.yaml 14 | environment: 15 | MNEMONIC: ${MNEMONIC} 16 | ETHERSCAN_KEY: ${ETHERSCAN_KEY} 17 | INFURA_KEY: ${INFURA_KEY} 18 | ETHERSCAN_NETWORK: ${ETHERSCAN_NETWORK} 19 | TENDERLY_PROJECT: ${TENDERLY_PROJECT} 20 | TENDERLY_USERNAME: ${TENDERLY_USERNAME} 21 | ALCHEMY_KEY: ${ALCHEMY_KEY} 22 | TENDERLY_FORK_ID: ${TENDERLY_FORK_ID} 23 | TENDERLY_HEAD_ID: ${TENDERLY_HEAD_ID} 24 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { HardhatUserConfig } from 'hardhat/types'; 4 | // @ts-ignore 5 | import { accounts } from './test-wallets'; 6 | import { 7 | eAvalancheNetwork, 8 | eEthereumNetwork, 9 | eNetwork, 10 | ePolygonNetwork, 11 | eXDaiNetwork, 12 | } from './helpers/types'; 13 | import { BUIDLEREVM_CHAINID, COVERAGE_CHAINID } from './helpers/constants'; 14 | import { NETWORKS_RPC_URL, NETWORKS_DEFAULT_GAS } from './helper-hardhat-config'; 15 | 16 | require('dotenv').config(); 17 | 18 | import '@nomiclabs/hardhat-ethers'; 19 | import '@nomiclabs/hardhat-waffle'; 20 | import 'temp-hardhat-etherscan'; 21 | import 'hardhat-gas-reporter'; 22 | import '@typechain/hardhat'; 23 | import '@tenderly/hardhat-tenderly'; 24 | import 'hardhat-deploy'; 25 | import 'solidity-coverage'; 26 | 27 | const SKIP_LOAD = process.env.SKIP_LOAD === 'true'; 28 | const DEFAULT_BLOCK_GAS_LIMIT = 12450000; 29 | const DEFAULT_GAS_MUL = 5; 30 | const HARDFORK = 'istanbul'; 31 | const ETHERSCAN_KEY = process.env.ETHERSCAN_KEY || ''; 32 | const MNEMONIC_PATH = "m/44'/60'/0'/0"; 33 | const MNEMONIC = process.env.MNEMONIC || ''; 34 | const MAINNET_FORK = process.env.MAINNET_FORK === 'true'; 35 | 36 | // Prevent to load scripts before compilation and typechain 37 | if (!SKIP_LOAD) { 38 | ['misc', /*'payloads',*/ 'migrations', 'deployment'].forEach((folder) => { 39 | const tasksPath = path.join(__dirname, 'tasks', folder); 40 | fs.readdirSync(tasksPath) 41 | .filter((pth) => pth.includes('.ts')) 42 | .forEach((task) => { 43 | require(`${tasksPath}/${task}`); 44 | }); 45 | }); 46 | } 47 | 48 | require(`${path.join(__dirname, 'tasks/misc')}/set-DRE.ts`); 49 | 50 | const getCommonNetworkConfig = (networkName: eNetwork, networkId: number) => ({ 51 | url: NETWORKS_RPC_URL[networkName], 52 | hardfork: HARDFORK, 53 | blockGasLimit: DEFAULT_BLOCK_GAS_LIMIT, 54 | gasMultiplier: DEFAULT_GAS_MUL, 55 | gasPrice: NETWORKS_DEFAULT_GAS[networkName], 56 | chainId: networkId, 57 | accounts: { 58 | mnemonic: MNEMONIC, 59 | path: MNEMONIC_PATH, 60 | initialIndex: 0, 61 | count: 20, 62 | }, 63 | }); 64 | 65 | const mainnetFork = MAINNET_FORK 66 | ? { 67 | blockNumber: 12290275, 68 | url: NETWORKS_RPC_URL['main'], 69 | } 70 | : undefined; 71 | 72 | const buidlerConfig: HardhatUserConfig = { 73 | solidity: { 74 | compilers: [ 75 | { 76 | version: '0.7.5', 77 | settings: { 78 | optimizer: { enabled: true, runs: 200 }, 79 | evmVersion: 'istanbul', 80 | }, 81 | }, 82 | { 83 | version: '0.6.10', 84 | settings: { 85 | optimizer: { enabled: true, runs: 200 }, 86 | evmVersion: 'istanbul', 87 | }, 88 | }, 89 | { 90 | version: '0.6.12', 91 | settings: { 92 | optimizer: { enabled: true, runs: 200 }, 93 | evmVersion: 'istanbul', 94 | }, 95 | }, 96 | ], 97 | }, 98 | typechain: { 99 | outDir: 'types', 100 | target: 'ethers-v5', 101 | }, 102 | etherscan: { 103 | apiKey: ETHERSCAN_KEY, 104 | }, 105 | mocha: { 106 | timeout: 0, 107 | }, 108 | tenderly: { 109 | project: process.env.TENDERLY_PROJECT || '', 110 | username: process.env.TENDERLY_USERNAME || '', 111 | forkNetwork: '3030', //Network id of the network we want to fork 112 | }, 113 | networks: { 114 | coverage: { 115 | url: 'http://localhost:8555', 116 | chainId: COVERAGE_CHAINID, 117 | }, 118 | kovan: getCommonNetworkConfig(eEthereumNetwork.kovan, 42), 119 | ropsten: getCommonNetworkConfig(eEthereumNetwork.ropsten, 3), 120 | main: getCommonNetworkConfig(eEthereumNetwork.main, 1), 121 | tenderlyMain: getCommonNetworkConfig(eEthereumNetwork.tenderlyMain, 3030), 122 | tenderly: getCommonNetworkConfig(eEthereumNetwork.tenderlyMain, 3030), 123 | matic: getCommonNetworkConfig(ePolygonNetwork.matic, 137), 124 | mumbai: getCommonNetworkConfig(ePolygonNetwork.mumbai, 80001), 125 | xdai: getCommonNetworkConfig(eXDaiNetwork.xdai, 100), 126 | fuji: getCommonNetworkConfig(eAvalancheNetwork.fuji, 43113), 127 | avalanche: getCommonNetworkConfig(eAvalancheNetwork.avalanche, 43114), 128 | hardhat: { 129 | hardfork: 'istanbul', 130 | blockGasLimit: DEFAULT_BLOCK_GAS_LIMIT, 131 | gas: DEFAULT_BLOCK_GAS_LIMIT, 132 | gasPrice: 133 | NETWORKS_DEFAULT_GAS[MAINNET_FORK ? eEthereumNetwork.main : eEthereumNetwork.hardhat], 134 | chainId: BUIDLEREVM_CHAINID, 135 | throwOnTransactionFailures: true, 136 | throwOnCallFailures: true, 137 | accounts: accounts.map(({ secretKey, balance }: { secretKey: string; balance: string }) => ({ 138 | privateKey: secretKey, 139 | balance, 140 | })), 141 | forking: mainnetFork, 142 | }, 143 | buidlerevm_docker: { 144 | hardfork: 'istanbul', 145 | blockGasLimit: 9500000, 146 | gas: 9500000, 147 | gasPrice: 8000000000, 148 | chainId: BUIDLEREVM_CHAINID, 149 | throwOnTransactionFailures: true, 150 | throwOnCallFailures: true, 151 | url: 'http://localhost:8545', 152 | }, 153 | ganache: { 154 | url: 'http://ganache:8545', 155 | accounts: { 156 | mnemonic: 'fox sight canyon orphan hotel grow hedgehog build bless august weather swarm', 157 | path: "m/44'/60'/0'/0", 158 | initialIndex: 0, 159 | count: 20, 160 | }, 161 | }, 162 | }, 163 | }; 164 | 165 | export default buidlerConfig; 166 | -------------------------------------------------------------------------------- /helper-hardhat-config.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { 3 | eAvalancheNetwork, 4 | eEthereumNetwork, 5 | ePolygonNetwork, 6 | eXDaiNetwork, 7 | iParamsPerNetwork, 8 | } from './helpers/types'; 9 | 10 | require('dotenv').config(); 11 | 12 | const INFURA_KEY = process.env.INFURA_KEY || ''; 13 | const ALCHEMY_KEY = process.env.ALCHEMY_KEY || ''; 14 | const TENDERLY_FORK_ID = process.env.TENDERLY_FORK_ID || ''; 15 | 16 | const GWEI = 1000 * 1000 * 1000; 17 | 18 | export const NETWORKS_RPC_URL: iParamsPerNetwork = { 19 | [eEthereumNetwork.kovan]: ALCHEMY_KEY 20 | ? `https://eth-kovan.alchemyapi.io/v2/${ALCHEMY_KEY}` 21 | : `https://kovan.infura.io/v3/${INFURA_KEY}`, 22 | [eEthereumNetwork.ropsten]: ALCHEMY_KEY 23 | ? `https://eth-ropsten.alchemyapi.io/v2/${ALCHEMY_KEY}` 24 | : `https://ropsten.infura.io/v3/${INFURA_KEY}`, 25 | [eEthereumNetwork.main]: ALCHEMY_KEY 26 | ? `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_KEY}` 27 | : `https://mainnet.infura.io/v3/${INFURA_KEY}`, 28 | [eEthereumNetwork.coverage]: 'http://localhost:8555', 29 | [eEthereumNetwork.hardhat]: 'http://localhost:8545', 30 | [eEthereumNetwork.buidlerevm]: 'http://localhost:8545', 31 | [eEthereumNetwork.tenderlyMain]: `https://rpc.tenderly.co/fork/${TENDERLY_FORK_ID}`, 32 | [ePolygonNetwork.mumbai]: 'https://rpc-mumbai.maticvigil.com', 33 | [ePolygonNetwork.matic]: 'https://rpc-mainnet.matic.network', 34 | [eXDaiNetwork.xdai]: 'https://rpc.xdaichain.com/', 35 | [eAvalancheNetwork.avalanche]: 'https://cchain.explorer.avax.network/', 36 | [eAvalancheNetwork.fuji]: 'https://api.avax-test.network/ext/bc/C/rpc', 37 | }; 38 | 39 | export const NETWORKS_DEFAULT_GAS: iParamsPerNetwork = { 40 | [eEthereumNetwork.kovan]: 1 * GWEI, 41 | [eEthereumNetwork.ropsten]: 1 * GWEI, 42 | [eEthereumNetwork.main]: 180 * GWEI, 43 | [eEthereumNetwork.coverage]: 1 * GWEI, 44 | [eEthereumNetwork.hardhat]: 1 * GWEI, 45 | [eEthereumNetwork.buidlerevm]: 1 * GWEI, 46 | [eEthereumNetwork.tenderlyMain]: 1 * GWEI, 47 | [ePolygonNetwork.mumbai]: 1 * GWEI, 48 | [ePolygonNetwork.matic]: 2 * GWEI, 49 | [eXDaiNetwork.xdai]: 1 * GWEI, 50 | [eAvalancheNetwork.fuji]: 225 * GWEI, 51 | [eAvalancheNetwork.avalanche]: 225 * GWEI, 52 | }; 53 | -------------------------------------------------------------------------------- /helpers/constants.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | 3 | // ---------------- 4 | // MATH 5 | // ---------------- 6 | 7 | export const PERCENTAGE_FACTOR = '10000'; 8 | export const HALF_PERCENTAGE = '5000'; 9 | export const WAD = Math.pow(10, 18).toString(); 10 | export const HALF_WAD = new BigNumber(WAD).multipliedBy(0.5).toString(); 11 | export const RAY = new BigNumber(10).exponentiatedBy(27).toFixed(); 12 | export const HALF_RAY = new BigNumber(RAY).multipliedBy(0.5).toFixed(); 13 | export const WAD_RAY_RATIO = Math.pow(10, 9).toString(); 14 | export const oneEther = new BigNumber(Math.pow(10, 18)); 15 | export const oneRay = new BigNumber(Math.pow(10, 27)); 16 | export const MAX_UINT_AMOUNT = 17 | '115792089237316195423570985008687907853269984665640564039457584007913129639935'; 18 | export const ONE_YEAR = '31536000'; 19 | export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; 20 | export const ONE_ADDRESS = '0x0000000000000000000000000000000000000001'; 21 | 22 | /* Buidler/Hardhat constants */ 23 | 24 | export const TEST_SNAPSHOT_ID = '0x1'; 25 | export const BUIDLEREVM_CHAINID = 31337; 26 | export const COVERAGE_CHAINID = 1337; 27 | 28 | export const RANDOM_ADDRESSES = [ 29 | '0x0000000000000000000000000000000000000221', 30 | '0x0000000000000000000000000000000000000321', 31 | '0x0000000000000000000000000000000000000211', 32 | '0x0000000000000000000000000000000000000251', 33 | '0x0000000000000000000000000000000000000271', 34 | '0x0000000000000000000000000000000000000291', 35 | '0x0000000000000000000000000000000000000321', 36 | '0x0000000000000000000000000000000000000421', 37 | '0x0000000000000000000000000000000000000521', 38 | '0x0000000000000000000000000000000000000621', 39 | '0x0000000000000000000000000000000000000721', 40 | ]; -------------------------------------------------------------------------------- /helpers/contracts-accessors.ts: -------------------------------------------------------------------------------- 1 | import { 2 | deployContract, 3 | getContractFactory, 4 | getContract, 5 | getFirstSigner, 6 | registerContractInJsonDb, 7 | } from './contracts-helpers'; 8 | import { eContractid, tEthereumAddress } from './types'; 9 | import { MintableErc20 } from '../types/MintableErc20'; 10 | import { SelfdestructTransfer } from '../types/SelfdestructTransfer'; 11 | import { IERC20Detailed } from '../types/IERC20Detailed'; 12 | import { verifyContract } from './etherscan-verification'; 13 | import { ATokenMock } from '../types/ATokenMock'; 14 | import { 15 | PullRewardsIncentivesController__factory, 16 | InitializableAdminUpgradeabilityProxy__factory, 17 | StakedTokenIncentivesController, 18 | StakedTokenIncentivesController__factory, 19 | } from '../types'; 20 | import { DefenderRelaySigner } from 'defender-relay-client/lib/ethers'; 21 | import { Signer } from 'ethers'; 22 | 23 | export const deployAaveIncentivesController = async ( 24 | [aavePsm, emissionManager]: [tEthereumAddress, tEthereumAddress], 25 | verify?: boolean, 26 | signer?: Signer | DefenderRelaySigner 27 | ) => { 28 | const args: [string, string] = [aavePsm, emissionManager]; 29 | const instance = await new StakedTokenIncentivesController__factory( 30 | signer || (await getFirstSigner()) 31 | ).deploy(...args); 32 | await instance.deployTransaction.wait(); 33 | if (verify) { 34 | await verifyContract(instance.address, args); 35 | } 36 | return instance; 37 | }; 38 | 39 | export const deployPullRewardsIncentivesController = async ( 40 | [rewardToken, emissionManager]: [tEthereumAddress, tEthereumAddress], 41 | verify?: boolean, 42 | signer?: Signer | DefenderRelaySigner 43 | ) => { 44 | const args: [string, string] = [rewardToken, emissionManager]; 45 | const instance = await new PullRewardsIncentivesController__factory( 46 | signer || (await getFirstSigner()) 47 | ).deploy(...args); 48 | await instance.deployTransaction.wait(); 49 | if (verify) { 50 | await verifyContract(instance.address, args); 51 | } 52 | return instance; 53 | }; 54 | 55 | export const deployInitializableAdminUpgradeabilityProxy = async (verify?: boolean) => { 56 | const args: string[] = []; 57 | const instance = await new InitializableAdminUpgradeabilityProxy__factory( 58 | await getFirstSigner() 59 | ).deploy(); 60 | await instance.deployTransaction.wait(); 61 | if (verify) { 62 | await verifyContract(instance.address, args); 63 | } 64 | return instance; 65 | }; 66 | 67 | export const deployMintableErc20 = async ([name, symbol]: [string, string]) => 68 | await deployContract(eContractid.MintableErc20, [name, symbol]); 69 | 70 | export const deployATokenMock = async (aicAddress: tEthereumAddress, slug: string) => { 71 | const instance = await deployContract(eContractid.ATokenMock, [aicAddress]); 72 | await registerContractInJsonDb(`${eContractid.ATokenMock}-${slug}`, instance); 73 | }; 74 | 75 | export const getMintableErc20 = getContractFactory(eContractid.MintableErc20); 76 | 77 | export const getAaveIncentivesController = getContractFactory( 78 | eContractid.StakedTokenIncentivesController 79 | ); 80 | 81 | export const getIncentivesController = async (address: tEthereumAddress) => 82 | StakedTokenIncentivesController__factory.connect(address, await getFirstSigner()); 83 | 84 | export const getPullRewardsIncentivesController = async (address: tEthereumAddress) => 85 | PullRewardsIncentivesController__factory.connect(address, await getFirstSigner()); 86 | 87 | export const getIErc20Detailed = getContractFactory(eContractid.IERC20Detailed); 88 | 89 | export const getATokenMock = getContractFactory(eContractid.ATokenMock); 90 | 91 | export const getERC20Contract = (address: tEthereumAddress) => 92 | getContract(eContractid.MintableErc20, address); 93 | 94 | export const deploySelfDestruct = async () => { 95 | const id = eContractid.MockSelfDestruct; 96 | const instance = await deployContract(id, []); 97 | await instance.deployTransaction.wait(); 98 | return instance; 99 | }; 100 | -------------------------------------------------------------------------------- /helpers/contracts-helpers.ts: -------------------------------------------------------------------------------- 1 | import { Contract, Signer, utils, ethers, BigNumberish } from 'ethers'; 2 | import { signTypedData_v4 } from 'eth-sig-util'; 3 | import { fromRpcSig, ECDSASignature } from 'ethereumjs-util'; 4 | import { getDb, DRE, waitForTx } from './misc-utils'; 5 | import { 6 | tEthereumAddress, 7 | eContractid, 8 | eEthereumNetwork, 9 | iParamsPerNetwork, 10 | ePolygonNetwork, 11 | eXDaiNetwork, 12 | eNetwork, 13 | iEthereumParamsPerNetwork, 14 | iPolygonParamsPerNetwork, 15 | iXDaiParamsPerNetwork, 16 | } from './types'; 17 | import { Artifact } from 'hardhat/types'; 18 | import { verifyContract } from './etherscan-verification'; 19 | import { usingTenderly } from './tenderly-utils'; 20 | 21 | export const registerContractInJsonDb = async (contractId: string, contractInstance: Contract) => { 22 | const currentNetwork = DRE.network.name; 23 | const MAINNET_FORK = process.env.MAINNET_FORK === 'true'; 24 | if (MAINNET_FORK || (currentNetwork !== 'hardhat' && !currentNetwork.includes('coverage'))) { 25 | console.log(`*** ${contractId} ***\n`); 26 | console.log(`Network: ${currentNetwork}`); 27 | console.log(`tx: ${contractInstance.deployTransaction.hash}`); 28 | console.log(`contract address: ${contractInstance.address}`); 29 | console.log(`deployer address: ${contractInstance.deployTransaction.from}`); 30 | console.log(`gas price: ${contractInstance.deployTransaction.gasPrice}`); 31 | console.log(`gas used: ${contractInstance.deployTransaction.gasLimit}`); 32 | console.log(`\n******`); 33 | console.log(); 34 | } 35 | 36 | await getDb() 37 | .set(`${contractId}.${currentNetwork}`, { 38 | address: contractInstance.address, 39 | deployer: contractInstance.deployTransaction.from, 40 | }) 41 | .write(); 42 | }; 43 | export const getFirstSigner = async () => (await DRE.ethers.getSigners())[0]; 44 | 45 | export const insertContractAddressInDb = async (id: eContractid, address: tEthereumAddress) => 46 | await getDb() 47 | .set(`${id}.${DRE.network.name}`, { 48 | address, 49 | }) 50 | .write(); 51 | 52 | export const rawInsertContractAddressInDb = async (id: string, address: tEthereumAddress) => 53 | await getDb() 54 | .set(`${id}.${DRE.network.name}`, { 55 | address, 56 | }) 57 | .write(); 58 | 59 | export const getEthersSigners = async (): Promise => 60 | await Promise.all(await DRE.ethers.getSigners()); 61 | 62 | export const getEthersSignersAddresses = async (): Promise => 63 | await Promise.all((await DRE.ethers.getSigners()).map((signer) => signer.getAddress())); 64 | 65 | export const getCurrentBlock = async () => { 66 | return DRE.ethers.provider.getBlockNumber(); 67 | }; 68 | 69 | export const decodeAbiNumber = (data: string): number => 70 | parseInt(utils.defaultAbiCoder.decode(['uint256'], data).toString()); 71 | 72 | export const deployContract = async ( 73 | contractName: string, 74 | args: any[] 75 | ): Promise => { 76 | const contract = (await (await DRE.ethers.getContractFactory(contractName)).deploy( 77 | ...args 78 | )) as ContractType; 79 | await waitForTx(contract.deployTransaction); 80 | await registerContractInJsonDb(contractName, contract); 81 | return contract; 82 | }; 83 | 84 | export const withSaveAndVerify = async ( 85 | instance: ContractType, 86 | id: string, 87 | args: (string | string[])[], 88 | verify?: boolean 89 | ): Promise => { 90 | await waitForTx(instance.deployTransaction); 91 | await registerContractInJsonDb(id, instance); 92 | if (usingTenderly()) { 93 | console.log(); 94 | console.log('Doing Tenderly contract verification of', id); 95 | await (DRE as any).tenderlyRPC.verify({ 96 | name: id, 97 | address: instance.address, 98 | }); 99 | console.log(`Verified ${id} at Tenderly!`); 100 | console.log(); 101 | } 102 | if (verify) { 103 | await verifyContract(instance.address, args); 104 | } 105 | return instance; 106 | }; 107 | 108 | export const getContract = async ( 109 | contractName: string, 110 | address: string 111 | ): Promise => (await DRE.ethers.getContractAt(contractName, address)) as ContractType; 112 | 113 | export const linkBytecode = (artifact: Artifact, libraries: any) => { 114 | let bytecode = artifact.bytecode; 115 | 116 | for (const [fileName, fileReferences] of Object.entries(artifact.linkReferences)) { 117 | for (const [libName, fixups] of Object.entries(fileReferences)) { 118 | const addr = libraries[libName]; 119 | 120 | if (addr === undefined) { 121 | continue; 122 | } 123 | 124 | for (const fixup of fixups) { 125 | bytecode = 126 | bytecode.substr(0, 2 + fixup.start * 2) + 127 | addr.substr(2) + 128 | bytecode.substr(2 + (fixup.start + fixup.length) * 2); 129 | } 130 | } 131 | } 132 | 133 | return bytecode; 134 | }; 135 | 136 | export const getParamPerNetwork = (param: iParamsPerNetwork, network: eNetwork) => { 137 | const { 138 | main, 139 | ropsten, 140 | kovan, 141 | coverage, 142 | buidlerevm, 143 | tenderlyMain, 144 | } = param as iEthereumParamsPerNetwork; 145 | const { matic, mumbai } = param as iPolygonParamsPerNetwork; 146 | const { xdai } = param as iXDaiParamsPerNetwork; 147 | const MAINNET_FORK = process.env.MAINNET_FORK === 'true'; 148 | if (MAINNET_FORK) { 149 | return main; 150 | } 151 | 152 | switch (network) { 153 | case eEthereumNetwork.coverage: 154 | return coverage; 155 | case eEthereumNetwork.buidlerevm: 156 | return buidlerevm; 157 | case eEthereumNetwork.hardhat: 158 | return buidlerevm; 159 | case eEthereumNetwork.kovan: 160 | return kovan; 161 | case eEthereumNetwork.ropsten: 162 | return ropsten; 163 | case eEthereumNetwork.main: 164 | return main; 165 | case eEthereumNetwork.tenderlyMain: 166 | return tenderlyMain; 167 | case ePolygonNetwork.matic: 168 | return matic; 169 | case ePolygonNetwork.mumbai: 170 | return mumbai; 171 | case eXDaiNetwork.xdai: 172 | return xdai; 173 | } 174 | }; 175 | 176 | export const getSignatureFromTypedData = ( 177 | privateKey: string, 178 | typedData: any // TODO: should be TypedData, from eth-sig-utils, but TS doesn't accept it 179 | ): ECDSASignature => { 180 | const signature = signTypedData_v4(Buffer.from(privateKey.substring(2, 66), 'hex'), { 181 | data: typedData, 182 | }); 183 | return fromRpcSig(signature); 184 | }; 185 | 186 | type ContractGetter = { address?: string; slug?: string }; 187 | export const getContractFactory = ( 188 | contractName: eContractid 189 | ) => async (contractGetter?: ContractGetter): Promise => { 190 | let deployedContract = ''; 191 | if (!contractGetter?.address) { 192 | try { 193 | deployedContract = ( 194 | await getDb() 195 | .get( 196 | `${contractName}${contractGetter?.slug ? `-${contractGetter.slug}` : ''}.${ 197 | DRE.network.name 198 | }` 199 | ) 200 | .value() 201 | ).address; 202 | } catch (e) { 203 | throw new Error( 204 | `Contract ${contractName} was not deployed on ${DRE.network.name} or not stored in DB` 205 | ); 206 | } 207 | } 208 | return (await DRE.ethers.getContractAt( 209 | contractName, 210 | contractGetter?.address || deployedContract 211 | )) as ContractType; 212 | }; 213 | 214 | export const getBlockTimestamp = async (blockNumber?: number): Promise => { 215 | if (!blockNumber) { 216 | blockNumber = await getCurrentBlock(); 217 | } 218 | const block = await DRE.ethers.provider.getBlock(blockNumber); 219 | return block.timestamp; 220 | }; 221 | -------------------------------------------------------------------------------- /helpers/defender-utils.ts: -------------------------------------------------------------------------------- 1 | import { formatEther } from '@ethersproject/units'; 2 | import { DefenderRelaySigner, DefenderRelayProvider } from 'defender-relay-client/lib/ethers'; 3 | import { Signer } from 'ethers'; 4 | import { DRE, impersonateAccountsHardhat } from './misc-utils'; 5 | 6 | export const getDefenderRelaySigner = async () => { 7 | const { DEFENDER_API_KEY, DEFENDER_SECRET_KEY } = process.env; 8 | let signer: Signer; 9 | 10 | if (!DEFENDER_API_KEY || !DEFENDER_SECRET_KEY) { 11 | throw new Error('Defender secrets required'); 12 | } 13 | 14 | const credentials = { apiKey: DEFENDER_API_KEY, apiSecret: DEFENDER_SECRET_KEY }; 15 | 16 | signer = new DefenderRelaySigner(credentials, new DefenderRelayProvider(credentials), { 17 | speed: 'fast', 18 | }); 19 | 20 | const defenderAddress = await signer.getAddress(); 21 | 22 | // Reemplace signer if MAINNET_FORK is active 23 | if (process.env.MAINNET_FORK === 'true') { 24 | console.log(' - Impersonating Defender Relay'); 25 | await impersonateAccountsHardhat([defenderAddress]); 26 | signer = await DRE.ethers.getSigner(defenderAddress); 27 | } 28 | // Reemplace signer if Tenderly network is active 29 | if (DRE.network.name.includes('tenderly')) { 30 | console.log(' - Impersonating Defender Relay via Tenderly'); 31 | signer = await DRE.ethers.getSigner(defenderAddress); 32 | } 33 | console.log(' - Balance: ', formatEther(await signer.getBalance())); 34 | 35 | return { signer, address: defenderAddress }; 36 | }; 37 | -------------------------------------------------------------------------------- /helpers/etherscan-verification.ts: -------------------------------------------------------------------------------- 1 | import { exit } from 'process'; 2 | import fs from 'fs'; 3 | import { file } from 'tmp-promise'; 4 | import { DRE } from './misc-utils'; 5 | 6 | const fatalErrors = [ 7 | `The address provided as argument contains a contract, but its bytecode`, 8 | `Daily limit of 100 source code submissions reached`, 9 | `has no bytecode. Is the contract deployed to this network`, 10 | `The constructor for`, 11 | ]; 12 | 13 | const okErrors = [`Contract source code already verified`]; 14 | 15 | const unableVerifyError = 'Fail - Unable to verify'; 16 | 17 | export const SUPPORTED_ETHERSCAN_NETWORKS = ['main', 'ropsten', 'kovan']; 18 | 19 | function delay(ms: number) { 20 | return new Promise((resolve) => setTimeout(resolve, ms)); 21 | } 22 | 23 | export const verifyContract = async ( 24 | address: string, 25 | constructorArguments: (string | string[])[], 26 | libraries?: string 27 | ) => { 28 | const currentNetwork = DRE.network.name; 29 | 30 | if (!process.env.ETHERSCAN_KEY) { 31 | throw Error('Missing process.env.ETHERSCAN_KEY.'); 32 | } 33 | if (!SUPPORTED_ETHERSCAN_NETWORKS.includes(currentNetwork)) { 34 | throw Error( 35 | `Current network ${currentNetwork} not supported. Please change to one of the next networks: ${SUPPORTED_ETHERSCAN_NETWORKS.toString()}` 36 | ); 37 | } 38 | 39 | try { 40 | console.log( 41 | '[ETHERSCAN][WARNING] Delaying Etherscan verification due their API can not find newly deployed contracts' 42 | ); 43 | const msDelay = 3000; 44 | const times = 4; 45 | // Write a temporal file to host complex parameters for buidler-etherscan https://github.com/nomiclabs/buidler/tree/development/packages/buidler-etherscan#complex-arguments 46 | const { fd, path, cleanup } = await file({ 47 | prefix: 'verify-params-', 48 | postfix: '.js', 49 | }); 50 | fs.writeSync(fd, `module.exports = ${JSON.stringify([...constructorArguments])};`); 51 | 52 | const params = { 53 | address: address, 54 | libraries, 55 | constructorArgs: path, 56 | relatedSources: true, 57 | }; 58 | await runTaskWithRetry('verify', params, times, msDelay, cleanup); 59 | } catch (error) {} 60 | }; 61 | 62 | export const runTaskWithRetry = async ( 63 | task: string, 64 | params: any, 65 | times: number, 66 | msDelay: number, 67 | cleanup: () => void 68 | ) => { 69 | let counter = times; 70 | await delay(msDelay); 71 | 72 | try { 73 | if (times > 1) { 74 | await DRE.run(task, params); 75 | cleanup(); 76 | } else if (times === 1) { 77 | console.log('[ETHERSCAN][WARNING] Trying to verify via uploading all sources.'); 78 | delete params.relatedSources; 79 | await DRE.run(task, params); 80 | cleanup(); 81 | } else { 82 | cleanup(); 83 | console.error( 84 | '[ETHERSCAN][ERROR] Errors after all the retries, check the logs for more information.' 85 | ); 86 | } 87 | } catch (error) { 88 | counter--; 89 | 90 | if (okErrors.some((okReason) => error.message.includes(okReason))) { 91 | console.info('[ETHERSCAN][INFO] Skipping due OK response: ', error.message); 92 | return; 93 | } 94 | 95 | if (fatalErrors.some((fatalError) => error.message.includes(fatalError))) { 96 | console.error( 97 | '[ETHERSCAN][ERROR] Fatal error detected, skip retries and resume deployment.', 98 | error.message 99 | ); 100 | return; 101 | } 102 | console.error('[ETHERSCAN][ERROR]', error.message); 103 | console.log(); 104 | console.info(`[ETHERSCAN][[INFO] Retrying attemps: ${counter}.`); 105 | if (error.message.includes(unableVerifyError)) { 106 | console.log('[ETHERSCAN][WARNING] Trying to verify via uploading all sources.'); 107 | delete params.relatedSources; 108 | } 109 | await runTaskWithRetry(task, params, counter, msDelay, cleanup); 110 | } 111 | }; 112 | 113 | export const checkVerification = () => { 114 | const currentNetwork = DRE.network.name; 115 | if (!process.env.ETHERSCAN_KEY) { 116 | console.error('Missing process.env.ETHERSCAN_KEY.'); 117 | exit(3); 118 | } 119 | if (!SUPPORTED_ETHERSCAN_NETWORKS.includes(currentNetwork)) { 120 | console.error( 121 | `Current network ${currentNetwork} not supported. Please change to one of the next networks: ${SUPPORTED_ETHERSCAN_NETWORKS.toString()}` 122 | ); 123 | exit(5); 124 | } 125 | }; 126 | -------------------------------------------------------------------------------- /helpers/misc-utils.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import BN = require('bn.js'); 3 | import low from 'lowdb'; 4 | import FileSync from 'lowdb/adapters/FileSync'; 5 | import { WAD } from './constants'; 6 | import { Wallet, ContractTransaction } from 'ethers'; 7 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 8 | import { tEthereumAddress } from './types'; 9 | import { isAddress } from 'ethers/lib/utils'; 10 | import { isZeroAddress } from 'ethereumjs-util'; 11 | import { TenderlyNetwork } from '@tenderly/hardhat-tenderly/dist/TenderlyNetwork'; 12 | import { setNewHead } from './tenderly-utils'; 13 | 14 | export const toWad = (value: string | number) => new BigNumber(value).times(WAD).toFixed(); 15 | 16 | export const bnToBigNumber = (amount: BN): BigNumber => new BigNumber(amount); 17 | export const stringToBigNumber = (amount: string): BigNumber => new BigNumber(amount); 18 | 19 | export const getDb = () => low(new FileSync('./deployed-contracts.json')); 20 | 21 | export let DRE: HardhatRuntimeEnvironment; 22 | 23 | export const setDRE = (_DRE: HardhatRuntimeEnvironment) => { 24 | DRE = _DRE; 25 | }; 26 | 27 | export const sleep = (milliseconds: number) => { 28 | return new Promise((resolve) => setTimeout(resolve, milliseconds)); 29 | }; 30 | 31 | export const createRandomAddress = () => Wallet.createRandom().address; 32 | 33 | export const evmSnapshot = async () => await DRE.ethers.provider.send('evm_snapshot', []); 34 | 35 | export const evmRevert = async (id: string) => DRE.ethers.provider.send('evm_revert', [id]); 36 | 37 | export const timeLatest = async () => { 38 | const block = await DRE.ethers.provider.getBlock('latest'); 39 | return new BigNumber(block.timestamp); 40 | }; 41 | 42 | export const increaseTime = async (secondsToIncrease: number) => { 43 | if (process.env.TENDERLY === 'true') { 44 | await DRE.ethers.provider.send('evm_increaseTime', [`0x${secondsToIncrease.toString(16)}`]); 45 | return; 46 | } 47 | await DRE.ethers.provider.send('evm_increaseTime', [secondsToIncrease]); 48 | await DRE.ethers.provider.send('evm_mine', []); 49 | }; 50 | 51 | // Workaround for time travel tests bug: https://github.com/Tonyhaenn/hh-time-travel/blob/0161d993065a0b7585ec5a043af2eb4b654498b8/test/test.js#L12 52 | export const advanceTimeAndBlock = async function (forwardTime: number) { 53 | const currentBlockNumber = await DRE.ethers.provider.getBlockNumber(); 54 | const currentBlock = await DRE.ethers.provider.getBlock(currentBlockNumber); 55 | 56 | if (currentBlock === null) { 57 | /* Workaround for https://github.com/nomiclabs/hardhat/issues/1183 58 | */ 59 | await DRE.ethers.provider.send('evm_increaseTime', [forwardTime]); 60 | await DRE.ethers.provider.send('evm_mine', []); 61 | //Set the next blocktime back to 15 seconds 62 | await DRE.ethers.provider.send('evm_increaseTime', [15]); 63 | return; 64 | } 65 | const currentTime = currentBlock.timestamp; 66 | const futureTime = currentTime + forwardTime; 67 | await DRE.ethers.provider.send('evm_setNextBlockTimestamp', [futureTime]); 68 | await DRE.ethers.provider.send('evm_mine', []); 69 | }; 70 | 71 | export const waitForTx = async (tx: ContractTransaction) => await tx.wait(1); 72 | 73 | export const filterMapBy = (raw: { [key: string]: any }, fn: (key: string) => boolean) => 74 | Object.keys(raw) 75 | .filter(fn) 76 | .reduce<{ [key: string]: any }>((obj, key) => { 77 | obj[key] = raw[key]; 78 | return obj; 79 | }, {}); 80 | 81 | export const chunk = (arr: Array, chunkSize: number): Array> => { 82 | return arr.reduce( 83 | (prevVal: any, currVal: any, currIndx: number, array: Array) => 84 | !(currIndx % chunkSize) 85 | ? prevVal.concat([array.slice(currIndx, currIndx + chunkSize)]) 86 | : prevVal, 87 | [] 88 | ); 89 | }; 90 | 91 | interface DbEntry { 92 | [network: string]: { 93 | deployer: string; 94 | address: string; 95 | }; 96 | } 97 | 98 | export const printContracts = () => { 99 | const network = DRE.network.name; 100 | const db = getDb(); 101 | console.log('Contracts deployed at', network); 102 | console.log('---------------------------------'); 103 | 104 | const entries = Object.entries(db.getState()).filter(([_k, value]) => !!value[network]); 105 | 106 | const contractsPrint = entries.map( 107 | ([key, value]: [string, DbEntry]) => `${key}: ${value[network].address}` 108 | ); 109 | 110 | console.log('N# Contracts:', entries.length); 111 | console.log(contractsPrint.join('\n'), '\n'); 112 | }; 113 | 114 | export const notFalsyOrZeroAddress = (address: tEthereumAddress | null | undefined): boolean => { 115 | if (!address) { 116 | return false; 117 | } 118 | return isAddress(address) && !isZeroAddress(address); 119 | }; 120 | 121 | export const latestBlock = async () => DRE.ethers.provider.getBlockNumber(); 122 | 123 | export const advanceBlock = async (timestamp: number) => 124 | await DRE.ethers.provider.send('evm_mine', [timestamp]); 125 | 126 | export const advanceBlockTo = async (target: number) => { 127 | const currentBlock = await latestBlock(); 128 | if (process.env.TENDERLY === 'true') { 129 | const pendingBlocks = target - currentBlock - 1; 130 | 131 | const response = await DRE.ethers.provider.send('evm_increaseBlocks', [ 132 | `0x${pendingBlocks.toString(16)}`, 133 | ]); 134 | 135 | return; 136 | } 137 | const start = Date.now(); 138 | let notified; 139 | if (target < currentBlock) 140 | throw Error(`Target block #(${target}) is lower than current block #(${currentBlock})`); 141 | // eslint-disable-next-line no-await-in-loop 142 | while ((await latestBlock()) < target) { 143 | if (!notified && Date.now() - start >= 5000) { 144 | notified = true; 145 | console.log("advanceBlockTo: Advancing too many blocks is causing this test to be slow.'"); 146 | } 147 | // eslint-disable-next-line no-await-in-loop 148 | await advanceBlock(0); 149 | } 150 | }; 151 | 152 | export const impersonateAccountsHardhat = async (accounts: string[]) => { 153 | if (process.env.TENDERLY === 'true') { 154 | return; 155 | } 156 | // eslint-disable-next-line no-restricted-syntax 157 | for (const account of accounts) { 158 | // eslint-disable-next-line no-await-in-loop 159 | await DRE.network.provider.request({ 160 | method: 'hardhat_impersonateAccount', 161 | params: [account], 162 | }); 163 | } 164 | }; 165 | -------------------------------------------------------------------------------- /helpers/tenderly-utils.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { DRE } from './misc-utils'; 3 | 4 | export const usingTenderly = () => 5 | DRE && 6 | ((DRE as HardhatRuntimeEnvironment).network.name.includes('tenderly') || 7 | process.env.TENDERLY === 'true'); 8 | 9 | export const setNewHead = (head: string) => { 10 | const net = DRE.tenderly.network(); 11 | net.setFork(process.env.TENDERLY_FORK_ID); 12 | net.setHead(head); 13 | const provider = new DRE.ethers.providers.Web3Provider(net); 14 | DRE.ethers.provider = provider; 15 | }; 16 | 17 | export const logError = () => { 18 | if (DRE.network.name.includes('tenderly')) { 19 | const transactionLink = `https://dashboard.tenderly.co/${DRE.config.tenderly.username}/${ 20 | DRE.config.tenderly.project 21 | }/fork/${DRE.tenderly.network().getFork()}/simulation/${DRE.tenderly.network().getHead()}`; 22 | console.error( 23 | '[TENDERLY] Transaction Reverted. Check TX simulation error at:', 24 | transactionLink 25 | ); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /helpers/types.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | 3 | export interface SymbolMap { 4 | [symbol: string]: T; 5 | } 6 | 7 | export type eNetwork = eEthereumNetwork | ePolygonNetwork | eXDaiNetwork | eAvalancheNetwork; 8 | 9 | export enum eContractid { 10 | DistributionManager = 'DistributionManager', 11 | AaveIncentivesController = 'AaveIncentivesController', 12 | MintableErc20 = 'MintableErc20', 13 | ATokenMock = 'ATokenMock', 14 | IERC20Detailed = 'IERC20Detailed', 15 | StakedTokenIncentivesController = 'StakedTokenIncentivesController', 16 | MockSelfDestruct = 'MockSelfDestruct', 17 | StakedAaveV3 = 'StakedAaveV3', 18 | PullRewardsIncentivesController = 'PullRewardsIncentivesController', 19 | } 20 | 21 | export enum eEthereumNetwork { 22 | buidlerevm = 'buidlerevm', 23 | kovan = 'kovan', 24 | ropsten = 'ropsten', 25 | main = 'main', 26 | coverage = 'coverage', 27 | hardhat = 'hardhat', 28 | tenderlyMain = 'tenderlyMain', 29 | } 30 | 31 | export enum ePolygonNetwork { 32 | matic = 'matic', 33 | mumbai = 'mumbai', 34 | } 35 | 36 | export enum eXDaiNetwork { 37 | xdai = 'xdai', 38 | } 39 | 40 | export enum eAvalancheNetwork { 41 | fuji = 'fuji', 42 | avalanche = 'avalanche', 43 | } 44 | 45 | export enum EthereumNetworkNames { 46 | kovan = 'kovan', 47 | ropsten = 'ropsten', 48 | main = 'main', 49 | matic = 'matic', 50 | mumbai = 'mumbai', 51 | xdai = 'xdai', 52 | } 53 | 54 | export type iParamsPerNetwork = 55 | | iEthereumParamsPerNetwork 56 | | iPolygonParamsPerNetwork 57 | | iXDaiParamsPerNetwork 58 | | iAvalancheParamsPerNetwork; 59 | 60 | export interface iEthereumParamsPerNetwork { 61 | [eEthereumNetwork.coverage]: T; 62 | [eEthereumNetwork.buidlerevm]: T; 63 | [eEthereumNetwork.kovan]: T; 64 | [eEthereumNetwork.ropsten]: T; 65 | [eEthereumNetwork.main]: T; 66 | [eEthereumNetwork.hardhat]: T; 67 | [eEthereumNetwork.tenderlyMain]: T; 68 | } 69 | 70 | export interface iPolygonParamsPerNetwork { 71 | [ePolygonNetwork.matic]: T; 72 | [ePolygonNetwork.mumbai]: T; 73 | } 74 | 75 | export interface iXDaiParamsPerNetwork { 76 | [eXDaiNetwork.xdai]: T; 77 | } 78 | 79 | export interface iAvalancheParamsPerNetwork { 80 | [eAvalancheNetwork.fuji]: T; 81 | [eAvalancheNetwork.avalanche]: T; 82 | } 83 | 84 | export type tEthereumAddress = string; 85 | export type tStringTokenBigUnits = string; // 1 ETH, or 10e6 USDC or 10e18 DAI 86 | export type tBigNumberTokenBigUnits = BigNumber; 87 | export type tStringTokenSmallUnits = string; // 1 wei, or 1 basic unit of USDC, or 1 basic unit of DAI 88 | export type tBigNumberTokenSmallUnits = BigNumber; 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "incentives-controller", 3 | "version": "1.0.0", 4 | "description": "Incentives controller for Aave protocol", 5 | "files": [ 6 | "contracts", 7 | "artifacts" 8 | ], 9 | "scripts": { 10 | "run-env": "npm i && tail -f /dev/null", 11 | "hardhat": "hardhat", 12 | "hardhat:kovan": "hardhat --network kovan", 13 | "hardhat:tenderly": "hardhat --network tenderly", 14 | "hardhat:ropsten": "hardhat--network ropsten", 15 | "hardhat:main": "hardhat --network main", 16 | "hardhat:docker": "hardhat --network hardhatevm_docker", 17 | "hardhat:mumbai": "hardhat --network mumbai", 18 | "hardhat:matic": "hardhat --network matic", 19 | "coverage": "hardhat coverage", 20 | "test": "npm run test-pull-rewards-incentives && npm run test-staked-incentives", 21 | "test-pull-rewards-incentives": "TS_NODE_TRANSPILE_ONLY=1 hardhat test ./test/__setup.spec.ts ./test/PullRewardsIncentivesController/*.spec.ts", 22 | "test-staked-incentives": "TS_NODE_TRANSPILE_ONLY=1 hardhat test ./test/__setup.spec.ts ./test/StakedIncentivesController/*.spec.ts", 23 | "test-proposal": "TS_NODE_TRANSPILE_ONLY=1 MAINNET_FORK=true hardhat test ./test-fork/incentivesProposal.spec.ts", 24 | "test-proposal:tenderly": "TS_NODE_TRANSPILE_ONLY=1 TENDERLY=true npm run hardhat:tenderly -- test ./test-fork/incentivesProposal.spec.ts", 25 | "test-proposal-skip": "TS_NODE_TRANSPILE_ONLY=1 MAINNET_FORK=true hardhat test ./test-fork/incentives-skip.spec.ts", 26 | "test-proposal-skip:tenderly": "TS_NODE_TRANSPILE_ONLY=1 TENDERLY=true npm run hardhat:tenderly -- test ./test-fork/incentives-skip.spec.ts", 27 | "spin-incentives:tenderly": "TENDERLY=true npx hardhat --network tenderly incentives-proposal:tenderly", 28 | "compile": "SKIP_LOAD=true hardhat compile", 29 | "console:fork": "MAINNET_FORK=true hardhat console", 30 | "prepublishOnly": "npm run compile", 31 | "deploy:incentives-controller-impl:main": "npm run hardhat:main -- deploy-incentives-impl", 32 | "submit-proposal:mainnet": "npx hardhat --network main incentives-submit-proposal:mainnet --defender --proposal-execution-payload 0x5778DAee2a634acd303dC9dC91e58D57C8FFfcC8 --a-tokens 0x7b2a3CF972C3193F26CdeC6217D27379b6417bD0,0xE994d6d8595741a6245bC3197fD66C10a3E75C5f,0x1C050bCa8BAbe53Ef769d0d2e411f556e1a27E7B,0x3F06560cfB7af6E6B5102c358f679DE5150b3b4C,0xC2fcab14Ec1F2dFA82a23C639c4770345085a50F,0x541dCd3F00Bcd1A683cc73E1b2A8693b602201f4 --variable-debt-tokens 0x3F87b818f94F3cC21e47FD3Bf015E8D8183A3E08,0x4aBF3e82D5f45A8D8E8C48B544bcA562e20EE2ff,0x1f57Cc62113C3a6346882DcF3Ed49120411ac2d2,0x99E81EDbcab512d393638C087fD29c3DC6c9B00E,0x52fdFB1157878f540DCB961561ce5F3b0bbe6f80,0xDddE1FA049209Bc24B69D5fa316a56EfeC918D79" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/aave/incentives-controller.git" 37 | }, 38 | "author": "David Racero", 39 | "license": "AGPLv3", 40 | "bugs": { 41 | "url": "https://github.com/aave/incentives-controller/issues" 42 | }, 43 | "homepage": "https://github.com/aave/incentives-controller#readme", 44 | "devDependencies": { 45 | "@aave/protocol-v2": "^1.0.2-fat.3", 46 | "@nomiclabs/hardhat-ethers": "^2.0.0", 47 | "@nomiclabs/hardhat-waffle": "^2.0.0", 48 | "@openzeppelin/contracts": "3.1.0", 49 | "@tenderly/hardhat-tenderly": "^1.1.0-beta.5", 50 | "@typechain/ethers-v5": "^6.0.5", 51 | "@typechain/hardhat": "^1.0.1", 52 | "@types/chai": "4.2.11", 53 | "@types/lowdb": "1.0.9", 54 | "@types/mocha": "7.0.2", 55 | "@types/node": "14.0.5", 56 | "bignumber.js": "9.0.0", 57 | "chai": "4.2.0", 58 | "chai-bignumber": "3.0.0", 59 | "chai-bn": "^0.2.1", 60 | "defender-relay-client": "^1.5.1", 61 | "dotenv": "^8.2.0", 62 | "eth-sig-util": "2.5.3", 63 | "ethereum-waffle": "3.0.2", 64 | "ethereumjs-util": "7.0.2", 65 | "ethers": "^5.0.19", 66 | "globby": "^11.0.1", 67 | "hardhat": "^2.1.2", 68 | "hardhat-gas-reporter": "^1.0.0", 69 | "husky": "^4.2.5", 70 | "is-ipfs": "^5.0.0", 71 | "kebab-case": "^1.0.1", 72 | "lowdb": "1.0.0", 73 | "prettier": "^2.0.5", 74 | "prettier-plugin-solidity": "^1.0.0-alpha.53", 75 | "pretty-quick": "^2.0.1", 76 | "solidity-coverage": "^0.7.16", 77 | "temp-hardhat-etherscan": "^2.0.2", 78 | "ts-generator": "^0.1.1", 79 | "ts-node": "^8.10.2", 80 | "tslint": "^6.1.2", 81 | "tslint-config-prettier": "^1.18.0", 82 | "tslint-plugin-prettier": "^2.3.0", 83 | "typechain": "^4.0.3", 84 | "typescript": "^4.0.5" 85 | }, 86 | "dependencies": { 87 | "@aave/aave-stake": "^1.0.4", 88 | "@aave/governance-v2": "^1.0.0", 89 | "hardhat-deploy": "^0.7.2" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) 2 | [![Build pass](https://github.com/aave/incentives-controller/actions/workflows/node.js.yml/badge.svg)](https://github.com/aave/incentives-controller/actions/workflows/node.js.yml) 3 | [![codecov](https://codecov.io/gh/aave/incentives-controller/branch/master/graph/badge.svg?token=DRFNLw506C)](https://codecov.io/gh/aave/incentives-controller) 4 | 5 | # Aave incentives 6 | 7 | ## Introduction 8 | 9 | This repo contains the code and implementation of the contracts used to activate the liquidity mining program on the main market of the Aave protocol. 10 | 11 | ## Implementation 12 | 13 | The rewards are distributed in the form of stkAAVE, which is obtained by staking Aave in the Safety Module Staking contract located at https://etherscan.io/address/0x4da27a545c0c5b758a6ba100e3a049001de870f5. 14 | 15 | The incentives controller contract, `StakedTokenIncentivesController`, stakes the rewards while transferring the obtained stkAAVE to the claiming user. 16 | 17 | The implementation logic is defined as follow: 18 | 19 | - Each asset has a defined `emissionPerSecond` 20 | - For each asset, an `assetIndex` keeps track of the total accumulated rewards 21 | - For each user, a `userIndex` keeps track of the user accumulated rewards 22 | - On `handleAction()`, that is triggered whenever an aToken/debt Token is minted/burned by a user, the `userIndex` and the `assetIndex` are accumulated depending on the time passed since the last action 23 | - At any point in time the user pending rewards can be queried through the `getRewardsBalance()` function 24 | -------------------------------------------------------------------------------- /tasks/deployment/deploy-atoken.ts: -------------------------------------------------------------------------------- 1 | import { Signer } from 'ethers'; 2 | import { task } from 'hardhat/config'; 3 | import { ZERO_ADDRESS } from '../../helpers/constants'; 4 | import { getDefenderRelaySigner } from '../../helpers/defender-utils'; 5 | import { ILendingPoolData__factory, IERC20Detailed__factory, AToken__factory } from '../../types'; 6 | 7 | task('deploy-atoken', 'Deploy AToken using prior reserve config') 8 | .addParam('pool') 9 | .addParam('asset') 10 | .addParam('treasury') 11 | .addParam('incentivesController') 12 | .addOptionalParam('tokenName') 13 | .addOptionalParam('tokenSymbol') 14 | .addFlag('defender') 15 | .setAction( 16 | async ( 17 | { defender, pool, asset, treasury, incentivesController, tokenName, tokenSymbol }, 18 | localBRE 19 | ) => { 20 | await localBRE.run('set-DRE'); 21 | 22 | let deployer: Signer; 23 | [deployer] = await localBRE.ethers.getSigners(); 24 | 25 | if (defender) { 26 | const { signer } = await getDefenderRelaySigner(); 27 | deployer = signer; 28 | } 29 | 30 | const { aTokenAddress } = await ILendingPoolData__factory.connect( 31 | pool, 32 | deployer 33 | ).getReserveData(asset); 34 | 35 | if (!tokenSymbol && aTokenAddress === ZERO_ADDRESS) { 36 | throw new Error( 37 | "Reserve does not exists or not initialized. Pass 'tokenSymbol' as param to the task.'" 38 | ); 39 | } 40 | if (!tokenName && aTokenAddress === ZERO_ADDRESS) { 41 | throw new Error( 42 | "Reserve does not exists or not initialized. Pass 'tokenName' as param to the task.'" 43 | ); 44 | } 45 | 46 | // Grab same name and symbol from old implementation 47 | if (!tokenName) { 48 | tokenName = await IERC20Detailed__factory.connect(aTokenAddress, deployer).name(); 49 | } 50 | if (!tokenSymbol) { 51 | tokenSymbol = await IERC20Detailed__factory.connect(aTokenAddress, deployer).symbol(); 52 | } 53 | 54 | const { address } = await new AToken__factory(deployer).deploy( 55 | pool, 56 | asset, 57 | treasury, 58 | tokenName, 59 | tokenSymbol, 60 | incentivesController 61 | ); 62 | 63 | return address; 64 | } 65 | ); 66 | -------------------------------------------------------------------------------- /tasks/deployment/deploy-incentives-impl.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config'; 2 | import { DefenderRelaySigner, DefenderRelayProvider } from 'defender-relay-client/lib/ethers'; 3 | import { deployAaveIncentivesController } from '../../helpers/contracts-accessors'; 4 | import { getDefenderRelaySigner } from '../../helpers/defender-utils'; 5 | 6 | // Mainnet addresses 7 | const AAVE_STAKE = '0x4da27a545c0c5B758a6BA100e3a049001de870f5'; 8 | const AAVE_SHORT_EXECUTOR = '0xee56e2b3d491590b5b31738cc34d5232f378a8d5'; 9 | 10 | task('deploy-incentives-impl', 'Incentives controller implementation deployment').setAction( 11 | async (_, localBRE) => { 12 | _; 13 | await localBRE.run('set-DRE'); 14 | 15 | const { signer } = await getDefenderRelaySigner(); 16 | const deployer = signer; 17 | 18 | const incentives = await deployAaveIncentivesController( 19 | [AAVE_STAKE, AAVE_SHORT_EXECUTOR], 20 | true, 21 | deployer 22 | ); 23 | console.log(`- Incentives implementation address ${incentives.address}`); 24 | 25 | return incentives.address; 26 | } 27 | ); 28 | -------------------------------------------------------------------------------- /tasks/deployment/deploy-reserve-implementations.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config'; 2 | import { 3 | AaveProtocolDataProvider__factory, 4 | ILendingPoolAddressesProvider__factory, 5 | } from '../../types'; 6 | 7 | task( 8 | 'deploy-reserve-implementations', 9 | 'Deploy AToken and Variable debt tokens using prior reserve config' 10 | ) 11 | .addParam('provider') 12 | .addParam('assets') 13 | .addParam('incentivesController') 14 | .addParam('treasury') 15 | .addFlag('defender') 16 | .setAction(async ({ defender, provider, assets, incentivesController, treasury }, localBRE) => { 17 | await localBRE.run('set-DRE'); 18 | const [deployer] = await localBRE.ethers.getSigners(); 19 | const tokensToUpdate = assets.split(','); 20 | // Return variables 21 | const aTokens: string[] = []; 22 | const variableDebtTokens: string[] = []; 23 | 24 | // Instances 25 | const poolProvider = await ILendingPoolAddressesProvider__factory.connect(provider, deployer); 26 | const protocolDataProvider = await AaveProtocolDataProvider__factory.connect( 27 | await poolProvider.getAddress( 28 | '0x0100000000000000000000000000000000000000000000000000000000000000' 29 | ), 30 | deployer 31 | ); 32 | 33 | // Params 34 | const pool = await poolProvider.getLendingPool(); 35 | 36 | const reserveConfigs = (await protocolDataProvider.getAllReservesTokens()) 37 | .filter(({ symbol }) => tokensToUpdate.includes(symbol)) 38 | .sort(({ symbol: a }, { symbol: b }) => a.localeCompare(b)); 39 | 40 | if (reserveConfigs.length != tokensToUpdate.length) { 41 | throw Error( 42 | "Reserves and assets missmatch. Check 'assets' task params to include all reserves" 43 | ); 44 | } 45 | 46 | for (let x = 0; x < reserveConfigs.length; x++) { 47 | aTokens[x] = await localBRE.run('deploy-atoken', { 48 | pool, 49 | asset: reserveConfigs[x].tokenAddress, 50 | treasury, 51 | incentivesController, 52 | defender, 53 | }); 54 | console.log(`- Deployed ${reserveConfigs[x].symbol} AToken impl at: ${aTokens[x]}`); 55 | variableDebtTokens[x] = await localBRE.run('deploy-var-debt-token', { 56 | pool, 57 | asset: reserveConfigs[x].tokenAddress, 58 | incentivesController, 59 | defender, 60 | }); 61 | console.log( 62 | `- Deployed ${reserveConfigs[x].symbol} Variable Debt Token impl at: ${variableDebtTokens[x]}` 63 | ); 64 | } 65 | 66 | return { 67 | aTokens, 68 | variableDebtTokens, 69 | }; 70 | }); 71 | -------------------------------------------------------------------------------- /tasks/deployment/deploy-variable-debt-token.ts: -------------------------------------------------------------------------------- 1 | import { Signer } from 'ethers/lib/ethers'; 2 | import { task } from 'hardhat/config'; 3 | import { ZERO_ADDRESS } from '../../helpers/constants'; 4 | import { getDefenderRelaySigner } from '../../helpers/defender-utils'; 5 | import { 6 | IERC20Detailed__factory, 7 | ILendingPoolData__factory, 8 | VariableDebtToken__factory, 9 | } from '../../types'; 10 | 11 | task('deploy-var-debt-token', 'Deploy AToken using prior reserve config') 12 | .addParam('pool') 13 | .addParam('asset') 14 | .addParam('incentivesController') 15 | .addOptionalParam('tokenName') 16 | .addOptionalParam('tokenSymbol') 17 | .addFlag('defender') 18 | .setAction( 19 | async ({ defender, pool, asset, incentivesController, tokenName, tokenSymbol }, localBRE) => { 20 | await localBRE.run('set-DRE'); 21 | 22 | let deployer: Signer; 23 | [deployer] = await localBRE.ethers.getSigners(); 24 | 25 | if (defender) { 26 | const { signer } = await getDefenderRelaySigner(); 27 | deployer = signer; 28 | } 29 | 30 | const { variableDebtTokenAddress } = await ILendingPoolData__factory.connect( 31 | pool, 32 | deployer 33 | ).getReserveData(asset); 34 | 35 | if (!tokenSymbol && variableDebtTokenAddress === ZERO_ADDRESS) { 36 | throw new Error( 37 | "Reserve does not exists or not initialized. Pass 'tokenSymbol' as param to the task.'" 38 | ); 39 | } 40 | if (!tokenName && variableDebtTokenAddress === ZERO_ADDRESS) { 41 | throw new Error( 42 | "Reserve does not exists or not initialized. Pass 'tokenName' as param to the task.'" 43 | ); 44 | } 45 | 46 | // Grab same name and symbol from old implementation 47 | if (!tokenName) { 48 | tokenName = await IERC20Detailed__factory.connect( 49 | variableDebtTokenAddress, 50 | deployer 51 | ).name(); 52 | } 53 | if (!tokenSymbol) { 54 | tokenSymbol = await IERC20Detailed__factory.connect( 55 | variableDebtTokenAddress, 56 | deployer 57 | ).symbol(); 58 | } 59 | 60 | const { address } = await new VariableDebtToken__factory(deployer).deploy( 61 | pool, 62 | asset, 63 | tokenName, 64 | tokenSymbol, 65 | incentivesController 66 | ); 67 | 68 | return address; 69 | } 70 | ); 71 | -------------------------------------------------------------------------------- /tasks/deployment/propose-incentives.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config'; 2 | import { config } from 'dotenv'; 3 | import { IAaveGovernanceV2__factory } from '../../types'; 4 | import { Signer } from 'ethers'; 5 | import { getDefenderRelaySigner } from '../../helpers/defender-utils'; 6 | import { DRE } from '../../helpers/misc-utils'; 7 | import { logError } from '../../helpers/tenderly-utils'; 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-var-requires 10 | const bs58 = require('bs58'); 11 | 12 | config(); 13 | 14 | task('propose-incentives', 'Create some proposals and votes') 15 | .addParam('proposalExecutionPayload') 16 | .addParam('aTokens') 17 | .addParam('variableDebtTokens') 18 | .addParam('aaveGovernance') 19 | .addParam('shortExecutor') 20 | .addFlag('defender') 21 | .setAction( 22 | async ( 23 | { 24 | aTokens, 25 | variableDebtTokens, 26 | aaveGovernance, 27 | shortExecutor, 28 | proposalExecutionPayload, 29 | defender, 30 | }, 31 | localBRE: any 32 | ) => { 33 | await localBRE.run('set-DRE'); 34 | 35 | let proposer: Signer; 36 | [proposer] = await localBRE.ethers.getSigners(); 37 | 38 | if (defender) { 39 | const { signer } = await getDefenderRelaySigner(); 40 | proposer = signer; 41 | } 42 | 43 | aTokens = aTokens.split(','); 44 | variableDebtTokens = variableDebtTokens.split(','); 45 | 46 | const callData = DRE.ethers.utils.defaultAbiCoder.encode( 47 | ['address[6]', 'address[6]'], 48 | [aTokens, variableDebtTokens] 49 | ); 50 | 51 | const executeSignature = 'execute(address[6],address[6])'; 52 | const gov = await IAaveGovernanceV2__factory.connect(aaveGovernance, proposer); 53 | const ipfsEncoded = '0xf7a1f565fcd7684fba6fea5d77c5e699653e21cb6ae25fbf8c5dbc8d694c7949'; 54 | 55 | try { 56 | const tx = await gov.create( 57 | shortExecutor, 58 | [proposalExecutionPayload], 59 | ['0'], 60 | [executeSignature], 61 | [callData], 62 | [true], 63 | ipfsEncoded, 64 | { gasLimit: 3000000 } 65 | ); 66 | console.log('- Proposal submitted to Governance'); 67 | await tx.wait(); 68 | } catch (error) { 69 | logError(); 70 | throw error; 71 | } 72 | 73 | console.log('Your Proposal has been submitted'); 74 | } 75 | ); 76 | -------------------------------------------------------------------------------- /tasks/migrations/deploy-pull-rewards-incentives.ts: -------------------------------------------------------------------------------- 1 | import { isAddress } from 'ethers/lib/utils'; 2 | import { task } from 'hardhat/config'; 3 | import { ZERO_ADDRESS } from '../../helpers/constants'; 4 | import { 5 | deployPullRewardsIncentivesController, 6 | deployInitializableAdminUpgradeabilityProxy, 7 | } from '../../helpers/contracts-accessors'; 8 | import { waitForTx } from '../../helpers/misc-utils'; 9 | 10 | task( 11 | `deploy-pull-rewards-incentives`, 12 | `Deploy and initializes the PullRewardsIncentivesController contract` 13 | ) 14 | .addFlag('verify') 15 | .addParam('rewardToken') 16 | .addParam('rewardsVault') 17 | .addParam('emissionManager') 18 | .addParam('proxyAdmin', `The address to be added as an Admin role at the Transparent Proxy.`) 19 | .setAction( 20 | async ({ verify, rewardToken, rewardsVault, emissionManager, proxyAdmin }, localBRE) => { 21 | await localBRE.run('set-DRE'); 22 | if (!isAddress(proxyAdmin)) { 23 | throw Error('Missing or incorrect admin param'); 24 | } 25 | if (!isAddress(rewardToken)) { 26 | throw Error('Missing or incorrect rewardToken param'); 27 | } 28 | if (!isAddress(rewardsVault)) { 29 | throw Error('Missing or incorrect rewardsVault param'); 30 | } 31 | emissionManager = isAddress(emissionManager) ? emissionManager : ZERO_ADDRESS; 32 | 33 | console.log(`[PullRewardsIncentivesController] Starting deployment:`); 34 | 35 | const incentivesControllerImpl = await deployPullRewardsIncentivesController( 36 | [rewardToken, emissionManager], 37 | verify 38 | ); 39 | console.log(` - Deployed implementation of PullRewardsIncentivesController`); 40 | 41 | const incentivesProxy = await deployInitializableAdminUpgradeabilityProxy(verify); 42 | console.log(` - Deployed proxy of PullRewardsIncentivesController`); 43 | 44 | const encodedParams = incentivesControllerImpl.interface.encodeFunctionData('initialize', [ 45 | rewardsVault, 46 | ]); 47 | 48 | await waitForTx( 49 | await incentivesProxy.functions['initialize(address,address,bytes)']( 50 | incentivesControllerImpl.address, 51 | proxyAdmin, 52 | encodedParams 53 | ) 54 | ); 55 | console.log(` - Initialized PullRewardsIncentivesController Proxy`); 56 | 57 | console.log(` - Finished PullRewardsIncentivesController deployment and initialization`); 58 | console.log(` - Proxy: ${incentivesProxy.address}`); 59 | console.log(` - Impl: ${incentivesControllerImpl.address}`); 60 | 61 | return { 62 | proxy: incentivesProxy.address, 63 | implementation: incentivesControllerImpl.address, 64 | }; 65 | } 66 | ); 67 | -------------------------------------------------------------------------------- /tasks/migrations/incentives-deploy-mainnet.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config'; 2 | import { DRE } from '../../helpers/misc-utils'; 3 | import { tEthereumAddress } from '../../helpers/types'; 4 | import { getReserveConfigs } from '../../test-fork/helpers'; 5 | import { ProposalIncentivesExecutor__factory, IERC20Detailed__factory } from '../../types'; 6 | import { ILendingPool } from '../../types/ILendingPool'; 7 | import { getDefenderRelaySigner } from '../../helpers/defender-utils'; 8 | import { Signer } from 'ethers'; 9 | import kebabCase from 'kebab-case'; 10 | 11 | const { 12 | RESERVES = 'DAI,GUSD,USDC,USDT,WBTC,WETH', 13 | POOL_PROVIDER = '0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5', 14 | POOL_DATA_PROVIDER = '0x057835Ad21a177dbdd3090bB1CAE03EaCF78Fc6d', 15 | AAVE_TOKEN = '0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9', 16 | TREASURY = '0x464c71f6c2f760dda6093dcb91c24c39e5d6e18c', 17 | AAVE_GOVERNANCE_V2 = '0xEC568fffba86c094cf06b22134B23074DFE2252c', // mainnet 18 | AAVE_SHORT_EXECUTOR = '0xee56e2b3d491590b5b31738cc34d5232f378a8d5', // mainnet 19 | } = process.env; 20 | 21 | const AAVE_LENDING_POOL = '0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9'; 22 | const INCENTIVES_PROXY = '0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5'; 23 | 24 | task( 25 | 'incentives-deploy:mainnet', 26 | 'Deploy the payload contract, atokens and variable debt token implementations. Print the params for submitting proposal' 27 | ) 28 | .addFlag('defender') 29 | .setAction(async ({ defender }, localBRE) => { 30 | let aTokensImpl: tEthereumAddress[]; 31 | let variableDebtTokensImpl: tEthereumAddress[]; 32 | let proposalExecutionPayload: tEthereumAddress; 33 | let symbols: { 34 | [key: string]: { 35 | aToken: { symbol: string; name: string }; 36 | variableDebtToken: { symbol: string; name: string }; 37 | }; 38 | } = {}; 39 | 40 | await localBRE.run('set-DRE'); 41 | 42 | let deployer: Signer; 43 | [deployer] = await DRE.ethers.getSigners(); 44 | 45 | if (defender) { 46 | const { signer } = await getDefenderRelaySigner(); 47 | deployer = signer; 48 | } 49 | 50 | const ethers = DRE.ethers; 51 | 52 | const incentivesProxy = INCENTIVES_PROXY; 53 | 54 | if ( 55 | !RESERVES || 56 | !POOL_DATA_PROVIDER || 57 | !AAVE_TOKEN || 58 | !AAVE_GOVERNANCE_V2 || 59 | !AAVE_SHORT_EXECUTOR || 60 | !TREASURY 61 | ) { 62 | throw new Error('You have not set correctly the .env file, make sure to read the README.md'); 63 | } 64 | 65 | console.log('- Deploying aTokens and Variable Debt Tokens implementations'); 66 | 67 | // Deploy aTokens and debt tokens 68 | const { aTokens, variableDebtTokens } = await DRE.run('deploy-reserve-implementations', { 69 | provider: POOL_PROVIDER, 70 | assets: RESERVES, 71 | incentivesController: incentivesProxy, 72 | treasury: TREASURY, 73 | defender: true, 74 | }); 75 | 76 | aTokensImpl = [...aTokens]; 77 | variableDebtTokensImpl = [...variableDebtTokens]; 78 | 79 | // Deploy Proposal Executor Payload 80 | const { 81 | address: proposalExecutionPayloadAddress, 82 | } = await new ProposalIncentivesExecutor__factory(deployer).deploy(); 83 | proposalExecutionPayload = proposalExecutionPayloadAddress; 84 | 85 | console.log('Deployed ProposalIncentivesExecutor at:', proposalExecutionPayloadAddress); 86 | 87 | console.log('- Finished deployment script'); 88 | 89 | console.log('=== INFO ==='); 90 | console.log('Proposal payload:', proposalExecutionPayloadAddress); 91 | console.log('Incentives Controller proxy:', incentivesProxy); 92 | console.log( 93 | 'Needed params to submit the proposal at the following task: ', 94 | '$ npx hardhat --network main incentives-submit-proposal:mainnet' 95 | ); 96 | const proposalParams = { 97 | proposalExecutionPayload, 98 | aTokens: aTokensImpl.join(','), 99 | variableDebtTokens: variableDebtTokensImpl.join(','), 100 | }; 101 | console.log( 102 | `--defender `, 103 | Object.keys(proposalParams) 104 | .map((str) => `--${kebabCase(str)} ${proposalParams[str]}`) 105 | .join(' ') 106 | ); 107 | 108 | await DRE.run('verify-proposal-etherscan', { 109 | assets: RESERVES, 110 | aTokens: aTokensImpl.join(','), 111 | variableDebtTokens: variableDebtTokensImpl.join(','), 112 | proposalPayloadAddress: proposalExecutionPayloadAddress, 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /tasks/migrations/incentives-submit-proposal-mainnet.ts: -------------------------------------------------------------------------------- 1 | import { formatEther } from 'ethers/lib/utils'; 2 | import { task } from 'hardhat/config'; 3 | import { DRE, impersonateAccountsHardhat, latestBlock } from '../../helpers/misc-utils'; 4 | import { IERC20__factory, IGovernancePowerDelegationToken__factory } from '../../types'; 5 | import { IAaveGovernanceV2 } from '../../types/IAaveGovernanceV2'; 6 | import { getDefenderRelaySigner } from '../../helpers/defender-utils'; 7 | import isIPFS from 'is-ipfs'; 8 | import { Signer } from '@ethersproject/abstract-signer'; 9 | 10 | const { 11 | AAVE_TOKEN = '0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9', 12 | AAVE_GOVERNANCE_V2 = '0xEC568fffba86c094cf06b22134B23074DFE2252c', // mainnet 13 | AAVE_SHORT_EXECUTOR = '0xee56e2b3d491590b5b31738cc34d5232f378a8d5', // mainnet 14 | } = process.env; 15 | 16 | task('incentives-submit-proposal:mainnet', 'Submit the incentives proposal to Aave Governance') 17 | .addParam('proposalExecutionPayload') 18 | .addParam('aTokens') 19 | .addParam('variableDebtTokens') 20 | .addFlag('defender') 21 | .setAction( 22 | async ({ defender, proposalExecutionPayload, aTokens, variableDebtTokens }, localBRE) => { 23 | await localBRE.run('set-DRE'); 24 | let proposer: Signer; 25 | [proposer] = await DRE.ethers.getSigners(); 26 | 27 | if (defender) { 28 | const { signer } = await getDefenderRelaySigner(); 29 | proposer = signer; 30 | } 31 | 32 | if (!AAVE_TOKEN || !AAVE_GOVERNANCE_V2 || !AAVE_SHORT_EXECUTOR) { 33 | throw new Error( 34 | 'You have not set correctly the .env file, make sure to read the README.md' 35 | ); 36 | } 37 | 38 | if (aTokens.split(',').length !== 6) { 39 | throw new Error('aTokens input param should have 6 elements'); 40 | } 41 | 42 | if (variableDebtTokens.split(',').length !== 6) { 43 | throw new Error('variable debt token param should have 6 elements'); 44 | } 45 | 46 | const proposerAddress = await proposer.getAddress(); 47 | 48 | // Initialize contracts and tokens 49 | const gov = (await DRE.ethers.getContractAt( 50 | 'IAaveGovernanceV2', 51 | AAVE_GOVERNANCE_V2, 52 | proposer 53 | )) as IAaveGovernanceV2; 54 | 55 | const aave = IERC20__factory.connect(AAVE_TOKEN, proposer); 56 | 57 | // Balance and proposal power check 58 | const balance = await aave.balanceOf(proposerAddress); 59 | const priorBlock = ((await latestBlock()) - 1).toString(); 60 | const aaveGovToken = IGovernancePowerDelegationToken__factory.connect(AAVE_TOKEN, proposer); 61 | const propositionPower = await aaveGovToken.getPowerAtBlock(proposerAddress, priorBlock, '1'); 62 | 63 | console.log('- AAVE Balance proposer', formatEther(balance)); 64 | console.log( 65 | `- Proposition power of ${proposerAddress} at block: ${priorBlock}`, 66 | formatEther(propositionPower) 67 | ); 68 | 69 | // Submit proposal 70 | const proposalId = await gov.getProposalsCount(); 71 | const proposalParams = { 72 | proposalExecutionPayload, 73 | aTokens, 74 | variableDebtTokens, 75 | aaveGovernance: AAVE_GOVERNANCE_V2, 76 | shortExecutor: AAVE_SHORT_EXECUTOR, 77 | defender: true, 78 | }; 79 | console.log('- Submitting proposal with following params:'); 80 | console.log(JSON.stringify(proposalParams, null, 2)); 81 | 82 | await DRE.run('propose-incentives', proposalParams); 83 | console.log('- Proposal Submited:', proposalId.toString()); 84 | } 85 | ); 86 | -------------------------------------------------------------------------------- /tasks/migrations/incentives-submit-proposal-tenderly.ts: -------------------------------------------------------------------------------- 1 | import { formatEther, parseEther } from 'ethers/lib/utils'; 2 | import { task } from 'hardhat/config'; 3 | import { advanceBlockTo, DRE, increaseTime, latestBlock } from '../../helpers/misc-utils'; 4 | import { IERC20__factory, IGovernancePowerDelegationToken__factory } from '../../types'; 5 | import { IAaveGovernanceV2 } from '../../types/IAaveGovernanceV2'; 6 | import { getDefenderRelaySigner } from '../../helpers/defender-utils'; 7 | import isIPFS from 'is-ipfs'; 8 | import { Signer } from '@ethersproject/abstract-signer'; 9 | import { logError } from '../../helpers/tenderly-utils'; 10 | 11 | const { 12 | AAVE_TOKEN = '0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9', 13 | AAVE_GOVERNANCE_V2 = '0xEC568fffba86c094cf06b22134B23074DFE2252c', // mainnet 14 | AAVE_SHORT_EXECUTOR = '0xee56e2b3d491590b5b31738cc34d5232f378a8d5', // mainnet 15 | } = process.env; 16 | const VOTING_DURATION = 19200; 17 | 18 | const AAVE_WHALE = '0x25f2226b597e8f9514b3f68f00f494cf4f286491'; 19 | 20 | task('incentives-submit-proposal:tenderly', 'Submit the incentives proposal to Aave Governance') 21 | .addParam('proposalExecutionPayload') 22 | .addParam('aTokens') 23 | .addParam('variableDebtTokens') 24 | .addFlag('defender') 25 | .setAction( 26 | async ({ defender, proposalExecutionPayload, aTokens, variableDebtTokens }, localBRE) => { 27 | await localBRE.run('set-DRE'); 28 | let proposer: Signer; 29 | [proposer] = await DRE.ethers.getSigners(); 30 | 31 | const { signer } = await getDefenderRelaySigner(); 32 | proposer = signer; 33 | 34 | const whale = DRE.ethers.provider.getSigner(AAVE_WHALE); 35 | const aave = IERC20__factory.connect(AAVE_TOKEN, whale); 36 | 37 | // Transfer enough AAVE to proposer 38 | await (await aave.transfer(await proposer.getAddress(), parseEther('2000000'))).wait(); 39 | 40 | if (!AAVE_TOKEN || !AAVE_GOVERNANCE_V2 || !AAVE_SHORT_EXECUTOR) { 41 | throw new Error( 42 | 'You have not set correctly the .env file, make sure to read the README.md' 43 | ); 44 | } 45 | 46 | if (aTokens.split(',').length !== 6) { 47 | throw new Error('aTokens input param should have 6 elements'); 48 | } 49 | 50 | if (variableDebtTokens.split(',').length !== 6) { 51 | throw new Error('variable debt token param should have 6 elements'); 52 | } 53 | 54 | const proposerAddress = await proposer.getAddress(); 55 | 56 | // Initialize contracts and tokens 57 | const gov = (await DRE.ethers.getContractAt( 58 | 'IAaveGovernanceV2', 59 | AAVE_GOVERNANCE_V2, 60 | proposer 61 | )) as IAaveGovernanceV2; 62 | 63 | // Balance and proposal power check 64 | const balance = await aave.balanceOf(proposerAddress); 65 | const priorBlock = ((await latestBlock()) - 1).toString(); 66 | const aaveGovToken = IGovernancePowerDelegationToken__factory.connect(AAVE_TOKEN, proposer); 67 | const propositionPower = await aaveGovToken.getPowerAtBlock(proposerAddress, priorBlock, '1'); 68 | 69 | console.log('- AAVE Balance proposer', formatEther(balance)); 70 | console.log( 71 | `- Proposition power of ${proposerAddress} at block: ${priorBlock}`, 72 | formatEther(propositionPower) 73 | ); 74 | 75 | // Submit proposal 76 | const proposalId = await gov.getProposalsCount(); 77 | const proposalParams = { 78 | proposalExecutionPayload, 79 | aTokens, 80 | variableDebtTokens, 81 | aaveGovernance: AAVE_GOVERNANCE_V2, 82 | shortExecutor: AAVE_SHORT_EXECUTOR, 83 | defender: true, 84 | }; 85 | console.log('- Submitting proposal with following params:'); 86 | console.log(JSON.stringify(proposalParams, null, 2)); 87 | 88 | await DRE.run('propose-incentives', proposalParams); 89 | console.log('- Proposal Submited:', proposalId.toString()); 90 | 91 | // Mine block due flash loan voting protection 92 | await advanceBlockTo((await latestBlock()) + 1); 93 | 94 | // Submit vote and advance block to Queue phase 95 | try { 96 | console.log('Submitting vote...'); 97 | await (await gov.submitVote(proposalId, true)).wait(); 98 | console.log('Voted'); 99 | } catch (error) { 100 | logError(); 101 | throw error; 102 | } 103 | 104 | await advanceBlockTo((await latestBlock()) + VOTING_DURATION + 1); 105 | 106 | try { 107 | // Queue and advance block to Execution phase 108 | console.log('Queueing'); 109 | await (await gov.queue(proposalId, { gasLimit: 3000000 })).wait(); 110 | console.log('Queued'); 111 | } catch (error) { 112 | logError(); 113 | throw error; 114 | } 115 | await increaseTime(86400 + 10); 116 | 117 | // Execute payload 118 | 119 | try { 120 | console.log('Executing'); 121 | await (await gov.execute(proposalId, { gasLimit: 6000000 })).wait(); 122 | } catch (error) { 123 | logError(); 124 | throw error; 125 | } 126 | console.log('Proposal executed'); 127 | } 128 | ); 129 | -------------------------------------------------------------------------------- /tasks/migrations/tenderly-execute-proposal-fork.ts: -------------------------------------------------------------------------------- 1 | import { formatEther, parseEther } from 'ethers/lib/utils'; 2 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; 3 | import { JsonRpcSigner } from '@ethersproject/providers'; 4 | import { task } from 'hardhat/config'; 5 | import { DRE, advanceBlockTo, latestBlock, increaseTime } from '../../helpers/misc-utils'; 6 | import { tEthereumAddress } from '../../helpers/types'; 7 | import { IERC20__factory, IGovernancePowerDelegationToken__factory } from '../../types'; 8 | import { IAaveGovernanceV2 } from '../../types/IAaveGovernanceV2'; 9 | import { logError } from '../../helpers/tenderly-utils'; 10 | import { getDefenderRelaySigner } from '../../helpers/defender-utils'; 11 | import { Signer } from 'ethers'; 12 | 13 | const { 14 | AAVE_TOKEN = '0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9', 15 | AAVE_GOVERNANCE_V2 = '0xEC568fffba86c094cf06b22134B23074DFE2252c', 16 | } = process.env; 17 | 18 | const VOTING_DURATION = 18200; 19 | 20 | const AAVE_WHALE = '0x25f2226b597e8f9514b3f68f00f494cf4f286491'; 21 | 22 | const INCENTIVES_PROXY = '0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5'; 23 | 24 | task('execute-proposal:tenderly', 'Spin a tenderly fork with incentives activated') 25 | .addParam('proposalId') 26 | .setAction(async ({ proposalId }, localBRE) => { 27 | let ethers; 28 | let whale: JsonRpcSigner; 29 | let proposer: Signer; 30 | let gov: IAaveGovernanceV2; 31 | 32 | await localBRE.run('set-DRE'); 33 | 34 | if (!localBRE.network.name.includes('tenderly')) { 35 | console.error('You must connect to tenderly via --network tenderly to use this task.'); 36 | throw Error('tenderly-network-missing'); 37 | } 38 | 39 | const [signer] = await DRE.ethers.getSigners(); 40 | 41 | proposer = signer; 42 | 43 | const proposerAddress = await proposer.getAddress(); 44 | 45 | ethers = DRE.ethers; 46 | 47 | // Impersonating holders 48 | whale = ethers.provider.getSigner(AAVE_WHALE); 49 | 50 | // Initialize contracts and tokens 51 | gov = (await ethers.getContractAt( 52 | 'IAaveGovernanceV2', 53 | AAVE_GOVERNANCE_V2, 54 | whale 55 | )) as IAaveGovernanceV2; 56 | 57 | // Mine block due flash loan voting protection 58 | await advanceBlockTo((await latestBlock()) + 100); 59 | 60 | // Submit vote and advance block to Queue phase 61 | await (await gov.submitVote(proposalId, true)).wait(); 62 | 63 | await advanceBlockTo((await latestBlock()) + VOTING_DURATION + 1); 64 | 65 | try { 66 | // Queue and advance block to Execution phase 67 | await (await gov.queue(proposalId, { gasLimit: 3000000 })).wait(); 68 | } catch (error) { 69 | logError(); 70 | throw error; 71 | } 72 | 73 | await increaseTime(86400 + 10); 74 | 75 | // Execute payload 76 | await (await gov.execute(proposalId)).wait(); 77 | console.log('Proposal executed'); 78 | }); 79 | -------------------------------------------------------------------------------- /tasks/misc/set-DRE.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config'; 2 | import { DRE, setDRE } from '../../helpers/misc-utils'; 3 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 4 | import { formatEther } from 'ethers/lib/utils'; 5 | 6 | task(`set-DRE`, `Inits the DRE, to have access to all the plugins' objects`).setAction( 7 | async (_, _DRE) => { 8 | if (DRE) { 9 | return; 10 | } 11 | if ( 12 | (_DRE as HardhatRuntimeEnvironment).network.name.includes('tenderly') || 13 | process.env.TENDERLY === 'true' 14 | ) { 15 | console.log('- Setting up Tenderly provider'); 16 | const net = _DRE.tenderly.network(); 17 | 18 | if (process.env.TENDERLY_FORK_ID && process.env.TENDERLY_HEAD_ID) { 19 | console.log('- Connecting to a Tenderly Fork'); 20 | await net.setFork(process.env.TENDERLY_FORK_ID); 21 | await net.setHead(process.env.TENDERLY_HEAD_ID); 22 | } else { 23 | console.log('- Creating a new Tenderly Fork'); 24 | await net.initializeFork(); 25 | } 26 | const provider = new _DRE.ethers.providers.Web3Provider(net); 27 | _DRE.ethers.provider = provider; 28 | console.log('- Initialized Tenderly fork:'); 29 | console.log(' - Fork: ', net.getFork()); 30 | console.log(' - Head: ', net.getHead()); 31 | console.log(' - First account:', await (await _DRE.ethers.getSigners())[0].getAddress()); 32 | console.log( 33 | ' - Balance:', 34 | formatEther(await (await _DRE.ethers.getSigners())[0].getBalance()) 35 | ); 36 | } 37 | 38 | setDRE(_DRE); 39 | return _DRE; 40 | } 41 | ); 42 | -------------------------------------------------------------------------------- /tasks/misc/verify-proposal-etherscan.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config'; 2 | import { 3 | AaveProtocolDataProvider__factory, 4 | IERC20Detailed__factory, 5 | ILendingPool, 6 | ILendingPoolAddressesProvider__factory, 7 | ILendingPoolData__factory, 8 | } from '../../types'; 9 | import { verifyContract } from '../../helpers/etherscan-verification'; 10 | 11 | const { 12 | POOL_PROVIDER = '0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5', 13 | TREASURY = '0x464c71f6c2f760dda6093dcb91c24c39e5d6e18c', 14 | } = process.env; 15 | 16 | if (!POOL_PROVIDER || !TREASURY) { 17 | throw new Error('You have not set correctly the .env file, make sure to read the README.md'); 18 | } 19 | 20 | const AAVE_LENDING_POOL = '0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9'; 21 | const INCENTIVES_PROXY = '0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5'; 22 | 23 | task('verify-proposal-etherscan', 'Verify proposals') 24 | .addParam('assets') 25 | .addParam('aTokens') 26 | .addParam('variableDebtTokens') 27 | .addParam('proposalPayloadAddress') 28 | .setAction(async ({ assets, aTokens, variableDebtTokens, proposalPayloadAddress }, localBRE) => { 29 | await localBRE.run('set-DRE'); 30 | const [deployer] = await localBRE.ethers.getSigners(); 31 | const tokensToUpdate = assets.split(','); 32 | aTokens = aTokens.split(','); 33 | variableDebtTokens = variableDebtTokens.split(','); 34 | 35 | // Instances 36 | const pool = (await localBRE.ethers.getContractAt( 37 | 'ILendingPool', 38 | AAVE_LENDING_POOL, 39 | deployer 40 | )) as ILendingPool; 41 | 42 | const poolProvider = await ILendingPoolAddressesProvider__factory.connect( 43 | POOL_PROVIDER, 44 | deployer 45 | ); 46 | const protocolDataProvider = await AaveProtocolDataProvider__factory.connect( 47 | await poolProvider.getAddress( 48 | '0x0100000000000000000000000000000000000000000000000000000000000000' 49 | ), 50 | deployer 51 | ); 52 | 53 | // Params 54 | const reserveConfigs = (await protocolDataProvider.getAllReservesTokens()) 55 | .filter(({ symbol }) => tokensToUpdate.includes(symbol)) 56 | .sort(({ symbol: a }, { symbol: b }) => a.localeCompare(b)); 57 | 58 | if (reserveConfigs.length != tokensToUpdate.length) { 59 | throw Error( 60 | "Reserves and assets missmatch. Check 'assets' task params to include all reserves" 61 | ); 62 | } 63 | console.log('==== Etherscan verification ===='); 64 | console.log('- Verify proposal payload'); 65 | await verifyContract(proposalPayloadAddress, []); 66 | console.log('- Verify aTokens'); 67 | 68 | // Params 69 | for (let x = 0; x < reserveConfigs.length; x++) { 70 | const { tokenAddress } = reserveConfigs[x]; 71 | console.log(`- Verifying ${reserveConfigs[x].symbol} aToken implementation at ${aTokens[x]}`); 72 | const { aTokenAddress, variableDebtTokenAddress } = await pool.getReserveData(tokenAddress); 73 | 74 | const aTokenName = await IERC20Detailed__factory.connect(aTokenAddress, deployer).name(); 75 | const aTokenSymbol = await IERC20Detailed__factory.connect(aTokenAddress, deployer).symbol(); 76 | 77 | await verifyContract(aTokens[x], [ 78 | AAVE_LENDING_POOL, 79 | reserveConfigs[x].tokenAddress, 80 | TREASURY, 81 | aTokenName, 82 | aTokenSymbol, 83 | INCENTIVES_PROXY, 84 | ]); 85 | console.log( 86 | `- Verifying ${reserveConfigs[x].symbol} variable debt implementation at ${variableDebtTokens[x]}` 87 | ); 88 | 89 | const varTokenName = await IERC20Detailed__factory.connect( 90 | variableDebtTokenAddress, 91 | deployer 92 | ).name(); 93 | const varTokenSymbol = await IERC20Detailed__factory.connect( 94 | variableDebtTokenAddress, 95 | deployer 96 | ).symbol(); 97 | 98 | await verifyContract(variableDebtTokens[x], [ 99 | AAVE_LENDING_POOL, 100 | reserveConfigs[x].tokenAddress, 101 | varTokenName, 102 | varTokenSymbol, 103 | INCENTIVES_PROXY, 104 | ]); 105 | } 106 | }); 107 | -------------------------------------------------------------------------------- /tasks/misc/verify-sc.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config'; 2 | import { verifyContract, checkVerification } from '../../helpers/etherscan-verification'; 3 | 4 | interface VerifyParams { 5 | contractName: string; 6 | address: string; 7 | constructorArguments: string[]; 8 | libraries: string; 9 | } 10 | 11 | task('verify-sc', 'Inits the DRE, to have access to all the plugins') 12 | .addParam('address', 'Ethereum address of the smart contract') 13 | .addOptionalParam( 14 | 'libraries', 15 | 'Stringified JSON object in format of {library1: "0x2956356cd2a2bf3202f771f50d3d14a367b48071"}' 16 | ) 17 | .addOptionalVariadicPositionalParam( 18 | 'constructorArguments', 19 | 'arguments for contract constructor', 20 | [] 21 | ) 22 | .setAction(async ({ address, constructorArguments = [], libraries }: VerifyParams, localBRE) => { 23 | await localBRE.run('set-DRE'); 24 | 25 | checkVerification(); 26 | 27 | const result = await verifyContract(address, constructorArguments, libraries); 28 | return result; 29 | }); 30 | -------------------------------------------------------------------------------- /test-fork/helpers.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 2 | import { tEthereumAddress } from '../helpers/types'; 3 | import { 4 | AaveProtocolDataProvider__factory, 5 | AToken__factory, 6 | IERC20__factory, 7 | ILendingPoolAddressesProvider__factory, 8 | } from '../types'; 9 | import { expect } from 'chai'; 10 | import { parseUnits } from 'ethers/lib/utils'; 11 | import { MAX_UINT_AMOUNT } from '../helpers/constants'; 12 | import { DRE } from '../helpers/misc-utils'; 13 | import { ILendingPool } from '../types/ILendingPool'; 14 | import { Signer } from 'ethers/lib/ethers'; 15 | 16 | export const spendList = { 17 | DAI: { 18 | holder: '0x72aabd13090af25dbb804f84de6280c697ed1150', 19 | transfer: '1000', 20 | deposit: '100', 21 | decimals: '18', 22 | }, 23 | GUSD: { 24 | holder: '0x3e6722f32cbe5b3c7bd3dca7017c7ffe1b9e5a2a', 25 | transfer: '1000', 26 | deposit: '100', 27 | decimals: '2', 28 | }, 29 | USDC: { 30 | holder: '0xAe2D4617c862309A3d75A0fFB358c7a5009c673F', 31 | transfer: '1000', 32 | deposit: '100', 33 | decimals: '6', 34 | }, 35 | USDT: { 36 | holder: '0x9f57dc21f521c64204b6190c3076a05b559b1a28', 37 | transfer: '1000', 38 | deposit: '100', 39 | decimals: '6', 40 | }, 41 | WBTC: { 42 | holder: '0x6dab3bcbfb336b29d06b9c793aef7eaa57888922', 43 | transfer: '1', 44 | deposit: '0.5', 45 | decimals: '8', 46 | }, 47 | WETH: { 48 | holder: '0x0f4ee9631f4be0a63756515141281a3e2b293bbe', 49 | transfer: '1', 50 | deposit: '0.5', 51 | decimals: '18', 52 | }, 53 | }; 54 | 55 | export const getReserveConfigs = async ( 56 | poolProviderAddress: tEthereumAddress, 57 | reserves: string, 58 | proposer: Signer 59 | ) => { 60 | const poolProvider = await ILendingPoolAddressesProvider__factory.connect( 61 | poolProviderAddress, 62 | proposer 63 | ); 64 | const protocolDataProvider = await AaveProtocolDataProvider__factory.connect( 65 | await poolProvider.getAddress( 66 | '0x0100000000000000000000000000000000000000000000000000000000000000' 67 | ), 68 | proposer 69 | ); 70 | 71 | const reservesConfigs = (await protocolDataProvider.getAllReservesTokens()) 72 | .filter(({ symbol }) => reserves.includes(symbol)) 73 | .sort(({ symbol: a }, { symbol: b }) => a.localeCompare(b)); 74 | 75 | expect(reservesConfigs.length).to.be.eq(6); 76 | return reservesConfigs; 77 | }; 78 | 79 | export const fullCycleLendingPool = async ( 80 | symbol: string, 81 | tokenAddress: string, 82 | proposer: SignerWithAddress, 83 | pool: ILendingPool 84 | ) => { 85 | const { aTokenAddress, variableDebtTokenAddress } = await pool.getReserveData(tokenAddress); 86 | const reserve = IERC20__factory.connect(tokenAddress, proposer); 87 | const aToken = AToken__factory.connect(aTokenAddress, proposer); 88 | const holderSigner = DRE.ethers.provider.getSigner(spendList[symbol].holder); 89 | 90 | // Transfer assets to proposer from reserve holder 91 | await ( 92 | await reserve 93 | .connect(holderSigner) 94 | .transfer( 95 | proposer.address, 96 | parseUnits(spendList[symbol].transfer, spendList[symbol].decimals) 97 | ) 98 | ).wait(); 99 | 100 | // Amounts 101 | const depositAmount = parseUnits(spendList[symbol].deposit, spendList[symbol].decimals); 102 | const borrowAmount = depositAmount.div('10'); 103 | 104 | // Deposit to LendingPool 105 | await (await reserve.connect(proposer).approve(pool.address, depositAmount)).wait(); 106 | const tx1 = await pool 107 | .connect(proposer) 108 | .deposit(reserve.address, depositAmount, proposer.address, 0); 109 | await tx1.wait(); 110 | expect(tx1).to.emit(pool, 'Deposit'); 111 | 112 | // Request loan to LendingPool 113 | const tx2 = await pool.borrow(reserve.address, borrowAmount, '2', '0', proposer.address); 114 | await tx2.wait(); 115 | expect(tx2).to.emit(pool, 'Borrow'); 116 | 117 | // Repay variable loan to LendingPool 118 | await (await reserve.connect(proposer).approve(pool.address, MAX_UINT_AMOUNT)).wait(); 119 | const tx3 = await pool.repay(reserve.address, MAX_UINT_AMOUNT, '2', proposer.address); 120 | await tx3.wait(); 121 | expect(tx3).to.emit(pool, 'Repay'); 122 | 123 | // Withdraw from LendingPool 124 | const priorBalance = await reserve.balanceOf(proposer.address); 125 | await (await aToken.connect(proposer).approve(pool.address, MAX_UINT_AMOUNT)).wait(); 126 | const tx4 = await pool.withdraw(reserve.address, MAX_UINT_AMOUNT, proposer.address); 127 | await tx4.wait(); 128 | expect(tx4).to.emit(pool, 'Withdraw'); 129 | 130 | const afterBalance = await reserve.balanceOf(proposer.address); 131 | expect(await aToken.balanceOf(proposer.address)).to.be.eq('0'); 132 | expect(afterBalance).to.be.gt(priorBalance); 133 | }; 134 | -------------------------------------------------------------------------------- /test-wallets.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import {WAD} from './helpers/constants'; 3 | 4 | const toWad = (value: string) => new BigNumber(value).times(WAD).toFixed(); 5 | 6 | export const accounts: {secretKey: string; balance: string}[] = [ 7 | { 8 | secretKey: '0xc5e8f61d1ab959b397eecc0a37a6517b8e67a0e7cf1f4bce5591f3ed80199122', 9 | balance: toWad('1000000'), 10 | }, 11 | { 12 | secretKey: '0xd49743deccbccc5dc7baa8e69e5be03298da8688a15dd202e20f15d5e0e9a9fb', 13 | balance: toWad('1000000'), 14 | }, 15 | { 16 | secretKey: '0x23c601ae397441f3ef6f1075dcb0031ff17fb079837beadaf3c84d96c6f3e569', 17 | balance: toWad('1000000'), 18 | }, 19 | { 20 | secretKey: '0xee9d129c1997549ee09c0757af5939b2483d80ad649a0eda68e8b0357ad11131', 21 | balance: toWad('1000000'), 22 | }, 23 | { 24 | secretKey: '0x87630b2d1de0fbd5044eb6891b3d9d98c34c8d310c852f98550ba774480e47cc', 25 | balance: toWad('1000000'), 26 | }, 27 | { 28 | secretKey: '0x275cc4a2bfd4f612625204a20a2280ab53a6da2d14860c47a9f5affe58ad86d4', 29 | balance: toWad('1000000'), 30 | }, 31 | { 32 | secretKey: '0xaee25d55ce586148a853ca83fdfacaf7bc42d5762c6e7187e6f8e822d8e6a650', 33 | balance: toWad('1000000'), 34 | }, 35 | { 36 | secretKey: '0xa2e0097c961c67ec197b6865d7ecea6caffc68ebeb00e6050368c8f67fc9c588', 37 | balance: toWad('1000000'), 38 | }, 39 | ]; 40 | -------------------------------------------------------------------------------- /test/DistributionManager/data-helpers/asset-data.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from 'ethers'; 2 | import { comparatorEngine, CompareRules } from '../../helpers/comparator-engine'; 3 | import { getNormalizedDistribution } from '../../helpers/ray-math'; 4 | import { AaveDistributionManager } from '../../../types/AaveDistributionManager'; 5 | import { PullRewardsIncentivesController, StakedTokenIncentivesController } from '../../../types'; 6 | 7 | export type AssetUpdateData = { 8 | emissionPerSecond: BigNumberish; 9 | totalStaked: BigNumberish; 10 | underlyingAsset: string; 11 | }; 12 | export type AssetData = { 13 | emissionPerSecond: BigNumber; 14 | index: BigNumber; 15 | lastUpdateTimestamp: BigNumber; 16 | }; 17 | 18 | export async function getAssetsData( 19 | peiContract: 20 | | AaveDistributionManager 21 | | StakedTokenIncentivesController 22 | | PullRewardsIncentivesController, 23 | assets: string[] 24 | ) { 25 | return await Promise.all( 26 | assets.map(async (underlyingAsset) => { 27 | const response = await peiContract.getAssetData(underlyingAsset); 28 | return { 29 | emissionPerSecond: response[1], 30 | lastUpdateTimestamp: response[2], 31 | index: response[0], 32 | underlyingAsset, 33 | }; 34 | }) 35 | ); 36 | } 37 | 38 | export function assetDataComparator< 39 | Input extends { underlyingAsset: string; totalStaked: BigNumberish }, 40 | State extends AssetData 41 | >( 42 | assetConfigUpdateInput: Input, 43 | assetConfigBefore: State, 44 | assetConfigAfter: State, 45 | actionBlockTimestamp: number, 46 | emissionEndTimestamp: number, 47 | compareRules: CompareRules 48 | ) { 49 | return comparatorEngine( 50 | ['emissionPerSecond', 'index', 'lastUpdateTimestamp'], 51 | assetConfigUpdateInput, 52 | assetConfigBefore, 53 | assetConfigAfter, 54 | actionBlockTimestamp, 55 | { 56 | ...compareRules, 57 | fieldsWithCustomLogic: [ 58 | // should happen on any update 59 | { 60 | fieldName: 'lastUpdateTimestamp', 61 | logic: (stateUpdate, stateBefore, stateAfter, txTimestamp) => txTimestamp.toString(), 62 | }, 63 | { 64 | fieldName: 'index', 65 | logic: async (stateUpdate, stateBefore, stateAfter, txTimestamp) => { 66 | return getNormalizedDistribution( 67 | stateUpdate.totalStaked.toString(), 68 | stateBefore.index, 69 | stateBefore.emissionPerSecond, 70 | stateBefore.lastUpdateTimestamp, 71 | txTimestamp, 72 | emissionEndTimestamp 73 | ).toString(10); 74 | }, 75 | }, 76 | ...(compareRules.fieldsWithCustomLogic || []), 77 | ], 78 | } 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /test/DistributionManager/data-helpers/asset-user-data.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers'; 2 | import { PullRewardsIncentivesController, StakedTokenIncentivesController } from '../../../types'; 3 | import { AaveDistributionManager } from '../../../types/AaveDistributionManager'; 4 | 5 | export type UserStakeInput = { 6 | underlyingAsset: string; 7 | stakedByUser: string; 8 | totalStaked: string; 9 | }; 10 | 11 | export type UserPositionUpdate = UserStakeInput & { 12 | user: string; 13 | }; 14 | export async function getUserIndex( 15 | distributionManager: 16 | | AaveDistributionManager 17 | | StakedTokenIncentivesController 18 | | PullRewardsIncentivesController, 19 | user: string, 20 | asset: string 21 | ): Promise { 22 | return await distributionManager.getUserAssetData(user, asset); 23 | } 24 | -------------------------------------------------------------------------------- /test/DistributionManager/data-helpers/base-math.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers'; 2 | 3 | import { BigNumberValue, valueToZDBigNumber } from '../../helpers/ray-math/bignumber'; 4 | 5 | export function getRewards( 6 | balance: BigNumberValue, 7 | assetIndex: BigNumberValue, 8 | userIndex: BigNumberValue, 9 | precision: number = 18 10 | ): BigNumber { 11 | return BigNumber.from( 12 | valueToZDBigNumber(balance) 13 | .multipliedBy(valueToZDBigNumber(assetIndex).minus(userIndex.toString())) 14 | .dividedBy(valueToZDBigNumber(10).exponentiatedBy(precision)) 15 | .toString() 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /test/PullRewardsIncentivesController/claim-on-behalf.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { MAX_UINT_AMOUNT, ZERO_ADDRESS } from '../../helpers/constants'; 3 | import { waitForTx } from '../../helpers/misc-utils'; 4 | 5 | import { makeSuite, TestEnv } from '../helpers/make-suite'; 6 | 7 | makeSuite('PullRewardsIncentivesController - Claim rewards on behalf', (testEnv: TestEnv) => { 8 | it('Should setClaimer revert if not called by emission manager', async () => { 9 | const { pullRewardsIncentivesController, users } = testEnv; 10 | const [userWithRewards, thirdClaimer] = users; 11 | await expect( 12 | pullRewardsIncentivesController 13 | .connect(userWithRewards.signer) 14 | .setClaimer(userWithRewards.address, thirdClaimer.address) 15 | ).to.be.revertedWith('ONLY_EMISSION_MANAGER'); 16 | }); 17 | it('Should claimRewardsOnBehalf revert if called claimer is not authorized', async () => { 18 | const { pullRewardsIncentivesController, users, aDaiBaseMock, aaveToken } = testEnv; 19 | const [userWithRewards, thirdClaimer] = users; 20 | 21 | await waitForTx( 22 | await pullRewardsIncentivesController.configureAssets([aDaiBaseMock.address], ['20000']) 23 | ); 24 | await waitForTx(await aDaiBaseMock.setUserBalanceAndSupply('300000', '300000')); 25 | 26 | // Claim from third party claimer 27 | const priorStkBalance = await aaveToken.balanceOf(thirdClaimer.address); 28 | 29 | await expect( 30 | pullRewardsIncentivesController 31 | .connect(thirdClaimer.signer) 32 | .claimRewardsOnBehalf( 33 | [aDaiBaseMock.address], 34 | MAX_UINT_AMOUNT, 35 | userWithRewards.address, 36 | thirdClaimer.address 37 | ) 38 | ).to.be.revertedWith('CLAIMER_UNAUTHORIZED'); 39 | 40 | const afterStkBalance = await aaveToken.balanceOf(thirdClaimer.address); 41 | expect(afterStkBalance).to.be.eq(priorStkBalance); 42 | }); 43 | it('Should setClaimer pass if called by emission manager', async () => { 44 | const { pullRewardsIncentivesController, users, rewardsVault } = testEnv; 45 | const [userWithRewards, thirdClaimer] = users; 46 | const emissionManager = rewardsVault; 47 | 48 | await expect( 49 | pullRewardsIncentivesController 50 | .connect(emissionManager.signer) 51 | .setClaimer(userWithRewards.address, thirdClaimer.address) 52 | ) 53 | .to.emit(pullRewardsIncentivesController, 'ClaimerSet') 54 | .withArgs(userWithRewards.address, thirdClaimer.address); 55 | await expect( 56 | await pullRewardsIncentivesController.getClaimer(userWithRewards.address) 57 | ).to.be.equal(thirdClaimer.address); 58 | }); 59 | it('Should claimRewardsOnBehalf pass if called by the assigned claimer', async () => { 60 | const { pullRewardsIncentivesController, users, aDaiBaseMock, aaveToken } = testEnv; 61 | const [userWithRewards, thirdClaimer] = users; 62 | 63 | await waitForTx( 64 | await pullRewardsIncentivesController.configureAssets([aDaiBaseMock.address], ['2000']) 65 | ); 66 | await waitForTx(await aDaiBaseMock.setUserBalanceAndSupply('300000', '30000')); 67 | 68 | // Claim from third party claimer 69 | const priorBalance = await aaveToken.balanceOf(thirdClaimer.address); 70 | 71 | await expect( 72 | pullRewardsIncentivesController 73 | .connect(thirdClaimer.signer) 74 | .claimRewardsOnBehalf( 75 | [aDaiBaseMock.address], 76 | MAX_UINT_AMOUNT, 77 | userWithRewards.address, 78 | thirdClaimer.address 79 | ) 80 | ) 81 | .to.emit(pullRewardsIncentivesController, 'RewardsClaimed') 82 | .withArgs(userWithRewards.address, thirdClaimer.address, thirdClaimer.address, '99999'); 83 | const afterStkBalance = await aaveToken.balanceOf(thirdClaimer.address); 84 | expect(afterStkBalance).to.be.gt(priorBalance); 85 | }); 86 | 87 | it('Should claimRewardsOnBehalf revert if to argument address is ZERO_ADDRESS', async () => { 88 | const { pullRewardsIncentivesController, users, aDaiBaseMock, aaveToken } = testEnv; 89 | const [userWithRewards, thirdClaimer] = users; 90 | 91 | await waitForTx( 92 | await pullRewardsIncentivesController.configureAssets([aDaiBaseMock.address], ['2000']) 93 | ); 94 | await waitForTx(await aDaiBaseMock.setUserBalanceAndSupply('300000', '30000')); 95 | 96 | await expect( 97 | pullRewardsIncentivesController 98 | .connect(thirdClaimer.signer) 99 | .claimRewardsOnBehalf( 100 | [aDaiBaseMock.address], 101 | MAX_UINT_AMOUNT, 102 | userWithRewards.address, 103 | ZERO_ADDRESS 104 | ) 105 | ).to.be.revertedWith('INVALID_TO_ADDRESS'); 106 | }); 107 | 108 | it('Should claimRewardsOnBehalf revert if user argument is ZERO_ADDRESS', async () => { 109 | const { pullRewardsIncentivesController, users, aDaiBaseMock, rewardsVault } = testEnv; 110 | const [, thirdClaimer] = users; 111 | 112 | const emissionManager = rewardsVault; 113 | 114 | await waitForTx( 115 | await pullRewardsIncentivesController.configureAssets([aDaiBaseMock.address], ['2000']) 116 | ); 117 | await waitForTx(await aDaiBaseMock.setUserBalanceAndSupply('300000', '30000')); 118 | 119 | await expect( 120 | pullRewardsIncentivesController 121 | .connect(emissionManager.signer) 122 | .setClaimer(ZERO_ADDRESS, thirdClaimer.address) 123 | ) 124 | .to.emit(pullRewardsIncentivesController, 'ClaimerSet') 125 | .withArgs(ZERO_ADDRESS, thirdClaimer.address); 126 | 127 | await expect( 128 | pullRewardsIncentivesController 129 | .connect(thirdClaimer.signer) 130 | .claimRewardsOnBehalf( 131 | [aDaiBaseMock.address], 132 | MAX_UINT_AMOUNT, 133 | ZERO_ADDRESS, 134 | thirdClaimer.address 135 | ) 136 | ).to.be.revertedWith('INVALID_USER_ADDRESS'); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /test/PullRewardsIncentivesController/configure-assets.spec.ts: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | import { makeSuite, TestEnv } from '../helpers/make-suite'; 4 | import { RANDOM_ADDRESSES } from '../../helpers/constants'; 5 | import { increaseTime, waitForTx } from '../../helpers/misc-utils'; 6 | import { getBlockTimestamp } from '../../helpers/contracts-helpers'; 7 | import { CompareRules, eventChecker } from '../helpers/comparator-engine'; 8 | import { 9 | AssetData, 10 | assetDataComparator, 11 | AssetUpdateData, 12 | getAssetsData, 13 | } from '../DistributionManager/data-helpers/asset-data'; 14 | import { BigNumberish } from '@ethersproject/bignumber'; 15 | 16 | type ScenarioAction = { 17 | caseName: string; 18 | customTimeMovement?: number; 19 | assets: Omit[]; 20 | compareRules?: CompareRules; 21 | }; 22 | 23 | const configureAssetScenarios: ScenarioAction[] = [ 24 | { 25 | caseName: 'Submit initial config for the assets', 26 | assets: [ 27 | { 28 | emissionPerSecond: '11', 29 | totalStaked: '0', 30 | }, 31 | ], 32 | compareRules: { 33 | fieldsEqualToInput: ['emissionPerSecond'], 34 | }, 35 | }, 36 | { 37 | caseName: 'Submit updated config for the assets', 38 | assets: [ 39 | { 40 | emissionPerSecond: '33', 41 | totalStaked: '0', 42 | }, 43 | { 44 | emissionPerSecond: '22', 45 | totalStaked: '0', 46 | }, 47 | ], 48 | compareRules: { 49 | fieldsEqualToInput: ['emissionPerSecond'], 50 | }, 51 | }, 52 | { 53 | caseName: 54 | 'Indexes should change if emission are set not to 0, and pool has deposited and borrowed funds', 55 | assets: [ 56 | { 57 | emissionPerSecond: '33', 58 | totalStaked: '100000', 59 | }, 60 | { 61 | emissionPerSecond: '22', 62 | totalStaked: '123123123', 63 | }, 64 | ], 65 | compareRules: { 66 | fieldsEqualToInput: ['emissionPerSecond'], 67 | }, 68 | }, 69 | { 70 | caseName: 'Indexes should cumulate rewards if next emission is 0', 71 | assets: [ 72 | { 73 | emissionPerSecond: '0', 74 | totalStaked: '100000', 75 | }, 76 | ], 77 | compareRules: { 78 | fieldsEqualToInput: ['emissionPerSecond'], 79 | }, 80 | }, 81 | { 82 | caseName: 'Indexes should not change if no emission', 83 | assets: [ 84 | { 85 | emissionPerSecond: '222', 86 | totalStaked: '213213213213', 87 | }, 88 | ], 89 | compareRules: { 90 | fieldsEqualToInput: ['emissionPerSecond'], 91 | }, 92 | }, 93 | { 94 | caseName: 'Should go to the limit if distribution ended', 95 | customTimeMovement: 1000 * 60 * 100, 96 | assets: [ 97 | { 98 | emissionPerSecond: '222', 99 | totalStaked: '213213213213', 100 | }, 101 | ], 102 | compareRules: { 103 | fieldsEqualToInput: ['emissionPerSecond'], 104 | }, 105 | }, 106 | { 107 | caseName: 'Should not accrue any rewards after end or distribution', 108 | customTimeMovement: 1000, 109 | assets: [ 110 | { 111 | emissionPerSecond: '222', 112 | totalStaked: '213213213213', 113 | }, 114 | ], 115 | compareRules: { 116 | fieldsEqualToInput: ['emissionPerSecond'], 117 | }, 118 | }, 119 | ]; 120 | 121 | makeSuite('pullRewardsIncentivesController configureAssets', (testEnv: TestEnv) => { 122 | let deployedAssets; 123 | 124 | before(async () => { 125 | deployedAssets = [testEnv.aDaiBaseMock, testEnv.aWethBaseMock]; 126 | }); 127 | 128 | // custom checks 129 | it('Tries to submit config updates not from emission manager', async () => { 130 | const { pullRewardsIncentivesController, users } = testEnv; 131 | await expect( 132 | pullRewardsIncentivesController.connect(users[2].signer).configureAssets([], []) 133 | ).to.be.revertedWith('ONLY_EMISSION_MANAGER'); 134 | }); 135 | 136 | for (const { 137 | assets: assetsConfig, 138 | caseName, 139 | compareRules, 140 | customTimeMovement, 141 | } of configureAssetScenarios) { 142 | it(caseName, async () => { 143 | const { pullRewardsIncentivesController } = testEnv; 144 | const distributionEndTimestamp = await pullRewardsIncentivesController.DISTRIBUTION_END(); 145 | 146 | const assets: string[] = []; 147 | const assetsEmissions: BigNumberish[] = []; 148 | const assetConfigsUpdate: AssetUpdateData[] = []; 149 | 150 | for (let i = 0; i < assetsConfig.length; i++) { 151 | const { emissionPerSecond, totalStaked } = assetsConfig[i]; 152 | if (i > deployedAssets.length) { 153 | throw new Error('to many assets to test'); 154 | } 155 | 156 | // Change current supply 157 | await deployedAssets[i].setUserBalanceAndSupply('0', totalStaked); 158 | 159 | // Push configs 160 | assets.push(deployedAssets[i].address); 161 | assetsEmissions.push(emissionPerSecond); 162 | assetConfigsUpdate.push({ 163 | emissionPerSecond, 164 | totalStaked, 165 | underlyingAsset: deployedAssets[i].address, 166 | }); 167 | } 168 | 169 | const assetsConfigBefore = await getAssetsData(pullRewardsIncentivesController, assets); 170 | 171 | if (customTimeMovement) { 172 | await increaseTime(customTimeMovement); 173 | } 174 | 175 | const txReceipt = await waitForTx( 176 | await pullRewardsIncentivesController.configureAssets(assets, assetsEmissions) 177 | ); 178 | const configsUpdateBlockTimestamp = await getBlockTimestamp(txReceipt.blockNumber); 179 | const assetsConfigAfter = await getAssetsData(pullRewardsIncentivesController, assets); 180 | 181 | const eventsEmitted = txReceipt.events || []; 182 | 183 | let eventArrayIndex = 0; 184 | for (let i = 0; i < assetsConfigBefore.length; i++) { 185 | const assetConfigBefore = assetsConfigBefore[i]; 186 | const assetConfigUpdateInput = assetConfigsUpdate[i]; 187 | const assetConfigAfter = assetsConfigAfter[i]; 188 | 189 | if (!assetConfigAfter.index.eq(assetConfigBefore.index)) { 190 | eventChecker(eventsEmitted[eventArrayIndex], 'AssetIndexUpdated', [ 191 | assetConfigAfter.underlyingAsset, 192 | assetConfigAfter.index, 193 | ]); 194 | eventArrayIndex += 1; 195 | } 196 | 197 | eventChecker(eventsEmitted[eventArrayIndex], 'AssetConfigUpdated', [ 198 | assetConfigAfter.underlyingAsset, 199 | assetConfigAfter.emissionPerSecond, 200 | ]); 201 | eventArrayIndex += 1; 202 | 203 | await assetDataComparator( 204 | assetConfigUpdateInput, 205 | assetConfigBefore, 206 | assetConfigAfter, 207 | configsUpdateBlockTimestamp, 208 | distributionEndTimestamp.toNumber(), 209 | compareRules || {} 210 | ); 211 | } 212 | expect(eventsEmitted[eventArrayIndex]).to.be.equal(undefined, 'Too many events emitted'); 213 | }); 214 | } 215 | }); 216 | -------------------------------------------------------------------------------- /test/PullRewardsIncentivesController/get-rewards-balance.spec.ts: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | import { makeSuite } from '../helpers/make-suite'; 4 | import { getRewards } from '../DistributionManager/data-helpers/base-math'; 5 | import { getUserIndex } from '../DistributionManager/data-helpers/asset-user-data'; 6 | import { getAssetsData } from '../DistributionManager/data-helpers/asset-data'; 7 | import { advanceBlock, timeLatest, waitForTx, increaseTime } from '../../helpers/misc-utils'; 8 | import { getNormalizedDistribution } from '../helpers/ray-math'; 9 | import { getBlockTimestamp } from '../../helpers/contracts-helpers'; 10 | 11 | type ScenarioAction = { 12 | caseName: string; 13 | emissionPerSecond: string; 14 | }; 15 | 16 | const getRewardsBalanceScenarios: ScenarioAction[] = [ 17 | { 18 | caseName: 'Accrued rewards are 0', 19 | emissionPerSecond: '0', 20 | }, 21 | { 22 | caseName: 'Accrued rewards are not 0', 23 | emissionPerSecond: '2432424', 24 | }, 25 | { 26 | caseName: 'Accrued rewards are not 0', 27 | emissionPerSecond: '2432424', 28 | }, 29 | ]; 30 | 31 | makeSuite('pullRewardsIncentivesController getRewardsBalance tests', (testEnv) => { 32 | for (const { caseName, emissionPerSecond } of getRewardsBalanceScenarios) { 33 | it(caseName, async () => { 34 | await increaseTime(100); 35 | 36 | const { pullRewardsIncentivesController, users, aDaiBaseMock } = testEnv; 37 | 38 | const distributionEndTimestamp = await pullRewardsIncentivesController.DISTRIBUTION_END(); 39 | const userAddress = users[1].address; 40 | const stakedByUser = 22 * caseName.length; 41 | const totalStaked = 33 * caseName.length; 42 | const underlyingAsset = aDaiBaseMock.address; 43 | 44 | // update emissionPerSecond in advance to not affect user calculations 45 | await advanceBlock((await timeLatest()).plus(100).toNumber()); 46 | if (emissionPerSecond) { 47 | await aDaiBaseMock.setUserBalanceAndSupply('0', totalStaked); 48 | await pullRewardsIncentivesController.configureAssets( 49 | [underlyingAsset], 50 | [emissionPerSecond] 51 | ); 52 | } 53 | await aDaiBaseMock.handleActionOnAic(userAddress, totalStaked, stakedByUser); 54 | await advanceBlock((await timeLatest()).plus(100).toNumber()); 55 | 56 | const lastTxReceipt = await waitForTx( 57 | await aDaiBaseMock.setUserBalanceAndSupply(stakedByUser, totalStaked) 58 | ); 59 | const lastTxTimestamp = await getBlockTimestamp(lastTxReceipt.blockNumber); 60 | 61 | const unclaimedRewardsBefore = await pullRewardsIncentivesController.getUserUnclaimedRewards( 62 | userAddress 63 | ); 64 | 65 | const unclaimedRewards = await pullRewardsIncentivesController.getRewardsBalance( 66 | [underlyingAsset], 67 | userAddress 68 | ); 69 | 70 | const userIndex = await getUserIndex( 71 | pullRewardsIncentivesController, 72 | userAddress, 73 | underlyingAsset 74 | ); 75 | const assetData = ( 76 | await getAssetsData(pullRewardsIncentivesController, [underlyingAsset]) 77 | )[0]; 78 | 79 | await aDaiBaseMock.cleanUserState(); 80 | 81 | const expectedAssetIndex = getNormalizedDistribution( 82 | totalStaked, 83 | assetData.index, 84 | assetData.emissionPerSecond, 85 | assetData.lastUpdateTimestamp, 86 | lastTxTimestamp, 87 | distributionEndTimestamp 88 | ); 89 | const expectedAccruedRewards = getRewards( 90 | stakedByUser, 91 | expectedAssetIndex, 92 | userIndex 93 | ).toString(); 94 | 95 | expect(unclaimedRewards.toString()).to.be.equal( 96 | unclaimedRewardsBefore.add(expectedAccruedRewards).toString() 97 | ); 98 | }); 99 | } 100 | }); 101 | -------------------------------------------------------------------------------- /test/PullRewardsIncentivesController/handle-action.spec.ts: -------------------------------------------------------------------------------- 1 | import { fail } from 'assert'; 2 | const { expect } = require('chai'); 3 | 4 | import { waitForTx, increaseTime } from '../../helpers/misc-utils'; 5 | import { makeSuite } from '../helpers/make-suite'; 6 | import { eventChecker } from '../helpers/comparator-engine'; 7 | import { getBlockTimestamp } from '../../helpers/contracts-helpers'; 8 | 9 | import { getUserIndex } from '../DistributionManager/data-helpers/asset-user-data'; 10 | import { assetDataComparator, getAssetsData } from '../DistributionManager/data-helpers/asset-data'; 11 | import { getRewards } from '../DistributionManager/data-helpers/base-math'; 12 | 13 | type ScenarioAction = { 14 | caseName: string; 15 | emissionPerSecond?: string; 16 | userBalance: string; 17 | totalSupply: string; 18 | customTimeMovement?: number; 19 | }; 20 | 21 | const handleActionScenarios: ScenarioAction[] = [ 22 | { 23 | caseName: 'All 0', 24 | emissionPerSecond: '0', 25 | userBalance: '0', 26 | totalSupply: '0', 27 | }, 28 | { 29 | caseName: 'Accrued rewards are 0, 0 emission', 30 | emissionPerSecond: '0', 31 | userBalance: '22', 32 | totalSupply: '22', 33 | }, 34 | { 35 | caseName: 'Accrued rewards are 0, 0 user balance', 36 | emissionPerSecond: '100', 37 | userBalance: '0', 38 | totalSupply: '22', 39 | }, 40 | { 41 | caseName: '1. Accrued rewards are not 0', 42 | userBalance: '22', 43 | totalSupply: '22', 44 | }, 45 | { 46 | caseName: '2. Accrued rewards are not 0', 47 | emissionPerSecond: '1000', 48 | userBalance: '2332', 49 | totalSupply: '3232', 50 | }, 51 | ]; 52 | 53 | makeSuite('pullRewardsIncentivesController handleAction tests', (testEnv) => { 54 | for (const { 55 | caseName, 56 | totalSupply, 57 | userBalance, 58 | customTimeMovement, 59 | emissionPerSecond, 60 | } of handleActionScenarios) { 61 | it(caseName, async () => { 62 | await increaseTime(100); 63 | 64 | const { pullRewardsIncentivesController, users, aDaiBaseMock } = testEnv; 65 | const userAddress = users[1].address; 66 | const underlyingAsset = aDaiBaseMock.address; 67 | 68 | // update emissionPerSecond in advance to not affect user calculations 69 | if (emissionPerSecond) { 70 | await pullRewardsIncentivesController.configureAssets( 71 | [underlyingAsset], 72 | [emissionPerSecond] 73 | ); 74 | } 75 | 76 | const distributionEndTimestamp = await pullRewardsIncentivesController.DISTRIBUTION_END(); 77 | 78 | const rewardsBalanceBefore = await pullRewardsIncentivesController.getUserUnclaimedRewards( 79 | userAddress 80 | ); 81 | const userIndexBefore = await getUserIndex( 82 | pullRewardsIncentivesController, 83 | userAddress, 84 | underlyingAsset 85 | ); 86 | const assetDataBefore = ( 87 | await getAssetsData(pullRewardsIncentivesController, [underlyingAsset]) 88 | )[0]; 89 | 90 | if (customTimeMovement) { 91 | await increaseTime(customTimeMovement); 92 | } 93 | 94 | await waitForTx(await aDaiBaseMock.setUserBalanceAndSupply(userBalance, totalSupply)); 95 | const handleActionReceipt = await waitForTx( 96 | await aDaiBaseMock.handleActionOnAic(userAddress, totalSupply, userBalance) 97 | ); 98 | const eventsEmitted = handleActionReceipt.events || []; 99 | const actionBlockTimestamp = await getBlockTimestamp(handleActionReceipt.blockNumber); 100 | 101 | const userIndexAfter = await getUserIndex( 102 | pullRewardsIncentivesController, 103 | userAddress, 104 | underlyingAsset 105 | ); 106 | 107 | const assetDataAfter = ( 108 | await getAssetsData(pullRewardsIncentivesController, [underlyingAsset]) 109 | )[0]; 110 | 111 | const expectedAccruedRewards = getRewards( 112 | userBalance, 113 | userIndexAfter, 114 | userIndexBefore 115 | ).toString(); 116 | 117 | const rewardsBalanceAfter = await pullRewardsIncentivesController.getUserUnclaimedRewards( 118 | userAddress 119 | ); 120 | 121 | // ------- Distribution Manager tests START ----- 122 | await assetDataComparator( 123 | { underlyingAsset, totalStaked: totalSupply }, 124 | assetDataBefore, 125 | assetDataAfter, 126 | actionBlockTimestamp, 127 | distributionEndTimestamp.toNumber(), 128 | {} 129 | ); 130 | expect(userIndexAfter.toString()).to.be.equal( 131 | assetDataAfter.index.toString(), 132 | 'user index are not correctly updated' 133 | ); 134 | if (!assetDataAfter.index.eq(assetDataBefore.index)) { 135 | const eventAssetUpdated = eventsEmitted.find(({ event }) => event === 'AssetIndexUpdated'); 136 | const eventUserIndexUpdated = eventsEmitted.find( 137 | ({ event }) => event === 'UserIndexUpdated' 138 | ); 139 | 140 | if (!eventAssetUpdated) { 141 | fail('missing AssetIndexUpdated event'); 142 | } 143 | if (!eventUserIndexUpdated) { 144 | fail('missing UserIndexUpdated event'); 145 | } 146 | eventChecker(eventAssetUpdated, 'AssetIndexUpdated', [ 147 | assetDataAfter.underlyingAsset, 148 | assetDataAfter.index, 149 | ]); 150 | eventChecker(eventUserIndexUpdated, 'UserIndexUpdated', [ 151 | userAddress, 152 | assetDataAfter.underlyingAsset, 153 | assetDataAfter.index, 154 | ]); 155 | } 156 | // ------- Distribution Manager tests END ----- 157 | 158 | // ------- PEI tests START ----- 159 | expect(rewardsBalanceAfter.toString()).to.be.equal( 160 | rewardsBalanceBefore.add(expectedAccruedRewards).toString(), 161 | 'rewards balance are incorrect' 162 | ); 163 | if (expectedAccruedRewards !== '0') { 164 | const eventAssetUpdated = eventsEmitted.find(({ event }) => event === 'RewardsAccrued'); 165 | if (!eventAssetUpdated) { 166 | fail('missing RewardsAccrued event'); 167 | } 168 | eventChecker(eventAssetUpdated, 'RewardsAccrued', [userAddress, expectedAccruedRewards]); 169 | } 170 | // ------- PEI tests END ----- 171 | }); 172 | } 173 | }); 174 | -------------------------------------------------------------------------------- /test/PullRewardsIncentivesController/initialize.spec.ts: -------------------------------------------------------------------------------- 1 | import { makeSuite, TestEnv } from '../helpers/make-suite'; 2 | import { MAX_UINT_AMOUNT, ZERO_ADDRESS } from '../../helpers/constants'; 3 | 4 | const { expect } = require('chai'); 5 | 6 | makeSuite('pullRewardsIncentivesController initialize', (testEnv: TestEnv) => { 7 | // TODO: useless or not? 8 | it('Tries to call initialize second time, should be reverted', async () => { 9 | const { pullRewardsIncentivesController } = testEnv; 10 | await expect(pullRewardsIncentivesController.initialize(ZERO_ADDRESS)).to.be.reverted; 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/PullRewardsIncentivesController/misc.spec.ts: -------------------------------------------------------------------------------- 1 | import { timeLatest, waitForTx } from '../../helpers/misc-utils'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { makeSuite } from '../helpers/make-suite'; 6 | import { deployPullRewardsIncentivesController } from '../../helpers/contracts-accessors'; 7 | import { MAX_UINT_AMOUNT, RANDOM_ADDRESSES, ZERO_ADDRESS } from '../../helpers/constants'; 8 | 9 | makeSuite('pullRewardsIncentivesController misc tests', (testEnv) => { 10 | it('constructor should assign correct params', async () => { 11 | const peiEmissionManager = RANDOM_ADDRESSES[1]; 12 | const fakeToken = RANDOM_ADDRESSES[5]; 13 | 14 | const pullRewardsIncentivesController = await deployPullRewardsIncentivesController([ 15 | fakeToken, 16 | peiEmissionManager, 17 | ]); 18 | await expect(await pullRewardsIncentivesController.REWARD_TOKEN()).to.be.equal(fakeToken); 19 | await expect((await pullRewardsIncentivesController.EMISSION_MANAGER()).toString()).to.be.equal( 20 | peiEmissionManager 21 | ); 22 | }); 23 | 24 | it('Should return same index while multiple asset index updates', async () => { 25 | const { aDaiBaseMock, pullRewardsIncentivesController, users } = testEnv; 26 | await waitForTx( 27 | await pullRewardsIncentivesController.configureAssets([aDaiBaseMock.address], ['100']) 28 | ); 29 | await waitForTx(await aDaiBaseMock.doubleHandleActionOnAic(users[1].address, '2000', '100')); 30 | }); 31 | 32 | it('Should overflow index if passed a large emission', async () => { 33 | const { aDaiBaseMock, pullRewardsIncentivesController, users } = testEnv; 34 | const MAX_104_UINT = '20282409603651670423947251286015'; 35 | 36 | await waitForTx( 37 | await pullRewardsIncentivesController.configureAssets([aDaiBaseMock.address], [MAX_104_UINT]) 38 | ); 39 | await expect( 40 | aDaiBaseMock.doubleHandleActionOnAic(users[1].address, '2000', '100') 41 | ).to.be.revertedWith('Index overflow'); 42 | }); 43 | 44 | it('Should configureAssets revert if parameters length does not match', async () => { 45 | const { aDaiBaseMock, pullRewardsIncentivesController } = testEnv; 46 | 47 | await expect( 48 | pullRewardsIncentivesController.configureAssets([aDaiBaseMock.address], ['1', '2']) 49 | ).to.be.revertedWith('INVALID_CONFIGURATION'); 50 | }); 51 | 52 | it('Should configureAssets revert if emission parameter overflows uin104', async () => { 53 | const { aDaiBaseMock, pullRewardsIncentivesController } = testEnv; 54 | 55 | await expect( 56 | pullRewardsIncentivesController.configureAssets([aDaiBaseMock.address], [MAX_UINT_AMOUNT]) 57 | ).to.be.revertedWith('Index overflow at emissionsPerSecond'); 58 | }); 59 | 60 | it('Should REWARD_TOKEN getter returns the stake token address to keep old interface compatibility', async () => { 61 | const { pullRewardsIncentivesController, aaveToken } = testEnv; 62 | await expect(await pullRewardsIncentivesController.REWARD_TOKEN()).to.be.equal( 63 | aaveToken.address 64 | ); 65 | }); 66 | 67 | it('Should claimRewards revert if to argument is ZERO_ADDRESS', async () => { 68 | const { pullRewardsIncentivesController, users, aDaiBaseMock } = testEnv; 69 | const [userWithRewards] = users; 70 | 71 | await waitForTx( 72 | await pullRewardsIncentivesController.configureAssets([aDaiBaseMock.address], ['2000']) 73 | ); 74 | await waitForTx(await aDaiBaseMock.setUserBalanceAndSupply('300000', '30000')); 75 | 76 | // Claim from third party claimer 77 | await expect( 78 | pullRewardsIncentivesController 79 | .connect(userWithRewards.signer) 80 | .claimRewards([aDaiBaseMock.address], MAX_UINT_AMOUNT, ZERO_ADDRESS) 81 | ).to.be.revertedWith('INVALID_TO_ADDRESS'); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/StakedIncentivesController/claim-on-behalf.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { MAX_UINT_AMOUNT, ZERO_ADDRESS } from '../../helpers/constants'; 3 | import { waitForTx } from '../../helpers/misc-utils'; 4 | 5 | import { makeSuite, TestEnv } from '../helpers/make-suite'; 6 | 7 | makeSuite('AaveIncentivesController - Claim rewards on behalf', (testEnv: TestEnv) => { 8 | it('Should setClaimer revert if not called by emission manager', async () => { 9 | const { aaveIncentivesController, users } = testEnv; 10 | const [userWithRewards, thirdClaimer] = users; 11 | await expect( 12 | aaveIncentivesController 13 | .connect(userWithRewards.signer) 14 | .setClaimer(userWithRewards.address, thirdClaimer.address) 15 | ).to.be.revertedWith('ONLY_EMISSION_MANAGER'); 16 | }); 17 | it('Should claimRewardsOnBehalf revert if called claimer is not authorized', async () => { 18 | const { aaveIncentivesController, users, aDaiMock, stakedAave } = testEnv; 19 | const [userWithRewards, thirdClaimer] = users; 20 | 21 | await waitForTx(await aaveIncentivesController.configureAssets([aDaiMock.address], ['2000'])); 22 | await waitForTx(await aDaiMock.setUserBalanceAndSupply('300000', '30000')); 23 | 24 | // Claim from third party claimer 25 | const priorStkBalance = await stakedAave.balanceOf(thirdClaimer.address); 26 | 27 | await expect( 28 | aaveIncentivesController 29 | .connect(thirdClaimer.signer) 30 | .claimRewardsOnBehalf( 31 | [aDaiMock.address], 32 | MAX_UINT_AMOUNT, 33 | userWithRewards.address, 34 | thirdClaimer.address 35 | ) 36 | ).to.be.revertedWith('CLAIMER_UNAUTHORIZED'); 37 | 38 | const afterStkBalance = await stakedAave.balanceOf(thirdClaimer.address); 39 | expect(afterStkBalance).to.be.eq(priorStkBalance); 40 | }); 41 | it('Should setClaimer pass if called by emission manager', async () => { 42 | const { aaveIncentivesController, users, rewardsVault } = testEnv; 43 | const [userWithRewards, thirdClaimer] = users; 44 | const emissionManager = rewardsVault; 45 | 46 | await expect( 47 | aaveIncentivesController 48 | .connect(emissionManager.signer) 49 | .setClaimer(userWithRewards.address, thirdClaimer.address) 50 | ) 51 | .to.emit(aaveIncentivesController, 'ClaimerSet') 52 | .withArgs(userWithRewards.address, thirdClaimer.address); 53 | await expect(await aaveIncentivesController.getClaimer(userWithRewards.address)).to.be.equal( 54 | thirdClaimer.address 55 | ); 56 | }); 57 | it('Should claimRewardsOnBehalf pass if called by the assigned claimer', async () => { 58 | const { aaveIncentivesController, users, aDaiMock, stakedAave } = testEnv; 59 | const [userWithRewards, thirdClaimer] = users; 60 | 61 | await waitForTx(await aaveIncentivesController.configureAssets([aDaiMock.address], ['2000'])); 62 | await waitForTx(await aDaiMock.setUserBalanceAndSupply('300000', '30000')); 63 | 64 | // Claim from third party claimer 65 | const priorStkBalance = await stakedAave.balanceOf(thirdClaimer.address); 66 | 67 | await expect( 68 | aaveIncentivesController 69 | .connect(thirdClaimer.signer) 70 | .claimRewardsOnBehalf( 71 | [aDaiMock.address], 72 | MAX_UINT_AMOUNT, 73 | userWithRewards.address, 74 | thirdClaimer.address 75 | ) 76 | ) 77 | .to.emit(aaveIncentivesController, 'RewardsClaimed') 78 | .withArgs(userWithRewards.address, thirdClaimer.address, thirdClaimer.address, '99999'); 79 | const afterStkBalance = await stakedAave.balanceOf(thirdClaimer.address); 80 | expect(afterStkBalance).to.be.gt(priorStkBalance); 81 | }); 82 | 83 | it('Should claimRewardsOnBehalf revert if to argument address is ZERO_ADDRESS', async () => { 84 | const { aaveIncentivesController, users, aDaiMock, stakedAave } = testEnv; 85 | const [userWithRewards, thirdClaimer] = users; 86 | 87 | await waitForTx(await aaveIncentivesController.configureAssets([aDaiMock.address], ['2000'])); 88 | await waitForTx(await aDaiMock.setUserBalanceAndSupply('300000', '30000')); 89 | 90 | await expect( 91 | aaveIncentivesController 92 | .connect(thirdClaimer.signer) 93 | .claimRewardsOnBehalf( 94 | [aDaiMock.address], 95 | MAX_UINT_AMOUNT, 96 | userWithRewards.address, 97 | ZERO_ADDRESS 98 | ) 99 | ).to.be.revertedWith('INVALID_TO_ADDRESS'); 100 | }); 101 | 102 | it('Should claimRewardsOnBehalf revert if user argument is ZERO_ADDRESS', async () => { 103 | const { aaveIncentivesController, users, aDaiMock, rewardsVault } = testEnv; 104 | const [, thirdClaimer] = users; 105 | 106 | const emissionManager = rewardsVault; 107 | 108 | await waitForTx(await aaveIncentivesController.configureAssets([aDaiMock.address], ['2000'])); 109 | await waitForTx(await aDaiMock.setUserBalanceAndSupply('300000', '30000')); 110 | 111 | await expect( 112 | aaveIncentivesController 113 | .connect(emissionManager.signer) 114 | .setClaimer(ZERO_ADDRESS, thirdClaimer.address) 115 | ) 116 | .to.emit(aaveIncentivesController, 'ClaimerSet') 117 | .withArgs(ZERO_ADDRESS, thirdClaimer.address); 118 | 119 | await expect( 120 | aaveIncentivesController 121 | .connect(thirdClaimer.signer) 122 | .claimRewardsOnBehalf( 123 | [aDaiMock.address], 124 | MAX_UINT_AMOUNT, 125 | ZERO_ADDRESS, 126 | thirdClaimer.address 127 | ) 128 | ).to.be.revertedWith('INVALID_USER_ADDRESS'); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /test/StakedIncentivesController/configure-assets.spec.ts: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | import { makeSuite, TestEnv } from '../helpers/make-suite'; 4 | import { RANDOM_ADDRESSES } from '../../helpers/constants'; 5 | import { increaseTime, waitForTx } from '../../helpers/misc-utils'; 6 | import { getBlockTimestamp } from '../../helpers/contracts-helpers'; 7 | import { CompareRules, eventChecker } from '../helpers/comparator-engine'; 8 | import { 9 | AssetData, 10 | assetDataComparator, 11 | AssetUpdateData, 12 | getAssetsData, 13 | } from '../DistributionManager/data-helpers/asset-data'; 14 | import { BigNumberish } from '@ethersproject/bignumber'; 15 | 16 | type ScenarioAction = { 17 | caseName: string; 18 | customTimeMovement?: number; 19 | assets: Omit[]; 20 | compareRules?: CompareRules; 21 | }; 22 | 23 | const configureAssetScenarios: ScenarioAction[] = [ 24 | { 25 | caseName: 'Submit initial config for the assets', 26 | assets: [ 27 | { 28 | emissionPerSecond: '11', 29 | totalStaked: '0', 30 | }, 31 | ], 32 | compareRules: { 33 | fieldsEqualToInput: ['emissionPerSecond'], 34 | }, 35 | }, 36 | { 37 | caseName: 'Submit updated config for the assets', 38 | assets: [ 39 | { 40 | emissionPerSecond: '33', 41 | totalStaked: '0', 42 | }, 43 | { 44 | emissionPerSecond: '22', 45 | totalStaked: '0', 46 | }, 47 | ], 48 | compareRules: { 49 | fieldsEqualToInput: ['emissionPerSecond'], 50 | }, 51 | }, 52 | { 53 | caseName: 54 | 'Indexes should change if emission are set not to 0, and pool has deposited and borrowed funds', 55 | assets: [ 56 | { 57 | emissionPerSecond: '33', 58 | totalStaked: '100000', 59 | }, 60 | { 61 | emissionPerSecond: '22', 62 | totalStaked: '123123123', 63 | }, 64 | ], 65 | compareRules: { 66 | fieldsEqualToInput: ['emissionPerSecond'], 67 | }, 68 | }, 69 | { 70 | caseName: 'Indexes should cumulate rewards if next emission is 0', 71 | assets: [ 72 | { 73 | emissionPerSecond: '0', 74 | totalStaked: '100000', 75 | }, 76 | ], 77 | compareRules: { 78 | fieldsEqualToInput: ['emissionPerSecond'], 79 | }, 80 | }, 81 | { 82 | caseName: 'Indexes should not change if no emission', 83 | assets: [ 84 | { 85 | emissionPerSecond: '222', 86 | totalStaked: '213213213213', 87 | }, 88 | ], 89 | compareRules: { 90 | fieldsEqualToInput: ['emissionPerSecond'], 91 | }, 92 | }, 93 | { 94 | caseName: 'Should go to the limit if distribution ended', 95 | customTimeMovement: 1000 * 60 * 100, 96 | assets: [ 97 | { 98 | emissionPerSecond: '222', 99 | totalStaked: '213213213213', 100 | }, 101 | ], 102 | compareRules: { 103 | fieldsEqualToInput: ['emissionPerSecond'], 104 | }, 105 | }, 106 | { 107 | caseName: 'Should not accrue any rewards after end or distribution', 108 | customTimeMovement: 1000, 109 | assets: [ 110 | { 111 | emissionPerSecond: '222', 112 | totalStaked: '213213213213', 113 | }, 114 | ], 115 | compareRules: { 116 | fieldsEqualToInput: ['emissionPerSecond'], 117 | }, 118 | }, 119 | ]; 120 | 121 | makeSuite('AaveIncentivesController configureAssets', (testEnv: TestEnv) => { 122 | let deployedAssets; 123 | 124 | before(async () => { 125 | deployedAssets = [testEnv.aDaiMock, testEnv.aWethMock]; 126 | }); 127 | 128 | // custom checks 129 | it('Tries to submit config updates not from emission manager', async () => { 130 | const { aaveIncentivesController, users } = testEnv; 131 | await expect( 132 | aaveIncentivesController.connect(users[2].signer).configureAssets([], []) 133 | ).to.be.revertedWith('ONLY_EMISSION_MANAGER'); 134 | }); 135 | 136 | for (const { 137 | assets: assetsConfig, 138 | caseName, 139 | compareRules, 140 | customTimeMovement, 141 | } of configureAssetScenarios) { 142 | it(caseName, async () => { 143 | const { aaveIncentivesController } = testEnv; 144 | const distributionEndTimestamp = await aaveIncentivesController.DISTRIBUTION_END(); 145 | 146 | const assets: string[] = []; 147 | const assetsEmissions: BigNumberish[] = []; 148 | const assetConfigsUpdate: AssetUpdateData[] = []; 149 | 150 | for (let i = 0; i < assetsConfig.length; i++) { 151 | const { emissionPerSecond, totalStaked } = assetsConfig[i]; 152 | if (i > deployedAssets.length) { 153 | throw new Error('to many assets to test'); 154 | } 155 | 156 | // Change current supply 157 | await deployedAssets[i].setUserBalanceAndSupply('0', totalStaked); 158 | 159 | // Push configs 160 | assets.push(deployedAssets[i].address); 161 | assetsEmissions.push(emissionPerSecond); 162 | assetConfigsUpdate.push({ 163 | emissionPerSecond, 164 | totalStaked, 165 | underlyingAsset: deployedAssets[i].address, 166 | }); 167 | } 168 | 169 | const assetsConfigBefore = await getAssetsData(aaveIncentivesController, assets); 170 | 171 | if (customTimeMovement) { 172 | await increaseTime(customTimeMovement); 173 | } 174 | 175 | const txReceipt = await waitForTx( 176 | await aaveIncentivesController.configureAssets(assets, assetsEmissions) 177 | ); 178 | const configsUpdateBlockTimestamp = await getBlockTimestamp(txReceipt.blockNumber); 179 | const assetsConfigAfter = await getAssetsData(aaveIncentivesController, assets); 180 | 181 | const eventsEmitted = txReceipt.events || []; 182 | 183 | let eventArrayIndex = 0; 184 | for (let i = 0; i < assetsConfigBefore.length; i++) { 185 | const assetConfigBefore = assetsConfigBefore[i]; 186 | const assetConfigUpdateInput = assetConfigsUpdate[i]; 187 | const assetConfigAfter = assetsConfigAfter[i]; 188 | 189 | if (!assetConfigAfter.index.eq(assetConfigBefore.index)) { 190 | eventChecker(eventsEmitted[eventArrayIndex], 'AssetIndexUpdated', [ 191 | assetConfigAfter.underlyingAsset, 192 | assetConfigAfter.index, 193 | ]); 194 | eventArrayIndex += 1; 195 | } 196 | 197 | eventChecker(eventsEmitted[eventArrayIndex], 'AssetConfigUpdated', [ 198 | assetConfigAfter.underlyingAsset, 199 | assetConfigAfter.emissionPerSecond, 200 | ]); 201 | eventArrayIndex += 1; 202 | 203 | await assetDataComparator( 204 | assetConfigUpdateInput, 205 | assetConfigBefore, 206 | assetConfigAfter, 207 | configsUpdateBlockTimestamp, 208 | distributionEndTimestamp.toNumber(), 209 | compareRules || {} 210 | ); 211 | } 212 | expect(eventsEmitted[eventArrayIndex]).to.be.equal(undefined, 'Too many events emitted'); 213 | }); 214 | } 215 | }); 216 | -------------------------------------------------------------------------------- /test/StakedIncentivesController/get-rewards-balance.spec.ts: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | import { makeSuite } from '../helpers/make-suite'; 4 | import { getRewards } from '../DistributionManager/data-helpers/base-math'; 5 | import { getUserIndex } from '../DistributionManager/data-helpers/asset-user-data'; 6 | import { getAssetsData } from '../DistributionManager/data-helpers/asset-data'; 7 | import { advanceBlock, timeLatest, waitForTx, increaseTime } from '../../helpers/misc-utils'; 8 | import { getNormalizedDistribution } from '../helpers/ray-math'; 9 | import { getBlockTimestamp } from '../../helpers/contracts-helpers'; 10 | 11 | type ScenarioAction = { 12 | caseName: string; 13 | emissionPerSecond: string; 14 | }; 15 | 16 | const getRewardsBalanceScenarios: ScenarioAction[] = [ 17 | { 18 | caseName: 'Accrued rewards are 0', 19 | emissionPerSecond: '0', 20 | }, 21 | { 22 | caseName: 'Accrued rewards are not 0', 23 | emissionPerSecond: '2432424', 24 | }, 25 | { 26 | caseName: 'Accrued rewards are not 0', 27 | emissionPerSecond: '2432424', 28 | }, 29 | ]; 30 | 31 | makeSuite('AaveIncentivesController getRewardsBalance tests', (testEnv) => { 32 | for (const { caseName, emissionPerSecond } of getRewardsBalanceScenarios) { 33 | it(caseName, async () => { 34 | await increaseTime(100); 35 | 36 | const { aaveIncentivesController, users, aDaiMock } = testEnv; 37 | 38 | const distributionEndTimestamp = await aaveIncentivesController.DISTRIBUTION_END(); 39 | const userAddress = users[1].address; 40 | const stakedByUser = 22 * caseName.length; 41 | const totalStaked = 33 * caseName.length; 42 | const underlyingAsset = aDaiMock.address; 43 | 44 | // update emissionPerSecond in advance to not affect user calculations 45 | await advanceBlock((await timeLatest()).plus(100).toNumber()); 46 | if (emissionPerSecond) { 47 | await aDaiMock.setUserBalanceAndSupply('0', totalStaked); 48 | await aaveIncentivesController.configureAssets([underlyingAsset], [emissionPerSecond]); 49 | } 50 | await aDaiMock.handleActionOnAic(userAddress, totalStaked, stakedByUser); 51 | await advanceBlock((await timeLatest()).plus(100).toNumber()); 52 | 53 | const lastTxReceipt = await waitForTx( 54 | await aDaiMock.setUserBalanceAndSupply(stakedByUser, totalStaked) 55 | ); 56 | const lastTxTimestamp = await getBlockTimestamp(lastTxReceipt.blockNumber); 57 | 58 | const unclaimedRewardsBefore = await aaveIncentivesController.getUserUnclaimedRewards( 59 | userAddress 60 | ); 61 | 62 | const unclaimedRewards = await aaveIncentivesController.getRewardsBalance( 63 | [underlyingAsset], 64 | userAddress 65 | ); 66 | 67 | const userIndex = await getUserIndex(aaveIncentivesController, userAddress, underlyingAsset); 68 | const assetData = (await getAssetsData(aaveIncentivesController, [underlyingAsset]))[0]; 69 | 70 | await aDaiMock.cleanUserState(); 71 | 72 | const expectedAssetIndex = getNormalizedDistribution( 73 | totalStaked, 74 | assetData.index, 75 | assetData.emissionPerSecond, 76 | assetData.lastUpdateTimestamp, 77 | lastTxTimestamp, 78 | distributionEndTimestamp 79 | ); 80 | const expectedAccruedRewards = getRewards( 81 | stakedByUser, 82 | expectedAssetIndex, 83 | userIndex 84 | ).toString(); 85 | 86 | expect(unclaimedRewards.toString()).to.be.equal( 87 | unclaimedRewardsBefore.add(expectedAccruedRewards).toString() 88 | ); 89 | }); 90 | } 91 | }); 92 | -------------------------------------------------------------------------------- /test/StakedIncentivesController/handle-action.spec.ts: -------------------------------------------------------------------------------- 1 | import { fail } from 'assert'; 2 | const { expect } = require('chai'); 3 | 4 | import { waitForTx, increaseTime } from '../../helpers/misc-utils'; 5 | import { makeSuite } from '../helpers/make-suite'; 6 | import { eventChecker } from '../helpers/comparator-engine'; 7 | import { getBlockTimestamp } from '../../helpers/contracts-helpers'; 8 | 9 | import { getUserIndex } from '../DistributionManager/data-helpers/asset-user-data'; 10 | import { assetDataComparator, getAssetsData } from '../DistributionManager/data-helpers/asset-data'; 11 | import { getRewards } from '../DistributionManager/data-helpers/base-math'; 12 | 13 | type ScenarioAction = { 14 | caseName: string; 15 | emissionPerSecond?: string; 16 | userBalance: string; 17 | totalSupply: string; 18 | customTimeMovement?: number; 19 | }; 20 | 21 | const handleActionScenarios: ScenarioAction[] = [ 22 | { 23 | caseName: 'All 0', 24 | emissionPerSecond: '0', 25 | userBalance: '0', 26 | totalSupply: '0', 27 | }, 28 | { 29 | caseName: 'Accrued rewards are 0, 0 emission', 30 | emissionPerSecond: '0', 31 | userBalance: '22', 32 | totalSupply: '22', 33 | }, 34 | { 35 | caseName: 'Accrued rewards are 0, 0 user balance', 36 | emissionPerSecond: '100', 37 | userBalance: '0', 38 | totalSupply: '22', 39 | }, 40 | { 41 | caseName: '1. Accrued rewards are not 0', 42 | userBalance: '22', 43 | totalSupply: '22', 44 | }, 45 | { 46 | caseName: '2. Accrued rewards are not 0', 47 | emissionPerSecond: '1000', 48 | userBalance: '2332', 49 | totalSupply: '3232', 50 | }, 51 | ]; 52 | 53 | makeSuite('AaveIncentivesController handleAction tests', (testEnv) => { 54 | for (const { 55 | caseName, 56 | totalSupply, 57 | userBalance, 58 | customTimeMovement, 59 | emissionPerSecond, 60 | } of handleActionScenarios) { 61 | it(caseName, async () => { 62 | await increaseTime(100); 63 | 64 | const { aaveIncentivesController, users, aDaiMock } = testEnv; 65 | const userAddress = users[1].address; 66 | const underlyingAsset = aDaiMock.address; 67 | 68 | // update emissionPerSecond in advance to not affect user calculations 69 | if (emissionPerSecond) { 70 | await aaveIncentivesController.configureAssets([underlyingAsset], [emissionPerSecond]); 71 | } 72 | 73 | const distributionEndTimestamp = await aaveIncentivesController.DISTRIBUTION_END(); 74 | 75 | const rewardsBalanceBefore = await aaveIncentivesController.getUserUnclaimedRewards( 76 | userAddress 77 | ); 78 | const userIndexBefore = await getUserIndex( 79 | aaveIncentivesController, 80 | userAddress, 81 | underlyingAsset 82 | ); 83 | const assetDataBefore = (await getAssetsData(aaveIncentivesController, [underlyingAsset]))[0]; 84 | 85 | if (customTimeMovement) { 86 | await increaseTime(customTimeMovement); 87 | } 88 | 89 | await waitForTx(await aDaiMock.setUserBalanceAndSupply(userBalance, totalSupply)); 90 | const handleActionReceipt = await waitForTx( 91 | await aDaiMock.handleActionOnAic(userAddress, totalSupply, userBalance) 92 | ); 93 | const eventsEmitted = handleActionReceipt.events || []; 94 | const actionBlockTimestamp = await getBlockTimestamp(handleActionReceipt.blockNumber); 95 | 96 | const userIndexAfter = await getUserIndex( 97 | aaveIncentivesController, 98 | userAddress, 99 | underlyingAsset 100 | ); 101 | 102 | const assetDataAfter = (await getAssetsData(aaveIncentivesController, [underlyingAsset]))[0]; 103 | 104 | const expectedAccruedRewards = getRewards( 105 | userBalance, 106 | userIndexAfter, 107 | userIndexBefore 108 | ).toString(); 109 | 110 | const rewardsBalanceAfter = await aaveIncentivesController.getUserUnclaimedRewards( 111 | userAddress 112 | ); 113 | 114 | // ------- Distribution Manager tests START ----- 115 | await assetDataComparator( 116 | { underlyingAsset, totalStaked: totalSupply }, 117 | assetDataBefore, 118 | assetDataAfter, 119 | actionBlockTimestamp, 120 | distributionEndTimestamp.toNumber(), 121 | {} 122 | ); 123 | expect(userIndexAfter.toString()).to.be.equal( 124 | assetDataAfter.index.toString(), 125 | 'user index are not correctly updated' 126 | ); 127 | if (!assetDataAfter.index.eq(assetDataBefore.index)) { 128 | const eventAssetUpdated = eventsEmitted.find(({ event }) => event === 'AssetIndexUpdated'); 129 | const eventUserIndexUpdated = eventsEmitted.find( 130 | ({ event }) => event === 'UserIndexUpdated' 131 | ); 132 | 133 | if (!eventAssetUpdated) { 134 | fail('missing AssetIndexUpdated event'); 135 | } 136 | if (!eventUserIndexUpdated) { 137 | fail('missing UserIndexUpdated event'); 138 | } 139 | eventChecker(eventAssetUpdated, 'AssetIndexUpdated', [ 140 | assetDataAfter.underlyingAsset, 141 | assetDataAfter.index, 142 | ]); 143 | eventChecker(eventUserIndexUpdated, 'UserIndexUpdated', [ 144 | userAddress, 145 | assetDataAfter.underlyingAsset, 146 | assetDataAfter.index, 147 | ]); 148 | } 149 | // ------- Distribution Manager tests END ----- 150 | 151 | // ------- PEI tests START ----- 152 | expect(rewardsBalanceAfter.toString()).to.be.equal( 153 | rewardsBalanceBefore.add(expectedAccruedRewards).toString(), 154 | 'rewards balance are incorrect' 155 | ); 156 | if (expectedAccruedRewards !== '0') { 157 | const eventAssetUpdated = eventsEmitted.find(({ event }) => event === 'RewardsAccrued'); 158 | if (!eventAssetUpdated) { 159 | fail('missing RewardsAccrued event'); 160 | } 161 | eventChecker(eventAssetUpdated, 'RewardsAccrued', [userAddress, expectedAccruedRewards]); 162 | } 163 | // ------- PEI tests END ----- 164 | }); 165 | } 166 | }); 167 | -------------------------------------------------------------------------------- /test/StakedIncentivesController/initialize.spec.ts: -------------------------------------------------------------------------------- 1 | import { makeSuite, TestEnv } from '../helpers/make-suite'; 2 | import { MAX_UINT_AMOUNT, ZERO_ADDRESS } from '../../helpers/constants'; 3 | 4 | const { expect } = require('chai'); 5 | 6 | makeSuite('AaveIncentivesController initialize', (testEnv: TestEnv) => { 7 | // TODO: useless or not? 8 | it('Tries to call initialize second time, should be reverted', async () => { 9 | const { aaveIncentivesController } = testEnv; 10 | await expect(aaveIncentivesController.initialize()).to.be.reverted; 11 | }); 12 | it('allowance on aave token should be granted to psm contract for pei', async () => { 13 | const { aaveIncentivesController, stakedAave, aaveToken } = testEnv; 14 | await expect( 15 | (await aaveToken.allowance(aaveIncentivesController.address, stakedAave.address)).toString() 16 | ).to.be.equal(MAX_UINT_AMOUNT); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/StakedIncentivesController/misc.spec.ts: -------------------------------------------------------------------------------- 1 | import { timeLatest, waitForTx } from '../../helpers/misc-utils'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { makeSuite } from '../helpers/make-suite'; 6 | import { deployAaveIncentivesController } from '../../helpers/contracts-accessors'; 7 | import { MAX_UINT_AMOUNT, RANDOM_ADDRESSES, ZERO_ADDRESS } from '../../helpers/constants'; 8 | 9 | makeSuite('AaveIncentivesController misc tests', (testEnv) => { 10 | it('constructor should assign correct params', async () => { 11 | const peiEmissionManager = RANDOM_ADDRESSES[1]; 12 | const psm = RANDOM_ADDRESSES[5]; 13 | 14 | const aaveIncentivesController = await deployAaveIncentivesController([ 15 | psm, 16 | peiEmissionManager, 17 | ]); 18 | await expect(await aaveIncentivesController.STAKE_TOKEN()).to.be.equal(psm); 19 | await expect((await aaveIncentivesController.EMISSION_MANAGER()).toString()).to.be.equal( 20 | peiEmissionManager 21 | ); 22 | }); 23 | 24 | it('Should return same index while multiple asset index updates', async () => { 25 | const { aDaiMock, aaveIncentivesController, users } = testEnv; 26 | await waitForTx(await aaveIncentivesController.configureAssets([aDaiMock.address], ['100'])); 27 | await waitForTx(await aDaiMock.doubleHandleActionOnAic(users[1].address, '2000', '100')); 28 | }); 29 | 30 | it('Should overflow index if passed a large emission', async () => { 31 | const { aDaiMock, aaveIncentivesController, users } = testEnv; 32 | const MAX_104_UINT = '20282409603651670423947251286015'; 33 | 34 | await waitForTx( 35 | await aaveIncentivesController.configureAssets([aDaiMock.address], [MAX_104_UINT]) 36 | ); 37 | await expect( 38 | aDaiMock.doubleHandleActionOnAic(users[1].address, '2000', '100') 39 | ).to.be.revertedWith('Index overflow'); 40 | }); 41 | 42 | it('Should configureAssets revert if parameters length does not match', async () => { 43 | const { aDaiMock, aaveIncentivesController } = testEnv; 44 | 45 | await expect( 46 | aaveIncentivesController.configureAssets([aDaiMock.address], ['1', '2']) 47 | ).to.be.revertedWith('INVALID_CONFIGURATION'); 48 | }); 49 | 50 | it('Should configureAssets revert if emission parameter overflows uin104', async () => { 51 | const { aDaiMock, aaveIncentivesController } = testEnv; 52 | 53 | await expect( 54 | aaveIncentivesController.configureAssets([aDaiMock.address], [MAX_UINT_AMOUNT]) 55 | ).to.be.revertedWith('Index overflow at emissionsPerSecond'); 56 | }); 57 | 58 | it('Should REWARD_TOKEN getter returns the stake token address to keep old interface compatibility', async () => { 59 | const { aaveIncentivesController, stakedAave } = testEnv; 60 | await expect(await aaveIncentivesController.REWARD_TOKEN()).to.be.equal(stakedAave.address); 61 | }); 62 | 63 | it('Should claimRewards revert if to argument is ZERO_ADDRESS', async () => { 64 | const { aaveIncentivesController, users, aDaiMock, stakedAave } = testEnv; 65 | const [userWithRewards] = users; 66 | 67 | await waitForTx(await aaveIncentivesController.configureAssets([aDaiMock.address], ['2000'])); 68 | await waitForTx(await aDaiMock.setUserBalanceAndSupply('300000', '30000')); 69 | 70 | // Claim from third party claimer 71 | await expect( 72 | aaveIncentivesController 73 | .connect(userWithRewards.signer) 74 | .claimRewards([aDaiMock.address], MAX_UINT_AMOUNT, ZERO_ADDRESS) 75 | ).to.be.revertedWith('INVALID_TO_ADDRESS'); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/__setup.spec.ts: -------------------------------------------------------------------------------- 1 | import rawBRE from 'hardhat'; 2 | import { Signer, ethers } from 'ethers'; 3 | import { getBlockTimestamp, getEthersSigners } from '../helpers/contracts-helpers'; 4 | import { initializeMakeSuite } from './helpers/make-suite'; 5 | import { deployMintableErc20, deployATokenMock } from '../helpers/contracts-accessors'; 6 | import { DRE, waitForTx } from '../helpers/misc-utils'; 7 | import { MintableErc20 } from '../types/MintableErc20'; 8 | import { testDeployIncentivesController } from './helpers/deploy'; 9 | import { 10 | PullRewardsIncentivesController, 11 | PullRewardsIncentivesController__factory, 12 | StakedAaveV3__factory, 13 | StakedTokenIncentivesController__factory, 14 | } from '../types'; 15 | import { parseEther } from '@ethersproject/units'; 16 | import { hrtime } from 'process'; 17 | import { MAX_UINT_AMOUNT } from '../helpers/constants'; 18 | 19 | const topUpWalletsWithAave = async ( 20 | wallets: Signer[], 21 | aaveToken: MintableErc20, 22 | amount: string 23 | ) => { 24 | for (const wallet of wallets) { 25 | await waitForTx(await aaveToken.connect(wallet).mint(amount)); 26 | } 27 | }; 28 | 29 | const buildTestEnv = async ( 30 | deployer: Signer, 31 | vaultOfRewards: Signer, 32 | proxyAdmin: Signer, 33 | restWallets: Signer[] 34 | ) => { 35 | console.time('setup'); 36 | 37 | const aaveToken = await deployMintableErc20(['Aave', 'aave']); 38 | 39 | await waitForTx(await aaveToken.connect(vaultOfRewards).mint(ethers.utils.parseEther('2000000'))); 40 | await topUpWalletsWithAave( 41 | [restWallets[0], restWallets[1], restWallets[2], restWallets[3], restWallets[4]], 42 | aaveToken, 43 | ethers.utils.parseEther('100').toString() 44 | ); 45 | 46 | const { incentivesProxy, stakeProxy } = await testDeployIncentivesController( 47 | deployer, 48 | vaultOfRewards, 49 | proxyAdmin, 50 | aaveToken 51 | ); 52 | const { proxy: baseIncentivesProxy } = await DRE.run('deploy-pull-rewards-incentives', { 53 | emissionManager: await deployer.getAddress(), 54 | rewardToken: aaveToken.address, 55 | rewardsVault: await vaultOfRewards.getAddress(), 56 | proxyAdmin: await proxyAdmin.getAddress(), 57 | }); 58 | 59 | await waitForTx( 60 | await aaveToken.connect(vaultOfRewards).approve(baseIncentivesProxy, MAX_UINT_AMOUNT) 61 | ); 62 | 63 | const distributionDuration = ((await getBlockTimestamp()) + 1000 * 60 * 60).toString(); 64 | await deployATokenMock(incentivesProxy.address, 'aDai'); 65 | await deployATokenMock(incentivesProxy.address, 'aWeth'); 66 | 67 | await deployATokenMock(baseIncentivesProxy, 'aDaiBase'); 68 | await deployATokenMock(baseIncentivesProxy, 'aWethBase'); 69 | 70 | const incentivesController = StakedTokenIncentivesController__factory.connect( 71 | incentivesProxy.address, 72 | deployer 73 | ); 74 | const pullRewardsIncentivesController = PullRewardsIncentivesController__factory.connect( 75 | baseIncentivesProxy, 76 | deployer 77 | ); 78 | 79 | await incentivesController.setDistributionEnd(distributionDuration); 80 | await pullRewardsIncentivesController.setDistributionEnd(distributionDuration); 81 | await waitForTx( 82 | await aaveToken 83 | .connect(vaultOfRewards) 84 | .transfer(incentivesController.address, parseEther('1000000')) 85 | ); 86 | 87 | console.timeEnd('setup'); 88 | 89 | return { 90 | aaveToken, 91 | incentivesController, 92 | pullRewardsIncentivesController, 93 | aaveStake: StakedAaveV3__factory.connect(stakeProxy.address, deployer), 94 | }; 95 | }; 96 | 97 | before(async () => { 98 | await rawBRE.run('set-DRE'); 99 | const [deployer, proxyAdmin, rewardsVault, ...restWallets] = await getEthersSigners(); 100 | const { 101 | aaveToken, 102 | aaveStake, 103 | incentivesController, 104 | pullRewardsIncentivesController, 105 | } = await buildTestEnv(deployer, rewardsVault, proxyAdmin, restWallets); 106 | await initializeMakeSuite( 107 | aaveToken, 108 | aaveStake, 109 | incentivesController, 110 | pullRewardsIncentivesController 111 | ); 112 | console.log('\n***************'); 113 | console.log('Setup and snapshot finished'); 114 | console.log('***************\n'); 115 | }); 116 | -------------------------------------------------------------------------------- /test/helpers/comparator-engine.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, Event } from 'ethers'; 2 | 3 | const { expect } = require('chai'); 4 | 5 | interface CustomFieldLogic { 6 | fieldName: keyof State; 7 | logic: ( 8 | stateUpdate: Update, 9 | stateBefore: State, 10 | stateAfter: State, 11 | txTimestamp: number 12 | ) => Promise | string | BigNumber; 13 | } 14 | 15 | export interface CompareRules { 16 | fieldsEqualToInput?: (keyof State)[]; 17 | fieldsEqualToAnother?: { fieldName: keyof State; equalTo: keyof Update }[]; 18 | fieldsWithCustomLogic?: CustomFieldLogic[]; 19 | } 20 | 21 | export async function comparatorEngine( 22 | fieldsToTrack: (keyof State)[], 23 | updateInput: Input, 24 | stateBefore: State, 25 | stateAfter: State, 26 | actionBlockTimestamp: number, 27 | { 28 | fieldsEqualToInput = [], 29 | fieldsEqualToAnother = [], 30 | fieldsWithCustomLogic = [], 31 | }: CompareRules 32 | ) { 33 | const unchangedFields = fieldsToTrack.filter( 34 | (fieldName) => 35 | !fieldsEqualToInput.includes(fieldName) && 36 | !fieldsEqualToAnother.find((eq) => eq.fieldName === fieldName) && 37 | !fieldsWithCustomLogic.find((eq) => eq.fieldName === fieldName) 38 | ); 39 | 40 | for (const fieldName of unchangedFields) { 41 | // @ts-ignore 42 | expect(stateAfter[fieldName].toString()).to.be.equal( 43 | // @ts-ignore 44 | stateBefore[fieldName].toString(), 45 | `${fieldName} should not change` 46 | ); 47 | } 48 | 49 | for (const fieldName of fieldsEqualToInput) { 50 | // @ts-ignore 51 | expect(stateAfter[fieldName].toString()).to.be.equal( 52 | // @ts-ignore 53 | updateInput[fieldName].toString(), 54 | `${fieldName} are not updated` 55 | ); 56 | } 57 | 58 | for (const { fieldName, equalTo } of fieldsEqualToAnother) { 59 | // @ts-ignore 60 | expect(stateAfter[fieldName].toString()).to.be.equal( 61 | // @ts-ignore 62 | updateInput[equalTo].toString(), 63 | `${fieldName} are not updated` 64 | ); 65 | } 66 | 67 | for (const { fieldName, logic } of fieldsWithCustomLogic) { 68 | const logicOutput = logic(updateInput, stateBefore, stateAfter, actionBlockTimestamp); 69 | const equalTo = logicOutput instanceof Promise ? await logicOutput : logicOutput; 70 | // @ts-ignore 71 | expect(stateAfter[fieldName].toString()).to.be.equal( 72 | equalTo.toString(), 73 | `${fieldName} are not correctly updated` 74 | ); 75 | } 76 | } 77 | 78 | export function eventChecker(event: Event, name: string, args: any[] = []): void { 79 | expect(event.event).to.be.equal(name, `Incorrect event emitted`); 80 | expect(event.args?.length || 0 / 2).to.be.equal(args.length, `${name} signature are wrong`); 81 | args.forEach((arg, index) => { 82 | expect(event.args && event.args[index].toString()).to.be.equal( 83 | arg.toString(), 84 | `${name} has incorrect value on position ${index}` 85 | ); 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /test/helpers/deploy.ts: -------------------------------------------------------------------------------- 1 | import { Signer } from 'ethers'; 2 | import { ZERO_ADDRESS } from '../../helpers/constants'; 3 | import { 4 | deployAaveIncentivesController, 5 | deployInitializableAdminUpgradeabilityProxy, 6 | deployMintableErc20, 7 | } from '../../helpers/contracts-accessors'; 8 | import { getFirstSigner, insertContractAddressInDb } from '../../helpers/contracts-helpers'; 9 | import { verifyContract } from '../../helpers/etherscan-verification'; 10 | import { eContractid, tEthereumAddress } from '../../helpers/types'; 11 | import { MintableErc20, StakedAaveV3__factory } from '../../types'; 12 | 13 | export const COOLDOWN_SECONDS = '3600'; // 1 hour in seconds 14 | export const UNSTAKE_WINDOW = '1800'; // 30 min in second 15 | 16 | export const testDeployIncentivesController = async ( 17 | emissionManager: Signer, 18 | vaultOfRewards: Signer, 19 | proxyAdmin: Signer, 20 | aaveToken: MintableErc20 21 | ) => { 22 | const emissionManagerAddress = await emissionManager.getAddress(); 23 | // Deploy proxies and implementations 24 | const stakeProxy = await deployInitializableAdminUpgradeabilityProxy(); 25 | const incentivesProxy = await deployInitializableAdminUpgradeabilityProxy(); 26 | 27 | const aaveStakeV3 = await deployStakedAaveV3([ 28 | aaveToken.address, 29 | aaveToken.address, 30 | COOLDOWN_SECONDS, 31 | UNSTAKE_WINDOW, 32 | await vaultOfRewards.getAddress(), 33 | emissionManagerAddress, 34 | (1000 * 60 * 60).toString(), 35 | ]); 36 | 37 | const incentivesImplementation = await deployAaveIncentivesController([ 38 | stakeProxy.address, 39 | emissionManagerAddress, 40 | ]); 41 | 42 | // Initialize proxies 43 | const aaveStakeInit = aaveStakeV3.interface.encodeFunctionData( 44 | // @ts-ignore 45 | 'initialize(address,address,address,uint256,string,string,uint8)', 46 | [ 47 | emissionManagerAddress, 48 | emissionManagerAddress, 49 | emissionManagerAddress, 50 | '2000', 51 | 'Staked AAVE', 52 | 'stkAAVE', 53 | '18', 54 | ] 55 | ); 56 | const incentivesInit = incentivesImplementation.interface.encodeFunctionData('initialize'); 57 | 58 | await ( 59 | await stakeProxy['initialize(address,address,bytes)']( 60 | aaveStakeV3.address, 61 | await proxyAdmin.getAddress(), 62 | aaveStakeInit 63 | ) 64 | ).wait(); 65 | await ( 66 | await incentivesProxy['initialize(address,address,bytes)']( 67 | incentivesImplementation.address, 68 | await proxyAdmin.getAddress(), 69 | incentivesInit 70 | ) 71 | ).wait(); 72 | 73 | await insertContractAddressInDb(eContractid.AaveIncentivesController, incentivesProxy.address); 74 | 75 | return { incentivesProxy, stakeProxy }; 76 | }; 77 | 78 | export const deployStakedAaveV3 = async ( 79 | [ 80 | stakedToken, 81 | rewardsToken, 82 | cooldownSeconds, 83 | unstakeWindow, 84 | rewardsVault, 85 | emissionManager, 86 | distributionDuration, 87 | ]: [ 88 | tEthereumAddress, 89 | tEthereumAddress, 90 | string, 91 | string, 92 | tEthereumAddress, 93 | tEthereumAddress, 94 | string 95 | ], 96 | verify?: boolean 97 | ) => { 98 | const id = eContractid.StakedAaveV3; 99 | const args: string[] = [ 100 | stakedToken, 101 | rewardsToken, 102 | cooldownSeconds, 103 | unstakeWindow, 104 | rewardsVault, 105 | emissionManager, 106 | distributionDuration, 107 | ZERO_ADDRESS, // gov address 108 | ]; 109 | const instance = await new StakedAaveV3__factory(await getFirstSigner()).deploy( 110 | stakedToken, 111 | rewardsToken, 112 | cooldownSeconds, 113 | unstakeWindow, 114 | rewardsVault, 115 | emissionManager, 116 | distributionDuration, 117 | ZERO_ADDRESS // gov address); 118 | ); 119 | if (verify) { 120 | await verifyContract(instance.address, args); 121 | } 122 | return instance; 123 | }; 124 | -------------------------------------------------------------------------------- /test/helpers/make-suite.ts: -------------------------------------------------------------------------------- 1 | import { evmRevert, evmSnapshot, DRE } from '../../helpers/misc-utils'; 2 | import { Signer } from 'ethers'; 3 | import { getEthersSigners } from '../../helpers/contracts-helpers'; 4 | import { tEthereumAddress } from '../../helpers/types'; 5 | 6 | import chai from 'chai'; 7 | // @ts-ignore 8 | import bignumberChai from 'chai-bignumber'; 9 | import { getATokenMock } from '../../helpers/contracts-accessors'; 10 | import { MintableErc20 } from '../../types/MintableErc20'; 11 | import { ATokenMock } from '../../types/ATokenMock'; 12 | import { 13 | PullRewardsIncentivesController, 14 | PullRewardsIncentivesController__factory, 15 | StakedAaveV3, 16 | StakedTokenIncentivesController, 17 | } from '../../types'; 18 | 19 | chai.use(bignumberChai()); 20 | 21 | export let stakedAaveInitializeTimestamp = 0; 22 | export const setStakedAaveInitializeTimestamp = (timestamp: number) => { 23 | stakedAaveInitializeTimestamp = timestamp; 24 | }; 25 | 26 | export interface SignerWithAddress { 27 | signer: Signer; 28 | address: tEthereumAddress; 29 | } 30 | export interface TestEnv { 31 | rewardsVault: SignerWithAddress; 32 | deployer: SignerWithAddress; 33 | users: SignerWithAddress[]; 34 | aaveToken: MintableErc20; 35 | aaveIncentivesController: StakedTokenIncentivesController; 36 | pullRewardsIncentivesController: PullRewardsIncentivesController; 37 | stakedAave: StakedAaveV3; 38 | aDaiMock: ATokenMock; 39 | aWethMock: ATokenMock; 40 | aDaiBaseMock: ATokenMock; 41 | aWethBaseMock: ATokenMock; 42 | } 43 | 44 | let buidlerevmSnapshotId: string = '0x1'; 45 | const setBuidlerevmSnapshotId = (id: string) => { 46 | if (DRE.network.name === 'hardhat') { 47 | buidlerevmSnapshotId = id; 48 | } 49 | }; 50 | 51 | const testEnv: TestEnv = { 52 | deployer: {} as SignerWithAddress, 53 | users: [] as SignerWithAddress[], 54 | aaveToken: {} as MintableErc20, 55 | stakedAave: {} as StakedAaveV3, 56 | aaveIncentivesController: {} as StakedTokenIncentivesController, 57 | pullRewardsIncentivesController: {} as PullRewardsIncentivesController, 58 | aDaiMock: {} as ATokenMock, 59 | aWethMock: {} as ATokenMock, 60 | aDaiBaseMock: {} as ATokenMock, 61 | aWethBaseMock: {} as ATokenMock, 62 | } as TestEnv; 63 | 64 | export async function initializeMakeSuite( 65 | aaveToken: MintableErc20, 66 | stakedAave: StakedAaveV3, 67 | aaveIncentivesController: StakedTokenIncentivesController, 68 | pullRewardsIncentivesController: PullRewardsIncentivesController 69 | ) { 70 | const [_deployer, _proxyAdmin, ...restSigners] = await getEthersSigners(); 71 | const deployer: SignerWithAddress = { 72 | address: await _deployer.getAddress(), 73 | signer: _deployer, 74 | }; 75 | 76 | const rewardsVault: SignerWithAddress = { 77 | address: await _deployer.getAddress(), 78 | signer: _deployer, 79 | }; 80 | 81 | for (const signer of restSigners) { 82 | testEnv.users.push({ 83 | signer, 84 | address: await signer.getAddress(), 85 | }); 86 | } 87 | testEnv.deployer = deployer; 88 | testEnv.rewardsVault = rewardsVault; 89 | testEnv.stakedAave = stakedAave; 90 | testEnv.aaveIncentivesController = aaveIncentivesController; 91 | testEnv.pullRewardsIncentivesController = pullRewardsIncentivesController; 92 | testEnv.aaveToken = aaveToken; 93 | testEnv.aDaiMock = await getATokenMock({ slug: 'aDai' }); 94 | testEnv.aWethMock = await getATokenMock({ slug: 'aWeth' }); 95 | testEnv.aDaiBaseMock = await getATokenMock({ slug: 'aDaiBase' }); 96 | testEnv.aWethBaseMock = await getATokenMock({ slug: 'aWethBase' }); 97 | } 98 | 99 | export function makeSuite(name: string, tests: (testEnv: TestEnv) => void) { 100 | describe(name, () => { 101 | before(async () => { 102 | setBuidlerevmSnapshotId(await evmSnapshot()); 103 | }); 104 | tests(testEnv); 105 | after(async () => { 106 | await evmRevert(buidlerevmSnapshotId); 107 | }); 108 | }); 109 | } 110 | -------------------------------------------------------------------------------- /test/helpers/ray-math/bignumber.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import { BigNumber as BigNumberEthers, BigNumberish } from 'ethers'; 3 | 4 | export type BigNumberValue = string | number | BigNumber | BigNumberEthers | BigNumberish; 5 | 6 | export const BigNumberZD = BigNumber.clone({ 7 | DECIMAL_PLACES: 0, 8 | ROUNDING_MODE: BigNumber.ROUND_DOWN, 9 | }); 10 | 11 | export function valueToBigNumber(amount: BigNumberValue): BigNumber { 12 | return new BigNumber(amount.toString()); 13 | } 14 | export function valueToZDBigNumber(amount: BigNumberValue): BigNumber { 15 | return new BigNumberZD(amount.toString()); 16 | } 17 | -------------------------------------------------------------------------------- /test/helpers/ray-math/index.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import { BigNumberValue, valueToZDBigNumber } from './bignumber'; 3 | 4 | export function getLinearCumulatedRewards( 5 | emissionPerSecond: BigNumberValue, 6 | lastUpdateTimestamp: BigNumberValue, 7 | currentTimestamp: BigNumberValue 8 | ): BigNumber { 9 | const timeDelta = valueToZDBigNumber(currentTimestamp).minus(lastUpdateTimestamp.toString()); 10 | return timeDelta.multipliedBy(emissionPerSecond.toString()); 11 | } 12 | 13 | export function getNormalizedDistribution( 14 | balance: BigNumberValue, 15 | oldIndex: BigNumberValue, 16 | emissionPerSecond: BigNumberValue, 17 | lastUpdateTimestamp: BigNumberValue, 18 | currentTimestamp: BigNumberValue, 19 | emissionEndTimestamp: BigNumberValue, 20 | precision: number = 18 21 | ): BigNumber { 22 | if ( 23 | balance.toString() === '0' || 24 | emissionPerSecond.toString() === '0' || 25 | valueToZDBigNumber(lastUpdateTimestamp).gte(emissionEndTimestamp.toString()) 26 | ) { 27 | return valueToZDBigNumber(oldIndex); 28 | } 29 | const linearReward = getLinearCumulatedRewards( 30 | emissionPerSecond, 31 | lastUpdateTimestamp, 32 | valueToZDBigNumber(currentTimestamp).gte(emissionEndTimestamp.toString()) 33 | ? emissionEndTimestamp 34 | : currentTimestamp 35 | ); 36 | 37 | return linearReward 38 | .multipliedBy(valueToZDBigNumber(10).exponentiatedBy(precision)) 39 | .div(balance.toString()) 40 | .plus(oldIndex.toString()); 41 | } 42 | -------------------------------------------------------------------------------- /test/helpers/ray-math/ray-math.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import { BigNumberValue, valueToZDBigNumber } from './bignumber'; 3 | 4 | export const WAD = valueToZDBigNumber(10).pow(18); 5 | export const HALF_WAD = WAD.dividedBy(2); 6 | 7 | export const RAY = valueToZDBigNumber(10).pow(27); 8 | export const HALF_RAY = RAY.dividedBy(2); 9 | 10 | export const WAD_RAY_RATIO = valueToZDBigNumber(10).pow(9); 11 | 12 | export function wadMul(a: BigNumberValue, b: BigNumberValue): BigNumber { 13 | return HALF_WAD.plus(valueToZDBigNumber(a).multipliedBy(b.toString())).div(WAD); 14 | } 15 | 16 | export function wadDiv(a: BigNumberValue, b: BigNumberValue): BigNumber { 17 | const halfB = valueToZDBigNumber(b).div(2); 18 | 19 | return halfB.plus(valueToZDBigNumber(a).multipliedBy(WAD)).div(b.toString()); 20 | } 21 | 22 | export function rayMul(a: BigNumberValue, b: BigNumberValue): BigNumber { 23 | return HALF_RAY.plus(valueToZDBigNumber(a).multipliedBy(b.toString())).div(RAY); 24 | } 25 | 26 | export function rayDiv(a: BigNumberValue, b: BigNumberValue): BigNumber { 27 | const halfB = valueToZDBigNumber(b).div(2); 28 | 29 | return halfB.plus(valueToZDBigNumber(a).multipliedBy(RAY)).div(b.toString()); 30 | } 31 | 32 | export function rayToWad(a: BigNumberValue): BigNumber { 33 | const halfRatio = valueToZDBigNumber(WAD_RAY_RATIO).div(2); 34 | 35 | return halfRatio.plus(a.toString()).div(WAD_RAY_RATIO); 36 | } 37 | 38 | export function wadToRay(a: BigNumberValue): BigNumber { 39 | return valueToZDBigNumber(a).multipliedBy(WAD_RAY_RATIO).decimalPlaces(0); 40 | } 41 | 42 | export function rayPow(a: BigNumberValue, p: BigNumberValue): BigNumber { 43 | let x = valueToZDBigNumber(a); 44 | let n = valueToZDBigNumber(p); 45 | let z = !n.modulo(2).eq(0) ? x : valueToZDBigNumber(RAY); 46 | 47 | for (n = n.div(2); !n.eq(0); n = n.div(2)) { 48 | x = rayMul(x, x); 49 | 50 | if (!n.modulo(2).eq(0)) { 51 | z = rayMul(z, x); 52 | } 53 | } 54 | return z; 55 | } 56 | 57 | export function rayToDecimal(a: BigNumberValue): BigNumber { 58 | return valueToZDBigNumber(a).dividedBy(RAY); 59 | } 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "noImplicitAny": false, 9 | "resolveJsonModule": true 10 | }, 11 | "include": ["./scripts", "./helpers", "./test-fork", "./test", "./tasks", "./types"], 12 | "files": [ 13 | "./hardhat.config.ts", 14 | "node_modules/@nomiclabs/buidler-ethers/src/type-extensions.d.ts", 15 | "node_modules/buidler-typechain/src/type-extensions.d.ts", 16 | "node_modules/@nomiclabs/buidler-waffle/src/type-extensions.d.ts", 17 | "node_modules/@nomiclabs/buidler-etherscan/src/type-extensions.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-prettier"], 3 | "rulesDirectory": ["tslint-plugin-prettier"], 4 | "rules": { 5 | "prettier": true, 6 | "max-line-length": [true, 100], 7 | "import-name": false 8 | }, 9 | "linterOptions": { 10 | "exclude": ["src/migration/**", "src/contracts/ABI/**"] 11 | } 12 | } 13 | --------------------------------------------------------------------------------