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:本次交易所能使用的最大gas
to:本次交易的目标地址
value:本次交易所发送的数量
data:调用函数或部署合约时就会发送他
v、r、s:交易签名时的加密技术
 
1、创建两个方法 fundwithdraw 方法。
//合约内容:从用户处回去资金
//退回资金
//设定一个以美元为单位的最低资金值

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

contract FundMe{

    function fund() public{
        //设置一个以美元计算的最小金额

        //1、我们如何发送ETH 到这个合约?

        
    }

    function withdraw() public{

    }
}
2、为了让函数可以被ETH或其他任何token支付,需要将函数设置为 payable
function 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.solimport 刚才的文件,并添加下面这句话
    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,超过则报错)
2、send(最大允许消耗2300gas,超过则返回bool)
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 一样的效果。相比 requirerevert 更节省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();
    }
posted @ 2023-03-07 15:28  Furaooooo  阅读(86)  评论(0)    收藏  举报