Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ezETH integration #63

Merged
merged 13 commits into from
May 7, 2024
8 changes: 7 additions & 1 deletion src/Constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {
ILRTOracle,
ILRTConfig,
IEtherFiLiquidityPool,
ILRTDepositPool
ILRTDepositPool,
IEzEth,
IRenzoOracle,
IRestakeManager
} from "./interfaces/ProviderInterfaces.sol";
import { IRedstonePriceFeed } from "./interfaces/IRedstone.sol";
import { IChainlink } from "./interfaces/IChainlink.sol";
Expand Down Expand Up @@ -64,6 +67,9 @@ IRswEth constant RSWETH = IRswEth(0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0);
// ezETH
IRedstonePriceFeed constant REDSTONE_EZETH_ETH_PRICE_FEED =
IRedstonePriceFeed(0xF4a3e183F59D2599ee3DF213ff78b1B3b1923696);
IEzEth constant EZETH = IEzEth(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110);
IRenzoOracle constant RENZO_ORACLE = IRenzoOracle(0x5a12796f7e7EBbbc8a402667d266d2e65A814042);
IRestakeManager constant RENZO_RESTAKE_MANAGER = IRestakeManager(0x74a09653A083691711cF8215a6ab074BB4e99ef5);

// Chainlink
IChainlink constant ETH_PER_STETH_CHAINLINK = IChainlink(0x86392dC19c0b719886221c78AB11eb8Cf5c52812);
Expand Down
239 changes: 239 additions & 0 deletions src/flash/UniswapFlashswapDirectMintHandlerWithDust.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.21;

import { IonHandlerBase } from "./IonHandlerBase.sol";
import { IWETH9 } from "../interfaces/IWETH9.sol";

import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import { IUniswapV3SwapCallback } from "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol";

