diff --git a/script/v2/DeployOracles.sol b/script/v2/DeployOracles.sol index 17a7957..561a897 100644 --- a/script/v2/DeployOracles.sol +++ b/script/v2/DeployOracles.sol @@ -6,6 +6,7 @@ import {Vm} from "forge-std/Vm.sol"; import {CappedOneDollarOracle} from "@level/src/v2/oracles/CappedOneDollarOracle.sol"; import {IERC4626Oracle} from "@level/src/v2/interfaces/level/IERC4626Oracle.sol"; import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {AaveUmbrellaOracle} from "@level/src/v2/oracles/AaveUmbrellaOracle.sol"; import {DeploymentUtils} from "@level/script/v2/DeploymentUtils.s.sol"; import {Configurable} from "@level/config/Configurable.sol"; @@ -66,8 +67,8 @@ contract DeployOracles is Configurable, DeploymentUtils, Script { console2.log("sUsdcOracle deployed to: %s", address(config.sparkVaults.sUsdc.oracle)); // Deploy waUsdcStakeTokenOracle - config.umbrellaVaults.waUsdcStakeToken.oracle = - deployERC4626Oracle(config.umbrellaVaults.waUsdcStakeToken.vault); + AaveUmbrellaOracle oracle = new AaveUmbrellaOracle(config.umbrellaVaults.waUsdcStakeToken.vault); + config.umbrellaVaults.waUsdcStakeToken.oracle = IERC4626Oracle(address(oracle)); console2.log("waUsdcStakeTokenOracle deployed to: %s", address(config.umbrellaVaults.waUsdcStakeToken.oracle)); vm.stopBroadcast(); diff --git a/src/v2/common/libraries/VaultLib.sol b/src/v2/common/libraries/VaultLib.sol index 97a767d..2722e62 100644 --- a/src/v2/common/libraries/VaultLib.sol +++ b/src/v2/common/libraries/VaultLib.sol @@ -145,23 +145,30 @@ library VaultLib { uint256 strategyBalance; uint256 remainingAmount = amount; withdrawn = 0; + uint256 i = 0; - for (uint256 i; i < strategies.length; i++) { + while (i < strategies.length && remainingAmount > 0) { StrategyConfig memory config = strategies[i]; strategyBalance = StrategyLib.getAssets(config, address(vault)); if (strategyBalance == 0) { + i++; continue; } if (remainingAmount > strategyBalance) { withdrawn += _withdraw(vault, config, strategyBalance); - remainingAmount -= strategyBalance; } else { withdrawn += _withdraw(vault, config, remainingAmount); + } - break; + if (withdrawn > amount) { + remainingAmount = 0; + } else { + remainingAmount = amount - withdrawn; } + + i++; } return withdrawn; @@ -341,6 +348,11 @@ library VaultLib { } /// @notice Deposits assets into Spark + /// + /// @dev In the future, Spark may charge fees on deposits. + /// Depositing into spark inclues PSM fees for converting between USDC, DAI, and USDS + /// These are all currently set to 0, but may change in the future. + /// /// @param vault The vault address /// @param _config The strategy config /// @param amount The amount of assets to deposit @@ -365,6 +377,11 @@ library VaultLib { } /// @notice Withdraws assets from Spark + /// + /// @dev In the future, Spark may charge fees on withdrawals. + /// Withdrawing from spark includes PSM fees for converting between USDC, DAI, and USDS + /// These are all currently set to 0, but may change in the future. + /// /// @param vault The vault address /// @param _config The strategy config /// @param amount The amount of assets to withdraw @@ -529,7 +546,7 @@ library VaultLib { uint256 afterBal = ERC20(baseCollateral).balanceOf(address(vault)); withdrawn = afterBal - before; - emit WithdrawFromM0(address(vault), baseCollateral, amount, withdrawn); + emit WithdrawFromM0(address(vault), baseCollateral, withdrawn, amount); return withdrawn; } @@ -543,25 +560,27 @@ library VaultLib { internal returns (uint256 staked) { + // config.baseCollateral is the USDC/USDT token + // config.depositContract is the Aave Umbrella contract (stwaToken) + // config.receiptToken is also stwaToken IERC4626StakeToken stakeToken = IERC4626StakeToken(_config.depositContract); + IERC4626 stataToken = IERC4626(stakeToken.asset()); - // Wrap the aTokens - IERC4626StataToken stataToken = IERC4626StataToken(stakeToken.asset()); + // Convert Token to waToken vault.setTokenAllowance(address(_config.baseCollateral), address(stataToken), amount); bytes memory sharesRaw = vault.manage( - address(stataToken), abi.encodeWithSignature("depositATokens(uint256,address)", amount, address(vault)), 0 + address(stataToken), abi.encodeWithSignature("deposit(uint256,address)", amount, address(vault)), 0 ); - uint256 shares = abi.decode(sharesRaw, (uint256)); + + uint256 waTokenBalance = abi.decode(sharesRaw, (uint256)); // waToken // Stake waTokens with Aave Umbrella - vault.setTokenAllowance(address(stataToken), address(_config.depositContract), shares); + vault.setTokenAllowance(address(stataToken), address(stakeToken), waTokenBalance); bytes memory stakedRaw = vault.manage( - address(_config.depositContract), - abi.encodeWithSignature("deposit(uint256,address)", shares, address(vault)), - 0 + address(stakeToken), abi.encodeWithSignature("deposit(uint256,address)", waTokenBalance, address(vault)), 0 ); - uint256 staked_ = abi.decode(stakedRaw, (uint256)); + uint256 staked_ = abi.decode(stakedRaw, (uint256)); // st-waToken emit StakeToAaveUmbrella(address(vault), address(_config.baseCollateral), amount, staked_); @@ -571,7 +590,7 @@ library VaultLib { /// @notice Unstakes waTokens from Aave Umbrella /// @param vault The vault address /// @param _config The strategy config - /// @param amount The amount of assets to unstake + /// @param amount The amount of assets to unstake (USDC/USDT) /// @return unstaked The amount of assets unstaked function _unstakeFromAaveUmbrella(BoringVault vault, StrategyConfig memory _config, uint256 amount) internal @@ -579,39 +598,47 @@ library VaultLib { { // Get cooldown snapshot IERC4626StakeToken stakeToken = IERC4626StakeToken(_config.depositContract); - IERC4626StataToken stataToken = IERC4626StataToken(stakeToken.asset()); + IERC4626 stataToken = IERC4626(stakeToken.asset()); // waToken + // cooldownSnapshot.amount is st-waToken IERC4626StakeToken.CooldownSnapshot memory cooldownSnapshot = stakeToken.getStakerCooldown(address(vault)); if ( - block.timestamp > cooldownSnapshot.endOfCooldown + block.timestamp >= cooldownSnapshot.endOfCooldown && block.timestamp - cooldownSnapshot.endOfCooldown <= cooldownSnapshot.withdrawalWindow ) { - if (amount > cooldownSnapshot.amount) { - amount = cooldownSnapshot.amount; + // How much waToken needs to be withdrawn to get the given amount of USDC/USDT + uint256 waTokenAmount = stataToken.previewWithdraw(amount); + + // Check cooldown limits + uint256 maxWithdrawal = stakeToken.maxWithdraw(address(vault)); + if (waTokenAmount > maxWithdrawal) { + waTokenAmount = maxWithdrawal; } // We're in the withdrawal window bytes memory unstakedRaw = vault.manage( address(stakeToken), - abi.encodeWithSignature("redeem(uint256,address,address)", amount, address(vault), address(vault)), + abi.encodeWithSignature( + "withdraw(uint256,address,address)", waTokenAmount, address(vault), address(vault) + ), 0 ); uint256 wrappedATokens = abi.decode(unstakedRaw, (uint256)); - bytes memory aTokensRaw = vault.manage( + bytes memory tokensRaw = vault.manage( address(stataToken), abi.encodeWithSignature( - "redeemATokens(uint256,address,address)", wrappedATokens, address(vault), address(vault) + "redeem(uint256,address,address)", wrappedATokens, address(vault), address(vault) ), 0 ); - uint256 aTokens = abi.decode(aTokensRaw, (uint256)); + uint256 tokens = abi.decode(tokensRaw, (uint256)); - emit UnstakeFromAaveUmbrella(address(vault), address(_config.baseCollateral), amount, aTokens); + emit UnstakeFromAaveUmbrella(address(vault), address(_config.baseCollateral), amount, tokens); - return aTokens; + return tokens; } else { // We're not in the withdrawal window, need to call cooldown revert("VaultManager: not in withdrawal window, call cooldown first"); diff --git a/src/v2/oracles/AaveUmbrellaOracle.sol b/src/v2/oracles/AaveUmbrellaOracle.sol new file mode 100644 index 0000000..431c966 --- /dev/null +++ b/src/v2/oracles/AaveUmbrellaOracle.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.20; + +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {IERC4626Oracle} from "@level/src/v2/interfaces/level/IERC4626Oracle.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +/** + * .-==+=======+: + * :---=-::-==: + * .-:-==-:-==: + * .:::--::::::. .--:-=--:--. .:--:::--.. + * .=++=++:::::.. .:::---::--. ....::...:::. + * :::-::..::.. .::::-:::::. ...::...:::. + * ...::..::::.. .::::--::-:. ....::...:::.. + * ............ ....:::..::. ------:...... + * ........... ........:.... .....::..:.. ======-...... ........... + * :------:.:... ...:+***++*#+ .------:---. ...::::.:::... .....:-----::. + * .::::::::-:.. .::--..:-::.. .-=+===++=-==: ...:::..:--:.. .:==+=++++++*: + * + * @title AaveUmbrellaOracle + * @author Level (https://level.money) + * @notice Oracle contract for Aave Umbrella aToken vaults. + * @notice Returns the price of a staked-wrapped-aToken in terms of the underlying token + */ +contract AaveUmbrellaOracle is IERC4626Oracle { + IERC4626 public immutable stakedWrappedVault; + + uint8 public immutable decimals_; + uint256 public immutable oneShare; + + constructor(IERC4626 _stakedWrappedVault) { + stakedWrappedVault = _stakedWrappedVault; + IERC4626 wrappedAaveToken = IERC4626(stakedWrappedVault.asset()); // waToken + + decimals_ = IERC20Metadata(wrappedAaveToken.asset()).decimals(); // decimals of underlying Token + oneShare = 10 ** stakedWrappedVault.decimals(); + } + + function update() external {} + + function decimals() external view returns (uint8) { + return decimals_; + } + + function description() external pure returns (string memory) { + return "Chainlink-compliant Aave Umbrella Oracle"; + } + + function version() external pure returns (uint256) { + return 1; + } + + function getRoundData(uint80 /*_roundId */ ) external view returns (uint80, int256, uint256, uint256, uint80) { + return this.latestRoundData(); + } + + function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) { + uint256 amountOfWrappedATokens = stakedWrappedVault.convertToAssets(oneShare); // 1 st-waToken to waToken + uint256 amountOfUnderlyingTokens = IERC4626(stakedWrappedVault.asset()).convertToAssets(amountOfWrappedATokens); // waToken to underlying Token + return (0, int256(amountOfUnderlyingTokens), block.timestamp, block.timestamp, 0); + } +} diff --git a/src/v2/oracles/CappedOneDollarOracle.sol b/src/v2/oracles/CappedOneDollarOracle.sol index 0ff0b39..01a4482 100644 --- a/src/v2/oracles/CappedOneDollarOracle.sol +++ b/src/v2/oracles/CappedOneDollarOracle.sol @@ -22,11 +22,15 @@ import {AggregatorV3Interface} from "@level/src/v2/interfaces/AggregatorV3Interf * @notice Oracle that returns the lower of 1 dollar or the price from another oracle */ contract CappedOneDollarOracle is AggregatorV3Interface { - uint8 public constant override decimals = 8; + error InvalidExternalOracle(); + + uint8 public immutable decimals; AggregatorV3Interface public immutable externalOracle; constructor(address _externalOracle) { + if (_externalOracle == address(0)) revert InvalidExternalOracle(); externalOracle = AggregatorV3Interface(_externalOracle); + decimals = externalOracle.decimals(); } function description() external pure override returns (string memory) { @@ -45,8 +49,8 @@ contract CappedOneDollarOracle is AggregatorV3Interface { { (roundId, answer, startedAt, updatedAt, answeredInRound) = externalOracle.getRoundData(_roundId); - // Cap the price at $1.00 (1e8) - int256 capped = answer < 1e8 ? answer : int256(1e8); + // Cap the price at $1.00 (10 ** decimals) + int256 capped = answer < int256(10 ** decimals) ? answer : int256(10 ** decimals); return (roundId, capped, startedAt, updatedAt, answeredInRound); } @@ -60,8 +64,8 @@ contract CappedOneDollarOracle is AggregatorV3Interface { (uint80 extRoundId, int256 extAnswer, uint256 extStartedAt, uint256 extUpdatedAt, uint80 extAnsweredInRound) = externalOracle.latestRoundData(); - // Cap the price at $1.00 (1e8) - int256 capped = extAnswer < 1e8 ? extAnswer : int256(1e8); + // Cap the price at $1.00 (10 ** decimals) + int256 capped = extAnswer < int256(10 ** decimals) ? extAnswer : int256(10 ** decimals); return (extRoundId, capped, extStartedAt, extUpdatedAt, extAnsweredInRound); } diff --git a/src/v2/usd/SwapManager.sol b/src/v2/usd/SwapManager.sol index 7653df9..d2dd1b1 100644 --- a/src/v2/usd/SwapManager.sol +++ b/src/v2/usd/SwapManager.sol @@ -91,7 +91,7 @@ contract SwapManager is SwapManagerStorage, Initializable, UUPSUpgradeable, Auth IERC20(tokenIn).approve(address(swapRouter), amountIn); // Calculate slippage min out - uint256 minOut = (amountIn * (10_000 - config.slippageBps)) / 10_000; + uint256 minOut = (amountIn * (10_000 - config.slippageBps) + 10_000 - 1) / 10_000; // Round up // Build params ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ @@ -99,7 +99,7 @@ contract SwapManager is SwapManagerStorage, Initializable, UUPSUpgradeable, Auth tokenOut: tokenOut, fee: config.fee, recipient: recipient, - deadline: block.timestamp + 60, + deadline: block.timestamp, amountIn: amountIn, amountOutMinimum: minOut, sqrtPriceLimitX96: 0 diff --git a/test/v2/integration/RewardsManager.t.sol b/test/v2/integration/RewardsManager.t.sol index 7013057..11597ad 100644 --- a/test/v2/integration/RewardsManager.t.sol +++ b/test/v2/integration/RewardsManager.t.sol @@ -11,6 +11,7 @@ import {VaultManager} from "@level/src/v2/usd/VaultManager.sol"; import {RewardsManager} from "@level/src/v2/usd/RewardsManager.sol"; import {SafeTransferLib} from "@solmate/src/utils/SafeTransferLib.sol"; import {ERC4626} from "@solmate/src/tokens/ERC4626.sol"; +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; import {MathLib} from "@level/src/v2/common/libraries/MathLib.sol"; import {StrategyConfig, StrategyLib, StrategyCategory} from "@level/src/v2/common/libraries/StrategyLib.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -26,6 +27,7 @@ import {lvlUSD} from "@level/src/v1/lvlUSD.sol"; import {CappedOneDollarOracle} from "@level/src/v2/oracles/CappedOneDollarOracle.sol"; import {ISuperstateToken} from "@level/src/v2/interfaces/superstate/ISuperstateToken.sol"; import {IAllowListV2} from "@level/src/v2/interfaces/superstate/IAllowListV2.sol"; +import {AaveUmbrellaOracle} from "@level/src/v2/oracles/AaveUmbrellaOracle.sol"; contract RewardsManagerMainnetTests is Utils, Configurable { using SafeTransferLib for ERC20; @@ -47,22 +49,25 @@ contract RewardsManagerMainnetTests is Utils, Configurable { address[] public assets; function setUp() public { - forkMainnet(22305203); + forkMainnet(22664895); deployer = vm.createWallet("deployer"); strategist = vm.createWallet("strategist"); - DeployLevel deployScript = new DeployLevel(); - - // Deploy - - vm.prank(deployer.addr); - deployScript.setUp_(1, deployer.privateKey); - - config = deployScript.run(); + initConfig(1); + _upgradeRewardsManager(); + _upgradeVaultManager(); mockOracle = new MockOracle(1e8, 8); + // Since we are using a fork, the vault has exisiting balances of various strategies + // The tests are designed according to zero initial balances + _resetTokenBalance(config.tokens.aUsdc, address(config.levelContracts.boringVault)); + _resetTokenBalance(config.tokens.aUsdt, address(config.levelContracts.boringVault)); + _resetTokenBalance( + ERC20(address(config.morphoVaults.steakhouseUsdc.vault)), address(config.levelContracts.boringVault) + ); + // Setup strategist address[] memory targets = new address[](4); targets[0] = address(config.levelContracts.rolesAuthority); @@ -299,21 +304,124 @@ contract RewardsManagerMainnetTests is Utils, Configurable { vm.startPrank(strategist.addr); uint256 accrued = 1000e6; config.tokens.aUsdc.transfer(address(rewardsManager.vault()), accrued); - console2.log("Transferring aUSDC to vault"); // Call getAccruedYield - should not revert uint256 yield = rewardsManager.getAccruedYield(assets); assertGt(yield, 0, "Should have accrued some yield"); } + function test_rewardYield_aaveUmbrellaYield_succeeds(uint256 deposit) public { + deposit = bound(deposit, 1e6, 500_000e6); + deal(address(config.tokens.usdc), address(strategist.addr), deposit); + + // Set Umbrella as a default strategy + address[] memory defaultStrategies = new address[](1); + defaultStrategies[0] = address(config.umbrellaVaults.waUsdcStakeToken.vault); + + // Set oracle + if (address(config.umbrellaVaults.waUsdcStakeToken.oracle) == address(0)) { + AaveUmbrellaOracle oracle = new AaveUmbrellaOracle(config.umbrellaVaults.waUsdcStakeToken.vault); + config.umbrellaVaults.waUsdcStakeToken.oracle = IERC4626Oracle(address(oracle)); + } + + StrategyConfig[] memory strategies = new StrategyConfig[](1); + strategies[0] = StrategyConfig({ + category: StrategyCategory.AAVEV3_UMBRELLA, + baseCollateral: config.tokens.usdc, + receiptToken: ERC20(address(config.umbrellaVaults.waUsdcStakeToken.vault)), + oracle: config.umbrellaVaults.waUsdcStakeToken.oracle, + depositContract: address(config.umbrellaVaults.waUsdcStakeToken.vault), + withdrawContract: address(config.umbrellaVaults.waUsdcStakeToken.vault), + heartbeat: 1 days + }); + + address[] memory targets = new address[](4); + targets[0] = address(config.levelContracts.vaultManager); + targets[1] = address(config.levelContracts.vaultManager); + targets[2] = address(config.levelContracts.rewardsManager); + targets[3] = address(config.levelContracts.levelMintingV2); + + bytes[] memory payloads = new bytes[](4); + // VaultManager.addAssetStrategy + payloads[0] = abi.encodeWithSelector( + VaultManager.addAssetStrategy.selector, + address(config.tokens.usdc), + address(config.umbrellaVaults.waUsdcStakeToken.vault), + strategies[0] + ); + // VaultManager.setDefaultStrategies + payloads[1] = abi.encodeWithSignature( + "setDefaultStrategies(address,address[])", address(config.tokens.usdc), defaultStrategies + ); + // RewardsManager.setAllStrategies + payloads[2] = + abi.encodeWithSelector(RewardsManager.setAllStrategies.selector, address(config.tokens.usdc), strategies); + payloads[3] = abi.encodeWithSignature( + "addOracle(address,address,bool)", address(config.tokens.usdc), address(mockOracle), false + ); + + _scheduleAndExecuteAdminActionBatch( + address(config.users.admin), address(config.levelContracts.adminTimelock), targets, payloads + ); + + vm.startPrank(strategist.addr); + + // Approve USDC + ERC20(address(config.tokens.usdc)).safeApprove(address(config.levelContracts.boringVault), type(uint256).max); + + uint256 vaultSharesBefore = vaultManager.vault().balanceOf(address(vaultManager.vault())); + + // Use levelMinting to mint + config.levelContracts.levelMintingV2.mint( + ILevelMintingV2Structs.Order({ + collateral_asset: address(config.tokens.usdc), + collateral_amount: deposit, + min_lvlusd_amount: 0, + beneficiary: address(strategist.addr) + }) + ); + + uint256 vaultSharesAfter = vaultManager.vault().balanceOf(address(vaultManager.vault())); + + // Expected waUsdc balance after depositing aUsdc + // This will also be the stk-waUsdc balance as waUsdc and stk-waUsdc are 1:1 + uint256 expectedWrappedBalance = + IERC4626(config.umbrellaVaults.waUsdcStakeToken.vault.asset()).previewDeposit(deposit); + + // Ensure that the vault has the correct amount of assets + assertApproxEqAbs( + config.umbrellaVaults.waUsdcStakeToken.vault.balanceOf(address(vaultManager.vault())), + expectedWrappedBalance, + 1, + "Vault should have the correct amount of assets" + ); + + assertApproxEqRel( + vaultSharesAfter - vaultSharesBefore, + deposit.convertDecimalsDown(ERC20(address(config.tokens.usdc)).decimals(), vaultManager.vault().decimals()), + 0.0001e18, + "Vault shares do not match" + ); + + // Get assets in strategy + uint256 assetsInUmbrella = + _getAssetsInStrategy(address(config.tokens.usdc), address(config.umbrellaVaults.waUsdcStakeToken.vault)); + + assertApproxEqRel(assetsInUmbrella, deposit, 0.0001e18, "Assets in strategy do not match"); + + vm.warp(block.timestamp + 100 days); + + // Get Accrued yield + uint256 accruedYield = rewardsManager.getAccruedYield(assets).convertDecimalsDown( + vaultManager.vault().decimals(), ERC20(assets[0]).decimals() + ); + assertGt(accruedYield, 0, "Accrued yield should be greater than 0"); + } + function test_rewardYield_morphoYield_succeeds2(uint256 deposit) public { - deposit = bound(deposit, 2000, 500000e6); + deposit = bound(deposit, 2000, 500_000e6); deal(address(config.tokens.usdc), address(strategist.addr), deposit); - vm.startPrank(config.users.admin); - lvlUSD _lvlUSD = lvlUSD(address(config.tokens.lvlUsd)); - _lvlUSD.setMinter(address(config.levelContracts.levelMintingV2)); - vm.stopPrank(); uint256 treasuryUsdcBalanceBefore = config.tokens.usdc.balanceOf(config.users.protocolTreasury); // Create a mock vault to simulate Morpho and manipulate yield @@ -442,9 +550,66 @@ contract RewardsManagerMainnetTests is Utils, Configurable { assertEq(price, 99e6, "Price should be 0.99 USD"); } + function test_cappedOneDollarOracle_withCustomDecimals_succeeds() public { + AggregatorV3Interface mockOracle = new MockOracle(1e10, 10); + CappedOneDollarOracle oracle = new CappedOneDollarOracle(address(mockOracle)); + + (, int256 price,,,) = oracle.latestRoundData(); + assertEq(price, 1e10, "Price should be 1 USD"); + + _mockChainlinkCall(address(mockOracle), 105e8); // 1.05 USD + + (, price,,,) = oracle.latestRoundData(); + assertEq(price, 1e10, "Price should be 1 USD"); + + _mockChainlinkCall(address(mockOracle), 99e8); // 0.99 USD + + (, price,,,) = oracle.latestRoundData(); + assertEq(price, 99e8, "Price should be 0.99 USD"); + } + + function test_aaveUmbrellaOracle_succeeds() public { + AaveUmbrellaOracle oracle = new AaveUmbrellaOracle(config.umbrellaVaults.waUsdcStakeToken.vault); + (, int256 price,,,) = oracle.latestRoundData(); // Price of 1 stwaToken in USD + assertGt(price, 1e6, "Price should be greater than 1 USD"); + } + function test_sparkYield_succeeds(uint256 deposit) public { deposit = bound(deposit, 1000, 1_000_000e6); + if (address(config.sparkVaults.sUsdc.oracle) == address(0)) { + config.sparkVaults.sUsdc.oracle = deployERC4626Oracle(config.sparkVaults.sUsdc.vault, 4 hours); + } + + // Add spark as a strategy + StrategyConfig[] memory strategies = new StrategyConfig[](1); + strategies[0] = StrategyConfig({ + category: StrategyCategory.SPARK, + baseCollateral: config.tokens.usdc, + receiptToken: ERC20(address(config.sparkVaults.sUsdc.vault)), + oracle: config.sparkVaults.sUsdc.oracle, + depositContract: address(config.sparkVaults.sUsdc.vault), + withdrawContract: address(config.sparkVaults.sUsdc.vault), + heartbeat: 1 days + }); + + address[] memory targets = new address[](2); + targets[0] = address(config.levelContracts.vaultManager); + targets[1] = address(config.levelContracts.rewardsManager); + + bytes[] memory payloads = new bytes[](2); + payloads[0] = abi.encodeWithSelector( + VaultManager.addAssetStrategy.selector, + address(config.tokens.usdc), + address(config.sparkVaults.sUsdc.vault), + strategies[0] + ); + payloads[1] = + abi.encodeWithSelector(RewardsManager.setAllStrategies.selector, address(config.tokens.usdc), strategies); + _scheduleAndExecuteAdminActionBatch( + address(config.users.admin), address(config.levelContracts.adminTimelock), targets, payloads + ); + // Deposit some USDC into the spark vault vm.prank(strategist.addr); vaultManager.deposit(address(config.tokens.usdc), address(config.sparkVaults.sUsdc.vault), deposit); @@ -464,10 +629,6 @@ contract RewardsManagerMainnetTests is Utils, Configurable { // Travel to the future to get yield vm.warp(block.timestamp + 10 days); - // Avoid stale prices - _mockChainlinkCall(address(config.oracles.ustb), 105e5); // 10.5 USD per USTB - _mockChainlinkCall(address(config.oracles.mNav), 1e8); // 1 USD per wrappedM - // Get the accrued yield in the redemption asset's decimals accruedYield = rewardsManager.getAccruedYield(assets); @@ -494,6 +655,35 @@ contract RewardsManagerMainnetTests is Utils, Configurable { function test_ustbYield_succeeds(uint256 deposit) public { deposit = bound(deposit, 1000, 1_000_000e6); + // Add ustb as a strategy + StrategyConfig[] memory strategies = new StrategyConfig[](1); + strategies[0] = StrategyConfig({ + category: StrategyCategory.SUPERSTATE, + baseCollateral: config.tokens.usdc, + receiptToken: config.tokens.ustb, + oracle: config.oracles.ustb, + depositContract: address(config.tokens.ustb), + withdrawContract: address(config.periphery.ustbRedemptionIdle), + heartbeat: 1 days + }); + + address[] memory targets = new address[](2); + targets[0] = address(config.levelContracts.vaultManager); + targets[1] = address(config.levelContracts.rewardsManager); + + bytes[] memory payloads = new bytes[](2); + payloads[0] = abi.encodeWithSelector( + VaultManager.addAssetStrategy.selector, + address(config.tokens.usdc), + address(config.tokens.ustb), + strategies[0] + ); + payloads[1] = + abi.encodeWithSelector(RewardsManager.setAllStrategies.selector, address(config.tokens.usdc), strategies); + _scheduleAndExecuteAdminActionBatch( + address(config.users.admin), address(config.levelContracts.adminTimelock), targets, payloads + ); + _mockChainlinkCall(USTB_CHAINLINK_FEED, 105e5); // 10.5 USD per USTB (uint256 superstateTokenOutAmount,,) = ISuperstateToken(address(config.tokens.ustb)).calculateSuperstateTokenOut( @@ -617,4 +807,40 @@ contract RewardsManagerMainnetTests is Utils, Configurable { function _printBalance(address asset, address vault) internal { console2.log(vm.getLabel(asset), ERC20(asset).balanceOf(vault)); } + + function deployERC4626Oracle(IERC4626 vault, uint256 delay) public returns (IERC4626Oracle) { + if (address(config.levelContracts.erc4626OracleFactory) == address(0)) { + revert("ERC4626OracleFactory must be deployed first"); + } + + IERC4626Oracle _erc4626Oracle = IERC4626Oracle(config.levelContracts.erc4626OracleFactory.create(vault)); + vm.label(address(_erc4626Oracle), string.concat(vault.name(), " Oracle")); + + return _erc4626Oracle; + } + + function _upgradeRewardsManager() internal { + RewardsManager impl = new RewardsManager(); + vm.prank(address(config.levelContracts.adminTimelock)); + config.levelContracts.rewardsManager.upgradeToAndCall(address(impl), ""); + } + + function _upgradeVaultManager() internal { + VaultManager impl = new VaultManager(); + vm.prank(address(config.levelContracts.adminTimelock)); + config.levelContracts.vaultManager.upgradeToAndCall(address(impl), ""); + } + + function _resetTokenBalance(ERC20 token, address account) internal { + uint256 balance = token.balanceOf(account); + if (balance == 0) { + return; + } + + // In case of tokens like aUsdc, we cannot use deal() to reset the balance + + vm.prank(account); + token.transfer(strategist.addr, balance); + return; + } } diff --git a/test/v2/integration/VaultManager.t.sol b/test/v2/integration/VaultManager.t.sol index 9205db5..07f126e 100644 --- a/test/v2/integration/VaultManager.t.sol +++ b/test/v2/integration/VaultManager.t.sol @@ -24,6 +24,7 @@ import {DeploySwapManager} from "@level/script/v2/usd/DeploySwapManager.s.sol"; import {IERC4626StataToken} from "@level/src/v2/interfaces/aave/IERC4626StataToken.sol"; import {IERC4626StakeToken} from "@level/src/v2/interfaces/aave/IERC4626StakeToken.sol"; import {CappedOneDollarOracle} from "@level/src/v2/oracles/CappedOneDollarOracle.sol"; +import {AaveUmbrellaOracle} from "@level/src/v2/oracles/AaveUmbrellaOracle.sol"; contract VaultManagerMainnetTests is Utils, Configurable { using SafeTransferLib for ERC20; @@ -242,8 +243,8 @@ contract VaultManagerMainnetTests is Utils, Configurable { } if (address(config.umbrellaVaults.waUsdcStakeToken.oracle) == address(0)) { - config.umbrellaVaults.waUsdcStakeToken.oracle = - deployERC4626Oracle(config.umbrellaVaults.waUsdcStakeToken.vault, 4 hours); + AaveUmbrellaOracle oracle = new AaveUmbrellaOracle(config.umbrellaVaults.waUsdcStakeToken.vault); + config.umbrellaVaults.waUsdcStakeToken.oracle = IERC4626Oracle(address(oracle)); } //--------------- Add test Morpho vaults as strategies @@ -299,7 +300,7 @@ contract VaultManagerMainnetTests is Utils, Configurable { umbrellaConfig = StrategyConfig({ category: StrategyCategory.AAVEV3_UMBRELLA, - baseCollateral: config.tokens.aUsdc, + baseCollateral: config.tokens.usdc, receiptToken: ERC20(address(config.umbrellaVaults.waUsdcStakeToken.vault)), oracle: config.umbrellaVaults.waUsdcStakeToken.oracle, depositContract: address(config.umbrellaVaults.waUsdcStakeToken.vault), @@ -634,17 +635,16 @@ contract VaultManagerMainnetTests is Utils, Configurable { deposit = bound(deposit, 1e3, INITIAL_BALANCE); vm.startPrank(strategist.addr); - // Need to get some aUsdc into the vault - config.tokens.aUsdc.transfer(address(config.levelContracts.boringVault), deposit); - - // Deposit aUsdc into the vault + // Deposit USDC into the vault vaultManager.deposit( address(config.tokens.usdc), address(config.umbrellaVaults.waUsdcStakeToken.vault), deposit ); + // Expected waUsdc balance after depositing aUsdc uint256 expectedWrappedBalance = IERC4626(config.umbrellaVaults.waUsdcStakeToken.vault.asset()).previewDeposit(deposit); + // Expected stk-waToken balance after depositing waUsdc uint256 expectedStakedBalance = config.umbrellaVaults.waUsdcStakeToken.vault.previewDeposit(expectedWrappedBalance); @@ -656,22 +656,25 @@ contract VaultManagerMainnetTests is Utils, Configurable { assertApproxEqAbs(balance, expectedStakedBalance, 1, "Wrong amount of stk-waToken"); } - function test_wrappingOfaUsdcToWaUsdc_succeeds(uint256 deposit) public { + function test_wrappingOfUsdcToWaUsdc_succeeds(uint256 deposit) public { deposit = bound(deposit, 1e3, INITIAL_BALANCE); + deal(address(config.tokens.usdc), strategist.addr, deposit); vm.startPrank(strategist.addr); - uint256 aUsdcBalance = config.tokens.aUsdc.balanceOf(strategist.addr); - console2.log("aUsdcBalance", aUsdcBalance); - - IERC4626StataToken stataToken = IERC4626StataToken(config.umbrellaVaults.waUsdcStakeToken.vault.asset()); + IERC4626 stataToken = IERC4626(config.umbrellaVaults.waUsdcStakeToken.vault.asset()); uint256 expectedWrappedBalance = IERC4626(address(stataToken)).previewDeposit(deposit); - config.tokens.aUsdc.approve(address(stataToken), deposit); - stataToken.depositATokens(deposit, strategist.addr); + config.tokens.usdc.approve(address(stataToken), deposit); + stataToken.deposit(deposit, strategist.addr); // Check we received the correct amount of waUsdc assertEq(IERC4626(address(stataToken)).balanceOf(strategist.addr), expectedWrappedBalance); + assertLt( + IERC4626(address(stataToken)).balanceOf(strategist.addr), + deposit, + "waUsdc balance should be less than deposit" + ); vm.stopPrank(); } @@ -680,10 +683,7 @@ contract VaultManagerMainnetTests is Utils, Configurable { deposit = bound(deposit, 1e3, INITIAL_BALANCE); vm.startPrank(strategist.addr); - // Need to get some aUsdc into the vault - config.tokens.aUsdc.transfer(address(config.levelContracts.boringVault), deposit); - - // Deposit aUsdc into the vault + // Deposit USDC into the vault vaultManager.deposit( address(config.tokens.usdc), address(config.umbrellaVaults.waUsdcStakeToken.vault), deposit ); @@ -703,13 +703,17 @@ contract VaultManagerMainnetTests is Utils, Configurable { vm.startPrank(strategist.addr); // Withdraw from the vault - vaultManager.withdraw( + uint256 withdrawn = vaultManager.withdraw( address(config.tokens.usdc), address(config.umbrellaVaults.waUsdcStakeToken.vault), deposit ); - // Check we received the correct amount of aUsdc - assertGe( - config.tokens.aUsdc.balanceOf(address(config.levelContracts.boringVault)), deposit, "Wrong amount of aUsdc" + assertApproxEqAbs(withdrawn, deposit, 1, "Wrong amount of withdrawn"); + + assertApproxEqAbs( + config.tokens.usdc.balanceOf(address(config.levelContracts.boringVault)), + INITIAL_BALANCE, + 1, + "Wrong amount of usdc after withdrawal" ); }