|
| 1 | +import "module-alias/register"; |
| 2 | +import { ethers } from "hardhat"; |
| 3 | +import { Signer } from "ethers"; |
| 4 | +import { Account, Address } from "@utils/types"; |
| 5 | +import DeployHelper from "@utils/deploys"; |
| 6 | +import { getAccounts, getWaffleExpect } from "@utils/index"; |
| 7 | +import { setBlockNumber, setBalance } from "@utils/test/testingUtils"; |
| 8 | +import { impersonateAccount } from "./utils"; |
| 9 | +import { PRODUCTION_ADDRESSES } from "./addresses"; |
| 10 | +import { |
| 11 | + IWETH, |
| 12 | + IERC20__factory, |
| 13 | + IDebtIssuanceModule, |
| 14 | + IDebtIssuanceModule__factory, |
| 15 | + ExchangeIssuanceIcEth, |
| 16 | +} from "../../../typechain"; |
| 17 | + |
| 18 | +const expect = getWaffleExpect(); |
| 19 | + |
| 20 | +enum Exchange { |
| 21 | + None, |
| 22 | + Sushiswap, |
| 23 | + Quickswap, |
| 24 | + UniV3, |
| 25 | + Curve, |
| 26 | +} |
| 27 | + |
| 28 | +type SwapData = { |
| 29 | + path: Address[]; |
| 30 | + fees: number[]; |
| 31 | + pool: Address; |
| 32 | + exchange: Exchange; |
| 33 | +}; |
| 34 | + |
| 35 | +// Run only if integration testing is enabled |
| 36 | +if (process.env.INTEGRATIONTEST) { |
| 37 | + describe.only("ExchangeIssuanceIcEth - Redeem deleveraged icETH for ETH (Curve)", () => { |
| 38 | + const addresses = PRODUCTION_ADDRESSES; |
| 39 | + |
| 40 | + let owner: Account; |
| 41 | + let deployer: DeployHelper; |
| 42 | + let operator: Signer; |
| 43 | + |
| 44 | + let flashMint: ExchangeIssuanceIcEth; |
| 45 | + |
| 46 | + const icEthHolder = "0x4f865D78Ed3Df19c473b54C4c25Bd3958B868846"; |
| 47 | + |
| 48 | + // Use a recent mainnet block where icETH is deleveraged with stETH aToken + WETH dust |
| 49 | + setBlockNumber(23673905, false); |
| 50 | + |
| 51 | + before(async () => { |
| 52 | + [owner] = await getAccounts(); |
| 53 | + deployer = new DeployHelper(owner.wallet); |
| 54 | + |
| 55 | + console.log("Deploying ExchangeIssuanceIcEth..."); |
| 56 | + |
| 57 | + // Deploy FlashMint contract variant with Curve support |
| 58 | + // Use Aave V2 address provider since icETH has Aave V2 aTokens |
| 59 | + flashMint = await deployer.extensions.deployExchangeIssuanceIcEth( |
| 60 | + addresses.tokens.weth, |
| 61 | + addresses.dexes.uniV2.router, |
| 62 | + addresses.dexes.sushiswap.router, |
| 63 | + addresses.dexes.uniV3.router, |
| 64 | + addresses.dexes.uniV3.quoter, |
| 65 | + addresses.set.controller, |
| 66 | + addresses.set.debtIssuanceModuleV2, |
| 67 | + addresses.set.aaveLeverageModule, |
| 68 | + addresses.lending.aave.addressProvider, |
| 69 | + addresses.dexes.curve.calculator, |
| 70 | + addresses.dexes.curve.addressProvider, |
| 71 | + ); |
| 72 | + |
| 73 | + console.log("Deployed ExchangeIssuanceIcEth to:", flashMint.address); |
| 74 | + |
| 75 | + // Impersonate icETH deployer/holder and fund with ETH for gas |
| 76 | + operator = await impersonateAccount(icEthHolder); |
| 77 | + await setBalance(icEthHolder, ethers.utils.parseEther("5")); |
| 78 | + flashMint = flashMint.connect(operator); |
| 79 | + |
| 80 | + // Approve the SetToken for the flashMint contract |
| 81 | + console.log("Approving SetToken for FlashMint contract..."); |
| 82 | + await flashMint.approveSetToken(addresses.tokens.icEth); |
| 83 | + }); |
| 84 | + |
| 85 | + it("has two equity components (aSTETH + WETH) and no debt", async () => { |
| 86 | + const debtIssuance: IDebtIssuanceModule = IDebtIssuanceModule__factory.connect( |
| 87 | + addresses.set.debtIssuanceModuleV2, |
| 88 | + owner.wallet, |
| 89 | + ); |
| 90 | + const oneIcEth = ethers.utils.parseEther("1"); |
| 91 | + console.log("Fetching required components for 1 icETH..."); |
| 92 | + const [components, equityPositions, debtPositions] = await debtIssuance.getRequiredComponentRedemptionUnits( |
| 93 | + addresses.tokens.icEth, |
| 94 | + oneIcEth, |
| 95 | + ); |
| 96 | + console.log("Components:", components); |
| 97 | + console.log("Equity Positions:", equityPositions); |
| 98 | + console.log("Debt Positions:", debtPositions); |
| 99 | + |
| 100 | + expect(components.length).to.eq(2); |
| 101 | + // Expect aSTETH and WETH as components |
| 102 | + const hasASteth = components.map(addr => addr.toLowerCase()).includes(addresses.tokens.aSTETH.toLowerCase()); |
| 103 | + const hasWeth = components.map(addr => addr.toLowerCase()).includes(addresses.tokens.weth.toLowerCase()); |
| 104 | + expect(hasASteth).to.eq(true); |
| 105 | + expect(hasWeth).to.eq(true); |
| 106 | + |
| 107 | + // Both equity components should be positive, with no debt |
| 108 | + expect(equityPositions[0].gt(0)).to.eq(true); |
| 109 | + expect(equityPositions[1].gt(0)).to.eq(true); |
| 110 | + expect(debtPositions[0].eq(0)).to.eq(true); |
| 111 | + expect(debtPositions[1].eq(0)).to.eq(true); |
| 112 | + }); |
| 113 | + |
| 114 | + it("redeems current deleveraged icETH for ETH (Curve stETH-ETH + WETH unwrap)", async () => { |
| 115 | + const icEth = IERC20__factory.connect(addresses.tokens.icEth, operator); |
| 116 | + const weth = (await ethers.getContractAt("IWETH", addresses.tokens.weth)) as IWETH; |
| 117 | + |
| 118 | + const holderAddress = await operator.getAddress(); |
| 119 | + const startEth = await ethers.provider.getBalance(holderAddress); |
| 120 | + const icEthBal = await icEth.balanceOf(holderAddress); |
| 121 | + console.log("icETH Balance:", ethers.utils.formatEther(icEthBal)); |
| 122 | + |
| 123 | + expect(icEthBal.gt(0)).to.eq(true); |
| 124 | + |
| 125 | + const redeemAmount = icEthBal; // redeem full balance held by deployer |
| 126 | + |
| 127 | + // Approve FlashMint to transfer icETH |
| 128 | + await icEth.connect(operator).approve(flashMint.address, redeemAmount); |
| 129 | + |
| 130 | + // SwapData for dust: no-op |
| 131 | + const dustSwapData: SwapData = { |
| 132 | + path: [], |
| 133 | + fees: [], |
| 134 | + pool: ethers.constants.AddressZero, |
| 135 | + exchange: Exchange.None, |
| 136 | + }; |
| 137 | + |
| 138 | + // SwapData for collateral: stETH -> ETH via Curve stETH-ETH pool |
| 139 | + const collateralSwapData: SwapData = { |
| 140 | + path: [addresses.tokens.stEth, addresses.dexes.curve.ethAddress], |
| 141 | + fees: [], |
| 142 | + pool: addresses.dexes.curve.pools.stEthEth, |
| 143 | + exchange: Exchange.Curve, |
| 144 | + }; |
| 145 | + |
| 146 | + // Redeem for ETH (minAmountOutputToken set to 0 for flexibility in test env) |
| 147 | + const tx = await flashMint.connect(operator).redeemExactSetForETH( |
| 148 | + addresses.tokens.icEth, |
| 149 | + redeemAmount, |
| 150 | + 0, |
| 151 | + dustSwapData, |
| 152 | + collateralSwapData, |
| 153 | + ); |
| 154 | + |
| 155 | + await tx.wait(); |
| 156 | + |
| 157 | + const endEth = await ethers.provider.getBalance(holderAddress); |
| 158 | + const endIcEth = await icEth.balanceOf(holderAddress); |
| 159 | + |
| 160 | + const endFlashMintWethBal = await weth.balanceOf(flashMint.address); |
| 161 | + console.log("endFlashMintWethBal:", ethers.utils.formatEther(endFlashMintWethBal)); |
| 162 | + expect(endFlashMintWethBal.eq(0)).to.eq(true); |
| 163 | + |
| 164 | + // icETH balance should decrease to near zero (allow minimal dust) |
| 165 | + console.log("endIcEth:", ethers.utils.formatEther(endIcEth)); |
| 166 | + expect(endIcEth).to.eq(0); |
| 167 | + // ETH balance should increase |
| 168 | + expect(endEth.gt(startEth.add(ethers.utils.parseEther("10")))).to.eq(true); |
| 169 | + }); |
| 170 | + }); |
| 171 | +} |
0 commit comments