区块链全栈以太坊(四)合约实战FoundMe

合约demo学习- FoundMe(众筹)

这个demo演示了 众筹以及合约所有者提现的功能,使用了chainlink获取eth/usd 价格。

学习掌握完这个demo,就学会了基本合约编写流程、 solidity基础、remix简单使用。

一、fund (筹款 )函数实现要点

让用户向合约里面存钱。

solidity_fundme3.png

)

1) payable

fund 声明为payable,让按钮变红,表示涉及支付功能。

2) msg.value金额

返回的币数量 value。单位是最小的wei

如下代码可以访问某人转账的金额。

solidity_fundme1.png

3) require

条件不满足,则操作、费用gas都被revert

当value传入0时,number=5被revert回滚了,require语句后面消耗的gas也被回滚。

当value传入2 ether时,number=5 生效,require条件满足。

solidity_fundme2.png

4) Library

我们可以使用库,给不同的变量增加更多的功能性,如PriceConverter.sol

  • 库和智能合约类似,但是你不能声明任何静态变量,也不能发送ETH。
  • 一个库的所有函数都是 internal (而不是public)的。

https://solidity-by-example.org/library/

  using PriceConverter for uint256;
solidity_fundme4.png

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喂价服务

见官网

  1. api doc https://docs.chain.link/data-feeds/api-reference#functions-in-aggregatorv3interface

  2. adress doc https://docs.chain.link/data-feeds/price-feeds/addresses?network=ethereum&page=1

  3. 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随机

区块链是确定性系统,不具备随机性。

所以需要通过外部来获得可验证的随机

https://docs.chain.link/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 没有值时)

contract_receive0.png

contract_receive1.png

2.fallback

合约里找不到要调用的函数时,会降级调用 fallback。

fallback 和receive谁优先主要取决于 calldata是否为空,如下图

contract_receivefallback.png

三、部署&测试

  1. 部署到测试网络上(才能有 chainlink获取币价)。
  2. 传入20000000000000000 Wei(这样才满足大于50美元)。
  3. 执行 fund()。
  4. 重复2,3。
solidity_fundme5.png
solidity_fundme6.png
solidity_fundme7.png
solidity_fundme8.png
  1. 切换账号account2测试提现,验证权限。
solidity_fundme9.png
solidity_fundme10.png
  1. 切换回account1,测试提现成功。
solidity_fundme11.png

四、常用类库

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;}
     }
}

版权声明:
作者:lichengxin
链接:https://www.techfm.club/p/120584.html
来源:TechFM
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>