Skip to content

Commit 27d68cf

Browse files
authored
feat: over-collateralized-lending migration to hh v3 (#462)
* feat: migration to hh v3 * fix: marketSimulator * fix: deployment types * fix: review * chore: bump create-eth 2.0.18 -> 2.0.20
1 parent 9ec6fdd commit 27d68cf

5 files changed

Lines changed: 140 additions & 105 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Deploy your contracts to a testnet then build and upload your app to a public we
4141
📥 Then download the challenge to your computer and install dependencies by running:
4242

4343
```sh
44-
npx create-eth@2.0.18 -e challenge-over-collateralized-lending challenge-over-collateralized-lending
44+
npx create-eth@2.0.20 -e challenge-over-collateralized-lending challenge-over-collateralized-lending
4545
cd challenge-over-collateralized-lending
4646
```
4747

Lines changed: 78 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,88 @@
1-
import { HardhatRuntimeEnvironment } from "hardhat/types";
2-
import { DeployFunction } from "hardhat-deploy/types";
3-
import { Contract } from "ethers";
1+
import { artifacts, deployScript } from "../rocketh/deploy.js";
2+
import { parseEther } from "viem";
43

54
/**
6-
* Deploys a contract named "YourContract" using the deployer account and
7-
* constructor arguments set to the deployer address
5+
* Deploys "Corn", "CornDEX", "Lending" and "MovePrice" using the deployer account.
86
*
9-
* @param hre HardhatRuntimeEnvironment object.
7+
* On localhost, the deployer account is the one that comes with Hardhat, which is already funded.
8+
*
9+
* When deploying to live networks (e.g `yarn deploy --network sepolia`), the deployer account
10+
* should have sufficient balance to pay for the gas fees for contract creation.
11+
*
12+
* You can generate a random account with `yarn generate` or `yarn account:import` to import your
13+
* existing PK which will fill DEPLOYER_PRIVATE_KEY_ENCRYPTED in the .env file (used in hardhat.config.ts).
14+
* Run `yarn account` to check the deployer balance on every network.
1015
*/
11-
const deployContracts: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
12-
/*
13-
On localhost, the deployer account is the one that comes with Hardhat, which is already funded.
14-
15-
When deploying to live networks (e.g `yarn deploy --network sepolia`), the deployer account
16-
should have sufficient balance to pay for the gas fees for contract creation.
17-
18-
You can generate a random account with `yarn generate` or `yarn account:import` to import your
19-
existing PK which will fill DEPLOYER_PRIVATE_KEY_ENCRYPTED in the .env file (then used on hardhat.config.ts)
20-
You can run the `yarn account` command to check your balance in every network.
21-
*/
22-
const { deployer } = await hre.getNamedAccounts();
23-
const { deploy } = hre.deployments;
16+
export default deployScript(
17+
async env => {
18+
const { deployer } = env.namedAccounts;
2419

25-
await deploy("Corn", {
26-
from: deployer,
27-
// Contract constructor arguments
28-
args: [],
29-
log: true,
30-
// autoMine: can be passed to the deploy function to make the deployment process faster on local networks by
31-
// automatically mining the contract deployment transaction. There is no effect on live networks.
32-
autoMine: true,
33-
});
34-
const cornToken = await hre.ethers.getContract<Contract>("Corn", deployer);
20+
const cornToken = await env.deploy("Corn", {
21+
account: deployer,
22+
artifact: artifacts.Corn,
23+
args: [],
24+
});
3525

36-
await deploy("CornDEX", {
37-
from: deployer,
38-
args: [cornToken.target],
39-
log: true,
40-
autoMine: true,
41-
});
42-
const cornDEX = await hre.ethers.getContract<Contract>("CornDEX", deployer);
43-
const lending = await deploy("Lending", {
44-
from: deployer,
45-
args: [cornDEX.target, cornToken.target],
46-
log: true,
47-
autoMine: true,
48-
});
26+
const cornDEX = await env.deploy("CornDEX", {
27+
account: deployer,
28+
artifact: artifacts.CornDEX,
29+
args: [cornToken.address],
30+
});
4931

50-
// Set up the move price contract
51-
const movePrice = await deploy("MovePrice", {
52-
from: deployer,
53-
args: [cornDEX.target, cornToken.target],
54-
log: true,
55-
autoMine: true,
56-
});
32+
const lending = await env.deploy("Lending", {
33+
account: deployer,
34+
artifact: artifacts.Lending,
35+
args: [cornDEX.address, cornToken.address],
36+
});
5737

58-
// Only set up contract state on local network
59-
if (hre.network.name == "localhost") {
60-
const GAS_LIMIT = 500_000n;
61-
// Give ETH and CORN to the move price contract
62-
await hre.ethers.provider.send("hardhat_setBalance", [
63-
movePrice.address,
64-
`0x${hre.ethers.parseEther("10000000000000000000000").toString(16)}`,
65-
]);
66-
await cornToken.mintTo(movePrice.address, hre.ethers.parseEther("10000000000000000000000"), { gasLimit: GAS_LIMIT });
67-
// Lenders deposit CORN to the lending contract
68-
await cornToken.mintTo(lending.address, hre.ethers.parseEther("10000000000000000000000"), { gasLimit: GAS_LIMIT });
69-
// Give CORN and ETH to the deployer
70-
await cornToken.mintTo(deployer, hre.ethers.parseEther("1000000000000"), { gasLimit: GAS_LIMIT });
71-
await hre.ethers.provider.send("hardhat_setBalance", [
72-
deployer,
73-
`0x${hre.ethers.parseEther("100000000000").toString(16)}`,
74-
]);
38+
// Set up the move price contract
39+
const movePrice = await env.deploy("MovePrice", {
40+
account: deployer,
41+
artifact: artifacts.MovePrice,
42+
args: [cornDEX.address, cornToken.address],
43+
});
7544

76-
await cornToken.approve(cornDEX.target, hre.ethers.parseEther("1000000000"), { gasLimit: GAS_LIMIT });
77-
await cornDEX.init(hre.ethers.parseEther("1000000000"), { value: hre.ethers.parseEther("1000000"), gasLimit: GAS_LIMIT });
78-
}
79-
};
45+
// Only set up contract state on local network
46+
if (env.name === "default" || env.name === "localhost") {
47+
// Give ETH and CORN to the move price contract
48+
await env.network.provider.request<{ params: [`0x${string}`, `0x${string}`]; result: null }>({
49+
method: "hardhat_setBalance",
50+
params: [movePrice.address, `0x${parseEther("10000000000000000000000").toString(16)}`],
51+
});
52+
await env.execute(cornToken, {
53+
functionName: "mintTo",
54+
args: [movePrice.address, parseEther("10000000000000000000000")],
55+
account: deployer,
56+
});
57+
// Lenders deposit CORN to the lending contract
58+
await env.execute(cornToken, {
59+
functionName: "mintTo",
60+
args: [lending.address, parseEther("10000000000000000000000")],
61+
account: deployer,
62+
});
63+
// Give CORN and ETH to the deployer
64+
await env.execute(cornToken, {
65+
functionName: "mintTo",
66+
args: [deployer, parseEther("1000000000000")],
67+
account: deployer,
68+
});
69+
await env.network.provider.request<{ params: [`0x${string}`, `0x${string}`]; result: null }>({
70+
method: "hardhat_setBalance",
71+
params: [deployer, `0x${parseEther("100000000000").toString(16)}`],
72+
});
8073

81-
export default deployContracts;
74+
await env.execute(cornToken, {
75+
functionName: "approve",
76+
args: [cornDEX.address, parseEther("1000000000")],
77+
account: deployer,
78+
});
79+
await env.execute(cornDEX, {
80+
functionName: "init",
81+
args: [parseEther("1000000000")],
82+
value: parseEther("1000000"),
83+
account: deployer,
84+
});
85+
}
86+
},
87+
{ tags: ["Lending"] },
88+
);

