Skip to content

Commit

Permalink
Merge pull request #63 from Ion-Protocol/hrikb/ezETH-integration
Browse files Browse the repository at this point in the history
ezETH integration
  • Loading branch information
HrikB authored May 7, 2024
2 parents 545f283 + 2f48bc5 commit 2a86a93
Show file tree
Hide file tree
Showing 19 changed files with 1,684 additions and 15 deletions.
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

0 comments on commit 2a86a93

Please sign in to comment.