Skip to content

Commit 8cd8189

Browse files
authored
feat: icETH deleveraged flash redemptions (#215)
1 parent 7df10ed commit 8cd8189

File tree

8 files changed

+686
-4
lines changed

8 files changed

+686
-4
lines changed

contracts/exchangeIssuance/ExchangeIssuanceIcEth.sol

Lines changed: 464 additions & 0 deletions
Large diffs are not rendered by default.

test/integration/ethereum/flashMintHyETHV3.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const NO_OP_SWAP_DATA: SwapData = {
5353
};
5454

5555
if (process.env.INTEGRATIONTEST) {
56-
describe.only("FlashMintHyETHV3 - Integration Test", async () => {
56+
describe("FlashMintHyETHV3 - Integration Test", async () => {
5757
const addresses = PRODUCTION_ADDRESSES;
5858
let owner: Account;
5959
let deployer: DeployHelper;
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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+
}

test/integration/ethereum/flashMintLeveraged.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type SwapData = {
3636
};
3737

3838
if (process.env.INTEGRATIONTEST) {
39-
describe.only("FlashMintLeveraged - Integration Test", async () => {
39+
describe("FlashMintLeveraged - Integration Test", async () => {
4040
const addresses = process.env.USE_STAGING_ADDRESSES ? STAGING_ADDRESSES : PRODUCTION_ADDRESSES;
4141
let owner: Account;
4242
let deployer: DeployHelper;

test/integration/ethereum/flashMintWrappedIntegration.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ class TestHelper {
280280
}
281281

282282
if (process.env.INTEGRATIONTEST) {
283-
describe.only("FlashMintWrapped - Integration Test", async () => {
283+
describe("FlashMintWrapped - Integration Test", async () => {
284284
let owner: Account;
285285
let deployer: DeployHelper;
286286
let setToken: StandardTokenMock;

test/integration/ethereum/flashMintWrappedRebasing.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ const whales = {
8080
};
8181

8282
if (process.env.INTEGRATIONTEST) {
83-
describe.only("FlashMintWrapped - RebasingComponentModule Integration Test", async () => {
83+
describe("FlashMintWrapped - RebasingComponentModule Integration Test", async () => {
8484
const TOKEN_TRANSFER_BUFFER = 10;
8585
const addresses = PRODUCTION_ADDRESSES;
8686

utils/contracts/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export { DEXAdapterV3 } from "../../typechain/DEXAdapterV3";
1818
export { ExchangeIssuance } from "../../typechain/ExchangeIssuance";
1919
export { ExchangeIssuanceV2 } from "../../typechain/ExchangeIssuanceV2";
2020
export { ExchangeIssuanceLeveraged } from "../../typechain/ExchangeIssuanceLeveraged";
21+
export { ExchangeIssuanceIcEth } from "../../typechain/ExchangeIssuanceIcEth";
2122
export { FlashMintLeveraged } from "../../typechain/FlashMintLeveraged";
2223
export { FlashMintLeveragedForCompound } from "../../typechain/FlashMintLeveragedForCompound";
2324
export { ExchangeIssuanceZeroEx } from "../../typechain/ExchangeIssuanceZeroEx";

utils/deploys/deployExtensions.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
ExchangeIssuance,
2020
ExchangeIssuanceV2,
2121
ExchangeIssuanceLeveraged,
22+
ExchangeIssuanceIcEth,
2223
FlashMintNotional,
2324
FlashMintLeveragedForCompound,
2425
ExchangeIssuanceZeroEx,
@@ -64,6 +65,7 @@ import { DEXAdapterV3__factory } from "../../typechain/factories/DEXAdapterV3__f
6465
import { ExchangeIssuance__factory } from "../../typechain/factories/ExchangeIssuance__factory";
6566
import { ExchangeIssuanceV2__factory } from "../../typechain/factories/ExchangeIssuanceV2__factory";
6667
import { ExchangeIssuanceLeveraged__factory } from "../../typechain/factories/ExchangeIssuanceLeveraged__factory";
68+
import { ExchangeIssuanceIcEth__factory } from "../../typechain/factories/ExchangeIssuanceIcEth__factory";
6769
import { FlashMintHyETH__factory } from "../../typechain/factories/FlashMintHyETH__factory";
6870
import { FlashMintHyETHV2__factory } from "../../typechain/factories/FlashMintHyETHV2__factory";
6971
import { FlashMintHyETHV3__factory } from "../../typechain/factories/FlashMintHyETHV3__factory";
@@ -245,6 +247,48 @@ export default class DeployExtensions {
245247
return await new DEXAdapterV5__factory(this._deployerSigner).deploy();
246248
}
247249

250+
public async deployExchangeIssuanceIcEth(
251+
wethAddress: Address,
252+
quickRouterAddress: Address,
253+
sushiRouterAddress: Address,
254+
uniV3RouterAddress: Address,
255+
uniswapV3QuoterAddress: Address,
256+
setControllerAddress: Address,
257+
basicIssuanceModuleAddress: Address,
258+
aaveLeveragedModuleAddress: Address,
259+
aaveAddressProviderAddress: Address,
260+
curveCalculatorAddress: Address,
261+
curveAddressProviderAddress: Address,
262+
): Promise<ExchangeIssuanceIcEth> {
263+
const dexAdapter = await this.deployDEXAdapter();
264+
265+
const linkId = convertLibraryNameToLinkId(
266+
"contracts/exchangeIssuance/DEXAdapter.sol:DEXAdapter",
267+
);
268+
269+
return await new ExchangeIssuanceIcEth__factory(
270+
// @ts-ignore
271+
{
272+
[linkId]: dexAdapter.address,
273+
},
274+
// @ts-ignore
275+
this._deployerSigner,
276+
).deploy(
277+
wethAddress,
278+
quickRouterAddress,
279+
sushiRouterAddress,
280+
uniV3RouterAddress,
281+
uniswapV3QuoterAddress,
282+
setControllerAddress,
283+
basicIssuanceModuleAddress,
284+
aaveLeveragedModuleAddress,
285+
aaveAddressProviderAddress,
286+
// NOTE: contract expects curveAddressProvider first, then curveCalculator
287+
curveAddressProviderAddress,
288+
curveCalculatorAddress,
289+
);
290+
}
291+
248292
public async deployExchangeIssuanceLeveraged(
249293
wethAddress: Address,
250294
quickRouterAddress: Address,
@@ -264,6 +308,8 @@ export default class DeployExtensions {
264308
"contracts/exchangeIssuance/DEXAdapter.sol:DEXAdapter",
265309
);
266310

311+
console.log("Deploying ExchangeIssuanceLeveraged");
312+
267313
return await new ExchangeIssuanceLeveraged__factory(
268314
// @ts-ignore
269315
{

0 commit comments

Comments
 (0)