目录
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当前浮动状况
上图显示,目前有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 { }
}
攻击失败结果展示