项目介绍
在这个项目中,我们建立一个小型智能合约应用,他允许用户向合约地址捐赠,允许合约所有者从合约中提取余额;并且还设定了捐赠的金额门槛;针对直接对地址进行的捐赠行为,我们也予以记录
源代码
https://github.com/PatrickAlphaC/fund-me-fcc
代码分析
PriceConverter.sol
✅ 函数 1:getPrice()
我们先创建一个价格转化器,这里我们先引入clainlink的包。然后,创建一个 getPrice()函数,他可以通过chainlink获取到真实世界的eth的价格,调用 Chainlink 的 latestRoundData() 方法,从中提取价格 answer(ETH/USD,单位通常是8位小数),代码如下:
bash
(, int256 answer, , , ) = priceFeed.latestRoundData();
为了与以太坊中的 18 位小数精度统一,这里乘以 1e10(即 10^10),因为原始数据是 8 位精度(即 1 ETH = 1234.56789000 USD),我们希望它变成 18 位精度(1234.567890000000000000 USD),代码如下:
bash
return uint256(answer * 10000000000);
✅ 函数 2:getConversionRate(uint256 ethAmount)
根据传入的 ETH 数量(单位为 Wei),返回对应的 USD 金额(同样以 18 位精度表示)。
调用上面那个函数,得到当前 ETH 的价格(单位为 USD,18 位小数)。
bash
uint256 ethPrice = getPrice();
下面一步是核心:
- ethPrice 是每 1 ETH 的价格(18 位精度)
- ethAmount 是传入的 ETH 数量(单位是 Wei,18 位精度)
- 乘积是 36 位小数
- 除以 1e18,得到结果回到 18 位精度的美元数值
bash
uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1e18;
完整代码如下:
pragma solidity ^0.8.8;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
library PriceConverter {
// We could make this public, but then we'd have to deploy it
// We can get eht price with this function
function getPrice() internal view returns (uint256) {
// Sepolia ETH / USD Address
// https://docs.chain.link/data-feeds/price-feeds/addresses#Sepolia%20Testnet
AggregatorV3Interface priceFeed = AggregatorV3Interface(
0x694AA1769357215DE4FAC081bf1f309aDC325306
);
(, int256 answer, , , ) = priceFeed.latestRoundData();
// ETH/USD rate in 18 digit
return uint256(answer * 10000000000);
// or (Both will do the same thing)
// return uint256(answer * 1e10); // 1* 10 ** 10 == 10000000000
}
// 1000000000
function getConversionRate(uint256 ethAmount)
internal
view
returns (uint256)
{
uint256 ethPrice = getPrice();
uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1000000000000000000;
// or (Both will do the same thing)
// uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1e18; // 1 * 10 ** 18 == 1000000000000000000
// the actual ETH/USD conversion rate, after adjusting the extra 0s.
return ethAmountInUsd;
}
}
FundMe.sol
🧱 状态变量
bash
mapping(address => uint256) public addressToAmountFunded;
address[] public funders;
address public /* immutable */ i_owner;
uint256 public constant MINIMUM_USD = 50 * 10 ** 18;
addressToAmountFunded:记录每个地址捐了多少 ETH。
-
funders:所有捐赠者的地址列表。
-
i_owner:部署合约的地址,是这个合约的管理员。 建议加上 immutable 关键字提高效率(只能在构造函数中赋值)。
-
MINIMUM_USD:捐款的最小限制,50 美元(以 18 位精度计算,单位 Wei)。
🚀 构造函数
bash
constructor() {
i_owner = msg.sender;
}
部署合约的人自动成为合约的拥有者。
💸 捐款函数 fund()
bash
function fund() public payable {
require(msg.value.getConversionRate() >= MINIMUM_USD, "You need to spend more ETH!");
addressToAmountFunded[msg.sender] += msg.value;
funders.push(msg.sender);
}
- msg.value 是用户发送的 ETH 数量(单位为 Wei)
- getConversionRate() 是库函数,转化为对应美元价值
- 如果小于 MINIMUM_USD(即 50 美元),就 require 失败
- 记录捐款金额,添加到地址映射中,追加到 funders 数组
🛡️ 只有管理员可调用的 modifier
bash
modifier onlyOwner {
if (msg.sender != i_owner) revert NotOwner();
_;
}
- 限制某些函数(比如 withdraw)只能由 i_owner 调用。
- 使用 revert NotOwner() 自定义错误节省 gas。
- _ 的含义是这个类修饰的函数接下来的代码逻辑
🏧 取款函数 withdraw()
bash
function withdraw() public onlyOwner {
for (uint256 funderIndex=0; funderIndex < funders.length; funderIndex++){
address funder = funders[funderIndex];
addressToAmountFunded[funder] = 0;
}
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");
}
- 清空每个捐赠者在 mapping 里的记录
- 重置 funders 数组
- 将所有合约余额通过 .call{value:...}() 的方式发送给合约拥有者
- 使用 call 而不是 transfer 或 send 是最佳实践(更灵活,处理 gas 限制更好)。
📥 fallback / receive 函数
bash
fallback() external payable { fund(); }
receive() external payable { fund(); }
- receive():处理空 msg.data 的 ETH 转账
- fallback():处理非空 msg.data 的 ETH 转账
- 它们都调用 fund(),意味着无论是直接转账还是调用错误函数名,都能正常执行捐赠逻辑
完整代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "./PriceConverter.sol";
error NotOwner();
contract FundMe {
using PriceConverter for uint256;
mapping(address => uint256) public addressToAmountFunded;
address[] public funders;
// Could we make this constant? /* hint: no! We should make it immutable! */
address public /* immutable */ i_owner;
uint256 public constant MINIMUM_USD = 50 * 10 ** 18;
constructor() {
i_owner = msg.sender;
}
function fund() public payable {
require(msg.value.getConversionRate() >= MINIMUM_USD, "You need to spend more ETH!");
// require(PriceConverter.getConversionRate(msg.value) >= MINIMUM_USD, "You need to spend more ETH!");
addressToAmountFunded[msg.sender] += msg.value;
funders.push(msg.sender);
}
function getVersion() public view returns (uint256){
// ETH/USD price feed address of Sepolia Network.
AggregatorV3Interface priceFeed = AggregatorV3Interface(0x694AA1769357215DE4FAC081bf1f309aDC325306);
return priceFeed.version();
}
modifier onlyOwner {
// require(msg.sender == owner);
if (msg.sender != i_owner) revert NotOwner();
_;
}
function withdraw() public onlyOwner {
for (uint256 funderIndex=0; funderIndex < funders.length; funderIndex++){
address funder = funders[funderIndex];
addressToAmountFunded[funder] = 0;
}
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");
}
// 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();
}
}
大家可以在 remix官网 上运行部署这两个合约,进行实战测试