WEB3-Day6—Solidity基础[part5]

WEB3-Day6—Solidity基础[part5]

继续完善fundMe合约 的 fund() 和 withDraw()

转换ETH为USD

如果我们的最小交易值需要用美元为单位,我们该如何和msg.value进行比较?

新建一个变量:

uint256 public  minimumUsd = 5 * 1e18;  // 补全精度

新建两个函数:

// Function to get the price of Ethereum in USD
function getPrice() public {}
// Function to convert a value based on the price
function getConversionRate() public {}
  • getPrice():获取真实世界中以太坊当前的美元市场价格
  • getConversionRate():根据当前输入价格进行计算和转换

去中心化预言机

以太坊等资产的美元价格无法仅通过区块链技术获取,而是由金融市场决定。为了获取正确的价格信息,必须在链下数据与链上数据之间建立连接,这一需求由去中心化预言机网络实现。

区块链存在这一局限性,是因为其确定性本质——它确保所有节点达成唯一共识。若尝试将外部数据引入区块链,将破坏这种共识,导致所谓的智能合约连接性问题预言机问题

若想让智能合约有效替代传统协议,它们必须具备与现实世界数据交互的能力。

依赖中心化预言机传输数据是不足的,因为这会重新引入潜在的单点故障风险。中心化数据源会削弱区块链功能所必需的信任前提。因此,中心化节点无法满足外部数据或计算需求。Chainlink通过提供去中心化预言机网络,解决了这些中心化挑战。

ChainLink数据源

Chainlink Data Feed documentation 提供如何与数据源合约交互的文档

image-20250703173726505

AggregatorV3Interface:是一个数据源地址作为输入的合约。保持ETH/USD价格实时更新。

合约中的latestRoundData 函数返回最新的以太坊价格。

为了使用合约,我们需要它的地址和ABI。该地址可在Chainlink文档中的 Price Feed Contract Addresses部分找到。我们将使用ETH/USD。

首先导入该合约,可以通过github地址导入

import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

在chainLink找到ETH/USD地址,调用latestRoundData函数获取answer,不用的参数可以用,相隔,因为返回的answer是int类型需要转换uint

完善getPrice() :

function getPrice() public  view returns(uint256) {
        AggregatorV3Interface addressToBeFetched = AggregatorV3Interface(0x694AA1769357215DE4FAC081bf1f309aDC325306);
        (, int256 answer,,,) = addressToBeFetched.latestRoundData(); // ( ,,,, , )  取出地址,值,错误码等
        return uint256(answer * 1e18);
    }

msg.value 是一个具有 18 位小数精度的 uint256 值。

answer 是一个具有 8 位小数精度的 int256 值(基于 USD 的交易对使用 8 位小数,而基于 ETH 的交易对使用 18 位小数)。

这意味着从我们的latestRoundData函数返回的价格与msg.value不直接兼容。为了匹配小数位数,我们将价格乘以1e10.

⚠️ 始终先乘后除保持精度避免截断错误。

完善getConversionRate():

function getConversionRate(uint256 ethAmmout) public view returns(uint256) {
      uint256 ethPrice = getPrice();
      return (ethPrice*ethAmmout)/1e18;
  }

uint256 ethAmountInUsd = (ethPrice * ethAmount)

得到的结果精度为 1e18 * 1e18 = 1e36。为了将 ethAmountInUsd 的精度恢复到 1e18,我们需要将结果除以 1e18。

更改fund():

require(getConversionRate(msg.value) >= MINIMUM_USD, "You need to spend more ETH!");

部署合约,调用getPrice函数获取当前以太坊价格。还可以向该合约发送资金,如果ETH金额低于5美元就一会报错。

完整代码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

contract FundMe{
    uint256 public  minimumUsd = 5 * 1e18; // 补全精度
    
    function fund() public payable {
        require(getConversionRate(msg.value) > minimumUsd, "Didn't send enough ETH"); //if the condition is false, revert with the error message    }
    }

    function withDraw() public {

    }

    // 获取当前的eth对应实际世界的美元价格
    function getPrice() public view returns(uint256) {
        AggregatorV3Interface addressToBeFetched = AggregatorV3Interface(0x1b44F3514812d835EB1BDB0acB33d3fA3351Ee43);
        (, int256 answer,,,) = addressToBeFetched.latestRoundData(); // ( ,,,, , )  取出地址,值,错误码等
        return uint256(answer * 1e18);
    }

    function getConversionRate(uint256 ethAmmout) public view returns(uint256) {
        uint256 ethPrice = getPrice();
        return (ethPrice*ethAmmout)/1e18;
    }

}

