Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions script/v2/DeployOracles.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand Down
75 changes: 51 additions & 24 deletions src/v2/common/libraries/VaultLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
Expand All @@ -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_);

Expand All @@ -571,47 +590,55 @@ 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
returns (uint256 unstaked)
{
// 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");
Expand Down
63 changes: 63 additions & 0 deletions src/v2/oracles/AaveUmbrellaOracle.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
14 changes: 9 additions & 5 deletions src/v2/oracles/CappedOneDollarOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions src/v2/usd/SwapManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,15 @@ 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({
tokenIn: tokenIn,
tokenOut: tokenOut,
fee: config.fee,
recipient: recipient,
deadline: block.timestamp + 60,
deadline: block.timestamp,
amountIn: amountIn,
amountOutMinimum: minOut,
sqrtPriceLimitX96: 0
Expand Down
Loading