Initial commit with 🏗️ Scaffold-ETH 2 @ 1.0.2

This commit is contained in:
han
2026-01-21 20:45:23 +07:00
commit e7b2a69f2a
156 changed files with 29196 additions and 0 deletions

View File

@@ -0,0 +1,386 @@
import { ethers } from "hardhat";
import { expect } from "chai";
import { ContractTransactionReceipt } from "ethers";
import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs";
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
import { Balloons, DEX } from "../typechain-types";
describe("🚩 Challenge: ⚖️ 🪙 DEX", () => {
// this.timeout(45000);
let dexContract: DEX;
let balloonsContract: Balloons;
let deployer: HardhatEthersSigner;
let user2: HardhatEthersSigner;
let user3: HardhatEthersSigner;
const contractAddress = process.env.CONTRACT_ADDRESS;
let contractArtifact: string;
if (contractAddress) {
contractArtifact = `contracts/download-${contractAddress}.sol:DEX`;
} else {
contractArtifact = "contracts/DEX.sol:DEX";
}
async function getEventValue(txReceipt: ContractTransactionReceipt | null, eventNumber: number) {
if (!txReceipt) return;
const dexContractAddress = await dexContract.getAddress();
const log = txReceipt.logs.find(log => log.address === dexContractAddress);
const logDescr = log && dexContract.interface.parseLog(log);
const args = logDescr?.args;
return args && args[eventNumber]; // index of ethAmount in event
}
async function deployNewInstance() {
before("Deploying fresh contracts", async function () {
console.log("\t", " 🛫 Deploying new contracts...");
const BalloonsContract = await ethers.getContractFactory("Balloons");
balloonsContract = await BalloonsContract.deploy();
const balloonsAddress = await balloonsContract.getAddress();
const DexContract = await ethers.getContractFactory(contractArtifact);
dexContract = (await DexContract.deploy(balloonsAddress)) as DEX;
const dexContractAddress = await dexContract.getAddress();
await balloonsContract.approve(dexContractAddress, ethers.parseEther("100"));
await dexContract.init(ethers.parseEther("5"), {
value: ethers.parseEther("5"),
gasLimit: 200000,
});
[deployer, user2, user3] = await ethers.getSigners();
await balloonsContract.transfer(user2.address, ethers.parseEther("10"));
await balloonsContract.transfer(user3.address, ethers.parseEther("10"));
});
}
// quick fix to let gas reporter fetch data from gas station & coinmarketcap
before(done => {
setTimeout(done, 2000);
});
// --------------------- START OF CHECKPOINT 2 ---------------------
describe("Checkpoint 2: Reserves", function () {
describe("Deploying the contracts and testing the init function", () => {
it("Should deploy contracts", async function () {
const BalloonsContract = await ethers.getContractFactory("Balloons");
balloonsContract = await BalloonsContract.deploy();
const balloonsAddress = await balloonsContract.getAddress();
const DexContract = await ethers.getContractFactory(contractArtifact);
dexContract = (await DexContract.deploy(balloonsAddress)) as DEX;
const dexContractAddress = await dexContract.getAddress();
await balloonsContract.approve(dexContractAddress, ethers.parseEther("100"));
const lpBefore = await dexContract.totalLiquidity();
console.log(
"\t",
" 💦 Expecting total liquidity to be 0 before initializing. Total liquidity:",
ethers.formatEther(lpBefore),
);
expect(lpBefore).to.equal(0);
console.log("\t", " 🔰 Calling init with 5 Eth");
await dexContract.init(ethers.parseEther("5"), {
value: ethers.parseEther("5"),
gasLimit: 200000,
});
const lpAfter = await dexContract.totalLiquidity();
console.log("\t", " 💦 Expecting new total liquidity to be 5. Total liquidity:", ethers.formatEther(lpAfter));
});
});
});
// ----------------- END OF CHECKPOINT 2 -----------------
// ----------------- START OF CHECKPOINT 3 -----------------
describe("Checkpoint 3: Price", async () => {
describe("price()", async () => {
// https://etherscan.io/address/0x7a250d5630b4cf539739df2c5dacb4c659f2488d#readContract
// in Uniswap the fee is build in getAmountOut() function
it("Should calculate the price correctly", async function () {
let xInput = ethers.parseEther("1");
let xReserves = ethers.parseEther("5");
let yReserves = ethers.parseEther("5");
let yOutput = await dexContract.price(xInput, xReserves, yReserves);
expect(
yOutput.toString(),
"Check your price function's calculations. Don't forget the 3% fee for liquidity providers, and the function should be view or pure.",
).to.equal("831248957812239453");
xInput = ethers.parseEther("1");
xReserves = ethers.parseEther("10");
yReserves = ethers.parseEther("15");
yOutput = await dexContract.price(xInput, xReserves, yReserves);
expect(yOutput.toString()).to.equal("1359916340820223697");
});
});
});
// ----------------- END OF CHECKPOINT 3 -----------------
// ----------------- START OF CHECKPOINT 4 -----------------
describe("Checkpoint 4: Trading", function () {
deployNewInstance();
describe("ethToToken()", function () {
it("Should be able to send 1 Ether to DEX in exchange for _ $BAL", async function () {
const dexContractAddress = await dexContract.getAddress();
const dex_eth_start = await ethers.provider.getBalance(dexContractAddress);
console.log("\t", " 💵 Dex contract's initial Eth balance:", ethers.formatEther(dex_eth_start));
console.log("\t", " 📞 Calling ethToToken with a value of 1 Eth...");
const tx1 = await dexContract.ethToToken({
value: ethers.parseEther("1"),
});
expect(tx1, "ethToToken should revert before initalization").not.to.be.reverted;
console.log("\t", " 🔰 Initializing...");
const tx1_receipt = await tx1.wait();
const ethSent_1 = await getEventValue(tx1_receipt, 2);
console.log("\t", " 🔼 Expecting the Eth value emitted to be 1. Value:", ethers.formatEther(ethSent_1));
expect(ethSent_1, "Check you are emitting the correct Eth value and in the correct order").to.equal(
ethers.parseEther("1"),
);
const dex_eth_after = await ethers.provider.getBalance(dexContractAddress);
console.log("\t", " 💵 Dex contract's new Eth balance:", ethers.formatEther(dex_eth_after));
console.log("\t", " 💵 Expecting final Dex balance to have increased by 1...");
expect(await ethers.provider.getBalance(dexContractAddress)).to.equal(ethers.parseEther("6"));
});
it("Should revert if 0 ETH sent", async function () {
await expect(
dexContract.ethToToken({ value: ethers.parseEther("0") }),
"ethToToken should revert when sending 0 value...",
).to.be.reverted;
});
it("Should send less tokens after the first trade (ethToToken called)", async function () {
const user2BalBefore = await balloonsContract.balanceOf(user2.address);
console.log("\t", " 💵 User2 initial $BAL balance:", ethers.formatEther(user2BalBefore));
console.log("\t", " 🥈 User2 calling ethToToken with value of 1 ETH...");
await dexContract.connect(user2).ethToToken({ value: ethers.parseEther("1") });
const user2BalAfter = await balloonsContract.balanceOf(user2.address);
console.log("\t", " 💵 User2 new $BAL balance:", ethers.formatEther(user2BalAfter));
const user3BalBefore = await balloonsContract.balanceOf(user3.address);
console.log("\t", " 💵 User3 initial $BAL balance:", ethers.formatEther(user3BalBefore));
console.log("\t", " 🥉 User3 calling ethToToken with value of 1 ETH...");
await dexContract.connect(user3).ethToToken({ value: ethers.parseEther("1") });
const user3BalAfter = await balloonsContract.balanceOf(user3.address);
console.log("\t", " 💵 User3 new $BAL balance:", ethers.formatEther(user3BalAfter));
console.log("\t", " 💵 Expecting User2 to have aquired more $BAL than User3...");
expect(user2BalAfter).to.greaterThan(user3BalAfter);
});
it("Should emit an event when ethToToken() called", async function () {
await expect(
dexContract.ethToToken({ value: ethers.parseEther("1") }),
"Make sure you're emitting the EthToTokenSwap event correctly",
).to.emit(dexContract, "EthToTokenSwap");
});
it("Should transfer tokens to purchaser after trade", async function () {
const user3_token_before = await balloonsContract.balanceOf(user3.address);
console.log("\t", " 💵 User3 initial $BAL balance:", ethers.formatEther(user3_token_before));
console.log("\t", " 🥉 User3 calling ethToToken with value of 1 ETH...");
const tx1 = await dexContract.connect(user3).ethToToken({
value: ethers.parseEther("1"),
});
await tx1.wait();
const user3_token_after = await balloonsContract.balanceOf(user3.address);
console.log("\t", " 💵 User3 new $BAL balance:", ethers.formatEther(user3_token_after));
const tokenDifferece = user3_token_after - user3_token_before;
console.log("\t", " 🥉 Expecting user3's $BAL balance to increase by the correct amount...");
expect(tokenDifferece).to.be.equal("277481486896167099");
});
// could insert more tests to show the declining price, and what happens when the pool becomes very imbalanced.
});
describe("tokenToEth()", async () => {
it("Should send 1 $BAL to DEX in exchange for _ $ETH", async function () {
const dexContractAddress = await dexContract.getAddress();
const balloons_bal_start = await balloonsContract.balanceOf(dexContractAddress);
console.log("\t", " 💵 Initial Ballons $BAL balance:", ethers.formatEther(balloons_bal_start));
const dex_eth_start = await ethers.provider.getBalance(dexContractAddress);
console.log("\t", " 💵 Initial DEX Eth balance:", ethers.formatEther(dex_eth_start));
console.log("\t", " 📞 Calling tokenToEth with 1 Eth...");
const tx1 = await dexContract.tokenToEth(ethers.parseEther("1"));
const balloons_bal_end = await balloonsContract.balanceOf(dexContractAddress);
console.log("\t", " 💵 Final Ballons $BAL balance:", ethers.formatEther(balloons_bal_end));
const dex_eth_end = await ethers.provider.getBalance(dexContractAddress);
console.log("\t", " 💵 Final DEX Eth balance:", ethers.formatEther(dex_eth_end));
await expect(tx1).not.to.be.revertedWith("Contract not initialized");
// Checks that the balance of the DEX contract has decreased by 1 $BAL
console.log("\t", " 🎈 Expecting the $BAL balance of the Ballon contract to have increased by 1...");
expect(await balloonsContract.balanceOf(dexContractAddress)).to.equal(
balloons_bal_start + ethers.parseEther("1"),
);
// Checks that the balance of the DEX contract has increased
console.log("\t", " ⚖️ Expecting the balance of the Dex contract to have decreased...");
expect(await ethers.provider.getBalance(dexContractAddress)).to.lessThan(dex_eth_start);
});
it("Should revert if 0 tokens sent to the DEX", async function () {
await expect(dexContract.tokenToEth(ethers.parseEther("0"))).to.be.reverted;
});
it("Should emit event TokenToEthSwap when tokenToEth() called", async function () {
await expect(
dexContract.tokenToEth(ethers.parseEther("1")),
"Make sure you're emitting the TokenToEthSwap event correctly",
).to.emit(dexContract, "TokenToEthSwap");
});
it("Should send less eth after the first trade (tokenToEth() called)", async function () {
const dexContractAddress = await dexContract.getAddress();
const dex_eth_start = await ethers.provider.getBalance(dexContractAddress);
console.log("\t", " 💵 Initial Dex balance:", ethers.formatEther(dex_eth_start));
console.log("\t", " 📞 Calling tokenToEth with 1 Eth...");
const tx1 = await dexContract.tokenToEth(ethers.parseEther("1"));
await tx1.wait();
const dex_eth_next = await ethers.provider.getBalance(dexContractAddress);
const tx1difference = dex_eth_next - dex_eth_start;
console.log(
"\t",
" 💵 Next Dex balance:",
ethers.formatEther(dex_eth_next),
" Eth sent:",
ethers.formatEther(tx1difference * -1n),
);
console.log("\t", " 📞 Calling tokenToEth with 1 Eth...");
const tx2 = await dexContract.tokenToEth(ethers.parseEther("1"));
await tx2.wait();
const dex_eth_end = await ethers.provider.getBalance(dexContractAddress);
const tx2difference = dex_eth_end - dex_eth_next;
console.log(
"\t",
" 💵 Final Dex balance:",
ethers.formatEther(dex_eth_end),
" Eth sent:",
ethers.formatEther(tx2difference * -1n),
);
console.log("\t", " Expecting the first call to get more Eth than the second...");
expect(tx2difference).to.greaterThan(tx1difference);
});
});
});
// ----------------- END OF CHECKPOINT 4 -----------------
// ----------------- START OF CHECKPOINT 5 -----------------
describe("Checkpoint 5: Liquidity", async () => {
describe("deposit()", async () => {
deployNewInstance();
it("Should increase liquidity in the pool when ETH is deposited", async function () {
const dexContractAddress = await dexContract.getAddress();
console.log("\t", " 💵 Approving 100 ETH...");
await balloonsContract.connect(user2).approve(dexContractAddress, ethers.parseEther("100"));
const liquidity_start = await dexContract.totalLiquidity();
console.log("\t", " ⚖️ Starting Dex liquidity", ethers.formatEther(liquidity_start));
const user2liquidity = await dexContract.getLiquidity(user2.address);
console.log("\t", " ⚖️ Expecting user's liquidity to be 0. Liquidity:", ethers.formatEther(user2liquidity));
expect(user2liquidity).to.equal("0");
console.log("\t", " 🔼 Expecting the deposit function to emit correctly...");
await expect(
dexContract.connect(user2).deposit((ethers.parseEther("5"), { value: ethers.parseEther("5") })),
"Check the order of the values when emitting LiquidityProvided: msg.sender, liquidityMinted, msg.value, tokenDeposit",
)
.to.emit(dexContract, "LiquidityProvided")
.withArgs(anyValue, ethers.parseEther("5"), ethers.parseEther("5"), anyValue);
const liquidity_end = await dexContract.totalLiquidity();
console.log(
"\t",
" 💸 Final liquidity should increase by 5. Final liquidity:",
ethers.formatEther(liquidity_end),
);
expect(liquidity_end, "Total liquidity should increase").to.equal(liquidity_start + ethers.parseEther("5"));
const user_lp = await dexContract.getLiquidity(user2.address);
console.log("\t", " ⚖️ User's liquidity provided should be 5. LP:", ethers.formatEther(user_lp));
expect(user_lp.toString(), "User's liquidity provided should be 5.").to.equal(ethers.parseEther("5"));
});
it("Should revert if 0 ETH deposited", async function () {
await expect(
dexContract.deposit((ethers.parseEther("0"), { value: ethers.parseEther("0") })),
"Should revert if 0 value is sent",
).to.be.reverted;
});
});
// pool should have 5:5 ETH:$BAL ratio
describe("withdraw()", async () => {
deployNewInstance();
it("Should withdraw 1 ETH and 1 $BAL when pool is at a 1:1 ratio", async function () {
const startingLiquidity = await dexContract.totalLiquidity();
console.log("\t", " ⚖️ Starting liquidity:", ethers.formatEther(startingLiquidity));
const userBallonsBalance = await balloonsContract.balanceOf(deployer.address);
console.log("\t", " 💵 User's starting $BAL balance:", ethers.formatEther(userBallonsBalance));
console.log("\t", " 🔽 Calling withdraw with with a value of 1 Eth...");
const tx1 = await dexContract.withdraw(ethers.parseEther("1"));
const userBallonsBalanceAfter = await balloonsContract.balanceOf(deployer.address);
const tx1_receipt = await tx1.wait();
const eth_out = await getEventValue(tx1_receipt, 2);
const token_out = await getEventValue(tx1_receipt, 3);
console.log("\t", " 💵 User's new $BAL balance:", ethers.formatEther(userBallonsBalanceAfter));
console.log(
"\t",
" 🎈 Expecting the balance to have increased by 1",
ethers.formatEther(userBallonsBalanceAfter),
);
expect(userBallonsBalance, "User $BAL balance shoud increase").to.equal(
userBallonsBalanceAfter - ethers.parseEther("1"),
);
console.log("\t", " 🔽 Expecting the Eth withdrawn emit to be 1...");
expect(eth_out, "ethWithdrawn incorrect in emit").to.be.equal(ethers.parseEther("1"));
console.log("\t", " 🔽 Expecting the $BAL withdrawn emit to be 1...");
expect(token_out, "tokenAmount incorrect in emit").to.be.equal(ethers.parseEther("1"));
});
it("Should revert if sender does not have enough liqudity", async function () {
console.log("\t", " ✋ Expecting withdraw to revert when there is not enough liquidity...");
await expect(dexContract.withdraw(ethers.parseEther("100"))).to.be.reverted;
});
it("Should decrease total liquidity", async function () {
const totalLpBefore = await dexContract.totalLiquidity();
console.log("\t", " ⚖️ Initial liquidity:", ethers.formatEther(totalLpBefore));
console.log("\t", " 📞 Calling withdraw with 1 Eth...");
const txWithdraw = await dexContract.withdraw(ethers.parseEther("1"));
const totalLpAfter = await dexContract.totalLiquidity();
console.log("\t", " ⚖️ Final liquidity:", ethers.formatEther(totalLpAfter));
const txWithdraw_receipt = await txWithdraw.wait();
const liquidityBurned = await getEventValue(txWithdraw_receipt, 2);
console.log("\t", " 🔼 Emitted liquidity removed:", ethers.formatEther(liquidityBurned));
expect(totalLpAfter, "Emitted incorrect liquidity amount burned").to.be.equal(totalLpBefore - liquidityBurned);
expect(totalLpBefore, "Emitted total liquidity should decrease").to.be.above(totalLpAfter);
});
it("Should emit event LiquidityWithdrawn when withdraw() called", async function () {
await expect(
dexContract.withdraw(ethers.parseEther("1.5")),
"Make sure you emit the LiquidityRemoved event correctly",
)
.to.emit(dexContract, "LiquidityRemoved")
.withArgs(deployer.address, ethers.parseEther("1.5"), ethers.parseEther("1.5"), ethers.parseEther("1.5"));
});
});
});
// ----------------- END OF CHECKPOINT 5 -----------------
});