WEB3-Day6—Solidity基础[part5]

WEB3-Day6—Solidity基础[part5]
SoniaChenWEB3-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 提供如何与数据源合约交互的文档

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!");
_;
}
}