-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #63 from Ion-Protocol/hrikb/ezETH-integration
ezETH integration
- Loading branch information
Showing
19 changed files
with
1,684 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
239 changes: 239 additions & 0 deletions
239
src/flash/UniswapFlashswapDirectMintHandlerWithDust.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.