add Fund Me Foundry Smart Contract
This commit is contained in:
parent
e87bce6d80
commit
517ff0c9c9
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -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
|
||||
|
||||
11
fund-me/.gas-snapshot
Normal file
11
fund-me/.gas-snapshot
Normal 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
3
fund-me/.gitignore
vendored
@ -12,3 +12,6 @@ docs/
|
||||
|
||||
# Dotenv file
|
||||
.env
|
||||
|
||||
broadcast/
|
||||
lib/
|
||||
|
||||
64
fund-me/Makefile
Normal file
64
fund-me/Makefile
Normal 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)
|
||||
@ -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
|
||||
|
||||
1
fund-me/lib/chainlink-brownie-contracts
Submodule
1
fund-me/lib/chainlink-brownie-contracts
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 5cb41fbc9b525338b6098da5ea7dd0b7e92f89e4
|
||||
1
fund-me/lib/foundry-devops
Submodule
1
fund-me/lib/foundry-devops
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 47393d0a85ad9f6aa127ba2aed2bf9a7a7488bcf
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
23
fund-me/script/DeployFundMe.s.sol
Normal file
23
fund-me/script/DeployFundMe.s.sol
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
48
fund-me/script/HelperConfig.s.sol
Normal file
48
fund-me/script/HelperConfig.s.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
47
fund-me/script/Interactions.s.sol
Normal file
47
fund-me/script/Interactions.s.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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
123
fund-me/src/FundMe.sol
Normal 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
|
||||
28
fund-me/src/PriceConverter.sol
Normal file
28
fund-me/src/PriceConverter.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
34
fund-me/test/integration/InteractionsTest.t.sol
Normal file
34
fund-me/test/integration/InteractionsTest.t.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
74
fund-me/test/mocks/MockV3Aggregator.sol
Normal file
74
fund-me/test/mocks/MockV3Aggregator.sol
Normal 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";
|
||||
}
|
||||
}
|
||||
152
fund-me/test/unit/FundMeTest.t.sol
Normal file
152
fund-me/test/unit/FundMeTest.t.sol
Normal 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()));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user