追踪资金

为了追踪向合约发送资金的地址,我们可以创建一个名为funders的地址array记录:

address[] public funders;

fund()函数中添加以下逻辑,每当有人向合约发送钱时,我们将使用push函数将他们的地址添加到数组中:

funders.push(msg.sender);

库[Libraries]

当某个功能具有通用性的时候,我们可以创建一个library库来高效管理重复代码。

getPrice()函数和getConversionRate()这些方法可以被任何使用价格预言机的合约多次复用。

在Solidity示例网站 Solidity by example上可以找到优秀的库示例。Solidity的库与合约类似,但不允许声明任何状态变量,也不能接收ETH。

📢 库中所有的函数都必须声明为internal,必须独立部署以后再链接到主合约。

创建新文件PriceConverter.sol,将两个函数的逻辑剪切到这边,注意声明internal

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

library PriceConverter {
    function getPrice() internal view returns (uint256) {
        AggregatorV3Interface priceFeed = AggregatorV3Interface(0x694AA1769357215DE4FAC081bf1f309aDC325306);
        (, int256 answer, , , ) = priceFeed.latestRoundData();
        return uint256(answer * 10000000000);
    }

    function getConversionRate(uint256 ethAmount) internal view returns (uint256) {
        uint256 ethPrice = getPrice();
        uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1000000000000000000;
        return ethAmountInUsd;
    }
}

主合约FundMe修改为:

  • import库
  • using PriceConvertor for uint256 :表示所有uint256类型可以调用该library的函数
  • msg.value.getConversionRate() 直接调用
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import {PriceConvertor} from "./PriceConvertor.sol";

contract FundMe{

    // 表示所有uint256类型可以调用该library的函数
    using PriceConvertor for uint256;

    address[] public funders; 

    mapping(address funderAddress => uint256 funderAmount) public funderAmountMap;

    uint256 public  minimumUsd = 5 * 1e18;
    
    function fund() public payable {
        require(msg.value.getConversionRate() > minimumUsd, "Didn't send enough ETH"); //if the condition is false, revert with the error message    }
    
    		//记录追踪资金
        funders.push(msg.sender);
        funderAmountMap[msg.sender] += msg.value;
    }

    function withDraw() public {

    }

}

solidity 0.8后变更

safeMath 作为0.8版本之前广泛被使用的库,曾是智能合约中的标配功能,为什么如今不再使用了呢?

可以调整到编译器版本为0.6.0 ,新建一个合约SafeMathTester

// SafeMathTester.sol
pragma solidity ^0.6.0;

contract SafeMathTester {
    uint8 public bigNumber = 255;

    function add() public {
        bigNumber = bigNumber + 1;
    }
}

编译调用该函数,发现bigNumber值会被重置为0,这是因为0.8版本之前,有符号或者无符号的整数都是未检查的,意味着如果他们超过变量类型能够容纳的最大值,就会重置为下限值。

safeMath会提供一个机制,达到最大限制时回滚交易,避免错误计算和漏洞。

function add(uint a, uint b) public pure returns (uint) {
    uint c = a + b;
    require(c >= a, "SafeMath: addition overflow");
    return c;
}

0.8.0以后,solidity已经实现自动化溢出下溢检查,在新版本部署SafeMathTester 将会报错而不是重置为0;

同时,也引入unchecked结构来提高代码的gas效率:

uint8 public bigNumber = 255;

function add() public {
    unchecked {
        bigNumber = bigNumber + 1;
    }
}

被unchecked包裹的代码块将忽略溢出和下溢检查,如果超出限制,就会重置到0。

从合约发送ETH

接下来进行资金的提取

重置资金

募集资金了以后,合约中已经存储了所有募集的资金,所以在资金提取这一步,我们可以先清空追踪资金的数组和集合,这里需要用到循环:

首先将map置空,再将数组置空

function withDraw() public {

    //  Reset the funder records
    for (uint256 funderIndex = 0; funderIndex < funders.length; funderIndex ++) {
        address funder = funders[funderIndex];
        funderAmountMap[funder] = 0;
    }
    // reset the array
    funders = new address[](0);
}

