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())); + } +}