区块链全栈以太坊(四)合约实战FoundMe
合约demo学习- FoundMe(众筹)
这个demo演示了 众筹以及合约所有者提现的功能,使用了chainlink获取eth/usd 价格。
学习掌握完这个demo,就学会了基本合约编写流程、 solidity基础、remix简单使用。
- 源码 https://github.com/PatrickAlphaC/fund-me-fcc
- 包含两个合约 fundme.sol, priceConverter.sol
- 合约跟钱包一样 ,可以持有资金。
一、fund (筹款 )函数实现要点
让用户向合约里面存钱。
)
1) payable
fund 声明为payable,让按钮变红,表示涉及支付功能。
2) msg.value金额
返回的币数量 value。单位是最小的wei。
如下代码可以访问某人转账的金额。
3) require
条件不满足,则操作、费用gas都被revert
当value传入0时,number=5被revert回滚了,require语句后面消耗的gas也被回滚。
当value传入2 ether时,number=5 生效,require条件满足。
4) Library
我们可以使用库,给不同的变量增加更多的功能性,如PriceConverter.sol
- 库和智能合约类似,但是你不能声明任何静态变量,也不能发送ETH。
- 一个库的所有函数都是 internal (而不是public)的。
using PriceConverter for uint256;
1.chainlink预言机
使用了 chainlink预言机[Sepolia Testnet](https://docs.chain.link/data-feeds/price-feeds/addresses?network=ethereum&page=1#sepolia-testnet)获取eth现价,把fund输入的eth转换成usd ,用以判断是否符合最低金额50美金的条件。
AggregatorV3Interface priceFeed = AggregatorV3Interface(
0x694AA1769357215DE4FAC081bf1f309aDC325306
);
a.ABI
引用solidity接口实现接口调用。
直接从github/npm引入,remix 可以自动识别@chainlink/contracts 就是指向Chainlink/contracts的npm包。
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
# 在remix上,不需要自己做以下操作。
# via yarn
yarn add @chainlink/contracts
# via npm
npm install @chainlink/contracts --save
b.Address
chainlink官网找到合约地址
https://docs.chain.link/data-feeds/price-feeds/addresses?network=ethereum&page=1
2.solidity中不支持浮点
不在solidity中计算小数,我们就不会丢失精度。
这个Library中getPrice(),getConversionRate()这些乘以10的几次方,都是为了避免浮点数计算。
// 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() internal view returns (uint256) {
// Sepolia ETH / USD Address
// https://docs.chain.link/data-feeds/price-feeds/addresses#Sepolia%20Testnet
AggregatorV3Interface priceFeed = AggregatorV3Interface(
0x694AA1769357215DE4FAC081bf1f309aDC325306
);
// solidity中不支持浮点,返回的价格 answer 是没有小数点的,类似 326281237684
// 实际应该是 3262.xx 左右,多了8个0,priceFeed.dicimals() 返回有多少位是在小数点之后的,这里返回8
//api doc
//https://docs.chain.link/data-feeds/api-reference#functions-in-aggregatorv3interface
//address doc
//https://docs.chain.link/data-feeds/price-feeds/addresses?network=ethereum&page=1
(, int256 answer, , , ) = priceFeed.latestRoundData();
// ETH/USD rate in 18 digit
// 再给10 个0 补到 18个零。
// 这样getConversionRate 中 (ethPrice * ethAmount) / 1000000000000000000; 才不会产生小数。
// 这是solidity不支持浮点数导致,所以这里的计算结果,其实放大了 10 **18 倍。
// 所以前面 FundMe.sol 中的最小金额 要 乘以10 ** 18,如下
//uint256 public constant MINIMUM_USD = 50 * 10 ** 18;
return uint256(answer * 10000000000);
// or (Both will do the same thing)
// return uint256(answer * 1e10); // 1* 10 ** 10 == 10000000000
}
//hardhat 可以更容易得测试这些数学计算
function getConversionRate(
uint256 ethAmount
) internal view returns (uint256) {
// ethPrice 大概长这样 3000_000000000000000000
uint256 ethPrice = getPrice();
//msg.value单位是最小的wei,我们要做汇率换算时,是以eth为标准,所以要除以10 **18
// 如果1eth,那ethAmount 大概长这样 1_000000000000000000
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;
}
}
5) (扩展学习)去中心化预言机chainlink
为什么需要使用预言机?
因为合约执行时,由于不同节点的执行时间,网络延迟影响等,各个节点执行合约时获取的eth价> 格是不同的,这样达不成共识,所以,需要由chainlink的pricefeed来提供统一的价格。为什么要用去中心化预言机?
因为中心化预言机将重新引入单点失败的风险。
区块链在设计上是个确定性的系统,智能合约不能连接现实世界的数据源,api等(否则就变得不确定了,因为不同节点可能会得到不同的结果,不可能达成共识)。
比如 不知道以太币价格,不知道随机数是什么,不知道是否晴天,不知道温度。
所以需要通过chainlink来与世界交互,给智能合约提供外部数据或者计算。
而不是通过http调用。
1) chainlink-pricefeed喂价服务
见官网
-
api doc https://docs.chain.link/data-feeds/api-reference#functions-in-aggregatorv3interface
-
adress doc https://docs.chain.link/data-feeds/price-feeds/addresses?network=ethereum&page=1
-
data https://data.chain.link
价格更新的两个条件:0.5%波动阈值或Heartbeat
demo 获取BTC/USDT现价
https://docs.chain.link/data-feeds/using-data-feeds open in Remix
价格位数有 问题,因为solidity中不能显示小数。
如何获取效数位数https://docs.chain.link/data-feeds/api-reference#description
2) chainlink VRF随机数
区块链是确定性系统,不具备随机性。
所以需要通过外部来获得可验证的随机数。
3) chainlink-automation 自动执行合约
以去中心化的方式,通过event触发的函数执行,定时任务/事件。
https://docs.chain.link/chainlink-automation
4) chainlink-functions可定制feed
- 可以发起http请求(见官网https://chain.link/functions)
- 需要消耗link token。
所以需要在合约中预先存入linktoken - 必须创建Chainlink网络,让网络从不同的chainlink节点/数据提供商 获取数据。
6) solidity基础
https://solidity-by-example.org/primitives/
1) 交易属性
关于交易属性的文档 , 例如 msg.value
https://learnblockchain.cn/docs/solidity/units-and-global-variables.html
2) 数组
address[] public funders;
//使用
funders.push(msg.sender);
3) 字典
mapping(address => uint256) public addressToAmountFunded;
//使用
addressToAmountFunded[msg.sender] = msg.value;
二、withdraw(提现 )函数实现要点
该函数可以让项目方从合约中提取资金。
1) 发送以太币的3个方式
如果我们想要转移资金,给调用这俄格withdraw函数的人,可以用如下3种方法。
https://solidity-by-example.org/sending-ether/
这种方法也可以用于不同合约之间互相发送代币。
1.transfer不建议使用
-
最简单、直观。
-
异常自动回滚。
-
2300gas消耗上限,超过就报错。
// // transfer
// this指的是合约本身
// address(this).balance 指这个地址(合约)的以太币余额
// msg.sender 调用这个合约的人的 address
// payable(msg.sender) 转换成 payable address,发送以太币必须使用这个类型的地址
// .transfer到底要转移多少资金
payable(msg.sender).transfer(address(this).balance);
这种方法也可以用于不同合约之间互相发送代币。
payable(/*目标地址*/).transfer(address(this).balance);
transfer 问题
2.send不建议使用
-
异常返回 false,需要自己断言 require。
-
2300gas消耗上限,超过返回false。
bool sendSuccess = payable(msg.sender).send(address(this).balance);
require(sendSuccess, "Send failed");
3.call推荐使用
-
底层函数,可以用它来调用几乎所有solidity函数,不需要ABI.
-
没有gas上限。
(bool callSuccess, ) = payable(msg.sender).call{value: address(this).balance}("");
require(callSuccess, "Call failed");
function sendViaCall(address payable _to) public payable {
// Call returns a boolean value indicating success or failure.
// This is the current recommended method to use.
(bool sent, bytes memory data) = _to.call{value: msg.value}("");
require(sent, "Failed to send Ether");
}
2) solidity基础
1.异常抛出
a.require()
- 退回剩下的 gas
- require 函数用来输入变量或合约状态变量是否满足条件。
require(msg.value.getConversionRate() >= MINIMUM_USD, "You need to spend more ETH!");
b.assert()
- 会消耗所有的 gas
- assert 函数用来检查(测试)内部错误。
- 一般地,尽量少使用 assert 调用,一般assert 应该在函数结尾处使用。
c.revert()
- 退回剩下的 gas
// 如果不等则异常
if(msg.sender != owner) { revert(); }
2.异常捕获
https://solidity-by-example.org/try-catch/
3.修饰器modifier
函数调用权限控制。
// Could we make this constant? /* hint: no! We should make it immutable! */
error NotOwner();
contract FundMe {
address public /* immutable */ i_owner;
constructor() {
i_owner = msg.sender;
}
modifier onlyOwner {
// 在调用withdraw() 之前,先调用这个修饰器里的代码。
// require(msg.sender == owner);
if (msg.sender != i_owner) revert NotOwner();
// 这个横线代表执行withdraw
_;
// require(xxx,"做一些后置操作");
}
// 用修饰器,代替每个函数中编写断言
function withdraw() public onlyOwner {
}
}
3) 优化:降低gas
1.使用constant,immutable。
2.自定义error代替 require减少字符串存储
error NotOwner();
contract FundMe {
modifier onlyOwner {
// require(msg.sender == owner);
if (msg.sender != i_owner) revert NotOwner();
_;
}
}
4) 统一入口
有时,可以直接将eth或者原生通行证发送给合约(钱包转账),而不执行某一个具体的函数来发送(fund)。
为了避免直接转账(或者调用一个不存在的函数)时不会触发fund函数的逻辑,
可以用receive、fallback来做统一入口。
1.receive
一个合约最多可以有一个 函数receive。
当你向合约发送交易的时候,如果没有指定某个函数
receive 函数就会被触发(当:calldata 没有值时)
2.fallback
合约里找不到要调用的函数时,会降级调用 fallback。
fallback 和receive谁优先主要取决于 calldata是否为空,如下图
三、部署&测试
- 部署到测试网络上(才能有 chainlink获取币价)。
- 传入20000000000000000 Wei(这样才满足大于50美元)。
- 执行 fund()。
- 重复2,3。
- 切换账号account2测试提现,验证权限。
- 切换回account1,测试提现成功。
四、常用类库
SafeMath
现在几乎已经不用这个库了
Solidity0.8版本之前
无符号整形和整形是运行在 unchecked下的,所以需要SafeMath库来抛出 数字太大的异常。
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract SafeMathTester{
uint8 public bigNumber = 255; // unchecked
//默认不抛出异常,会回到0
function add() public {
bigNumber = bigNumber + 1;
}
}
Solidity0.8版本之后
是默认运行在checked模式下的,默认会抛异常;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SafeMathTester{
uint8 public bigNumber = 255; // checked
//默认会抛出异常
function add() public {
unchecked {bigNumber = bigNumber + 1;}
}
}
除非是手动unchecked。
这个unchecked关键字可以节省gas
contract SafeMathTester{
uint8 public bigNumber = 255; // uint8 最大255 checked
function add() public {
//这样就不会抛出异常了会回到0
unchecked {bigNumber = bigNumber + 1;}
}
}
共有 0 条评论