├── contracts ├── test │ └── MockV3Aggregator.sol ├── PriceConverter.sol └── FundMe.sol ├── .gitignore ├── README.md ├── ignition └── modules │ └── Lock.js ├── helper-hardhat-config.js ├── utils └── verify.js ├── script ├── withdraw.js └── fund.js ├── deploy ├── 00-deploy-mocks.js └── 01-deploy-fund-me.js ├── test ├── staging │ └── FundMe.staging.test.js └── unit │ └── FundMe.test.js ├── hardhat.config.js └── package.json /contracts/test/MockV3Aggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@chainlink/contracts/src/v0.8/tests/MockV3Aggregator.sol"; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | gas-report.txt 4 | deployments 5 | 6 | # Hardhat files 7 | /cache 8 | /artifacts 9 | 10 | # TypeChain files 11 | /typechain 12 | /typechain-types 13 | 14 | # solidity-coverage files 15 | /coverage 16 | /coverage.json 17 | 18 | # Hardhat Ignition default folder for deployments against a local node 19 | ignition/deployments/chain-31337 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sample Hardhat Project 2 | 3 | This project demonstrates a basic Hardhat use case. It comes with a sample contract, a test for that contract, and a Hardhat Ignition module that deploys that contract. 4 | 5 | Try running some of the following tasks: 6 | 7 | ```shell 8 | npx hardhat help 9 | npx hardhat test 10 | REPORT_GAS=true npx hardhat test 11 | npx hardhat node 12 | npx hardhat ignition deploy ./ignition/modules/Lock.js 13 | ``` 14 | -------------------------------------------------------------------------------- /ignition/modules/Lock.js: -------------------------------------------------------------------------------- 1 | const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules"); 2 | 3 | const JAN_1ST_2030 = 1893456000; 4 | const ONE_GWEI = 1_000_000_000n; 5 | 6 | module.exports = buildModule("LockModule", (m) => { 7 | const unlockTime = m.getParameter("unlockTime", JAN_1ST_2030); 8 | const lockedAmount = m.getParameter("lockedAmount", ONE_GWEI); 9 | 10 | const lock = m.contract("Lock", [unlockTime], { 11 | value: lockedAmount, 12 | }); 13 | 14 | return { lock }; 15 | }); 16 | -------------------------------------------------------------------------------- /helper-hardhat-config.js: -------------------------------------------------------------------------------- 1 | const networkConfig = { 2 | 11155111: { 3 | name: "sepolia", 4 | ethUsdPriceFeed: "0x694AA1769357215DE4FAC081bf1f309aDC325306", 5 | }, 6 | 4: { 7 | name: "rinkby", 8 | ethUsdPriceFeed: "", 9 | }, 10 | 137: { 11 | name: "polygon", 12 | ethUsdPriceFeed: "", 13 | }, 14 | }; 15 | 16 | const developmentChains = ["hardhat", "localhost"]; 17 | const DECIMALS = 8; 18 | const INITIAL_ANSWER = 200000000000; 19 | 20 | module.exports = { networkConfig, developmentChains, DECIMALS, INITIAL_ANSWER }; 21 | -------------------------------------------------------------------------------- /utils/verify.js: -------------------------------------------------------------------------------- 1 | const { run } = require("hardhat"); 2 | 3 | async function verify(contractAddress, args) { 4 | console.log("verifying contract..."); 5 | try { 6 | await run("verify:verify", { 7 | address: contractAddress, 8 | constructorArguments: args, 9 | }); 10 | console.log("Verification Successful!"); 11 | } catch (error) { 12 | if (error.message.toLowerCase().includes("already verified")) { 13 | console.log("Already Verified!"); 14 | } else { 15 | console.log("Verification Error:", error); 16 | } 17 | } 18 | } 19 | 20 | module.exports = { verify }; 21 | -------------------------------------------------------------------------------- /script/withdraw.js: -------------------------------------------------------------------------------- 1 | const { ethers, deployments } = require("hardhat"); 2 | 3 | const getAddress = async (contractName) => { 4 | return (await deployments.get(contractName)).address; 5 | }; 6 | 7 | async function main() { 8 | const deployer = await ethers.provider.getSigner(); 9 | const fundMe = await ethers.getContractAt( 10 | "FundMe", 11 | await getAddress("FundMe"), 12 | deployer 13 | ); 14 | console.log("withdrawing funds..."); 15 | const transactionResponse = await fundMe.withdraw(); 16 | await transactionResponse.wait(1); 17 | console.log("Got it back!"); 18 | } 19 | 20 | main() 21 | .then(() => process.exit(0)) 22 | .catch((error) => { 23 | console.error(error); 24 | process.exit(1); 25 | }); 26 | -------------------------------------------------------------------------------- /deploy/00-deploy-mocks.js: -------------------------------------------------------------------------------- 1 | const { network } = require("hardhat"); 2 | const { 3 | developmentChains, 4 | DECIMALS, 5 | INITIAL_ANSWER, 6 | } = require("../helper-hardhat-config"); 7 | 8 | module.exports = async ({ getNamedAccounts, deployments }) => { 9 | const { deploy, log } = deployments; 10 | const { deployer } = await getNamedAccounts(); 11 | 12 | if (developmentChains.includes(network.name)) { 13 | log("Local network detected! Deploying mocks..."); 14 | await deploy("MockV3Aggregator", { 15 | contract: "MockV3Aggregator", 16 | from: deployer, 17 | log: true, 18 | args: [DECIMALS, INITIAL_ANSWER], 19 | }); 20 | log("Mocks deployed!"); 21 | log("--------------------------------------------------"); 22 | } 23 | }; 24 | 25 | module.exports.tags = ["all", "mocks"]; 26 | -------------------------------------------------------------------------------- /script/fund.js: -------------------------------------------------------------------------------- 1 | const { ethers, deployments } = require("hardhat"); 2 | 3 | const getAddress = async (contractName) => { 4 | return (await deployments.get(contractName)).address; 5 | }; 6 | 7 | async function main() { 8 | const deployer = await ethers.provider.getSigner(); 9 | const fundMe = await ethers.getContractAt( 10 | "FundMe", 11 | await getAddress("FundMe"), 12 | deployer 13 | ); 14 | console.log("Funding Contract..."); 15 | // Fund with an amount between MINIMUM_FUND_AMOUNT (0.01 ether) and MAXIMUM_FUND_AMOUNT (10 ether) 16 | const transactionResponse = await fundMe.fund({ 17 | value: ethers.parseEther("0.5"), // Changed from 0.1 to 0.5 for demonstration 18 | }); 19 | await transactionResponse.wait(1); 20 | console.log("Funded!"); 21 | } 22 | 23 | main() 24 | .then(() => process.exit(0)) 25 | .catch((error) => { 26 | console.error(error); 27 | process.exit(1); 28 | }); 29 | -------------------------------------------------------------------------------- /test/staging/FundMe.staging.test.js: -------------------------------------------------------------------------------- 1 | const { assert, expect } = require("chai"); 2 | const { deployments, ethers, network } = require("hardhat"); 3 | const { developmentChains } = require("../../helper-hardhat-config"); 4 | 5 | developmentChains.includes(network.name) 6 | ? describe.skip 7 | : describe("FundMe", function () { 8 | let fundMe; 9 | let deployer; 10 | const sendValue = ethers.parseEther("1"); 11 | 12 | const getAddress = async (contractName) => { 13 | return (await deployments.get(contractName)).address; 14 | }; 15 | 16 | beforeEach(async function () { 17 | deployer = await ethers.provider.getSigner(); 18 | fundMe = await ethers.getContractAt( 19 | "FundMe", 20 | await getAddress("FundMe"), 21 | deployer 22 | ); 23 | }); 24 | 25 | it("allows people to fund and withdraw", async function () { 26 | await fundMe.fund({ value: sendValue }); 27 | await fundMe.withdraw(); 28 | const endingBalance = await ethers.provider.getBalance( 29 | await fundMe.getAddress() 30 | ); 31 | assert.equal(endingBalance.toString(), "0"); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomicfoundation/hardhat-toolbox"); 2 | require("hardhat-deploy"); 3 | require("dotenv").config(); 4 | 5 | const SEPOLIA_RPC_URL = process.env.SEPOLIA_RPC_URL || ""; 6 | const PRIVATE_KEY = process.env.PRIVATE_KEY || ""; 7 | const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY || ""; 8 | const COINMARKETCAP_API_KEY = process.env.COINMARKETCAP_API_KEY || ""; 9 | 10 | /** @type import('hardhat/config').HardhatUserConfig */ 11 | module.exports = { 12 | solidity: { 13 | compilers: [{ version: "0.8.8" }], 14 | }, 15 | defaultNetwork: "hardhat", 16 | networks: { 17 | sepolia: { 18 | url: SEPOLIA_RPC_URL, 19 | accounts: [PRIVATE_KEY], 20 | chainId: 11155111, 21 | blockConfirmations: 6, 22 | }, 23 | localhost: { 24 | url: "http://127.0.0.1:8545/", 25 | chainId: 31337, 26 | }, 27 | }, 28 | namedAccounts: { 29 | deployer: { 30 | default: 0, 31 | }, 32 | }, 33 | etherscan: { 34 | apiKey: ETHERSCAN_API_KEY, 35 | }, 36 | gasReporter: { 37 | enabled: true, 38 | outputFile: "gas-report.txt", 39 | noColors: true, 40 | currency: "USD", 41 | // coinmarketcap: COINMARKETCAP_API_KEY, 42 | token: "MATIC", 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /contracts/PriceConverter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.8; 3 | 4 | import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; 5 | 6 | // Why is this a library and not abstract? 7 | // Why not an interface? 8 | library PriceConverter { 9 | // We could make this public, but then we'd have to deploy it 10 | function getPrice( 11 | AggregatorV3Interface priceFeed 12 | ) internal view returns (uint256) { 13 | (, int256 answer, , , ) = priceFeed.latestRoundData(); 14 | // ETH/USD rate in 18 digit 15 | return uint256(answer * 10000000000); 16 | // or (Both will do the same thing) 17 | // return uint256(answer * 1e10); // 1* 10 ** 10 == 10000000000 18 | } 19 | 20 | // 1000000000 21 | function getConversionRate( 22 | uint256 ethAmount, 23 | AggregatorV3Interface priceFeed 24 | ) internal view returns (uint256) { 25 | uint256 ethPrice = getPrice(priceFeed); 26 | uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1000000000000000000; 27 | // or (Both will do the same thing) 28 | // uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1e18; // 1 * 10 ** 18 == 1000000000000000000 29 | // the actual ETH/USD conversion rate, after adjusting the extra 0s. 30 | return ethAmountInUsd; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardhat-fund-me", 3 | "author": "Kunal K", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "test": "yarn hardhat test", 7 | "test:staging": "yarn hardhat test --network sepolia", 8 | "lint": "yarn solhint 'contracts/*.sol'", 9 | "lint:fix": "yarn solhint 'contracts/*.sol' --fix", 10 | "format": "yarn prettier --write .", 11 | "coverage": "yarn hardhat coverage" 12 | }, 13 | "devDependencies": { 14 | "@chainlink/contracts": "^1.1.1", 15 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", 16 | "@nomicfoundation/hardhat-ethers": "^3.0.0", 17 | "@nomicfoundation/hardhat-ignition": "^0.15.0", 18 | "@nomicfoundation/hardhat-ignition-ethers": "^0.15.0", 19 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0", 20 | "@nomicfoundation/hardhat-toolbox": "^5.0.0", 21 | "@nomicfoundation/hardhat-verify": "^2.0.0", 22 | "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers", 23 | "@nomiclabs/hardhat-waffle": "^2.0.6", 24 | "@typechain/ethers-v6": "^0.5.0", 25 | "@typechain/hardhat": "^9.0.0", 26 | "chai": "^4.2.0", 27 | "dotenv": "^16.4.5", 28 | "ethers": "^6.12.1", 29 | "hardhat": "^2.22.4", 30 | "hardhat-deploy": "^0.12.4", 31 | "hardhat-gas-reporter": "^1.0.8", 32 | "solidity-coverage": "^0.8.0", 33 | "typechain": "^8.3.0" 34 | }, 35 | "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" 36 | } 37 | -------------------------------------------------------------------------------- /deploy/01-deploy-fund-me.js: -------------------------------------------------------------------------------- 1 | const { network } = require("hardhat"); 2 | const { 3 | networkConfig, 4 | developmentChains, 5 | } = require("../helper-hardhat-config"); 6 | const { verify } = require("../utils/verify"); 7 | 8 | module.exports = async ({ getNamedAccounts, deployments }) => { 9 | const { deploy, log } = deployments; 10 | const { deployer } = await getNamedAccounts(); 11 | const chainId = network.config.chainId; 12 | 13 | if (developmentChains.includes(network.name)) { 14 | // if contract doesn't exist, we deploy a minimal version for our local testing 15 | const ethUsdAggregator = await deployments.get("MockV3Aggregator"); 16 | ethUsdPriceFeedAddress = ethUsdAggregator.address; 17 | } else { 18 | // get pricefeed based on chainId 19 | ethUsdPriceFeedAddress = networkConfig[chainId]["ethUsdPriceFeed"]; 20 | } 21 | 22 | const args = [ethUsdPriceFeedAddress]; 23 | 24 | // when going for localhost or hardhat network we want to use mock 25 | const fundMe = await deploy("FundMe", { 26 | from: deployer, 27 | args: args, 28 | log: true, 29 | waitConfirmations: network.config.blockConfirmations || 1, 30 | }); 31 | 32 | if ( 33 | !developmentChains.includes(network.name) && 34 | process.env.ETHERSCAN_API_KEY 35 | ) { 36 | await verify(fundMe.address, args); 37 | } 38 | log("------------------------------------------"); 39 | }; 40 | 41 | module.exports.tags = ["all", "fundme"]; 42 | -------------------------------------------------------------------------------- /contracts/FundMe.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // pragma 4 | pragma solidity ^0.8.8; 5 | 6 | // imports 7 | import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; 8 | import "./PriceConverter.sol"; 9 | 10 | // Error codes 11 | error FundMe__NotOwner(); 12 | 13 | // Interfaces, Libraries, Contracts 14 | /** 15 | * @title A contract for crowd finding 16 | * @author Kunal K. 17 | * @notice This contract is to demo sample funding contract 18 | * @dev This implements price feeds as our library 19 | */ 20 | contract FundMe { 21 | // type declarations 22 | using PriceConverter for uint256; 23 | 24 | // State Variables 25 | mapping(address => uint256) private s_addressToAmountFunded; 26 | address[] private s_funders; 27 | address private i_owner; 28 | uint256 public constant MINIMUM_USD = 50 * 10 ** 18; 29 | uint256 public constant MINIMUM_FUND_AMOUNT = 0.01 ether; // Example: 0.01 ETH 30 | uint256 public constant MAXIMUM_FUND_AMOUNT = 10 ether; // Example: 10 ETH 31 | AggregatorV3Interface private s_priceFeed; 32 | 33 | // Modifiers 34 | modifier onlyOwner() { 35 | // require(msg.sender == owner); 36 | if (msg.sender != i_owner) { 37 | revert FundMe__NotOwner(); 38 | } 39 | _; 40 | } 41 | 42 | // Constructor 43 | constructor(address priceFeedAddress) { 44 | i_owner = msg.sender; 45 | s_priceFeed = AggregatorV3Interface(priceFeedAddress); 46 | } 47 | 48 | // Receive 49 | receive() external payable { 50 | fund(); 51 | } 52 | 53 | // Fallback 54 | fallback() external payable { 55 | fund(); 56 | } 57 | 58 | // Other functions(external, public, internal, private) 59 | /** 60 | * @notice This function funds this contract 61 | * @dev This implements price feeds as our library 62 | */ 63 | function fund() public payable { 64 | require( 65 | msg.value.getConversionRate(s_priceFeed) >= MINIMUM_USD, 66 | "You need to spend more ETH!" 67 | ); 68 | require(msg.value >= MINIMUM_FUND_AMOUNT, "Fund amount is too low!"); 69 | require(msg.value <= MAXIMUM_FUND_AMOUNT, "Fund amount is too high!"); 70 | // require(PriceConverter.getConversionRate(msg.value) >= MINIMUM_USD, "You need to spend more ETH!"); 71 | s_addressToAmountFunded[msg.sender] += msg.value; 72 | s_funders.push(msg.sender); 73 | } 74 | 75 | // function withdraw() public onlyOwner { 76 | // for ( 77 | // uint256 funderIndex = 0; 78 | // funderIndex < s_funders.length; 79 | // funderIndex++ 80 | // ) { 81 | // address funder = s_funders[funderIndex]; 82 | // s_addressToAmountFunded[funder] = 0; 83 | // } 84 | // s_funders = new address[](0); 85 | // // // transfer 86 | // // payable(msg.sender).transfer(address(this).balance); 87 | // // // send 88 | // // bool sendSuccess = payable(msg.sender).send(address(this).balance); 89 | // // require(sendSuccess, "Send failed"); 90 | // // call 91 | // (bool callSuccess, ) = payable(msg.sender).call{ 92 | // value: address(this).balance 93 | // }(""); 94 | // require(callSuccess, "Call failed"); 95 | // } 96 | 97 | function withdraw() public payable onlyOwner { 98 | address[] memory funders = s_funders; 99 | for ( 100 | uint256 funderIndex = 0; 101 | funderIndex < funders.length; 102 | funderIndex++ 103 | ) { 104 | address funder = funders[funderIndex]; 105 | s_addressToAmountFunded[funder] = 0; 106 | } 107 | s_funders = new address[](0); 108 | (bool success, ) = i_owner.call{value: address(this).balance}(""); 109 | require(success, "Call failed"); 110 | } 111 | 112 | // view/pure 113 | function getOwner() public view returns (address) { 114 | return i_owner; 115 | } 116 | 117 | function getFunder(uint256 index) public view returns (address) { 118 | return s_funders[index]; 119 | } 120 | 121 | function getAddressToAmountFunded( 122 | address funder 123 | ) public view returns (uint256) { 124 | return s_addressToAmountFunded[funder]; 125 | } 126 | 127 | function getPriceFeed() public view returns (AggregatorV3Interface) { 128 | return s_priceFeed; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /test/unit/FundMe.test.js: -------------------------------------------------------------------------------- 1 | const { assert, expect } = require("chai"); 2 | const { deployments, ethers, network } = require("hardhat"); 3 | const { developmentChains } = require("../../helper-hardhat-config"); 4 | 5 | !developmentChains.includes(network.name) 6 | ? describe.skip 7 | : describe("FundMe", function () { 8 | let fundMe; 9 | let deployer; 10 | let mockV3Aggregator; 11 | const sendValue = ethers.parseEther("1"); 12 | 13 | const getAddress = async (contractName) => { 14 | return (await deployments.get(contractName)).address; 15 | }; 16 | 17 | beforeEach(async function () { 18 | deployer = await ethers.provider.getSigner(); 19 | await deployments.fixture(["all"]); 20 | fundMe = await ethers.getContractAt( 21 | "FundMe", 22 | await getAddress("FundMe"), 23 | deployer 24 | ); 25 | mockV3Aggregator = await ethers.getContractAt( 26 | "MockV3Aggregator", 27 | await getAddress("MockV3Aggregator"), 28 | deployer 29 | ); 30 | }); 31 | 32 | describe("constructor", function () { 33 | it("sets the aggregator addresses correctly", async function () { 34 | const response = await fundMe.getPriceFeed(); 35 | assert.equal(response, await mockV3Aggregator.getAddress()); 36 | }); 37 | }); 38 | 39 | describe("fund", async function () { 40 | it("should fail if you don't send enough ETH", async function () { 41 | await expect(fundMe.fund()).to.be.revertedWith( 42 | "You need to spend more ETH!" 43 | ); 44 | }); 45 | it("updates the amount funded data structure", async function () { 46 | await fundMe.fund({ value: sendValue }); 47 | const response = await fundMe.getAddressToAmountFunded( 48 | deployer.address 49 | ); 50 | assert.equal(response.toString(), sendValue.toString()); 51 | }); 52 | it("Adds funder to array of funders", async function () { 53 | await fundMe.fund({ value: sendValue }); 54 | const funder = await fundMe.getFunder(0); 55 | assert.equal(funder, deployer.address); 56 | }); 57 | }); 58 | 59 | describe("withdraw", async function () { 60 | beforeEach(async function () { 61 | await fundMe.fund({ value: sendValue }); 62 | }); 63 | 64 | it("withdraws ETH from a single funder", async function () { 65 | // Arange 66 | const startingFundMeBalance = await ethers.provider.getBalance( 67 | await fundMe.getAddress() 68 | ); 69 | const startingDeployerBalance = await ethers.provider.getBalance( 70 | deployer.address 71 | ); 72 | // Act 73 | const transactionResponse = await fundMe.withdraw(); 74 | const transactionReciept = await transactionResponse.wait(1); 75 | const gasCost = 76 | transactionReciept.gasUsed * transactionReciept.gasPrice; 77 | 78 | const endingFundMeBalance = await ethers.provider.getBalance( 79 | await fundMe.getAddress() 80 | ); 81 | const endingDeployerBalance = await ethers.provider.getBalance( 82 | deployer.address 83 | ); 84 | // // Assert 85 | assert.equal(endingFundMeBalance, 0); 86 | assert.equal( 87 | startingFundMeBalance + startingDeployerBalance, 88 | endingDeployerBalance + gasCost 89 | ); 90 | }); 91 | 92 | it("allows us to withdraw with multiple funders", async function () { 93 | // Arrange 94 | const accounts = await ethers.getSigners(); 95 | for (let i = 1; i < accounts.length; i++) { 96 | const fundMeConnectedContract = await fundMe.connect(accounts[i]); 97 | await fundMeConnectedContract.fund({ value: sendValue }); 98 | } 99 | const startingFundMeBalance = await ethers.provider.getBalance( 100 | await fundMe.getAddress() 101 | ); 102 | const startingDeployerBalance = await ethers.provider.getBalance( 103 | deployer.address 104 | ); 105 | 106 | // Act 107 | const transactionResponse = await fundMe.withdraw(); 108 | const transactionReciept = await transactionResponse.wait(1); 109 | const gasCost = 110 | transactionReciept.gasUsed * transactionReciept.gasPrice; 111 | const endingFundMeBalance = await ethers.provider.getBalance( 112 | await fundMe.getAddress() 113 | ); 114 | const endingDeployerBalance = await ethers.provider.getBalance( 115 | deployer.address 116 | ); 117 | 118 | // Assert 119 | assert.equal(endingFundMeBalance, 0); 120 | assert.equal( 121 | startingFundMeBalance + startingDeployerBalance, 122 | endingDeployerBalance + gasCost 123 | ); 124 | 125 | // Make sure funders array is reset properly 126 | await expect(fundMe.getFunder(0)).to.be.reverted; 127 | for (let i = 1; i < accounts.length; i++) { 128 | assert.equal( 129 | await fundMe.getAddressToAmountFunded(accounts[i].address), 130 | 0 131 | ); 132 | } 133 | }); 134 | 135 | it("only allows the owner to withdraw", async function () { 136 | const accounts = await ethers.getSigners(); 137 | const attacker = accounts[1]; 138 | const attackerConnectedContract = await fundMe.connect(attacker); 139 | // await attackerConnectedContract.withdraw(); 140 | await expect( 141 | attackerConnectedContract.withdraw() 142 | ).to.be.revertedWithCustomError( 143 | attackerConnectedContract, 144 | "FundMe__NotOwner" 145 | ); 146 | }); 147 | }); 148 | }); 149 | --------------------------------------------------------------------------------