diff --git a/foundry.toml b/foundry.toml index 983f3b6d..ff12d144 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,6 +2,7 @@ optimizer_runs = 1000000 verbosity = 1 solc = "0.8.21" +evm_version = "cancun" [fuzz] runs = 256 diff --git a/src/interfaces/aave-v3/IRewardsController.sol b/src/interfaces/aave-v3/IRewardsController.sol new file mode 100644 index 00000000..cc315820 --- /dev/null +++ b/src/interfaces/aave-v3/IRewardsController.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IRewardsController { + function claimAllRewardsToSelf(address[] calldata assets) + external + returns (address[] memory rewardsList, uint256[] memory claimedAmounts); +} diff --git a/src/lib/Constants.sol b/src/lib/Constants.sol index c30b1154..032b6630 100644 --- a/src/lib/Constants.sol +++ b/src/lib/Constants.sol @@ -48,9 +48,13 @@ library Constants { address public constant AAVE_V3_POOL = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; // address of the Aave pool data provider contract address public constant AAVE_V3_POOL_DATA_PROVIDER = 0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3; + // address of the Aave Rewards Controller contract + address public constant AAVE_V3_REWARDS_CONTROLLER = 0x8164Cc65827dcFe994AB23944CBC90e0aa80bFcb; // address of the Aave v3 "aEthUSDC" token (supply token) address public constant AAVE_V3_AUSDC_TOKEN = 0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c; + // address of the Aave v3 "aEthUSDS" token (supply token) + address public constant AAVE_V3_AUSDS_TOKEN = 0x32a6268f9Ba3642Dda7892aDd74f1D34469A4259; // address of the Aave v3 "aEthUSDT" token (supply token) address public constant AAVE_V3_AUSDT_TOKEN = 0x23878914EFE38d27C4D67Ab83ed1b93A74D4086a; // address of the Aave v3 "aEthwstETH" token (supply token) diff --git a/src/steth/priceConverter/SUsdsWethPriceConverter.sol b/src/steth/priceConverter/DaiWethPriceConverter.sol similarity index 76% rename from src/steth/priceConverter/SUsdsWethPriceConverter.sol rename to src/steth/priceConverter/DaiWethPriceConverter.sol index 3f464974..6efdea77 100644 --- a/src/steth/priceConverter/SUsdsWethPriceConverter.sol +++ b/src/steth/priceConverter/DaiWethPriceConverter.sol @@ -8,14 +8,14 @@ import {AggregatorV3Interface} from "../../interfaces/chainlink/AggregatorV3Inte import {Constants as C} from "../../lib/Constants.sol"; /** - * @title SDaiWethPriceConverter - * @notice Contract for price conversion between sDAI and WETH. + * @title DaiWethPriceConverter + * @notice Contract for price conversion between DAI/USDS and WETH. */ -contract SUsdsWethPriceConverter is ISinglePairPriceConverter { +contract DaiWethPriceConverter is ISinglePairPriceConverter { using FixedPointMathLib for uint256; /// @notice The address of the asset token (sDAI). - address public constant override asset = C.SUSDS; + address public constant override asset = C.DAI; /// @notice The address of the target token (WETH). address public constant override targetToken = C.WETH; @@ -24,21 +24,21 @@ contract SUsdsWethPriceConverter is ISinglePairPriceConverter { AggregatorV3Interface public constant DAI_ETH_PRICE_FEED = AggregatorV3Interface(C.CHAINLINK_DAI_ETH_PRICE_FEED); /** - * @notice Converts an amount of WETH to the equivalent amount of sDAI. + * @notice Converts an amount of WETH to the equivalent amount of DAI. * @param _ethAmount The amount of WETH to convert. - * @return The equivalent amount of sDAI. + * @return The equivalent amount of DAI. */ function targetTokenToAsset(uint256 _ethAmount) external view override returns (uint256) { - return IERC4626(asset).convertToShares(_ethToDai(_ethAmount)); + return _ethToDai(_ethAmount); } /** - * @notice Converts an amount of sDAI to the equivalent amount of WETH. - * @param _sDaiAmount The amount of sDAI to convert. + * @notice Converts an amount of DAI to the equivalent amount of WETH. + * @param _daiAmount The amount of DAI to convert. * @return The equivalent amount of WETH. */ - function assetToTargetToken(uint256 _sDaiAmount) external view override returns (uint256) { - return _daiToEth(IERC4626(asset).convertToAssets(_sDaiAmount)); + function assetToTargetToken(uint256 _daiAmount) external view override returns (uint256) { + return _daiToEth(_daiAmount); } /** diff --git a/src/steth/scUSDSv2.sol b/src/steth/scUSDSv2.sol new file mode 100644 index 00000000..2ce8a322 --- /dev/null +++ b/src/steth/scUSDSv2.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {ERC4626} from "solmate/mixins/ERC4626.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; + +import {scCrossAssetYieldVault} from "./scCrossAssetYieldVault.sol"; +import {Constants as C} from "../lib/Constants.sol"; +import {ISinglePairPriceConverter} from "./priceConverter/ISinglePairPriceConverter.sol"; +import {ISinglePairSwapper} from "./swapper/ISinglePairSwapper.sol"; + +/** + * @title scUSDSv2 + * @notice Sandclock USDS Vault implementation. + * @dev Inherits from scCrossAssetYieldVault to manage and generate USDS yield. + * @dev There is no USDS Chainlink Feed, but since USDS to DAI is always 1:1 so + * we are using the DAI Price Converter here. + * @dev This vault also receives aUSDS rewards which must be claimed periodically using claimRewards() + */ +contract scUSDSv2 is scCrossAssetYieldVault { + using SafeTransferLib for ERC20; + + constructor( + address _admin, + address _keeper, + ERC4626 _targetVault, + ISinglePairPriceConverter _priceConverter, + ISinglePairSwapper _swapper + ) + scCrossAssetYieldVault( + _admin, + _keeper, + ERC20(C.USDS), + _targetVault, + _priceConverter, + _swapper, + "Sandclock USDS Real Yield Vault", + "scUSDSv2" + ) + { + ERC20(C.DAI).safeApprove(C.DAI_USDS_CONVERTER, type(uint256).max); + ERC20(C.USDS).safeApprove(C.DAI_USDS_CONVERTER, type(uint256).max); + } +} diff --git a/src/steth/scUsds-adapters/AaveV3ScUsdsAdapter.sol b/src/steth/scUsds-adapters/AaveV3ScUsdsAdapter.sol new file mode 100644 index 00000000..b843dfd4 --- /dev/null +++ b/src/steth/scUsds-adapters/AaveV3ScUsdsAdapter.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {WETH} from "solmate/tokens/WETH.sol"; +import {IRewardsController} from "../../interfaces/aave-v3/IRewardsController.sol"; + +import {Constants as C} from "../../lib/Constants.sol"; +import {IPool} from "aave-v3/interfaces/IPool.sol"; +import {IPoolDataProvider} from "aave-v3/interfaces/IPoolDataProvider.sol"; +import {IAdapter} from "../IAdapter.sol"; + +/** + * @title Aave v3 Lending Protocol Adapter + * @notice Facilitates lending and borrowing for the Aave v3 lending protocol + */ +contract AaveV3ScUsdsAdapter is IAdapter { + using SafeTransferLib for ERC20; + using SafeTransferLib for WETH; + + ERC20 public constant usds = ERC20(C.USDS); + WETH public constant weth = WETH(payable(C.WETH)); + + // Aave v3 pool contract + IPool public constant pool = IPool(C.AAVE_V3_POOL); + // Aave v3 pool data provider contract + IPoolDataProvider public constant aaveV3PoolDataProvider = IPoolDataProvider(C.AAVE_V3_POOL_DATA_PROVIDER); + // Aave v3 "aEthUSDS" token (supply token) + ERC20 public constant aUsds = ERC20(C.AAVE_V3_AUSDS_TOKEN); + // Aave v3 "variableDebtEthWETH" token (variable debt token) + ERC20 public constant dWeth = ERC20(C.AAVE_V3_VAR_DEBT_WETH_TOKEN); + + /// @inheritdoc IAdapter + uint256 public constant override id = 1; + + /// @inheritdoc IAdapter + function setApprovals() external override { + usds.safeApprove(address(pool), type(uint256).max); + weth.safeApprove(address(pool), type(uint256).max); + } + + /// @inheritdoc IAdapter + function revokeApprovals() external override { + usds.safeApprove(address(pool), 0); + weth.safeApprove(address(pool), 0); + } + + /// @inheritdoc IAdapter + function supply(uint256 _amount) external override { + pool.supply(address(usds), _amount, address(this), 0); + } + + /// @inheritdoc IAdapter + function borrow(uint256 _amount) external override { + pool.borrow(address(weth), _amount, C.AAVE_VAR_INTEREST_RATE_MODE, 0, address(this)); + } + + /// @inheritdoc IAdapter + function repay(uint256 _amount) external override { + pool.repay(address(weth), _amount, C.AAVE_VAR_INTEREST_RATE_MODE, address(this)); + } + + /// @inheritdoc IAdapter + function withdraw(uint256 _amount) external override { + pool.withdraw(address(usds), _amount, address(this)); + } + + /// @inheritdoc IAdapter + function claimRewards(bytes calldata) external override { + address[] memory assets = new address[](1); + assets[0] = C.AAVE_V3_AUSDS_TOKEN; + + IRewardsController(C.AAVE_V3_REWARDS_CONTROLLER).claimAllRewardsToSelf(assets); + } + + /// @inheritdoc IAdapter + function getCollateral(address _account) external view override returns (uint256) { + return aUsds.balanceOf(_account); + } + + /// @inheritdoc IAdapter + function getDebt(address _account) external view override returns (uint256) { + return dWeth.balanceOf(_account); + } + + /// @inheritdoc IAdapter + function getMaxLtv() external view override returns (uint256) { + (, uint256 ltv,,,,,,,,) = aaveV3PoolDataProvider.getReserveConfigurationData(address(usds)); + + // ltv is returned as a percentage with 2 decimals (e.g. 80% = 8000) so we need to multiply by 1e14 + return ltv * 1e14; + } +} diff --git a/src/steth/swapper/UsdsWethSwapper.sol b/src/steth/swapper/UsdsWethSwapper.sol new file mode 100644 index 00000000..a714fae9 --- /dev/null +++ b/src/steth/swapper/UsdsWethSwapper.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {ERC4626} from "solmate/mixins/ERC4626.sol"; + +import {Constants as C} from "../../lib/Constants.sol"; +import {ISinglePairSwapper} from "../swapper/ISinglePairSwapper.sol"; +import {SwapperLib} from "./SwapperLib.sol"; +import {UniversalSwapper} from "./UniversalSwapper.sol"; +import {IDaiUsds} from "../../interfaces/sky/IDaiUsds.sol"; + +contract UsdsWethSwapper is ISinglePairSwapper, UniversalSwapper { + /// @notice The address of the asset token (USDS). + address public constant override asset = address(C.USDS); + + /// @notice The address of the target token (WETH). + address public constant override targetToken = address(C.WETH); + + /// @notice DAI token used as an intermediate token for swaps. + ERC20 public constant dai = ERC20(C.DAI); + + /// @notice The Dai - USDS converter contract from sky + IDaiUsds public constant converter = IDaiUsds(C.DAI_USDS_CONVERTER); + + /// @notice Encoded swap path from WETH to DAI. + bytes public constant swapPath = abi.encodePacked(targetToken, uint24(500), C.USDC, uint24(100), dai); + + /** + * @notice Swap WETH for USDS. + * @param _wethAmount The amount of WETH to swap. + * @param _usdsAmountOutMin The minimum amount of USDS to receive. + * @return usdsReceived The amount of USDS received from the swap. + */ + function swapTargetTokenForAsset(uint256 _wethAmount, uint256 _usdsAmountOutMin) + external + override + returns (uint256 usdsReceived) + { + // swap weth to dai + usdsReceived = SwapperLib._uniswapSwapExactInputMultihop(targetToken, _wethAmount, _usdsAmountOutMin, swapPath); + + // swap dai to usds + converter.daiToUsds(address(this), usdsReceived); + } + + /** + * @notice Swap USDS for an exact amount of WETH. + * @param _wethAmountOut The exact amount of WETH desired. + * @return usdsSpent The amount of USDS spent to receive `_wethAmountOut` of WETH. + */ + function swapAssetForExactTargetToken(uint256 _wethAmountOut) external override returns (uint256 usdsSpent) { + // convert all USDS to DAI + uint256 usdsBalance = ERC20(asset).balanceOf(address(this)); + converter.usdsToDai(address(this), usdsBalance); + + // Swap DAI for exact amount of WETH + usdsSpent = SwapperLib._uniswapSwapExactOutputMultihop(address(dai), _wethAmountOut, usdsBalance, swapPath); + + // convert remaining DAI back to USDS + converter.daiToUsds(address(this), usdsBalance - usdsSpent); + } +} diff --git a/test/scUSDSv2.t.sol b/test/scUSDSv2.t.sol new file mode 100644 index 00000000..0a7060e1 --- /dev/null +++ b/test/scUSDSv2.t.sol @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +import "forge-std/console2.sol"; +import "forge-std/Test.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {WETH} from "solmate/tokens/WETH.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; + +import {Constants as C} from "../src/lib/Constants.sol"; +import {scUSDSv2} from "../src/steth/scUSDSv2.sol"; +import {AaveV3ScUsdsAdapter} from "../src/steth/scUsds-adapters/AaveV3ScUsdsAdapter.sol"; +import {DaiWethPriceConverter} from "../src/steth/priceConverter/DaiWethPriceConverter.sol"; + +import {scWETH} from "../src/steth/scWETH.sol"; +import {scCrossAssetYieldVault} from "../src/steth/scCrossAssetYieldVault.sol"; +import {ISinglePairPriceConverter} from "../src/steth/priceConverter/ISinglePairPriceConverter.sol"; +import {ISinglePairSwapper} from "../src/steth/swapper/ISinglePairSwapper.sol"; +import "../src/errors/scErrors.sol"; +import {Address} from "openzeppelin-contracts/utils/Address.sol"; +import {MainnetAddresses as M} from "../script/base/MainnetAddresses.sol"; +import {UsdsWethSwapper} from "../src/steth/swapper/UsdsWethSwapper.sol"; + +contract scUSDSv2Test is Test { + using Address for address; + using FixedPointMathLib for uint256; + using SafeTransferLib for ERC20; + + event Disinvested(uint256 targetTokenAmount); + + uint256 mainnetFork; + + address constant keeper = address(0x05); + address constant alice = address(0x06); + + WETH weth; + ERC20 usds; + + scWETH wethVault = scWETH(payable(M.SCWETHV2)); + scUSDSv2 vault; + + AaveV3ScUsdsAdapter aaveV3Adapter; + ISinglePairSwapper swapper; + ISinglePairPriceConverter priceConverter; + + uint256 pps; + + constructor() { + mainnetFork = vm.createFork(vm.envString("RPC_URL_MAINNET")); + vm.selectFork(mainnetFork); + vm.rollFork(21072810); + + usds = ERC20(C.USDS); + weth = WETH(payable(C.WETH)); + aaveV3Adapter = new AaveV3ScUsdsAdapter(); + + pps = wethVault.totalAssets().divWadDown(wethVault.totalSupply()); + + _deployAndSetUpVault(); + } + + function test_constructor() public { + assertEq(address(vault.asset()), C.USDS); + assertEq(address(vault.targetToken()), address(weth), "target token"); + assertEq(address(vault.targetVault()), address(wethVault), "weth vault"); + assertEq(address(vault.priceConverter()), address(priceConverter), "price converter"); + assertEq(address(vault.swapper()), address(swapper), "swapper"); + + assertEq(weth.allowance(address(vault), address(vault.targetVault())), type(uint256).max, "scWETH allowance"); + assertEq(usds.allowance(address(vault), address(aaveV3Adapter.pool())), type(uint256).max, "usds allowance"); + assertEq(weth.allowance(address(vault), address(aaveV3Adapter.pool())), type(uint256).max, "weth allowance"); + } + + function test_rebalance() public { + uint256 initialBalance = 1_000_000e18; + uint256 initialDebt = 100 ether; + deal(address(usds), address(vault), initialBalance); + + bytes[] memory callData = new bytes[](2); + callData[0] = abi.encodeWithSelector(scCrossAssetYieldVault.supply.selector, aaveV3Adapter.id(), initialBalance); + callData[1] = abi.encodeWithSelector(scCrossAssetYieldVault.borrow.selector, aaveV3Adapter.id(), initialDebt); + + vault.rebalance(callData); + + assertEq(vault.totalDebt(), initialDebt, "total debt"); + assertEq(vault.totalCollateral(), initialBalance, "total collateral"); + + _assertCollateralAndDebt(aaveV3Adapter.id(), initialBalance, initialDebt); + + assertApproxEqRel(wethVault.balanceOf(address(vault)), initialDebt.divWadDown(pps), 1e5, "scETH shares"); + } + + function test_disinvest() public { + uint256 initialBalance = 1_000_000e18; + uint256 initialDebt = 100 ether; + deal(address(usds), address(vault), initialBalance); + bytes[] memory callData = new bytes[](2); + callData[0] = abi.encodeWithSelector(scCrossAssetYieldVault.supply.selector, aaveV3Adapter.id(), initialBalance); + callData[1] = abi.encodeWithSelector(scCrossAssetYieldVault.borrow.selector, aaveV3Adapter.id(), initialDebt); + vault.rebalance(callData); + + uint256 disinvestAmount = vault.targetTokenInvestedAmount() / 2; + vault.disinvest(disinvestAmount); + + assertApproxEqRel(weth.balanceOf(address(vault)), disinvestAmount, 1e2, "weth balance"); + assertApproxEqRel(vault.targetTokenInvestedAmount(), initialDebt - disinvestAmount, 1e2, "weth invested"); + } + + function test_sellProfit() public { + uint256 initialBalance = 100000e18; + uint256 initialDebt = 10 ether; + deal(address(usds), address(vault), initialBalance); + + bytes[] memory callData = new bytes[](2); + callData[0] = abi.encodeWithSelector(scCrossAssetYieldVault.supply.selector, aaveV3Adapter.id(), initialBalance); + callData[1] = abi.encodeWithSelector(scCrossAssetYieldVault.borrow.selector, aaveV3Adapter.id(), initialDebt); + + vault.rebalance(callData); + + // add 100% profit to the weth vault + uint256 initialWethInvested = vault.targetTokenInvestedAmount(); + deal(address(weth), address(wethVault), initialWethInvested * 2); + + uint256 usdsBalanceBefore = vault.assetBalance(); + uint256 profit = vault.getProfit(); + + vm.prank(keeper); + vault.sellProfit(0); + + uint256 expectedDaiBalance = usdsBalanceBefore + priceConverter.targetTokenToAsset(profit); + _assertCollateralAndDebt(aaveV3Adapter.id(), initialBalance, initialDebt); + assertApproxEqRel(vault.assetBalance(), expectedDaiBalance, 0.01e18, "usds balance"); + assertApproxEqRel( + vault.targetTokenInvestedAmount(), initialWethInvested, 0.001e18, "sold more than actual profit" + ); + } + + function test_withdrawFunds() public { + uint256 initialBalance = 1_000_000e18; + _deposit(alice, initialBalance); + + bytes[] memory callData = new bytes[](2); + callData[0] = abi.encodeWithSelector(scCrossAssetYieldVault.supply.selector, aaveV3Adapter.id(), initialBalance); + callData[1] = abi.encodeWithSelector(scCrossAssetYieldVault.borrow.selector, aaveV3Adapter.id(), 100 ether); + + vault.rebalance(callData); + + uint256 withdrawAmount = vault.convertToAssets(vault.balanceOf(alice)); + vm.prank(alice); + vault.withdraw(withdrawAmount, alice, alice); + + assertEq(usds.balanceOf(alice), withdrawAmount, "alice asset balance"); + } + + function testFuzz_withdraw_whenInProfit(uint256 _amount, uint256 _withdrawAmount) public { + _amount = 0; + _amount = bound(_amount, 1e18, 10_000_000e18); // upper limit constrained by weth available on aave v3 + deal(address(usds), alice, _amount); + + vm.startPrank(alice); + usds.approve(address(vault), type(uint256).max); + vault.deposit(_amount, alice); + vm.stopPrank(); + + uint256 borrowAmount = priceConverter.assetToTargetToken(_amount.mulWadDown(0.7e18)); + + bytes[] memory callData = new bytes[](2); + callData[0] = abi.encodeWithSelector(scCrossAssetYieldVault.supply.selector, aaveV3Adapter.id(), _amount); + callData[1] = abi.encodeWithSelector(scCrossAssetYieldVault.borrow.selector, aaveV3Adapter.id(), borrowAmount); + + vault.rebalance(callData); + + // add 10% profit to the weth vault + uint256 wethInvested = weth.balanceOf(address(wethVault)); + deal(address(weth), address(wethVault), wethInvested.mulWadUp(1.1e18)); + + uint256 total = vault.totalAssets(); + _withdrawAmount = bound(_withdrawAmount, 1e18, total); + vm.startPrank(alice); + vault.withdraw(_withdrawAmount, alice, alice); + + assertApproxEqAbs(vault.totalAssets(), total - _withdrawAmount, total.mulWadDown(0.001e18), "total assets"); + assertApproxEqAbs(usds.balanceOf(alice), _withdrawAmount, _amount.mulWadDown(0.001e18), "usds balance"); + } + + function test_exitAllPositions_RepaysDebtAndReleasesCollateralNoProfit() public { + uint256 initialBalance = 10_000e18; + deal(address(usds), address(vault), initialBalance); + + bytes[] memory callData = new bytes[](2); + callData[0] = abi.encodeWithSelector(scCrossAssetYieldVault.supply.selector, aaveV3Adapter.id(), initialBalance); + callData[1] = abi.encodeWithSelector(scCrossAssetYieldVault.borrow.selector, aaveV3Adapter.id(), 2 ether); + + vault.rebalance(callData); + + assertEq(vault.getProfit(), 0, "profit"); + + uint256 totalBefore = vault.totalAssets(); + + vault.exitAllPositions(0); + + assertApproxEqRel(vault.assetBalance(), totalBefore, 0.001e18, "vault asset balance"); + assertEq(weth.balanceOf(address(vault)), 0, "weth balance"); + assertEq(vault.targetTokenInvestedAmount(), 0, "weth invested"); + assertEq(vault.totalCollateral(), 0, "total collateral"); + assertEq(vault.totalDebt(), 0, "total debt"); + } + + function test_exitAllPositions_RepaysDebtAndReleasesCollateralOnOneProtocolWhenUnderwater() public { + uint256 initialBalance = 1000000e18; + deal(address(usds), address(vault), initialBalance); + + bytes[] memory callData = new bytes[](2); + callData[0] = abi.encodeWithSelector(scCrossAssetYieldVault.supply.selector, aaveV3Adapter.id(), initialBalance); + callData[1] = abi.encodeWithSelector(scCrossAssetYieldVault.borrow.selector, aaveV3Adapter.id(), 100 ether); + + vault.rebalance(callData); + + // simulate 50% loss + deal(address(weth), address(wethVault), 95 ether); + + uint256 totalBefore = vault.totalAssets(); + + assertFalse(vault.flashLoanInitiated(), "flash loan initiated"); + + vault.exitAllPositions(0); + + assertFalse(vault.flashLoanInitiated(), "flash loan initiated"); + + assertApproxEqRel(vault.assetBalance(), totalBefore, 0.02e18, "vault usds balance"); + assertEq(vault.totalCollateral(), 0, "vault collateral"); + assertEq(vault.totalDebt(), 0, "vault debt"); + assertEq(weth.balanceOf(address(vault)), 0, "weth balance"); + assertEq(vault.targetTokenInvestedAmount(), 0, "weth invested"); + } + + function test_exitAllPositions_RepaysDebtAndReleasesCollateralOnOneProtocolWhenInProfit() public { + uint256 initialBalance = 1_000_000e18; + deal(address(usds), address(vault), initialBalance); + + bytes[] memory callData = new bytes[](2); + callData[0] = abi.encodeWithSelector(scCrossAssetYieldVault.supply.selector, aaveV3Adapter.id(), initialBalance); + callData[1] = abi.encodeWithSelector(scCrossAssetYieldVault.borrow.selector, aaveV3Adapter.id(), 100 ether); + + vault.rebalance(callData); + + // simulate profit + uint256 wethInvested = weth.balanceOf(address(wethVault)); + deal(address(weth), address(wethVault), wethInvested.mulWadUp(1.5e18)); + + uint256 totalBefore = vault.totalAssets(); + + vault.exitAllPositions(0); + + assertApproxEqRel(vault.assetBalance(), totalBefore, 0.2e18, "vault usds balance"); + assertEq(vault.totalCollateral(), 0, "vault collateral"); + assertEq(vault.totalDebt(), 0, "vault debt"); + assertEq(weth.balanceOf(address(vault)), 0, "weth balance"); + assertEq(vault.targetTokenInvestedAmount(), 0, "weth invested"); + } + + function test_claimRewards() public { + uint256 initialBalance = 1_000_000e18; + _deposit(address(this), initialBalance); + + bytes[] memory callData = new bytes[](2); + callData[0] = abi.encodeWithSelector(scCrossAssetYieldVault.supply.selector, aaveV3Adapter.id(), initialBalance); + callData[1] = abi.encodeWithSelector(scCrossAssetYieldVault.borrow.selector, aaveV3Adapter.id(), 100 ether); + + vault.rebalance(callData); + + ERC20 aUsds = ERC20(C.AAVE_V3_AUSDS_TOKEN); + uint256 initialAUsdsBalance = aUsds.balanceOf(address(vault)); + uint256 initialCollateral = vault.totalCollateral(); + + vm.warp(block.timestamp + 30 days); + + // the vault gets aUsds Rewards + vault.claimRewards(aaveV3Adapter.id(), ""); + + uint256 newCollateral = vault.totalCollateral(); + uint256 newAUsdsBalance = aUsds.balanceOf(address(vault)); + + assertGt(newCollateral, initialCollateral, "collateral did not increase"); + assertGt(newAUsdsBalance, initialAUsdsBalance, "no aUsds rewards"); + } + + ///////////////////////////////// INTERNAL METHODS ///////////////////////////////// + + function _deposit(address _user, uint256 _amount) public returns (uint256 shares) { + deal(address(usds), _user, _amount); + + vm.startPrank(_user); + usds.approve(address(vault), _amount); + shares = vault.deposit(_amount, _user); + vm.stopPrank(); + } + + function _assertCollateralAndDebt(uint256 _protocolId, uint256 _expectedCollateral, uint256 _expectedDebt) + internal + { + uint256 collateral = vault.getCollateral(_protocolId); + uint256 debt = vault.getDebt(_protocolId); + string memory protocolName = _protocolIdToString(_protocolId); + + assertApproxEqAbs(collateral, _expectedCollateral, 1, string(abi.encodePacked("collateral on ", protocolName))); + assertApproxEqAbs(debt, _expectedDebt, 1, string(abi.encodePacked("debt on ", protocolName))); + } + + function _deployAndSetUpVault() internal { + priceConverter = new DaiWethPriceConverter(); + swapper = new UsdsWethSwapper(); + + vault = new scUSDSv2(address(this), keeper, wethVault, priceConverter, swapper); + + vault.addAdapter(aaveV3Adapter); + + // set vault eth balance to zero + vm.deal(address(vault), 0); + // set float percentage to 0 for most tests + vault.setFloatPercentage(0); + // assign keeper role to deployer + vault.grantRole(vault.KEEPER_ROLE(), address(this)); + } + + function _protocolIdToString(uint256 _protocolId) public view returns (string memory) { + if (_protocolId == aaveV3Adapter.id()) { + return "Aave V3 Adapter"; + } + + revert("unknown protocol"); + } +}