extension/packages/hardhat/scripts/marketSimulator.ts

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
import { HDNodeWallet, parseEther } from "ethers";
2-
import hre from "hardhat";
3-
import { CornDEX, Lending, Corn, MovePrice } from "../typechain-types";
4-
const ethers = hre.ethers;
2+
import { network } from "hardhat";
3+
import { readFileSync } from "fs";
4+
import { join, dirname } from "path";
5+
import { fileURLToPath } from "url";
6+
import type { CornDEX, Lending, Corn, MovePrice } from "../types/ethers-contracts/index.js";
7+
8+
const __dirname = dirname(fileURLToPath(import.meta.url));
9+
10+
const { ethers } = await network.create();
11+
12+
// This script only runs against the local chain: it reads contract addresses
13+
// from the `default` deployments folder written by the local `yarn deploy`.
14+
function loadDeployment(name: string): { address: string; abi: any } {
15+
const path = join(__dirname, `../deployments/default/${name}.json`);
16+
return JSON.parse(readFileSync(path, "utf8"));
17+
}
518

619
interface SimulatedAccount {
720
wallet: HDNodeWallet;
@@ -63,6 +76,7 @@ async function simulateMarketActions(
6376
movePrice: MovePrice,
6477
lending: Lending,
6578
corn: Corn,
79+
cornDEX: CornDEX,
6680
accounts: SimulatedAccount[],
6781
deployer: any,
6882
) {
@@ -105,7 +119,7 @@ async function simulateMarketActions(
105119
}
106120

107121
// 5. Check for and perform liquidations
108-
await checkAndPerformLiquidations(lending, corn, accounts);
122+
await checkAndPerformLiquidations(lending, corn, cornDEX, accounts);
109123
} catch (error) {
110124
console.error("Error in market simulation interval");
111125
if (process.env.DEBUG) {
@@ -136,8 +150,8 @@ async function simulateBorrowing(lending: Lending, accounts: SimulatedAccount[])
136150
await lendingWithAccount.borrowCorn(maxBorrowAmount);
137151
console.log(
138152
`Account ${randomAccount.wallet.address} borrowed ${ethers.formatEther(maxBorrowAmount)} CORN ` +
139-
`(${aggressiveBorrower ? "aggressive" : "conservative"}, ` +
140-
`${((Number(maxBorrowAmount) * 100) / Number(collateralValue)).toFixed(1)}% of collateral)`,
153+
`(${aggressiveBorrower ? "aggressive" : "conservative"}, ` +
154+
`${((Number(maxBorrowAmount) * 100) / Number(collateralValue)).toFixed(1)}% of collateral)`,
141155
);
142156
} catch (error) {
143157
if (process.env.DEBUG) {
@@ -166,8 +180,8 @@ async function simulateAddCollateral(lending: Lending, accounts: SimulatedAccoun
166180

167181
console.log(
168182
`Account ${randomAccount.wallet.address} added ${ethers.formatEther(amountToAdd)} ETH as collateral ` +
169-
`(${percentage.toFixed(1)}% of available balance, ` +
170-
`total collateral: ${ethers.formatEther(currentCollateral + amountToAdd)} ETH)`,
183+
`(${percentage.toFixed(1)}% of available balance, ` +
184+
`total collateral: ${ethers.formatEther(currentCollateral + amountToAdd)} ETH)`,
171185
);
172186
} catch (error) {
173187
if (process.env.DEBUG) {
@@ -178,9 +192,12 @@ async function simulateAddCollateral(lending: Lending, accounts: SimulatedAccoun
178192
}
179193
}
180194

181-
async function checkAndPerformLiquidations(lending: Lending, corn: Corn, accounts: SimulatedAccount[]) {
182-
const cornDEX = await ethers.getContract<CornDEX>("CornDEX");
183-
195+
async function checkAndPerformLiquidations(
196+
lending: Lending,
197+
corn: Corn,
198+
cornDEX: CornDEX,
199+
accounts: SimulatedAccount[],
200+
) {
184201
const filter = lending.filters.CollateralAdded();
185202
const events = await lending.queryFilter(filter);
186203
const users = [...new Set(events.map(event => event.args[0]))];
@@ -195,18 +212,18 @@ async function checkAndPerformLiquidations(lending: Lending, corn: Corn, account
195212
if (!isLiquidatable) continue;
196213

197214
const liquidators = accounts.filter(account => account.wallet.address.toLowerCase() !== user.toLowerCase());
198-
215+
199216
const liquidatorsWithBalance = await Promise.all(
200217
liquidators.map(async account => {
201218
const cornBalance = await corn.balanceOf(account.wallet.address);
202219
return { account, hasEnough: cornBalance >= amountBorrowed };
203-
})
220+
}),
204221
);
205222

206223
const eligibleLiquidators = liquidatorsWithBalance
207224
.filter(({ hasEnough }) => hasEnough)
208225
.map(({ account }) => account);
209-
226+
210227
if (eligibleLiquidators.length === 0) {
211228
const randomAccount = accounts[Math.floor(Math.random() * accounts.length)];
212229
if (randomAccount.wallet.address.toLowerCase() !== user.toLowerCase()) {
@@ -277,14 +294,25 @@ async function checkAndPerformLiquidations(lending: Lending, corn: Corn, account
277294

278295
async function main() {
279296
const [deployer] = await ethers.getSigners();
280-
const movePriceContract = await ethers.getContract<MovePrice>("MovePrice", deployer);
281-
const lending = await ethers.getContract<Lending>("Lending", deployer);
282-
const corn = await ethers.getContract<Corn>("Corn", deployer);
297+
298+
const movePriceDep = loadDeployment("MovePrice");
299+
const lendingDep = loadDeployment("Lending");
300+
const cornDep = loadDeployment("Corn");
301+
const cornDEXDep = loadDeployment("CornDEX");
302+
303+
const movePriceContract = new ethers.Contract(
304+
movePriceDep.address,
305+
movePriceDep.abi,
306+
deployer,
307+
) as unknown as MovePrice;
308+
const lending = new ethers.Contract(lendingDep.address, lendingDep.abi, deployer) as unknown as Lending;
309+
const corn = new ethers.Contract(cornDep.address, cornDep.abi, deployer) as unknown as Corn;
310+
const cornDEX = new ethers.Contract(cornDEXDep.address, cornDEXDep.abi, deployer) as unknown as CornDEX;
283311

284312
const accounts = await setupAccounts();
285313

286314
// Start the combined market simulation
287-
await simulateMarketActions(movePriceContract, lending, corn, accounts, deployer);
315+
await simulateMarketActions(movePriceContract, lending, corn, cornDEX, accounts, deployer);
288316

289317
// Keep the script running
290318
process.stdin.resume();

extension/packages/hardhat/test/Lending.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,35 @@
22
// This script executes when you run 'yarn test'
33
//
44

5-
import { ethers } from "hardhat";
5+
import { network } from "hardhat";
66
import { expect } from "chai";
7-
import { Corn, CornDEX, Lending } from "../typechain-types";
8-
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
7+
import { parseEther } from "ethers";
8+
import type { Corn, CornDEX, Lending } from "../types/ethers-contracts/index.js";
9+
import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types";
910

1011
describe("💳🌽 Over-collateralized Lending Challenge 🤓", function () {
12+
let ethers: Awaited<ReturnType<typeof network.create>>["ethers"];
13+
1114
const contractAddress = process.env.CONTRACT_ADDRESS;
1215
let cornToken: Corn;
1316
let cornDEX: CornDEX;
1417
let lending: Lending;
15-
let owner: SignerWithAddress;
16-
let user1: SignerWithAddress;
17-
let user2: SignerWithAddress;
18+
let owner: HardhatEthersSigner;
19+
let user1: HardhatEthersSigner;
20+
let user2: HardhatEthersSigner;
1821

19-
const collateralAmount = ethers.parseEther("10");
20-
const borrowAmount = ethers.parseEther("5000");
22+
const collateralAmount = parseEther("10");
23+
const borrowAmount = parseEther("5000");
2124

2225
beforeEach(async function () {
23-
await ethers.provider.send("hardhat_reset", []);
26+
({ ethers } = await network.create());
2427
[owner, user1, user2] = await ethers.getSigners();
2528

2629
const Corn = await ethers.getContractFactory("Corn");
27-
cornToken = await Corn.deploy();
30+
cornToken = (await Corn.deploy()) as unknown as Corn;
2831

2932
const CornDEX = await ethers.getContractFactory("CornDEX");
30-
cornDEX = await CornDEX.deploy(await cornToken.getAddress());
33+
cornDEX = (await CornDEX.deploy(await cornToken.getAddress())) as unknown as CornDEX;
3134

3235
await cornToken.mintTo(owner.address, ethers.parseEther("1000000"));
3336
await cornToken.approve(cornDEX.target, ethers.parseEther("1000000"));
@@ -42,7 +45,7 @@ describe("💳🌽 Over-collateralized Lending Challenge 🤓", function () {
4245
}
4346

4447
const Lending = await ethers.getContractFactory(contractArtifact);
45-
lending = (await Lending.deploy(cornDEX.target, cornToken.target)) as Lending;
48+
lending = (await Lending.deploy(cornDEX.target, cornToken.target)) as unknown as Lending;
4649
await cornToken.mintTo(lending.target, ethers.parseEther("10000000000000000000000"));
4750
});
4851

extension/packages/nextjs/next.config.ts.args.mjs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,4 @@ export const configOverrides = {
22
typescript: {
33
ignoreBuildErrors: true,
44
},
5-
eslint: {
6-
ignoreDuringBuilds: true,
7-
},
85
};

0 commit comments

Comments
 (0)