diff --git a/.gitmodules b/.gitmodules
index 3a24f0d..65da54f 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -7,3 +7,9 @@
[submodule "fund-me/lib/forge-std"]
path = fund-me/lib/forge-std
url = https://github.com/foundry-rs/forge-std
+[submodule "fund-me/lib/chainlink-brownie-contracts"]
+ path = fund-me/lib/chainlink-brownie-contracts
+ url = https://github.com/smartcontractkit/chainlink-brownie-contracts
+[submodule "fund-me/lib/foundry-devops"]
+ path = fund-me/lib/foundry-devops
+ url = https://github.com/Cyfrin/foundry-devops
diff --git a/fund-me/.gas-snapshot b/fund-me/.gas-snapshot
new file mode 100644
index 0000000..7c31ad9
--- /dev/null
+++ b/fund-me/.gas-snapshot
@@ -0,0 +1,11 @@
+FundMeTest:testAddsFunderToArrayOfFunders() (gas: 100163)
+FundMeTest:testFundFailsWithoutEnoughETH() (gas: 23040)
+FundMeTest:testFundUpdatesFundedDataStructure() (gas: 99804)
+FundMeTest:testMinimumDollarIsFive() (gas: 8445)
+FundMeTest:testOnlyOwnerCanWithdraw() (gas: 102152)
+FundMeTest:testOwnerIsMsgSender() (gas: 10638)
+FundMeTest:testPriceFeedVersionIsAccurate() (gas: 13665)
+FundMeTest:testPrintStorageData() (gas: 17083)
+FundMeTest:testWithdrawFromASingleFunder() (gas: 87210)
+FundMeTest:testWithdrawFromMultipleFunders() (gas: 445744)
+FundMeTest:testWithdrawFromMultipleFundersCheaper() (gas: 445064)
\ No newline at end of file
diff --git a/fund-me/.gitignore b/fund-me/.gitignore
index 85198aa..3b3c473 100644
--- a/fund-me/.gitignore
+++ b/fund-me/.gitignore
@@ -12,3 +12,6 @@ docs/
# Dotenv file
.env
+
+broadcast/
+lib/
diff --git a/fund-me/Makefile b/fund-me/Makefile
new file mode 100644
index 0000000..55b7100
--- /dev/null
+++ b/fund-me/Makefile
@@ -0,0 +1,64 @@
+-include .env
+
+.PHONY: all test clean deploy fund help install snapshot format anvil zktest
+
+DEFAULT_ANVIL_KEY := 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
+DEFAULT_ZKSYNC_LOCAL_KEY := 0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110
+
+all: clean remove install update build
+
+# Clean the repo
+clean :; forge clean
+
+# Remove modules
+remove :; rm -rf .gitmodules && rm -rf .git/modules/* && rm -rf lib && touch .gitmodules && git add . && git commit -m "modules"
+
+install :; forge install cyfrin/foundry-devops@0.2.2 --no-commit && forge install smartcontractkit/chainlink-brownie-contracts@1.1.1 --no-commit && forge install foundry-rs/forge-std@v1.8.2 --no-commit
+
+# Update Dependencies
+update:; forge update
+
+build:; forge build
+
+zkbuild :; forge build --zksync
+
+test :; forge test
+
+zktest :; foundryup-zksync && forge test --zksync && foundryup
+
+snapshot :; forge snapshot
+
+format :; forge fmt
+
+anvil :; anvil -m 'test test test test test test test test test test test junk' --steps-tracing --block-time 1
+
+zk-anvil :; npx zksync-cli dev start
+
+deploy:
+ @forge script script/DeployFundMe.s.sol:DeployFundMe $(NETWORK_ARGS)
+
+NETWORK_ARGS := --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast
+
+ifeq ($(findstring --network sepolia,$(ARGS)),--network sepolia)
+ NETWORK_ARGS := --rpc-url $(SEPOLIA_RPC_URL) --account $(ACCOUNT) --broadcast --verify --etherscan-api-key $(ETHERSCAN_API_KEY) -vvvv
+endif
+
+deploy-sepolia:
+ @forge script script/DeployFundMe.s.sol:DeployFundMe $(NETWORK_ARGS)
+
+# As of writing, the Alchemy zkSync RPC URL is not working correctly
+deploy-zk:
+ forge create src/FundMe.sol:FundMe --rpc-url http://127.0.0.1:8011 --private-key $(DEFAULT_ZKSYNC_LOCAL_KEY) --constructor-args $(shell forge create test/mock/MockV3Aggregator.sol:MockV3Aggregator --rpc-url http://127.0.0.1:8011 --private-key $(DEFAULT_ZKSYNC_LOCAL_KEY) --constructor-args 8 200000000000 --legacy --zksync | grep "Deployed to:" | awk '{print $$3}') --legacy --zksync
+
+deploy-zk-sepolia:
+ forge create src/FundMe.sol:FundMe --rpc-url ${ZKSYNC_SEPOLIA_RPC_URL} --account default --constructor-args 0xfEefF7c3fB57d18C5C6Cdd71e45D2D0b4F9377bF --legacy --zksync
+
+
+# For deploying Interactions.s.sol:FundFundMe as well as for Interactions.s.sol:WithdrawFundMe we have to include a sender's address `--sender
`
+SENDER_ADDRESS :=
+
+fund:
+ @forge script script/Interactions.s.sol:FundFundMe --sender $(SENDER_ADDRESS) $(NETWORK_ARGS)
+
+withdraw:
+ @forge script script/Interactions.s.sol:WithdrawFundMe --sender $(SENDER_ADDRESS) $(NETWORK_ARGS)
diff --git a/fund-me/foundry.toml b/fund-me/foundry.toml
index 25b918f..b90fa82 100644
--- a/fund-me/foundry.toml
+++ b/fund-me/foundry.toml
@@ -2,5 +2,7 @@
src = "src"
out = "out"
libs = ["lib"]
+remappings = ["@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/"]
+ffi = true
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
diff --git a/fund-me/lib/chainlink-brownie-contracts b/fund-me/lib/chainlink-brownie-contracts
new file mode 160000
index 0000000..5cb41fb
--- /dev/null
+++ b/fund-me/lib/chainlink-brownie-contracts
@@ -0,0 +1 @@
+Subproject commit 5cb41fbc9b525338b6098da5ea7dd0b7e92f89e4
diff --git a/fund-me/lib/foundry-devops b/fund-me/lib/foundry-devops
new file mode 160000
index 0000000..47393d0
--- /dev/null
+++ b/fund-me/lib/foundry-devops
@@ -0,0 +1 @@
+Subproject commit 47393d0a85ad9f6aa127ba2aed2bf9a7a7488bcf
diff --git a/fund-me/script/Counter.s.sol b/fund-me/script/Counter.s.sol
deleted file mode 100644
index cdc1fe9..0000000
--- a/fund-me/script/Counter.s.sol
+++ /dev/null
@@ -1,19 +0,0 @@
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.13;
-
-import {Script, console} from "forge-std/Script.sol";
-import {Counter} from "../src/Counter.sol";
-
-contract CounterScript is Script {
- Counter public counter;
-
- function setUp() public {}
-
- function run() public {
- vm.startBroadcast();
-
- counter = new Counter();
-
- vm.stopBroadcast();
- }
-}
diff --git a/fund-me/script/DeployFundMe.s.sol b/fund-me/script/DeployFundMe.s.sol
new file mode 100644
index 0000000..f8e3c6d
--- /dev/null
+++ b/fund-me/script/DeployFundMe.s.sol
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity 0.8.18;
+
+import {Script} from "forge-std/Script.sol";
+import {FundMe} from "../src/FundMe.sol";
+import {HelperConfig} from "./HelperConfig.s.sol";
+
+contract DeployFundMe is Script {
+
+ function run() external returns (FundMe) {
+ // simulation-tx
+ HelperConfig helperConfig = new HelperConfig();
+ address ethUsdPriceFeed = helperConfig.activeNetworkConfig();
+
+ // real-tx
+ vm.startBroadcast();
+ FundMe fundMe = new FundMe(ethUsdPriceFeed);
+ vm.stopBroadcast();
+ return fundMe;
+ }
+
+}
diff --git a/fund-me/script/HelperConfig.s.sol b/fund-me/script/HelperConfig.s.sol
new file mode 100644
index 0000000..86e4c7a
--- /dev/null
+++ b/fund-me/script/HelperConfig.s.sol
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity 0.8.18;
+
+import {Script} from "forge-std/Script.sol";
+import {MockV3Aggregator} from "../test/mocks/MockV3Aggregator.sol";
+
+contract HelperConfig is Script {
+
+ NetworkConfig public activeNetworkConfig;
+
+ uint8 public constant DECIMALS = 8;
+ int256 public constant INITIAL_PRICE = 2000e8;
+
+ struct NetworkConfig {
+ address priceFeed;
+ }
+
+ constructor() {
+ if (block.chainid == 11155111) {
+ activeNetworkConfig = getSepoliaEthConfig();
+ } else {
+ activeNetworkConfig = getOrCreateAnvilEthConfig();
+ }
+ }
+
+ function getSepoliaEthConfig() public pure returns (NetworkConfig memory) {
+ NetworkConfig memory sepoliaConfig = NetworkConfig({
+ priceFeed: 0x694AA1769357215DE4FAC081bf1f309aDC325306
+ });
+ return sepoliaConfig;
+ }
+
+ function getOrCreateAnvilEthConfig() public returns (NetworkConfig memory) {
+ if (activeNetworkConfig.priceFeed != address(0)) {
+ return activeNetworkConfig;
+ }
+
+ vm.startBroadcast();
+ MockV3Aggregator mockPriceFeed = new MockV3Aggregator(DECIMALS, INITIAL_PRICE);
+ vm.stopBroadcast();
+
+ NetworkConfig memory anvilConfig = NetworkConfig({
+ priceFeed: address(mockPriceFeed)
+ });
+ return anvilConfig;
+ }
+}
diff --git a/fund-me/script/Interactions.s.sol b/fund-me/script/Interactions.s.sol
new file mode 100644
index 0000000..022fbb1
--- /dev/null
+++ b/fund-me/script/Interactions.s.sol
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.18;
+
+// Fund
+
+// Withdraw
+
+import {Script, console} from "forge-std/Script.sol";
+import {DevOpsTools} from "foundry-devops/src/DevOpsTools.sol";
+import {FundMe} from "../src/FundMe.sol";
+
+contract FundFundMe is Script {
+ uint256 constant SEND_VALUE = 0.01 ether;
+
+ function fundFundMe(address mostRecentDeployed) public {
+ vm.startBroadcast();
+ FundMe(payable(mostRecentDeployed)).fund{value: SEND_VALUE}();
+ vm.stopBroadcast();
+ console.log("Funded FundMe with %s", SEND_VALUE);
+ }
+
+ function run() external {
+ address mostRecentlyDeployed = DevOpsTools.get_most_recent_deployment(
+ "FundMe",
+ block.chainid
+ );
+ fundFundMe(mostRecentlyDeployed);
+ }
+}
+
+contract WithdrawFundMe is Script {
+ function withdrawFundMe(address mostRecentDeployed) public {
+ vm.startBroadcast();
+ FundMe(payable(mostRecentDeployed)).withdraw();
+ vm.stopBroadcast();
+ console.log("Withdraw FundMe");
+ }
+
+ function run() external {
+ address mostRecentlyDeployed = DevOpsTools.get_most_recent_deployment(
+ "FundMe",
+ block.chainid
+ );
+ withdrawFundMe(mostRecentlyDeployed);
+ }
+}
diff --git a/fund-me/src/Counter.sol b/fund-me/src/Counter.sol
deleted file mode 100644
index aded799..0000000
--- a/fund-me/src/Counter.sol
+++ /dev/null
@@ -1,14 +0,0 @@
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.13;
-
-contract Counter {
- uint256 public number;
-
- function setNumber(uint256 newNumber) public {
- number = newNumber;
- }
-
- function increment() public {
- number++;
- }
-}
diff --git a/fund-me/src/FundMe.sol b/fund-me/src/FundMe.sol
new file mode 100644
index 0000000..64252a2
--- /dev/null
+++ b/fund-me/src/FundMe.sol
@@ -0,0 +1,123 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.18;
+
+// Note: The AggregatorV3Interface might be at a different location than what was in the video!
+import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
+import {PriceConverter} from "./PriceConverter.sol";
+
+error NotOwner();
+
+contract FundMe {
+ using PriceConverter for uint256;
+
+ // private is more gas efficient compared to public
+ mapping(address => uint256) private s_addressToAmountFunded;
+ address[] private s_funders;
+
+ // Could we make this constant? /* hint: no! We should make it immutable! */
+ address private /* immutable */ i_owner;
+ uint256 public constant MINIMUM_USD = 5 * 10 ** 18;
+ AggregatorV3Interface private s_priceFeed;
+
+ constructor(address priceFeed) {
+ i_owner = msg.sender;
+ s_priceFeed = AggregatorV3Interface(priceFeed);
+ }
+
+ function fund() public payable {
+ require(msg.value.getConversionRate(s_priceFeed) >= MINIMUM_USD, "You need to spend more ETH!");
+ // require(PriceConverter.getConversionRate(msg.value) >= MINIMUM_USD, "You need to spend more ETH!");
+ s_addressToAmountFunded[msg.sender] += msg.value;
+ s_funders.push(msg.sender);
+ }
+
+ function getVersion() public view returns (uint256) {
+ return s_priceFeed.version();
+ }
+
+ modifier onlyOwner() {
+ // require(msg.sender == owner);
+ if (msg.sender != i_owner) revert NotOwner();
+ _;
+ }
+
+ function withdraw() public onlyOwner {
+ for (uint256 funderIndex = 0; funderIndex < s_funders.length; funderIndex++) {
+ address funder = s_funders[funderIndex];
+ s_addressToAmountFunded[funder] = 0;
+ }
+ s_funders = new address[](0);
+ // // transfer
+ // payable(msg.sender).transfer(address(this).balance);
+
+ // // send
+ // bool sendSuccess = payable(msg.sender).send(address(this).balance);
+ // require(sendSuccess, "Send failed");
+
+ // call
+ (bool callSuccess,) = payable(msg.sender).call{value: address(this).balance}("");
+ require(callSuccess, "Call failed");
+ }
+
+ function cheaperWithdraw() public onlyOwner {
+ uint256 fundersLength = s_funders.length;
+ for (uint256 funderIndex = 0; funderIndex < fundersLength; funderIndex++) {
+ address funder = s_funders[funderIndex];
+ s_addressToAmountFunded[funder] = 0;
+ }
+ s_funders = new address[](0);
+
+ (bool callSuccess,) = payable(msg.sender).call{value: address(this).balance}("");
+ require(callSuccess, "Call failed");
+ }
+
+ // Explainer from: https://solidity-by-example.org/fallback/
+ // Ether is sent to contract
+ // is msg.data empty?
+ // / \
+ // yes no
+ // / \
+ // receive()? fallback()
+ // / \
+ // yes no
+ // / \
+ //receive() fallback()
+
+ fallback() external payable {
+ fund();
+ }
+
+ receive() external payable {
+ fund();
+ }
+
+ /**
+ * View / Pure Functions (Getters)
+ */
+ function getAddressToAmountFunded(
+ address fundingAddress
+ ) external view returns (uint256) {
+ return s_addressToAmountFunded[fundingAddress];
+ }
+
+ function getFunder(uint256 funderIndex) external view returns (address) {
+ return s_funders[funderIndex];
+ }
+
+ function getOwner() external view returns (address) {
+ return i_owner;
+ }
+
+ function getPriceFeed() external view returns (AggregatorV3Interface) {
+ return s_priceFeed;
+ }
+}
+
+// Concepts we didn't cover yet (will cover in later sections)
+// 1. Enum
+// 2. Events
+// 3. Try / Catch
+// 4. Function Selector
+// 5. abi.encode / decode
+// 6. Hash with keccak256
+// 7. Yul / Assembly
diff --git a/fund-me/src/PriceConverter.sol b/fund-me/src/PriceConverter.sol
new file mode 100644
index 0000000..e3264ea
--- /dev/null
+++ b/fund-me/src/PriceConverter.sol
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.18;
+
+import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
+
+// Why is this a library and not abstract?
+// Why not an interface?
+library PriceConverter {
+ // We could make this public, but then we'd have to deploy it
+ function getPrice(AggregatorV3Interface priceFeed) internal view returns (uint256) {
+ // Sepolia ETH / USD Address
+ // https://docs.chain.link/data-feeds/price-feeds/addresses
+ (, int256 answer, , , ) = priceFeed.latestRoundData();
+ // ETH/USD rate in 18 digit
+ return uint256(answer * 10000000000);
+ }
+
+ // 1000000000
+ function getConversionRate(
+ uint256 ethAmount,
+ AggregatorV3Interface priceFeed
+ ) internal view returns (uint256) {
+ uint256 ethPrice = getPrice(priceFeed);
+ uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1000000000000000000;
+ // the actual ETH/USD conversion rate, after adjusting the extra 0s.
+ return ethAmountInUsd;
+ }
+}
diff --git a/fund-me/test/Counter.t.sol b/fund-me/test/Counter.t.sol
deleted file mode 100644
index 54b724f..0000000
--- a/fund-me/test/Counter.t.sol
+++ /dev/null
@@ -1,24 +0,0 @@
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.13;
-
-import {Test, console} from "forge-std/Test.sol";
-import {Counter} from "../src/Counter.sol";
-
-contract CounterTest is Test {
- Counter public counter;
-
- function setUp() public {
- counter = new Counter();
- counter.setNumber(0);
- }
-
- function test_Increment() public {
- counter.increment();
- assertEq(counter.number(), 1);
- }
-
- function testFuzz_SetNumber(uint256 x) public {
- counter.setNumber(x);
- assertEq(counter.number(), x);
- }
-}
diff --git a/fund-me/test/integration/InteractionsTest.t.sol b/fund-me/test/integration/InteractionsTest.t.sol
new file mode 100644
index 0000000..27b76a7
--- /dev/null
+++ b/fund-me/test/integration/InteractionsTest.t.sol
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.18;
+
+import {console} from "forge-std/console.sol";
+import {Test} from "forge-std/Test.sol";
+import {FundMe} from "../../src/FundMe.sol";
+import {DeployFundMe} from "../../script/DeployFundMe.s.sol";
+import {FundFundMe, WithdrawFundMe} from "../../script/Interactions.s.sol";
+
+contract InteractionsTest is Test {
+ FundMe fundMe;
+
+ address USER = makeAddr("user");
+ uint256 constant SEND_VALUE = 0.1 ether;
+ uint256 constant STARTING_BALANCE = 10 ether;
+ uint256 constant GAS_PRICE = 1;
+
+ function setUp() external {
+ DeployFundMe deploy = new DeployFundMe();
+ fundMe = deploy.run();
+ vm.deal(USER, STARTING_BALANCE);
+ }
+
+ function testUserCanFundInteractions() public {
+ FundFundMe fundFundMe = new FundFundMe();
+ fundFundMe.fundFundMe(address(fundMe));
+
+ WithdrawFundMe withdrawFundMe = new WithdrawFundMe();
+ withdrawFundMe.withdrawFundMe(address(fundMe));
+
+ assert(address(fundMe).balance == 0);
+ }
+}
diff --git a/fund-me/test/mocks/MockV3Aggregator.sol b/fund-me/test/mocks/MockV3Aggregator.sol
new file mode 100644
index 0000000..7dcaf82
--- /dev/null
+++ b/fund-me/test/mocks/MockV3Aggregator.sol
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
+
+/**
+ * @title MockV3Aggregator
+ * @notice Based on the FluxAggregator contract
+ * @notice Use this contract when you need to test
+ * other contract's ability to read data from an
+ * aggregator contract, but how the aggregator got
+ * its answer is unimportant
+ */
+contract MockV3Aggregator is AggregatorV3Interface {
+ uint256 public constant version = 4;
+
+ uint8 public decimals;
+ int256 public latestAnswer;
+ uint256 public latestTimestamp;
+ uint256 public latestRound;
+
+ mapping(uint256 => int256) public getAnswer;
+ mapping(uint256 => uint256) public getTimestamp;
+ mapping(uint256 => uint256) private getStartedAt;
+
+ constructor(uint8 _decimals, int256 _initialAnswer) {
+ decimals = _decimals;
+ updateAnswer(_initialAnswer);
+ }
+
+ function updateAnswer(int256 _answer) public {
+ latestAnswer = _answer;
+ latestTimestamp = block.timestamp;
+ latestRound++;
+ getAnswer[latestRound] = _answer;
+ getTimestamp[latestRound] = block.timestamp;
+ getStartedAt[latestRound] = block.timestamp;
+ }
+
+ function updateRoundData(uint80 _roundId, int256 _answer, uint256 _timestamp, uint256 _startedAt) public {
+ latestRound = _roundId;
+ latestAnswer = _answer;
+ latestTimestamp = _timestamp;
+ getAnswer[latestRound] = _answer;
+ getTimestamp[latestRound] = _timestamp;
+ getStartedAt[latestRound] = _startedAt;
+ }
+
+ function getRoundData(uint80 _roundId)
+ external
+ view
+ returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
+ {
+ return (_roundId, getAnswer[_roundId], getStartedAt[_roundId], getTimestamp[_roundId], _roundId);
+ }
+
+ function latestRoundData()
+ external
+ view
+ returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
+ {
+ return (
+ uint80(latestRound),
+ getAnswer[latestRound],
+ getStartedAt[latestRound],
+ getTimestamp[latestRound],
+ uint80(latestRound)
+ );
+ }
+
+ function description() external pure returns (string memory) {
+ return "v0.6/test/mock/MockV3Aggregator.sol";
+ }
+}
diff --git a/fund-me/test/unit/FundMeTest.t.sol b/fund-me/test/unit/FundMeTest.t.sol
new file mode 100644
index 0000000..2d79b79
--- /dev/null
+++ b/fund-me/test/unit/FundMeTest.t.sol
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.18;
+
+import {console} from "forge-std/console.sol";
+import {Test} from "forge-std/Test.sol";
+import {FundMe} from "../../src/FundMe.sol";
+import {DeployFundMe} from "../../script/DeployFundMe.s.sol";
+
+contract FundMeTest is Test {
+ FundMe fundMe;
+
+ address USER = makeAddr("user");
+ uint256 constant SEND_VALUE = 0.1 ether;
+ uint256 constant STARTING_BALANCE = 10 ether;
+ // uint256 constant GAS_PRICE = 1;
+
+ function setUp() external {
+ // fundMe = new FundMe(0x694AA1769357215DE4FAC081bf1f309aDC325306);
+ DeployFundMe deployFundMe = new DeployFundMe();
+ fundMe = deployFundMe.run();
+ vm.deal(USER, STARTING_BALANCE);
+ }
+
+ function testMinimumDollarIsFive() public view {
+ assertEq(fundMe.MINIMUM_USD(), 5e18);
+ }
+
+ function testOwnerIsMsgSender() public view {
+ assertEq(fundMe.getOwner(), msg.sender);
+ }
+
+ function testPriceFeedVersionIsAccurate() public view {
+ uint256 version = fundMe.getVersion();
+ assertEq(version, 4);
+ }
+
+ function testFundFailsWithoutEnoughETH() public {
+ vm.expectRevert();
+ fundMe.fund();
+ }
+
+ function testFundUpdatesFundedDataStructure() public {
+ vm.prank(USER); // the next tx will be sent by USER
+ fundMe.fund{value: SEND_VALUE}();
+
+ uint256 amountFunded = fundMe.getAddressToAmountFunded(USER);
+ assertEq(amountFunded, SEND_VALUE);
+ }
+
+ function testAddsFunderToArrayOfFunders() public {
+ vm.prank(USER); // the next tx will be sent by USER
+ fundMe.fund{value: SEND_VALUE}();
+
+ address funder = fundMe.getFunder(0);
+ assertEq(funder, USER);
+ }
+
+ modifier funded() {
+ vm.prank(USER); // the next tx will be sent by USER
+ fundMe.fund{value: SEND_VALUE}();
+ _ ;
+
+ }
+
+ function testOnlyOwnerCanWithdraw() public funded {
+ vm.expectRevert();
+ vm.prank(USER);
+ fundMe.withdraw();
+ }
+
+ function testWithdrawFromASingleFunder() public funded {
+ // Arrange
+ uint256 startingOwnerBalance = fundMe.getOwner().balance;
+ uint256 startingFundMeBalance = address(fundMe).balance;
+
+ // Act
+ vm.prank(fundMe.getOwner());
+ fundMe.withdraw();
+
+ // Assert
+ uint256 endingOwnerBalance = fundMe.getOwner().balance;
+ uint256 endingFundMeBalance = address(fundMe).balance;
+ assertEq(endingFundMeBalance, 0);
+ assertEq(startingFundMeBalance + startingOwnerBalance, endingOwnerBalance);
+ }
+
+ function testWithdrawFromMultipleFunders() public funded {
+ // Arrange
+ uint160 numberOfFunders = 10;
+ uint160 startingFunderIndex = 2;
+ for(uint160 i = startingFunderIndex; i < numberOfFunders; i++) {
+ hoax(address(i), SEND_VALUE);
+ fundMe.fund{value: SEND_VALUE}();
+ }
+
+ uint256 startingOwnerBalance = fundMe.getOwner().balance;
+ uint256 startingFundMeBalance = address(fundMe).balance;
+
+ // Act
+ // uint256 gasStart = gasleft();
+ // vm.txGasPrice(GAS_PRICE);
+ // vm.prank(fundMe.getOwner()); // or vm.startPrank() and vm.endPrank()
+ vm.startPrank(fundMe.getOwner());
+ fundMe.withdraw();
+ vm.stopPrank();
+
+ // uint256 gasEnd = gasleft();
+ // uint256 gasUsed = (gasStart - gasEnd) * tx.gasprice;
+
+ // Assert
+ assert(address(fundMe).balance == 0);
+ assert(startingFundMeBalance + startingOwnerBalance == fundMe.getOwner().balance);
+ }
+
+ function testWithdrawFromMultipleFundersCheaper() public funded {
+ // Arrange
+ uint160 numberOfFunders = 10;
+ uint160 startingFunderIndex = 2;
+ for(uint160 i = startingFunderIndex; i < numberOfFunders; i++) {
+ hoax(address(i), SEND_VALUE);
+ fundMe.fund{value: SEND_VALUE}();
+ }
+
+ uint256 startingOwnerBalance = fundMe.getOwner().balance;
+ uint256 startingFundMeBalance = address(fundMe).balance;
+
+ // Act
+ // uint256 gasStart = gasleft();
+ // vm.txGasPrice(GAS_PRICE);
+ // vm.prank(fundMe.getOwner()); // or vm.startPrank() and vm.endPrank()
+ vm.startPrank(fundMe.getOwner());
+ fundMe.cheaperWithdraw();
+ vm.stopPrank();
+
+ // uint256 gasEnd = gasleft();
+ // uint256 gasUsed = (gasStart - gasEnd) * tx.gasprice;
+
+ // Assert
+ assert(address(fundMe).balance == 0);
+ assert(startingFundMeBalance + startingOwnerBalance == fundMe.getOwner().balance);
+ }
+
+ function testPrintStorageData() public view {
+ for (uint256 i = 0; i < 3; i++) {
+ bytes32 value = vm.load(address(fundMe), bytes32(i));
+ console.log("Value at location i", i, ":");
+ console.logBytes32(value);
+ }
+ console.log("PriceFeed address:", address(fundMe.getPriceFeed()));
+ }
+}