import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/**
* @notice This contract is forked off of the UniswapFlashswapDirectMintHandler,
* with one distinction that it handles potential dust collateral amounts that
* can accrue when the contract ends up minting more collateral than originally
* intended. This situation can occur when the user has a desired leverage
* amount and thus an exact resulting collateral amount, but due to rounding
* errors in the minting contract, the handler is forced to mint a dust amount
* more than the desired collateral amount. In this contract, the dust is added
* to the total final deposit amount and ends up in the user's vault as
* additional collateral.
*
* The key difference between this contract and
* `UniswapFlashswapDirectMintHandler` is a relaxed bound in comparing the sum
* of initial user deposit and additionally minted collateral to the caller's
* requested resulting additional collateral amount.
*
* This contract allows for easy creation of leverage positions through a Uniswap
* flashswap and direct mint of the collateral from the provider. This will be
* used when the collateral cannot be minted directly with the base asset but
* can be directly minted by a token that the base asset has a UniswapV3 pool
* with.
*
* This contract is to be used when there exists a UniswapV3 pool between the
* base asset and the mint asset.
*
* @custom:security-contact [email protected]
*/
abstract contract UniswapFlashswapDirectMintHandlerWithDust is IonHandlerBase, IUniswapV3SwapCallback {
using SafeERC20 for IERC20;
using SafeERC20 for IWETH9;
using SafeCast for uint256;

error InvalidUniswapPool();
error InvalidZeroLiquidityRegionSwap();
error CallbackOnlyCallableByPool(address unauthorizedCaller);
error OutputAmountNotReceived(uint256 amountReceived, uint256 amountRequired);

/// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK)
uint160 internal constant MIN_SQRT_RATIO = 4_295_128_739;
/// @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK)
uint160 internal constant MAX_SQRT_RATIO = 1_461_446_703_485_210_103_287_273_052_203_988_822_378_723_970_342;

IUniswapV3Pool public immutable UNISWAP_POOL;
IERC20 public immutable MINT_ASSET;
bool private immutable MINT_IS_TOKEN0;

/**
* @notice Creates a new `UniswapFlashswapDirectMintHandler` instance.
* @param _uniswapPool Pool to perform the flashswap on.
* @param _mintAsset The asset used to mint the collateral.
*/
constructor(IUniswapV3Pool _uniswapPool, IERC20 _mintAsset) {
if (address(_uniswapPool) == address(0)) revert InvalidUniswapPool();

MINT_ASSET = _mintAsset;

address token0 = _uniswapPool.token0();
address token1 = _uniswapPool.token1();

if (token0 != address(MINT_ASSET) && token1 != address(MINT_ASSET)) {
revert InvalidUniswapPool();
}
if (token0 == address(MINT_ASSET) && token1 == address(MINT_ASSET)) {
revert InvalidUniswapPool();
}

UNISWAP_POOL = _uniswapPool;
MINT_IS_TOKEN0 = token0 == address(MINT_ASSET) ? true : false;

address baseAsset = MINT_IS_TOKEN0 ? token1 : token0;

if (baseAsset != address(BASE)) revert InvalidUniswapPool();
}

/**
* @notice Transfer collateral from user -> Initiate flashswap between from
* base asset to mint asset -> Use the mint asset to mint the collateral ->
* Deposit all collateral into `IonPool` -> Borrow the base asset -> Close
* the flashswap by sending the base asset to the Uniswap pool.
* @param initialDeposit in collateral terms. [WAD]
* @param resultingAdditionalCollateral in collateral terms. [WAD]
* @param maxResultingDebt in base asset terms. [WAD]
* @param deadline The unix timestamp after which the uniswap transaction reverts.
* @param proof used to validate the user is whitelisted.
*/
function flashswapAndMint(
uint256 initialDeposit,
uint256 resultingAdditionalCollateral,
uint256 maxResultingDebt,
uint256 deadline,
bytes32[] memory proof
)
external
onlyWhitelistedBorrowers(proof)
checkDeadline(deadline)
{
LST_TOKEN.safeTransferFrom(msg.sender, address(this), initialDeposit);
_flashswapAndMint(initialDeposit, resultingAdditionalCollateral, maxResultingDebt);
}

function _flashswapAndMint(
uint256 initialDeposit,
uint256 resultingAdditionalCollateral,
uint256 maxResultingDebt
)
internal
{
uint256 amountLrt = resultingAdditionalCollateral - initialDeposit; // in collateral terms
uint256 amountWethToFlashloan = _getAmountInForCollateralAmountOut(amountLrt);

if (amountWethToFlashloan == 0) {
// AmountToBorrow.IS_MAX because we don't want to create any new debt here
_depositAndBorrow(msg.sender, address(this), resultingAdditionalCollateral, 0, AmountToBorrow.IS_MAX);
return;
}

// We want to swap for ETH here
bool zeroForOne = MINT_IS_TOKEN0 ? false : true;
uint256 baseAssetSwappedIn = _initiateFlashSwap({
zeroForOne: zeroForOne,
amountOut: amountWethToFlashloan,
recipient: address(this),
data: abi.encode(msg.sender, resultingAdditionalCollateral, initialDeposit)
});

if (baseAssetSwappedIn > maxResultingDebt) {
revert FlashloanRepaymentTooExpensive(amountWethToFlashloan, maxResultingDebt);
}
}

/**
* @notice Handles swap initiation logic. This function can only initiate
* exact output swaps.
* @param zeroForOne Direction of the swap.
* @param amountOut Desired amount of output.
* @param recipient of output tokens.
* @param data Arbitrary data to be passed through swap callback.
*/
function _initiateFlashSwap(
bool zeroForOne,
uint256 amountOut,
address recipient,
bytes memory data
)
private
returns (uint256 amountIn)
{
(int256 amount0Delta, int256 amount1Delta) = UNISWAP_POOL.swap(
recipient, zeroForOne, -amountOut.toInt256(), zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1, data
);

uint256 amountOutReceived;
(amountIn, amountOutReceived) = zeroForOne
? (uint256(amount0Delta), uint256(-amount1Delta))
: (uint256(amount1Delta), uint256(-amount0Delta));

// it's technically possible to not receive the full output amount,
if (amountOutReceived != amountOut) revert OutputAmountNotReceived(amountOutReceived, amountOut);
}

/**
* @notice From the perspective of the pool i.e. Negative amount means pool is
* sending. This function is intended to never be called directly. It should
* only be called by the Uniswap pool during a swap initiated by this
* contract.
*
* @dev One thing to note from a security perspective is that the pool only calls
* the callback on `msg.sender`. So a theoretical attacker cannot call this
* function by directing where to call the callback.
*
* @param amount0Delta change in token0
* @param amount1Delta change in token1
* @param _data arbitrary data
*/
function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata _data) external override {
if (msg.sender != address(UNISWAP_POOL)) revert CallbackOnlyCallableByPool(msg.sender);

// swaps entirely within 0-liquidity regions are not supported
if (amount0Delta == 0 && amount1Delta == 0) revert InvalidZeroLiquidityRegionSwap();
(address user, uint256 resultingAdditionalCollateral, uint256 initialDeposit) =
abi.decode(_data, (address, uint256, uint256));

// Code below this if statement will always assume token0 is MINT_ASSET. If it
// is not actually the case, we will flip the vars
if (!MINT_IS_TOKEN0) {
(amount0Delta, amount1Delta) = (amount1Delta, amount0Delta);
}

address tokenIn = address(BASE);

// Sanity check that Uniswap is sending MINT_ASSET
assert(amount0Delta < 0 && amount1Delta > 0);

// MINT_ASSET needs to be converted into collateral asset
uint256 collateralFromDeposit = _mintCollateralAsset(uint256(-amount0Delta));

// Sanity check
// Greater than and not equal to if extra dust collateral was minted due to
// rounding errors. Guarantees minimum bound for the leveraged collateral amount.
uint256 depositAmount = initialDeposit + collateralFromDeposit;
assert(depositAmount >= resultingAdditionalCollateral);

// AmountToBorrow.IS_MIN because we want to make sure enough is borrowed
// to cover the amount owed back to Uniswap
_depositAndBorrow(user, address(this), depositAmount, uint256(amount1Delta), AmountToBorrow.IS_MIN);

IERC20(tokenIn).safeTransfer(msg.sender, uint256(amount1Delta));
}

