From 269e008a2fe20d19088331d2195cf1b0230bebf3 Mon Sep 17 00:00:00 2001 From: Jun Kim <64379343+junkim012@users.noreply.github.com> Date: Wed, 24 Jul 2024 05:30:52 +0900 Subject: [PATCH 1/3] feat: base network ezETH/WETH uniswap flashswap strategy and spot/reserve oracles and tests --- src/Constants.sol | 6 + src/flash/lrt/base/BaseEzEthWethHandler.sol | 41 +++ .../lrt/base/BaseEzEthWethReserveOracle.sol | 71 +++++ .../spot/lrt/base/BaseEzEthWethSpotOracle.sol | 87 ++++++ .../LrtUniswapFlashswapHandler.t.sol | 267 ++++++++++++++++++ test/fork/concrete/lrt/ReserveOracle.t.sol | 69 ++++- test/fork/concrete/lrt/SpotOracle.t.sol | 22 +- .../lrt/base/BaseEzEthWethHandler.t.sol | 94 ++++++ .../LrtUniswapFlashswapHandler.t.sol | 211 ++++++++++++++ .../fuzz/lrt/base/BaseEzEthWethHandler.t.sol | 90 ++++++ 10 files changed, 956 insertions(+), 2 deletions(-) create mode 100644 src/flash/lrt/base/BaseEzEthWethHandler.sol create mode 100644 src/oracles/reserve/lrt/base/BaseEzEthWethReserveOracle.sol create mode 100644 src/oracles/spot/lrt/base/BaseEzEthWethSpotOracle.sol create mode 100644 test/fork/concrete/handlers-base/LrtUniswapFlashswapHandler.t.sol create mode 100644 test/fork/concrete/lrt/base/BaseEzEthWethHandler.t.sol create mode 100644 test/fork/fuzz/handlers-base/LrtUniswapFlashswapHandler.t.sol create mode 100644 test/fork/fuzz/lrt/base/BaseEzEthWethHandler.t.sol diff --git a/src/Constants.sol b/src/Constants.sol index bb61368d..a208d042 100644 --- a/src/Constants.sol +++ b/src/Constants.sol @@ -110,3 +110,9 @@ IUniswapV3Pool constant BASE_WSTETH_WETH_UNISWAP = IUniswapV3Pool(0x20E068D76f9E IChainlink constant BASE_SEQUENCER_UPTIME_FEED = IChainlink(0xBCF85224fc0756B9Fa45aA7892530B47e10b6433); IERC20 constant BASE_WEETH = IERC20(0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A); IWETH9 constant BASE_WETH = IWETH9(0x4200000000000000000000000000000000000006); + +// Renzo +IERC20 constant BASE_EZETH = IERC20(0x2416092f143378750bb29b79eD961ab195CcEea5); +IUniswapV3Pool constant BASE_EZETH_WETH_AERODROME = IUniswapV3Pool(0xDC7EAd706795eDa3FEDa08Ad519d9452BAdF2C0d); +IChainlink constant BASE_EZETH_ETH_EXCHANGE_RATE_CHAINLINK = IChainlink(0xC4300B7CF0646F0Fe4C5B2ACFCCC4dCA1346f5d8); +IChainlink constant BASE_EZETH_ETH_PRICE_CHAINLINK = IChainlink(0x960BDD1dFD20d7c98fa482D793C3dedD73A113a3); diff --git a/src/flash/lrt/base/BaseEzEthWethHandler.sol b/src/flash/lrt/base/BaseEzEthWethHandler.sol new file mode 100644 index 00000000..54e73441 --- /dev/null +++ b/src/flash/lrt/base/BaseEzEthWethHandler.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.21; + +import { IonPool } from "./../../../IonPool.sol"; +import { IonPool } from "./../../../IonPool.sol"; +import { GemJoin } from "./../../../join/GemJoin.sol"; +import { Whitelist } from "./../../../Whitelist.sol"; +import { IonHandlerBase } from "./../../IonHandlerBase.sol"; +import { IWETH9 } from "./../../../interfaces/IWETH9.sol"; +import { UniswapFlashswapHandler } from "./../../UniswapFlashswapHandler.sol"; +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; + +/** + * @notice Handler for the ezETH collateral in the ezETH/WETH market on Base. + * + * @custom:security-contact security@molecularlabs.io + */ +contract BaseEzEthWethHandler is UniswapFlashswapHandler { + /** + * @notice Creates a new `EzEthWethHandler` instance. + * @param _ilkIndex Ilk index of the pool. + * @param _ionPool address. + * @param _gemJoin address. + * @param _whitelist address. + * @param _pool address of the ezETH/WETH Aerodrome pool. + * @param _wethIsToken0 Whether WETH is token0 or token1. + * @param _weth The WETH address of this chain. + */ + constructor( + uint8 _ilkIndex, + IonPool _ionPool, + GemJoin _gemJoin, + Whitelist _whitelist, + IUniswapV3Pool _pool, + bool _wethIsToken0, + IWETH9 _weth + ) + IonHandlerBase(_ilkIndex, _ionPool, _gemJoin, _whitelist, _weth) + UniswapFlashswapHandler(_pool, _wethIsToken0) + { } +} diff --git a/src/oracles/reserve/lrt/base/BaseEzEthWethReserveOracle.sol b/src/oracles/reserve/lrt/base/BaseEzEthWethReserveOracle.sol new file mode 100644 index 00000000..91e52967 --- /dev/null +++ b/src/oracles/reserve/lrt/base/BaseEzEthWethReserveOracle.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { WadRayMath } from "../../../../libraries/math/WadRayMath.sol"; +import { ReserveOracle } from "../../ReserveOracle.sol"; +import { BASE_EZETH_ETH_EXCHANGE_RATE_CHAINLINK, BASE_SEQUENCER_UPTIME_FEED } from "../../../../Constants.sol"; +import { SafeCast } from "openzeppelin-contracts/contracts/utils/math/SafeCast.sol"; + +/** + * @notice Reserve Oracle for ezETH denominated in WETH. + * + * @custom:security-contact security@molecularlabs.io + */ +contract BaseEzEthWethReserveOracle is ReserveOracle { + using WadRayMath for uint256; + using SafeCast for int256; + + error SequencerDown(); + error GracePeriodNotOver(); + error MaxTimeFromLastUpdateExceeded(uint256, uint256); + + uint256 public immutable MAX_TIME_FROM_LAST_UPDATE; // seconds + uint256 public immutable GRACE_PERIOD; + + /** + * @notice Creates a new `BaseEzEthWethReserveOracle` instance. Provides + * the amount of WETH equal to one ezETH (ETH / ezETH). + * @dev The value of ezETH denominated in WETH by Chainlink. + * @param _feeds List of alternative data sources for the WETH/ezETH exchange rate. + * @param _quorum The amount of alternative data sources to aggregate. + * @param _maxChange Maximum percent change between exchange rate updates. [RAY] + */ + constructor( + uint8 _ilkIndex, + address[] memory _feeds, + uint8 _quorum, + uint256 _maxChange, + uint256 _maxTimeFromLastUpdate, + uint256 _gracePeriod + ) + ReserveOracle(_ilkIndex, _feeds, _quorum, _maxChange) + { + MAX_TIME_FROM_LAST_UPDATE = _maxTimeFromLastUpdate; + GRACE_PERIOD = _gracePeriod; + _initializeExchangeRate(); + } + + function _getProtocolExchangeRate() internal view override returns (uint256) { + ( + /*uint80 roundID*/ + , + int256 answer, + uint256 startedAt, + /*uint256 updatedAt*/ + , + /*uint80 answeredInRound*/ + ) = BASE_SEQUENCER_UPTIME_FEED.latestRoundData(); + + if (answer == 1) revert SequencerDown(); + if (block.timestamp - startedAt <= GRACE_PERIOD) revert GracePeriodNotOver(); + + (, int256 ethPerEzEth,, uint256 ethPerEzEthUpdatedAt,) = + BASE_EZETH_ETH_EXCHANGE_RATE_CHAINLINK.latestRoundData(); + + if (block.timestamp - ethPerEzEthUpdatedAt > MAX_TIME_FROM_LAST_UPDATE) { + revert MaxTimeFromLastUpdateExceeded(block.timestamp - ethPerEzEthUpdatedAt, MAX_TIME_FROM_LAST_UPDATE); + } else { + return ethPerEzEth.toUint256(); + } + } +} diff --git a/src/oracles/spot/lrt/base/BaseEzEthWethSpotOracle.sol b/src/oracles/spot/lrt/base/BaseEzEthWethSpotOracle.sol new file mode 100644 index 00000000..012c9619 --- /dev/null +++ b/src/oracles/spot/lrt/base/BaseEzEthWethSpotOracle.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.21; + +import { SpotOracle } from "../../../../oracles/spot/SpotOracle.sol"; +import { WadRayMath } from "../../../../libraries/math/WadRayMath.sol"; +import { BASE_SEQUENCER_UPTIME_FEED, BASE_EZETH_ETH_PRICE_CHAINLINK } from "../../../../Constants.sol"; + +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +/** + * @notice The ezETH spot oracle denominated in WETH on Base. + * + * @custom:security-contact security@molecularlabs.io + */ +contract BaseEzEthWethSpotOracle is SpotOracle { + using WadRayMath for uint256; + using SafeCast for int256; + + error SequencerDown(); + error GracePeriodNotOver(); + + /** + * @notice The maximum delay for the oracle update in seconds before the + * data is considered stale. + */ + uint256 public immutable MAX_TIME_FROM_LAST_UPDATE; // seconds + + /** + * @notice Amount of time to wait after the sequencer restarts. + */ + uint256 public immutable GRACE_PERIOD; + + /** + * @notice Creates a new `WeEthWethSpotOracle` instance. + * @param _ltv The loan to value ratio for the weETH/WETH market. + * @param _reserveOracle The associated reserve oracle. + * @param _maxTimeFromLastUpdate The maximum delay for the oracle update in seconds + */ + constructor( + uint256 _ltv, + address _reserveOracle, + uint256 _maxTimeFromLastUpdate, + uint256 _gracePeriod + ) + SpotOracle(_ltv, _reserveOracle) + { + MAX_TIME_FROM_LAST_UPDATE = _maxTimeFromLastUpdate; + GRACE_PERIOD = _gracePeriod; + } + + /** + * @notice Gets the price of weETH in WETH. + * @dev Redstone oracle returns ETH per weETH with 8 decimals. This + * @return wethPerWeEth price of weETH in WETH. [WAD] + */ + function getPrice() public view override returns (uint256) { + ( + /*uint80 roundID*/ + , + int256 answer, + uint256 startedAt, + /*uint256 updatedAt*/ + , + /*uint80 answeredInRound*/ + ) = BASE_SEQUENCER_UPTIME_FEED.latestRoundData(); + + if (answer == 1) revert SequencerDown(); + if (block.timestamp - startedAt <= GRACE_PERIOD) revert GracePeriodNotOver(); + + ( + /*uint80 roundID*/ + , + int256 ethPerEzEth, + /*uint startedAt*/ + , + uint256 ethPerEzEthUpdatedAt, + /*uint80 answeredInRound*/ + ) = BASE_EZETH_ETH_PRICE_CHAINLINK.latestRoundData(); // [WAD] + + if (block.timestamp - ethPerEzEthUpdatedAt > MAX_TIME_FROM_LAST_UPDATE) { + return 0; // collateral valuation is zero if oracle data is stale + } else { + return ethPerEzEth.toUint256(); // [wad] + } + } +} diff --git a/test/fork/concrete/handlers-base/LrtUniswapFlashswapHandler.t.sol b/test/fork/concrete/handlers-base/LrtUniswapFlashswapHandler.t.sol new file mode 100644 index 00000000..3a1a615c --- /dev/null +++ b/test/fork/concrete/handlers-base/LrtUniswapFlashswapHandler.t.sol @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { LrtHandler_ForkBase } from "../../../helpers/handlers/LrtHandlerForkBase.sol"; +import { WadRayMath, RAY, WAD } from "../../../../src/libraries/math/WadRayMath.sol"; +import { UniswapFlashswapHandler } from "../../../../src/flash/UniswapFlashswapHandler.sol"; +import { IonHandlerBase } from "../../../../src/flash/IonHandlerBase.sol"; +import { Whitelist } from "../../../../src/Whitelist.sol"; + +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; + +import { Vm } from "forge-std/Vm.sol"; +import { console2 } from "forge-std/console2.sol"; + +using WadRayMath for uint256; + +abstract contract UniswapFlashswapHandler_Test is LrtHandler_ForkBase { + uint160 sqrtPriceLimitX96; + + function _getUniswapPools() internal pure virtual returns (address[] memory uniswapPools); + + function testFork_FlashswapLeverage() external { + uint256 initialDeposit = 1e18; + uint256 resultingAdditionalCollateral = 5e18; + uint256 maxResultingDebt = 6e18; // In weth + + weth.approve(address(_getTypedUFHandler()), type(uint256).max); + ionPool.addOperator(address(_getTypedUFHandler())); + + vm.expectRevert(abi.encodeWithSelector(IonHandlerBase.TransactionDeadlineReached.selector, block.timestamp)); + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingAdditionalCollateral, + maxResultingDebt, + sqrtPriceLimitX96, + block.timestamp, + borrowerWhitelistProof + ); + + if (Whitelist(whitelist).borrowersRoot(0) != 0) { + vm.expectRevert(abi.encodeWithSelector(Whitelist.NotWhitelistedBorrower.selector, 0, address(this))); + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingAdditionalCollateral, + maxResultingDebt, + sqrtPriceLimitX96, + block.timestamp + 1, + new bytes32[](0) + ); + } + + uint256 gasBefore = gasleft(); + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingAdditionalCollateral, + maxResultingDebt, + sqrtPriceLimitX96, + block.timestamp + 1, + borrowerWhitelistProof + ); + uint256 gasAfter = gasleft(); + if (vm.envOr("SHOW_GAS", uint256(0)) == 1) console2.log("Gas used: %d", gasBefore - gasAfter); + + uint256 currentRate = ionPool.rate(_getIlkIndex()); + uint256 roundingError = currentRate / RAY; + + assertEq(ionPool.collateral(_getIlkIndex(), address(this)), resultingAdditionalCollateral); + assertEq(IERC20(ionPool.getIlkAddress(_getIlkIndex())).balanceOf(address(_getTypedUFHandler())), 0); + assertLe(weth.balanceOf(address(_getTypedUFHandler())), roundingError); + assertLt( + ionPool.normalizedDebt(_getIlkIndex(), address(this)).rayMulUp(ionPool.rate(_getIlkIndex())), + maxResultingDebt + ); + } + + function testFork_FlashswapDeleveragePartial() external { + uint256 initialDeposit = 1e18; + uint256 resultingAdditionalCollateral = 5e18; + uint256 maxResultingDebt = type(uint256).max; + + weth.approve(address(_getTypedUFHandler()), type(uint256).max); + ionPool.addOperator(address(_getTypedUFHandler())); + + vm.recordLogs(); + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingAdditionalCollateral, + maxResultingDebt, + sqrtPriceLimitX96, + block.timestamp + 1, + borrowerWhitelistProof + ); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + uint256 normalizedDebtCreated; + for (uint256 i = 0; i < entries.length; i++) { + // keccak256("Borrow(uint8,address,address,uint256,uint256,uint256)") + if (entries[i].topics[0] != 0xe3e92e977f830d2a0b92c58e8866694b5dc929a35e2b95846f427de0f0bb412f) continue; + normalizedDebtCreated = abi.decode(entries[i].data, (uint256)); + } + + assertEq(ionPool.collateral(_getIlkIndex(), address(this)), resultingAdditionalCollateral); + assertLt( + ionPool.normalizedDebt(_getIlkIndex(), address(this)).rayMulUp(ionPool.rate(_getIlkIndex())), + maxResultingDebt + ); + assertEq(ionPool.normalizedDebt(_getIlkIndex(), address(this)), normalizedDebtCreated); + + vm.warp(block.timestamp + 3 hours); + + uint256 slippageAndFeeTolerance = 1.01e18; // 1% + // Want to completely deleverage position and only leave initial capital + // in vault + uint256 maxCollateralToRemove = (resultingAdditionalCollateral - initialDeposit) * slippageAndFeeTolerance / WAD; + // Remove all debt + uint256 normalizedDebtToRemove = ionPool.normalizedDebt(_getIlkIndex(), address(this)); + + // Round up otherwise can leave 1 wei of dust in debt left + uint256 debtToRemove = normalizedDebtToRemove.rayMulUp(ionPool.rate(_getIlkIndex())); + + vm.expectRevert(abi.encodeWithSelector(IonHandlerBase.TransactionDeadlineReached.selector, block.timestamp)); + _getTypedUFHandler().flashswapDeleverage(maxCollateralToRemove, debtToRemove, 0, block.timestamp); + + _getTypedUFHandler().flashswapDeleverage(maxCollateralToRemove, debtToRemove, 0, block.timestamp + 1); + + uint256 currentRate = ionPool.rate(_getIlkIndex()); + uint256 roundingError = currentRate / RAY; + + assertGe( + ionPool.collateral(_getIlkIndex(), address(this)), resultingAdditionalCollateral - maxCollateralToRemove + ); + assertEq(ionPool.normalizedDebt(_getIlkIndex(), address(this)), 0); + assertEq(IERC20(ionPool.getIlkAddress(_getIlkIndex())).balanceOf(address(_getTypedUFHandler())), 0); + assertLe(weth.balanceOf(address(_getTypedUFHandler())), roundingError); + } + + function testFork_FlashswapDeleverageFull() external { + uint256 initialDeposit = 1e18; + uint256 resultingAdditionalCollateral = 5e18; + uint256 maxResultingDebt = type(uint256).max; + + weth.approve(address(_getTypedUFHandler()), type(uint256).max); + ionPool.addOperator(address(_getTypedUFHandler())); + + vm.recordLogs(); + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingAdditionalCollateral, + maxResultingDebt, + sqrtPriceLimitX96, + block.timestamp + 1, + borrowerWhitelistProof + ); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + uint256 normalizedDebtCreated; + for (uint256 i = 0; i < entries.length; i++) { + // keccak256("Borrow(uint8,address,address,uint256,uint256,uint256)") + if (entries[i].topics[0] != 0xe3e92e977f830d2a0b92c58e8866694b5dc929a35e2b95846f427de0f0bb412f) continue; + normalizedDebtCreated = abi.decode(entries[i].data, (uint256)); + } + + assertEq(ionPool.collateral(_getIlkIndex(), address(this)), resultingAdditionalCollateral); + assertLt( + ionPool.normalizedDebt(_getIlkIndex(), address(this)).rayMulUp(ionPool.rate(_getIlkIndex())), + maxResultingDebt + ); + assertEq(ionPool.normalizedDebt(_getIlkIndex(), address(this)), normalizedDebtCreated); + + uint256 slippageAndFeeTolerance = 1.01e18; // 1% + // Want to completely deleverage position and only leave initial capital + // in vault + uint256 maxCollateralToRemove = (resultingAdditionalCollateral - initialDeposit) * slippageAndFeeTolerance / WAD; + + // Remove all debt + uint256 debtToRemove = type(uint256).max; + + _getTypedUFHandler().flashswapDeleverage(maxCollateralToRemove, debtToRemove, 0, block.timestamp + 1); + + uint256 currentRate = ionPool.rate(_getIlkIndex()); + uint256 roundingError = currentRate / RAY; + + assertGe( + ionPool.collateral(_getIlkIndex(), address(this)), resultingAdditionalCollateral - maxCollateralToRemove + ); + assertEq(ionPool.normalizedDebt(_getIlkIndex(), address(this)), 0); + assertEq(IERC20(ionPool.getIlkAddress(_getIlkIndex())).balanceOf(address(_getTypedUFHandler())), 0); + assertLe(weth.balanceOf(address(_getTypedUFHandler())), roundingError); + } + + function testFork_RevertWhen_UntrustedCallerCallsFlashswapCallback() external { + vm.skip(borrowerWhitelistProof.length > 0); + + vm.expectRevert( + abi.encodeWithSelector(UniswapFlashswapHandler.CallbackOnlyCallableByPool.selector, address(this)) + ); + _getTypedUFHandler().uniswapV3SwapCallback(1, 1, ""); + } + + function testFork_RevertWhen_TradingInZeroLiquidityRegion() external { + vm.skip(borrowerWhitelistProof.length > 0); + + vm.prank(address(_getUniswapPools()[_getIlkIndex()])); + vm.expectRevert(UniswapFlashswapHandler.InvalidZeroLiquidityRegionSwap.selector); + _getTypedUFHandler().uniswapV3SwapCallback(0, 0, ""); + } + + function testFork_RevertWhen_FlashswapLeverageCreatesMoreDebtThanUserIsWilling() external { + vm.skip(borrowerWhitelistProof.length > 0); + + uint256 initialDeposit = 1e18; + uint256 resultingAdditionalCollateral = 5e18; + uint256 maxResultingDebt = 3e18; // In weth + + weth.approve(address(_getTypedUFHandler()), type(uint256).max); + ionPool.addOperator(address(_getTypedUFHandler())); + + vm.expectRevert(); + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingAdditionalCollateral, + maxResultingDebt, + sqrtPriceLimitX96, + block.timestamp + 1, + new bytes32[](0) + ); + } + + function testFork_RevertWhen_FlashswapDeleverageSellsMoreCollateralThanUserIsWilling() external { + vm.skip(borrowerWhitelistProof.length > 0); + + uint256 initialDeposit = 1e18; + uint256 resultingAdditionalCollateral = 5e18; + uint256 maxResultingDebt = type(uint256).max; + + weth.approve(address(_getTypedUFHandler()), type(uint256).max); + ionPool.addOperator(address(_getTypedUFHandler())); + + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingAdditionalCollateral, + maxResultingDebt, + sqrtPriceLimitX96, + block.timestamp + 1, + new bytes32[](0) + ); + + uint256 slippageAndFeeTolerance = 1.0e18; // 0% + // Want to completely deleverage position and only leave initial capital + // in vault + uint256 maxCollateralToRemove = (resultingAdditionalCollateral - initialDeposit) * slippageAndFeeTolerance / WAD; + // Remove all debt + uint256 normalizedDebtToRemove = ionPool.normalizedDebt(_getIlkIndex(), address(this)); + + // Round up otherwise can leave 1 wei of dust in debt left + uint256 debtToRemove = normalizedDebtToRemove.rayMulUp(ionPool.rate(_getIlkIndex())); + + vm.expectRevert(); + _getTypedUFHandler().flashswapDeleverage(maxCollateralToRemove, debtToRemove, 0, block.timestamp + 1); + } + + function _getTypedUFHandler() private view returns (UniswapFlashswapHandler) { + return UniswapFlashswapHandler(payable(_getHandler())); + } +} diff --git a/test/fork/concrete/lrt/ReserveOracle.t.sol b/test/fork/concrete/lrt/ReserveOracle.t.sol index f7aa41c3..0eca78fb 100644 --- a/test/fork/concrete/lrt/ReserveOracle.t.sol +++ b/test/fork/concrete/lrt/ReserveOracle.t.sol @@ -7,6 +7,7 @@ import { RsEthWstEthReserveOracle } from "../../../../src/oracles/reserve/lrt/Rs import { RswEthWstEthReserveOracle } from "../../../../src/oracles/reserve/lrt/RswEthWstEthReserveOracle.sol"; import { EzEthWethReserveOracle } from "./../../../../src/oracles/reserve/lrt/EzEthWethReserveOracle.sol"; import { WeEthWethReserveOracle } from "./../../../../src/oracles/reserve/lrt/WeEthWethReserveOracle.sol"; +import { BaseEzEthWethReserveOracle } from "./../../../../src/oracles/reserve/lrt/base/BaseEzEthWethReserveOracle.sol"; import { WadRayMath } from "../../../../src/libraries/math/WadRayMath.sol"; import { UPDATE_COOLDOWN } from "../../../../src/oracles/reserve/ReserveOracle.sol"; import { @@ -20,7 +21,8 @@ import { RENZO_RESTAKE_MANAGER, BASE_WEETH_ETH_EXCHANGE_RATE_CHAINLINK, ETHER_FI_LIQUIDITY_POOL_ADDRESS, - WEETH_ADDRESS + WEETH_ADDRESS, + BASE_EZETH_ETH_EXCHANGE_RATE_CHAINLINK } from "../../../../src/Constants.sol"; import { ReserveOracleSharedSetup } from "../../../helpers/ReserveOracleSharedSetup.sol"; import { StdStorage, stdStorage } from "../../../../lib/forge-safe/lib/forge-std/src/StdStorage.sol"; @@ -488,3 +490,68 @@ contract WeEthWethReserveOracle_ForkTest is ReserveOracle_ForkTest { require(newExchangeRate < prevExchangeRate, "price should decrease"); } } + +contract BaseEzEthWethReserveOracle_ForkTest is ReserveOracle_ForkTest { + using SafeCast for int256; + + error MaxTimeFromLastUpdateExceeded(uint256, uint256); + + uint256 public immutable MAX_TIME_FROM_LAST_UPDATE = 87_000; // seconds + uint256 public immutable GRACE_PERIOD = 3600; + + function setUp() public override { + super.setUp(); + reserveOracle = new BaseEzEthWethReserveOracle( + ILK_INDEX, emptyFeeds, QUORUM, MAX_CHANGE, MAX_TIME_FROM_LAST_UPDATE, GRACE_PERIOD + ); + } + + function _getForkRpc() internal override returns (string memory) { + return vm.envString("BASE_MAINNET_RPC_URL"); + } + + function _convertToEth(uint256 amt) internal view override returns (uint256) { + return amt; + } + + function _getProtocolExchangeRate() internal view override returns (uint256) { + (, int256 ethPerEzEth,, uint256 ethPerEzEthUpdatedAt,) = + BASE_EZETH_ETH_EXCHANGE_RATE_CHAINLINK.latestRoundData(); + if (block.timestamp - ethPerEzEthUpdatedAt > MAX_TIME_FROM_LAST_UPDATE) { + revert MaxTimeFromLastUpdateExceeded(block.timestamp, ethPerEzEthUpdatedAt); + } else { + return ethPerEzEth.toUint256(); // [WAD] + } + } + + // --- Slashing Scenario --- + function _increaseExchangeRate() internal override returns (uint256 newPrice) { + // Replace the Chainlink contract that returns the exchange rate with a + // new dummy contract that returns a higher exchange rate. + (, int256 prevExchangeRate,,,) = BASE_EZETH_ETH_EXCHANGE_RATE_CHAINLINK.latestRoundData(); + + MockChainlink chainlink = new MockChainlink(); + + vm.etch(address(BASE_EZETH_ETH_EXCHANGE_RATE_CHAINLINK), address(chainlink).code); + + MockChainlink(address(BASE_EZETH_ETH_EXCHANGE_RATE_CHAINLINK)).setExchangeRate(1.8e18); + + (, int256 newExchangeRate,,,) = BASE_EZETH_ETH_EXCHANGE_RATE_CHAINLINK.latestRoundData(); + + require(newExchangeRate > prevExchangeRate, "price should increase"); + } + + function _decreaseExchangeRate() internal override returns (uint256 newPrice) { + (, int256 prevExchangeRate,,,) = BASE_EZETH_ETH_EXCHANGE_RATE_CHAINLINK.latestRoundData(); + + MockChainlink chainlink = new MockChainlink(); + + vm.etch(address(BASE_EZETH_ETH_EXCHANGE_RATE_CHAINLINK), address(chainlink).code); + + MockChainlink(address(BASE_EZETH_ETH_EXCHANGE_RATE_CHAINLINK)).setExchangeRate(0.5e18); + + (, int256 newExchangeRate,,,) = BASE_EZETH_ETH_EXCHANGE_RATE_CHAINLINK.latestRoundData(); + + require(newExchangeRate < prevExchangeRate, "price should decrease"); + } +} diff --git a/test/fork/concrete/lrt/SpotOracle.t.sol b/test/fork/concrete/lrt/SpotOracle.t.sol index c28f96f4..d910f28e 100644 --- a/test/fork/concrete/lrt/SpotOracle.t.sol +++ b/test/fork/concrete/lrt/SpotOracle.t.sol @@ -16,7 +16,8 @@ import { EzEthWethReserveOracle } from "./../../../../src/oracles/reserve/lrt/Ez import { EzEthWethSpotOracle } from "./../../../../src/oracles/spot/lrt/EzEthWethSpotOracle.sol"; import { WeEthWethReserveOracle } from "./../../../../src/oracles/reserve/lrt/WeEthWethReserveOracle.sol"; import { WeEthWethSpotOracle } from "./../../../../src/oracles/spot/lrt/WeEthWethSpotOracle.sol"; - +import { BaseEzEthWethReserveOracle } from "./../../../../src/oracles/reserve/lrt/base/BaseEzEthWethReserveOracle.sol"; +import { BaseEzEthWethSpotOracle } from "./../../../../src/oracles/spot/lrt/base/BaseEzEthWethSpotOracle.sol"; import { WadRayMath } from "../../../../src/libraries/math/WadRayMath.sol"; import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; @@ -158,3 +159,22 @@ contract WeEthWethSpotOracle_ForkTest is SpotOracle_ForkTest { return vm.envString("BASE_MAINNET_RPC_URL"); } } + +contract BaseEzEthWethSpotOracle_ForkTest is SpotOracle_ForkTest { + uint256 constant GRACE_PERIOD = 3600; + uint256 constant MAX_TIME_FROM_LAST_UPDATE = 87_000; + uint256 constant MAX_LTV = 0.8e27; + + function setUp() public override { + super.setUp(); + reserveOracle = new BaseEzEthWethReserveOracle( + ILK_INDEX, emptyFeeds, QUORUM, DEFAULT_MAX_CHANGE, MAX_TIME_FROM_LAST_UPDATE, GRACE_PERIOD + ); + spotOracle = + new BaseEzEthWethSpotOracle(MAX_LTV, address(reserveOracle), MAX_TIME_FROM_LAST_UPDATE, GRACE_PERIOD); + } + + function _getForkRpc() internal override returns (string memory) { + return vm.envString("BASE_MAINNET_RPC_URL"); + } +} diff --git a/test/fork/concrete/lrt/base/BaseEzEthWethHandler.t.sol b/test/fork/concrete/lrt/base/BaseEzEthWethHandler.t.sol new file mode 100644 index 00000000..c8ee30c8 --- /dev/null +++ b/test/fork/concrete/lrt/base/BaseEzEthWethHandler.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.21; + +import { UniswapFlashswapHandler_Test } from "../../handlers-base/LrtUniswapFlashswapHandler.t.sol"; +import { BaseEzEthWethHandler } from "./../../../../../src/flash/lrt/base/BaseEzEthWethHandler.sol"; +import { + BASE_WETH, + BASE_EZETH, + BASE_EZETH_WETH_AERODROME, + BASE_EZETH_ETH_PRICE_CHAINLINK +} from "./../../../../../src/Constants.sol"; +import { Whitelist } from "./../../../../../src/Whitelist.sol"; +import { IProviderLibraryExposed } from "./../../../../helpers/IProviderLibraryExposed.sol"; + +import { IERC20 } from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; +import { SafeCast } from "openzeppelin-contracts/contracts/utils/math/SafeCast.sol"; + +contract BaseEzEthWethHandler_ForkTest is UniswapFlashswapHandler_Test { + using SafeCast for int256; + + BaseEzEthWethHandler handler; + uint8 constant ILK_INDEX = 0; + + function setUp() public virtual override { + super.setUp(); + + handler = new BaseEzEthWethHandler( + ILK_INDEX, + ionPool, + gemJoins[ILK_INDEX], + Whitelist(whitelist), + BASE_EZETH_WETH_AERODROME, + false, // _wethIsToken0 + BASE_WETH + ); + + BASE_EZETH.approve(address(handler), type(uint256).max); + + deal(address(BASE_EZETH), address(this), INITIAL_BORROWER_COLLATERAL_BALANCE); + } + + function _getUnderlying() internal pure override returns (address) { + return address(BASE_WETH); + } + + function _getCollaterals() internal view override returns (IERC20[] memory _collaterals) { + _collaterals = new IERC20[](1); + _collaterals[0] = BASE_EZETH; + } + + // Only used for constructing IonRegistry which is now deprecated + function _getDepositContracts() internal view override returns (address[] memory) { + address[] memory _depositContracts = new address[](1); + _depositContracts[0] = address(handler); + return _depositContracts; + } + + function _getIlkIndex() internal view override returns (uint8) { + return ILK_INDEX; + } + + function _getHandler() internal view override returns (address) { + return address(handler); + } + + function _getForkRpc() internal view override returns (string memory) { + return vm.envString("BASE_MAINNET_RPC_URL"); + } + + // Should be unused + function _getProviderLibrary() internal view override returns (IProviderLibraryExposed) { + return IProviderLibraryExposed(address(0)); + } + + function _getInitialSpotPrice() internal view override returns (uint256) { + ( + /*uint80 roundID*/ + , + int256 ethPerEzEth, + /*uint startedAt*/ + , + uint256 ethPerEzEthUpdatedAt, + /*uint80 answeredInRound*/ + ) = BASE_EZETH_ETH_PRICE_CHAINLINK.latestRoundData(); // [WAD] + + return ethPerEzEth.toUint256(); + } + + // These are aerodrome pools that are just forks of UniV3. + function _getUniswapPools() internal pure override returns (address[] memory uniswapPools) { + uniswapPools = new address[](1); + uniswapPools[0] = address(BASE_EZETH_WETH_AERODROME); + } +} diff --git a/test/fork/fuzz/handlers-base/LrtUniswapFlashswapHandler.t.sol b/test/fork/fuzz/handlers-base/LrtUniswapFlashswapHandler.t.sol new file mode 100644 index 00000000..31e4063d --- /dev/null +++ b/test/fork/fuzz/handlers-base/LrtUniswapFlashswapHandler.t.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { LrtHandler_ForkBase } from "../../../helpers/handlers/LrtHandlerForkBase.sol"; +import { WadRayMath, RAY, WAD } from "../../../../src/libraries/math/WadRayMath.sol"; +import { UniswapFlashswapHandler } from "../../../../src/flash/UniswapFlashswapHandler.sol"; + +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; + +import { Vm } from "forge-std/Vm.sol"; + +using WadRayMath for uint256; + +struct Config { + uint256 initialDepositLowerBound; +} + +abstract contract UniswapFlashswapHandler_FuzzTest is LrtHandler_ForkBase { + uint160 sqrtPriceLimitX96; + Config ufConfig; + + function testForkFuzz_FlashswapLeverage(uint256 initialDeposit, uint256 resultingCollateralMultiplier) public { + initialDeposit = bound(initialDeposit, ufConfig.initialDepositLowerBound, INITIAL_THIS_UNDERLYING_BALANCE); + uint256 resultingCollateral = initialDeposit * bound(resultingCollateralMultiplier, 1, 5); + uint256 maxResultingDebt = resultingCollateral; // in weth. This is technically subject to slippage but we will + // skip protecting for this in the test + + weth.approve(address(_getTypedUFHandler()), type(uint256).max); + ionPool.addOperator(address(_getTypedUFHandler())); + + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingCollateral, + maxResultingDebt, + sqrtPriceLimitX96, + block.timestamp + 1, + new bytes32[](0) + ); + + uint256 currentRate = ionPool.rate(_getIlkIndex()); + uint256 roundingError = currentRate / RAY; + + assertEq(ionPool.collateral(_getIlkIndex(), address(this)), resultingCollateral); + assertEq(IERC20(address(_getCollaterals()[_getIlkIndex()])).balanceOf(address(_getTypedUFHandler())), 0); + assertLe(weth.balanceOf(address(_getTypedUFHandler())), roundingError); + assertLt( + ionPool.normalizedDebt(_getIlkIndex(), address(this)).rayMulUp(ionPool.rate(_getIlkIndex())), + maxResultingDebt + ); + } + + function testForkFuzz_FlashswapDeleverage(uint256 initialDeposit, uint256 resultingCollateralMultiplier) public { + initialDeposit = bound(initialDeposit, ufConfig.initialDepositLowerBound, INITIAL_THIS_UNDERLYING_BALANCE); + uint256 resultingCollateral = initialDeposit * bound(resultingCollateralMultiplier, 1, 5); + uint256 maxResultingDebt = resultingCollateral; // in weth. This is technically subject to slippage but we will + // skip protecting for this in the test + + weth.approve(address(_getTypedUFHandler()), type(uint256).max); + ionPool.addOperator(address(_getTypedUFHandler())); + + vm.recordLogs(); + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingCollateral, + maxResultingDebt, + sqrtPriceLimitX96, + block.timestamp + 1, + new bytes32[](0) + ); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + uint256 normalizedDebtCreated; + for (uint256 i = 0; i < entries.length; i++) { + // keccak256("Borrow(uint8,address,address,uint256,uint256,uint256)") + if (entries[i].topics[0] != 0xe3e92e977f830d2a0b92c58e8866694b5dc929a35e2b95846f427de0f0bb412f) continue; + normalizedDebtCreated = abi.decode(entries[i].data, (uint256)); + } + + assertEq(ionPool.collateral(_getIlkIndex(), address(this)), resultingCollateral); + assertLt( + ionPool.normalizedDebt(_getIlkIndex(), address(this)).rayMulUp(ionPool.rate(_getIlkIndex())), + maxResultingDebt + ); + assertEq(ionPool.normalizedDebt(_getIlkIndex(), address(this)), normalizedDebtCreated); + + vm.warp(block.timestamp + 3 hours); + + uint256 slippageAndFeeTolerance = 1.01e18; // 1% + // Want to completely deleverage position and only leave initial capital + // in vault + uint256 maxCollateralToRemove = (resultingCollateral - initialDeposit) * slippageAndFeeTolerance / WAD; + // Remove all debt + uint256 normalizedDebtToRemove = ionPool.normalizedDebt(_getIlkIndex(), address(this)); + + // Round up otherwise can leave 1 wei of dust in debt left + uint256 debtToRemove = normalizedDebtToRemove.rayMulUp(ionPool.rate(_getIlkIndex())); + + _getTypedUFHandler().flashswapDeleverage(maxCollateralToRemove, debtToRemove, 0, block.timestamp + 1); + + uint256 currentRate = ionPool.rate(_getIlkIndex()); + uint256 roundingError = currentRate / RAY; + + assertGe(ionPool.collateral(_getIlkIndex(), address(this)), resultingCollateral - maxCollateralToRemove); + assertEq(ionPool.normalizedDebt(_getIlkIndex(), address(this)), 0); + assertEq(IERC20(address(_getCollaterals()[_getIlkIndex()])).balanceOf(address(_getTypedUFHandler())), 0); + assertLe(weth.balanceOf(address(_getTypedUFHandler())), roundingError); + } + + function testForkFuzz_FlashswapDeleverageFull( + uint256 initialDeposit, + uint256 resultingCollateralMultiplier + ) + public + { + initialDeposit = bound(initialDeposit, ufConfig.initialDepositLowerBound, INITIAL_THIS_UNDERLYING_BALANCE); + uint256 resultingCollateral = initialDeposit * bound(resultingCollateralMultiplier, 1, 5); + uint256 maxResultingDebt = resultingCollateral; // in weth. This is technically subject to slippage but we will + // skip protecting for this in the test + + weth.approve(address(_getTypedUFHandler()), type(uint256).max); + ionPool.addOperator(address(_getTypedUFHandler())); + + vm.recordLogs(); + _getTypedUFHandler().flashswapLeverage( + initialDeposit, + resultingCollateral, + maxResultingDebt, + sqrtPriceLimitX96, + block.timestamp + 1, + new bytes32[](0) + ); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + uint256 normalizedDebtCreated; + for (uint256 i = 0; i < entries.length; i++) { + // keccak256("Borrow(uint8,address,address,uint256,uint256,uint256)") + if (entries[i].topics[0] != 0xe3e92e977f830d2a0b92c58e8866694b5dc929a35e2b95846f427de0f0bb412f) continue; + normalizedDebtCreated = abi.decode(entries[i].data, (uint256)); + } + + assertEq(ionPool.collateral(_getIlkIndex(), address(this)), resultingCollateral); + assertLt( + ionPool.normalizedDebt(_getIlkIndex(), address(this)).rayMulUp(ionPool.rate(_getIlkIndex())), + maxResultingDebt + ); + assertEq(ionPool.normalizedDebt(_getIlkIndex(), address(this)), normalizedDebtCreated); + + uint256 slippageAndFeeTolerance = 1.01e18; // 1% + // Want to completely deleverage position and only leave initial capital + // in vault + uint256 maxCollateralToRemove = (resultingCollateral - initialDeposit) * slippageAndFeeTolerance / WAD; + uint256 normalizedDebtCurrent = ionPool.normalizedDebt(_getIlkIndex(), address(this)); + + // Remove all debt if any + uint256 debtToRemove = normalizedDebtCurrent == 0 ? 0 : type(uint256).max; + + _getTypedUFHandler().flashswapDeleverage(maxCollateralToRemove, debtToRemove, 0, block.timestamp + 1); + + uint256 currentRate = ionPool.rate(_getIlkIndex()); + uint256 roundingError = currentRate / RAY; + + assertGe(ionPool.collateral(_getIlkIndex(), address(this)), resultingCollateral - maxCollateralToRemove); + assertEq(ionPool.normalizedDebt(_getIlkIndex(), address(this)), 0); + assertEq(IERC20(address(_getCollaterals()[_getIlkIndex()])).balanceOf(address(_getTypedUFHandler())), 0); + assertLe(weth.balanceOf(address(_getTypedUFHandler())), roundingError); + } + + function _getTypedUFHandler() private view returns (UniswapFlashswapHandler) { + return UniswapFlashswapHandler(payable(_getHandler())); + } +} + +abstract contract UniswapFlashswapHandler_WithRateChange_FuzzTest is UniswapFlashswapHandler_FuzzTest { + function testForkFuzz_WithRateChange_FlashswapLeverage( + uint256 initialDeposit, + uint256 resultingCollateralMultiplier, + uint104 rate + ) + external + { + rate = uint104(bound(rate, 1e27, 10e27)); + ionPool.setRate(_getIlkIndex(), rate); + super.testForkFuzz_FlashswapLeverage(initialDeposit, resultingCollateralMultiplier); + } + + function testForkFuzz_WithRateChange_FlashswapDeleverage( + uint256 initialDeposit, + uint256 resultingCollateralMultiplier, + uint104 rate + ) + external + { + rate = uint104(bound(rate, 1e27, 10e27)); + ionPool.setRate(_getIlkIndex(), rate); + super.testForkFuzz_FlashswapDeleverage(initialDeposit, resultingCollateralMultiplier); + } + + function testForkFuzz_WithRateChange_FlashswapDeleverageFull( + uint256 initialDeposit, + uint256 resultingCollateralMultiplier, + uint104 rate + ) + external + { + rate = uint104(bound(rate, 1e27, 10e27)); + ionPool.setRate(_getIlkIndex(), rate); + super.testForkFuzz_FlashswapDeleverageFull(initialDeposit, resultingCollateralMultiplier); + } +} diff --git a/test/fork/fuzz/lrt/base/BaseEzEthWethHandler.t.sol b/test/fork/fuzz/lrt/base/BaseEzEthWethHandler.t.sol new file mode 100644 index 00000000..ff27ba90 --- /dev/null +++ b/test/fork/fuzz/lrt/base/BaseEzEthWethHandler.t.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.21; + +import { UniswapFlashswapHandler_WithRateChange_FuzzTest } from "../../handlers-base/LrtUniswapFlashswapHandler.t.sol"; +import { BaseEzEthWethHandler } from "./../../../../../src/flash/lrt/base/BaseEzEthWethHandler.sol"; +import { + BASE_WETH, + BASE_EZETH, + BASE_EZETH_WETH_AERODROME, + BASE_EZETH_ETH_PRICE_CHAINLINK +} from "./../../../../../src/Constants.sol"; +import { Whitelist } from "./../../../../../src/Whitelist.sol"; +import { IProviderLibraryExposed } from "./../../../../helpers/IProviderLibraryExposed.sol"; + +import { IERC20 } from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; +import { SafeCast } from "openzeppelin-contracts/contracts/utils/math/SafeCast.sol"; + +contract BaseEzEthWethHandler_ForkFuzzTest is UniswapFlashswapHandler_WithRateChange_FuzzTest { + using SafeCast for int256; + + BaseEzEthWethHandler handler; + uint8 constant ILK_INDEX = 0; + + function setUp() public virtual override { + super.setUp(); + + ufConfig.initialDepositLowerBound = 1 ether; + + handler = new BaseEzEthWethHandler( + ILK_INDEX, + ionPool, + gemJoins[ILK_INDEX], + Whitelist(whitelist), + BASE_EZETH_WETH_AERODROME, + false, // _wethIsToken0 + BASE_WETH + ); + + BASE_EZETH.approve(address(handler), type(uint256).max); + + deal(address(BASE_EZETH), address(this), INITIAL_BORROWER_COLLATERAL_BALANCE); + } + + function _getUnderlying() internal pure override returns (address) { + return address(BASE_WETH); + } + + function _getCollaterals() internal view override returns (IERC20[] memory _collaterals) { + _collaterals = new IERC20[](1); + _collaterals[0] = BASE_EZETH; + } + + // Only used for constructing IonRegistry which is now deprecated + function _getDepositContracts() internal view override returns (address[] memory) { + address[] memory _depositContracts = new address[](1); + _depositContracts[0] = address(handler); + return _depositContracts; + } + + function _getIlkIndex() internal view override returns (uint8) { + return ILK_INDEX; + } + + function _getHandler() internal view override returns (address) { + return address(handler); + } + + function _getForkRpc() internal view override returns (string memory) { + return vm.envString("BASE_MAINNET_RPC_URL"); + } + + // Should be unused + function _getProviderLibrary() internal view override returns (IProviderLibraryExposed) { + return IProviderLibraryExposed(address(0)); + } + + function _getInitialSpotPrice() internal view override returns (uint256) { + ( + /*uint80 roundID*/ + , + int256 ethPerEzEth, + /*uint startedAt*/ + , + uint256 ethPerEzEthUpdatedAt, + /*uint80 answeredInRound*/ + ) = BASE_EZETH_ETH_PRICE_CHAINLINK.latestRoundData(); // [WAD] + + return ethPerEzEth.toUint256(); + } +} From c4d57986263a8f773715ef3834c49e4fb0849693 Mon Sep 17 00:00:00 2001 From: Jun Kim <64379343+junkim012@users.noreply.github.com> Date: Thu, 25 Jul 2024 22:35:41 +0900 Subject: [PATCH 2/3] chore: set up ezETH/WETH deployment scripts --- deployment-config/00_Default.json | 2 +- script/deploy/04_DeployIonPool.s.sol | 12 ++++++------ .../05_DeployInitialReserveAndSpotOracles.s.sol | 12 ++++++------ script/deploy/08_DeployHandlers.s.sol | 16 ++++++++-------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/deployment-config/00_Default.json b/deployment-config/00_Default.json index 7bddcccb..67cadadd 100644 --- a/deployment-config/00_Default.json +++ b/deployment-config/00_Default.json @@ -1,5 +1,5 @@ { "initialDefaultAdmin": "0x94544835Cf97c631f101c5f538787fE14E2E04f6", "protocol": "0xE5a5F3A6C88B894710992e1C2626be0DEB99566E", - "ilkAddress": "0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A" + "ilkAddress": "0x2416092f143378750bb29b79eD961ab195CcEea5" } \ No newline at end of file diff --git a/script/deploy/04_DeployIonPool.s.sol b/script/deploy/04_DeployIonPool.s.sol index cdcc2dac..10507217 100644 --- a/script/deploy/04_DeployIonPool.s.sol +++ b/script/deploy/04_DeployIonPool.s.sol @@ -31,7 +31,7 @@ contract DeployIonPoolScript is DeployScript { bytes32 salt = config.readBytes32(".salt"); function createX() public returns (IonPool ionImpl, IonPool ionPool) { - // ionImpl = IonPool(config.readAddress(".ionImpl")); + ionImpl = IonPool(config.readAddress(".ionImpl")); _validateInterface(IERC20(underlying)); _validateInterface(interestRateModule); @@ -39,11 +39,11 @@ contract DeployIonPoolScript is DeployScript { // ionImpl = IonPool(); - if (deployCreate2) { - ionImpl = new IonPool{ salt: DEFAULT_SALT }(); - } else { - ionImpl = new IonPool(); - } + // if (deployCreate2) { + // ionImpl = new IonPool{ salt: DEFAULT_SALT }(); + // } else { + // ionImpl = new IonPool(); + // } _validateInterfaceIonPool(ionImpl); diff --git a/script/deploy/05_DeployInitialReserveAndSpotOracles.s.sol b/script/deploy/05_DeployInitialReserveAndSpotOracles.s.sol index 5bae94fe..699727e1 100644 --- a/script/deploy/05_DeployInitialReserveAndSpotOracles.s.sol +++ b/script/deploy/05_DeployInitialReserveAndSpotOracles.s.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.21; import { DeployScript } from "../Deploy.s.sol"; import { RAY } from "../../src/libraries/math/WadRayMath.sol"; -import { WeEthWethReserveOracle } from "./../../src/oracles/reserve/lrt/WeEthWethReserveOracle.sol"; -import { WeEthWethSpotOracle } from "./../../src/oracles/spot/lrt/WeEthWethSpotOracle.sol"; +import {BaseEzEthWethReserveOracle} from "./../../src/oracles/reserve/lrt/base/BaseEzEthWethReserveOracle.sol"; +import {BaseEzEthWethSpotOracle} from "./../../src/oracles/spot/lrt/base/BaseEzEthWethSpotOracle.sol"; import { stdJson as StdJson } from "forge-std/StdJson.sol"; @@ -30,21 +30,21 @@ contract DeployInitialReserveAndSpotOraclesScript is DeployScript { if (deployCreate2) { reserveOracle = address( - new WeEthWethReserveOracle{ salt: DEFAULT_SALT }( + new BaseEzEthWethReserveOracle{ salt: DEFAULT_SALT }( 0, new address[](3), 0, maxChange, maxTimeFromLastUpdate, gracePeriod ) ); spotOracle = address( - new WeEthWethSpotOracle{ salt: DEFAULT_SALT }( + new BaseEzEthWethSpotOracle{ salt: DEFAULT_SALT }( ltv, address(reserveOracle), maxTimeFromLastUpdate, gracePeriod ) ); } else { reserveOracle = address( - new WeEthWethReserveOracle(0, new address[](3), 0, maxChange, maxTimeFromLastUpdate, gracePeriod) + new BaseEzEthWethReserveOracle(0, new address[](3), 0, maxChange, maxTimeFromLastUpdate, gracePeriod) ); spotOracle = - address(new WeEthWethSpotOracle(ltv, address(reserveOracle), maxTimeFromLastUpdate, gracePeriod)); + address(new BaseEzEthWethSpotOracle(ltv, address(reserveOracle), maxTimeFromLastUpdate, gracePeriod)); } } } diff --git a/script/deploy/08_DeployHandlers.s.sol b/script/deploy/08_DeployHandlers.s.sol index b915ef70..2b9fbff8 100644 --- a/script/deploy/08_DeployHandlers.s.sol +++ b/script/deploy/08_DeployHandlers.s.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.21; -import { BASE_WETH, BASE_WEETH_WETH_BALANCER_POOL_ID, BASE_WSTETH_WETH_UNISWAP } from "../../src/Constants.sol"; +import { BASE_WETH, BASE_EZETH_WETH_AERODROME } from "../../src/Constants.sol"; import { DeployScript } from "../Deploy.s.sol"; import { IonPool } from "../../src/IonPool.sol"; import { GemJoin } from "../../src/join/GemJoin.sol"; import { Whitelist } from "../../src/Whitelist.sol"; import { IonHandlerBase } from "../../src/flash/IonHandlerBase.sol"; -import { WeEthWethHandler } from "./../../src/flash/lrt/WeEthWethHandler.sol"; +import {BaseEzEthWethHandler} from "./../../src/flash/lrt/base/BaseEzEthWethHandler.sol"; import { stdJson as StdJson } from "forge-std/StdJson.sol"; // NOTE: Different handlers will have different constructor parameters. @@ -28,23 +28,23 @@ contract DeployHandlersScript is DeployScript { _validateInterfaceIonPool(ionPool); if (deployCreate2) { - handler = new WeEthWethHandler{ salt: DEFAULT_SALT }( + handler = new BaseEzEthWethHandler{ salt: DEFAULT_SALT }( ILK_INDEX_ZERO, ionPool, gemJoin, whitelist, - BASE_WSTETH_WETH_UNISWAP, - BASE_WEETH_WETH_BALANCER_POOL_ID, + BASE_EZETH_WETH_AERODROME, + false, BASE_WETH ); } else { - handler = new WeEthWethHandler{ salt: DEFAULT_SALT }( + handler = new BaseEzEthWethHandler( ILK_INDEX_ZERO, ionPool, gemJoin, whitelist, - BASE_WSTETH_WETH_UNISWAP, - BASE_WEETH_WETH_BALANCER_POOL_ID, + BASE_EZETH_WETH_AERODROME, + false, BASE_WETH ); } From 4b679787ede98a1aa77173ec62bcd138a23f5fff Mon Sep 17 00:00:00 2001 From: Jun Kim <64379343+junkim012@users.noreply.github.com> Date: Sat, 27 Jul 2024 00:05:52 +0900 Subject: [PATCH 3/3] chore: base ezETH/WETH production deployment --- deployment-config/04_DeployIonPool.json | 7 ++++--- deployment-config/06_SetupCollateral.json | 4 ++-- deployment-config/07_DeployGemJoin.json | 2 +- deployment-config/08_DeployHandlers.json | 4 ++-- deployment-config/09_DeployLiquidation.json | 6 +++--- deployment-config/10_AdminTransfer.json | 8 ++++---- script/deploy-test/10_AdminTransfer.t.sol | 2 +- ...05_DeployInitialReserveAndSpotOracles.s.sol | 4 ++-- script/deploy/08_DeployHandlers.s.sol | 18 +++--------------- script/deploy/10_AdminTransfer.s.sol | 2 +- 10 files changed, 23 insertions(+), 34 deletions(-) diff --git a/deployment-config/04_DeployIonPool.json b/deployment-config/04_DeployIonPool.json index 57ebb657..a41ac93a 100644 --- a/deployment-config/04_DeployIonPool.json +++ b/deployment-config/04_DeployIonPool.json @@ -2,9 +2,10 @@ "underlying": "0x4200000000000000000000000000000000000006", "treasury": "0xE5a5F3A6C88B894710992e1C2626be0DEB99566E", "decimals": "18", - "name": "Ion weETH WETH Token", - "symbol": "iweETH-WETH", + "name": "Ion ezETH WETH Token", + "symbol": "iezETH-WETH", "whitelist": "0xf98248f0dA9D51d827A3C42d608acF65c77BD76A", "interestRateModule": "0x7BC91582b10c3ce83be1918daE5136B59FC55e01", - "salt": "0xa53bcb7572e19b03e4aae7000000000000000000000000000000000000000000" + "salt": "0xeba688b6a0169602b0f4c5000000000000000000000000000000000000000000", + "ionImpl": "0xB1F7395E72B0045600c946afAB7e44cB35A6EB9B" } diff --git a/deployment-config/06_SetupCollateral.json b/deployment-config/06_SetupCollateral.json index 2e00c3d2..ac7b6a77 100644 --- a/deployment-config/06_SetupCollateral.json +++ b/deployment-config/06_SetupCollateral.json @@ -1,6 +1,6 @@ { - "ionPool": "0x00000000000fA8e0FD26b4554d067CF1856De7F5", - "spotOracle": "0x5D83953248cbF1FF723978FbD3490D9a2385A52d", + "ionPool": "0x000000000054D04Bd84f465ae7aD3aC39072255D", + "spotOracle": "0x1B39e88465e097FDB5d4E4BEad3d88655786ac07", "debtCeiling": "000000000000000000000000000000000000000000000000", "dust": "4000000000000000000000000000000000000000000000" } diff --git a/deployment-config/07_DeployGemJoin.json b/deployment-config/07_DeployGemJoin.json index 81a8102e..306a6026 100644 --- a/deployment-config/07_DeployGemJoin.json +++ b/deployment-config/07_DeployGemJoin.json @@ -1,3 +1,3 @@ { - "ionPool": "0x00000000000fA8e0FD26b4554d067CF1856De7F5" + "ionPool": "0x000000000054D04Bd84f465ae7aD3aC39072255D" } diff --git a/deployment-config/08_DeployHandlers.json b/deployment-config/08_DeployHandlers.json index e060d270..bf0ec3e1 100644 --- a/deployment-config/08_DeployHandlers.json +++ b/deployment-config/08_DeployHandlers.json @@ -1,5 +1,5 @@ { - "ionPool": "0x00000000000fA8e0FD26b4554d067CF1856De7F5", - "gemJoin": "0xe21ae2d45dEDF8dEE2D854774a904d33b8700E78", + "ionPool": "0x000000000054D04Bd84f465ae7aD3aC39072255D", + "gemJoin": "0x177DAaA9aA0ab22E4B84f32B3A01d9dD57728253", "whitelist": "0xf98248f0dA9D51d827A3C42d608acF65c77BD76A" } diff --git a/deployment-config/09_DeployLiquidation.json b/deployment-config/09_DeployLiquidation.json index 005845d5..f1a0d0b5 100644 --- a/deployment-config/09_DeployLiquidation.json +++ b/deployment-config/09_DeployLiquidation.json @@ -3,7 +3,7 @@ "liquidationThreshold": "960000000000000000000000000", "maxDiscount": "200000000000000000000000000", "reserveFactor": "10000000000000000000000000", - "ionPool": "0x00000000000fA8e0FD26b4554d067CF1856De7F5", - "reserveOracle": "0x39c66dEAA7BA7576ed1498C5A0601454740e386C", - "salt": "0x7dbc99ede0e74803aa149e000000000000000000000000000000000000000000" + "ionPool": "0x000000000054D04Bd84f465ae7aD3aC39072255D", + "reserveOracle": "0xcce76ed19a17380d1a5E90adD25317F70650D92b", + "salt": "0x2aa182b96665000197a6b5000000000000000000000000000000000000000000" } diff --git a/deployment-config/10_AdminTransfer.json b/deployment-config/10_AdminTransfer.json index 6525b770..f17b84c3 100644 --- a/deployment-config/10_AdminTransfer.json +++ b/deployment-config/10_AdminTransfer.json @@ -1,8 +1,8 @@ { - "ionPool": "0x00000000000fA8e0FD26b4554d067CF1856De7F5", + "ionPool": "0x000000000054D04Bd84f465ae7aD3aC39072255D", "yieldOracle": "0x2CAe5eD3b35654499EE605cD66A9b14a0d053773", "whitelist": "0xf98248f0dA9D51d827A3C42d608acF65c77BD76A", - "proxyAdmin": "0xe559662C42FF6460BE7c7f0C67aBA789Fde53861", - "liquidation": "0x00000000009229776762B5e6b865a06afeB4444c", - "gemJoin": "0xe21ae2d45dEDF8dEE2D854774a904d33b8700E78" + "proxyAdmin": "0xb475DcdD1C820b6B9c2f03a31C298d0722a1E27C", + "liquidation": "0x0000000000B0874413358bf8E80855f67CEE5C2a", + "gemJoin": "0x177DAaA9aA0ab22E4B84f32B3A01d9dD57728253" } diff --git a/script/deploy-test/10_AdminTransfer.t.sol b/script/deploy-test/10_AdminTransfer.t.sol index 083ae411..3b53aeaf 100644 --- a/script/deploy-test/10_AdminTransfer.t.sol +++ b/script/deploy-test/10_AdminTransfer.t.sol @@ -16,7 +16,7 @@ contract DeployAdminTransferTest is DeployTestBase, AdminTransferScript { assertLe(addressSchedule, block.timestamp, "address schedule"); // assertEq(yieldOracle.pendingOwner(), protocol, "yield oracle pending owner"); - assertEq(whitelist.pendingOwner(), protocol, "whitelist pending owner"); + // assertEq(whitelist.pendingOwner(), protocol, "whitelist pending owner"); assertEq(proxyAdmin.pendingOwner(), protocol, "proxy admin pending owner"); assertTrue(ionPool.hasRole(ionPool.GEM_JOIN_ROLE(), address(gemJoin)), "gem join role"); assertTrue(ionPool.hasRole(ionPool.LIQUIDATOR_ROLE(), address(liquidation)), "gem join role"); diff --git a/script/deploy/05_DeployInitialReserveAndSpotOracles.s.sol b/script/deploy/05_DeployInitialReserveAndSpotOracles.s.sol index 699727e1..395479b7 100644 --- a/script/deploy/05_DeployInitialReserveAndSpotOracles.s.sol +++ b/script/deploy/05_DeployInitialReserveAndSpotOracles.s.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.21; import { DeployScript } from "../Deploy.s.sol"; import { RAY } from "../../src/libraries/math/WadRayMath.sol"; -import {BaseEzEthWethReserveOracle} from "./../../src/oracles/reserve/lrt/base/BaseEzEthWethReserveOracle.sol"; -import {BaseEzEthWethSpotOracle} from "./../../src/oracles/spot/lrt/base/BaseEzEthWethSpotOracle.sol"; +import { BaseEzEthWethReserveOracle } from "./../../src/oracles/reserve/lrt/base/BaseEzEthWethReserveOracle.sol"; +import { BaseEzEthWethSpotOracle } from "./../../src/oracles/spot/lrt/base/BaseEzEthWethSpotOracle.sol"; import { stdJson as StdJson } from "forge-std/StdJson.sol"; diff --git a/script/deploy/08_DeployHandlers.s.sol b/script/deploy/08_DeployHandlers.s.sol index 2b9fbff8..f3ba9454 100644 --- a/script/deploy/08_DeployHandlers.s.sol +++ b/script/deploy/08_DeployHandlers.s.sol @@ -7,7 +7,7 @@ import { IonPool } from "../../src/IonPool.sol"; import { GemJoin } from "../../src/join/GemJoin.sol"; import { Whitelist } from "../../src/Whitelist.sol"; import { IonHandlerBase } from "../../src/flash/IonHandlerBase.sol"; -import {BaseEzEthWethHandler} from "./../../src/flash/lrt/base/BaseEzEthWethHandler.sol"; +import { BaseEzEthWethHandler } from "./../../src/flash/lrt/base/BaseEzEthWethHandler.sol"; import { stdJson as StdJson } from "forge-std/StdJson.sol"; // NOTE: Different handlers will have different constructor parameters. @@ -29,23 +29,11 @@ contract DeployHandlersScript is DeployScript { if (deployCreate2) { handler = new BaseEzEthWethHandler{ salt: DEFAULT_SALT }( - ILK_INDEX_ZERO, - ionPool, - gemJoin, - whitelist, - BASE_EZETH_WETH_AERODROME, - false, - BASE_WETH + ILK_INDEX_ZERO, ionPool, gemJoin, whitelist, BASE_EZETH_WETH_AERODROME, false, BASE_WETH ); } else { handler = new BaseEzEthWethHandler( - ILK_INDEX_ZERO, - ionPool, - gemJoin, - whitelist, - BASE_EZETH_WETH_AERODROME, - false, - BASE_WETH + ILK_INDEX_ZERO, ionPool, gemJoin, whitelist, BASE_EZETH_WETH_AERODROME, false, BASE_WETH ); } } diff --git a/script/deploy/10_AdminTransfer.s.sol b/script/deploy/10_AdminTransfer.s.sol index 791ce8cc..105a2e20 100644 --- a/script/deploy/10_AdminTransfer.s.sol +++ b/script/deploy/10_AdminTransfer.s.sol @@ -49,7 +49,7 @@ contract AdminTransferScript is DeployScript { ionPool.beginDefaultAdminTransfer(protocol); // yieldOracle.transferOwnership(protocol); - whitelist.transferOwnership(protocol); + // whitelist.transferOwnership(protocol); proxyAdmin.transferOwnership(protocol); } }