Initial commit with 🏗️ Scaffold-ETH 2 @ 1.0.2
This commit is contained in:
13
packages/hardhat/.env.example
Normal file
13
packages/hardhat/.env.example
Normal file
@@ -0,0 +1,13 @@
|
||||
# Template for Hardhat environment variables.
|
||||
|
||||
# To use this template, copy this file, rename it .env, and fill in the values.
|
||||
|
||||
# If not set, we provide default values (check `hardhat.config.ts`) so developers can start prototyping out of the box,
|
||||
# but we recommend getting your own API Keys for Production Apps.
|
||||
|
||||
# To access the values stored in this .env file you can use: process.env.VARIABLENAME
|
||||
ALCHEMY_API_KEY=
|
||||
ETHERSCAN_V2_API_KEY=
|
||||
|
||||
# Don't fill this value manually, run yarn generate to generate a new account or yarn account:import to import an existing PK.
|
||||
DEPLOYER_PRIVATE_KEY_ENCRYPTED=
|
||||
30
packages/hardhat/.gitignore
vendored
Normal file
30
packages/hardhat/.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# dependencies
|
||||
node_modules
|
||||
|
||||
# env files
|
||||
.env
|
||||
|
||||
# coverage
|
||||
coverage
|
||||
coverage.json
|
||||
|
||||
# typechain
|
||||
typechain
|
||||
typechain-types
|
||||
|
||||
# hardhat files
|
||||
cache
|
||||
artifacts
|
||||
|
||||
# zkSync files
|
||||
artifacts-zk
|
||||
cache-zk
|
||||
|
||||
# deployments
|
||||
deployments/localhost
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# other
|
||||
temp
|
||||
18
packages/hardhat/.prettierrc.json
Normal file
18
packages/hardhat/.prettierrc.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-solidity"],
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.sol",
|
||||
"options": {
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
10
packages/hardhat/contracts/Balloons.sol
Normal file
10
packages/hardhat/contracts/Balloons.sol
Normal file
@@ -0,0 +1,10 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.8.0 <0.9.0;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
|
||||
contract Balloons is ERC20 {
|
||||
constructor() ERC20("Balloons", "BAL") {
|
||||
_mint(msg.sender, 1000 ether); // mints 1000 balloons!
|
||||
}
|
||||
}
|
||||
99
packages/hardhat/contracts/DEX.sol
Normal file
99
packages/hardhat/contracts/DEX.sol
Normal file
@@ -0,0 +1,99 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity >=0.8.0 <0.9.0;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
|
||||
/**
|
||||
* @title DEX Template
|
||||
* @author stevepham.eth and m00npapi.eth
|
||||
* @notice Empty DEX.sol that just outlines what features could be part of the challenge (up to you!)
|
||||
* @dev We want to create an automatic market where our contract will hold reserves of both ETH and 🎈 Balloons. These reserves will provide liquidity that allows anyone to swap between the assets.
|
||||
* NOTE: functions outlined here are what work with the front end of this challenge. Also return variable names need to be specified exactly may be referenced (It may be helpful to cross reference with front-end code function calls).
|
||||
*/
|
||||
contract DEX {
|
||||
/* ========== GLOBAL VARIABLES ========== */
|
||||
|
||||
IERC20 token; //instantiates the imported contract
|
||||
|
||||
/* ========== EVENTS ========== */
|
||||
|
||||
/**
|
||||
* @notice Emitted when ethToToken() swap transacted
|
||||
*/
|
||||
event EthToTokenSwap(address swapper, uint256 tokenOutput, uint256 ethInput);
|
||||
|
||||
/**
|
||||
* @notice Emitted when tokenToEth() swap transacted
|
||||
*/
|
||||
event TokenToEthSwap(address swapper, uint256 tokensInput, uint256 ethOutput);
|
||||
|
||||
/**
|
||||
* @notice Emitted when liquidity provided to DEX and mints LPTs.
|
||||
*/
|
||||
event LiquidityProvided(address liquidityProvider, uint256 liquidityMinted, uint256 ethInput, uint256 tokensInput);
|
||||
|
||||
/**
|
||||
* @notice Emitted when liquidity removed from DEX and decreases LPT count within DEX.
|
||||
*/
|
||||
event LiquidityRemoved(
|
||||
address liquidityRemover,
|
||||
uint256 liquidityWithdrawn,
|
||||
uint256 tokensOutput,
|
||||
uint256 ethOutput
|
||||
);
|
||||
|
||||
/* ========== CONSTRUCTOR ========== */
|
||||
|
||||
constructor(address tokenAddr) {
|
||||
token = IERC20(tokenAddr); //specifies the token address that will hook into the interface and be used through the variable 'token'
|
||||
}
|
||||
|
||||
/* ========== MUTATIVE FUNCTIONS ========== */
|
||||
|
||||
/**
|
||||
* @notice initializes amount of tokens that will be transferred to the DEX itself from the erc20 contract mintee (and only them based on how Balloons.sol is written). Loads contract up with both ETH and Balloons.
|
||||
* @param tokens amount to be transferred to DEX
|
||||
* @return totalLiquidity is the number of LPTs minting as a result of deposits made to DEX contract
|
||||
* NOTE: since ratio is 1:1, this is fine to initialize the totalLiquidity (wrt to balloons) as equal to eth balance of contract.
|
||||
*/
|
||||
function init(uint256 tokens) public payable returns (uint256) {}
|
||||
|
||||
/**
|
||||
* @notice returns yOutput, or yDelta for xInput (or xDelta)
|
||||
* @dev Follow along with the [original tutorial](https://medium.com/@austin_48503/%EF%B8%8F-minimum-viable-exchange-d84f30bd0c90) Price section for an understanding of the DEX's pricing model and for a price function to add to your contract. You may need to update the Solidity syntax (e.g. use + instead of .add, * instead of .mul, etc). Deploy when you are done.
|
||||
*/
|
||||
function price(uint256 xInput, uint256 xReserves, uint256 yReserves) public pure returns (uint256 yOutput) {}
|
||||
|
||||
/**
|
||||
* @notice returns liquidity for a user.
|
||||
* NOTE: this is not needed typically due to the `liquidity()` mapping variable being public and having a getter as a result. This is left though as it is used within the front end code (App.jsx).
|
||||
* NOTE: if you are using a mapping liquidity, then you can use `return liquidity[lp]` to get the liquidity for a user.
|
||||
* NOTE: if you will be submitting the challenge make sure to implement this function as it is used in the tests.
|
||||
*/
|
||||
function getLiquidity(address lp) public view returns (uint256) {}
|
||||
|
||||
/**
|
||||
* @notice sends Ether to DEX in exchange for $BAL
|
||||
*/
|
||||
function ethToToken() public payable returns (uint256 tokenOutput) {}
|
||||
|
||||
/**
|
||||
* @notice sends $BAL tokens to DEX in exchange for Ether
|
||||
*/
|
||||
function tokenToEth(uint256 tokenInput) public returns (uint256 ethOutput) {}
|
||||
|
||||
/**
|
||||
* @notice allows deposits of $BAL and $ETH to liquidity pool
|
||||
* NOTE: parameter is the msg.value sent with this function call. That amount is used to determine the amount of $BAL needed as well and taken from the depositor.
|
||||
* NOTE: user has to make sure to give DEX approval to spend their tokens on their behalf by calling approve function prior to this function call.
|
||||
* NOTE: Equal parts of both assets will be removed from the user's wallet with respect to the price outlined by the AMM.
|
||||
*/
|
||||
function deposit() public payable returns (uint256 tokensDeposited) {}
|
||||
|
||||
/**
|
||||
* @notice allows withdrawal of $BAL and $ETH from liquidity pool
|
||||
* NOTE: with this current code, the msg caller could end up getting very little back if the liquidity is super low in the pool. I guess they could see that with the UI.
|
||||
*/
|
||||
function withdraw(uint256 amount) public returns (uint256 ethAmount, uint256 tokenAmount) {}
|
||||
}
|
||||
72
packages/hardhat/deploy/00_deploy_dex.ts
Normal file
72
packages/hardhat/deploy/00_deploy_dex.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import { DeployFunction } from "hardhat-deploy/types";
|
||||
import { DEX } from "../typechain-types/contracts/DEX";
|
||||
import { Balloons } from "../typechain-types/contracts/Balloons";
|
||||
|
||||
/**
|
||||
* Deploys a contract named "YourContract" using the deployer account and
|
||||
* constructor arguments set to the deployer address
|
||||
*
|
||||
* @param hre HardhatRuntimeEnvironment object.
|
||||
*/
|
||||
const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
|
||||
/*
|
||||
On localhost, the deployer account is the one that comes with Hardhat, which is already funded.
|
||||
|
||||
When deploying to live networks (e.g `yarn deploy --network sepolia`), the deployer account
|
||||
should have sufficient balance to pay for the gas fees for contract creation.
|
||||
|
||||
You can generate a random account with `yarn generate` which will fill DEPLOYER_PRIVATE_KEY
|
||||
with a random private key in the .env file (then used on hardhat.config.ts)
|
||||
You can run the `yarn account` command to check your balance in every network.
|
||||
*/
|
||||
const { deployer } = await hre.getNamedAccounts();
|
||||
const { deploy } = hre.deployments;
|
||||
|
||||
await deploy("Balloons", {
|
||||
from: deployer,
|
||||
// Contract constructor arguments
|
||||
//args: [deployer],
|
||||
log: true,
|
||||
// autoMine: can be passed to the deploy function to make the deployment process faster on local networks by
|
||||
// automatically mining the contract deployment transaction. There is no effect on live networks.
|
||||
autoMine: true,
|
||||
});
|
||||
// Get the deployed contract
|
||||
// const yourContract = await hre.ethers.getContract("YourContract", deployer);
|
||||
const balloons: Balloons = await hre.ethers.getContract("Balloons", deployer);
|
||||
const balloonsAddress = await balloons.getAddress();
|
||||
|
||||
await deploy("DEX", {
|
||||
from: deployer,
|
||||
// Contract constructor arguments
|
||||
args: [balloonsAddress],
|
||||
log: true,
|
||||
// autoMine: can be passed to the deploy function to make the deployment process faster on local networks by
|
||||
// automatically mining the contract deployment transaction. There is no effect on live networks.
|
||||
autoMine: true,
|
||||
});
|
||||
|
||||
const dex = (await hre.ethers.getContract("DEX", deployer)) as DEX;
|
||||
|
||||
// // paste in your front-end address here to get 10 balloons on deploy:
|
||||
// await balloons.transfer("YOUR_FRONTEND_ADDRESS", "" + 10 * 10 ** 18);
|
||||
|
||||
// // uncomment to init DEX on deploy:
|
||||
|
||||
// const dexAddress = await dex.getAddress();
|
||||
// console.log("Approving DEX (" + dexAddress + ") to take Balloons from main account...");
|
||||
// // If you are going to the testnet make sure your deployer account has enough ETH
|
||||
// await balloons.approve(dexAddress, hre.ethers.parseEther("100"));
|
||||
// console.log("INIT exchange...");
|
||||
// await dex.init(hre.ethers.parseEther("5"), {
|
||||
// value: hre.ethers.parseEther("5"),
|
||||
// gasLimit: 200000,
|
||||
// });
|
||||
};
|
||||
|
||||
export default deployYourContract;
|
||||
|
||||
// Tags are useful if you have multiple deploy files and only want to run one of them.
|
||||
// e.g. yarn deploy --tags YourContract
|
||||
deployYourContract.tags = ["Balloons", "DEX"];
|
||||
44
packages/hardhat/eslint.config.mjs
Normal file
44
packages/hardhat/eslint.config.mjs
Normal file
@@ -0,0 +1,44 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import prettierPlugin from "eslint-plugin-prettier";
|
||||
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(["**/artifacts", "**/cache", "**/contracts", "**/node_modules/", "**/typechain-types", "**/*.json"]),
|
||||
{
|
||||
extends: compat.extends("plugin:@typescript-eslint/recommended", "prettier"),
|
||||
|
||||
plugins: {
|
||||
prettier: prettierPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
},
|
||||
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
||||
"prettier/prettier": [
|
||||
"warn",
|
||||
{
|
||||
endOfLine: "auto",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
152
packages/hardhat/hardhat.config.ts
Normal file
152
packages/hardhat/hardhat.config.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { HardhatUserConfig } from "hardhat/config";
|
||||
import "@nomicfoundation/hardhat-ethers";
|
||||
import "@nomicfoundation/hardhat-chai-matchers";
|
||||
import "@typechain/hardhat";
|
||||
import "hardhat-gas-reporter";
|
||||
import "solidity-coverage";
|
||||
import "@nomicfoundation/hardhat-verify";
|
||||
import "hardhat-deploy";
|
||||
import "hardhat-deploy-ethers";
|
||||
import { task } from "hardhat/config";
|
||||
import generateTsAbis from "./scripts/generateTsAbis";
|
||||
|
||||
// If not set, it uses ours Alchemy's default API key.
|
||||
// You can get your own at https://dashboard.alchemyapi.io
|
||||
const providerApiKey = process.env.ALCHEMY_API_KEY || "oKxs-03sij-U_N0iOlrSsZFr29-IqbuF";
|
||||
// If not set, it uses the hardhat account 0 private key.
|
||||
// You can generate a random account with `yarn generate` or `yarn account:import` to import your existing PK
|
||||
const deployerPrivateKey =
|
||||
process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY ?? "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
|
||||
// If not set, it uses our block explorers default API keys.
|
||||
const etherscanApiKey = process.env.ETHERSCAN_V2_API_KEY || "DNXJA8RX2Q3VZ4URQIWP7Z68CJXQZSC6AW";
|
||||
|
||||
const config: HardhatUserConfig = {
|
||||
solidity: {
|
||||
compilers: [
|
||||
{
|
||||
version: "0.8.20",
|
||||
settings: {
|
||||
optimizer: {
|
||||
enabled: true,
|
||||
// https://docs.soliditylang.org/en/latest/using-the-compiler.html#optimizer-options
|
||||
runs: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultNetwork: "localhost",
|
||||
namedAccounts: {
|
||||
deployer: {
|
||||
// By default, it will take the first Hardhat account as the deployer
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
networks: {
|
||||
// View the networks that are pre-configured.
|
||||
// If the network you are looking for is not here you can add new network settings
|
||||
hardhat: {
|
||||
forking: {
|
||||
url: `https://eth-mainnet.alchemyapi.io/v2/${providerApiKey}`,
|
||||
enabled: process.env.MAINNET_FORKING_ENABLED === "true",
|
||||
},
|
||||
},
|
||||
mainnet: {
|
||||
url: "https://mainnet.rpc.buidlguidl.com",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
sepolia: {
|
||||
url: `https://eth-sepolia.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
arbitrum: {
|
||||
url: `https://arb-mainnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
arbitrumSepolia: {
|
||||
url: `https://arb-sepolia.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
optimism: {
|
||||
url: `https://opt-mainnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
optimismSepolia: {
|
||||
url: `https://opt-sepolia.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
polygon: {
|
||||
url: `https://polygon-mainnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
polygonAmoy: {
|
||||
url: `https://polygon-amoy.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
polygonZkEvm: {
|
||||
url: `https://polygonzkevm-mainnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
polygonZkEvmCardona: {
|
||||
url: `https://polygonzkevm-cardona.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
gnosis: {
|
||||
url: "https://rpc.gnosischain.com",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
chiado: {
|
||||
url: "https://rpc.chiadochain.net",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
base: {
|
||||
url: "https://mainnet.base.org",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
baseSepolia: {
|
||||
url: "https://sepolia.base.org",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
scrollSepolia: {
|
||||
url: "https://sepolia-rpc.scroll.io",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
scroll: {
|
||||
url: "https://rpc.scroll.io",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
celo: {
|
||||
url: "https://forno.celo.org",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
celoAlfajores: {
|
||||
url: "https://alfajores-forno.celo-testnet.org",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
},
|
||||
// Configuration for harhdat-verify plugin
|
||||
etherscan: {
|
||||
apiKey: etherscanApiKey,
|
||||
},
|
||||
// Configuration for etherscan-verify from hardhat-deploy plugin
|
||||
verify: {
|
||||
etherscan: {
|
||||
apiKey: etherscanApiKey,
|
||||
},
|
||||
},
|
||||
sourcify: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Extend the deploy task
|
||||
task("deploy").setAction(async (args, hre, runSuper) => {
|
||||
// Run the original deploy task
|
||||
await runSuper(args);
|
||||
// Force run the generateTsAbis script
|
||||
await generateTsAbis(hre);
|
||||
});
|
||||
|
||||
export default config;
|
||||
63
packages/hardhat/package.json
Normal file
63
packages/hardhat/package.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "@se-2/hardhat",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"account": "hardhat run scripts/listAccount.ts",
|
||||
"account:generate": "hardhat run scripts/generateAccount.ts",
|
||||
"account:import": "hardhat run scripts/importAccount.ts",
|
||||
"account:reveal-pk": "hardhat run scripts/revealPK.ts",
|
||||
"chain": "hardhat node --network hardhat --no-deploy",
|
||||
"check-types": "tsc --noEmit --incremental",
|
||||
"clean": "hardhat clean",
|
||||
"compile": "hardhat compile",
|
||||
"deploy": "ts-node scripts/runHardhatDeployWithPK.ts",
|
||||
"flatten": "hardhat flatten",
|
||||
"fork": "MAINNET_FORKING_ENABLED=true hardhat node --network hardhat --no-deploy",
|
||||
"format": "prettier --write './**/*.(ts|sol)'",
|
||||
"generate": "yarn account:generate",
|
||||
"hardhat-verify": "hardhat verify",
|
||||
"lint": "eslint",
|
||||
"lint-staged": "eslint",
|
||||
"test": "REPORT_GAS=true hardhat test --network hardhat",
|
||||
"verify": "hardhat etherscan-verify"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/password": "^4.0.2",
|
||||
"@openzeppelin/contracts": "~5.0.2",
|
||||
"@typechain/ethers-v6": "~0.5.1",
|
||||
"dotenv": "~16.4.5",
|
||||
"envfile": "~7.1.0",
|
||||
"qrcode": "~1.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ethersproject/abi": "~5.7.0",
|
||||
"@ethersproject/providers": "~5.7.2",
|
||||
"@nomicfoundation/hardhat-chai-matchers": "~2.0.7",
|
||||
"@nomicfoundation/hardhat-ethers": "~3.0.8",
|
||||
"@nomicfoundation/hardhat-network-helpers": "~1.0.11",
|
||||
"@nomicfoundation/hardhat-verify": "~2.0.10",
|
||||
"@typechain/ethers-v5": "~11.1.2",
|
||||
"@typechain/hardhat": "~9.1.0",
|
||||
"@types/eslint": "~9.6.1",
|
||||
"@types/mocha": "~10.0.10",
|
||||
"@types/prettier": "~3.0.0",
|
||||
"@types/qrcode": "~1.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "~8.27.0",
|
||||
"@typescript-eslint/parser": "~8.27.0",
|
||||
"chai": "~4.5.0",
|
||||
"eslint": "~9.23.0",
|
||||
"eslint-config-prettier": "~10.1.1",
|
||||
"eslint-plugin-prettier": "~5.2.4",
|
||||
"ethers": "~6.13.2",
|
||||
"hardhat": "~2.22.10",
|
||||
"hardhat-deploy": "^1.0.4",
|
||||
"hardhat-deploy-ethers": "~0.4.2",
|
||||
"hardhat-gas-reporter": "~2.2.1",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-solidity": "~1.4.1",
|
||||
"solidity-coverage": "~0.8.13",
|
||||
"ts-node": "~10.9.1",
|
||||
"typechain": "~8.3.2",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
58
packages/hardhat/scripts/generateAccount.ts
Normal file
58
packages/hardhat/scripts/generateAccount.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { ethers } from "ethers";
|
||||
import { parse, stringify } from "envfile";
|
||||
import * as fs from "fs";
|
||||
import password from "@inquirer/password";
|
||||
|
||||
const envFilePath = "./.env";
|
||||
|
||||
const getValidatedPassword = async () => {
|
||||
while (true) {
|
||||
const pass = await password({ message: "Enter a password to encrypt your private key:" });
|
||||
const confirmation = await password({ message: "Confirm password:" });
|
||||
|
||||
if (pass === confirmation) {
|
||||
return pass;
|
||||
}
|
||||
console.log("❌ Passwords don't match. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const setNewEnvConfig = async (existingEnvConfig = {}) => {
|
||||
console.log("👛 Generating new Wallet\n");
|
||||
const randomWallet = ethers.Wallet.createRandom();
|
||||
|
||||
const pass = await getValidatedPassword();
|
||||
const encryptedJson = await randomWallet.encrypt(pass);
|
||||
|
||||
const newEnvConfig = {
|
||||
...existingEnvConfig,
|
||||
DEPLOYER_PRIVATE_KEY_ENCRYPTED: encryptedJson,
|
||||
};
|
||||
|
||||
// Store in .env
|
||||
fs.writeFileSync(envFilePath, stringify(newEnvConfig));
|
||||
console.log("\n📄 Encrypted Private Key saved to packages/hardhat/.env file");
|
||||
console.log("🪄 Generated wallet address:", randomWallet.address, "\n");
|
||||
console.log("⚠️ Make sure to remember your password! You'll need it to decrypt the private key.");
|
||||
};
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(envFilePath)) {
|
||||
// No .env file yet.
|
||||
await setNewEnvConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
const existingEnvConfig = parse(fs.readFileSync(envFilePath).toString());
|
||||
if (existingEnvConfig.DEPLOYER_PRIVATE_KEY_ENCRYPTED) {
|
||||
console.log("⚠️ You already have a deployer account. Check the packages/hardhat/.env file");
|
||||
return;
|
||||
}
|
||||
|
||||
await setNewEnvConfig(existingEnvConfig);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
127
packages/hardhat/scripts/generateTsAbis.ts
Normal file
127
packages/hardhat/scripts/generateTsAbis.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* DON'T MODIFY OR DELETE THIS SCRIPT (unless you know what you're doing)
|
||||
*
|
||||
* This script generates the file containing the contracts Abi definitions.
|
||||
* These definitions are used to derive the types needed in the custom scaffold-eth hooks, for example.
|
||||
* This script should run as the last deploy script.
|
||||
*/
|
||||
|
||||
import * as fs from "fs";
|
||||
import prettier from "prettier";
|
||||
import { DeployFunction } from "hardhat-deploy/types";
|
||||
|
||||
const generatedContractComment = `
|
||||
/**
|
||||
* This file is autogenerated by Scaffold-ETH.
|
||||
* You should not edit it manually or your changes might be overwritten.
|
||||
*/
|
||||
`;
|
||||
|
||||
const DEPLOYMENTS_DIR = "./deployments";
|
||||
const ARTIFACTS_DIR = "./artifacts";
|
||||
|
||||
function getDirectories(path: string) {
|
||||
return fs
|
||||
.readdirSync(path, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name);
|
||||
}
|
||||
|
||||
function getContractNames(path: string) {
|
||||
return fs
|
||||
.readdirSync(path, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isFile() && dirent.name.endsWith(".json"))
|
||||
.map(dirent => dirent.name.split(".")[0]);
|
||||
}
|
||||
|
||||
function getActualSourcesForContract(sources: Record<string, any>, contractName: string) {
|
||||
for (const sourcePath of Object.keys(sources)) {
|
||||
const sourceName = sourcePath.split("/").pop()?.split(".sol")[0];
|
||||
if (sourceName === contractName) {
|
||||
const contractContent = sources[sourcePath].content as string;
|
||||
const regex = /contract\s+(\w+)\s+is\s+([^{}]+)\{/;
|
||||
const match = contractContent.match(regex);
|
||||
|
||||
if (match) {
|
||||
const inheritancePart = match[2];
|
||||
// Split the inherited contracts by commas to get the list of inherited contracts
|
||||
const inheritedContracts = inheritancePart.split(",").map(contract => `${contract.trim()}.sol`);
|
||||
|
||||
return inheritedContracts;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function getInheritedFunctions(sources: Record<string, any>, contractName: string) {
|
||||
const actualSources = getActualSourcesForContract(sources, contractName);
|
||||
const inheritedFunctions = {} as Record<string, any>;
|
||||
|
||||
for (const sourceContractName of actualSources) {
|
||||
const sourcePath = Object.keys(sources).find(key => key.includes(`/${sourceContractName}`));
|
||||
if (sourcePath) {
|
||||
const sourceName = sourcePath?.split("/").pop()?.split(".sol")[0];
|
||||
const { abi } = JSON.parse(fs.readFileSync(`${ARTIFACTS_DIR}/${sourcePath}/${sourceName}.json`).toString());
|
||||
for (const functionAbi of abi) {
|
||||
if (functionAbi.type === "function") {
|
||||
inheritedFunctions[functionAbi.name] = sourcePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return inheritedFunctions;
|
||||
}
|
||||
|
||||
function getContractDataFromDeployments() {
|
||||
if (!fs.existsSync(DEPLOYMENTS_DIR)) {
|
||||
throw Error("At least one other deployment script should exist to generate an actual contract.");
|
||||
}
|
||||
const output = {} as Record<string, any>;
|
||||
for (const chainName of getDirectories(DEPLOYMENTS_DIR)) {
|
||||
const chainId = fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/.chainId`).toString();
|
||||
const contracts = {} as Record<string, any>;
|
||||
for (const contractName of getContractNames(`${DEPLOYMENTS_DIR}/${chainName}`)) {
|
||||
const { abi, address, metadata, receipt } = JSON.parse(
|
||||
fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/${contractName}.json`).toString(),
|
||||
);
|
||||
const inheritedFunctions = metadata ? getInheritedFunctions(JSON.parse(metadata).sources, contractName) : {};
|
||||
contracts[contractName] = { address, abi, inheritedFunctions, deployedOnBlock: receipt.blockNumber };
|
||||
}
|
||||
output[chainId] = contracts;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the TypeScript contract definition file based on the json output of the contract deployment scripts
|
||||
* This script should be run last.
|
||||
*/
|
||||
const generateTsAbis: DeployFunction = async function () {
|
||||
const TARGET_DIR = "../nextjs/contracts/";
|
||||
const allContractsData = getContractDataFromDeployments();
|
||||
|
||||
const fileContent = Object.entries(allContractsData).reduce((content, [chainId, chainConfig]) => {
|
||||
return `${content}${parseInt(chainId).toFixed(0)}:${JSON.stringify(chainConfig, null, 2)},`;
|
||||
}, "");
|
||||
|
||||
if (!fs.existsSync(TARGET_DIR)) {
|
||||
fs.mkdirSync(TARGET_DIR);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
`${TARGET_DIR}deployedContracts.ts`,
|
||||
await prettier.format(
|
||||
`${generatedContractComment} import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; \n\n
|
||||
const deployedContracts = {${fileContent}} as const; \n\n export default deployedContracts satisfies GenericContractsDeclaration`,
|
||||
{
|
||||
parser: "typescript",
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
console.log(`📝 Updated TypeScript contract definition file on ${TARGET_DIR}deployedContracts.ts`);
|
||||
};
|
||||
|
||||
export default generateTsAbis;
|
||||
72
packages/hardhat/scripts/importAccount.ts
Normal file
72
packages/hardhat/scripts/importAccount.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { ethers } from "ethers";
|
||||
import { parse, stringify } from "envfile";
|
||||
import * as fs from "fs";
|
||||
import password from "@inquirer/password";
|
||||
|
||||
const envFilePath = "./.env";
|
||||
|
||||
const getValidatedPassword = async () => {
|
||||
while (true) {
|
||||
const pass = await password({ message: "Enter a password to encrypt your private key:" });
|
||||
const confirmation = await password({ message: "Confirm password:" });
|
||||
|
||||
if (pass === confirmation) {
|
||||
return pass;
|
||||
}
|
||||
console.log("❌ Passwords don't match. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const getWalletFromPrivateKey = async () => {
|
||||
while (true) {
|
||||
const privateKey = await password({ message: "Paste your private key:" });
|
||||
try {
|
||||
const wallet = new ethers.Wallet(privateKey);
|
||||
return wallet;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.log("❌ Invalid private key format. Please try again.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setNewEnvConfig = async (existingEnvConfig = {}) => {
|
||||
console.log("👛 Importing Wallet\n");
|
||||
|
||||
const wallet = await getWalletFromPrivateKey();
|
||||
|
||||
const pass = await getValidatedPassword();
|
||||
const encryptedJson = await wallet.encrypt(pass);
|
||||
|
||||
const newEnvConfig = {
|
||||
...existingEnvConfig,
|
||||
DEPLOYER_PRIVATE_KEY_ENCRYPTED: encryptedJson,
|
||||
};
|
||||
|
||||
// Store in .env
|
||||
fs.writeFileSync(envFilePath, stringify(newEnvConfig));
|
||||
console.log("\n📄 Encrypted Private Key saved to packages/hardhat/.env file");
|
||||
console.log("🪄 Imported wallet address:", wallet.address, "\n");
|
||||
console.log("⚠️ Make sure to remember your password! You'll need it to decrypt the private key.");
|
||||
};
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(envFilePath)) {
|
||||
// No .env file yet.
|
||||
await setNewEnvConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
const existingEnvConfig = parse(fs.readFileSync(envFilePath).toString());
|
||||
if (existingEnvConfig.DEPLOYER_PRIVATE_KEY_ENCRYPTED) {
|
||||
console.log("⚠️ You already have a deployer account. Check the packages/hardhat/.env file");
|
||||
return;
|
||||
}
|
||||
|
||||
await setNewEnvConfig(existingEnvConfig);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
52
packages/hardhat/scripts/listAccount.ts
Normal file
52
packages/hardhat/scripts/listAccount.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { ethers, Wallet } from "ethers";
|
||||
import QRCode from "qrcode";
|
||||
import { config } from "hardhat";
|
||||
import password from "@inquirer/password";
|
||||
|
||||
async function main() {
|
||||
const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
|
||||
|
||||
if (!encryptedKey) {
|
||||
console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
|
||||
return;
|
||||
}
|
||||
|
||||
const pass = await password({ message: "Enter your password to decrypt the private key:" });
|
||||
let wallet: Wallet;
|
||||
try {
|
||||
wallet = (await Wallet.fromEncryptedJson(encryptedKey, pass)) as Wallet;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.log("❌ Failed to decrypt private key. Wrong password?");
|
||||
return;
|
||||
}
|
||||
|
||||
const address = wallet.address;
|
||||
console.log(await QRCode.toString(address, { type: "terminal", small: true }));
|
||||
console.log("Public address:", address, "\n");
|
||||
|
||||
// Balance on each network
|
||||
const availableNetworks = config.networks;
|
||||
for (const networkName in availableNetworks) {
|
||||
try {
|
||||
const network = availableNetworks[networkName];
|
||||
if (!("url" in network)) continue;
|
||||
const provider = new ethers.JsonRpcProvider(network.url);
|
||||
await provider._detectNetwork();
|
||||
const balance = await provider.getBalance(address);
|
||||
console.log("--", networkName, "-- 📡");
|
||||
console.log(" balance:", +ethers.formatEther(balance));
|
||||
console.log(" nonce:", +(await provider.getTransactionCount(address)));
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.log("Can't connect to network", networkName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
31
packages/hardhat/scripts/revealPK.ts
Normal file
31
packages/hardhat/scripts/revealPK.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { Wallet } from "ethers";
|
||||
import password from "@inquirer/password";
|
||||
|
||||
async function main() {
|
||||
const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
|
||||
|
||||
if (!encryptedKey) {
|
||||
console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("👀 This will reveal your private key on the console.\n");
|
||||
|
||||
const pass = await password({ message: "Enter your password to decrypt the private key:" });
|
||||
let wallet: Wallet;
|
||||
try {
|
||||
wallet = (await Wallet.fromEncryptedJson(encryptedKey, pass)) as Wallet;
|
||||
} catch {
|
||||
console.log("❌ Failed to decrypt private key. Wrong password?");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("\n🔑 Private key:", wallet.privateKey);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
58
packages/hardhat/scripts/runHardhatDeployWithPK.ts
Normal file
58
packages/hardhat/scripts/runHardhatDeployWithPK.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { Wallet } from "ethers";
|
||||
import password from "@inquirer/password";
|
||||
import { spawn } from "child_process";
|
||||
import { config } from "hardhat";
|
||||
|
||||
/**
|
||||
* Unencrypts the private key and runs the hardhat deploy command
|
||||
*/
|
||||
async function main() {
|
||||
const networkIndex = process.argv.indexOf("--network");
|
||||
const networkName = networkIndex !== -1 ? process.argv[networkIndex + 1] : config.defaultNetwork;
|
||||
|
||||
if (networkName === "localhost" || networkName === "hardhat") {
|
||||
// Deploy command on the localhost network
|
||||
const hardhat = spawn("hardhat", ["deploy", ...process.argv.slice(2)], {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
hardhat.on("exit", code => {
|
||||
process.exit(code || 0);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
|
||||
|
||||
if (!encryptedKey) {
|
||||
console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
|
||||
return;
|
||||
}
|
||||
|
||||
const pass = await password({ message: "Enter password to decrypt private key:" });
|
||||
|
||||
try {
|
||||
const wallet = await Wallet.fromEncryptedJson(encryptedKey, pass);
|
||||
process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY = wallet.privateKey;
|
||||
|
||||
const hardhat = spawn("hardhat", ["deploy", ...process.argv.slice(2)], {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
hardhat.on("exit", code => {
|
||||
process.exit(code || 0);
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.error("Failed to decrypt private key. Wrong password?");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
2
packages/hardhat/test/.gitkeep
Normal file
2
packages/hardhat/test/.gitkeep
Normal file
@@ -0,0 +1,2 @@
|
||||
# Write tests for your smart contract in this directory
|
||||
# Example: YourContract.ts
|
||||
386
packages/hardhat/test/DEX.ts
Normal file
386
packages/hardhat/test/DEX.ts
Normal 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 -----------------
|
||||
});
|
||||
11
packages/hardhat/tsconfig.json
Normal file
11
packages/hardhat/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user