/**
* @notice Deposits the mint asset into the provider's collateral-asset
* deposit contract.
* @param amountMintAsset amount of "mint asset" to deposit. [WAD]
* @return Amount of collateral asset received. [WAD]
*/
function _mintCollateralAsset(uint256 amountMintAsset) internal virtual returns (uint256);

/**
* @notice Calculates the amount of mint asset required to receive
* `amountLrt`.
* @dev Calculates the amount of mint asset required to receive `amountLrt`.
* @param amountLrt Desired output amount. [WAD]
* @return Amount mint asset required for desired output. [WAD]
*/
function _getAmountInForCollateralAmountOut(uint256 amountLrt) internal view virtual returns (uint256);
}
58 changes: 58 additions & 0 deletions src/flash/lrt/EzEthHandler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.21;

import { IonPool } from "../../IonPool.sol";
import { GemJoin } from "../../join/GemJoin.sol";
import { Whitelist } from "../../Whitelist.sol";
import { UniswapFlashswapDirectMintHandlerWithDust } from "./../UniswapFlashswapDirectMintHandlerWithDust.sol";
import { IonHandlerBase } from "../IonHandlerBase.sol";
import { RenzoLibrary } from "./../../libraries/lrt/RenzoLibrary.sol";
import { WETH_ADDRESS } from "../../Constants.sol";

import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";

/**
* @notice Handler for the ezETH collateral.
*
* @custom:security-contact [email protected]
*/
contract EzEthHandler is UniswapFlashswapDirectMintHandlerWithDust {
/**
* @notice Creates a new `EzEthHandler` instance.
* @param _ilkIndex Ilk index of the pool.
* @param _ionPool address.
* @param _gemJoin address.
* @param _whitelist address.
* @param _wstEthUniswapPool address of the wstETH/WETH Uniswap pool (0.01% fee).
*/
constructor(
uint8 _ilkIndex,
IonPool _ionPool,
GemJoin _gemJoin,
Whitelist _whitelist,
IUniswapV3Pool _wstEthUniswapPool
)
IonHandlerBase(_ilkIndex, _ionPool, _gemJoin, _whitelist)
UniswapFlashswapDirectMintHandlerWithDust(_wstEthUniswapPool, WETH_ADDRESS)
{ }

/**
* @inheritdoc UniswapFlashswapDirectMintHandlerWithDust
*/
function _mintCollateralAsset(uint256 amountWeth) internal override returns (uint256) {
WETH.withdraw(amountWeth);
return RenzoLibrary.depositForLrt(amountWeth);
}

/**
* @inheritdoc UniswapFlashswapDirectMintHandlerWithDust
*/
function _getAmountInForCollateralAmountOut(uint256 amountOut)
internal
view
override
returns (uint256 ethAmountIn)
{
(ethAmountIn,) = RenzoLibrary.getEthAmountInForLstAmountOut(amountOut);
}
}
56 changes: 56 additions & 0 deletions src/interfaces/ProviderInterfaces.sol
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,59 @@ interface ILRTConfig {

function depositLimitByAsset(address asset) external view returns (uint256);
}

// Renzo

interface IEzEth is IERC20 { }

interface IRenzoOracle {
function lookupTokenValue(IERC20 _token, uint256 _balance) external view returns (uint256);
function lookupTokenAmountFromValue(IERC20 _token, uint256 _value) external view returns (uint256);
function lookupTokenValues(IERC20[] memory _tokens, uint256[] memory _balances) external view returns (uint256);
function calculateMintAmount(
uint256 _currentValueInProtocol,
uint256 _newValueAdded,
uint256 _existingEzETHSupply
)
external
pure
returns (uint256);
function calculateRedeemAmount(
uint256 _ezETHBeingBurned,
uint256 _existingEzETHSupply,
uint256 _currentValueInProtocol
)
external
pure
returns (uint256);
function calculateTVLs() external view returns (uint256[][] memory, uint256[] memory, uint256);
}

interface IOperatorDelegator {
function getTokenBalanceFromStrategy(IERC20 token) external view returns (uint256);

function deposit(IERC20 _token, uint256 _tokenAmount) external returns (uint256 shares);

function startWithdrawal(IERC20 _token, uint256 _tokenAmount) external returns (bytes32);

function getStakedETHBalance() external view returns (uint256);

function stakeEth(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable;

function pendingUnstakedDelayedWithdrawalAmount() external view returns (uint256);
}

interface IRestakeManager {
function stakeEthInOperatorDelegator(
IOperatorDelegator operatorDelegator,
bytes calldata pubkey,
bytes calldata signature,
bytes32 depositDataRoot
)
external
payable;
function depositTokenRewardsFromProtocol(IERC20 _token, uint256 _amount) external;

function calculateTVLs() external view returns (uint256[][] memory, uint256[] memory, uint256);
function depositETH(uint256 _referralId) external payable;
}
Loading
Loading