Solidity入门实战—web3

项目介绍

在这个项目中,我们建立一个小型智能合约应用,他允许用户向合约地址捐赠,允许合约所有者从合约中提取余额;并且还设定了捐赠的金额门槛;针对直接对地址进行的捐赠行为,我们也予以记录

源代码

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官网 上运行部署这两个合约,进行实战测试

相关推荐
人类群星闪耀时15 分钟前
区块链点燃游戏行业新未来——技术变革与实践指南
游戏·区块链
电报号dapp11910 小时前
2025年智能合约玩法创新白皮书:九大核心模块与收益模型重构Web3经济范式
人工智能·重构·web3·去中心化·区块链·智能合约
区块链蓝海11 小时前
Cables 现已正式启动积分计划 Alpha 阶段,开放早期白名单申请
人工智能·区块链
晚霞山空静18 小时前
区块链技术的应用场景和优势
区块链
倒霉男孩18 小时前
多链协议和跨链桥
区块链
阳光普照世界和平1 天前
金融行业软件介绍
人工智能·金融·区块链
Sui_Network1 天前
Crossmint 与 Walrus 合作,将协议集成至其跨链铸造 API 中
人工智能·物联网·游戏·区块链·智能合约
limit00751 天前
CesiumEarth能够本地浏览的三维倾斜模型切片(3DTiles)
chrome·低代码·arcgis·web3·旅游
OneBlock Community2 天前
了解 DeFi:去中心化金融的入门指南与未来展望
金融·去中心化·区块链
Allover#566992 天前
GZ036区块链卷一 EtherStore合约漏洞详解
区块链·智能合约