构造函数

目前,任何人都可以调用提款函数并将所有资金从合约中提走。为了解决这个问题,我们需要将提款函数限制为仅合约所有者可以调用。

解决办法就是构造函数:

constructor() {}

构造函数会在合约部署期间自动调用,与部署合约的交易在同一笔交易中执行。

在函数里,我们将状态变量所有者初始化为合约部署者的地址(msg.sender)。

address public owner;
constructor() {
    owner = msg.sender;
}

更新withDraw函数:

function withdraw() public {
    require(msg.sender == owner, "must be owner");
    // rest of the function here
}

限制只有合约拥有者才可以进行提取。

modify优化

Solidity 中的修饰器(Modifier)是一种强大的合约元编程工具,用于在执行函数前 / 后注入额外逻辑(如权限检查、状态验证等)。合理使用修饰器不仅能提高代码复用性,还能显著优化合约性能与安全性。

上述的权限检查可以单独拆出来作为modify函数

modifier onlyOwner {
        require(msg.sender == owner,"Must be the Owner!");
        _;
    }

原函数添加onlyOwner

function withdraw() public onlyOwner {
   
}

转账

Transfer

是以太币转帐至接收地址的最简单方式

payable(msg.sender).transfer(amount);
// the current contract sends the Ether amount to the msg.sender

然而,transfer有一个显著的限制。它只能使用最多2300个gas,并且它会回滚任何超出这个限制的交易,正如《Solidity实例教程》 Solidity by Example所示。

🧐 为什么需要payable关键字?

必须将接收者地址转换为可支付地址,以便它能够接收以太币。可以通过将msg.sender用可payable关键字包裹来实现。

Send

bool success = payable(msg.sender).send(address(this).balance);
require(success, "Send failed");

与transfer类似,send也有2300的gas限制。如果达到gas限制,它不会撤销交易,但会返回一个布尔值(true或false)以指示交易的成功或失败。处理失败是开发者的责任,如果send返回false,触发撤销条件是一种良好的实践。

Call

call函数非常灵活和强大。它可以用来调用任何函数,而不需要它的ABI。它没有gas限制,并且像send一样,它返回一个布尔值,而不是像transfer那样回滚。

(bool success, ) = payable(msg.sender).call{value: address(this).balance}("");
require(success, "Call failed");

call函数返回两个变量(bool success, ):一个表示成功或失败的布尔值,以及一个存储返回数据的字节对象(如果有)。

❗call是发送和接收以太坊或其他区块链原生代币的推荐方式。

完整代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import {PriceConvertor} from "./PriceConvertor.sol";

contract FundMe{

    address public owner;

    constructor () {
        owner = msg.sender;
    }

    // 表示所有uint256类型可以调用该library的函数
    using PriceConvertor for uint256;

    address[] public funders; 

    mapping(address funderAddress => uint256 funderAmount) public funderAmountMap;

    uint256 public  minimumUsd = 5 * 1e18;
    
    function fund() public payable {
        require(msg.value.getConversionRate() > minimumUsd, "Didn't send enough ETH"); //if the condition is false, revert with the error message    }
    
        funders.push(msg.sender);
        funderAmountMap[msg.sender] += msg.value;
    }

    function withDraw() public onlyOwner{

        //  Reset the funder records
        for (uint256 funderIndex = 0; funderIndex < funders.length; funderIndex ++) {
            address funder = funders[funderIndex];
            funderAmountMap[funder] = 0;
        }
        // reset the array
        funders = new address[](0);

        // send money to msg.sender[three ways]
        // transfer
            // payable(msg.sender).transfer(amount); // the current contract sends the Ether amount to the msg.sender
        // send
            // bool success = payable(msg.sender).send(address(this).balance); require(success, "Send failed");
        // call
        (bool success, ) = payable(msg.sender).call{value: address(this).balance}("");
        require(success, "Call failed");
    }

    // 任何由onlyOwner修饰都会先执行这部分代码,“_;”代表被修饰的函数的代码,放在require之前就是先执行完函数代码再执行modifier代码块中内容
    modifier onlyOwner {
        require(msg.sender == owner,"Must be the Owner!");
        _;
    }
}