From 8e9e20c6e26124c50007fd16ff295f7a521d124a Mon Sep 17 00:00:00 2001 From: Atharva Date: Thu, 19 Jun 2025 20:28:22 +0530 Subject: [PATCH] #989: Changes befofe audit kickoff (57feb8d) --- lib/v3-core | 2 +- script/v2/DeployLevel.s.sol | 19 ++-- script/v2/DeployOracles.sol | 90 +++++++++++++++ script/v2/lens/UpgradeLevelReserveLens.s.sol | 55 +++++---- script/v2/usd/DeploySwapManager.s.sol | 13 ++- script/v2/usd/UpgradeRewardsManager.s.sol | 29 +---- script/v2/usd/UpgradeVaultManager.s.sol | 113 +------------------ src/v2/usd/SwapManager.sol | 14 ++- test/v2/integration/RewardsManager.t.sol | 103 +++++++++++++++++ test/v2/integration/VaultManager.t.sol | 87 ++++++++++++-- 10 files changed, 343 insertions(+), 182 deletions(-) create mode 100644 script/v2/DeployOracles.sol diff --git a/lib/v3-core b/lib/v3-core index d8b1c63..e3589b1 160000 --- a/lib/v3-core +++ b/lib/v3-core @@ -1 +1 @@ -Subproject commit d8b1c635c275d2a9450bd6a78f3fa2484fef73eb +Subproject commit e3589b192d0be27e100cd0daaf6c97204fdb1899 diff --git a/script/v2/DeployLevel.s.sol b/script/v2/DeployLevel.s.sol index 5348cb8..0d6dae5 100644 --- a/script/v2/DeployLevel.s.sol +++ b/script/v2/DeployLevel.s.sol @@ -302,16 +302,6 @@ contract DeployLevel is Configurable, DeploymentUtils, Script { address(config.levelContracts.vaultManager), bytes4(abi.encodeWithSignature("withdrawDefault(address,uint256)")) ); - _setRoleCapabilityIfNotExists( - STRATEGIST_ROLE, - address(config.levelContracts.vaultManager), - bytes4(abi.encodeWithSignature("modifyAaveUmbrellaCooldownOperator(address,address,bool)")) - ); - _setRoleCapabilityIfNotExists( - STRATEGIST_ROLE, - address(config.levelContracts.vaultManager), - bytes4(abi.encodeWithSignature("modifyAaveUmbrellaRewardsClaimer(address,address,bool)")) - ); _setRoleIfNotExists(address(config.levelContracts.levelMintingV2), STRATEGIST_ROLE); _setRoleIfNotExists(address(config.users.operator), STRATEGIST_ROLE); @@ -600,8 +590,15 @@ contract DeployLevel is Configurable, DeploymentUtils, Script { revert("RolesAuthority must be deployed first"); } + if (address(config.levelContracts.pauserGuard) == address(0)) { + revert("PauserGuard must be deployed first"); + } + bytes memory constructorArgs = abi.encodeWithSignature( - "initialize(address,address)", deployerWallet.addr, address(config.periphery.uniswapV3Router) + "initialize(address,address,address)", + deployerWallet.addr, + address(config.periphery.uniswapV3Router), + address(config.levelContracts.pauserGuard) ); SwapManager _swapManager = new SwapManager{salt: convertNameToBytes32(LevelUsdSwapManagerName)}(); diff --git a/script/v2/DeployOracles.sol b/script/v2/DeployOracles.sol new file mode 100644 index 0000000..17a7957 --- /dev/null +++ b/script/v2/DeployOracles.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.19; + +import {Script} from "forge-std/Script.sol"; +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 {DeploymentUtils} from "@level/script/v2/DeploymentUtils.s.sol"; +import {Configurable} from "@level/config/Configurable.sol"; + +import {console2} from "forge-std/console2.sol"; + +/// @notice Script used to deploy new oracles required after v2 changes +/// @notice This script is meant to be used for mainnet deployments. Not a testing script. +contract DeployOracles is Configurable, DeploymentUtils, Script { + uint256 public chainId; + + Vm.Wallet public deployerWallet; + + error InvalidProxyAddress(); + error UpgradeFailed(); + error VerificationFailed(); + + function setUp() external { + uint256 _chainId = vm.envUint("CHAIN_ID"); + setUp_(_chainId); + } + + function setUp_(uint256 _chainId) public { + chainId = _chainId; + initConfig(_chainId); + + vm.label(msg.sender, "Deployer EOA"); + } + + function setUp_(uint256 _chainId, uint256 _privateKey) public { + chainId = _chainId; + initConfig(_chainId); + + if (msg.sender != vm.addr(_privateKey)) { + revert("Private key does not match sender"); + } + + deployerWallet.privateKey = _privateKey; + deployerWallet.addr = vm.addr(_privateKey); + + vm.label(msg.sender, "Deployer EOA"); + } + + function run() external { + return upgrade(); + } + + function upgrade() public { + vm.startBroadcast(deployerWallet.privateKey); + + // Deploy CappedMNavOracle + CappedOneDollarOracle mNavOracle = new CappedOneDollarOracle(address(config.oracles.mNav)); + vm.label(address(mNavOracle), "CappedMNavOracle"); + console2.log("CappedMNavOracle deployed to: %s", address(mNavOracle)); + + // Deploy sUsdcOracle + config.sparkVaults.sUsdc.oracle = deployERC4626Oracle(config.sparkVaults.sUsdc.vault); + console2.log("sUsdcOracle deployed to: %s", address(config.sparkVaults.sUsdc.oracle)); + + // Deploy waUsdcStakeTokenOracle + config.umbrellaVaults.waUsdcStakeToken.oracle = + deployERC4626Oracle(config.umbrellaVaults.waUsdcStakeToken.vault); + console2.log("waUsdcStakeTokenOracle deployed to: %s", address(config.umbrellaVaults.waUsdcStakeToken.oracle)); + + vm.stopBroadcast(); + } + + function verify() public view { + // TODO: Add verification logic here + } + + function deployERC4626Oracle(IERC4626 vault) 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; + } +} diff --git a/script/v2/lens/UpgradeLevelReserveLens.s.sol b/script/v2/lens/UpgradeLevelReserveLens.s.sol index 6c8ccdd..d92235d 100644 --- a/script/v2/lens/UpgradeLevelReserveLens.s.sol +++ b/script/v2/lens/UpgradeLevelReserveLens.s.sol @@ -73,26 +73,41 @@ contract UpgradeLevelReserveLens is Configurable, DeploymentUtils, Script { "LevelReserveLens Implementation : https://etherscan.io/address/%s", address(impl) ); - // Call timelock to upgrade the proxy - vm.startBroadcast(config.users.admin); - console2.log("Scheduling upgrade of LevelReserveLens from proxy %s", address(proxy)); - TimelockController timelock = TimelockController(payable(config.levelContracts.adminTimelock)); - timelock.schedule( - address(proxy), - 0, - abi.encodeWithSelector(proxy.upgradeToAndCall.selector, address(impl), ""), - bytes32(0), - 0, - 5 days - ); - - vm.warp(block.timestamp + 5 days); - - timelock.execute( - address(proxy), 0, abi.encodeWithSelector(proxy.upgradeToAndCall.selector, address(impl), ""), bytes32(0), 0 - ); - - vm.stopBroadcast(); + bool isTesting = vm.activeFork() != uint256(0); + + // Only execute timelock operations in test environment + if (isTesting) { + // Call timelock to upgrade the proxy + vm.startBroadcast(config.users.admin); + console2.log("Scheduling upgrade of LevelReserveLens from proxy %s", address(proxy)); + + TimelockController timelock = TimelockController(payable(config.levelContracts.adminTimelock)); + timelock.schedule( + address(proxy), + 0, + abi.encodeWithSelector(proxy.upgradeToAndCall.selector, address(impl), ""), + bytes32(0), + 0, + 5 days + ); + + vm.warp(block.timestamp + 5 days); + + timelock.execute( + address(proxy), + 0, + abi.encodeWithSelector(proxy.upgradeToAndCall.selector, address(impl), ""), + bytes32(0), + 0 + ); + + console2.log("=====> LevelReserveLens upgraded ...."); + console2.log( + "LevelReserveLens Proxy address : https://etherscan.io/address/%s", address(proxy) + ); + + vm.stopBroadcast(); + } // verify(impl); } diff --git a/script/v2/usd/DeploySwapManager.s.sol b/script/v2/usd/DeploySwapManager.s.sol index 0d6df9b..865921c 100644 --- a/script/v2/usd/DeploySwapManager.s.sol +++ b/script/v2/usd/DeploySwapManager.s.sol @@ -60,8 +60,19 @@ contract DeploySwapManager is Configurable, DeploymentUtils, Script { console2.log("Deploying SwapManager from address %s", deployerWallet.addr); + if (address(config.levelContracts.pauserGuard) == address(0)) { + revert("PauserGuard must be deployed first"); + } + + if (address(config.levelContracts.rolesAuthority) == address(0)) { + revert("RolesAuthority must be deployed first"); + } + bytes memory constructorArgs = abi.encodeWithSignature( - "initialize(address,address)", deployerWallet.addr, address(config.periphery.uniswapV3Router) + "initialize(address,address,address)", + deployerWallet.addr, + address(config.periphery.uniswapV3Router), + address(config.levelContracts.pauserGuard) ); SwapManager _swapManager = new SwapManager{salt: convertNameToBytes32(LevelUsdSwapManagerName)}(); diff --git a/script/v2/usd/UpgradeRewardsManager.s.sol b/script/v2/usd/UpgradeRewardsManager.s.sol index 2476f56..7ae5e60 100644 --- a/script/v2/usd/UpgradeRewardsManager.s.sol +++ b/script/v2/usd/UpgradeRewardsManager.s.sol @@ -68,32 +68,9 @@ contract UpgradeRewardsManager is Configurable, DeploymentUtils, Script { console2.log("=====> RewardsManager deployed ...."); console2.log("RewardsManager Implementation : https://etherscan.io/address/%s", address(impl)); - vm.startBroadcast(config.users.admin); - - console2.log("Upgrading RewardsManager from proxy %s", address(proxy)); - console2.log("New implementation: %s", address(impl)); - - try proxy.upgradeToAndCall(address(impl), "") { - console2.log("Upgrade successful!"); - } catch { - revert UpgradeFailed(); - } - - vm.stopBroadcast(); - - // verify(impl); - - /* STEPS AFTER UPGRADE - - - Add all strategies - - StrategyConfig[] memory usdcConfigs = new StrategyConfig[](3); - usdcConfigs[0] = aUsdcConfig; - usdcConfigs[1] = steakhouseUsdcConfig; - usdcConfigs[2] = sUsdcConfig; - - config.levelContracts.rewardsManager.setAllStrategies(address(config.tokens.usdc), usdcConfigs); - */ + // Since RewardsManager is owned by timelock, we cannot directly upgrade the proxy + // For a real deployment, use the above implementation address to externally schedule a proxy upgrade + // through the timelock. } function verify(RewardsManager manager) public view { diff --git a/script/v2/usd/UpgradeVaultManager.s.sol b/script/v2/usd/UpgradeVaultManager.s.sol index 95b4ba4..888f5e9 100644 --- a/script/v2/usd/UpgradeVaultManager.s.sol +++ b/script/v2/usd/UpgradeVaultManager.s.sol @@ -49,20 +49,6 @@ contract UpgradeVaultManager is Configurable, DeploymentUtils, Script { vm.label(msg.sender, "Deployer EOA"); } - function setUp_(uint256 _chainId, uint256 _privateKey, BaseConfig.Config memory _config) public { - chainId = _chainId; - config = _config; - - if (msg.sender != vm.addr(_privateKey)) { - revert("Private key does not match sender"); - } - - deployerWallet.privateKey = _privateKey; - deployerWallet.addr = vm.addr(_privateKey); - - vm.label(msg.sender, "Deployer EOA"); - } - function run() external returns (BaseConfig.Config memory) { return upgrade(); } @@ -86,106 +72,13 @@ contract UpgradeVaultManager is Configurable, DeploymentUtils, Script { console2.log("=====> VaultManager deployed ...."); console2.log("VaultManager Implementation : https://etherscan.io/address/%s", address(impl)); - // Setup update - - CappedOneDollarOracle mNavOracle = new CappedOneDollarOracle(address(config.oracles.mNav)); - vm.label(address(mNavOracle), "CappedMNavOracle"); - - StrategyConfig memory ustbConfig = 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 - }); - - StrategyConfig memory mConfig = StrategyConfig({ - category: StrategyCategory.M0, - baseCollateral: config.tokens.usdc, - receiptToken: config.tokens.wrappedM, - oracle: AggregatorV3Interface(address(mNavOracle)), - depositContract: address(config.levelContracts.swapManager), - withdrawContract: address(config.levelContracts.swapManager), - heartbeat: 26 hours - }); - - address[] memory targets = new address[](5); - targets[0] = address(config.levelContracts.vaultManager); - targets[1] = address(config.levelContracts.vaultManager); - targets[2] = address(config.levelContracts.vaultManager); - targets[3] = address(config.levelContracts.rolesAuthority); - targets[4] = address(config.levelContracts.rolesAuthority); - - bytes[] memory payloads = new bytes[](5); - // Upgrade to new implementation - payloads[0] = abi.encodeWithSelector(proxy.upgradeToAndCall.selector, address(impl), ""); - // Add ustb as a strategy - payloads[1] = abi.encodeWithSelector( - config.levelContracts.vaultManager.addAssetStrategy.selector, - address(config.tokens.usdc), - address(config.tokens.ustb), - ustbConfig - ); - // Add m as a strategy - payloads[2] = abi.encodeWithSelector( - config.levelContracts.vaultManager.addAssetStrategy.selector, - address(config.tokens.usdc), - address(config.tokens.wrappedM), - mConfig - ); - // Add cooldown operator capability to strategist role - payloads[3] = abi.encodeWithSelector( - config.levelContracts.rolesAuthority.setRoleCapability.selector, - STRATEGIST_ROLE, - address(config.levelContracts.vaultManager), - bytes4(abi.encodeWithSignature("modifyAaveUmbrellaCooldownOperator(address,address,bool)")), - true - ); - // Add rewards claimer capability to strategist role - payloads[4] = abi.encodeWithSelector( - config.levelContracts.rolesAuthority.setRoleCapability.selector, - STRATEGIST_ROLE, - address(config.levelContracts.vaultManager), - bytes4(abi.encodeWithSignature("modifyAaveUmbrellaRewardsClaimer(address,address,bool)")), - true - ); - - vm.startBroadcast(config.users.admin); - - TimelockController timelock = TimelockController(payable(config.levelContracts.adminTimelock)); - timelock.scheduleBatch(targets, new uint256[](5), payloads, bytes32(0), bytes32(0), 5 days); - - vm.warp(block.timestamp + 5 days); - - timelock.executeBatch(targets, new uint256[](5), payloads, bytes32(0), bytes32(0)); - - vm.stopBroadcast(); + // As the deployed vaultManager is owned by timelock, we cannot directly upgrade the proxy + // For a real deployment, use the above implementation address to externally schedule a proxy upgrade + // through the timelock. return config; // verify(impl); - - /* STEPS AFTER UPGRADE - - - Add asset strategy for sUsdc - if (address(config.sparkVaults.sUsdc.vault) == address(0)) { - revert("Spark USDC vaults not deployed"); - } else { - config.levelContracts.vaultManager.addAssetStrategy( - address(config.tokens.usdc), address(config.sparkVaults.sUsdc.vault), sUsdcConfig - ); - } - - - Add default strategy for USDC - address[] memory usdcDefaultStrategies = new address[](3); - usdcDefaultStrategies[0] = address(config.periphery.aaveV3); - usdcDefaultStrategies[1] = address(config.morphoVaults.steakhouseUsdc.vault); - usdcDefaultStrategies[2] = address(config.sparkVaults.sUsdc.vault); - - config.levelContracts.vaultManager.setDefaultStrategies(address(config.tokens.usdc), usdcDefaultStrategies); - */ } function verify(VaultManager manager) public view { diff --git a/src/v2/usd/SwapManager.sol b/src/v2/usd/SwapManager.sol index 1bdf88e..7653df9 100644 --- a/src/v2/usd/SwapManager.sol +++ b/src/v2/usd/SwapManager.sol @@ -9,6 +9,7 @@ import {Initializable} from "@openzeppelin-upgradeable/proxy/utils/Initializable import {UUPSUpgradeable} from "@openzeppelin-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {AuthUpgradeable} from "@level/src/v2/auth/AuthUpgradeable.sol"; import {SwapConfig} from "./SwapManagerStorage.sol"; +import {PauserGuardedUpgradable} from "@level/src/v2/common/guard/PauserGuardedUpgradable.sol"; /** * .-==+=======+: @@ -28,7 +29,7 @@ import {SwapConfig} from "./SwapManagerStorage.sol"; * @notice Manages token swaps using Uniswap V3 pools with configurable parameters * @dev This contract is upgradeable and uses UUPS pattern */ -contract SwapManager is SwapManagerStorage, Initializable, UUPSUpgradeable, AuthUpgradeable { +contract SwapManager is SwapManagerStorage, Initializable, UUPSUpgradeable, AuthUpgradeable, PauserGuardedUpgradable { /// @notice Constructor that disables initializers constructor() { _disableInitializers(); @@ -37,9 +38,11 @@ contract SwapManager is SwapManagerStorage, Initializable, UUPSUpgradeable, Auth /// @notice Initializes the contract with admin and swap router addresses /// @param admin_ The address of the admin who can manage the contract /// @param swapRouter_ The address of the Uniswap V3 Swap Router - function initialize(address admin_, address swapRouter_) external initializer { + /// @param guard_ The address of the guard that controls the pause state of the contract + function initialize(address admin_, address swapRouter_, address guard_) external initializer { __UUPSUpgradeable_init(); __Auth_init(admin_, address(0)); + __PauserGuarded_init(guard_); swapRouter = ISwapRouter(swapRouter_); } @@ -48,7 +51,11 @@ contract SwapManager is SwapManagerStorage, Initializable, UUPSUpgradeable, Auth /// @param tokenOut The address of the output token /// @param config The swap configuration parameters /// @dev Only callable by authorized addresses - function setSwapConfig(address tokenIn, address tokenOut, SwapConfig memory config) external requiresAuth { + function setSwapConfig(address tokenIn, address tokenOut, SwapConfig memory config) + external + requiresAuth + notPaused + { // add access control here require(config.pool != address(0), "Invalid pool"); swapConfigs[tokenIn][tokenOut] = config; @@ -66,6 +73,7 @@ contract SwapManager is SwapManagerStorage, Initializable, UUPSUpgradeable, Auth /// use the slippage in the pair's swap config. function swap(address tokenIn, address tokenOut, uint256 amountIn, address recipient) external + notPaused returns (uint256 amountOut) { SwapConfig memory config = swapConfigs[tokenIn][tokenOut]; diff --git a/test/v2/integration/RewardsManager.t.sol b/test/v2/integration/RewardsManager.t.sol index 5d3de96..7013057 100644 --- a/test/v2/integration/RewardsManager.t.sol +++ b/test/v2/integration/RewardsManager.t.sol @@ -24,6 +24,8 @@ import {IERC4626Oracle} from "@level/src/v2/interfaces/level/IERC4626Oracle.sol" import {ILevelMintingV2Structs} from "@level/src/v2/interfaces/level/ILevelMintingV2.sol"; 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"; contract RewardsManagerMainnetTests is Utils, Configurable { using SafeTransferLib for ERC20; @@ -40,6 +42,8 @@ contract RewardsManagerMainnetTests is Utils, Configurable { uint256 public constant INITIAL_BALANCE = 100_000_000e6; uint256 public constant INITIAL_SHARES = 200_000_000e18; + address public constant USTB_CHAINLINK_FEED = 0xE4fA682f94610cCd170680cc3B045d77D9E528a8; + address[] public assets; function setUp() public { @@ -438,6 +442,105 @@ contract RewardsManagerMainnetTests is Utils, Configurable { assertEq(price, 99e6, "Price should be 0.99 USD"); } + function test_sparkYield_succeeds(uint256 deposit) public { + deposit = bound(deposit, 1000, 1_000_000e6); + + // Deposit some USDC into the spark vault + vm.prank(strategist.addr); + vaultManager.deposit(address(config.tokens.usdc), address(config.sparkVaults.sUsdc.vault), deposit); + + // Preview deposit + uint256 expectedShares = config.sparkVaults.sUsdc.vault.convertToShares(deposit); + + // Ensure we have the expected shares + assertEq(config.sparkVaults.sUsdc.vault.balanceOf(address(vaultManager.vault())), expectedShares); + + // Get the accrued yield in the redemption asset's decimals + uint256 accruedYield = rewardsManager.getAccruedYield(assets); + + // Accrued yield should be 0 at this point + assertApproxEqAbs(accruedYield, 0, 1, "Accrued yield should be 0"); + + // 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); + + // Yield should be non-zero + assertGt(accruedYield, 0, "Accrued yield should be non-zero"); + + uint256 treasuryUsdcBalanceBefore = config.tokens.usdc.balanceOf(config.users.protocolTreasury); + + // Fuzz a yield amount between 0 and the accrued yield + uint256 yieldAmount; + yieldAmount = bound(yieldAmount, 1, accruedYield); + + // Reward the yield + vm.prank(strategist.addr); + rewardsManager.reward(assets[0], yieldAmount); + + uint256 treasuryUsdcBalanceAfter = config.tokens.usdc.balanceOf(config.users.protocolTreasury); + + assertApproxEqAbs( + treasuryUsdcBalanceAfter - treasuryUsdcBalanceBefore, yieldAmount, 2, "Rewarded amount does not match" + ); + } + + function test_ustbYield_succeeds(uint256 deposit) public { + deposit = bound(deposit, 1000, 1_000_000e6); + + _mockChainlinkCall(USTB_CHAINLINK_FEED, 105e5); // 10.5 USD per USTB + + (uint256 superstateTokenOutAmount,,) = ISuperstateToken(address(config.tokens.ustb)).calculateSuperstateTokenOut( + deposit, address(config.tokens.usdc) + ); + + // Superstate Allowlist V2 on Mainnet + IAllowListV2 allowList = IAllowListV2(0x02f1fA8B196d21c7b733EB2700B825611d8A38E5); + address[] memory addresses = new address[](1); + addresses[0] = address(config.levelContracts.boringVault); + + vm.prank(allowList.owner()); + allowList.setProtocolAddressPermissions(addresses, "USTB", true); + + // Deposit some USDC into the ustb vault + vm.prank(strategist.addr); + vaultManager.deposit(address(config.tokens.usdc), address(config.tokens.ustb), deposit); + + // Ensure we have the expected USTB + assertEq(config.tokens.ustb.balanceOf(address(vaultManager.vault())), superstateTokenOutAmount); + + // Overtime, the USTB NAV will increase, and we will get yield + _mockChainlinkCall(address(config.oracles.ustb), 107e5); // 10.7 USD per USTB + + // Get the accrued yield in the redemption asset's decimals + uint256 accruedYield = rewardsManager.getAccruedYield(assets); + + // Yield should be non-zero + assertGt(accruedYield, 0, "Accrued yield should be non-zero"); + + uint256 treasuryUsdcBalanceBefore = config.tokens.usdc.balanceOf(config.users.protocolTreasury); + + // Fuzz a yield amount between 0 and the accrued yield + uint256 yieldAmount; + yieldAmount = bound(yieldAmount, 1, accruedYield); + + // Reward the yield + vm.prank(strategist.addr); + rewardsManager.reward(assets[0], yieldAmount); + + uint256 treasuryUsdcBalanceAfter = config.tokens.usdc.balanceOf(config.users.protocolTreasury); + + assertApproxEqAbs( + treasuryUsdcBalanceAfter - treasuryUsdcBalanceBefore, yieldAmount, 2, "Rewarded amount does not match" + ); + } + // ------------- Internal Helpers ------------- function _getAssetsInStrategy(address asset, address strategy) public view returns (uint256) { diff --git a/test/v2/integration/VaultManager.t.sol b/test/v2/integration/VaultManager.t.sol index 03b0020..9205db5 100644 --- a/test/v2/integration/VaultManager.t.sol +++ b/test/v2/integration/VaultManager.t.sol @@ -23,6 +23,7 @@ import {UpgradeVaultManager} from "@level/script/v2/usd/UpgradeVaultManager.s.so 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"; contract VaultManagerMainnetTests is Utils, Configurable { using SafeTransferLib for ERC20; @@ -61,18 +62,12 @@ contract VaultManagerMainnetTests is Utils, Configurable { deploySwapManager.setUp_(1, deployer.privateKey); config = deploySwapManager.run(); - UpgradeVaultManager upgradeScript = new UpgradeVaultManager(); - - // Deploy - - vm.prank(deployer.addr); - upgradeScript.setUp_(1, deployer.privateKey, config); - - config = upgradeScript.run(); + _upgradeVaultManager(); // Setup strategist vm.prank(config.users.admin); _setupVaultsForTests(); + _setupTreasuriesForTests(); address[] memory targets = new address[](2); targets[0] = address(config.levelContracts.rolesAuthority); @@ -152,6 +147,60 @@ contract VaultManagerMainnetTests is Utils, Configurable { } } + function _upgradeVaultManager() internal { + VaultManager impl = new VaultManager(); + vm.prank(address(config.levelContracts.adminTimelock)); + config.levelContracts.vaultManager.upgradeToAndCall(address(impl), ""); + } + + function _setupTreasuriesForTests() internal { + CappedOneDollarOracle mNavOracle = new CappedOneDollarOracle(address(config.oracles.mNav)); + + StrategyConfig memory ustbConfig = 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 + }); + + StrategyConfig memory mConfig = StrategyConfig({ + category: StrategyCategory.M0, + baseCollateral: config.tokens.usdc, + receiptToken: config.tokens.wrappedM, + oracle: AggregatorV3Interface(address(mNavOracle)), + depositContract: address(config.levelContracts.swapManager), + withdrawContract: address(config.levelContracts.swapManager), + heartbeat: 26 hours + }); + + address[] memory targets = new address[](2); + targets[0] = address(config.levelContracts.vaultManager); + targets[1] = address(config.levelContracts.vaultManager); + + bytes[] memory payloads = new bytes[](2); + // Add ustb as a strategy + payloads[0] = abi.encodeWithSelector( + config.levelContracts.vaultManager.addAssetStrategy.selector, + address(config.tokens.usdc), + address(config.tokens.ustb), + ustbConfig + ); + // Add m as a strategy + payloads[1] = abi.encodeWithSelector( + config.levelContracts.vaultManager.addAssetStrategy.selector, + address(config.tokens.usdc), + address(config.tokens.wrappedM), + mConfig + ); + + _scheduleAndExecuteAdminActionBatch( + address(config.users.admin), address(config.levelContracts.adminTimelock), targets, payloads + ); + } + function _setupVaultsForTests() internal { //--------------- Add test Morpho vaults as strategies if (address(config.morphoVaults.steakhouseUsdt.vault) == address(0)) { @@ -269,7 +318,7 @@ contract VaultManagerMainnetTests is Utils, Configurable { usdtDefaultStrategies[1] = address(config.morphoVaults.steakhouseUsdt.vault); usdtDefaultStrategies[2] = address(config.morphoVaults.steakhouseUsdtLite.vault); - address[] memory targets = new address[](7); + address[] memory targets = new address[](9); targets[0] = address(config.levelContracts.vaultManager); targets[1] = address(config.levelContracts.vaultManager); targets[2] = address(config.levelContracts.vaultManager); @@ -277,8 +326,10 @@ contract VaultManagerMainnetTests is Utils, Configurable { targets[4] = address(config.levelContracts.vaultManager); targets[5] = address(config.levelContracts.vaultManager); targets[6] = address(config.levelContracts.vaultManager); + targets[7] = address(config.levelContracts.rolesAuthority); + targets[8] = address(config.levelContracts.rolesAuthority); - bytes[] memory payloads = new bytes[](7); + bytes[] memory payloads = new bytes[](9); payloads[0] = abi.encodeWithSelector( VaultManager.addAssetStrategy.selector, address(config.tokens.usdc), @@ -315,6 +366,22 @@ contract VaultManagerMainnetTests is Utils, Configurable { payloads[6] = abi.encodeWithSelector( VaultManager.setDefaultStrategies.selector, address(config.tokens.usdt), usdtDefaultStrategies ); + // Add cooldown operator capability to strategist role + payloads[7] = abi.encodeWithSelector( + config.levelContracts.rolesAuthority.setRoleCapability.selector, + STRATEGIST_ROLE, + address(config.levelContracts.vaultManager), + bytes4(abi.encodeWithSignature("modifyAaveUmbrellaCooldownOperator(address,address,bool)")), + true + ); + // Add rewards claimer capability to strategist role + payloads[8] = abi.encodeWithSelector( + config.levelContracts.rolesAuthority.setRoleCapability.selector, + STRATEGIST_ROLE, + address(config.levelContracts.vaultManager), + bytes4(abi.encodeWithSignature("modifyAaveUmbrellaRewardsClaimer(address,address,bool)")), + true + ); _scheduleAndExecuteAdminActionBatch( address(config.users.admin), address(config.levelContracts.adminTimelock), targets, payloads