add Fund Me Foundry Smart Contract

This commit is contained in:
han 2024-12-18 18:15:59 +07:00
parent e87bce6d80
commit 517ff0c9c9
18 changed files with 617 additions and 57 deletions

6
.gitmodules vendored
View File

@ -7,3 +7,9 @@
[submodule "fund-me/lib/forge-std"] [submodule "fund-me/lib/forge-std"]
path = fund-me/lib/forge-std path = fund-me/lib/forge-std
url = https://github.com/foundry-rs/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

11
fund-me/.gas-snapshot Normal file
View File

@ -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)

3
fund-me/.gitignore vendored
View File

@ -12,3 +12,6 @@ docs/
# Dotenv file # Dotenv file
.env .env
broadcast/
lib/

64
fund-me/Makefile Normal file
View File

@ -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 <ADDRESS>`
SENDER_ADDRESS := <sender's 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)

View File

@ -2,5 +2,7 @@
src = "src" src = "src"
out = "out" out = "out"
libs = ["lib"] 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 # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

@ -0,0 +1 @@
Subproject commit 5cb41fbc9b525338b6098da5ea7dd0b7e92f89e4

@ -0,0 +1 @@
Subproject commit 47393d0a85ad9f6aa127ba2aed2bf9a7a7488bcf

View File

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

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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++;
}
}

123
fund-me/src/FundMe.sol Normal file
View File

@ -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

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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";
}
}

View File

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