├── .gitignore ├── .solhint.json ├── LICENSE ├── README.md ├── contracts ├── AaveLoop.sol ├── IAaveInterfaces.sol └── ImmutableOwnable.sol ├── hardhat.config.ts ├── package.json ├── test ├── aave-e2e-test.ts ├── aave-emergency-test.ts ├── consts.ts ├── sanity-test.ts └── test-base.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .config.json 2 | artifacts/ 3 | cache/ 4 | typechain/ 5 | typechain-abi/ 6 | typechain-hardhat/ 7 | 8 | 9 | # Created by https://www.toptal.com/developers/gitignore/api/zsh,node,macos,intellij+all,solidity,soliditytruffle,vscode,sublimetext 10 | # Edit at https://www.toptal.com/developers/gitignore?templates=zsh,node,macos,intellij+all,solidity,soliditytruffle,vscode,sublimetext 11 | 12 | ### Intellij+all ### 13 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 14 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 15 | 16 | # User-specific stuff 17 | .idea/**/workspace.xml 18 | .idea/**/tasks.xml 19 | .idea/**/usage.statistics.xml 20 | .idea/**/dictionaries 21 | .idea/**/shelf 22 | 23 | # Generated files 24 | .idea/**/contentModel.xml 25 | 26 | # Sensitive or high-churn files 27 | .idea/**/dataSources/ 28 | .idea/**/dataSources.ids 29 | .idea/**/dataSources.local.xml 30 | .idea/**/sqlDataSources.xml 31 | .idea/**/dynamic.xml 32 | .idea/**/uiDesigner.xml 33 | .idea/**/dbnavigator.xml 34 | 35 | # Gradle 36 | .idea/**/gradle.xml 37 | .idea/**/libraries 38 | 39 | # Gradle and Maven with auto-import 40 | # When using Gradle or Maven with auto-import, you should exclude module files, 41 | # since they will be recreated, and may cause churn. Uncomment if using 42 | # auto-import. 43 | # .idea/artifacts 44 | # .idea/compiler.xml 45 | # .idea/jarRepositories.xml 46 | # .idea/modules.xml 47 | # .idea/*.iml 48 | # .idea/modules 49 | # *.iml 50 | # *.ipr 51 | 52 | # CMake 53 | cmake-build-*/ 54 | 55 | # Mongo Explorer plugin 56 | .idea/**/mongoSettings.xml 57 | 58 | # File-based project format 59 | *.iws 60 | 61 | # IntelliJ 62 | out/ 63 | 64 | # mpeltonen/sbt-idea plugin 65 | .idea_modules/ 66 | 67 | # JIRA plugin 68 | atlassian-ide-plugin.xml 69 | 70 | # Cursive Clojure plugin 71 | .idea/replstate.xml 72 | 73 | # Crashlytics plugin (for Android Studio and IntelliJ) 74 | com_crashlytics_export_strings.xml 75 | crashlytics.properties 76 | crashlytics-build.properties 77 | fabric.properties 78 | 79 | # Editor-based Rest Client 80 | .idea/httpRequests 81 | 82 | # Android studio 3.1+ serialized cache file 83 | .idea/caches/build_file_checksums.ser 84 | 85 | ### Intellij+all Patch ### 86 | # Ignores the whole .idea folder and all .iml files 87 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 88 | 89 | .idea/ 90 | 91 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 92 | 93 | *.iml 94 | modules.xml 95 | .idea/misc.xml 96 | *.ipr 97 | 98 | # Sonarlint plugin 99 | .idea/sonarlint 100 | 101 | ### macOS ### 102 | # General 103 | .DS_Store 104 | .AppleDouble 105 | .LSOverride 106 | 107 | # Icon must end with two \r 108 | Icon 109 | 110 | 111 | # Thumbnails 112 | ._* 113 | 114 | # Files that might appear in the root of a volume 115 | .DocumentRevisions-V100 116 | .fseventsd 117 | .Spotlight-V100 118 | .TemporaryItems 119 | .Trashes 120 | .VolumeIcon.icns 121 | .com.apple.timemachine.donotpresent 122 | 123 | # Directories potentially created on remote AFP share 124 | .AppleDB 125 | .AppleDesktop 126 | Network Trash Folder 127 | Temporary Items 128 | .apdisk 129 | 130 | ### Node ### 131 | # Logs 132 | logs 133 | *.log 134 | npm-debug.log* 135 | yarn-debug.log* 136 | yarn-error.log* 137 | lerna-debug.log* 138 | 139 | # Diagnostic reports (https://nodejs.org/api/report.html) 140 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 141 | 142 | # Runtime data 143 | pids 144 | *.pid 145 | *.seed 146 | *.pid.lock 147 | 148 | # Directory for instrumented libs generated by jscoverage/JSCover 149 | lib-cov 150 | 151 | # Coverage directory used by tools like istanbul 152 | coverage 153 | *.lcov 154 | 155 | # nyc test coverage 156 | .nyc_output 157 | 158 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 159 | .grunt 160 | 161 | # Bower dependency directory (https://bower.io/) 162 | bower_components 163 | 164 | # node-waf configuration 165 | .lock-wscript 166 | 167 | # Compiled binary addons (https://nodejs.org/api/addons.html) 168 | build/Release 169 | 170 | # Dependency directories 171 | node_modules/ 172 | jspm_packages/ 173 | 174 | # TypeScript v1 declaration files 175 | typings/ 176 | 177 | # TypeScript cache 178 | *.tsbuildinfo 179 | 180 | # Optional npm cache directory 181 | .npm 182 | 183 | # Optional eslint cache 184 | .eslintcache 185 | 186 | # Optional stylelint cache 187 | .stylelintcache 188 | 189 | # Microbundle cache 190 | .rpt2_cache/ 191 | .rts2_cache_cjs/ 192 | .rts2_cache_es/ 193 | .rts2_cache_umd/ 194 | 195 | # Optional REPL history 196 | .node_repl_history 197 | 198 | # Output of 'npm pack' 199 | *.tgz 200 | 201 | # Yarn Integrity file 202 | .yarn-integrity 203 | 204 | # dotenv environment variables file 205 | .env 206 | .env.test 207 | .env*.local 208 | 209 | # parcel-bundler cache (https://parceljs.org/) 210 | .cache 211 | .parcel-cache 212 | 213 | # Next.js build output 214 | .next 215 | 216 | # Nuxt.js build / generate output 217 | .nuxt 218 | dist 219 | 220 | # Storybook build outputs 221 | .out 222 | .storybook-out 223 | storybook-static 224 | 225 | # rollup.js default build output 226 | dist/ 227 | 228 | # Gatsby files 229 | .cache/ 230 | # Comment in the public line in if your project uses Gatsby and not Next.js 231 | # https://nextjs.org/blog/next-9-1#public-directory-support 232 | # public 233 | 234 | # vuepress build output 235 | .vuepress/dist 236 | 237 | # Serverless directories 238 | .serverless/ 239 | 240 | # FuseBox cache 241 | .fusebox/ 242 | 243 | # DynamoDB Local files 244 | .dynamodb/ 245 | 246 | # TernJS port file 247 | .tern-port 248 | 249 | # Stores VSCode versions used for testing VSCode extensions 250 | .vscode-test 251 | 252 | # Temporary folders 253 | tmp/ 254 | temp/ 255 | 256 | ### Solidity ### 257 | # Logs 258 | 259 | # Diagnostic reports (https://nodejs.org/api/report.html) 260 | 261 | # Runtime data 262 | 263 | # Directory for instrumented libs generated by jscoverage/JSCover 264 | 265 | # Coverage directory used by tools like istanbul 266 | 267 | # nyc test coverage 268 | 269 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 270 | 271 | # Bower dependency directory (https://bower.io/) 272 | 273 | # node-waf configuration 274 | 275 | # Compiled binary addons (https://nodejs.org/api/addons.html) 276 | 277 | # Dependency directories 278 | 279 | # TypeScript v1 declaration files 280 | 281 | # TypeScript cache 282 | 283 | # Optional npm cache directory 284 | 285 | # Optional eslint cache 286 | 287 | # Optional stylelint cache 288 | 289 | # Microbundle cache 290 | 291 | # Optional REPL history 292 | 293 | # Output of 'npm pack' 294 | 295 | # Yarn Integrity file 296 | 297 | # dotenv environment variables file 298 | 299 | # parcel-bundler cache (https://parceljs.org/) 300 | 301 | # Next.js build output 302 | 303 | # Nuxt.js build / generate output 304 | 305 | # Storybook build outputs 306 | 307 | # rollup.js default build output 308 | 309 | # Gatsby files 310 | # Comment in the public line in if your project uses Gatsby and not Next.js 311 | # https://nextjs.org/blog/next-9-1#public-directory-support 312 | # public 313 | 314 | # vuepress build output 315 | 316 | # Serverless directories 317 | 318 | # FuseBox cache 319 | 320 | # DynamoDB Local files 321 | 322 | # TernJS port file 323 | 324 | # Stores VSCode versions used for testing VSCode extensions 325 | 326 | # Temporary folders 327 | 328 | ### SolidityTruffle ### 329 | # depedencies 330 | node_modules 331 | 332 | # testing 333 | 334 | # production 335 | build 336 | build_webpack 337 | 338 | # misc 339 | npm-debug.log 340 | .truffle-solidity-loader 341 | .vagrant/** 342 | blockchain/geth/** 343 | blockchain/keystore/** 344 | blockchain/history 345 | 346 | #truffle 347 | yarn.lock 348 | package-lock.json 349 | 350 | ### SublimeText ### 351 | # Cache files for Sublime Text 352 | *.tmlanguage.cache 353 | *.tmPreferences.cache 354 | *.stTheme.cache 355 | 356 | # Workspace files are user-specific 357 | *.sublime-workspace 358 | 359 | # Project files should be checked into the repository, unless a significant 360 | # proportion of contributors will probably not be using Sublime Text 361 | # *.sublime-project 362 | 363 | # SFTP configuration file 364 | sftp-config.json 365 | 366 | # Package control specific files 367 | Package Control.last-run 368 | Package Control.ca-list 369 | Package Control.ca-bundle 370 | Package Control.system-ca-bundle 371 | Package Control.cache/ 372 | Package Control.ca-certs/ 373 | Package Control.merged-ca-bundle 374 | Package Control.user-ca-bundle 375 | oscrypto-ca-bundle.crt 376 | bh_unicode_properties.cache 377 | 378 | # Sublime-github package stores a github token in this file 379 | # https://packagecontrol.io/packages/sublime-github 380 | GitHub.sublime-settings 381 | 382 | ### vscode ### 383 | .vscode/* 384 | !.vscode/settings.json 385 | !.vscode/tasks.json 386 | !.vscode/launch.json 387 | !.vscode/extensions.json 388 | *.code-workspace 389 | 390 | ### Zsh ### 391 | # Zsh compiled script + zrecompile backup 392 | *.zwc 393 | *.zwc.old 394 | 395 | # Zsh completion-optimization dumpfile 396 | *zcompdump* 397 | 398 | # Zsh zcalc history 399 | .zcalc_history 400 | 401 | # A popular plugin manager's files 402 | ._zinit 403 | .zinit_lstupd 404 | 405 | # zdharma/zshelldoc tool's files 406 | zsdoc/data 407 | 408 | # robbyrussell/oh-my-zsh/plugins/per-directory-history plugin's files 409 | # (when set-up to store the history in the local directory) 410 | .directory_history 411 | 412 | # MichaelAquilina/zsh-autoswitch-virtualenv plugin's files 413 | # (for Zsh plugins using Python) 414 | .venv 415 | 416 | # Zunit tests' output 417 | /tests/_output/* 418 | !/tests/_output/.gitkeep 419 | 420 | # End of https://www.toptal.com/developers/gitignore/api/zsh,node,macos,intellij+all,solidity,soliditytruffle,vscode,sublimetext 421 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["error", "0.8"], 5 | "func-visibility": ["warn", { "ignoreConstructors": true }], 6 | "modifier-name-mixedcase": "warn", 7 | "func-param-name-mixedcase": "warn", 8 | "constructor-syntax": "warn", 9 | "code-complexity": "warn" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 DeFi.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AaveLoop 2 | 3 | ## What is this 4 | 5 | A self-deployed DeFi strategy for Leveraged Reborrowing (borrow-supply loop) of a single supported asset (such as USDC) on [Aave](https://aave.com/) to maximize yield. 6 | 7 | Cross-asset loops are not (yet?) supported. 8 | 9 | All funds are fully controlled by the contract owner, as provided to the constructor at deployment. 10 | All values are immutable, contract is only accesible to owner, with the exception of `claimRewardsToOwner` which can be called by anyone. 11 | 12 | Supported networks: 13 | 14 | - Ethereum 15 | - Avalanche 16 | - Polygon 17 | 18 | > Use at your own risk 19 | 20 | ## E2E Tests 21 | 22 | Run test on a mainnet fork with Hardhat: 23 | 24 | ``` 25 | npm install 26 | npm run build 27 | npm run test 28 | npm run test:avax 29 | npm run test:poly 30 | ``` 31 | 32 | ## Contract deployment 33 | 34 | 1. Clone and initialize the repo: 35 | - `git clone` 36 | - create a .config.json file in root with keys as shown below 37 | - `npm install` 38 | - `npm run build` 39 | 1. Enter your infura/alchemy endpoint in `hardhat.config.ts` under `networks.eth.url` 40 | 1. Create a new temporary address with a [mnemonic generator](https://iancoleman.io/bip39/) and import the private key to metamask 41 | 1. Send enough ETH for deployment to the temp address 42 | 1. `npm run deploy eth` and follow the prompts 43 | 1. The deploy script will take care of everything, after deployment send any leftover funds back and burn the temp private keys 44 | 1. A backup is created under `./deployments` just in case 45 | 1. The contract is ready to be used by the owner 46 | 1. To add custom abi to etherscan, use the ABI in `deployments/*/artifacts/contracts/AaveLoop.sol/AaveLoop.json` 47 | 48 | ### expected .config.json 49 | ```json 50 | { 51 | "NODE_URL_ETH": "", 52 | "NODE_URL_POLY": "", 53 | "NODE_URL_AVAX": "", 54 | "coinmarketcapKey": "", 55 | "ETHERSCAN_ETH": "", 56 | "ETHERSCAN_POLY": "", 57 | "ETHERSCAN_AVAX": "" 58 | } 59 | ``` 60 | 61 | 62 | ## Management roles 63 | 64 | - _Owner_ owns the contract and the funds inside. Can enter, exit and withdraw. 65 | 66 | ## Sending management transactions 67 | 68 | It's recommended to take the ABI created during deployment and upload it as private [custom ABI](https://info.etherscan.com/custom-abi/) to Etherscan and this way we can easily use Etherscan's read/write interface without publishing the contract source. (pass false at contract deployment to skip source upload). 69 | 70 | ## Monitoring against liquidations 71 | 72 | - Call `getPositionData` or `getLiquidity` to monitor liquidity decrease over time. 73 | - If `healthFactor < 1e18` or `liquidity = 0` the position will be liquidated! it is recommended not to go below `0.5%-1%` of the principal. 74 | 75 | ## Emergencies 76 | 77 | If `exitPosition` fails due to hittin gas limit, exit can be done manually: 78 | 79 | 1. Using a lower number in iterations. Partial exits are supported, will de-leverage but stay in position 80 | 2. Using multiple manual rollback transactions 81 | 3. The owner of the contract can also execute an arbitrary transaction using `emergencyFunctionCall` or `emergencyFunctionDelegateCall`. Check the tests. 82 | 4. By sending more of the asset to the contract before running `exitPosition` again, this will reduce the number of exit iterations 83 | -------------------------------------------------------------------------------- /contracts/AaveLoop.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | import "@openzeppelin/contracts/utils/Address.sol"; 8 | import "./IAaveInterfaces.sol"; 9 | import "./ImmutableOwnable.sol"; 10 | 11 | /** 12 | * Single asset leveraged reborrowing strategy on AAVE, chain agnostic. 13 | * Position managed by this contract, with full ownership and control by Owner. 14 | * Monitor position health to avoid liquidation. 15 | */ 16 | contract AaveLoop is ImmutableOwnable { 17 | using SafeERC20 for ERC20; 18 | 19 | uint256 public constant USE_VARIABLE_DEBT = 2; 20 | uint256 public constant SAFE_BUFFER = 10; // wei 21 | 22 | ERC20 public immutable ASSET; // solhint-disable-line 23 | ILendingPool public immutable LENDING_POOL; // solhint-disable-line 24 | IAaveIncentivesController public immutable INCENTIVES; // solhint-disable-line 25 | 26 | /** 27 | * @param owner The contract owner, has complete ownership, immutable 28 | * @param asset The target underlying asset ex. USDC 29 | * @param lendingPool The deployed AAVE ILendingPool 30 | * @param incentives The deployed AAVE IAaveIncentivesController 31 | */ 32 | constructor( 33 | address owner, 34 | address asset, 35 | address lendingPool, 36 | address incentives 37 | ) ImmutableOwnable(owner) { 38 | require(asset != address(0) && lendingPool != address(0) && incentives != address(0), "address 0"); 39 | 40 | ASSET = ERC20(asset); 41 | LENDING_POOL = ILendingPool(lendingPool); 42 | INCENTIVES = IAaveIncentivesController(incentives); 43 | } 44 | 45 | // ---- views ---- 46 | 47 | function getSupplyAndBorrowAssets() public view returns (address[] memory assets) { 48 | DataTypes.ReserveData memory data = LENDING_POOL.getReserveData(address(ASSET)); 49 | assets = new address[](2); 50 | assets[0] = data.aTokenAddress; 51 | assets[1] = data.variableDebtTokenAddress; 52 | } 53 | 54 | /** 55 | * @return The ASSET price in ETH according to Aave PriceOracle, used internally for all ASSET amounts calculations 56 | */ 57 | function getAssetPrice() public view returns (uint256) { 58 | return IAavePriceOracle(LENDING_POOL.getAddressesProvider().getPriceOracle()).getAssetPrice(address(ASSET)); 59 | } 60 | 61 | /** 62 | * @return total supply balance in ASSET 63 | */ 64 | function getSupplyBalance() public view returns (uint256) { 65 | (uint256 totalCollateralETH, , , , , ) = getPositionData(); 66 | return (totalCollateralETH * (10**ASSET.decimals())) / getAssetPrice(); 67 | } 68 | 69 | /** 70 | * @return total borrow balance in ASSET 71 | */ 72 | function getBorrowBalance() public view returns (uint256) { 73 | (, uint256 totalDebtETH, , , , ) = getPositionData(); 74 | return (totalDebtETH * (10**ASSET.decimals())) / getAssetPrice(); 75 | } 76 | 77 | /** 78 | * @return available liquidity in ASSET 79 | */ 80 | function getLiquidity() public view returns (uint256) { 81 | (, , uint256 availableBorrowsETH, , , ) = getPositionData(); 82 | return (availableBorrowsETH * (10**ASSET.decimals())) / getAssetPrice(); 83 | } 84 | 85 | /** 86 | * @return ASSET balanceOf(this) 87 | */ 88 | function getAssetBalance() public view returns (uint256) { 89 | return ASSET.balanceOf(address(this)); 90 | } 91 | 92 | /** 93 | * @return Pending rewards 94 | */ 95 | function getPendingRewards() public view returns (uint256) { 96 | return INCENTIVES.getRewardsBalance(getSupplyAndBorrowAssets(), address(this)); 97 | } 98 | 99 | /** 100 | * Position data from Aave 101 | */ 102 | function getPositionData() 103 | public 104 | view 105 | returns ( 106 | uint256 totalCollateralETH, 107 | uint256 totalDebtETH, 108 | uint256 availableBorrowsETH, 109 | uint256 currentLiquidationThreshold, 110 | uint256 ltv, 111 | uint256 healthFactor 112 | ) 113 | { 114 | return LENDING_POOL.getUserAccountData(address(this)); 115 | } 116 | 117 | /** 118 | * @return LTV of ASSET in 4 decimals ex. 82.5% == 8250 119 | */ 120 | function getLTV() public view returns (uint256) { 121 | DataTypes.ReserveConfigurationMap memory config = LENDING_POOL.getConfiguration(address(ASSET)); 122 | return config.data & 0xffff; // bits 0-15 in BE 123 | } 124 | 125 | // ---- unrestricted ---- 126 | 127 | /** 128 | * Claims and transfers all pending rewards to OWNER 129 | */ 130 | function claimRewardsToOwner() external { 131 | INCENTIVES.claimRewards(getSupplyAndBorrowAssets(), type(uint256).max, OWNER); 132 | } 133 | 134 | // ---- main ---- 135 | 136 | /** 137 | * @param iterations - Loop count 138 | * @return Liquidity at end of the loop 139 | */ 140 | function enterPositionFully(uint256 iterations) external onlyOwner returns (uint256) { 141 | return enterPosition(ASSET.balanceOf(msg.sender), iterations); 142 | } 143 | 144 | /** 145 | * @param principal - ASSET transferFrom sender amount, can be 0 146 | * @param iterations - Loop count 147 | * @return Liquidity at end of the loop 148 | */ 149 | function enterPosition(uint256 principal, uint256 iterations) public onlyOwner returns (uint256) { 150 | if (principal > 0) { 151 | ASSET.safeTransferFrom(msg.sender, address(this), principal); 152 | } 153 | 154 | if (getAssetBalance() > 0) { 155 | _supply(getAssetBalance()); 156 | } 157 | 158 | for (uint256 i = 0; i < iterations; i++) { 159 | _borrow(getLiquidity() - SAFE_BUFFER); 160 | _supply(getAssetBalance()); 161 | } 162 | 163 | return getLiquidity(); 164 | } 165 | 166 | /** 167 | * @param iterations - MAX loop count 168 | * @return Withdrawn amount of ASSET to OWNER 169 | */ 170 | function exitPosition(uint256 iterations) external onlyOwner returns (uint256) { 171 | (, , , , uint256 ltv, ) = getPositionData(); // 4 decimals 172 | 173 | for (uint256 i = 0; i < iterations && getBorrowBalance() > 0; i++) { 174 | _redeemSupply(((getLiquidity() * 1e4) / ltv) - SAFE_BUFFER); 175 | _repayBorrow(getAssetBalance()); 176 | } 177 | 178 | if (getBorrowBalance() == 0) { 179 | _redeemSupply(type(uint256).max); 180 | } 181 | 182 | return _withdrawToOwner(address(ASSET)); 183 | } 184 | 185 | // ---- internals, public onlyOwner in case of emergency ---- 186 | 187 | /** 188 | * amount in ASSET 189 | */ 190 | function _supply(uint256 amount) public onlyOwner { 191 | ASSET.safeIncreaseAllowance(address(LENDING_POOL), amount); 192 | LENDING_POOL.deposit(address(ASSET), amount, address(this), 0); 193 | } 194 | 195 | /** 196 | * amount in ASSET 197 | */ 198 | function _borrow(uint256 amount) public onlyOwner { 199 | LENDING_POOL.borrow(address(ASSET), amount, USE_VARIABLE_DEBT, 0, address(this)); 200 | } 201 | 202 | /** 203 | * amount in ASSET 204 | */ 205 | function _redeemSupply(uint256 amount) public onlyOwner { 206 | LENDING_POOL.withdraw(address(ASSET), amount, address(this)); 207 | } 208 | 209 | /** 210 | * amount in ASSET 211 | */ 212 | function _repayBorrow(uint256 amount) public onlyOwner { 213 | ASSET.safeIncreaseAllowance(address(LENDING_POOL), amount); 214 | LENDING_POOL.repay(address(ASSET), amount, USE_VARIABLE_DEBT, address(this)); 215 | } 216 | 217 | function _withdrawToOwner(address asset) public onlyOwner returns (uint256) { 218 | uint256 balance = ERC20(asset).balanceOf(address(this)); 219 | ERC20(asset).safeTransfer(OWNER, balance); 220 | return balance; 221 | } 222 | 223 | // ---- emergency ---- 224 | 225 | function emergencyFunctionCall(address target, bytes memory data) external onlyOwner { 226 | Address.functionCall(target, data); 227 | } 228 | 229 | function emergencyFunctionDelegateCall(address target, bytes memory data) external onlyOwner { 230 | Address.functionDelegateCall(target, data); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /contracts/IAaveInterfaces.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // solhint-disable 3 | pragma solidity 0.8.6; 4 | 5 | interface ILendingPool { 6 | /** 7 | * @dev Emitted on deposit() 8 | * @param reserve The address of the underlying asset of the reserve 9 | * @param user The address initiating the deposit 10 | * @param onBehalfOf The beneficiary of the deposit, receiving the aTokens 11 | * @param amount The amount deposited 12 | * @param referral The referral code used 13 | **/ 14 | event Deposit(address indexed reserve, address user, address indexed onBehalfOf, uint256 amount, uint16 indexed referral); 15 | 16 | /** 17 | * @dev Emitted on withdraw() 18 | * @param reserve The address of the underlyng asset being withdrawn 19 | * @param user The address initiating the withdrawal, owner of aTokens 20 | * @param to Address that will receive the underlying 21 | * @param amount The amount to be withdrawn 22 | **/ 23 | event Withdraw(address indexed reserve, address indexed user, address indexed to, uint256 amount); 24 | 25 | /** 26 | * @dev Emitted on borrow() and flashLoan() when debt needs to be opened 27 | * @param reserve The address of the underlying asset being borrowed 28 | * @param user The address of the user initiating the borrow(), receiving the funds on borrow() or just 29 | * initiator of the transaction on flashLoan() 30 | * @param onBehalfOf The address that will be getting the debt 31 | * @param amount The amount borrowed out 32 | * @param borrowRateMode The rate mode: 1 for Stable, 2 for Variable 33 | * @param borrowRate The numeric rate at which the user has borrowed 34 | * @param referral The referral code used 35 | **/ 36 | event Borrow(address indexed reserve, address user, address indexed onBehalfOf, uint256 amount, uint256 borrowRateMode, uint256 borrowRate, uint16 indexed referral); 37 | 38 | /** 39 | * @dev Emitted on repay() 40 | * @param reserve The address of the underlying asset of the reserve 41 | * @param user The beneficiary of the repayment, getting his debt reduced 42 | * @param repayer The address of the user initiating the repay(), providing the funds 43 | * @param amount The amount repaid 44 | **/ 45 | event Repay(address indexed reserve, address indexed user, address indexed repayer, uint256 amount); 46 | 47 | /** 48 | * @dev Emitted on swapBorrowRateMode() 49 | * @param reserve The address of the underlying asset of the reserve 50 | * @param user The address of the user swapping his rate mode 51 | * @param rateMode The rate mode that the user wants to swap to 52 | **/ 53 | event Swap(address indexed reserve, address indexed user, uint256 rateMode); 54 | 55 | /** 56 | * @dev Emitted on setUserUseReserveAsCollateral() 57 | * @param reserve The address of the underlying asset of the reserve 58 | * @param user The address of the user enabling the usage as collateral 59 | **/ 60 | event ReserveUsedAsCollateralEnabled(address indexed reserve, address indexed user); 61 | 62 | /** 63 | * @dev Emitted on setUserUseReserveAsCollateral() 64 | * @param reserve The address of the underlying asset of the reserve 65 | * @param user The address of the user enabling the usage as collateral 66 | **/ 67 | event ReserveUsedAsCollateralDisabled(address indexed reserve, address indexed user); 68 | 69 | /** 70 | * @dev Emitted on rebalanceStableBorrowRate() 71 | * @param reserve The address of the underlying asset of the reserve 72 | * @param user The address of the user for which the rebalance has been executed 73 | **/ 74 | event RebalanceStableBorrowRate(address indexed reserve, address indexed user); 75 | 76 | /** 77 | * @dev Emitted on flashLoan() 78 | * @param target The address of the flash loan receiver contract 79 | * @param initiator The address initiating the flash loan 80 | * @param asset The address of the asset being flash borrowed 81 | * @param amount The amount flash borrowed 82 | * @param premium The fee flash borrowed 83 | * @param referralCode The referral code used 84 | **/ 85 | event FlashLoan(address indexed target, address indexed initiator, address indexed asset, uint256 amount, uint256 premium, uint16 referralCode); 86 | 87 | /** 88 | * @dev Emitted when the pause is triggered. 89 | */ 90 | event Paused(); 91 | 92 | /** 93 | * @dev Emitted when the pause is lifted. 94 | */ 95 | event Unpaused(); 96 | 97 | /** 98 | * @dev Emitted when a borrower is liquidated. This event is emitted by the LendingPool via 99 | * LendingPoolCollateral manager using a DELEGATECALL 100 | * This allows to have the events in the generated ABI for LendingPool. 101 | * @param collateralAsset The address of the underlying asset used as collateral, to receive as result of the liquidation 102 | * @param debtAsset The address of the underlying borrowed asset to be repaid with the liquidation 103 | * @param user The address of the borrower getting liquidated 104 | * @param debtToCover The debt amount of borrowed `asset` the liquidator wants to cover 105 | * @param liquidatedCollateralAmount The amount of collateral received by the liiquidator 106 | * @param liquidator The address of the liquidator 107 | * @param receiveAToken `true` if the liquidators wants to receive the collateral aTokens, `false` if he wants 108 | * to receive the underlying collateral asset directly 109 | **/ 110 | event LiquidationCall( 111 | address indexed collateralAsset, 112 | address indexed debtAsset, 113 | address indexed user, 114 | uint256 debtToCover, 115 | uint256 liquidatedCollateralAmount, 116 | address liquidator, 117 | bool receiveAToken 118 | ); 119 | 120 | /** 121 | * @dev Emitted when the state of a reserve is updated. NOTE: This event is actually declared 122 | * in the ReserveLogic library and emitted in the updateInterestRates() function. Since the function is internal, 123 | * the event will actually be fired by the LendingPool contract. The event is therefore replicated here so it 124 | * gets added to the LendingPool ABI 125 | * @param reserve The address of the underlying asset of the reserve 126 | * @param liquidityRate The new liquidity rate 127 | * @param stableBorrowRate The new stable borrow rate 128 | * @param variableBorrowRate The new variable borrow rate 129 | * @param liquidityIndex The new liquidity index 130 | * @param variableBorrowIndex The new variable borrow index 131 | **/ 132 | event ReserveDataUpdated( 133 | address indexed reserve, 134 | uint256 liquidityRate, 135 | uint256 stableBorrowRate, 136 | uint256 variableBorrowRate, 137 | uint256 liquidityIndex, 138 | uint256 variableBorrowIndex 139 | ); 140 | 141 | /** 142 | * @dev Deposits an `amount` of underlying asset into the reserve, receiving in return overlying aTokens. 143 | * - E.g. User deposits 100 USDC and gets in return 100 aUSDC 144 | * @param asset The address of the underlying asset to deposit 145 | * @param amount The amount to be deposited 146 | * @param onBehalfOf The address that will receive the aTokens, same as msg.sender if the user 147 | * wants to receive them on his own wallet, or a different address if the beneficiary of aTokens 148 | * is a different wallet 149 | * @param referralCode Code used to register the integrator originating the operation, for potential rewards. 150 | * 0 if the action is executed directly by the user, without any middle-man 151 | **/ 152 | function deposit( 153 | address asset, 154 | uint256 amount, 155 | address onBehalfOf, 156 | uint16 referralCode 157 | ) external; 158 | 159 | /** 160 | * @dev Withdraws an `amount` of underlying asset from the reserve, burning the equivalent aTokens owned 161 | * E.g. User has 100 aUSDC, calls withdraw() and receives 100 USDC, burning the 100 aUSDC 162 | * @param asset The address of the underlying asset to withdraw 163 | * @param amount The underlying amount to be withdrawn 164 | * - Send the value type(uint256).max in order to withdraw the whole aToken balance 165 | * @param to Address that will receive the underlying, same as msg.sender if the user 166 | * wants to receive it on his own wallet, or a different address if the beneficiary is a 167 | * different wallet 168 | * @return The final amount withdrawn 169 | **/ 170 | function withdraw( 171 | address asset, 172 | uint256 amount, 173 | address to 174 | ) external returns (uint256); 175 | 176 | /** 177 | * @dev Allows users to borrow a specific `amount` of the reserve underlying asset, provided that the borrower 178 | * already deposited enough collateral, or he was given enough allowance by a credit delegator on the 179 | * corresponding debt token (StableDebtToken or VariableDebtToken) 180 | * - E.g. User borrows 100 USDC passing as `onBehalfOf` his own address, receiving the 100 USDC in his wallet 181 | * and 100 stable/variable debt tokens, depending on the `interestRateMode` 182 | * @param asset The address of the underlying asset to borrow 183 | * @param amount The amount to be borrowed 184 | * @param interestRateMode The interest rate mode at which the user wants to borrow: 1 for Stable, 2 for Variable 185 | * @param referralCode Code used to register the integrator originating the operation, for potential rewards. 186 | * 0 if the action is executed directly by the user, without any middle-man 187 | * @param onBehalfOf Address of the user who will receive the debt. Should be the address of the borrower itself 188 | * calling the function if he wants to borrow against his own collateral, or the address of the credit delegator 189 | * if he has been given credit delegation allowance 190 | **/ 191 | function borrow( 192 | address asset, 193 | uint256 amount, 194 | uint256 interestRateMode, 195 | uint16 referralCode, 196 | address onBehalfOf 197 | ) external; 198 | 199 | /** 200 | * @notice Repays a borrowed `amount` on a specific reserve, burning the equivalent debt tokens owned 201 | * - E.g. User repays 100 USDC, burning 100 variable/stable debt tokens of the `onBehalfOf` address 202 | * @param asset The address of the borrowed underlying asset previously borrowed 203 | * @param amount The amount to repay 204 | * - Send the value type(uint256).max in order to repay the whole debt for `asset` on the specific `debtMode` 205 | * @param rateMode The interest rate mode at of the debt the user wants to repay: 1 for Stable, 2 for Variable 206 | * @param onBehalfOf Address of the user who will get his debt reduced/removed. Should be the address of the 207 | * user calling the function if he wants to reduce/remove his own debt, or the address of any other 208 | * other borrower whose debt should be removed 209 | * @return The final amount repaid 210 | **/ 211 | function repay( 212 | address asset, 213 | uint256 amount, 214 | uint256 rateMode, 215 | address onBehalfOf 216 | ) external returns (uint256); 217 | 218 | /** 219 | * @dev Allows a borrower to swap his debt between stable and variable mode, or viceversa 220 | * @param asset The address of the underlying asset borrowed 221 | * @param rateMode The rate mode that the user wants to swap to 222 | **/ 223 | function swapBorrowRateMode(address asset, uint256 rateMode) external; 224 | 225 | /** 226 | * @dev Rebalances the stable interest rate of a user to the current stable rate defined on the reserve. 227 | * - Users can be rebalanced if the following conditions are satisfied: 228 | * 1. Usage ratio is above 95% 229 | * 2. the current deposit APY is below REBALANCE_UP_THRESHOLD * maxVariableBorrowRate, which means that too much has been 230 | * borrowed at a stable rate and depositors are not earning enough 231 | * @param asset The address of the underlying asset borrowed 232 | * @param user The address of the user to be rebalanced 233 | **/ 234 | function rebalanceStableBorrowRate(address asset, address user) external; 235 | 236 | /** 237 | * @dev Allows depositors to enable/disable a specific deposited asset as collateral 238 | * @param asset The address of the underlying asset deposited 239 | * @param useAsCollateral `true` if the user wants to use the deposit as collateral, `false` otherwise 240 | **/ 241 | function setUserUseReserveAsCollateral(address asset, bool useAsCollateral) external; 242 | 243 | /** 244 | * @dev Function to liquidate a non-healthy position collateral-wise, with Health Factor below 1 245 | * - The caller (liquidator) covers `debtToCover` amount of debt of the user getting liquidated, and receives 246 | * a proportionally amount of the `collateralAsset` plus a bonus to cover market risk 247 | * @param collateralAsset The address of the underlying asset used as collateral, to receive as result of the liquidation 248 | * @param debtAsset The address of the underlying borrowed asset to be repaid with the liquidation 249 | * @param user The address of the borrower getting liquidated 250 | * @param debtToCover The debt amount of borrowed `asset` the liquidator wants to cover 251 | * @param receiveAToken `true` if the liquidators wants to receive the collateral aTokens, `false` if he wants 252 | * to receive the underlying collateral asset directly 253 | **/ 254 | function liquidationCall( 255 | address collateralAsset, 256 | address debtAsset, 257 | address user, 258 | uint256 debtToCover, 259 | bool receiveAToken 260 | ) external; 261 | 262 | /** 263 | * @dev Allows smartcontracts to access the liquidity of the pool within one transaction, 264 | * as long as the amount taken plus a fee is returned. 265 | * IMPORTANT There are security concerns for developers of flashloan receiver contracts that must be kept into consideration. 266 | * For further details please visit https://developers.aave.com 267 | * @param receiverAddress The address of the contract receiving the funds, implementing the IFlashLoanReceiver interface 268 | * @param assets The addresses of the assets being flash-borrowed 269 | * @param amounts The amounts amounts being flash-borrowed 270 | * @param modes Types of the debt to open if the flash loan is not returned: 271 | * 0 -> Don't open any debt, just revert if funds can't be transferred from the receiver 272 | * 1 -> Open debt at stable rate for the value of the amount flash-borrowed to the `onBehalfOf` address 273 | * 2 -> Open debt at variable rate for the value of the amount flash-borrowed to the `onBehalfOf` address 274 | * @param onBehalfOf The address that will receive the debt in the case of using on `modes` 1 or 2 275 | * @param params Variadic packed params to pass to the receiver as extra information 276 | * @param referralCode Code used to register the integrator originating the operation, for potential rewards. 277 | * 0 if the action is executed directly by the user, without any middle-man 278 | **/ 279 | function flashLoan( 280 | address receiverAddress, 281 | address[] calldata assets, 282 | uint256[] calldata amounts, 283 | uint256[] calldata modes, 284 | address onBehalfOf, 285 | bytes calldata params, 286 | uint16 referralCode 287 | ) external; 288 | 289 | /** 290 | * @dev Returns the user account data across all the reserves 291 | * @param user The address of the user 292 | * @return totalCollateralETH the total collateral in ETH of the user 293 | * @return totalDebtETH the total debt in ETH of the user 294 | * @return availableBorrowsETH the borrowing power left of the user 295 | * @return currentLiquidationThreshold the liquidation threshold of the user 296 | * @return ltv the loan to value of the user 297 | * @return healthFactor the current health factor of the user 298 | **/ 299 | function getUserAccountData(address user) 300 | external 301 | view 302 | returns ( 303 | uint256 totalCollateralETH, 304 | uint256 totalDebtETH, 305 | uint256 availableBorrowsETH, 306 | uint256 currentLiquidationThreshold, 307 | uint256 ltv, 308 | uint256 healthFactor 309 | ); 310 | 311 | function initReserve( 312 | address reserve, 313 | address aTokenAddress, 314 | address stableDebtAddress, 315 | address variableDebtAddress, 316 | address interestRateStrategyAddress 317 | ) external; 318 | 319 | function setReserveInterestRateStrategyAddress(address reserve, address rateStrategyAddress) external; 320 | 321 | function setConfiguration(address reserve, uint256 configuration) external; 322 | 323 | /** 324 | * @dev Returns the configuration of the reserve 325 | * @param asset The address of the underlying asset of the reserve 326 | * @return The configuration of the reserve 327 | **/ 328 | function getConfiguration(address asset) external view returns (DataTypes.ReserveConfigurationMap memory); 329 | 330 | /** 331 | * @dev Returns the configuration of the user across all the reserves 332 | * @param user The user address 333 | * @return The configuration of the user 334 | **/ 335 | function getUserConfiguration(address user) external view returns (DataTypes.UserConfigurationMap memory); 336 | 337 | /** 338 | * @dev Returns the normalized income normalized income of the reserve 339 | * @param asset The address of the underlying asset of the reserve 340 | * @return The reserve's normalized income 341 | */ 342 | function getReserveNormalizedIncome(address asset) external view returns (uint256); 343 | 344 | /** 345 | * @dev Returns the normalized variable debt per unit of asset 346 | * @param asset The address of the underlying asset of the reserve 347 | * @return The reserve normalized variable debt 348 | */ 349 | function getReserveNormalizedVariableDebt(address asset) external view returns (uint256); 350 | 351 | /** 352 | * @dev Returns the state and configuration of the reserve 353 | * @param asset The address of the underlying asset of the reserve 354 | * @return The state of the reserve 355 | **/ 356 | function getReserveData(address asset) external view returns (DataTypes.ReserveData memory); 357 | 358 | function finalizeTransfer( 359 | address asset, 360 | address from, 361 | address to, 362 | uint256 amount, 363 | uint256 balanceFromAfter, 364 | uint256 balanceToBefore 365 | ) external; 366 | 367 | function getReservesList() external view returns (address[] memory); 368 | 369 | function getAddressesProvider() external view returns (ILendingPoolAddressesProvider); 370 | 371 | function setPause(bool val) external; 372 | 373 | function paused() external view returns (bool); 374 | } 375 | 376 | /** 377 | * @title LendingPoolAddressesProvider contract 378 | * @dev Main registry of addresses part of or connected to the protocol, including permissioned roles 379 | * - Acting also as factory of proxies and admin of those, so with right to change its implementations 380 | * - Owned by the Aave Governance 381 | * @author Aave 382 | **/ 383 | interface ILendingPoolAddressesProvider { 384 | event MarketIdSet(string newMarketId); 385 | event LendingPoolUpdated(address indexed newAddress); 386 | event ConfigurationAdminUpdated(address indexed newAddress); 387 | event EmergencyAdminUpdated(address indexed newAddress); 388 | event LendingPoolConfiguratorUpdated(address indexed newAddress); 389 | event LendingPoolCollateralManagerUpdated(address indexed newAddress); 390 | event PriceOracleUpdated(address indexed newAddress); 391 | event LendingRateOracleUpdated(address indexed newAddress); 392 | event ProxyCreated(bytes32 id, address indexed newAddress); 393 | event AddressSet(bytes32 id, address indexed newAddress, bool hasProxy); 394 | 395 | function getMarketId() external view returns (string memory); 396 | 397 | function setMarketId(string calldata marketId) external; 398 | 399 | function setAddress(bytes32 id, address newAddress) external; 400 | 401 | function setAddressAsProxy(bytes32 id, address impl) external; 402 | 403 | function getAddress(bytes32 id) external view returns (address); 404 | 405 | function getLendingPool() external view returns (address); 406 | 407 | function setLendingPoolImpl(address pool) external; 408 | 409 | function getLendingPoolConfigurator() external view returns (address); 410 | 411 | function setLendingPoolConfiguratorImpl(address configurator) external; 412 | 413 | function getLendingPoolCollateralManager() external view returns (address); 414 | 415 | function setLendingPoolCollateralManager(address manager) external; 416 | 417 | function getPoolAdmin() external view returns (address); 418 | 419 | function setPoolAdmin(address admin) external; 420 | 421 | function getEmergencyAdmin() external view returns (address); 422 | 423 | function setEmergencyAdmin(address admin) external; 424 | 425 | function getPriceOracle() external view returns (address); 426 | 427 | function setPriceOracle(address priceOracle) external; 428 | 429 | function getLendingRateOracle() external view returns (address); 430 | 431 | function setLendingRateOracle(address lendingRateOracle) external; 432 | } 433 | 434 | library DataTypes { 435 | // refer to the whitepaper, section 1.1 basic concepts for a formal description of these properties. 436 | struct ReserveData { 437 | //stores the reserve configuration 438 | ReserveConfigurationMap configuration; 439 | //the liquidity index. Expressed in ray 440 | uint128 liquidityIndex; 441 | //variable borrow index. Expressed in ray 442 | uint128 variableBorrowIndex; 443 | //the current supply rate. Expressed in ray 444 | uint128 currentLiquidityRate; 445 | //the current variable borrow rate. Expressed in ray 446 | uint128 currentVariableBorrowRate; 447 | //the current stable borrow rate. Expressed in ray 448 | uint128 currentStableBorrowRate; 449 | uint40 lastUpdateTimestamp; 450 | //tokens addresses 451 | address aTokenAddress; 452 | address stableDebtTokenAddress; 453 | address variableDebtTokenAddress; 454 | //address of the interest rate strategy 455 | address interestRateStrategyAddress; 456 | //the id of the reserve. Represents the position in the list of the active reserves 457 | uint8 id; 458 | } 459 | 460 | struct ReserveConfigurationMap { 461 | //bit 0-15: LTV 462 | //bit 16-31: Liq. threshold 463 | //bit 32-47: Liq. bonus 464 | //bit 48-55: Decimals 465 | //bit 56: Reserve is active 466 | //bit 57: reserve is frozen 467 | //bit 58: borrowing is enabled 468 | //bit 59: stable rate borrowing enabled 469 | //bit 60-63: reserved 470 | //bit 64-79: reserve factor 471 | uint256 data; 472 | } 473 | 474 | struct UserConfigurationMap { 475 | uint256 data; 476 | } 477 | 478 | enum InterestRateMode { 479 | NONE, 480 | STABLE, 481 | VARIABLE 482 | } 483 | } 484 | 485 | interface IStakedAave { 486 | function stake(address to, uint256 amount) external; 487 | 488 | function redeem(address to, uint256 amount) external; 489 | 490 | function cooldown() external; 491 | 492 | function claimRewards(address to, uint256 amount) external; 493 | 494 | function getTotalRewardsBalance(address staker) external view returns (uint256); 495 | } 496 | 497 | interface IAaveIncentivesController { 498 | /** 499 | * @dev Whitelists an address to claim the rewards on behalf of another address 500 | * @param user The address of the user 501 | * @param claimer The address of the claimer 502 | */ 503 | function setClaimer(address user, address claimer) external; 504 | 505 | /** 506 | * @dev Returns the whitelisted claimer for a certain address (0x0 if not set) 507 | * @param user The address of the user 508 | * @return The claimer address 509 | */ 510 | function getClaimer(address user) external view returns (address); 511 | 512 | /** 513 | * @dev Configure assets for a certain rewards emission 514 | * @param assets The assets to incentivize 515 | * @param emissionsPerSecond The emission for each asset 516 | */ 517 | function configureAssets(address[] calldata assets, uint256[] calldata emissionsPerSecond) external; 518 | 519 | /** 520 | * @dev Called by the corresponding asset on any update that affects the rewards distribution 521 | * @param asset The address of the user 522 | * @param userBalance The balance of the user of the asset in the lending pool 523 | * @param totalSupply The total supply of the asset in the lending pool 524 | **/ 525 | function handleAction( 526 | address asset, 527 | uint256 userBalance, 528 | uint256 totalSupply 529 | ) external; 530 | 531 | /** 532 | * @dev Returns the total of rewards of an user, already accrued + not yet accrued 533 | * @param user The address of the user 534 | * @return The rewards 535 | **/ 536 | function getRewardsBalance(address[] calldata assets, address user) external view returns (uint256); 537 | 538 | /** 539 | * @dev Claims reward for an user, on all the assets of the lending pool, accumulating the pending rewards 540 | * @param amount Amount of rewards to claim 541 | * @param to Address that will be receiving the rewards 542 | * @return Rewards claimed 543 | **/ 544 | function claimRewards( 545 | address[] calldata assets, 546 | uint256 amount, 547 | address to 548 | ) external returns (uint256); 549 | 550 | /** 551 | * @dev Claims reward for an user on behalf, on all the assets of the lending pool, accumulating the pending rewards. The caller must 552 | * be whitelisted via "allowClaimOnBehalf" function by the RewardsAdmin role manager 553 | * @param amount Amount of rewards to claim 554 | * @param user Address to check and claim rewards 555 | * @param to Address that will be receiving the rewards 556 | * @return Rewards claimed 557 | **/ 558 | function claimRewardsOnBehalf( 559 | address[] calldata assets, 560 | uint256 amount, 561 | address user, 562 | address to 563 | ) external returns (uint256); 564 | 565 | /** 566 | * @dev returns the unclaimed rewards of the user 567 | * @param user the address of the user 568 | * @return the unclaimed user rewards 569 | */ 570 | function getUserUnclaimedRewards(address user) external view returns (uint256); 571 | 572 | /** 573 | * @dev for backward compatibility with previous implementation of the Incentives controller 574 | */ 575 | function REWARD_TOKEN() external view returns (address); 576 | } 577 | 578 | /** 579 | * @title IPriceOracleGetter interface 580 | * @notice Interface for the Aave price oracle. 581 | **/ 582 | interface IAavePriceOracle { 583 | /** 584 | * @dev returns the asset price in ETH 585 | * @param asset the address of the asset 586 | * @return the ETH price of the asset 587 | **/ 588 | function getAssetPrice(address asset) external view returns (uint256); 589 | } 590 | -------------------------------------------------------------------------------- /contracts/ImmutableOwnable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.6; 4 | 5 | /** 6 | * Immutable version of Ownable 7 | */ 8 | abstract contract ImmutableOwnable { 9 | address public immutable OWNER; // solhint-disable-line 10 | 11 | modifier onlyOwner() { 12 | require(msg.sender == OWNER, "onlyOwner"); 13 | _; 14 | } 15 | 16 | constructor(address owner) { 17 | require(owner != address(0), "owner 0"); 18 | OWNER = owner; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/types"; 2 | import "@typechain/hardhat"; 3 | import "hardhat-gas-reporter"; 4 | import "hardhat-tracer"; 5 | import "@nomiclabs/hardhat-web3"; 6 | import "@nomiclabs/hardhat-etherscan"; 7 | import { task } from "hardhat/config"; 8 | import { bn18, networks } from "@defi.org/web3-candies"; 9 | 10 | task("deploy").setAction(async () => { 11 | // initNetworkContracts(); 12 | // const owner = await askAddress("owner address 0x"); 13 | // const gasLimit = 2_000_000; 14 | // console.log(await deploy("AaveLoop", [owner, asset.address, lendingPool.options.address, incentives.options.address], gasLimit, 0, true, 1)); 15 | }); 16 | 17 | const configFile = () => require("./.config.json"); 18 | 19 | export default { 20 | solidity: { 21 | version: "0.8.6", 22 | settings: { 23 | optimizer: { 24 | enabled: true, 25 | runs: 200, 26 | }, 27 | }, 28 | }, 29 | defaultNetwork: "hardhat", 30 | networks: { 31 | hardhat: { 32 | forking: { 33 | blockNumber: process.env.BLOCK_NUMBER ? parseInt(process.env.BLOCK_NUMBER!) : undefined, 34 | url: configFile()[`NODE_URL_${process.env.NETWORK?.toUpperCase() || "ETH"}`] as string, 35 | }, 36 | blockGasLimit: 10e6, 37 | accounts: { 38 | accountsBalance: bn18("1,000,000").toString(), 39 | }, 40 | }, 41 | eth: { 42 | chainId: networks.eth.id, 43 | url: configFile().NODE_URL_ETH, 44 | }, 45 | poly: { 46 | chainId: networks.poly.id, 47 | url: configFile().NODE_URL_POLY, 48 | }, 49 | avax: { 50 | chainId: networks.avax.id, 51 | url: configFile().NODE_URL_AVAX, 52 | }, 53 | }, 54 | typechain: { 55 | outDir: "typechain-hardhat", 56 | target: "web3-v1", 57 | }, 58 | mocha: { 59 | timeout: 240_000, 60 | retries: 0, 61 | bail: true, 62 | }, 63 | gasReporter: { 64 | currency: "USD", 65 | coinmarketcap: configFile().coinmarketcapKey, 66 | token: process.env.NETWORK?.toLowerCase() == "avax" ? "AVAX" : process.env.NETWORK?.toLowerCase() == "poly" ? "MATIC" : undefined, 67 | gasPriceApi: 68 | process.env.NETWORK?.toLowerCase() == "avax" 69 | ? "https://api.snowtrace.io/api?module=proxy&action=eth_gasPrice" 70 | : process.env.NETWORK?.toLowerCase() == "poly" 71 | ? "https://api.polygonscan.com/api?module=proxy&action=eth_gasPrice" 72 | : undefined, 73 | showTimeSpent: true, 74 | }, 75 | etherscan: { 76 | apiKey: configFile()[`ETHERSCAN_${process.env.NETWORK?.toUpperCase() || "ETH"}`], 77 | }, 78 | } as HardhatUserConfig; 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aave-loop", 3 | "version": "2.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/defi-org-code/AaveLoop.git" 7 | }, 8 | "author": "Orbs", 9 | "license": "MIT", 10 | "dependencies": { 11 | "@defi.org/web3-candies": "2.x", 12 | "axios": "0.24.x" 13 | }, 14 | "scripts": { 15 | "prettier": "prettier --write '{test,src,contracts}/**/*.{ts,js,json,sol}'", 16 | "typechain": "npx hardhat typechain", 17 | "prebuild": "rm -rf artifacts typechain-hardhat typechain-abi && npm run prettier && npm run typechain", 18 | "build": "npx hardhat compile && npx solhint 'contracts/**/*.sol'", 19 | "test": "DEBUG=web3-candies npx hardhat test --logs", 20 | "test:avax": "DEBUG=web3-candies NETWORK=avax npx hardhat test --logs", 21 | "test:poly": "DEBUG=web3-candies NETWORK=poly npx hardhat test --logs", 22 | "deploy": "DEBUG=web3-candies npx hardhat deploy --network $1" 23 | }, 24 | "prettier": { 25 | "printWidth": 180 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/aave-e2e-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { 3 | aaveloop, 4 | asset, 5 | DAYS_TO_SAFE_GAS, 6 | deployer, 7 | expectInPosition, 8 | expectOutOfPosition, 9 | fundOwner, 10 | getPrice, 11 | initFixture, 12 | owner, 13 | config, 14 | PRINCIPAL, 15 | reward, 16 | } from "./test-base"; 17 | import { bn, bn18, ether, expectRevert, fmt18, maxUint256, useChaiBN, zero } from "@defi.org/web3-candies"; 18 | import { mineBlock, mineBlocks, resetNetworkFork } from "@defi.org/web3-candies/dist/hardhat"; 19 | 20 | useChaiBN(); 21 | 22 | describe("AaveLoop E2E Tests", () => { 23 | beforeEach(async () => { 24 | await resetNetworkFork(); 25 | await initFixture(); 26 | expect(await aaveloop.methods.getLTV().call()).bignumber.eq(bn(config.LTV), `assuming ${config.LTV} LTV`); 27 | 28 | await fundOwner(PRINCIPAL); 29 | 30 | const initialAssetBalance = bn(await asset.methods.balanceOf(owner).call()); 31 | expect(initialAssetBalance).bignumber.eq(await asset.amount(PRINCIPAL), `assuming ${PRINCIPAL} principal`); 32 | await asset.methods.approve(aaveloop.options.address, maxUint256).send({ from: owner }); 33 | }); 34 | 35 | it(`Full E2E ${DAYS_TO_SAFE_GAS} days exit safely under current market conditions: LTV=${config.LTV} ITERATIONS=${config.iterations}`, async () => { 36 | await aaveloop.methods.enterPositionFully(config.iterations).send({ from: owner }); 37 | // ITERATIONS will result in free liquidity of 5% (+-1%) of PRINCIPAL 38 | expect(await aaveloop.methods.getLiquidity().call()).bignumber.closeTo(await asset.amount(PRINCIPAL * 0.05), await asset.amount(PRINCIPAL * 0.01)); 39 | await expectInPosition(PRINCIPAL, config.expectedLeverage); 40 | 41 | await mineBlock(60 * 60 * 24 * DAYS_TO_SAFE_GAS); 42 | 43 | expect(await aaveloop.methods.getLiquidity().call()) 44 | .bignumber.gt(zero) 45 | .lt(await asset.amount(PRINCIPAL * 0.02)); // < 2% liquidity 46 | 47 | const tx = await aaveloop.methods.exitPosition(50).send({ from: owner }); 48 | expect(tx.gasUsed).lt(10_000_000); 49 | await expectOutOfPosition(); 50 | 51 | expect(await asset.methods.balanceOf(owner).call()).bignumber.closeTo(await asset.amount(PRINCIPAL), await asset.amount(PRINCIPAL * 0.05)); // 5% max interest paid over 1 year 52 | }); 53 | 54 | [0, 1, 2].map((iterations) => 55 | it(`Enter & exit with ${iterations} iterations`, async () => { 56 | await aaveloop.methods.enterPositionFully(iterations).send({ from: owner }); 57 | await expectInPosition(PRINCIPAL, iterations == 0 ? 0 : 1.1); // depends on LTV 58 | 59 | // expect(await aaveloop.methods.exitPosition(100).call({ from: owner })).bignumber.closeTo(await asset.amount(PRINCIPAL), await asset.amount(1)); 60 | await aaveloop.methods.exitPosition(100).send({ from: owner }); 61 | await expectOutOfPosition(); 62 | 63 | expect(await asset.methods.balanceOf(owner).call()).bignumber.closeTo(await asset.amount(PRINCIPAL), await asset.amount(1)); 64 | }) 65 | ); 66 | 67 | it("Show me the money", async () => { 68 | await aaveloop.methods.enterPosition(await asset.amount(PRINCIPAL), config.iterations).send({ from: owner }); 69 | await expectInPosition(PRINCIPAL, config.expectedLeverage); 70 | 71 | await mineBlocks(60 * 60 * 24, 10); // secondsPerBlock does not change outcome (Aave uses block.timestamp) 72 | 73 | const pending = await aaveloop.methods.getPendingRewards().call(); 74 | expect(pending).bignumber.gt(zero); 75 | expect(await reward.methods.balanceOf(owner).call()).bignumber.zero; 76 | 77 | await aaveloop.methods.claimRewardsToOwner().send({ from: deployer }); 78 | 79 | expect(await reward.methods.balanceOf(owner).call()) 80 | .bignumber.gt(zero) 81 | .closeTo(pending, bn(pending).muln(0.01)); 82 | 83 | await aaveloop.methods.exitPosition(50).send({ from: owner }); 84 | await expectOutOfPosition(); 85 | 86 | const endBalance = bn(await asset.methods.balanceOf(owner).call()); 87 | const profitFromInterest = await asset.mantissa(endBalance.sub(await asset.amount(PRINCIPAL))); 88 | console.log("profit from interest", fmt18(profitFromInterest)); 89 | 90 | const rewardPrice = await getPrice(reward); 91 | console.log("assuming reward price in USD", rewardPrice); 92 | const rewardBalance = await reward.mantissa(await reward.methods.balanceOf(owner).call()); 93 | const profitFromRewards = await reward.mantissa(rewardBalance.muln(rewardPrice)); 94 | console.log("profit from rewards", fmt18(profitFromRewards)); 95 | const profit = profitFromInterest.add(profitFromRewards); 96 | console.log("total profit", fmt18(profit)); 97 | 98 | const principalUsd = PRINCIPAL * (await getPrice(asset)); 99 | const dailyRate = profit.mul(ether).div(bn18(principalUsd)); 100 | console.log("dailyRate:", fmt18(dailyRate.muln(100)), "%"); 101 | 102 | const APR = dailyRate.muln(365); 103 | console.log("result APR: ", fmt18(APR.muln(100)), "%"); 104 | 105 | const APY = Math.pow(1 + parseFloat(fmt18(APR.divn(365))), 365) - 1; 106 | console.log("result APY: ", APY * 100, "%"); 107 | console.log("================="); 108 | }); 109 | 110 | it("Add to existing position", async () => { 111 | await aaveloop.methods.enterPosition(await asset.amount(PRINCIPAL), config.iterations).send({ from: owner }); 112 | await expectInPosition(PRINCIPAL, config.expectedLeverage); 113 | 114 | await fundOwner(PRINCIPAL); 115 | await aaveloop.methods.enterPosition(await asset.amount(PRINCIPAL), config.iterations).send({ from: owner }); 116 | await expectInPosition(PRINCIPAL * 2, config.expectedLeverage); 117 | }); 118 | 119 | it("partial exit, lower leverage", async () => { 120 | await aaveloop.methods.enterPositionFully(config.iterations).send({ from: owner }); 121 | await expectInPosition(10_000_000, config.expectedLeverage); 122 | expect(await asset.methods.balanceOf(owner).call()).bignumber.zero; 123 | const startBorrowBalance = await aaveloop.methods.getBorrowBalance().call(); 124 | const startLiquidity = await aaveloop.methods.getLiquidity().call(); 125 | const startHealthFactor = (await aaveloop.methods.getPositionData().call()).healthFactor; 126 | 127 | await aaveloop.methods.exitPosition(5).send({ from: owner }); 128 | expect(await aaveloop.methods.getSupplyBalance().call()).bignumber.gt(zero); 129 | expect(await asset.methods.balanceOf(owner).call()).bignumber.zero; 130 | expect(await aaveloop.methods.getBorrowBalance().call()) 131 | .bignumber.lt(startBorrowBalance) 132 | .gt(zero); 133 | expect(await aaveloop.methods.getLiquidity().call()) 134 | .bignumber.gt(startLiquidity) 135 | .gt(zero); 136 | expect((await aaveloop.methods.getPositionData().call()).healthFactor).bignumber.gt(startHealthFactor); 137 | 138 | await aaveloop.methods.exitPosition(100).send({ from: owner }); 139 | await expectOutOfPosition(); 140 | expect(await asset.methods.balanceOf(owner).call()).bignumber.closeTo(await asset.amount(PRINCIPAL), await asset.amount(1)); 141 | }); 142 | 143 | it("Liquidity decrease over time", async () => { 144 | await aaveloop.methods.enterPositionFully(5).send({ from: owner }); 145 | 146 | const startHF = bn((await aaveloop.methods.getPositionData().call()).healthFactor); 147 | const startLiquidity = bn(await aaveloop.methods.getLiquidity().call()); 148 | 149 | await mineBlock(60 * 60 * 24 * 365); 150 | 151 | expect((await aaveloop.methods.getPositionData().call()).healthFactor) 152 | .bignumber.lt(startHF) 153 | .gt(ether); // must be > 1e18 or be liquidated 154 | expect(await aaveloop.methods.getLiquidity().call()) 155 | .bignumber.lt(startLiquidity) 156 | .gt(zero); // must be > 0 or be liquidated 157 | }); 158 | 159 | it("When low liquidity, must provide additional collateral and exit in multiple txs", async () => { 160 | await aaveloop.methods.enterPositionFully(config.iterations).send({ from: owner }); 161 | await expectInPosition(10_000_000, config.expectedLeverage); 162 | 163 | const redeemable = bn(await aaveloop.methods.getLiquidity().call()) 164 | .muln(1e4) 165 | .divn(config.LTV); 166 | await aaveloop.methods._redeemSupply(redeemable.subn(100)).send({ from: owner }); // redeem 99.99999% 167 | await aaveloop.methods._withdrawToOwner(asset.address).send({ from: owner }); 168 | 169 | expect(await aaveloop.methods.getLiquidity().call()).bignumber.closeTo(zero, await asset.amount(1)); 170 | 171 | await expectRevert(() => aaveloop.methods.exitPosition(100).send({ from: owner }), "revert"); // block gas limit 172 | 173 | await fundOwner(1_000); 174 | await aaveloop.methods.enterPosition(await asset.amount(1_000), 0).send({ from: owner }); 175 | 176 | while (bn(await aaveloop.methods.getSupplyBalance().call()).gtn(0)) { 177 | await aaveloop.methods.exitPosition(20).send({ from: owner }); 178 | } 179 | 180 | await expectOutOfPosition(); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /test/aave-emergency-test.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js"; 2 | import { expect } from "chai"; 3 | import { aaveloop, asset, deployer, expectInPosition, expectOutOfPosition, fundOwner, incentives, initFixture, lendingPool, networkShortName, owner } from "./test-base"; 4 | import { weth } from "./consts"; 5 | import { bn, ether, maxUint256 } from "@defi.org/web3-candies"; 6 | import { deployArtifact } from "@defi.org/web3-candies/dist/hardhat"; 7 | import { AaveLoop } from "../typechain-hardhat/AaveLoop"; 8 | 9 | describe("AaveLoop Emergency Tests", () => { 10 | const PRINCIPAL = 1_000_000; 11 | let initialBalance: BN; 12 | 13 | beforeEach(async () => { 14 | await initFixture(); 15 | await fundOwner(PRINCIPAL); 16 | initialBalance = bn(await asset.methods.balanceOf(owner).call()); 17 | }); 18 | 19 | it("Owner able to call step by step", async () => { 20 | await asset.methods.transfer(aaveloop.options.address, 100).send({ from: owner }); 21 | await aaveloop.methods._supply(100).send({ from: owner }); 22 | await aaveloop.methods._borrow(50).send({ from: owner }); 23 | await aaveloop.methods._repayBorrow(50).send({ from: owner }); 24 | await aaveloop.methods._redeemSupply(100).send({ from: owner }); 25 | await aaveloop.methods._withdrawToOwner(asset.address).send({ from: owner }); 26 | await expectOutOfPosition(); 27 | }); 28 | 29 | it("withdrawToOwner", async () => { 30 | const _weth = weth[networkShortName](); 31 | await _weth.methods.deposit().send({ from: owner, value: ether }); 32 | const balance = await _weth.methods.balanceOf(owner).call(); 33 | 34 | await _weth.methods.transfer(aaveloop.options.address, ether).send({ from: owner }); 35 | expect(await _weth.methods.balanceOf(owner).call()).bignumber.zero; 36 | 37 | await aaveloop.methods._withdrawToOwner(_weth.address).send({ from: owner }); 38 | 39 | expect(await _weth.methods.balanceOf(owner).call()).bignumber.eq(balance); 40 | }); 41 | 42 | it("emergencyFunctionCall", async () => { 43 | await asset.methods.transfer(aaveloop.options.address, await asset.amount(PRINCIPAL)).send({ from: owner }); 44 | 45 | const encoded = asset.methods.transfer(owner, await asset.amount(PRINCIPAL)).encodeABI(); 46 | await aaveloop.methods.emergencyFunctionCall(asset.options.address, encoded).send({ from: owner }); 47 | 48 | expect(await asset.methods.balanceOf(aaveloop.options.address).call()).bignumber.zero; 49 | expect(await asset.methods.balanceOf(owner).call()).bignumber.eq(initialBalance); 50 | }); 51 | 52 | it("emergencyFunctionDelegateCall", async () => { 53 | await asset.methods.transfer(aaveloop.options.address, await asset.amount(PRINCIPAL)).send({ from: owner }); 54 | 55 | const deployed = await deployArtifact("AaveLoop", { from: deployer }, [owner, asset.address, lendingPool.options.address, incentives.options.address], 0); 56 | const encoded = deployed.methods._withdrawToOwner(asset.address).encodeABI(); 57 | await aaveloop.methods.emergencyFunctionDelegateCall(deployed.options.address, encoded).send({ from: owner }); // run _withdrawToOwner in the context of original aaveloop 58 | 59 | expect(await asset.methods.balanceOf(aaveloop.options.address).call()).bignumber.zero; 60 | expect(await asset.methods.balanceOf(owner).call()).bignumber.eq(initialBalance); 61 | }); 62 | 63 | it("Exit position one by one manually", async () => { 64 | await asset.methods.approve(aaveloop.options.address, await asset.amount(PRINCIPAL)).send({ from: owner }); 65 | 66 | await aaveloop.methods.enterPosition(await asset.amount(PRINCIPAL), 1).send({ from: owner }); 67 | await expectInPosition(PRINCIPAL, 1.5); // at least x1.5 68 | 69 | while (bn(await aaveloop.methods.getBorrowBalance().call()).gtn(0)) { 70 | await aaveloop.methods._redeemSupply(await asset.amount(100_000)).send({ from: owner }); 71 | await aaveloop.methods._repayBorrow(await asset.amount(100_000)).send({ from: owner }); 72 | } 73 | await aaveloop.methods._redeemSupply(maxUint256).send({ from: owner }); 74 | await aaveloop.methods._withdrawToOwner(asset.address).send({ from: owner }); 75 | 76 | expect(await asset.methods.balanceOf(owner).call()) 77 | .bignumber.gt(initialBalance) 78 | .closeTo(initialBalance, await asset.amount(1)); 79 | await expectOutOfPosition(); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/consts.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { erc20s as erc20sOrig, contracts as contractsOrig } from "@defi.org/web3-candies/dist/erc20"; 3 | 4 | export const erc20s = _.merge({}, erc20sOrig, { 5 | eth: { 6 | USDC: () => _.merge(erc20sOrig.eth.USDC(), { whale: "0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8" }), 7 | }, 8 | poly: { 9 | USDC: () => _.merge(erc20sOrig.poly.USDC(), { whale: "0xBA12222222228d8Ba445958a75a0704d566BF2C8" }), 10 | WETH: () => _.merge(erc20sOrig.poly.WETH(), { whale: "0xBA12222222228d8Ba445958a75a0704d566BF2C8" }), 11 | DAI: () => _.merge(erc20sOrig.poly.DAI(), { whale: "0xBA12222222228d8Ba445958a75a0704d566BF2C8" }), 12 | }, 13 | avax: { 14 | USDC: () => erc20s.avax.USDCe(), 15 | USDCe: () => _.merge(erc20sOrig.avax.USDCe(), { whale: "0xA389f9430876455C36478DeEa9769B7Ca4E3DDB1" }), 16 | }, 17 | }); 18 | 19 | export const contracts = _.merge({}, contractsOrig, { 20 | eth: { 21 | // 22 | }, 23 | poly: { 24 | // 25 | }, 26 | avax: { 27 | // 28 | }, 29 | }); 30 | 31 | export const rewards = { 32 | eth: () => erc20s.eth.Aave_stkAAVE(), 33 | poly: () => erc20s.poly.WMATIC(), 34 | avax: () => erc20s.avax.WAVAX(), 35 | }; 36 | 37 | export const weth = { 38 | eth: () => erc20s.eth.WETH(), 39 | poly: () => erc20s.poly.WMATIC(), 40 | avax: () => erc20s.avax.WAVAX(), 41 | }; 42 | -------------------------------------------------------------------------------- /test/sanity-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { erc20s, expectRevert, maxUint256, useChaiBN, web3, zeroAddress } from "@defi.org/web3-candies"; 3 | import { aaveloop, asset, deployer, initFixture, owner } from "./test-base"; 4 | import { deployArtifact } from "@defi.org/web3-candies/dist/hardhat"; 5 | 6 | useChaiBN(); 7 | 8 | describe("AaveLoop Sanity Tests", () => { 9 | beforeEach(async () => { 10 | await initFixture(); 11 | }); 12 | 13 | it("empty state", async () => { 14 | expect(await aaveloop.methods.OWNER().call()).eq(owner); 15 | expect(await aaveloop.methods.getSupplyBalance().call()).bignumber.zero; 16 | expect(await aaveloop.methods.getBorrowBalance().call()).bignumber.zero; 17 | expect(await aaveloop.methods.getAssetBalance().call()).bignumber.zero; 18 | expect(await aaveloop.methods.getLiquidity().call()).bignumber.zero; 19 | const result = await aaveloop.methods.getPositionData().call(); 20 | expect(result.healthFactor).bignumber.eq(maxUint256); 21 | expect(result.ltv).bignumber.zero; 22 | await aaveloop.methods.claimRewardsToOwner().send({ from: owner }); 23 | }); 24 | 25 | it("constructor args", async () => { 26 | await expectRevert(() => deployArtifact("AaveLoop", { from: deployer }, [zeroAddress, zeroAddress, zeroAddress, zeroAddress], 0), "owner 0"); 27 | await expectRevert(() => deployArtifact("AaveLoop", { from: deployer }, [deployer, zeroAddress, zeroAddress, zeroAddress], 0), "address 0"); 28 | await expectRevert(() => deployArtifact("AaveLoop", { from: deployer }, [deployer, deployer, zeroAddress, zeroAddress], 0), "address 0"); 29 | await expectRevert(() => deployArtifact("AaveLoop", { from: deployer }, [deployer, deployer, deployer, zeroAddress], 0), "address 0"); 30 | }); 31 | 32 | it("access control", async () => { 33 | await expectRevert(() => aaveloop.methods._supply(100).send({ from: deployer }), "onlyOwner"); 34 | await expectRevert(() => aaveloop.methods._borrow(50).send({ from: deployer }), "onlyOwner"); 35 | await expectRevert(() => aaveloop.methods._repayBorrow(50).send({ from: deployer }), "onlyOwner"); 36 | await expectRevert(() => aaveloop.methods._redeemSupply(100).send({ from: deployer }), "onlyOwner"); 37 | 38 | await expectRevert(() => aaveloop.methods.enterPosition(100, 1).send({ from: deployer }), "onlyOwner"); 39 | await expectRevert(() => aaveloop.methods.exitPosition(1).send({ from: deployer }), "onlyOwner"); 40 | 41 | await expectRevert(() => aaveloop.methods._withdrawToOwner(asset.address).send({ from: deployer }), "onlyOwner"); 42 | await expectRevert(() => aaveloop.methods.emergencyFunctionCall(deployer, zeroAddress).send({ from: deployer }), "onlyOwner"); 43 | await expectRevert(() => aaveloop.methods.emergencyFunctionDelegateCall(deployer, zeroAddress).send({ from: deployer }), "onlyOwner"); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/test-base.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import axios from "axios"; 3 | import { expect } from "chai"; 4 | import { contracts, erc20s, rewards } from "./consts"; 5 | import { account, bn, maxUint256, Network, networks, Token, useChaiBN, web3, zero } from "@defi.org/web3-candies"; 6 | import { deployArtifact, hre, impersonate, tag } from "@defi.org/web3-candies/dist/hardhat"; 7 | import type { AaveLoop } from "../typechain-hardhat/AaveLoop"; 8 | import type { ILendingPool } from "../typechain-hardhat/ILendingPool"; 9 | import type { IAaveIncentivesController } from "../typechain-hardhat/IAaveIncentivesController"; 10 | 11 | useChaiBN(); 12 | 13 | export const networkShortName = (process.env.NETWORK || (hre().network.name != "hardhat" ? hre().network.name : "eth")).toLowerCase() as "eth" | "poly" | "avax"; 14 | export const network = (networks as any)[networkShortName] as Network; 15 | console.log("🌐 using network 🌐", network.name); 16 | 17 | export let aaveloop: AaveLoop; 18 | export let lendingPool: ILendingPool; 19 | export let incentives: IAaveIncentivesController; 20 | export let asset: Token; 21 | export let reward: Token; 22 | 23 | export let deployer: string; // used only in tests 24 | export let owner: string; // used only in tests 25 | 26 | const CONFIG = { 27 | eth: { LTV: 8250, iterations: 15, expectedLeverage: 5.3 }, 28 | avax: { LTV: 7500, iterations: 10, expectedLeverage: 3.7 }, 29 | poly: { LTV: 8000, iterations: 13, expectedLeverage: 4.6 }, 30 | }; 31 | 32 | export const DAYS_TO_SAFE_GAS = 365; 33 | export const PRINCIPAL = 10_000_000; 34 | export const config = CONFIG[networkShortName]; 35 | 36 | export function initNetworkContracts() { 37 | asset = erc20s[networkShortName].USDC(); 38 | reward = rewards[networkShortName](); 39 | lendingPool = (contracts[networkShortName] as any).Aave_LendingPool(); 40 | incentives = (contracts[networkShortName] as any).Aave_Incentives(); 41 | } 42 | 43 | export async function initFixture() { 44 | initNetworkContracts(); 45 | deployer = await account(0); 46 | owner = await account(1); 47 | tag(deployer, "deployer"); 48 | tag(owner, "owner"); 49 | 50 | aaveloop = await deployArtifact("AaveLoop", { from: deployer }, [owner, asset.address, lendingPool.options.address, incentives.options.address], 0); 51 | } 52 | 53 | export async function fundOwner(amount: number) { 54 | const whale = (asset as any).whale; 55 | await impersonate(whale); 56 | await hre().network.provider.send("hardhat_setBalance", [whale, web3().utils.numberToHex(maxUint256)]); 57 | await asset.methods.transfer(owner, await asset.amount(amount)).send({ from: whale }); 58 | } 59 | 60 | export async function expectInPosition(principal: number, leverage: number) { 61 | expect(await aaveloop.methods.getSupplyBalance().call()) 62 | .bignumber.gt(zero) 63 | .gte(await asset.amount(bn(principal * leverage))); 64 | if (leverage > 1) { 65 | expect(await aaveloop.methods.getBorrowBalance().call()).bignumber.gte(await asset.amount(bn(principal * leverage - principal))); 66 | } else { 67 | expect(await aaveloop.methods.getBorrowBalance().call()).bignumber.zero; 68 | } 69 | expect(await aaveloop.methods.getLiquidity().call()).bignumber.gt(zero); //depends on LTV 70 | expect(await aaveloop.methods.getAssetBalance().call()).bignumber.zero; 71 | } 72 | 73 | export async function expectOutOfPosition() { 74 | expect(await aaveloop.methods.getSupplyBalance().call()).bignumber.zero; 75 | expect(await aaveloop.methods.getBorrowBalance().call()).bignumber.zero; 76 | expect(await aaveloop.methods.getLiquidity().call()).bignumber.zero; 77 | expect(await aaveloop.methods.getAssetBalance().call()).bignumber.zero; 78 | } 79 | 80 | export async function getPrice(asset: Token) { 81 | if (asset.address.toLowerCase() == erc20s.eth.Aave_stkAAVE().address.toLowerCase()) { 82 | // special case for stkAAVE 83 | asset = erc20s.eth.AAVE(); 84 | } 85 | const coingeckoIds = { 86 | [networks.eth.id]: "ethereum", 87 | [networks.poly.id]: "polygon-pos", 88 | [networks.avax.id]: "avalanche", 89 | }; 90 | const url = `https://api.coingecko.com/api/v3/simple/token_price/${coingeckoIds[network.id]}?contract_addresses=${asset.address}&vs_currencies=usd`; 91 | const response = await axios.get(url); 92 | return (_.values(response.data)[0]["usd"] as number) || 1; 93 | } 94 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "CommonJS", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true 8 | }, 9 | "files": ["./hardhat.config.ts"] 10 | } 11 | --------------------------------------------------------------------------------