├── .nvmrc ├── .prettierignore ├── .gitattributes ├── .solhintignore ├── .yarnrc.yml ├── .prettierrc ├── scripts ├── package-version.sh ├── fork │ ├── index.ts │ ├── impersonateAccounts.ts │ └── distributeEtherFromBinance.ts ├── helpers.ts ├── generateDeployment.ts ├── runLifecycle.ts ├── createAndRunPrizePoolPolygon.ts └── createAndRunPrizePoolMumbai.ts ├── .solcover.js ├── aave ├── aaveMumbai.json ├── aavePolygon.json ├── aaveKovan.json └── aaveMainnet.json ├── aave.config.ts ├── contracts ├── external │ └── aave │ │ ├── ATokenInterface.sol │ │ ├── ILendingPoolAddressesProviderRegistry.sol │ │ ├── IProtocolYieldSource.sol │ │ ├── IProtocolDataProvider.sol │ │ ├── ILendingPool.sol │ │ ├── ILendingPoolAddressesProvider.sol │ │ ├── IAToken.sol │ │ └── IAaveIncentivesController.sol ├── test │ ├── ATokenMintable.sol │ ├── ERC20Mintable.sol │ ├── AaveLendingPool.sol │ ├── ATokenYieldSourceHarness.sol │ └── SafeERC20Wrapper.sol └── yield-source │ └── ATokenYieldSource.sol ├── .gitignore ├── .envrc.example ├── tsconfig.json ├── .github └── workflows │ └── main.yml ├── .solhint.json ├── Constant.ts ├── hardhat.config.ts ├── hardhat.network.ts ├── package.json ├── README.md └── test └── ATokenYieldSource.test.ts /.nvmrc: -------------------------------------------------------------------------------- 1 | 15.11.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | types 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/** linguist-vendored 2 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | contracts/hardhat-dependency-compiler 2 | contracts/external/aave -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: ".yarn/releases/yarn-berry.cjs" 2 | nodeLinker: node-modules 3 | enableGlobalCache: true 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "tabWidth": 2 6 | } 7 | -------------------------------------------------------------------------------- /scripts/package-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo $(cat package.json | grep \\\"version\\\" | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: [ 3 | 'external/aave', 4 | 'test/ERC20Mintable.sol', 5 | 'test/SafeERC20Wrapper.sol' 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /scripts/fork/index.ts: -------------------------------------------------------------------------------- 1 | export * as impersonateAccountsTask from './impersonateAccounts'; 2 | export * as distributeEtherFromBinanceTask from './distributeEtherFromBinance'; 3 | -------------------------------------------------------------------------------- /scripts/helpers.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export const info = (message: string) => console.log(chalk.dim(message)); 4 | export const success = (message: string) => console.log(chalk.green(message)); 5 | -------------------------------------------------------------------------------- /aave/aaveMumbai.json: -------------------------------------------------------------------------------- 1 | {"proto":[{"aTokenAddress":"0x7ec62b6fC19174255335C8f4346E0C2fcf870a6B","aTokenSymbol":"aAAVE","stableDebtTokenAddress":"", 2 | "variableDebtTokenAddress":"","symbol":"AAVE","address":"0x341d1f30e77D3FBfbD43D17183E2acb9dF25574E","decimals":18}]} -------------------------------------------------------------------------------- /aave.config.ts: -------------------------------------------------------------------------------- 1 | export const yieldSourceTokenSymbols: { [chainId: number]: string[] } = { 2 | 1: ['sUSD', 'BUSD', 'GUSD', 'USDT'], 3 | 42: ['AAVE', 'BAT', 'BUSD', 'DAI', 'ENJ', 'KNC', 'LINK', 'MANA', 'MKR'], 4 | 137: ['AAVE', 'DAI', 'USDC', 'USDT', 'WBTC', 'WETH', 'WMATIC'], 5 | 80001: ['AAVE'], 6 | }; 7 | -------------------------------------------------------------------------------- /contracts/external/aave/ATokenInterface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "./IAToken.sol"; 6 | 7 | interface ATokenInterface is IAToken { 8 | /** 9 | * @dev Returns the address of the underlying asset of this aToken (E.g. WETH for aWETH) 10 | **/ 11 | /* solhint-disable-next-line func-name-mixedcase */ 12 | function UNDERLYING_ASSET_ADDRESS() external view returns (address); 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.yarn/* 2 | !/.yarn/releases 3 | !/.yarn/plugins 4 | !/.yarn/sdks 5 | 6 | # Swap the comments on the following lines if you don't wish to use zero-installs 7 | # Documentation here: https://yarnpkg.com/features/zero-installs 8 | #!/.yarn/cache 9 | /.pnp.* 10 | 11 | .envrc 12 | 13 | abis 14 | artifacts 15 | cache 16 | node_modules 17 | types 18 | deployments/localhost/ 19 | 20 | # Solidity Coverage 21 | coverage 22 | coverage.json 23 | 24 | # VS Code 25 | .history -------------------------------------------------------------------------------- /.envrc.example: -------------------------------------------------------------------------------- 1 | # Used to deploy to testnets 2 | export HDWALLET_MNEMONIC='' 3 | 4 | # Used to deploy to testnets 5 | export INFURA_API_KEY='' 6 | 7 | # Used for verifying contracts on Etherscan 8 | export ETHERSCAN_API_KEY='' 9 | 10 | # Required for forking 11 | export ALCHEMY_URL='' 12 | export FORK_ENABLED='' 13 | 14 | # Used to hide deploy logs 15 | export HIDE_DEPLOY_LOG='' 16 | 17 | # Used to generate a gas usage report 18 | export REPORT_GAS='' 19 | export COINMARKETCAP_API_KEY='' 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "moduleResolution": "node", 8 | "forceConsistentCasingInFileNames": true, 9 | "resolveJsonModule": true, 10 | }, 11 | "include": [ 12 | "hardhat.config.ts", 13 | "hardhat.network.ts", 14 | "./deploy", 15 | "./scripts", 16 | "./test", 17 | "types/**/*", 18 | "Constant.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Coveralls 2 | 3 | on: ["push", "pull_request"] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Use Node.js 15.x 11 | uses: actions/setup-node@v2 12 | with: 13 | node-version: 15.x 14 | - name: yarn, compile, hint, coverage 15 | run: | 16 | yarn 17 | yarn compile 18 | yarn hint 19 | yarn coverage 20 | - name: Coveralls 21 | uses: coverallsapp/github-action@master 22 | with: 23 | github-token: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /contracts/test/ATokenMintable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "./ERC20Mintable.sol"; 6 | 7 | contract ATokenMintable is ERC20Mintable { 8 | address public immutable underlyingAssetAddress; 9 | 10 | constructor( 11 | address _underlyingAssetAddress, 12 | string memory _name, 13 | string memory _symbol, 14 | uint8 decimals_ 15 | ) ERC20Mintable(_name, _symbol, decimals_) { 16 | underlyingAssetAddress = _underlyingAssetAddress; 17 | } 18 | 19 | /* solhint-disable func-name-mixedcase */ 20 | function UNDERLYING_ASSET_ADDRESS() external view returns (address) { 21 | return underlyingAssetAddress; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": [], 4 | "rules": { 5 | "func-order": "off", 6 | "mark-callable-contracts": "off", 7 | "no-empty-blocks": "off", 8 | "compiler-version": ["error", "0.8.6"], 9 | "private-vars-leading-underscore": "off", 10 | "code-complexity": "warn", 11 | "const-name-snakecase": "warn", 12 | "function-max-lines": "warn", 13 | "func-visibility": ["warn",{"ignoreConstructors":true}], 14 | "max-line-length": ["warn", 160], 15 | "avoid-suicide": "error", 16 | "avoid-sha3": "warn", 17 | "not-rely-on-time": "off", 18 | "reason-string": ["warn", {"maxLength": 64}] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /contracts/test/ERC20Mintable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | contract ERC20Mintable is ERC20 { 8 | uint8 internal __decimals; 9 | 10 | constructor(string memory _name, string memory _symbol, uint8 _decimals) ERC20(_name, _symbol) { 11 | __decimals = _decimals; 12 | } 13 | 14 | function decimals() public override view returns (uint8) { 15 | return __decimals; 16 | } 17 | 18 | function mint(address account, uint256 amount) public returns (bool) { 19 | _mint(account, amount); 20 | return true; 21 | } 22 | 23 | function burn(address account, uint256 amount) public returns (bool) { 24 | _burn(account, amount); 25 | return true; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /contracts/test/AaveLendingPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | import { ATokenMintable } from "./ATokenMintable.sol"; 8 | 9 | contract AaveLendingPool { 10 | IERC20 public immutable token; 11 | ATokenMintable public immutable aToken; 12 | 13 | constructor( 14 | IERC20 _token, 15 | ATokenMintable _aToken 16 | ) { 17 | token = _token; 18 | aToken = _aToken; 19 | } 20 | 21 | /* solhint-disable no-unused-vars */ 22 | function deposit( 23 | address asset, 24 | uint256 amount, 25 | address onBehalfOf, 26 | uint16 referralCode 27 | ) external { 28 | IERC20(asset).transferFrom(msg.sender, address(this), amount); 29 | aToken.mint(onBehalfOf, amount); 30 | } 31 | 32 | function withdraw( 33 | address asset, 34 | uint256 amount, 35 | address to 36 | ) external returns (uint256) { 37 | aToken.burn(msg.sender, amount); 38 | IERC20(asset).transfer(to, amount); 39 | return amount; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /contracts/external/aave/ILendingPoolAddressesProviderRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.8.6; 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/external/aave/IProtocolYieldSource.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@pooltogether/yield-source-interface/contracts/IYieldSource.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | /// @title The interface used for all Yield Sources for the PoolTogether protocol 9 | /// @dev There are two privileged roles: the owner and the asset manager. The owner can configure the asset managers. 10 | interface IProtocolYieldSource is IYieldSource { 11 | /// @notice Allows the owner to transfer ERC20 tokens held by this contract to the target address. 12 | /// @dev This function is callable by the owner or asset manager. 13 | /// This function should not be able to transfer any tokens that represent user deposits. 14 | /// @param token The ERC20 token to transfer 15 | /// @param to The recipient of the tokens 16 | /// @param amount The amount of tokens to transfer 17 | function transferERC20(IERC20 token, address to, uint256 amount) external; 18 | 19 | /// @notice Allows someone to deposit into the yield source without receiving any shares. The deposited token will be the same as token() 20 | /// This allows anyone to distribute tokens among the share holders. 21 | function sponsor(uint256 amount) external; 22 | } 23 | -------------------------------------------------------------------------------- /scripts/generateDeployment.ts: -------------------------------------------------------------------------------- 1 | interface ProxyDeployment { 2 | address?: string; 3 | abi?: any; 4 | transactionHash?: string; 5 | receipt?: any; 6 | args?: any; 7 | bytecode?: string; 8 | } 9 | 10 | import { Contract } from 'ethers'; 11 | import { writeFileSync } from 'fs'; 12 | import { ethers, getNamedAccounts } from 'hardhat'; 13 | 14 | async function createDeployment() { 15 | const hash = '0x04e348a719e5e68b858ef78a9140af1e9440a4d48febc5c40a879cfd184757b5'; 16 | const receipt = await ethers.provider.getTransactionReceipt(hash); 17 | 18 | console.log('receipt is ', receipt); 19 | 20 | const { genericProxyFactory } = await getNamedAccounts(); 21 | let proxyFactoryContract: Contract = await ethers.getContractAt( 22 | 'GenericProxyFactory', 23 | genericProxyFactory, 24 | ); 25 | 26 | const createdEvent = proxyFactoryContract.interface.parseLog(receipt.logs[0]); 27 | 28 | let jsonObj: ProxyDeployment = { 29 | address: createdEvent.args.created, 30 | transactionHash: receipt.transactionHash, 31 | receipt: receipt, 32 | args: ``, 33 | bytecode: `${await ethers.provider.getCode(createdEvent.args.created)}`, 34 | }; 35 | 36 | writeFileSync(`./deployments/mainnet/aGUSD.json`, JSON.stringify(jsonObj), { 37 | encoding: 'utf8', 38 | flag: 'w', 39 | }); 40 | 41 | console.log('finished'); 42 | } 43 | 44 | createDeployment(); 45 | -------------------------------------------------------------------------------- /Constant.ts: -------------------------------------------------------------------------------- 1 | export const ADAI_ADDRESS_KOVAN = '0xdCf0aF9e59C002FA3AA091a46196b37530FD48a8'; 2 | export const LENDING_POOL_ADDRESSES_PROVIDER_REGISTRY_ADDRESS_KOVAN = 3 | '0x1E40B561EC587036f9789aF83236f057D1ed2A90'; 4 | 5 | export const DAI_ADDRESS_MAINNET = '0x6B175474E89094C44Da98b954EedeAC495271d0F'; 6 | export const ADAI_ADDRESS_MAINNET = '0x028171bCA77440897B824Ca71D1c56caC55b68A3'; 7 | export const LENDING_POOL_ADDRESSES_PROVIDER_REGISTRY_ADDRESS_MAINNET = 8 | '0x52D306e36E3B6B02c153d0266ff0f85d18BCD413'; 9 | 10 | export const INCENTIVES_CONTROLLER_ADDRESS_MAINNET = '0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5'; 11 | 12 | export const INCENTIVES_CONTROLLER_ADDRESS_POLYGON = '0x357D51124f59836DeD84c8a1730D72B749d8BC23'; 13 | 14 | export const BINANCE_ADDRESS = '0x564286362092D8e7936f0549571a803B203aAceD'; 15 | export const BINANCE7_ADDRESS = '0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8'; 16 | export const DAI_RICH_ADDRESS = '0xF977814e90dA44bFA03b6295A0616a897441aceC'; 17 | 18 | export const LARGE_GUSD_ADDRESS = '0xd2E801f22e3E85A12Dce545A9a6064cb04DE9C43'; 19 | export const GUSD_ADDRESS = '0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd'; 20 | 21 | export const LARGE_SUSD_ADDRESS = '0x3f5CE5FBFe3E9af3971dD833D26bA9b5C936f0bE'; 22 | export const SUSD_ADDRESS = '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51'; 23 | 24 | export const BUSD_ADDRESS = '0x4Fabb145d64652a948d72533023f6E7A623C7C53'; 25 | export const LARGE_BUSD_ADDRESS = '0xF977814e90dA44bFA03b6295A0616a897441aceC'; 26 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import '@nomiclabs/hardhat-etherscan'; 2 | import '@nomiclabs/hardhat-ethers'; 3 | import '@nomiclabs/hardhat-waffle'; 4 | import '@typechain/hardhat'; 5 | import 'hardhat-abi-exporter'; 6 | import 'hardhat-deploy'; 7 | import 'hardhat-deploy-ethers'; 8 | import 'hardhat-gas-reporter'; 9 | import 'solidity-coverage'; 10 | import 'hardhat-dependency-compiler'; 11 | 12 | import { HardhatUserConfig } from 'hardhat/config'; 13 | 14 | import * as forkTasks from './scripts/fork'; 15 | import networks from './hardhat.network'; 16 | 17 | const optimizerEnabled = !process.env.OPTIMIZER_DISABLED; 18 | 19 | const config: HardhatUserConfig = { 20 | abiExporter: { 21 | path: './abis', 22 | clear: true, 23 | flat: true, 24 | }, 25 | etherscan: { 26 | apiKey: process.env.ETHERSCAN_API_KEY, 27 | }, 28 | gasReporter: { 29 | currency: 'USD', 30 | gasPrice: 100, 31 | enabled: process.env.REPORT_GAS ? true : false, 32 | coinmarketcap: process.env.COINMARKETCAP_API_KEY, 33 | maxMethodDiff: 10, 34 | }, 35 | mocha: { 36 | timeout: 30000, 37 | }, 38 | networks, 39 | solidity: { 40 | compilers: [ 41 | { 42 | version: '0.8.6', 43 | settings: { 44 | optimizer: { 45 | enabled: optimizerEnabled, 46 | runs: 200, 47 | }, 48 | evmVersion: 'istanbul', 49 | }, 50 | }, 51 | ], 52 | }, 53 | typechain: { 54 | outDir: 'types', 55 | target: 'ethers-v5', 56 | }, 57 | }; 58 | 59 | forkTasks; 60 | 61 | export default config; 62 | -------------------------------------------------------------------------------- /scripts/fork/impersonateAccounts.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config'; 2 | 3 | import { 4 | BINANCE_ADDRESS, 5 | BINANCE7_ADDRESS, 6 | DAI_RICH_ADDRESS, 7 | LARGE_GUSD_ADDRESS, 8 | LARGE_BUSD_ADDRESS, 9 | LARGE_SUSD_ADDRESS, 10 | } from '../../Constant'; 11 | import { info, success } from '../helpers'; 12 | 13 | export default task('fork:impersonate-accounts', 'Impersonate accounts').setAction( 14 | async (taskArguments, hre) => { 15 | info('Impersonate accounts...'); 16 | 17 | await hre.network.provider.request({ 18 | method: 'hardhat_impersonateAccount', 19 | params: ['0x364d6D0333432C3Ac016Ca832fb8594A8cE43Ca6'], 20 | }); 21 | 22 | await hre.network.provider.request({ 23 | method: 'hardhat_impersonateAccount', 24 | params: [BINANCE_ADDRESS], 25 | }); 26 | 27 | await hre.network.provider.request({ 28 | method: 'hardhat_impersonateAccount', 29 | params: [BINANCE7_ADDRESS], 30 | }); 31 | 32 | await hre.network.provider.request({ 33 | method: 'hardhat_impersonateAccount', 34 | params: [DAI_RICH_ADDRESS], 35 | }); 36 | 37 | await hre.network.provider.request({ 38 | method: 'hardhat_impersonateAccount', 39 | params: [LARGE_GUSD_ADDRESS], 40 | }); 41 | 42 | await hre.network.provider.request({ 43 | method: 'hardhat_impersonateAccount', 44 | params: [LARGE_BUSD_ADDRESS], 45 | }); 46 | 47 | await hre.network.provider.request({ 48 | method: 'hardhat_impersonateAccount', 49 | params: [LARGE_SUSD_ADDRESS], 50 | }); 51 | 52 | success('Done!'); 53 | }, 54 | ); 55 | -------------------------------------------------------------------------------- /contracts/test/ATokenYieldSourceHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "../yield-source/ATokenYieldSource.sol"; 6 | import "../external/aave/ATokenInterface.sol"; 7 | import "../external/aave/ILendingPoolAddressesProviderRegistry.sol"; 8 | 9 | /* solium-disable security/no-block-members */ 10 | contract ATokenYieldSourceHarness is ATokenYieldSource { 11 | 12 | constructor ( 13 | ATokenInterface _aToken, 14 | IAaveIncentivesController _incentivesController, 15 | ILendingPoolAddressesProviderRegistry _lendingPoolAddressesProviderRegistry, 16 | uint8 _decimals, 17 | string memory _symbol, 18 | string memory _name, 19 | address _owner 20 | ) ATokenYieldSource(_aToken, _incentivesController, _lendingPoolAddressesProviderRegistry, _decimals, _symbol, _name, _owner) { 21 | 22 | } 23 | 24 | function approveLendingPool(uint256 amount) external { 25 | IERC20(aToken.UNDERLYING_ASSET_ADDRESS()).approve(address(_lendingPool()), amount); 26 | } 27 | 28 | function mint(address account, uint256 amount) public returns (bool) { 29 | _mint(account, amount); 30 | return true; 31 | } 32 | 33 | function tokenToShares(uint256 tokens) external view returns (uint256) { 34 | return _tokenToShares(tokens); 35 | } 36 | 37 | function sharesToToken(uint256 shares) external view returns (uint256) { 38 | return _sharesToToken(shares); 39 | } 40 | 41 | function tokenAddress() external view returns (address) { 42 | return aToken.UNDERLYING_ASSET_ADDRESS(); 43 | } 44 | 45 | function lendingPool() external view returns (ILendingPool) { 46 | return _lendingPool(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /contracts/external/aave/IProtocolDataProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.8.6; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import {ILendingPoolAddressesProvider} from "./ILendingPoolAddressesProvider.sol"; 6 | 7 | interface IProtocolDataProvider { 8 | struct TokenData { 9 | string symbol; 10 | address tokenAddress; 11 | } 12 | 13 | function ADDRESSES_PROVIDER() external view returns (ILendingPoolAddressesProvider); 14 | function getAllReservesTokens() external view returns (TokenData[] memory); 15 | function getAllATokens() external view returns (TokenData[] memory); 16 | function getReserveConfigurationData(address asset) external view returns (uint256 decimals, uint256 ltv, uint256 liquidationThreshold, uint256 liquidationBonus, uint256 reserveFactor, bool usageAsCollateralEnabled, bool borrowingEnabled, bool stableBorrowRateEnabled, bool isActive, bool isFrozen); 17 | function getReserveData(address asset) external view returns (uint256 availableLiquidity, uint256 totalStableDebt, uint256 totalVariableDebt, uint256 liquidityRate, uint256 variableBorrowRate, uint256 stableBorrowRate, uint256 averageStableBorrowRate, uint256 liquidityIndex, uint256 variableBorrowIndex, uint40 lastUpdateTimestamp); 18 | function getUserReserveData(address asset, address user) external view returns (uint256 currentATokenBalance, uint256 currentStableDebt, uint256 currentVariableDebt, uint256 principalStableDebt, uint256 scaledVariableDebt, uint256 stableBorrowRate, uint256 liquidityRate, uint40 stableRateLastUpdated, bool usageAsCollateralEnabled); 19 | function getReserveTokensAddresses(address asset) external view returns (address aTokenAddress, address stableDebtTokenAddress, address variableDebtTokenAddress); 20 | } 21 | -------------------------------------------------------------------------------- /contracts/external/aave/ILendingPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.8.6; 3 | 4 | interface ILendingPool { 5 | 6 | /** 7 | * @dev Deposits an `amount` of underlying asset into the reserve, receiving in return overlying aTokens. 8 | * - E.g. User deposits 100 USDC and gets in return 100 aUSDC 9 | * @param asset The address of the underlying asset to deposit 10 | * @param amount The amount to be deposited 11 | * @param onBehalfOf The address that will receive the aTokens, same as msg.sender if the user 12 | * wants to receive them on his own wallet, or a different address if the beneficiary of aTokens 13 | * is a different wallet 14 | * @param referralCode Code used to register the integrator originating the operation, for potential rewards. 15 | * 0 if the action is executed directly by the user, without any middle-man 16 | **/ 17 | function deposit( 18 | address asset, 19 | uint256 amount, 20 | address onBehalfOf, 21 | uint16 referralCode 22 | ) external; 23 | 24 | /** 25 | * @dev Withdraws an `amount` of underlying asset from the reserve, burning the equivalent aTokens owned 26 | * E.g. User has 100 aUSDC, calls withdraw() and receives 100 USDC, burning the 100 aUSDC 27 | * @param asset The address of the underlying asset to withdraw 28 | * @param amount The underlying amount to be withdrawn 29 | * - Send the value type(uint256).max in order to withdraw the whole aToken balance 30 | * @param to Address that will receive the underlying, same as msg.sender if the user 31 | * wants to receive it on his own wallet, or a different address if the beneficiary is a 32 | * different wallet 33 | * @return The final amount withdrawn 34 | **/ 35 | function withdraw( 36 | address asset, 37 | uint256 amount, 38 | address to 39 | ) external returns (uint256); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /aave/aavePolygon.json: -------------------------------------------------------------------------------- 1 | {"proto":[ 2 | { 3 | "aTokenAddress":"0x27F8D03b3a2196956ED754baDc28D73be8830A6e", 4 | "aTokenSymbol":"amDAI", 5 | "stableDebtTokenAddress":"", 6 | "variableDebtTokenAddress":"", 7 | "symbol":"DAI", 8 | "address":"0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063", 9 | "decimals":18 10 | }, 11 | { 12 | "aTokenAddress":"0x1a13F4Ca1d028320A707D99520AbFefca3998b7F", 13 | "aTokenSymbol":"amUSDC", 14 | "stableDebtTokenAddress":"", 15 | "variableDebtTokenAddress":"", 16 | "symbol":"USDC", 17 | "address":"0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", 18 | "decimals":6 19 | }, 20 | { 21 | "aTokenAddress":"0x60D55F02A771d515e077c9C2403a1ef324885CeC", 22 | "aTokenSymbol":"amUSDT", 23 | "stableDebtTokenAddress":"", 24 | "variableDebtTokenAddress":"", 25 | "symbol":"USDT", 26 | "address":"0xc2132D05D31c914a87C6611C10748AEb04B58e8F", 27 | "decimals":6 28 | }, 29 | { 30 | "aTokenAddress":"0x5c2ed810328349100A66B82b78a1791B101C9D61", 31 | "aTokenSymbol":"amWBTC", 32 | "stableDebtTokenAddress":"", 33 | "variableDebtTokenAddress":"", 34 | "symbol":"WBTC", 35 | "address":"0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6", 36 | "decimals":8 37 | }, 38 | { 39 | "aTokenAddress":"0x28424507fefb6f7f8E9D3860F56504E4e5f5f390", 40 | "aTokenSymbol":"amWETH", 41 | "stableDebtTokenAddress":"", 42 | "variableDebtTokenAddress":"", 43 | "symbol":"WETH", 44 | "address":"0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", 45 | "decimals":18 46 | }, 47 | { 48 | "aTokenAddress":"0x8dF3aad3a84da6b69A4DA8aeC3eA40d9091B2Ac4", 49 | "aTokenSymbol":"amWMATIC", 50 | "stableDebtTokenAddress":"", 51 | "variableDebtTokenAddress":"", 52 | "symbol":"WMATIC", 53 | "address":"0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", 54 | "decimals":18 55 | }, 56 | { 57 | "aTokenAddress":"0x1d2a0E5EC8E5bBDCA5CB219e649B565d8e5c3360", 58 | "aTokenSymbol":"amAAVE", 59 | "stableDebtTokenAddress":"", 60 | "variableDebtTokenAddress":"", 61 | "symbol":"AAVE", 62 | "address":"0xD6DF932A45C0f255f85145f286eA0b292B21C90B", 63 | "decimals":18 64 | } 65 | ]} -------------------------------------------------------------------------------- /hardhat.network.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from 'hardhat/config'; 2 | 3 | const alchemyUrl = process.env.ALCHEMY_URL; 4 | const infuraApiKey = process.env.INFURA_API_KEY; 5 | const mnemonic = process.env.HDWALLET_MNEMONIC; 6 | 7 | const networks: HardhatUserConfig['networks'] = { 8 | coverage: { 9 | url: 'http://127.0.0.1:8555', 10 | blockGasLimit: 200000000, 11 | allowUnlimitedContractSize: true, 12 | }, 13 | localhost: { 14 | chainId: 1, 15 | url: 'http://127.0.0.1:8545', 16 | allowUnlimitedContractSize: true, 17 | }, 18 | }; 19 | 20 | if (alchemyUrl && process.env.FORK_ENABLED && mnemonic) { 21 | networks.hardhat = { 22 | chainId: 1, 23 | forking: { 24 | url: alchemyUrl, 25 | blockNumber: 12226812, 26 | }, 27 | accounts: { 28 | mnemonic, 29 | }, 30 | }; 31 | } else { 32 | networks.hardhat = { 33 | allowUnlimitedContractSize: true, 34 | }; 35 | } 36 | 37 | if (mnemonic) { 38 | networks.xdai = { 39 | chainId: 100, 40 | url: 'https://rpc.xdaichain.com/', 41 | accounts: { 42 | mnemonic, 43 | }, 44 | }; 45 | networks.poaSokol = { 46 | chainId: 77, 47 | url: 'https://sokol.poa.network', 48 | accounts: { 49 | mnemonic, 50 | }, 51 | }; 52 | networks.matic = { 53 | chainId: 137, 54 | url: 'https://rpc-mainnet.maticvigil.com', 55 | accounts: { 56 | mnemonic, 57 | }, 58 | }; 59 | networks.mumbai = { 60 | chainId: 80001, 61 | url: 'https://rpc-mumbai.matic.today', 62 | accounts: { 63 | mnemonic, 64 | }, 65 | loggingEnabled: true, 66 | }; 67 | } 68 | 69 | if (infuraApiKey && mnemonic) { 70 | networks.kovan = { 71 | url: `https://kovan.infura.io/v3/${infuraApiKey}`, 72 | accounts: { 73 | mnemonic, 74 | }, 75 | }; 76 | 77 | networks.ropsten = { 78 | url: `https://ropsten.infura.io/v3/${infuraApiKey}`, 79 | accounts: { 80 | mnemonic, 81 | }, 82 | }; 83 | 84 | networks.rinkeby = { 85 | url: `https://rinkeby.infura.io/v3/${infuraApiKey}`, 86 | accounts: { 87 | mnemonic, 88 | }, 89 | }; 90 | 91 | networks.mainnet = { 92 | url: alchemyUrl, 93 | accounts: { 94 | mnemonic, 95 | }, 96 | }; 97 | } else { 98 | console.warn('No infura or hdwallet available for testnets'); 99 | } 100 | 101 | export default networks; 102 | -------------------------------------------------------------------------------- /contracts/external/aave/ILendingPoolAddressesProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.8.6; 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 | -------------------------------------------------------------------------------- /scripts/fork/distributeEtherFromBinance.ts: -------------------------------------------------------------------------------- 1 | import { dai, usdc } from '@studydefi/money-legos/erc20'; 2 | 3 | import { task } from 'hardhat/config'; 4 | 5 | import { 6 | BINANCE_ADDRESS, 7 | BINANCE7_ADDRESS, 8 | DAI_RICH_ADDRESS, 9 | LARGE_GUSD_ADDRESS, 10 | GUSD_ADDRESS, 11 | LARGE_BUSD_ADDRESS, 12 | BUSD_ADDRESS, 13 | SUSD_ADDRESS, 14 | LARGE_SUSD_ADDRESS, 15 | } from '../../Constant'; 16 | import { info, success } from '../helpers'; 17 | 18 | export default task( 19 | 'fork:distribute-ether-from-binance', 20 | 'Distribute Ether from Binance', 21 | ).setAction(async (taskArguments, hre) => { 22 | info('Gathering funds from Binance...'); 23 | 24 | const { getNamedAccounts, ethers } = hre; 25 | const { provider, getContractAt, getSigners } = ethers; 26 | const { deployer } = await getNamedAccounts(); 27 | 28 | const [contractsOwner, yieldSourceOwner] = await getSigners(); 29 | 30 | const binance = provider.getUncheckedSigner(BINANCE_ADDRESS); 31 | const binance7 = provider.getUncheckedSigner(BINANCE7_ADDRESS); 32 | const gusdHolder = provider.getUncheckedSigner(LARGE_GUSD_ADDRESS); 33 | const busdHolder = provider.getUncheckedSigner(LARGE_BUSD_ADDRESS); 34 | const susdHolder = provider.getUncheckedSigner(LARGE_SUSD_ADDRESS); 35 | 36 | const daiContract = await getContractAt(dai.abi, dai.address, binance); 37 | const usdcContract = await getContractAt(usdc.abi, usdc.address, binance7); 38 | const gusdContract = await getContractAt(dai.abi, GUSD_ADDRESS, gusdHolder); 39 | const busdContract = await getContractAt(dai.abi, BUSD_ADDRESS, busdHolder); 40 | const susdContract = await getContractAt(dai.abi, SUSD_ADDRESS, susdHolder); 41 | 42 | const recipients: { [key: string]: string } = { 43 | ['Deployer']: deployer, 44 | ['Dai Rich Signer']: DAI_RICH_ADDRESS, 45 | ['contractsOwner']: contractsOwner.address, 46 | ['yieldSourceOwner']: yieldSourceOwner.address, 47 | }; 48 | 49 | await binance.sendTransaction({ to: gusdHolder._address, value: ethers.utils.parseEther('10') }); 50 | 51 | const keys = Object.keys(recipients); 52 | 53 | for (var i = 0; i < keys.length; i++) { 54 | const name = keys[i]; 55 | const address = recipients[name]; 56 | 57 | info(`Sending 1000 Ether to ${name}...`); 58 | await binance.sendTransaction({ to: address, value: ethers.utils.parseEther('1000') }); 59 | 60 | info(`Sending 1000 Dai to ${name}...`); 61 | await daiContract.transfer(address, ethers.utils.parseEther('1000')); 62 | 63 | info(`Sending 1000 GUSD to ${name}...`); 64 | await gusdContract.transfer(address, ethers.utils.parseUnits('1000', 2)); 65 | 66 | info(`Sending 1000 BUSD to ${name}...`); 67 | await busdContract.transfer(address, ethers.utils.parseUnits('1000', 18)); 68 | 69 | info(`Sending 1000 SUSD to ${name}...`); 70 | await susdContract.transfer(address, ethers.utils.parseUnits('1000', 18)); 71 | } 72 | 73 | success('Done!'); 74 | }); 75 | -------------------------------------------------------------------------------- /contracts/test/SafeERC20Wrapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 8 | 9 | // Mock implementation from OpenZeppelin modified for our usage in tests 10 | // https://github.com/OpenZeppelin/openzeppelin-contracts-/blob/master/contracts/mocks/SafeERC20Helper.sol 11 | contract ERC20ReturnTrueMock is ERC20 { 12 | 13 | mapping (address => uint256) private _allowances; 14 | 15 | // IERC20's functions are not pure, but these mock implementations are: to prevent Solidity from issuing warnings, 16 | // we write to a dummy state variable. 17 | uint256 private _dummy; 18 | 19 | constructor (string memory _name, string memory _symbol) ERC20(_name, _symbol) {} 20 | 21 | function transfer(address, uint256) public override returns (bool) { 22 | _dummy = 0; 23 | return true; 24 | } 25 | 26 | function transferFrom(address, address, uint256) public override returns (bool) { 27 | _dummy = 0; 28 | return true; 29 | } 30 | 31 | function approve(address, uint256) public override returns (bool) { 32 | _dummy = 0; 33 | return true; 34 | } 35 | 36 | function setAllowance(uint256 allowance_) public { 37 | _allowances[_msgSender()] = allowance_; 38 | } 39 | 40 | function allowance(address owner, address) public view override returns (uint256) { 41 | return _allowances[owner]; 42 | } 43 | uint256[48] private __gap; 44 | } 45 | 46 | contract SafeERC20Wrapper { 47 | using SafeERC20 for IERC20; 48 | 49 | IERC20 private _token; 50 | 51 | // solhint-disable func-name-mixedcase 52 | constructor (IERC20 token) public { 53 | _token = token; 54 | } 55 | 56 | function balanceOf(address account) public view returns (uint256) { 57 | _token.balanceOf(account); 58 | } 59 | 60 | function transfer(address recipient, uint256 amount) public returns (bool) { 61 | _token.safeTransfer(recipient, amount); 62 | } 63 | 64 | function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) { 65 | _token.safeTransferFrom(sender, recipient, amount); 66 | } 67 | 68 | function approve(address spender, uint256 amount) public returns (bool) { 69 | _token.safeApprove(spender, amount); 70 | } 71 | 72 | function increaseAllowance(uint256 amount) public { 73 | _token.safeIncreaseAllowance(address(0), amount); 74 | } 75 | 76 | function decreaseAllowance(uint256 amount) public { 77 | _token.safeDecreaseAllowance(address(0), amount); 78 | } 79 | 80 | function setAllowance(uint256 allowance_) public { 81 | ERC20ReturnTrueMock(address(_token)).setAllowance(allowance_); 82 | } 83 | 84 | function allowance(address owner, address spender) public view returns (uint256) { 85 | return _token.allowance(owner, spender); 86 | } 87 | uint256[49] private __gap; 88 | } 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pooltogether/aave-yield-source", 3 | "version": "1.2.1", 4 | "description": "PoolTogether Aave Yield Source", 5 | "main": "index.js", 6 | "license": "GPL-3.0", 7 | "scripts": { 8 | "clean": "rm -rf cache/ artifacts/", 9 | "compile": "hardhat --show-stack-traces --max-memory 8192 compile", 10 | "coverage": "HIDE_DEPLOY_LOG=true OPTIMIZER_DISABLED=true hardhat coverage", 11 | "deploy": "hardhat deploy --write true --network", 12 | "etherscan-verify": "hardhat etherscan:verify --network", 13 | "format": "prettier --config .prettierrc --write **/*.ts", 14 | "format:file": "prettier --config .prettierrc --write", 15 | "hint": "solhint \"contracts/**/*.sol\"", 16 | "start-fork": "FORK_ENABLED=true hardhat node --no-reset --no-deploy", 17 | "impersonate-accounts": "hardhat --network localhost fork:impersonate-accounts ", 18 | "distribute-ether": "hardhat --network localhost fork:distribute-ether-from-binance", 19 | "create-prize-pool": "hardhat --network localhost fork:create-aave-prize-pool", 20 | "deploy-fork": "FORK_ENABLED=true; rm -rf deployments/localhost && yarn deploy localhost", 21 | "run-fork": "FORK_ENABLED=true; yarn impersonate-accounts && yarn distribute-ether && yarn create-prize-pool", 22 | "test": "yarn clean && hardhat test", 23 | "typechain": "typechain --target=ethers-v5 \"./abis/*.json\" --out-dir \"./types\"", 24 | "verify": "hardhat etherscan-verify --license GPL-3.0 --solc-input --network", 25 | "integration-test-mumbai": "yarn deploy mumbai && hardhat run --network mumbai ./scripts/createAndRunPrizePoolMumbai.ts ", 26 | "prepack": "rm -rf build cache && yarn compile", 27 | "postpublish": "PACKAGE_VERSION=$(./scripts/package-version.sh) && git tag -ae v$PACKAGE_VERSION && git push --tags" 28 | }, 29 | "dependencies": { 30 | "@openzeppelin/contracts": "4.3.2", 31 | "@pooltogether/fixed-point": "1.0.0", 32 | "@pooltogether/owner-manager-contracts": "1.1.0", 33 | "@pooltogether/yield-source-interface": "1.3.0" 34 | }, 35 | "devDependencies": { 36 | "@nomiclabs/hardhat-ethers": "2.0.2", 37 | "@nomiclabs/hardhat-etherscan": "2.1.1", 38 | "@nomiclabs/hardhat-waffle": "2.0.1", 39 | "@openzeppelin/hardhat-upgrades": "1.6.0", 40 | "@studydefi/money-legos": "2.4.1", 41 | "@typechain/ethers-v5": "7.0.1", 42 | "@typechain/hardhat": "2.3.0", 43 | "@types/chai": "4.2.15", 44 | "@types/debug": "4.1.5", 45 | "@types/mocha": "8.2.1", 46 | "@types/node": "14.14.32", 47 | "chai": "4.3.4", 48 | "chalk": "4.1.0", 49 | "debug": "4.3.1", 50 | "ethereum-waffle": "3.3.0", 51 | "ethers": "5.4.6", 52 | "evm-chains": "0.2.0", 53 | "hardhat": "2.1.1", 54 | "hardhat-abi-exporter": "2.1.2", 55 | "hardhat-dependency-compiler": "1.1.1", 56 | "hardhat-deploy": "0.9.0", 57 | "hardhat-deploy-ethers": "0.3.0-beta.7", 58 | "hardhat-gas-reporter": "1.0.4", 59 | "prettier": "2.2.1", 60 | "solhint": "3.3.3", 61 | "solidity-coverage": "0.7.17", 62 | "ts-generator": "0.1.1", 63 | "ts-node": "9.1.1", 64 | "typechain": "5.1.2", 65 | "typescript": "4.2.3" 66 | }, 67 | "files": [ 68 | "LICENSE", 69 | "abis/**", 70 | "aave/", 71 | "artifacts/**", 72 | "contracts/**", 73 | "types/**" 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /contracts/external/aave/IAToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.8.6; 3 | 4 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | interface IAToken is IERC20 { 7 | /** 8 | * @dev Emitted after the mint action 9 | * @param from The address performing the mint 10 | * @param value The amount being 11 | * @param index The new liquidity index of the reserve 12 | **/ 13 | event Mint(address indexed from, uint256 value, uint256 index); 14 | 15 | /** 16 | * @dev Mints `amount` aTokens to `user` 17 | * @param user The address receiving the minted tokens 18 | * @param amount The amount of tokens getting minted 19 | * @param index The new liquidity index of the reserve 20 | * @return `true` if the the previous balance of the user was 0 21 | */ 22 | function mint( 23 | address user, 24 | uint256 amount, 25 | uint256 index 26 | ) external returns (bool); 27 | 28 | /** 29 | * @dev Emitted after aTokens are burned 30 | * @param from The owner of the aTokens, getting them burned 31 | * @param target The address that will receive the underlying 32 | * @param value The amount being burned 33 | * @param index The new liquidity index of the reserve 34 | **/ 35 | event Burn(address indexed from, address indexed target, uint256 value, uint256 index); 36 | 37 | /** 38 | * @dev Emitted during the transfer action 39 | * @param from The user whose tokens are being transferred 40 | * @param to The recipient 41 | * @param value The amount being transferred 42 | * @param index The new liquidity index of the reserve 43 | **/ 44 | event BalanceTransfer(address indexed from, address indexed to, uint256 value, uint256 index); 45 | 46 | /** 47 | * @dev Burns aTokens from `user` and sends the equivalent amount of underlying to `receiverOfUnderlying` 48 | * @param user The owner of the aTokens, getting them burned 49 | * @param receiverOfUnderlying The address that will receive the underlying 50 | * @param amount The amount being burned 51 | * @param index The new liquidity index of the reserve 52 | **/ 53 | function burn( 54 | address user, 55 | address receiverOfUnderlying, 56 | uint256 amount, 57 | uint256 index 58 | ) external; 59 | 60 | /** 61 | * @dev Mints aTokens to the reserve treasury 62 | * @param amount The amount of tokens getting minted 63 | * @param index The new liquidity index of the reserve 64 | */ 65 | function mintToTreasury(uint256 amount, uint256 index) external; 66 | 67 | /** 68 | * @dev Transfers aTokens in the event of a borrow being liquidated, in case the liquidators reclaims the aToken 69 | * @param from The address getting liquidated, current owner of the aTokens 70 | * @param to The recipient 71 | * @param value The amount of tokens getting transferred 72 | **/ 73 | function transferOnLiquidation( 74 | address from, 75 | address to, 76 | uint256 value 77 | ) external; 78 | 79 | /** 80 | * @dev Transfers the underlying asset to `target`. Used by the LendingPool to transfer 81 | * assets in borrow(), withdraw() and flashLoan() 82 | * @param user The recipient of the aTokens 83 | * @param amount The amount getting transferred 84 | * @return The amount transferred 85 | **/ 86 | function transferUnderlyingTo(address user, uint256 amount) external returns (uint256); 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | PoolTogether Brand 4 | 5 |

6 | 7 |
8 | 9 | # PoolTogether Aave Yield Source 👻 10 | 11 | ![Tests](https://github.com/pooltogether/aave-yield-source/actions/workflows/main.yml/badge.svg) 12 | [![Coverage Status](https://coveralls.io/repos/github/pooltogether/aave-yield-source/badge.svg?branch=main)](https://coveralls.io/github/pooltogether/aave-yield-source?branch=main) 13 | [![built-with openzeppelin](https://img.shields.io/badge/built%20with-OpenZeppelin-3677FF)](https://docs.openzeppelin.com/) 14 | 15 | PoolTogether Yield Source that uses [Aave](https://aave.com/) V2 to generate yield by lending any ERC20 token supported by Aave and deposited into the Aave Yield Source. 16 | 17 | # Usage 18 | 19 | ## Adding a new Aave Lending Pool 20 | 21 | First make sure the Aave json config (`aave/aaveMainnet.json`, etc) includes the lending pool. 22 | 23 | Then, add the token symbol to the list in `aave.config.ts`. 24 | 25 | ## Deployment 26 | 27 | Follow Installation instructions. 28 | 29 | Aave provides a json blob per network of the files in the [docs](https://docs.aave.com/developers/deployed-contracts/deployed-contracts) 30 | The deploy script parses this and deploys a proxy contract if the aToken file does exist in the deployments directory. 31 | 32 | Ensure the `lendingPoolAddressesProviderRegistry` is up to date in the namedAccounts field of `hardhat.config.ts` . 33 | 34 | To add a new network, add a json file in the `./aave` directory then run: 35 | 36 | `yarn deploy ` 37 | 38 | To add a new lending market, update the appropriate network json at `./aave` and run: 39 | 40 | `yarn deploy ` 41 | 42 | The deployment script can be found in `deploy/deploy.ts`. 43 | 44 | ## Development 45 | 46 | Clone this repository and enter the directory. 47 | 48 | ### Installation 49 | 50 | Install dependencies: 51 | 52 | ``` 53 | yarn 54 | ``` 55 | 56 | This project uses [Yarn 2](https://yarnpkg.com), dependencies should get installed pretty quickly. 57 | 58 | ### Env 59 | 60 | We use [direnv](https://direnv.net) to manage environment variables. You'll likely need to install it. 61 | 62 | Copy `.envrc.example` and write down the env variables needed to run this project. 63 | ``` 64 | cp .envrc.example .envrc 65 | ``` 66 | 67 | Once your env variables are setup, load them with: 68 | ``` 69 | direnv allow 70 | ``` 71 | 72 | ### Test 73 | 74 | We use the [Hardhat](https://hardhat.org) ecosystem to test and deploy our contracts. 75 | 76 | To run unit tests: 77 | 78 | ``` 79 | yarn test 80 | ``` 81 | 82 | To run [solhint](https://protofire.github.io/solhint/) and tests: 83 | 84 | ``` 85 | yarn verify 86 | ``` 87 | 88 | To run coverage: 89 | 90 | ``` 91 | yarn coverage 92 | ``` 93 | 94 | ### Mainnet fork 95 | 96 | Before deploying, you can make sure your implementation works by deploying a Yield Source Prize Pool on a fork of Mainnet. 97 | 98 | Start Mainnet fork in a terminal window with the command: 99 | 100 | ``` 101 | yarn start-fork 102 | ``` 103 | 104 | In another window, start the scripts to deploy and create a Aave Yield Source Prize Pool, deposit Dai into it, award the prize and withdraw. 105 | 106 | ``` 107 | yarn deploy-fork && yarn run-fork 108 | ``` 109 | 110 | ### Contract Verification 111 | 112 | Once deployment is done, you can verify your contracts on [Etherscan](https://etherscan.io) by typing: 113 | 114 | ``` 115 | yarn verify 116 | ``` 117 | 118 | ### Code quality 119 | 120 | [Prettier](https://prettier.io) is used to format TypeScript code. Use it by running: 121 | 122 | ``` 123 | yarn format 124 | ``` 125 | 126 | [Solhint](https://protofire.github.io/solhint/) is used to lint Solidity files. Run it with: 127 | ``` 128 | yarn hint 129 | ``` 130 | 131 | [TypeChain](https://github.com/ethereum-ts/Typechain) is used to generates types for scripts and tests. Generate types by running: 132 | ``` 133 | yarn typechain 134 | ``` 135 | -------------------------------------------------------------------------------- /scripts/runLifecycle.ts: -------------------------------------------------------------------------------- 1 | import { ethers, deployments, getNamedAccounts, getChainId } from 'hardhat'; 2 | import YieldSourcePrizePool from '@pooltogether/pooltogether-contracts/abis/YieldSourcePrizePool.json'; 3 | import PeriodicPrizeStrategy from '@pooltogether/pooltogether-contracts/abis/PeriodicPrizeStrategy.json'; 4 | import MultipleWinners from '@pooltogether/pooltogether-contracts/abis/MultipleWinners.json'; 5 | import { dai } from '@studydefi/money-legos/erc20'; 6 | 7 | const daiERC20Address = '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063'; 8 | const daiYieldSourcePrizePool = '0xA4579F6C63858a28eA88ee4c1bdaD628888C4aDB'; 9 | 10 | async function runLifeCycle() { 11 | depositTo(); 12 | await new Promise((r) => setTimeout(r, 5000)); 13 | startAward(); 14 | await new Promise((r) => setTimeout(r, 5000)); 15 | completeAward(); 16 | await new Promise((r) => setTimeout(r, 5000)); 17 | instantWithdraw(); 18 | } 19 | runLifeCycle(); 20 | 21 | async function depositTo() { 22 | const { deployer } = await getNamedAccounts(); 23 | 24 | console.log('deployer is ', deployer); 25 | const signer = await ethers.provider.getSigner(deployer); 26 | 27 | const daiERC20Contract = await ethers.getContractAt(dai.abi, daiERC20Address); 28 | 29 | console.log('balanceOf deployer ', await daiERC20Contract.balanceOf(deployer)); 30 | 31 | const prizePool = await ethers.getContractAt( 32 | YieldSourcePrizePool, 33 | daiYieldSourcePrizePool, 34 | signer, 35 | ); 36 | 37 | console.log('controlledToken is ', await prizePool.token()); 38 | 39 | const depositAmount = ethers.utils.parseEther('2'); 40 | const approveResult = await daiERC20Contract.approve(daiYieldSourcePrizePool, depositAmount); 41 | console.log('approveResult ', approveResult.toString()); 42 | 43 | const prizeStrategy = await ethers.getContractAt( 44 | MultipleWinners, 45 | await prizePool.prizeStrategy(), 46 | signer, 47 | ); 48 | const ticketAddress = await prizeStrategy.ticket(); 49 | 50 | const depositToResult = await prizePool.depositTo( 51 | deployer, 52 | depositAmount, 53 | ticketAddress, 54 | ethers.constants.AddressZero, 55 | ); 56 | console.log('depositTo is ', depositToResult); 57 | } 58 | // depositTo() 59 | 60 | async function instantWithdraw() { 61 | const { deployer } = await getNamedAccounts(); 62 | const signer = await ethers.provider.getSigner(deployer); 63 | 64 | const prizePool = await ethers.getContractAt( 65 | YieldSourcePrizePool, 66 | daiYieldSourcePrizePool, 67 | signer, 68 | ); 69 | const withdrawAmount = ethers.utils.parseEther('0.5'); 70 | const prizeStrategy = await ethers.getContractAt( 71 | MultipleWinners, 72 | await prizePool.prizeStrategy(), 73 | signer, 74 | ); 75 | const ticketAddress = await prizeStrategy.ticket(); 76 | 77 | const exitFee = await ethers.utils.parseEther('2'); 78 | 79 | const withdrawResult = await prizePool.withdrawInstantlyFrom( 80 | deployer, 81 | withdrawAmount, 82 | ticketAddress, 83 | exitFee, 84 | ); 85 | console.log(withdrawResult); 86 | } 87 | // instantWithdraw() 88 | 89 | async function balanceOf() { 90 | const { deployer } = await getNamedAccounts(); 91 | const signer = await ethers.provider.getSigner(deployer); 92 | 93 | const prizePool = await ethers.getContractAt( 94 | YieldSourcePrizePool, 95 | daiYieldSourcePrizePool, 96 | signer, 97 | ); 98 | console.log(await prizePool.balance()); 99 | } 100 | // balanceOf() 101 | 102 | async function startAward() { 103 | const { deployer } = await getNamedAccounts(); 104 | const signer = await ethers.provider.getSigner(deployer); 105 | 106 | const prizePool = await ethers.getContractAt( 107 | YieldSourcePrizePool, 108 | daiYieldSourcePrizePool, 109 | signer, 110 | ); 111 | const prizeStrategy = await ethers.getContractAt( 112 | PeriodicPrizeStrategy, 113 | await prizePool.prizeStrategy(), 114 | signer, 115 | ); 116 | console.log(await prizeStrategy.startAward()); 117 | } 118 | // startAward() 119 | 120 | async function completeAward() { 121 | const { deployer } = await getNamedAccounts(); 122 | const signer = await ethers.provider.getSigner(deployer); 123 | 124 | const prizePool = await ethers.getContractAt( 125 | YieldSourcePrizePool, 126 | daiYieldSourcePrizePool, 127 | signer, 128 | ); 129 | const prizeStrategy = await ethers.getContractAt( 130 | PeriodicPrizeStrategy, 131 | await prizePool.prizeStrategy(), 132 | signer, 133 | ); 134 | console.log(await prizeStrategy.completeAward()); 135 | } 136 | // completeAward() 137 | -------------------------------------------------------------------------------- /contracts/external/aave/IAaveIncentivesController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | pragma experimental ABIEncoderV2; 6 | 7 | interface IAaveIncentivesController { 8 | event RewardsAccrued(address indexed user, uint256 amount); 9 | 10 | event RewardsClaimed(address indexed user, address indexed to, uint256 amount); 11 | 12 | // Commented out to avoid displaying warnings about duplicate definition 13 | // event RewardsClaimed( 14 | // address indexed user, 15 | // address indexed to, 16 | // address indexed claimer, 17 | // uint256 amount 18 | // ); 19 | 20 | event ClaimerSet(address indexed user, address indexed claimer); 21 | 22 | /* 23 | * @dev Returns the configuration of the distribution for a certain asset 24 | * @param asset The address of the reference asset of the distribution 25 | * @return The asset index, the emission per second and the last updated timestamp 26 | **/ 27 | function getAssetData(address asset) 28 | external 29 | view 30 | returns ( 31 | uint256, 32 | uint256, 33 | uint256 34 | ); 35 | 36 | /** 37 | * @dev Whitelists an address to claim the rewards on behalf of another address 38 | * @param user The address of the user 39 | * @param claimer The address of the claimer 40 | */ 41 | function setClaimer(address user, address claimer) external; 42 | 43 | /** 44 | * @dev Returns the whitelisted claimer for a certain address (0x0 if not set) 45 | * @param user The address of the user 46 | * @return The claimer address 47 | */ 48 | function getClaimer(address user) external view returns (address); 49 | 50 | /** 51 | * @dev Configure assets for a certain rewards emission 52 | * @param assets The assets to incentivize 53 | * @param emissionsPerSecond The emission for each asset 54 | */ 55 | function configureAssets(address[] calldata assets, uint256[] calldata emissionsPerSecond) 56 | external; 57 | 58 | /** 59 | * @dev Called by the corresponding asset on any update that affects the rewards distribution 60 | * @param asset The address of the user 61 | * @param userBalance The balance of the user of the asset in the lending pool 62 | * @param totalSupply The total supply of the asset in the lending pool 63 | **/ 64 | function handleAction( 65 | address asset, 66 | uint256 userBalance, 67 | uint256 totalSupply 68 | ) external; 69 | 70 | /** 71 | * @dev Returns the total of rewards of an user, already accrued + not yet accrued 72 | * @param user The address of the user 73 | * @return The rewards 74 | **/ 75 | function getRewardsBalance(address[] calldata assets, address user) 76 | external 77 | view 78 | returns (uint256); 79 | 80 | /** 81 | * @dev Claims reward for an user, on all the assets of the lending pool, accumulating the pending rewards 82 | * @param amount Amount of rewards to claim 83 | * @param to Address that will be receiving the rewards 84 | * @return Rewards claimed 85 | **/ 86 | function claimRewards( 87 | address[] calldata assets, 88 | uint256 amount, 89 | address to 90 | ) external returns (uint256); 91 | 92 | /** 93 | * @dev Claims reward for an user on behalf, on all the assets of the lending pool, accumulating the pending rewards. The caller must 94 | * be whitelisted via "allowClaimOnBehalf" function by the RewardsAdmin role manager 95 | * @param amount Amount of rewards to claim 96 | * @param user Address to check and claim rewards 97 | * @param to Address that will be receiving the rewards 98 | * @return Rewards claimed 99 | **/ 100 | function claimRewardsOnBehalf( 101 | address[] calldata assets, 102 | uint256 amount, 103 | address user, 104 | address to 105 | ) external returns (uint256); 106 | 107 | /** 108 | * @dev returns the unclaimed rewards of the user 109 | * @param user the address of the user 110 | * @return the unclaimed user rewards 111 | */ 112 | function getUserUnclaimedRewards(address user) external view returns (uint256); 113 | 114 | /** 115 | * @dev returns the unclaimed rewards of the user 116 | * @param user the address of the user 117 | * @param asset The asset to incentivize 118 | * @return the user index for the asset 119 | */ 120 | function getUserAssetData(address user, address asset) external view returns (uint256); 121 | 122 | /** 123 | * @dev for backward compatibility with previous implementation of the Incentives controller 124 | */ 125 | function REWARD_TOKEN() external view returns (address); 126 | 127 | /** 128 | * @dev for backward compatibility with previous implementation of the Incentives controller 129 | */ 130 | function PRECISION() external view returns (uint8); 131 | 132 | /** 133 | * @dev Gets the distribution end timestamp of the emissions 134 | */ 135 | function DISTRIBUTION_END() external view returns (uint256); 136 | } 137 | -------------------------------------------------------------------------------- /aave/aaveKovan.json: -------------------------------------------------------------------------------- 1 | {"proto":[{"aTokenAddress":"0x6d93ef8093F067f19d33C2360cE17b20a8c45CD7","aTokenSymbol":"aAAVE","stableDebtTokenAddress":"0x72d2Aea8aCcD3277D90093a974eFf3e1945871D7","variableDebtTokenAddress":"0x5aF7bAC415D9c249176ea233E92646E5c9288b92","symbol":"AAVE","address":"0xB597cd8D3217ea6477232F9217fa70837ff667Af","decimals":18},{"aTokenAddress":"0x28f92b4c8Bdab37AF6C4422927158560b4bB446e","aTokenSymbol":"aBAT","stableDebtTokenAddress":"0x07a0B32983ab8203E8C3493F0AbE5bFe784fAa15","variableDebtTokenAddress":"0xcE271C229576605bdabD0A3D664685cbC383f3a6","symbol":"BAT","address":"0x2d12186Fbb9f9a8C28B3FfdD4c42920f8539D738","decimals":18},{"aTokenAddress":"0xfe3E41Db9071458e39104711eF1Fa668bae44e85","aTokenSymbol":"aBUSD","stableDebtTokenAddress":"0x597c5d0390E7e995d36F2e49F9eD979697723bE9","variableDebtTokenAddress":"0xB85eCAd7a9C9F09749CeCF84122189A7908eC934","symbol":"BUSD","address":"0x4c6E1EFC12FDfD568186b7BAEc0A43fFfb4bCcCf","decimals":18},{"aTokenAddress":"0xdCf0aF9e59C002FA3AA091a46196b37530FD48a8","aTokenSymbol":"aDAI","stableDebtTokenAddress":"0x3B91257Fe5CA63b4114ac41A0d467D25E2F747F3","variableDebtTokenAddress":"0xEAbBDBe7aaD7d5A278da40967E62C8c8Fe5fAec8","symbol":"DAI","address":"0xFf795577d9AC8bD7D90Ee22b6C1703490b6512FD","decimals":18},{"aTokenAddress":"0x1d1F2Cb9ED46A8d5bf0254E5CE400514D62d55F0","aTokenSymbol":"aENJ","stableDebtTokenAddress":"0x8af08B5874380E1F1816e30bE12d773f4EB70e67","variableDebtTokenAddress":"0xc11e09B03634144a1862E14ef7569DbEb4b7F3a2","symbol":"ENJ","address":"0xC64f90Cd7B564D3ab580eb20a102A8238E218be2","decimals":18},{"aTokenAddress":"0xdDdEC78e29f3b579402C42ca1fd633DE00D23940","aTokenSymbol":"aKNC","stableDebtTokenAddress":"0x7f4E5bA1eE5dCAa4440371ec521cBc130De12E5e","variableDebtTokenAddress":"0x196d717b2D8a5694572C2742343C333EA27B8288","symbol":"KNC","address":"0x3F80c39c0b96A0945f9F0E9f55d8A8891c5671A8","decimals":18},{"aTokenAddress":"0xeD9044cA8F7caCe8eACcD40367cF2bee39eD1b04","aTokenSymbol":"aLINK","stableDebtTokenAddress":"0x0DBEE55AB73e3C14421d3f437a218ea99A520556","variableDebtTokenAddress":"0xcCead10A3BA54b1FA6D107b63B7D5e5e2f9888D8","symbol":"LINK","address":"0xAD5ce863aE3E4E9394Ab43d4ba0D80f419F61789","decimals":18},{"aTokenAddress":"0xA288B1767C91Aa9d8A14a65dC6B2E7ce68c02DFd","aTokenSymbol":"aMANA","stableDebtTokenAddress":"0xd4aEcF57cbcfeA373565DE75537aAc911EAF1759","variableDebtTokenAddress":"0xaEE5AA094B55b6538388A4E8CBAe9E81Bfe815e6","symbol":"MANA","address":"0x738Dc6380157429e957d223e6333Dc385c85Fec7","decimals":18},{"aTokenAddress":"0x9d9DaBEae6BcBa881404A9e499B13B2B3C1F329E","aTokenSymbol":"aMKR","stableDebtTokenAddress":"0xC37AadA7758e10a49bdECb9078753d5D096A4649","variableDebtTokenAddress":"0xB86a93aA1325e4F58E3dbA7CE9DA251D83374fA2","symbol":"MKR","address":"0x61e4CAE3DA7FD189e52a4879C7B8067D7C2Cc0FA","decimals":18},{"aTokenAddress":"0x01875ee883B32f5f961A92eC597DcEe2dB7589c1","aTokenSymbol":"aREN","stableDebtTokenAddress":"0xc66a5fd3Bd3D0329895ceE5755e161FD89c2EecD","variableDebtTokenAddress":"0x75f318b9B40c5bEb0EEAdab5294C4108A376a22d","symbol":"REN","address":"0x5eebf65A6746eed38042353Ba84c8e37eD58Ac6f","decimals":18},{"aTokenAddress":"0xAA74AdA92dE4AbC0371b75eeA7b1bd790a69C9e1","aTokenSymbol":"aSNX","stableDebtTokenAddress":"0x14B7a7Ab57190aEc3210303ef1cF29088535B329","variableDebtTokenAddress":"0x7dF2a710751cb9f1FD392107187e4Aed0Ae867b0","symbol":"SNX","address":"0x7FDb81B0b8a010dd4FFc57C3fecbf145BA8Bd947","decimals":18},{"aTokenAddress":"0x9488fF6F29ff75bfdF8cd5a95C6aa679bc7Cd65c","aTokenSymbol":"aSUSD","stableDebtTokenAddress":"0xB155258d3c18dd5D41e8838c8b45CaE1B17a11D9","variableDebtTokenAddress":"0xf3B942441Bd9d335E64413BeA6b76a49A5853C54","symbol":"sUSD","address":"0x99b267b9D96616f906D53c26dECf3C5672401282","decimals":18},{"aTokenAddress":"0x39914AdBe5fDbC2b9ADeedE8Bcd444b20B039204","aTokenSymbol":"aTUSD","stableDebtTokenAddress":"0x082576C4CfC2eE1e0b8088B84d50CEb97CD84E49","variableDebtTokenAddress":"0xC0cFab5E4A9D8DA2Bc98D0a2b3f9dc20f7eec19C","symbol":"TUSD","address":"0x016750AC630F711882812f24Dba6c95b9D35856d","decimals":18},{"aTokenAddress":"0xe12AFeC5aa12Cf614678f9bFeeB98cA9Bb95b5B0","aTokenSymbol":"aUSDC","stableDebtTokenAddress":"0x252C017036b144A812b53BC122d0E67cBB451aD4","variableDebtTokenAddress":"0xBE9B058a0f2840130372a81eBb3181dcE02BE957","symbol":"USDC","address":"0xe22da380ee6B445bb8273C81944ADEB6E8450422","decimals":6},{"aTokenAddress":"0xFF3c8bc103682FA918c954E84F5056aB4DD5189d","aTokenSymbol":"aUSDT","stableDebtTokenAddress":"0xf3DCeaDf668607bFCF565E84d9644c42eea518cd","variableDebtTokenAddress":"0xa6EfAF3B1C6c8E2be44818dB64E4DEC7416983a1","symbol":"USDT","address":"0x13512979ADE267AB5100878E2e0f485B568328a4","decimals":6},{"aTokenAddress":"0x62538022242513971478fcC7Fb27ae304AB5C29F","aTokenSymbol":"aWBTC","stableDebtTokenAddress":"0x45b85733E2609B9Eb18DbF1315765ddB8431e0B6","variableDebtTokenAddress":"0x9b8107B86A3cD6c8d766B30d3aDD046348bf8dB4","symbol":"WBTC","address":"0xD1B98B6607330172f1D991521145A22BCe793277","decimals":8},{"aTokenAddress":"0x87b1f4cf9BD63f7BBD3eE1aD04E8F52540349347","aTokenSymbol":"aWETH","stableDebtTokenAddress":"0x1F85D0dc45332D00aead98D26db0735350F80D18","variableDebtTokenAddress":"0xDD13CE9DE795E7faCB6fEC90E346C7F3abe342E2","symbol":"WETH","address":"0xd0A1E359811322d97991E03f863a0C30C2cF029C","decimals":18},{"aTokenAddress":"0xF6c7282943Beac96f6C70252EF35501a6c1148Fe","aTokenSymbol":"aYFI","stableDebtTokenAddress":"0x7417855ed88C62e610e612Be52AeE510703Dff04","variableDebtTokenAddress":"0xfF682fF79FEb2C057eC3Ff1e083eFdC66f9b37FB","symbol":"YFI","address":"0xb7c325266ec274fEb1354021D27FA3E3379D840d","decimals":18},{"aTokenAddress":"0xf02D7C23948c9178C68f5294748EB778Ab5e5D9c","aTokenSymbol":"aZRX","stableDebtTokenAddress":"0x7488Eb7fce7e31b91eB9cA4158d54D92e4BB03D7","variableDebtTokenAddress":"0x7a1C28e06bcb4b1fF4768BC2CB9cd33b7622cD62","symbol":"ZRX","address":"0xD0d76886cF8D952ca26177EB7CfDf83bad08C00C","decimals":18},{"aTokenAddress":"0x601FFc9b7309bdb0132a02a569FBd57d6D1740f2","aTokenSymbol":"aUNI","stableDebtTokenAddress":"0x7A43B2653FF42BDE048e3b14fB42028956a7B6b1","variableDebtTokenAddress":"0x10339d6562e8867bB93506572fF8Aea94B2fF656","symbol":"UNI","address":"0x075A36BA8846C6B6F53644fDd3bf17E5151789DC","decimals":18}]} -------------------------------------------------------------------------------- /scripts/createAndRunPrizePoolPolygon.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { ethers, deployments, getNamedAccounts, getChainId } from 'hardhat'; 3 | import Erc20Abi from '../abis/ERC20.json'; 4 | 5 | import PoolWithMultipleWinnersBuilder from '@pooltogether/pooltogether-contracts/deployments/matic/PoolWithMultipleWinnersBuilder.json'; 6 | import RNGBlockhash from '@pooltogether/pooltogether-rng-contracts/deployments/matic_137/RNGBlockhash.json'; 7 | import ControlledToken from '@pooltogether/pooltogether-contracts/abis/ControlledToken.json'; 8 | import MultipleWinners from '@pooltogether/pooltogether-contracts/abis/MultipleWinners.json'; 9 | import YieldSourcePrizePool from '@pooltogether/pooltogether-contracts/abis/YieldSourcePrizePool.json'; 10 | 11 | import { BigNumber } from 'ethers'; 12 | import { dai } from '@studydefi/money-legos/erc20'; 13 | import { info, success } from './helpers'; 14 | 15 | interface DepositAsset { 16 | depositAssetName: string; 17 | depositAssetAddress: string; 18 | depositAmount: BigNumber; 19 | depositAssetAbi: any; 20 | } 21 | 22 | async function createPrizePools() { 23 | console.log('running create prize pool script with chainId ', await getChainId()); 24 | 25 | const { deployer } = await getNamedAccounts(); 26 | 27 | console.log('deployer is ', deployer); 28 | const signer = await ethers.provider.getSigner(deployer); 29 | 30 | const allDeployments = await deployments.all(); 31 | 32 | const aaveTokenAddress = '0x341d1f30e77D3FBfbD43D17183E2acb9dF25574E'; 33 | 34 | console.log('balance of deployer ', await ethers.provider.getBalance(deployer)); 35 | 36 | // minting mumbai AAVE 37 | const aaveTokenContract = await ethers.getContractAt( 38 | Erc20Abi, 39 | aaveTokenAddress, 40 | signer, 41 | ); 42 | // const mintResult = await aaveTokenContract.transfer(deployer, "100") 43 | 44 | // console.log(mintResult) 45 | 46 | // call for each deployed yield source 47 | console.log('running lifecycle for aAAVE'); 48 | 49 | await poolLifecycle(signer, allDeployments.aAAVE.address, { 50 | depositAssetName: 'AAVE', 51 | depositAssetAddress: aaveTokenAddress, 52 | depositAssetAbi: dai.abi, 53 | depositAmount: BigNumber.from(50), 54 | }); 55 | } 56 | createPrizePools(); 57 | 58 | async function poolLifecycle( 59 | contractsOwner: any, 60 | aTokenYieldSourceAddress: string, 61 | depositArgs: DepositAsset, 62 | ) { 63 | const { depositAssetAddress, depositAssetName, depositAmount, depositAssetAbi } = depositArgs; 64 | 65 | const { constants, provider, getContractAt, utils } = ethers; 66 | const { getBlock, getBlockNumber, getTransactionReceipt, send } = provider; 67 | 68 | const { AddressZero } = constants; 69 | const { parseEther: toWei } = utils; 70 | 71 | async function increaseTime(time: number) { 72 | await send('evm_increaseTime', [time]); 73 | await send('evm_mine', []); 74 | } 75 | 76 | const aTokenYieldSource = await getContractAt( 77 | 'ATokenYieldSource', 78 | aTokenYieldSourceAddress, 79 | contractsOwner, 80 | ); 81 | 82 | info('Deploying ATokenYieldSourcePrizePool...'); 83 | 84 | const poolBuilder = await getContractAt( 85 | PoolWithMultipleWinnersBuilder.abi, 86 | PoolWithMultipleWinnersBuilder.address, 87 | contractsOwner, 88 | ); 89 | 90 | const aaveYieldSourcePrizePoolConfig = { 91 | yieldSource: aTokenYieldSource.address, 92 | maxExitFeeMantissa: toWei('0.5'), 93 | maxTimelockDuration: 1000, 94 | }; 95 | 96 | const block = await getBlock(await getBlockNumber()); 97 | 98 | info(`Block number ${block.number}`); 99 | 100 | const multipleWinnersConfig = { 101 | rngService: RNGBlockhash.address, 102 | prizePeriodStart: block.timestamp, 103 | prizePeriodSeconds: 60, 104 | ticketName: 'Ticket', 105 | ticketSymbol: 'TICK', 106 | sponsorshipName: 'Sponsorship', 107 | sponsorshipSymbol: 'SPON', 108 | ticketCreditLimitMantissa: toWei('0.1'), 109 | ticketCreditRateMantissa: toWei('0.001'), 110 | numberOfWinners: 1, 111 | }; 112 | 113 | const yieldSourceMultipleWinnersTx = await poolBuilder.createYieldSourceMultipleWinners( 114 | aaveYieldSourcePrizePoolConfig, 115 | multipleWinnersConfig, 116 | 18, 117 | ); 118 | 119 | console.info(yieldSourceMultipleWinnersTx); 120 | 121 | await new Promise((r) => setTimeout(r, 120000)); // sleep so that rpc provider has it 122 | 123 | console.log('calling getTranasctionReceipt with ', yieldSourceMultipleWinnersTx.hash); 124 | 125 | const yieldSourceMultipleWinnersReceipt = await getTransactionReceipt( 126 | yieldSourceMultipleWinnersTx.hash, 127 | ); 128 | 129 | console.log('yieldSourceMultipleWinnersReceipt ', yieldSourceMultipleWinnersReceipt); 130 | 131 | const yieldSourcePrizePoolInitializedEvents = yieldSourceMultipleWinnersReceipt.logs.map( 132 | (log: any) => { 133 | try { 134 | return poolBuilder.interface.parseLog(log); 135 | } catch (e) { 136 | return null; 137 | } 138 | }, 139 | ); 140 | 141 | console.log('yieldSourcePrizePoolInitializedEvent ', yieldSourcePrizePoolInitializedEvents); 142 | 143 | const yieldSourcePrizePoolInitializedEvent = yieldSourcePrizePoolInitializedEvents.find( 144 | (event: any) => event && event.name === 'YieldSourcePrizePoolWithMultipleWinnersCreated', 145 | ); 146 | 147 | console.log('yieldSourcePrizePoolInitializedEvent ', yieldSourcePrizePoolInitializedEvent); 148 | 149 | console.log( 150 | 'yieldSourcePrizePoolInitializedEvent.args.prizePool ', 151 | yieldSourcePrizePoolInitializedEvent?.args.prizePool, 152 | ); 153 | const prizePool = await getContractAt( 154 | YieldSourcePrizePool, 155 | yieldSourcePrizePoolInitializedEvent?.args.prizePool, 156 | contractsOwner, 157 | ); 158 | 159 | success(`Deployed ATokenYieldSourcePrizePool! ${prizePool.address}`); 160 | 161 | const prizeStrategy = await getContractAt( 162 | MultipleWinners, 163 | await prizePool.prizeStrategy(), 164 | contractsOwner, 165 | ); 166 | 167 | const depositAssetContract = await getContractAt( 168 | depositAssetAbi, 169 | depositAssetAddress, 170 | contractsOwner, 171 | ); 172 | await depositAssetContract.approve(prizePool.address, depositAmount); 173 | 174 | info( 175 | `Depositing ${depositAmount} ${depositAssetName} for ${ 176 | contractsOwner._address 177 | }, ticket ${await prizeStrategy.ticket()}`, 178 | ); 179 | 180 | await prizePool.depositTo( 181 | contractsOwner._address, 182 | depositAmount, 183 | await prizeStrategy.ticket(), 184 | AddressZero, 185 | ); 186 | 187 | success('Deposit Successful!'); 188 | 189 | info(`Prize strategy owner: ${await prizeStrategy.owner()}`); 190 | 191 | info('Starting award...'); 192 | await prizeStrategy.startAward(); 193 | 194 | await new Promise((r) => setTimeout(r, 60000)); // sleep so can completeAward 195 | 196 | info('Completing award...'); 197 | const awardTx = await prizeStrategy.completeAward(); 198 | 199 | await new Promise((r) => setTimeout(r, 220000)); 200 | const awardReceipt = await getTransactionReceipt(awardTx.hash); 201 | 202 | console.log('awardReceipt ', awardReceipt); 203 | 204 | const awardLogs = awardReceipt.logs.map((log: any) => { 205 | try { 206 | return prizePool.interface.parseLog(log); 207 | } catch (e) { 208 | return null; 209 | } 210 | }); 211 | 212 | const completeAwardLogs = awardReceipt.logs.map((log: any) => { 213 | try { 214 | return prizeStrategy.interface.parseLog(log); 215 | } catch (e) { 216 | return null; 217 | } 218 | }); 219 | 220 | console.log('completeAwardLogs ', completeAwardLogs); 221 | 222 | const awarded = awardLogs.find((event: any) => event && event.name === 'Awarded'); 223 | // console.log("awarded event", awarded) 224 | 225 | if (awarded) { 226 | success(`Awarded ${awarded?.args?.amount} ${depositAssetName}!`); 227 | } 228 | 229 | info('Withdrawing...'); 230 | const ticketAddress = await prizeStrategy.ticket(); 231 | const ticket = await getContractAt(ControlledToken, ticketAddress, contractsOwner); 232 | 233 | const withdrawalAmount = depositAmount.div(2); // withdraw half the amount deposited 234 | 235 | const earlyExitFee = await prizePool.callStatic.calculateEarlyExitFee( 236 | contractsOwner.address, 237 | ticket.address, 238 | withdrawalAmount, 239 | ); 240 | 241 | const withdrawTx = await prizePool.withdrawInstantlyFrom( 242 | contractsOwner._address, 243 | withdrawalAmount, 244 | ticket.address, 245 | earlyExitFee.exitFee, 246 | ); 247 | 248 | await new Promise((r) => setTimeout(r, 220000)); 249 | const withdrawReceipt = await getTransactionReceipt(withdrawTx.hash); 250 | const withdrawLogs = withdrawReceipt.logs.map((log: any) => { 251 | try { 252 | return prizePool.interface.parseLog(log); 253 | } catch (e) { 254 | return null; 255 | } 256 | }); 257 | 258 | const withdrawn = withdrawLogs.find((event: any) => event && event.name === 'InstantWithdrawal'); 259 | success(`Withdrawn ${withdrawn?.args?.redeemed} ${depositAssetName}!`); 260 | success(`Exit fee was ${withdrawn?.args?.exitFee} ${depositAssetName}`); 261 | 262 | await prizePool.captureAwardBalance(); 263 | const awardBalance = await prizePool.callStatic.awardBalance(); 264 | success(`Current awardable balance is ${awardBalance} ${depositAssetName}`); 265 | } 266 | -------------------------------------------------------------------------------- /scripts/createAndRunPrizePoolMumbai.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { ethers, deployments, getNamedAccounts, getChainId } from 'hardhat'; 3 | import Erc20MintableAbi from '../abis/ERC20Mintable.json'; 4 | 5 | import PoolWithMultipleWinnersBuilder from '@pooltogether/pooltogether-contracts/deployments/mumbai/PoolWithMultipleWinnersBuilder.json'; 6 | import RNGBlockhash from '@pooltogether/pooltogether-rng-contracts/deployments/mumbai_80001/RNGBlockhash.json'; 7 | import ControlledToken from '@pooltogether/pooltogether-contracts/abis/ControlledToken.json'; 8 | import MultipleWinners from '@pooltogether/pooltogether-contracts/abis/MultipleWinners.json'; 9 | import YieldSourcePrizePool from '@pooltogether/pooltogether-contracts/abis/YieldSourcePrizePool.json'; 10 | 11 | import { BigNumber, utils } from 'ethers'; 12 | import { dai } from '@studydefi/money-legos/erc20'; 13 | import { info, success } from './helpers'; 14 | 15 | interface DepositAsset { 16 | depositAssetName: string; 17 | depositAssetAddress: string; 18 | depositAmount: BigNumber; 19 | depositAssetAbi: any; 20 | } 21 | 22 | const toWei = utils.parseEther; 23 | 24 | async function createPrizePools() { 25 | console.log('running create prize pool script with chainId ', await getChainId()); 26 | 27 | const { deployer } = await getNamedAccounts(); 28 | 29 | console.log('deployer is ', deployer); 30 | const signer = ethers.provider.getSigner(deployer); 31 | 32 | const allDeployments = await deployments.all(); 33 | 34 | const aaveTokenAddress = '0x341d1f30e77D3FBfbD43D17183E2acb9dF25574E'; 35 | 36 | console.log( 37 | 'balance of deployer ', 38 | ethers.utils.formatEther(await ethers.provider.getBalance(deployer)), 39 | ); 40 | 41 | // minting mumbai AAVE 42 | // const aaveTokenContract = await ethers.getContractAt(Erc20MintableAbi, aaveTokenAddress, signer) 43 | 44 | // await aaveTokenContract.transfer(deployer, toWei('50')) 45 | 46 | // call for each deployed yield source 47 | console.log('running lifecycle for aAAVE'); 48 | 49 | await poolLifecycle(signer, allDeployments.AaveAAVEYieldSource.address, { 50 | depositAssetName: 'AAVE', 51 | depositAssetAddress: aaveTokenAddress, 52 | depositAssetAbi: dai.abi, 53 | depositAmount: BigNumber.from(50), 54 | }); 55 | } 56 | 57 | createPrizePools(); 58 | 59 | async function poolLifecycle( 60 | contractsOwner: any, 61 | aTokenYieldSourceAddress: string, 62 | depositArgs: DepositAsset, 63 | ) { 64 | const { depositAssetAddress, depositAssetName, depositAmount, depositAssetAbi } = depositArgs; 65 | 66 | const { constants, provider, getContractAt, utils } = ethers; 67 | const { getBlock, getBlockNumber, getTransactionReceipt, send } = provider; 68 | 69 | const { AddressZero } = constants; 70 | const { formatEther, parseEther: toWei } = utils; 71 | 72 | async function increaseTime(time: number) { 73 | await send('evm_increaseTime', [time]); 74 | await send('evm_mine', []); 75 | } 76 | 77 | const aTokenYieldSource = await getContractAt( 78 | 'ATokenYieldSource', 79 | aTokenYieldSourceAddress, 80 | contractsOwner, 81 | ); 82 | 83 | info('Deploying ATokenYieldSourcePrizePool...'); 84 | 85 | const poolBuilder = await getContractAt( 86 | PoolWithMultipleWinnersBuilder.abi, 87 | PoolWithMultipleWinnersBuilder.address, 88 | contractsOwner, 89 | ); 90 | 91 | const aaveYieldSourcePrizePoolConfig = { 92 | yieldSource: aTokenYieldSource.address, 93 | maxExitFeeMantissa: toWei('0.5'), 94 | maxTimelockDuration: 1000, 95 | }; 96 | 97 | const block = await getBlock(await getBlockNumber()); 98 | 99 | info(`Block number ${block.number}`); 100 | 101 | const multipleWinnersConfig = { 102 | rngService: RNGBlockhash.address, 103 | prizePeriodStart: block.timestamp, 104 | prizePeriodSeconds: 60, 105 | ticketName: 'Ticket', 106 | ticketSymbol: 'TICK', 107 | sponsorshipName: 'Sponsorship', 108 | sponsorshipSymbol: 'SPON', 109 | ticketCreditLimitMantissa: toWei('0.1'), 110 | ticketCreditRateMantissa: toWei('0.001'), 111 | numberOfWinners: 1, 112 | }; 113 | 114 | console.log('poolBuilder', poolBuilder); 115 | 116 | const yieldSourceMultipleWinnersTx = await poolBuilder.createYieldSourceMultipleWinners( 117 | aaveYieldSourcePrizePoolConfig, 118 | multipleWinnersConfig, 119 | 18, 120 | ); 121 | 122 | console.info(yieldSourceMultipleWinnersTx); 123 | 124 | await new Promise((r) => setTimeout(r, 120000)); // sleep so that rpc provider has it 125 | 126 | console.log('calling getTranasctionReceipt with ', yieldSourceMultipleWinnersTx.hash); 127 | 128 | const yieldSourceMultipleWinnersReceipt = await getTransactionReceipt( 129 | yieldSourceMultipleWinnersTx.hash, 130 | ); 131 | 132 | console.log('yieldSourceMultipleWinnersReceipt ', yieldSourceMultipleWinnersReceipt); 133 | 134 | const yieldSourcePrizePoolInitializedEvents = yieldSourceMultipleWinnersReceipt.logs.map( 135 | (log: any) => { 136 | try { 137 | return poolBuilder.interface.parseLog(log); 138 | } catch (e) { 139 | return null; 140 | } 141 | }, 142 | ); 143 | 144 | console.log('yieldSourcePrizePoolInitializedEvent ', yieldSourcePrizePoolInitializedEvents); 145 | 146 | const yieldSourcePrizePoolInitializedEvent = yieldSourcePrizePoolInitializedEvents.find( 147 | (event: any) => event && event.name === 'YieldSourcePrizePoolWithMultipleWinnersCreated', 148 | ); 149 | 150 | console.log('yieldSourcePrizePoolInitializedEvent ', yieldSourcePrizePoolInitializedEvent); 151 | 152 | console.log( 153 | 'yieldSourcePrizePoolInitializedEvent.args.prizePool ', 154 | yieldSourcePrizePoolInitializedEvent?.args.prizePool, 155 | ); 156 | const prizePool = await getContractAt( 157 | YieldSourcePrizePool, 158 | yieldSourcePrizePoolInitializedEvent?.args.prizePool, 159 | contractsOwner, 160 | ); 161 | 162 | success(`Deployed ATokenYieldSourcePrizePool! ${prizePool.address}`); 163 | 164 | const prizeStrategy = await getContractAt( 165 | MultipleWinners, 166 | await prizePool.prizeStrategy(), 167 | contractsOwner, 168 | ); 169 | 170 | const depositAssetContract = await getContractAt( 171 | depositAssetAbi, 172 | depositAssetAddress, 173 | contractsOwner, 174 | ); 175 | await depositAssetContract.approve(prizePool.address, depositAmount); 176 | 177 | info( 178 | `Depositing ${depositAmount} ${depositAssetName} for ${ 179 | contractsOwner._address 180 | }, ticket ${await prizeStrategy.ticket()}`, 181 | ); 182 | 183 | await prizePool.depositTo( 184 | contractsOwner._address, 185 | depositAmount, 186 | await prizeStrategy.ticket(), 187 | AddressZero, 188 | ); 189 | 190 | success('Deposited To!'); 191 | 192 | info(`Prize strategy owner: ${await prizeStrategy.owner()}`); 193 | 194 | info('Starting award...'); 195 | await prizeStrategy.startAward(); 196 | 197 | await new Promise((r) => setTimeout(r, 60000)); // sleep so can completeAward 198 | 199 | info('Completing award...'); 200 | const awardTx = await prizeStrategy.completeAward(); 201 | 202 | await new Promise((r) => setTimeout(r, 220000)); 203 | const awardReceipt = await getTransactionReceipt(awardTx.hash); 204 | 205 | console.log('awardReceipt ', awardReceipt); 206 | 207 | const awardLogs = awardReceipt.logs.map((log: any) => { 208 | try { 209 | return prizePool.interface.parseLog(log); 210 | } catch (e) { 211 | return null; 212 | } 213 | }); 214 | 215 | const completeAwardLogs = awardReceipt.logs.map((log: any) => { 216 | try { 217 | return prizeStrategy.interface.parseLog(log); 218 | } catch (e) { 219 | return null; 220 | } 221 | }); 222 | 223 | console.log('completeAwardLogs ', completeAwardLogs); 224 | 225 | const awarded = awardLogs.find((event: any) => event && event.name === 'Awarded'); 226 | // console.log("awarded event", awarded) 227 | 228 | if (awarded) { 229 | success(`Awarded ${awarded?.args?.amount} ${depositAssetName}!`); 230 | } 231 | 232 | info('Withdrawing...'); 233 | const ticketAddress = await prizeStrategy.ticket(); 234 | const ticket = await getContractAt(ControlledToken, ticketAddress, contractsOwner); 235 | 236 | const withdrawalAmount = depositAmount.div(2); // withdraw half the amount deposited 237 | 238 | const earlyExitFee = await prizePool.callStatic.calculateEarlyExitFee( 239 | contractsOwner.address, 240 | ticket.address, 241 | withdrawalAmount, 242 | ); 243 | 244 | const withdrawTx = await prizePool.withdrawInstantlyFrom( 245 | contractsOwner._address, 246 | withdrawalAmount, 247 | ticket.address, 248 | earlyExitFee.exitFee, 249 | ); 250 | 251 | await new Promise((r) => setTimeout(r, 220000)); 252 | const withdrawReceipt = await getTransactionReceipt(withdrawTx.hash); 253 | const withdrawLogs = withdrawReceipt.logs.map((log: any) => { 254 | try { 255 | return prizePool.interface.parseLog(log); 256 | } catch (e) { 257 | return null; 258 | } 259 | }); 260 | 261 | const withdrawn = withdrawLogs.find((event: any) => event && event.name === 'InstantWithdrawal'); 262 | success(`Withdrawn ${withdrawn?.args?.redeemed} ${depositAssetName}!`); 263 | success(`Exit fee was ${withdrawn?.args?.exitFee} ${depositAssetName}`); 264 | 265 | await prizePool.captureAwardBalance(); 266 | const awardBalance = await prizePool.callStatic.awardBalance(); 267 | success(`Current awardable balance is ${awardBalance} ${depositAssetName}`); 268 | } 269 | -------------------------------------------------------------------------------- /contracts/yield-source/ATokenYieldSource.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/utils/math/SafeMath.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 9 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 10 | import "@pooltogether/fixed-point/contracts/FixedPoint.sol"; 11 | import "@pooltogether/owner-manager-contracts/contracts/Manageable.sol"; 12 | 13 | import "../external/aave/ILendingPool.sol"; 14 | import "../external/aave/ILendingPoolAddressesProvider.sol"; 15 | import "../external/aave/ILendingPoolAddressesProviderRegistry.sol"; 16 | import "../external/aave/ATokenInterface.sol"; 17 | import "../external/aave/IAaveIncentivesController.sol"; 18 | import "../external/aave/IProtocolYieldSource.sol"; 19 | 20 | /// @title Aave Yield Source integration contract, implementing PoolTogether's generic yield source interface 21 | /// @dev This contract inherits from the ERC20 implementation to keep track of users deposits 22 | /// @dev This contract inherits AssetManager which extends OwnableUpgradable 23 | /// @notice Yield source for a PoolTogether prize pool that generates yield by depositing into Aave V2 24 | contract ATokenYieldSource is ERC20, IProtocolYieldSource, Manageable, ReentrancyGuard { 25 | using SafeMath for uint256; 26 | using SafeERC20 for IERC20; 27 | 28 | /// @notice Emitted when the yield source is initialized 29 | event ATokenYieldSourceInitialized( 30 | IAToken indexed aToken, 31 | ILendingPoolAddressesProviderRegistry lendingPoolAddressesProviderRegistry, 32 | uint8 decimals, 33 | string name, 34 | string symbol, 35 | address owner 36 | ); 37 | 38 | /// @notice Emitted when asset tokens are redeemed from the yield source 39 | event RedeemedToken( 40 | address indexed from, 41 | uint256 shares, 42 | uint256 amount 43 | ); 44 | 45 | /// @notice Emitted when Aave rewards have been claimed 46 | event Claimed( 47 | address indexed user, 48 | address indexed to, 49 | uint256 amount 50 | ); 51 | 52 | /// @notice Emitted when asset tokens are supplied to the yield source 53 | event SuppliedTokenTo( 54 | address indexed from, 55 | uint256 shares, 56 | uint256 amount, 57 | address indexed to 58 | ); 59 | 60 | /// @notice Emitted when asset tokens are supplied to sponsor the yield source 61 | event Sponsored( 62 | address indexed from, 63 | uint256 amount 64 | ); 65 | 66 | /// @notice Emitted when ERC20 tokens other than yield source's aToken are withdrawn from the yield source 67 | event TransferredERC20( 68 | address indexed from, 69 | address indexed to, 70 | uint256 amount, 71 | IERC20 indexed token 72 | ); 73 | 74 | /// @notice Interface for the yield-bearing Aave aToken 75 | ATokenInterface public immutable aToken; 76 | 77 | /// @notice Interface for Aave incentivesController 78 | IAaveIncentivesController public immutable incentivesController; 79 | 80 | /// @notice Interface for Aave lendingPoolAddressesProviderRegistry 81 | ILendingPoolAddressesProviderRegistry public lendingPoolAddressesProviderRegistry; 82 | 83 | /// @notice Underlying asset token address. 84 | address private immutable _tokenAddress; 85 | 86 | /// @notice ERC20 token decimals. 87 | uint8 private immutable __decimals; 88 | 89 | /// @dev Aave genesis market LendingPoolAddressesProvider's ID 90 | /// @dev This variable could evolve in the future if we decide to support other markets 91 | uint256 private constant ADDRESSES_PROVIDER_ID = uint256(0); 92 | 93 | /// @dev PoolTogether's Aave Referral Code 94 | uint16 private constant REFERRAL_CODE = uint16(188); 95 | 96 | /// @notice Initializes the yield source with Aave aToken 97 | /// @param _aToken Aave aToken address 98 | /// @param _incentivesController Aave incentivesController address 99 | /// @param _lendingPoolAddressesProviderRegistry Aave lendingPoolAddressesProviderRegistry address 100 | /// @param _decimals Number of decimals the shares (inhereted ERC20) will have. Set as same as underlying asset to ensure sane ExchangeRates 101 | /// @param _symbol Token symbol for the underlying shares ERC20 102 | /// @param _name Token name for the underlying shares ERC20 103 | constructor ( 104 | ATokenInterface _aToken, 105 | IAaveIncentivesController _incentivesController, 106 | ILendingPoolAddressesProviderRegistry _lendingPoolAddressesProviderRegistry, 107 | uint8 _decimals, 108 | string memory _symbol, 109 | string memory _name, 110 | address _owner 111 | ) Ownable(_owner) ERC20(_name, _symbol) ReentrancyGuard() 112 | { 113 | require(address(_aToken) != address(0), "ATokenYieldSource/aToken-not-zero-address"); 114 | require(address(_incentivesController) != address(0), "ATokenYieldSource/incentivesController-not-zero-address"); 115 | require(address(_lendingPoolAddressesProviderRegistry) != address(0), "ATokenYieldSource/lendingPoolRegistry-not-zero-address"); 116 | require(_owner != address(0), "ATokenYieldSource/owner-not-zero-address"); 117 | require(_decimals > 0, "ATokenYieldSource/decimals-gt-zero"); 118 | 119 | aToken = _aToken; 120 | incentivesController = _incentivesController; 121 | lendingPoolAddressesProviderRegistry = _lendingPoolAddressesProviderRegistry; 122 | __decimals = _decimals; 123 | 124 | address tokenAddress = address(_aToken.UNDERLYING_ASSET_ADDRESS()); 125 | _tokenAddress = tokenAddress; 126 | 127 | // Approve once for max amount 128 | IERC20(tokenAddress).safeApprove(address(_lendingPool()), type(uint256).max); 129 | 130 | emit ATokenYieldSourceInitialized ( 131 | _aToken, 132 | _lendingPoolAddressesProviderRegistry, 133 | _decimals, 134 | _name, 135 | _symbol, 136 | _owner 137 | ); 138 | } 139 | 140 | /// @notice Returns the number of decimals that the token repesenting yield source shares has 141 | /// @return The number of decimals 142 | function decimals() public override view returns (uint8) { 143 | return __decimals; 144 | } 145 | 146 | /// @notice Approve lending pool contract to spend max uint256 amount 147 | /// @dev Emergency function to re-approve max amount if approval amount dropped too low 148 | /// @return true if operation is successful 149 | function approveMaxAmount() external onlyOwner returns (bool) { 150 | address _lendingPoolAddress = address(_lendingPool()); 151 | IERC20 _underlyingAsset = IERC20(_tokenAddress); 152 | 153 | _underlyingAsset.safeIncreaseAllowance( 154 | _lendingPoolAddress, 155 | type(uint256).max.sub(_underlyingAsset.allowance(address(this), _lendingPoolAddress)) 156 | ); 157 | 158 | return true; 159 | } 160 | 161 | /// @notice Returns the ERC20 asset token used for deposits 162 | /// @return The ERC20 asset token address 163 | function depositToken() public view override returns (address) { 164 | return _tokenAddress; 165 | } 166 | 167 | /// @notice Returns user total balance (in asset tokens). This includes the deposits and interest. 168 | /// @param addr User address 169 | /// @return The underlying balance of asset tokens 170 | function balanceOfToken(address addr) external override view returns (uint256) { 171 | return _sharesToToken(balanceOf(addr)); 172 | } 173 | 174 | /// @notice Calculates the number of shares that should be mint or burned when a user deposit or withdraw 175 | /// @param _tokens Amount of tokens 176 | /// @return Number of shares 177 | function _tokenToShares(uint256 _tokens) internal view returns (uint256) { 178 | uint256 _shares; 179 | uint256 _totalSupply = totalSupply(); 180 | 181 | if (_totalSupply == 0) { 182 | _shares = _tokens; 183 | } else { 184 | // rate = tokens / shares 185 | // shares = tokens * (totalShares / yieldSourceTotalSupply) 186 | uint256 _exchangeMantissa = FixedPoint.calculateMantissa(_totalSupply, aToken.balanceOf(address(this))); 187 | _shares = FixedPoint.multiplyUintByMantissa(_tokens, _exchangeMantissa); 188 | } 189 | 190 | return _shares; 191 | } 192 | 193 | /// @notice Calculates the number of tokens a user has in the yield source 194 | /// @param _shares Amount of shares 195 | /// @return Number of tokens 196 | function _sharesToToken(uint256 _shares) internal view returns (uint256) { 197 | uint256 _tokens; 198 | uint256 _totalSupply = totalSupply(); 199 | 200 | if (_totalSupply == 0) { 201 | _tokens = _shares; 202 | } else { 203 | // tokens = (shares * yieldSourceTotalSupply) / totalShares 204 | _tokens = _shares.mul(aToken.balanceOf(address(this))).div(_totalSupply); 205 | } 206 | 207 | return _tokens; 208 | } 209 | 210 | /// @notice Checks that the amount of shares is greater than zero. 211 | /// @param _shares Amount of shares to check 212 | function _requireSharesGTZero(uint256 _shares) internal pure { 213 | require(_shares > 0, "ATokenYieldSource/shares-gt-zero"); 214 | } 215 | 216 | /// @notice Deposit asset tokens to Aave 217 | /// @param mintAmount The amount of asset tokens to be deposited 218 | function _depositToAave(uint256 mintAmount) internal { 219 | IERC20(_tokenAddress).safeTransferFrom(msg.sender, address(this), mintAmount); 220 | _lendingPool().deposit(_tokenAddress, mintAmount, address(this), REFERRAL_CODE); 221 | } 222 | 223 | /// @notice Supplies asset tokens to the yield source 224 | /// @dev Shares corresponding to the number of tokens supplied are mint to the user's balance 225 | /// @dev Asset tokens are supplied to the yield source, then deposited into Aave 226 | /// @param mintAmount The amount of asset tokens to be supplied 227 | /// @param to The user whose balance will receive the tokens 228 | function supplyTokenTo(uint256 mintAmount, address to) external override nonReentrant { 229 | uint256 shares = _tokenToShares(mintAmount); 230 | _requireSharesGTZero(shares); 231 | 232 | _depositToAave(mintAmount); 233 | _mint(to, shares); 234 | 235 | emit SuppliedTokenTo(msg.sender, shares, mintAmount, to); 236 | } 237 | 238 | /// @notice Redeems asset tokens from the yield source 239 | /// @dev Shares corresponding to the number of tokens withdrawn are burnt from the user's balance 240 | /// @dev Asset tokens are withdrawn from Aave, then transferred from the yield source to the user's wallet 241 | /// @param redeemAmount The amount of asset tokens to be redeemed 242 | /// @return The actual amount of asset tokens that were redeemed 243 | function redeemToken(uint256 redeemAmount) external override nonReentrant returns (uint256) { 244 | uint256 shares = _tokenToShares(redeemAmount); 245 | _requireSharesGTZero(shares); 246 | 247 | _burn(msg.sender, shares); 248 | 249 | IERC20 _depositToken = IERC20(_tokenAddress); 250 | uint256 beforeBalance = _depositToken.balanceOf(address(this)); 251 | _lendingPool().withdraw(_tokenAddress, redeemAmount, address(this)); 252 | uint256 afterBalance = _depositToken.balanceOf(address(this)); 253 | 254 | uint256 balanceDiff = afterBalance.sub(beforeBalance); 255 | _depositToken.safeTransfer(msg.sender, balanceDiff); 256 | 257 | emit RedeemedToken(msg.sender, shares, redeemAmount); 258 | return balanceDiff; 259 | } 260 | 261 | /// @notice Transfer ERC20 tokens other than the aTokens held by this contract to the recipient address 262 | /// @dev This function is only callable by the owner or asset manager 263 | /// @param erc20Token The ERC20 token to transfer 264 | /// @param to The recipient of the tokens 265 | /// @param amount The amount of tokens to transfer 266 | function transferERC20(IERC20 erc20Token, address to, uint256 amount) external override onlyManagerOrOwner { 267 | require(address(erc20Token) != address(aToken), "ATokenYieldSource/aToken-transfer-not-allowed"); 268 | erc20Token.safeTransfer(to, amount); 269 | emit TransferredERC20(msg.sender, to, amount, erc20Token); 270 | } 271 | 272 | /// @notice Allows someone to deposit into the yield source without receiving any shares 273 | /// @dev This allows anyone to distribute tokens among the share holders 274 | /// @param amount The amount of tokens to deposit 275 | function sponsor(uint256 amount) external override nonReentrant { 276 | _depositToAave(amount); 277 | emit Sponsored(msg.sender, amount); 278 | } 279 | 280 | /// @notice Claims the accrued rewards for the aToken, accumulating any pending rewards. 281 | /// @param to Address where the claimed rewards will be sent. 282 | /// @return True if operation was successful. 283 | function claimRewards(address to) external onlyManagerOrOwner returns (bool) { 284 | require(to != address(0), "ATokenYieldSource/recipient-not-zero-address"); 285 | 286 | IAaveIncentivesController _incentivesController = incentivesController; 287 | 288 | address[] memory _assets = new address[](1); 289 | _assets[0] = address(aToken); 290 | 291 | uint256 _amount = _incentivesController.getRewardsBalance(_assets, address(this)); 292 | uint256 _amountClaimed = _incentivesController.claimRewards(_assets, _amount, to); 293 | 294 | emit Claimed(msg.sender, to, _amountClaimed); 295 | return true; 296 | } 297 | 298 | /// @notice Retrieves Aave LendingPool address 299 | /// @return A reference to LendingPool interface 300 | function _lendingPool() internal view returns (ILendingPool) { 301 | return ILendingPool( 302 | ILendingPoolAddressesProvider( 303 | lendingPoolAddressesProviderRegistry.getAddressesProvidersList()[ADDRESSES_PROVIDER_ID] 304 | ).getLendingPool() 305 | ); 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /aave/aaveMainnet.json: -------------------------------------------------------------------------------- 1 | {"proto":[{"aTokenAddress":"0x3Ed3B47Dd13EC9a98b44e6204A523E766B225811","aTokenSymbol":"aUSDT","stableDebtTokenAddress":"0xe91D55AB2240594855aBd11b3faAE801Fd4c4687","variableDebtTokenAddress":"0x531842cEbbdD378f8ee36D171d6cC9C4fcf475Ec","symbol":"USDT","address":"0xdAC17F958D2ee523a2206206994597C13D831ec7","decimals":6},{"aTokenAddress":"0x9ff58f4fFB29fA2266Ab25e75e2A8b3503311656","aTokenSymbol":"aWBTC","stableDebtTokenAddress":"0x51B039b9AFE64B78758f8Ef091211b5387eA717c","variableDebtTokenAddress":"0x9c39809Dec7F95F5e0713634a4D0701329B3b4d2","symbol":"WBTC","address":"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599","decimals":8},{"aTokenAddress":"0x030bA81f1c18d280636F32af80b9AAd02Cf0854e","aTokenSymbol":"aWETH","stableDebtTokenAddress":"0x4e977830ba4bd783C0BB7F15d3e243f73FF57121","variableDebtTokenAddress":"0xF63B34710400CAd3e044cFfDcAb00a0f32E33eCf","symbol":"WETH","address":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","decimals":18},{"aTokenAddress":"0x5165d24277cD063F5ac44Efd447B27025e888f37","aTokenSymbol":"aYFI","stableDebtTokenAddress":"0xca823F78C2Dd38993284bb42Ba9b14152082F7BD","variableDebtTokenAddress":"0x7EbD09022Be45AD993BAA1CEc61166Fcc8644d97","symbol":"YFI","address":"0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e","decimals":18},{"aTokenAddress":"0xDf7FF54aAcAcbFf42dfe29DD6144A69b629f8C9e","aTokenSymbol":"aZRX","stableDebtTokenAddress":"0x071B4323a24E73A5afeEbe34118Cd21B8FAAF7C3","variableDebtTokenAddress":"0x85791D117A392097590bDeD3bD5abB8d5A20491A","symbol":"ZRX","address":"0xE41d2489571d322189246DaFA5ebDe1F4699F498","decimals":18},{"aTokenAddress":"0xB9D7CB55f463405CDfBe4E90a6D2Df01C2B92BF1","aTokenSymbol":"aUNI","stableDebtTokenAddress":"0xD939F7430dC8D5a427f156dE1012A56C18AcB6Aa","variableDebtTokenAddress":"0x5BdB050A92CADcCfCDcCCBFC17204a1C9cC0Ab73","symbol":"UNI","address":"0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984","decimals":18},{"aTokenAddress":"0xFFC97d72E13E01096502Cb8Eb52dEe56f74DAD7B","aTokenSymbol":"aAAVE","stableDebtTokenAddress":"0x079D6a3E844BcECf5720478A718Edb6575362C5f","variableDebtTokenAddress":"0xF7DBA49d571745D9d7fcb56225B05BEA803EBf3C","symbol":"AAVE","address":"0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9","decimals":18},{"aTokenAddress":"0x05Ec93c0365baAeAbF7AefFb0972ea7ECdD39CF1","aTokenSymbol":"aBAT","stableDebtTokenAddress":"0x277f8676FAcf4dAA5a6EA38ba511B7F65AA02f9F","variableDebtTokenAddress":"0xfc218A6Dfe6901CB34B1a5281FC6f1b8e7E56877","symbol":"BAT","address":"0x0D8775F648430679A709E98d2b0Cb6250d2887EF","decimals":18},{"aTokenAddress":"0xA361718326c15715591c299427c62086F69923D9","aTokenSymbol":"aBUSD","stableDebtTokenAddress":"0x4A7A63909A72D268b1D8a93a9395d098688e0e5C","variableDebtTokenAddress":"0xbA429f7011c9fa04cDd46a2Da24dc0FF0aC6099c","symbol":"BUSD","address":"0x4Fabb145d64652a948d72533023f6E7A623C7C53","decimals":18},{"aTokenAddress":"0x028171bCA77440897B824Ca71D1c56caC55b68A3","aTokenSymbol":"aDAI","stableDebtTokenAddress":"0x778A13D3eeb110A4f7bb6529F99c000119a08E92","variableDebtTokenAddress":"0x6C3c78838c761c6Ac7bE9F59fe808ea2A6E4379d","symbol":"DAI","address":"0x6B175474E89094C44Da98b954EedeAC495271d0F","decimals":18},{"aTokenAddress":"0xaC6Df26a590F08dcC95D5a4705ae8abbc88509Ef","aTokenSymbol":"aENJ","stableDebtTokenAddress":"0x943DcCA156b5312Aa24c1a08769D67FEce4ac14C","variableDebtTokenAddress":"0x38995F292a6E31b78203254fE1cdd5Ca1010A446","symbol":"ENJ","address":"0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c","decimals":18},{"aTokenAddress":"0x39C6b3e42d6A679d7D776778Fe880BC9487C2EDA","aTokenSymbol":"aKNC","stableDebtTokenAddress":"0x9915dfb872778B2890a117DA1F35F335eb06B54f","variableDebtTokenAddress":"0x6B05D1c608015Ccb8e205A690cB86773A96F39f1","symbol":"KNC","address":"0xdd974D5C2e2928deA5F71b9825b8b646686BD200","decimals":18},{"aTokenAddress":"0xa06bC25B5805d5F8d82847D191Cb4Af5A3e873E0","aTokenSymbol":"aLINK","stableDebtTokenAddress":"0xFB4AEc4Cc858F2539EBd3D37f2a43eAe5b15b98a","variableDebtTokenAddress":"0x0b8f12b1788BFdE65Aa1ca52E3e9F3Ba401be16D","symbol":"LINK","address":"0x514910771AF9Ca656af840dff83E8264EcF986CA","decimals":18},{"aTokenAddress":"0xa685a61171bb30d4072B338c80Cb7b2c865c873E","aTokenSymbol":"aMANA","stableDebtTokenAddress":"0xD86C74eA2224f4B8591560652b50035E4e5c0a3b","variableDebtTokenAddress":"0x0A68976301e46Ca6Ce7410DB28883E309EA0D352","symbol":"MANA","address":"0x0F5D2fB29fb7d3CFeE444a200298f468908cC942","decimals":18},{"aTokenAddress":"0xc713e5E149D5D0715DcD1c156a020976e7E56B88","aTokenSymbol":"aMKR","stableDebtTokenAddress":"0xC01C8E4b12a89456a9fD4e4e75B72546Bf53f0B5","variableDebtTokenAddress":"0xba728eAd5e496BE00DCF66F650b6d7758eCB50f8","symbol":"MKR","address":"0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2","decimals":18},{"aTokenAddress":"0xCC12AbE4ff81c9378D670De1b57F8e0Dd228D77a","aTokenSymbol":"aREN","stableDebtTokenAddress":"0x3356Ec1eFA75d9D150Da1EC7d944D9EDf73703B7","variableDebtTokenAddress":"0xcd9D82d33bd737De215cDac57FE2F7f04DF77FE0","symbol":"REN","address":"0x408e41876cCCDC0F92210600ef50372656052a38","decimals":18},{"aTokenAddress":"0x35f6B052C598d933D69A4EEC4D04c73A191fE6c2","aTokenSymbol":"aSNX","stableDebtTokenAddress":"0x8575c8ae70bDB71606A53AeA1c6789cB0fBF3166","variableDebtTokenAddress":"0x267EB8Cf715455517F9BD5834AeAE3CeA1EBdbD8","symbol":"SNX","address":"0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F","decimals":18},{"aTokenAddress":"0x6C5024Cd4F8A59110119C56f8933403A539555EB","aTokenSymbol":"aSUSD","stableDebtTokenAddress":"0x30B0f7324feDF89d8eff397275F8983397eFe4af","variableDebtTokenAddress":"0xdC6a3Ab17299D9C2A412B0e0a4C1f55446AE0817","symbol":"sUSD","address":"0x57Ab1ec28D129707052df4dF418D58a2D46d5f51","decimals":18},{"aTokenAddress":"0x101cc05f4A51C0319f570d5E146a8C625198e636","aTokenSymbol":"aTUSD","stableDebtTokenAddress":"0x7f38d60D94652072b2C44a18c0e14A481EC3C0dd","variableDebtTokenAddress":"0x01C0eb1f8c6F1C1bF74ae028697ce7AA2a8b0E92","symbol":"TUSD","address":"0x0000000000085d4780B73119b644AE5ecd22b376","decimals":18},{"aTokenAddress":"0xBcca60bB61934080951369a648Fb03DF4F96263C","aTokenSymbol":"aUSDC","stableDebtTokenAddress":"0xE4922afAB0BbaDd8ab2a88E0C79d884Ad337fcA6","variableDebtTokenAddress":"0x619beb58998eD2278e08620f97007e1116D5D25b","symbol":"USDC","address":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","decimals":6},{"aTokenAddress":"0x8dAE6Cb04688C62d939ed9B68d32Bc62e49970b1","aTokenSymbol":"aCRV","stableDebtTokenAddress":"0x9288059a74f589C919c7Cf1Db433251CdFEB874B","variableDebtTokenAddress":"0x00ad8eBF64F141f1C81e9f8f792d3d1631c6c684","symbol":"CRV","address":"0xD533a949740bb3306d119CC777fa900bA034cd52","decimals":18},{"aTokenAddress":"0xD37EE7e4f452C6638c96536e68090De8cBcdb583","aTokenSymbol":"aGUSD","stableDebtTokenAddress":"0xf8aC64ec6Ff8E0028b37EB89772d21865321bCe0","variableDebtTokenAddress":"0x279AF5b99540c1A3A7E3CDd326e19659401eF99e","symbol":"GUSD","address":"0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd","decimals":2},{"aTokenAddress":"0x272F97b7a56a387aE942350bBC7Df5700f8a4576","aTokenSymbol":"aBAL","stableDebtTokenAddress":"0xe569d31590307d05DA3812964F1eDd551D665a0b","variableDebtTokenAddress":"0x13210D4Fe0d5402bd7Ecbc4B5bC5cFcA3b71adB0","symbol":"BAL","address":"0xba100000625a3754423978a60c9317c58a424e3D","decimals":18},{"aTokenAddress":"0xF256CC7847E919FAc9B808cC216cAc87CCF2f47a","aTokenSymbol":"aXSUSHI","stableDebtTokenAddress":"0x73Bfb81D7dbA75C904f430eA8BAe82DB0D41187B","variableDebtTokenAddress":"0xfAFEDF95E21184E3d880bd56D4806c4b8d31c69A","symbol":"xSUSHI","address":"0x8798249c2E607446EfB7Ad49eC89dD1865Ff4272","decimals":18}],"amm":[{"aTokenAddress":"0xf9Fb4AD91812b704Ba883B11d2B576E890a6730A","aTokenSymbol":"aAmmWETH","stableDebtTokenAddress":"0x118Ee405c6be8f9BA7cC7a98064EB5DA462235CF","variableDebtTokenAddress":"0xA4C273d9A0C1fe2674F0E845160d6232768a3064","symbol":"WETH","address":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","decimals":18},{"aTokenAddress":"0x79bE75FFC64DD58e66787E4Eae470c8a1FD08ba4","aTokenSymbol":"aAmmDAI","stableDebtTokenAddress":"0x8da51a5a3129343468a63A96ccae1ff1352a3dfE","variableDebtTokenAddress":"0x3F4fA4937E72991367DC32687BC3278f095E7EAa","symbol":"DAI","address":"0x6B175474E89094C44Da98b954EedeAC495271d0F","decimals":18},{"aTokenAddress":"0xd24946147829DEaA935bE2aD85A3291dbf109c80","aTokenSymbol":"aAmmUSDC","stableDebtTokenAddress":"0xE5971a8a741892F3b3ac3E9c94d02588190cE220","variableDebtTokenAddress":"0xCFDC74b97b69319683fec2A4Ef95c4Ab739F1B12","symbol":"USDC","address":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","decimals":6},{"aTokenAddress":"0x17a79792Fe6fE5C95dFE95Fe3fCEE3CAf4fE4Cb7","aTokenSymbol":"aAmmUSDT","stableDebtTokenAddress":"0x04A0577a89E1b9E8f6c87ee26cCe6a168fFfC5b5","variableDebtTokenAddress":"0xDcFE9BfC246b02Da384de757464a35eFCa402797","symbol":"USDT","address":"0xdAC17F958D2ee523a2206206994597C13D831ec7","decimals":6},{"aTokenAddress":"0x13B2f6928D7204328b0E8E4BCd0379aA06EA21FA","aTokenSymbol":"aAmmWBTC","stableDebtTokenAddress":"0x55E575d092c934503D7635A837584E2900e01d2b","variableDebtTokenAddress":"0x3b99fdaFdfE70d65101a4ba8cDC35dAFbD26375f","symbol":"WBTC","address":"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599","decimals":8},{"aTokenAddress":"0x9303EabC860a743aABcc3A1629014CaBcc3F8D36","aTokenSymbol":"aAmmUniDAIWETH","stableDebtTokenAddress":"0xE9562bf0A11315A1e39f9182F446eA58002f010E","variableDebtTokenAddress":"0x23bcc861b989762275165d08B127911F09c71628","symbol":"UNI-V2","address":"0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11","decimals":18},{"aTokenAddress":"0xc58F53A8adff2fB4eb16ED56635772075E2EE123","aTokenSymbol":"aAmmUniWBTCWETH","stableDebtTokenAddress":"0xeef7d082D9bE2F5eC73C072228706286dea1f492","variableDebtTokenAddress":"0x02aAeB4C7736177242Ee0f71f6f6A0F057Aba87d","symbol":"UNI-V2","address":"0xBb2b8038a1640196FbE3e38816F3e67Cba72D940","decimals":18},{"aTokenAddress":"0xe59d2FF6995a926A574390824a657eEd36801E55","aTokenSymbol":"aAmmUniAAVEWETH","stableDebtTokenAddress":"0x997b26eFf106f138e71160022CaAb0AFC5814643","variableDebtTokenAddress":"0x859ED7D9E92d1fe42fF95C3BC3a62F7cB59C373E","symbol":"UNI-V2","address":"0xDFC14d2Af169B0D36C4EFF567Ada9b2E0CAE044f","decimals":18},{"aTokenAddress":"0xA1B0edF4460CC4d8bFAA18Ed871bFF15E5b57Eb4","aTokenSymbol":"aAmmUniBATWETH","stableDebtTokenAddress":"0x27c67541a4ea26a436e311b2E6fFeC82083a6983","variableDebtTokenAddress":"0x3Fbef89A21Dc836275bC912849627b33c61b09b4","symbol":"UNI-V2","address":"0xB6909B960DbbE7392D405429eB2b3649752b4838","decimals":18},{"aTokenAddress":"0xE340B25fE32B1011616bb8EC495A4d503e322177","aTokenSymbol":"aAmmUniDAIUSDC","stableDebtTokenAddress":"0x6Bb2BdD21920FcB2Ad855AB5d523222F31709d1f","variableDebtTokenAddress":"0x925E3FDd927E20e33C3177C4ff6fb72aD1133C87","symbol":"UNI-V2","address":"0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5","decimals":18},{"aTokenAddress":"0x0ea20e7fFB006d4Cfe84df2F72d8c7bD89247DB0","aTokenSymbol":"aAmmUniCRVWETH","stableDebtTokenAddress":"0xd6035f8803eE9f173b1D3EBc3BDE0Ea6B5165636","variableDebtTokenAddress":"0xF3f1a76cA6356a908CdCdE6b2AC2eaace3739Cd0","symbol":"UNI-V2","address":"0x3dA1313aE46132A397D90d95B1424A9A7e3e0fCE","decimals":18},{"aTokenAddress":"0xb8db81B84d30E2387de0FF330420A4AAA6688134","aTokenSymbol":"aAmmUniLINKWETH","stableDebtTokenAddress":"0xeb32b3A1De9a1915D2b452B673C53883b9Fa6a97","variableDebtTokenAddress":"0xeDe4052ed8e1F422F4E5062c679f6B18693fEcdc","symbol":"UNI-V2","address":"0xa2107FA5B38d9bbd2C461D6EDf11B11A50F6b974","decimals":18},{"aTokenAddress":"0x370adc71f67f581158Dc56f539dF5F399128Ddf9","aTokenSymbol":"aAmmUniMKRWETH","stableDebtTokenAddress":"0x6E7E38bB73E19b62AB5567940Caaa514e9d85982","variableDebtTokenAddress":"0xf36C394775285F89bBBDF09533421E3e81e8447c","symbol":"UNI-V2","address":"0xC2aDdA861F89bBB333c90c492cB837741916A225","decimals":18},{"aTokenAddress":"0xA9e201A4e269d6cd5E9F0FcbcB78520cf815878B","aTokenSymbol":"aAmmUniRENWETH","stableDebtTokenAddress":"0x312edeADf68E69A0f53518bF27EAcD1AbcC2897e","variableDebtTokenAddress":"0x2A8d5B1c1de15bfcd5EC41368C0295c60D8Da83c","symbol":"UNI-V2","address":"0x8Bd1661Da98EBDd3BD080F0bE4e6d9bE8cE9858c","decimals":18},{"aTokenAddress":"0x38E491A71291CD43E8DE63b7253E482622184894","aTokenSymbol":"aAmmUniSNXWETH","stableDebtTokenAddress":"0xef62A0C391D89381ddf8A8C90Ba772081107D287","variableDebtTokenAddress":"0xfd15008efA339A2390B48d2E0Ca8Abd523b406d3","symbol":"UNI-V2","address":"0x43AE24960e5534731Fc831386c07755A2dc33D47","decimals":18},{"aTokenAddress":"0x3D26dcd840fCC8e4B2193AcE8A092e4a65832F9f","aTokenSymbol":"aAmmUniUNIWETH","stableDebtTokenAddress":"0x6febCE732191Dc915D6fB7Dc5FE3AEFDDb85Bd1B","variableDebtTokenAddress":"0x0D878FbB01fbEEa7ddEFb896d56f1D3167af919F","symbol":"UNI-V2","address":"0xd3d2E2692501A5c9Ca623199D38826e513033a17","decimals":18},{"aTokenAddress":"0x391E86e2C002C70dEe155eAceB88F7A3c38f5976","aTokenSymbol":"aAmmUniUSDCWETH","stableDebtTokenAddress":"0xfAB4C9775A4316Ec67a8223ecD0F70F87fF532Fc","variableDebtTokenAddress":"0x26625d1dDf520fC8D975cc68eC6E0391D9d3Df61","symbol":"UNI-V2","address":"0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc","decimals":18},{"aTokenAddress":"0x2365a4890eD8965E564B7E2D27C38Ba67Fec4C6F","aTokenSymbol":"aAmmUniWBTCUSDC","stableDebtTokenAddress":"0xc66bfA05cCe646f05F71DeE333e3229cE24Bbb7e","variableDebtTokenAddress":"0x36dA0C5dC23397CBf9D13BbD74E93C04f99633Af","symbol":"UNI-V2","address":"0x004375Dff511095CC5A197A54140a24eFEF3A416","decimals":18},{"aTokenAddress":"0x5394794Be8b6eD5572FCd6b27103F46b5F390E8f","aTokenSymbol":"aAmmUniYFIWETH","stableDebtTokenAddress":"0x9B054B76d6DE1c4892ba025456A9c4F9be5B1766","variableDebtTokenAddress":"0xDf70Bdf01a3eBcd0D918FF97390852A914a92Df7","symbol":"UNI-V2","address":"0x2fDbAdf3C4D5A8666Bc06645B8358ab803996E28","decimals":18},{"aTokenAddress":"0x358bD0d980E031E23ebA9AA793926857703783BD","aTokenSymbol":"aAmmBptWBTCWETH","stableDebtTokenAddress":"0x46406eCd20FDE1DF4d80F15F07c434fa95CB6b33","variableDebtTokenAddress":"0xF655DF3832859cfB0AcfD88eDff3452b9Aa6Db24","symbol":"BPT","address":"0x1efF8aF5D577060BA4ac8A29A13525bb0Ee2A3D5","decimals":18},{"aTokenAddress":"0xd109b2A304587569c84308c55465cd9fF0317bFB","aTokenSymbol":"aAmmBptBALWETH","stableDebtTokenAddress":"0x6474d116476b8eDa1B21472a599Ff76A829AbCbb","variableDebtTokenAddress":"0xF41A5Cc7a61519B08056176d7B4b87AB34dF55AD","symbol":"BPT","address":"0x59A19D8c652FA0284f44113D0ff9aBa70bd46fB4","decimals":18}]} -------------------------------------------------------------------------------- /test/ATokenYieldSource.test.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | 3 | import { Signer } from '@ethersproject/abstract-signer'; 4 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 5 | 6 | import { expect } from 'chai'; 7 | import { BigNumber } from 'ethers'; 8 | import { MockContract } from 'ethereum-waffle'; 9 | import { ethers, waffle } from 'hardhat'; 10 | 11 | import { 12 | ATokenYieldSourceHarness, 13 | ATokenYieldSourceHarness__factory, 14 | AaveLendingPool, 15 | ATokenMintable, 16 | ERC20Mintable, 17 | } from '../types'; 18 | 19 | import IAaveIncentivesController from '../abis/IAaveIncentivesController.json'; 20 | import ILendingPoolAddressesProvider from '../abis/ILendingPoolAddressesProvider.json'; 21 | import ILendingPoolAddressesProviderRegistry from '../abis/ILendingPoolAddressesProviderRegistry.json'; 22 | import SafeERC20Wrapper from '../abis/SafeERC20Wrapper.json'; 23 | 24 | const { constants, getContractFactory, getSigners, utils } = ethers; 25 | const { AddressZero, MaxUint256, Zero } = constants; 26 | const { parseUnits, formatEther } = utils; 27 | 28 | const DECIMALS = 6; 29 | 30 | const toWei = (amount: string) => parseUnits(amount, DECIMALS); 31 | 32 | describe('ATokenYieldSource', () => { 33 | let contractsOwner: Signer; 34 | let yieldSourceOwner: SignerWithAddress; 35 | let wallet2: SignerWithAddress; 36 | let attacker: SignerWithAddress; 37 | 38 | let aToken: ATokenMintable; 39 | let incentivesController: MockContract; 40 | let lendingPool: AaveLendingPool; 41 | let lendingPoolAddressesProvider: MockContract; 42 | let lendingPoolAddressesProviderRegistry: MockContract; 43 | 44 | let aTokenYieldSource: ATokenYieldSourceHarness; 45 | 46 | let erc20Token: MockContract; 47 | let usdcToken: ERC20Mintable; 48 | 49 | let constructorTest = false; 50 | 51 | const deployATokenYieldSource = async ( 52 | aTokenAddress: string, 53 | incentivesControllerAddress: string, 54 | lendingPoolAddressesProviderRegistryAddress: string, 55 | decimals: number, 56 | owner: string, 57 | ) => { 58 | const ATokenYieldSource = (await ethers.getContractFactory( 59 | 'ATokenYieldSourceHarness', 60 | )) as ATokenYieldSourceHarness__factory; 61 | 62 | return await ATokenYieldSource.deploy( 63 | aTokenAddress, 64 | incentivesControllerAddress, 65 | lendingPoolAddressesProviderRegistryAddress, 66 | decimals, 67 | 'PoolTogether aUSDC Yield', 68 | 'PTaUSDC', 69 | owner, 70 | ); 71 | }; 72 | 73 | const supplyTokenTo = async (user: SignerWithAddress, userAmount: BigNumber) => { 74 | const userAddress = user.address; 75 | 76 | await usdcToken.mint(userAddress, userAmount); 77 | await usdcToken.connect(user).approve(aTokenYieldSource.address, MaxUint256); 78 | 79 | await aTokenYieldSource.connect(user).supplyTokenTo(userAmount, userAddress); 80 | }; 81 | 82 | const sharesToToken = async (shares: BigNumber, yieldSourceTotalSupply: BigNumber) => { 83 | const totalShares = await aTokenYieldSource.totalSupply(); 84 | 85 | // tokens = (shares * yieldSourceTotalSupply) / totalShares 86 | return shares.mul(yieldSourceTotalSupply).div(totalShares); 87 | }; 88 | 89 | const tokenToShares = async (token: BigNumber, yieldSourceTotalSupply: BigNumber) => { 90 | const totalShares = await aTokenYieldSource.totalSupply(); 91 | 92 | // shares = (tokens * totalSupply) / yieldSourceBalanceOfAToken 93 | return token.mul(totalShares).div(yieldSourceTotalSupply); 94 | }; 95 | 96 | beforeEach(async () => { 97 | const { deployMockContract } = waffle; 98 | 99 | [contractsOwner, yieldSourceOwner, wallet2, attacker] = await getSigners(); 100 | 101 | const ERC20MintableContract = await getContractFactory('ERC20Mintable', contractsOwner); 102 | 103 | debug('Mocking tokens...'); 104 | erc20Token = await deployMockContract(contractsOwner, SafeERC20Wrapper); 105 | usdcToken = await ERC20MintableContract.deploy('USDC Stablecoin', 'USDC', DECIMALS); 106 | 107 | const ATokenMintableContract = await getContractFactory('ATokenMintable', contractsOwner); 108 | 109 | aToken = (await ATokenMintableContract.deploy( 110 | usdcToken.address, 111 | 'Aave interest bearing USDC', 112 | 'aUSDC', 113 | DECIMALS, 114 | )) as ATokenMintable; 115 | 116 | debug('Mocking contracts...'); 117 | 118 | const AavePoolContract = await getContractFactory('AaveLendingPool', contractsOwner); 119 | 120 | lendingPool = (await AavePoolContract.deploy( 121 | usdcToken.address, 122 | aToken.address, 123 | )) as AaveLendingPool; 124 | 125 | incentivesController = await deployMockContract(contractsOwner, IAaveIncentivesController); 126 | 127 | lendingPoolAddressesProvider = await deployMockContract( 128 | contractsOwner, 129 | ILendingPoolAddressesProvider, 130 | ); 131 | 132 | lendingPoolAddressesProviderRegistry = await deployMockContract( 133 | contractsOwner, 134 | ILendingPoolAddressesProviderRegistry, 135 | ); 136 | 137 | await lendingPoolAddressesProvider.mock.getLendingPool.returns(lendingPool.address); 138 | await lendingPoolAddressesProviderRegistry.mock.getAddressesProvidersList.returns([ 139 | lendingPoolAddressesProvider.address, 140 | '0x67FB118A780fD740C8936511947cC4bE7bb7730c', 141 | ]); 142 | 143 | debug('Deploying ATokenYieldSource...'); 144 | 145 | if (!constructorTest) { 146 | aTokenYieldSource = await deployATokenYieldSource( 147 | aToken.address, 148 | incentivesController.address, 149 | lendingPoolAddressesProviderRegistry.address, 150 | DECIMALS, 151 | yieldSourceOwner.address, 152 | ); 153 | } 154 | }); 155 | 156 | describe('constructor()', () => { 157 | beforeEach(() => { 158 | constructorTest = true; 159 | }); 160 | 161 | afterEach(() => { 162 | constructorTest = false; 163 | }); 164 | 165 | it('should fail if aToken is address zero', async () => { 166 | await expect( 167 | deployATokenYieldSource( 168 | AddressZero, 169 | incentivesController.address, 170 | lendingPoolAddressesProviderRegistry.address, 171 | DECIMALS, 172 | yieldSourceOwner.address, 173 | ), 174 | ).to.be.revertedWith('ATokenYieldSource/aToken-not-zero-address'); 175 | }); 176 | 177 | it('should fail if incentivesController is address zero', async () => { 178 | await expect( 179 | deployATokenYieldSource( 180 | aToken.address, 181 | AddressZero, 182 | lendingPoolAddressesProviderRegistry.address, 183 | DECIMALS, 184 | yieldSourceOwner.address, 185 | ), 186 | ).to.be.revertedWith('ATokenYieldSource/incentivesController-not-zero-address'); 187 | }); 188 | 189 | it('should fail if lendingPoolAddressesProviderRegistry is address zero', async () => { 190 | await expect( 191 | deployATokenYieldSource( 192 | aToken.address, 193 | incentivesController.address, 194 | AddressZero, 195 | DECIMALS, 196 | yieldSourceOwner.address, 197 | ), 198 | ).to.be.revertedWith('ATokenYieldSource/lendingPoolRegistry-not-zero-address'); 199 | }); 200 | 201 | it('should fail if owner is address zero', async () => { 202 | await expect( 203 | deployATokenYieldSource( 204 | aToken.address, 205 | incentivesController.address, 206 | lendingPoolAddressesProviderRegistry.address, 207 | DECIMALS, 208 | AddressZero, 209 | ), 210 | ).to.be.revertedWith('ATokenYieldSource/owner-not-zero-address'); 211 | }); 212 | 213 | it('should fail if token decimal is not greater than 0', async () => { 214 | await expect( 215 | deployATokenYieldSource( 216 | aToken.address, 217 | incentivesController.address, 218 | lendingPoolAddressesProviderRegistry.address, 219 | 0, 220 | yieldSourceOwner.address, 221 | ), 222 | ).to.be.revertedWith('ATokenYieldSource/decimals-gt-zero'); 223 | }); 224 | }); 225 | 226 | describe('create()', () => { 227 | it('should create ATokenYieldSource', async () => { 228 | expect(await aTokenYieldSource.decimals()).to.equal(DECIMALS); 229 | expect(await aTokenYieldSource.aToken()).to.equal(aToken.address); 230 | expect(await aTokenYieldSource.lendingPoolAddressesProviderRegistry()).to.equal( 231 | lendingPoolAddressesProviderRegistry.address, 232 | ); 233 | 234 | expect(await aTokenYieldSource.owner()).to.equal(yieldSourceOwner.address); 235 | }); 236 | }); 237 | 238 | describe('approveMaxAmount()', () => { 239 | it('should approve lending pool to spend max uint256 amount', async () => { 240 | await aTokenYieldSource.approveLendingPool(Zero); 241 | await aTokenYieldSource.connect(yieldSourceOwner).approveMaxAmount(); 242 | 243 | expect(await usdcToken.allowance(aTokenYieldSource.address, lendingPool.address)).to.equal( 244 | MaxUint256, 245 | ); 246 | }); 247 | 248 | it('should fail if not owner', async () => { 249 | await expect(aTokenYieldSource.connect(wallet2).approveMaxAmount()).to.be.revertedWith( 250 | 'Ownable/caller-not-owner', 251 | ); 252 | }); 253 | }); 254 | 255 | describe('depositToken()', () => { 256 | it('should return the underlying token', async () => { 257 | expect(await aTokenYieldSource.depositToken()).to.equal(usdcToken.address); 258 | }); 259 | }); 260 | 261 | describe('balanceOfToken()', () => { 262 | it('should return user balance', async () => { 263 | const firstAmount = toWei('100'); 264 | const yieldSourceTotalSupply = firstAmount.mul(2); 265 | 266 | await supplyTokenTo(yieldSourceOwner, firstAmount); 267 | await supplyTokenTo(yieldSourceOwner, firstAmount); 268 | 269 | const shares = await aTokenYieldSource.balanceOf(yieldSourceOwner.address); 270 | const tokens = await sharesToToken(shares, yieldSourceTotalSupply); 271 | 272 | expect(await aTokenYieldSource.balanceOfToken(yieldSourceOwner.address)).to.equal(tokens); 273 | }); 274 | }); 275 | 276 | describe('_tokenToShares()', () => { 277 | it('should return shares amount', async () => { 278 | const amount = toWei('100'); 279 | 280 | await supplyTokenTo(yieldSourceOwner, amount); 281 | await supplyTokenTo(wallet2, amount); 282 | 283 | const tokens = toWei('10'); 284 | const shares = await tokenToShares(tokens, amount.mul(2)); 285 | 286 | expect(await aTokenYieldSource.tokenToShares(toWei('10'))).to.equal(shares); 287 | }); 288 | 289 | it('should return 0 if tokens param is 0', async () => { 290 | expect(await aTokenYieldSource.tokenToShares('0')).to.equal('0'); 291 | }); 292 | 293 | it('should return tokens if totalSupply is 0', async () => { 294 | expect(await aTokenYieldSource.tokenToShares(toWei('100'))).to.equal(toWei('100')); 295 | }); 296 | 297 | it('should return shares even if aToken total supply has a lot of decimals', async () => { 298 | const tokens = toWei('0.000005'); 299 | const shares = toWei('1'); 300 | 301 | await aTokenYieldSource.mint(yieldSourceOwner.address, shares); 302 | await aToken.mint(aTokenYieldSource.address, tokens); 303 | 304 | expect(await aTokenYieldSource.tokenToShares(tokens)).to.equal(shares); 305 | }); 306 | 307 | it('should return shares even if aToken total supply increases', async () => { 308 | const amount = toWei('100'); 309 | const tokens = toWei('1'); 310 | 311 | await aTokenYieldSource.mint(yieldSourceOwner.address, amount); 312 | await aTokenYieldSource.mint(wallet2.address, amount); 313 | await aToken.mint(aTokenYieldSource.address, amount); 314 | 315 | expect(await aTokenYieldSource.tokenToShares(tokens)).to.equal(toWei('2')); 316 | 317 | await aToken.mint(aTokenYieldSource.address, parseUnits('100', 12).sub(amount)); 318 | 319 | expect(await aTokenYieldSource.tokenToShares(tokens)).to.equal(2); 320 | }); 321 | 322 | it('should fail to return shares if aToken total supply increases too much', async () => { 323 | const amount = toWei('100'); 324 | const tokens = toWei('1'); 325 | 326 | await aTokenYieldSource.mint(yieldSourceOwner.address, amount); 327 | await aTokenYieldSource.mint(wallet2.address, amount); 328 | await aToken.mint(aTokenYieldSource.address, amount); 329 | 330 | expect(await aTokenYieldSource.tokenToShares(tokens)).to.equal(toWei('2')); 331 | 332 | await aToken.mint(aTokenYieldSource.address, parseUnits('100', 13).sub(amount)); 333 | 334 | await expect(aTokenYieldSource.supplyTokenTo(tokens, wallet2.address)).to.be.revertedWith( 335 | 'ATokenYieldSource/shares-gt-zero', 336 | ); 337 | }); 338 | }); 339 | 340 | describe('_sharesToToken()', () => { 341 | it('should return tokens amount', async () => { 342 | const amount = toWei('100'); 343 | 344 | await aTokenYieldSource.mint(yieldSourceOwner.address, amount); 345 | await aTokenYieldSource.mint(wallet2.address, amount); 346 | await aToken.mint(aTokenYieldSource.address, toWei('1000')); 347 | 348 | expect(await aTokenYieldSource.sharesToToken(toWei('2'))).to.equal(toWei('10')); 349 | }); 350 | 351 | it('should return shares if totalSupply is 0', async () => { 352 | const shares = toWei('100'); 353 | expect(await aTokenYieldSource.sharesToToken(shares)).to.equal(shares); 354 | }); 355 | 356 | it('should return tokens even if shares are very small', async () => { 357 | const shares = toWei('0.000005'); 358 | const tokens = toWei('100'); 359 | 360 | await aTokenYieldSource.mint(yieldSourceOwner.address, shares); 361 | await aToken.mint(aTokenYieldSource.address, tokens); 362 | 363 | expect(await aTokenYieldSource.sharesToToken(shares)).to.equal(tokens); 364 | }); 365 | 366 | it('should return tokens even if aToken total supply increases', async () => { 367 | const amount = toWei('100'); 368 | const tokens = toWei('1'); 369 | 370 | await aTokenYieldSource.mint(yieldSourceOwner.address, amount); 371 | await aTokenYieldSource.mint(wallet2.address, amount); 372 | await aToken.mint(aTokenYieldSource.address, amount); 373 | 374 | expect(await aTokenYieldSource.sharesToToken(toWei('2'))).to.equal(tokens); 375 | 376 | await aToken.mint(aTokenYieldSource.address, parseUnits('100', 12).sub(amount)); 377 | 378 | expect(await aTokenYieldSource.sharesToToken(2)).to.equal(tokens); 379 | }); 380 | }); 381 | 382 | describe('supplyTokenTo()', () => { 383 | let amount: BigNumber; 384 | let tokenAddress: any; 385 | 386 | beforeEach(async () => { 387 | amount = toWei('100'); 388 | tokenAddress = await aTokenYieldSource.tokenAddress(); 389 | }); 390 | 391 | it('should supply assets if totalSupply is 0', async () => { 392 | await supplyTokenTo(yieldSourceOwner, amount); 393 | expect(await aTokenYieldSource.totalSupply()).to.equal(amount); 394 | }); 395 | 396 | it('should supply assets if totalSupply is not 0', async () => { 397 | await supplyTokenTo(yieldSourceOwner, amount); 398 | await supplyTokenTo(wallet2, amount); 399 | }); 400 | 401 | it('should fail to manipulate share price significantly', async () => { 402 | const attackAmount = toWei('10'); 403 | const aTokenAmount = toWei('1000'); 404 | const attackerBalance = attackAmount.add(aTokenAmount); 405 | 406 | await supplyTokenTo(attacker, attackAmount); 407 | 408 | // Attacker sends 1000 aTokens directly to the contract to manipulate share price 409 | await aToken.mint(attacker.address, aTokenAmount); 410 | await aToken.connect(attacker).approve(aTokenYieldSource.address, aTokenAmount); 411 | await aToken.connect(attacker).transfer(aTokenYieldSource.address, aTokenAmount); 412 | 413 | await supplyTokenTo(wallet2, amount); 414 | 415 | expect(await aTokenYieldSource.balanceOfToken(attacker.address)).to.equal(attackerBalance); 416 | 417 | // We account for a small loss in precision due to the attack 418 | expect(await aTokenYieldSource.balanceOfToken(wallet2.address)).to.be.gte( 419 | amount.sub(toWei('0.0001')), 420 | ); 421 | }); 422 | 423 | it('should succeed to manipulate share price significantly but users should not be able to deposit smaller amounts', async () => { 424 | const attackAmount = BigNumber.from(1); 425 | const aTokenAmount = toWei('1000'); 426 | 427 | await supplyTokenTo(attacker, attackAmount); 428 | 429 | // Attacker sends 1000 aTokens directly to the contract to manipulate share price 430 | await aToken.mint(attacker.address, aTokenAmount); 431 | await aToken.connect(attacker).approve(aTokenYieldSource.address, aTokenAmount); 432 | await aToken.connect(attacker).transfer(aTokenYieldSource.address, aTokenAmount); 433 | 434 | await expect(supplyTokenTo(wallet2, amount)).to.be.revertedWith( 435 | 'ATokenYieldSource/shares-gt-zero', 436 | ); 437 | }); 438 | }); 439 | 440 | describe('redeemToken()', () => { 441 | let yieldSourceOwnerBalance: BigNumber; 442 | let redeemAmount: BigNumber; 443 | 444 | beforeEach(() => { 445 | yieldSourceOwnerBalance = toWei('300'); 446 | redeemAmount = toWei('100'); 447 | }); 448 | 449 | it('should redeem assets', async () => { 450 | await supplyTokenTo(yieldSourceOwner, yieldSourceOwnerBalance); 451 | 452 | await aTokenYieldSource.connect(yieldSourceOwner).redeemToken(redeemAmount); 453 | 454 | expect(await aTokenYieldSource.balanceOf(yieldSourceOwner.address)).to.equal( 455 | yieldSourceOwnerBalance.sub(redeemAmount), 456 | ); 457 | }); 458 | 459 | it('should not be able to redeem assets if balance is 0', async () => { 460 | await expect( 461 | aTokenYieldSource.connect(yieldSourceOwner).redeemToken(redeemAmount), 462 | ).to.be.revertedWith('ERC20: burn amount exceeds balance'); 463 | }); 464 | 465 | it('should fail to redeem if amount is greater than balance', async () => { 466 | const yieldSourceOwnerLowBalance = toWei('10'); 467 | 468 | await supplyTokenTo(yieldSourceOwner, yieldSourceOwnerLowBalance); 469 | 470 | await expect( 471 | aTokenYieldSource.connect(yieldSourceOwner).redeemToken(redeemAmount), 472 | ).to.be.revertedWith('ERC20: burn amount exceeds balance'); 473 | }); 474 | 475 | it('should succeed to manipulate share price but fail to redeem without burning any shares', async () => { 476 | const amount = toWei('100000'); 477 | const attackAmount = BigNumber.from(1); 478 | const aTokenAmount = toWei('10000'); 479 | 480 | await supplyTokenTo(attacker, attackAmount); 481 | 482 | // Attacker sends 10000 aTokens directly to the contract to manipulate share price 483 | await aToken.mint(attacker.address, aTokenAmount); 484 | await aToken.connect(attacker).approve(aTokenYieldSource.address, aTokenAmount); 485 | await aToken.connect(attacker).transfer(aTokenYieldSource.address, aTokenAmount); 486 | 487 | await supplyTokenTo(wallet2, amount); 488 | 489 | const sharePrice = await aTokenYieldSource.sharesToToken(BigNumber.from(1)); 490 | 491 | // Redeem 1 wei less than the full amount of a share to burn 0 share instead of 1 because of rounding error 492 | // The actual amount of shares to be burnt should be 0.99 but since Solidity truncates down, it will be 0 493 | const attackerRedeemAmount = sharePrice.sub(1); 494 | 495 | await expect( 496 | aTokenYieldSource.connect(attacker).redeemToken(attackerRedeemAmount), 497 | ).to.be.revertedWith('ATokenYieldSource/shares-gt-zero'); 498 | }); 499 | }); 500 | 501 | describe('transferERC20()', () => { 502 | it('should transferERC20 if yieldSourceOwner', async () => { 503 | const transferAmount = toWei('10'); 504 | 505 | await erc20Token.mock.transfer.withArgs(wallet2.address, transferAmount).returns(true); 506 | 507 | await aTokenYieldSource 508 | .connect(yieldSourceOwner) 509 | .transferERC20(erc20Token.address, wallet2.address, transferAmount); 510 | }); 511 | 512 | it('should transferERC20 if assetManager', async () => { 513 | const transferAmount = toWei('10'); 514 | 515 | await erc20Token.mock.transfer 516 | .withArgs(yieldSourceOwner.address, transferAmount) 517 | .returns(true); 518 | 519 | await aTokenYieldSource.connect(yieldSourceOwner).setManager(wallet2.address); 520 | 521 | await aTokenYieldSource 522 | .connect(wallet2) 523 | .transferERC20(erc20Token.address, yieldSourceOwner.address, transferAmount); 524 | }); 525 | 526 | it('should not allow to transfer aToken', async () => { 527 | await expect( 528 | aTokenYieldSource 529 | .connect(yieldSourceOwner) 530 | .transferERC20(aToken.address, wallet2.address, toWei('10')), 531 | ).to.be.revertedWith('ATokenYieldSource/aToken-transfer-not-allowed'); 532 | }); 533 | 534 | it('should fail to transferERC20 if not yieldSourceOwner or assetManager', async () => { 535 | await expect( 536 | aTokenYieldSource 537 | .connect(wallet2) 538 | .transferERC20(erc20Token.address, yieldSourceOwner.address, toWei('10')), 539 | ).to.be.revertedWith('Manageable/caller-not-manager-or-owner'); 540 | }); 541 | }); 542 | 543 | describe('sponsor()', () => { 544 | let amount: BigNumber; 545 | let tokenAddress: any; 546 | 547 | beforeEach(async () => { 548 | amount = toWei('500'); 549 | tokenAddress = await aTokenYieldSource.tokenAddress(); 550 | }); 551 | 552 | it('should sponsor Yield Source', async () => { 553 | const wallet2Amount = toWei('100'); 554 | 555 | await supplyTokenTo(wallet2, wallet2Amount); 556 | 557 | await usdcToken.mint(yieldSourceOwner.address, amount); 558 | await usdcToken.connect(yieldSourceOwner).approve(aTokenYieldSource.address, MaxUint256); 559 | 560 | await aTokenYieldSource.connect(yieldSourceOwner).sponsor(amount); 561 | 562 | expect(await aTokenYieldSource.balanceOfToken(wallet2.address)).to.equal( 563 | amount.add(wallet2Amount), 564 | ); 565 | }); 566 | }); 567 | 568 | describe('claimRewards()', () => { 569 | const claimAmount = toWei('100'); 570 | 571 | beforeEach(async () => { 572 | await incentivesController.mock.getRewardsBalance 573 | .withArgs([aToken.address], aTokenYieldSource.address) 574 | .returns(claimAmount); 575 | 576 | await incentivesController.mock.claimRewards 577 | .withArgs([aToken.address], claimAmount, wallet2.address) 578 | .returns(claimAmount); 579 | }); 580 | 581 | it('should claimRewards if yieldSourceOwner', async () => { 582 | await expect(aTokenYieldSource.connect(yieldSourceOwner).claimRewards(wallet2.address)) 583 | .to.emit(aTokenYieldSource, 'Claimed') 584 | .withArgs(yieldSourceOwner.address, wallet2.address, claimAmount); 585 | }); 586 | 587 | it('should claimRewards if assetManager', async () => { 588 | await aTokenYieldSource.connect(yieldSourceOwner).setManager(wallet2.address); 589 | 590 | await expect(aTokenYieldSource.connect(wallet2).claimRewards(wallet2.address)) 591 | .to.emit(aTokenYieldSource, 'Claimed') 592 | .withArgs(wallet2.address, wallet2.address, claimAmount); 593 | }); 594 | 595 | it('should fail to claimRewards if recipient is address zero', async () => { 596 | await expect( 597 | aTokenYieldSource.connect(yieldSourceOwner).claimRewards(AddressZero), 598 | ).to.be.revertedWith('ATokenYieldSource/recipient-not-zero-address'); 599 | }); 600 | 601 | it('should fail to claimRewards if not yieldSourceOwner or assetManager', async () => { 602 | await expect( 603 | aTokenYieldSource.connect(wallet2).claimRewards(wallet2.address), 604 | ).to.be.revertedWith('Manageable/caller-not-manager-or-owner'); 605 | }); 606 | }); 607 | 608 | describe('_lendingPool()', () => { 609 | it('should return Aave LendingPool address', async () => { 610 | expect(await aTokenYieldSource.lendingPool()).to.equal(lendingPool.address); 611 | }); 612 | }); 613 | }); 614 | --------------------------------------------------------------------------------