Lesson 4: Remix Fund Me
通过函数发送eth和返回eth
1ETH = 10,000,000,000 Gwei
1ETH = 1,000,000,000,000,000,000 Wei
交易字段:nonce:交易序号gas price:gas费用。wei为单位gas limit:本次交易所能使用的最大gasto:本次交易的目标地址value:本次交易所发送的数量data:调用函数或部署合约时就会发送他v、r、s:交易签名时的加密技术
1、创建两个方法
fund 和 withdraw 方法。//合约内容:从用户处回去资金
//退回资金
//设定一个以美元为单位的最低资金值
//SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;
contract FundMe{
function fund() public{
//设置一个以美元计算的最小金额
//1、我们如何发送ETH 到这个合约?
}
function withdraw() public{
}
}
2、为了让函数可以被ETH或其他任何token支付,需要将函数设置为
payablefunction fund() public payable {}
就像我们的钱包可以持有资金,合约也可以持有资金,每次部署合约时,合约也有一个地址。他和钱包地址几乎一致。所以钱包和合约都可以持有像ETH这样的原生区块链token
3、向合约转账需要用到关键词
require ,表示需要多少资金。msg.value 表示用户实际转了多少资金。msg是内置函数。 Message 函数表示在合约内获取用户签名后的信息。require 函数前面接受一个boolean,如果为否,则执行后面的。并 revert 所有的操作。并且返回剩下的gas。revert 表示回滚。如果在fund 函数的 require 之前执行了操作,那么将回滚操作。require函数用于确认条件有效性,例如输入变量,或合约状态变量是否满足条件,或验证外部合约调用返回的值。当条件为假的时候,不会对gas有任何消耗,同时也不会在继续执行下面的语句。
function fund() public payable{
//设置一个以美元计算的最小金额
//1、我们如何发送ETH 到这个合约?
require(msg.value > 1e18 , "didn't send enough");
//2、什么是reverting?是将之前的操作回滚,并且将剩余的gas费用返回
}
chainlink 和 预言机
chainlink地址:
在去中心化的环境中为智能合约获取外部数据和进行外部计算
1、chainlink dataFeeds:从现实世界中读取定价信息或其他数据,这些数已经聚合并去中心化
2、chainlink VRF:将可证明随机数从现实世界获取到智能合约中
3、chainlink keepers:去中心化驱动事件的方法。可以设置一些触发器
4、chainlink any api:利用api获取网络中的数据
1、data feeds
2、chainlink VRF
3、chainlink keepers
接口和price feeds
可以直接根据ABI进行调用某个链上的接口。
AggregatorV3Interface priceFeed = AggregatorV3Interface(0x8A753747A1Fa494EC906CE90E9f37563A8AF630e);
//合约内容:从用户处回去资金
//退回资金
//设定一个以美元为单位的最低资金值
//SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;
contract FundMe{
function getVersion() public view returns (uint256){
AggregatorV3Interface priceFeed = AggregatorV3Interface(0x8A753747A1Fa494EC906cE90E9f37563A8AF630e);
return priceFeed.version();
}
}
interface AggregatorV3Interface {
function decimals() external view returns (uint8);
function description() external view returns (string memory);
function version() external view returns (uint256);
function getRoundData(uint80 _roundId)
external
view
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
);
function latestRoundData()
external
view
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
);
}
从github上引入和 NPM
NPM上也有直接从chainlink的github代码中同步的代码。可以直接用npm进行引入。
使用方法:
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
完善从预言机获取链外数据并转换汇率
//合约内容:从用户处回去资金
//退回资金
//设定一个以美元为单位的最低资金值
//SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract FundMe{
uint256 public minimumUsd = 50 * 1e18;
function fund() public payable{
//设置一个以美元计算的最小金额
//1、我们如何发送ETH 到这个合约?
require(getConversionRate(msg.value) > 1e18 , "didn't send enough");
//2、什么是reverting?是将之前的操作回滚,并且将剩余的gas费用返回
}
function getPrice() public view returns(uint256) {
AggregatorV3Interface priceFeed = AggregatorV3Interface(0x8A753747A1Fa494EC906cE90E9f37563A8AF630e);
(,int256 price,,,)= priceFeed.latestRoundData();
//返回值进行类型转换,int 转 uint 比较容易
return uint256(price * 1e10);
}
function getConversionRate(uint256 ethAmount) public view returns (uint256){
uint256 ethPrice = getPrice();
uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1e18;
return ethAmountInUsd;
}
// function getVersion() public view returns (uint256){
// AggregatorV3Interface priceFeed = AggregatorV3Interface(0x8A753747A1Fa494EC906cE90E9f37563A8AF630e);
// return priceFeed.version();
// }
function withdraw() public{
}
}
solidity基础:数组和结构体Ⅱ
我们已经有了方法能接受一个地址的ETH
fund 方法。能接收某个地址发送过来的ETH那么我们可以创建一个数组和一个mapping,用来存储发送ETH的地址以及发送了多少ETH
//创建一个数组接收发送人
address[] public funders;
//创建一个mapping接收发送人和发送的ETH有多少
mapping(address => uint256) public addressToAmountFunded;
//更新fund函数,添加向数组和mapping中添加元素的方法
function fund() public payable{
//设置一个以美元计算的最小金额
//1、我们如何发送ETH 到这个合约?
//2、什么是reverting?是将之前的操作回滚,并且将剩余的gas费用返回
require(getConversionRate(msg.value) > 1e18 , "didn't send enough");
//存储所有的发送者
funders.push(msg.sender);
//存储所有的发送者和发了多少ETH
addressToAmountFunded[msg.sender] = msg.value;
}
库
库和智能合约类似, 但是你不能声明任何静态变量,也不能发送 ETH,我们可以使用库,给不同的变量增加更多的功能性。
如上面的代码,可以在12行使用
msg.value.getConversionRate() 方法直接调用 getConversionRate() 方法。首先创建一个sol文件 名为
priceConverter.sol 的文件,把fundMe下对应的代码剪切到这个文件。//SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
library PriceConverter{
function getPrice() public view returns(uint256) {
AggregatorV3Interface priceFeed = AggregatorV3Interface(0x8A753747A1Fa494EC906cE90E9f37563A8AF630e);
(,int256 price,,,)= priceFeed.latestRoundData();
//返回值进行类型转换,int 转 uint 比较容易
return uint256(price * 1e10);
}
function getConversionRate(uint256 ethAmount) public view returns (uint256){
uint256 ethPrice = getPrice();
uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1e18;
return ethAmountInUsd;
}
}
然后在
fundMe.sol 中 import 刚才的文件,并添加下面这句话 using PriceConverter for uint256;
fundMe.sol 完整内容如下
//合约内容:从用户处回去资金
//退回资金
//设定一个以美元为单位的最低资金值
//SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;
import "./PriceConverter.sol";
contract FundMe{
using PriceConverter for uint256;
uint256 public minimumUsd = 50 * 1e18;
address[] public funders;
mapping(address => uint256) public addressToAmountFunded;
function fund() public payable{
//设置一个以美元计算的最小金额
//1、我们如何发送ETH 到这个合约?
//2、什么是reverting?是将之前的操作回滚,并且将剩余的gas费用返回
//require(getConversionRate(msg.value) > 1e18 , "didn't send enough");
require(msg.value.getConversionRate() > 1e18 , "didn't send enough");
funders.push(msg.sender);
addressToAmountFunded[msg.sender] = msg.value;
}
// function getVersion() public view returns (uint256){
// AggregatorV3Interface priceFeed = AggregatorV3Interface(0x8A753747A1Fa494EC906cE90E9f37563A8AF630e);
// return priceFeed.version();
// }
//提取合约资金
function withdraw() public{
}
}
SafeMath库、溢出检查和unchecked 关键字
首先需要了解的是,在
0.8.0 之前,无符号整型和整形运行在unchecked 环境下。如果你给一个uint5 = 255 的变量加1,则他会从最小值开始重新累加。如下代码。在运行 add 方法后,bigNumber 会变为 2。//SPDX-License-Identifier:MIT
pragma solidity ^0.6.0;
contract SafeMathTester{
uint8 public bigNumber = 255;
function add() public {
bigNumber = bigNumber + 3; //255 + 1 = 0 ;255+2 = 1;255+3 = 2.....
}
}
但是到了
0.8.0 之后,solidity 对无符号整形和整形改为了checked 环境。他会自动对变量执行所谓溢出或下溢检查。这时候如果执行了 add 方法,会报错。我们可以通过使用unchecked 关键字恢复到unchecked 版本。如下:
unchecked {bigNumber = bigNumber + 1; }
为什么在0.8.0之后的代码中使用unchecked关键字?因为他会让合约更节省gas。如果你确保在累加的过程中不会达到上限或下限。那么使用unchecked会是不错的选择。
For Loop
上面已经说了如何去通过函数让资助者进行支付ETH,并记录下所有的资助者及资助金额。下面说一下如何提取合约中的ETH。
要想把ETH发送给合约的调用者(方法的调用者),有三种方式可以使用。
1、transfer (最大允许消耗2300gas,超过则报错)
3、call(转移所有gas所以没有上限。并返回boolean表示成功或失败)
//1 transfer
payable(msg.sender).transfer(address(this).balance);
//2 send
bool sendSuccess = payable(msg.sender).send(address(this).balance);
//3 call (bytes memory dataResturned 可以省略)
(bool callSuccess,bytes memory dataResturned) = payable(msg.sender).call{value: address(this).balance }("");
用代码把刚才接收到的ETH清空,之后提取出来(任何人都能提取,只要是方法调用人就行)
function withdraw() public{
//用循环把map中的数据清空
for( uint256 index = 0; index < funders.length; index ++ ){
address funder = funders[index];
addressToAmountFunded[funder] = 0;
}
//清空数组。这里使用创建空数组方式清空
funders = new address[](0);
//1 transfer
//payable(msg.sender).transfer(address(this).balance);
//2 send
//bool sendSuccess = payable(msg.sender).send(address(this).balance);
//3 call 推荐
(bool callSuccess,) = payable(msg.sender).call{value: address(this).balance }("");
require(callSuccess,"call failed");
}
构造函数
因为上面的提取方法任何人都可以调用。所以现在可以通过构造函数,当部署合约时,只让合约部署人进行提取资金。
solidity构造函数与java相同,当部署合约后立即调用一次。如下
address public owner;
constructor () {
owner = msg.sender;
}
然后在
withdraw 方法第一行添加校验
function withdraw() public{
require( msg.sender == owner , "sender is not owner!");
//......todo sth
}
modifier
类似于AOP编程,声明一个修饰器,这个修饰器可以修饰方法。使用到这个修饰器的方法会在之前或之后执行某段代码。
如上,如果向让所有方法只让合约部署人执行,不能把 require... 放在所有方法开始前。所以我们可以创建一个修饰器。然后再使用这个修饰器。
//创建一个叫 onlyOwner 的修饰器
modifier onlyOwner{
require( msg.sender == owner , "sender is not owner!");
_;
}
//在public 后面添加这个修饰器
function withdraw() public onlyOwner{
//todo sth...
}
这样既可以在方法执行前先执行这个修饰器里面的东西。
solidity会在编译的时候查看是否有修饰器,如果有修饰器,则进入到修饰器,查看修饰器里面的代码。
_; 是一个非常重要的关键字。他表示被修饰的方法里面的代码。当他放在修饰器最后时,表示先执行修饰器代码,再执行被修饰方法中的代码。房子啊修饰器开始时,相反。并且可以放在修饰器中。如下。test方法运行时,先执行 a+1;再执行 a*2,再执行a-1;最后结果是a=3。
uint public a = 1;
modifier addModi{
a = a + 1;
_;
a = a - 1;
}
function test() public addModi{
a = a * 2;
}
进阶:immutable 和 constant
1、被
constant 修饰的变量称之为常量。常量能极大减少合约部署时的gas消耗。(示例中减少了19,000个gas)uint256 public constant MINIMUM_USD = 50 * 1e18;
2、对于另外一个只需要设置一次的变量
owner ,只在构造函数中被赋过值,这些不和声明写在一行的,被一次性设置的变量,可以修饰为 immutable (不可变的)。通常一个 immutable 变量命名约定是 i_ 开头。(示例中减少了2000个左右的gas)address public immutable i_owner;
这两种被修饰的变量节省gas的原因是没有把变量存储在存储槽中。而是存储在合约的字节码中。
自定义异常
从
0.8.4 开始,可以使用自定义异常。比如 revert ,只需要添加一个revert 语句,就能实现和require 一样的效果。相比 require ,revert 更节省gas。//SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;
import "./PriceConverter.sol";
//1 声明一个错误
error NotOwner();
contract FundMe{
//todo sth
//2 使用revert 而不是 require
modifier onlyOwner{
//require( msg.sender == owner , "sender is not owner!");
if(msg.sender != i_owner ){
revert NotOwner();
}
_;
}
}
receive 和 fallback 方法
两个特殊的方法。
//SPDX-License-Identifier:MIT
pragma solidity ^0.8.7;
contract FallbackExample{
uint256 public result;
receive() external payable{
result = 1;
}
}
当你向合约发送交易时(不管交易了0wei还是1wei都是交易), 没有指定某个函数 , receive 就会触发 (msg.data 是空时)。这是result就会变为1。
当数据与交易一起发送时(msg.data有值 0x00 ),solidity会判断你不是在寻找receive 方法。你是在寻找某个函数(0x00)。所以会自动寻找那个函数。但是他找不到这个函数的时候,会触发fallback函数。但是这个时候没有fallback函数,remix就会报错。
当有了fallback函数,如果发送数据与交易时,calldata 有值 0x00,solidity会去寻找这个方法。但是没有找到。因此会调用fallback函数。
//SPDX-License-Identifier:MIT
pragma solidity ^0.8.7;
contract FallbackExample{
uint256 public result;
receive() external payable{
result = 1;
}
fallback() external payable{
result = 2;
}
}
判断是否调用recive 或 fallback
如果我们在fundMe.sol 中添加下面这两个方法,那么如果有人给我们转钱而不调用fund方法,那么合约也会自动调用fund方法。
receive() external payable{
fund();
}
fallback() external payable{
fund();
}

浙公网安备 33010602011771号