From 929ac56e39572bb5dbf5b090191f0eb4e99d59cb Mon Sep 17 00:00:00 2001 From: Atharva Date: Wed, 2 Jul 2025 18:32:43 +0530 Subject: [PATCH 1/2] #994: Spearbit #2: Umbrella update (df78120) --- src/v2/common/libraries/VaultLib.sol | 31 ++- src/v2/oracles/AaveUmbrellaOracle.sol | 63 ++++++ test/v2/integration/RewardsManager.t.sol | 246 +++++++++++++++++++++-- test/v2/integration/VaultManager.t.sol | 22 +- 4 files changed, 329 insertions(+), 33 deletions(-) create mode 100644 src/v2/oracles/AaveUmbrellaOracle.sol diff --git a/src/v2/common/libraries/VaultLib.sol b/src/v2/common/libraries/VaultLib.sol index 97a767d..1084263 100644 --- a/src/v2/common/libraries/VaultLib.sol +++ b/src/v2/common/libraries/VaultLib.sol @@ -543,13 +543,36 @@ library VaultLib { internal returns (uint256 staked) { - IERC4626StakeToken stakeToken = IERC4626StakeToken(_config.depositContract); + // config.baseCollateral is the USDC/USDT token + // config.depositContract is the Aave Umbrella contract (stwaToken) + // config.receiptToken is also stwaToken - // Wrap the aTokens + // Supply baseCollateral to Aave in order to get aToken + IERC4626StakeToken stakeToken = IERC4626StakeToken(_config.depositContract); IERC4626StataToken stataToken = IERC4626StataToken(stakeToken.asset()); - vault.setTokenAllowance(address(_config.baseCollateral), address(stataToken), amount); + address aToken = stataToken.aToken(); + + address aaveV3 = _getAaveV3Pool(); + vault.setTokenAllowance(address(_config.baseCollateral), aaveV3, amount); + uint256 sharesBefore = ERC20(aToken).balanceOf(address(vault)); + + vault.manage( + address(aaveV3), + abi.encodeWithSignature( + "supply(address,uint256,address,uint16)", address(_config.baseCollateral), amount, address(vault), 0 + ), + 0 + ); + + uint256 sharesAfter = ERC20(aToken).balanceOf(address(vault)); + uint256 aTokenBalance = sharesAfter - sharesBefore; + + // Wrap the aTokens into waTokens + vault.setTokenAllowance(aToken, address(stataToken), aTokenBalance); bytes memory sharesRaw = vault.manage( - address(stataToken), abi.encodeWithSignature("depositATokens(uint256,address)", amount, address(vault)), 0 + address(stataToken), + abi.encodeWithSignature("depositATokens(uint256,address)", aTokenBalance, address(vault)), + 0 ); uint256 shares = abi.decode(sharesRaw, (uint256)); 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/test/v2/integration/RewardsManager.t.sol b/test/v2/integration/RewardsManager.t.sol index 7013057..4492398 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,48 @@ contract RewardsManagerMainnetTests is Utils, Configurable { assertEq(price, 99e6, "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 +611,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 +637,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 +789,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..bb818c7 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); @@ -660,9 +660,6 @@ contract VaultManagerMainnetTests is Utils, Configurable { deposit = bound(deposit, 1e3, INITIAL_BALANCE); vm.startPrank(strategist.addr); - uint256 aUsdcBalance = config.tokens.aUsdc.balanceOf(strategist.addr); - console2.log("aUsdcBalance", aUsdcBalance); - IERC4626StataToken stataToken = IERC4626StataToken(config.umbrellaVaults.waUsdcStakeToken.vault.asset()); uint256 expectedWrappedBalance = IERC4626(address(stataToken)).previewDeposit(deposit); @@ -672,6 +669,11 @@ contract VaultManagerMainnetTests is Utils, Configurable { // 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(); } From 918603bf939bf9ea577695e0bee433e0933a67d1 Mon Sep 17 00:00:00 2001 From: Atharva Date: Thu, 3 Jul 2025 16:05:34 +0530 Subject: [PATCH 2/2] 997: Add oracle to script and fix stake/unstake (682be1c) --- script/v2/DeployOracles.sol | 5 +- src/v2/common/libraries/VaultLib.sol | 67 +++++++++++--------------- test/v2/integration/VaultManager.t.sol | 26 +++++----- 3 files changed, 44 insertions(+), 54 deletions(-) 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 1084263..d7783d5 100644 --- a/src/v2/common/libraries/VaultLib.sol +++ b/src/v2/common/libraries/VaultLib.sol @@ -546,45 +546,24 @@ library VaultLib { // config.baseCollateral is the USDC/USDT token // config.depositContract is the Aave Umbrella contract (stwaToken) // config.receiptToken is also stwaToken - - // Supply baseCollateral to Aave in order to get aToken IERC4626StakeToken stakeToken = IERC4626StakeToken(_config.depositContract); - IERC4626StataToken stataToken = IERC4626StataToken(stakeToken.asset()); - address aToken = stataToken.aToken(); - - address aaveV3 = _getAaveV3Pool(); - vault.setTokenAllowance(address(_config.baseCollateral), aaveV3, amount); - uint256 sharesBefore = ERC20(aToken).balanceOf(address(vault)); - - vault.manage( - address(aaveV3), - abi.encodeWithSignature( - "supply(address,uint256,address,uint16)", address(_config.baseCollateral), amount, address(vault), 0 - ), - 0 - ); - - uint256 sharesAfter = ERC20(aToken).balanceOf(address(vault)); - uint256 aTokenBalance = sharesAfter - sharesBefore; + IERC4626 stataToken = IERC4626(stakeToken.asset()); - // Wrap the aTokens into waTokens - vault.setTokenAllowance(aToken, address(stataToken), aTokenBalance); + // Convert Token to waToken + vault.setTokenAllowance(address(_config.baseCollateral), address(stataToken), amount); bytes memory sharesRaw = vault.manage( - address(stataToken), - abi.encodeWithSignature("depositATokens(uint256,address)", aTokenBalance, 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_); @@ -594,7 +573,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 @@ -602,39 +581,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 <= 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/test/v2/integration/VaultManager.t.sol b/test/v2/integration/VaultManager.t.sol index bb818c7..07f126e 100644 --- a/test/v2/integration/VaultManager.t.sol +++ b/test/v2/integration/VaultManager.t.sol @@ -656,16 +656,17 @@ 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); - 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); @@ -682,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 ); @@ -705,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" ); }