Gas机制与转账设计
学习目标
- 理解区块链的经济模型与激励机制
- 掌握 Gas、Gas Price、Gas Fee 的概念与关系
- 理解 Ether 单位与转换
- 掌握合约转账设计(receive / fallback / payable)
区块链的经济系统
为什么需要经济模型?
- 计算与存储资源是稀缺的:每个节点都要执行和存储所有交易
- 共识和 trustless 需要矿工工作:矿工(验证者)需要激励才会持续参与
- Transaction 执行有成本(gas):gas 费成为矿工的直接激励
- Ether 是生态系统的原生货币:用于支付 gas 费、转账、合约交互
Ether 单位
1 ETH = 10^18 Wei(Wei 是最小单位)
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;
contract EtherUnits {
uint public oneWei = 1 wei;
bool public isOneWei = (1 wei == 1);
uint public oneEther = 1 ether;
bool public isOneEther = (1 ether == 1e18);
}
常用单位换算:
| 单位 | Wei 值 |
|---|---|
| 1 Wei | 1 |
| 1 Gwei | 10^9 |
| 1 Ether | 10^18 |
Gas、Gas Fee、Gas Price
三者的关系
Gas Fee = Gas × Gas Price
- Gas:完全由执行逻辑决定。固定逻辑的函数,gas 消耗是不变的(例如一次 SSTORE 操作固定消耗 20000 gas)
- Gas Price:由交易发起者在 transaction 中设定,单位为 Gwei。Gas Price 由市场供需决定——网络拥堵时价格上涨,空闲时价格下降
- Gas Fee:最终支付的费用,等于实际消耗的 gas 乘以 gas price
Gas Limit
Gas Limit 存在于三个层面:
- 交易层面:交易发起者设定 gaslimit,表示最多愿意消耗多少 gas
- 合约调用层面:合约间调用时可以指定 gaslimit,控制子调用的 gas 消耗
- 区块层面:每个区块有 gaslimit 上限,限制单个区块的总计算量
gasleft() 函数
gasleft() 返回当前剩余可用的 gas,可用于:
- 监控 gas 消耗
- 在 gas 不足前提前退出
- 限制子调用的 gas 消耗
退款规则
- 剩余未用的 gas 会退款给交易发起者
- 交易失败(revert / out of gas)时,已消耗的 gas 不退
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;
contract Gas {
uint public i = 0;
uint public remained;
function forever() public {
while (true) {
if (i > 100) return;
if (i == 10) {
remained = gasleft(); // 记录剩余 gas
}
i += 1;
}
}
}
contract GasCaller {
Gas private gas;
constructor(Gas _gas) {
gas = _gas;
}
function callForever() public {
gas.forever{gas: 200000}(); // 限制子调用最多消耗 200000 gas
}
}
合约持有 Ether
- 合约可以持有 Ether! 合约地址和 EOA 地址一样,都有余额
- 合约与其他合约或 EOA 之间可以自由转账
- 典型应用:多签钱包(Multisig Wallet)、DeFi 协议、ICO 合约
转账设计思想
核心理解
转账没有单独的函数,转账是与函数调用一起发生的。
调用合约函数时,通过附加选项 {value: amount} 来同时发送 Ether:
// 通过 call 转账
_to.call{value: 1 ether}("");
// 通过具名函数转账
contract.deposit{value: 1 ether}();
被调用函数解析逻辑(重要!)
当合约收到一笔调用时,EVM 按以下逻辑决定执行哪个函数:
发送 Ether(函数调用)
│
msg.data 是否为空?
/
否 是
/
函数名匹配? receive() 存在?
/ /
是 否 是 否
/ /
匹配函数 fallback() receive() fallback()
payable 检查
- 如果
msg.value > 0且目标函数没有payable修饰符 → 调用失败(自动 revert) payable修饰符的唯一作用就是允许函数接收 Ether
receive() 函数
receive() 是专门处理纯转账(calldata 为空)的函数,使 fallback 的职责更加清晰:
receive():处理纯 Ether 转账fallback():处理调用不存在的函数
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;
contract ReceiveEther {
string public caller;
receive() external payable {
caller = "receive";
}
fallback() external payable {
caller = "fallback";
}
function deposit() public payable {
// 接收 Ether 的普通函数
}
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
转账方式对比
| 方式 | Gas Limit | 推荐程度 | 说明 |
|---|---|---|---|
transfer |
固定 2300 | 不推荐 | 旧设计,gas 不足以执行复杂逻辑 |
send |
固定 2300 | 不推荐 | 旧设计,返回 bool 但常被忽略 |
call |
可自定义 | 推荐 | 灵活,安全(需检查返回值) |
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;
contract SendEther {
// 推荐方式:通过 call 转账
function sendViaCall(address payable _to) public payable {
(bool sent, ) = _to.call{value: msg.value}("");
require(sent, "Failed to send Ether");
}
// 通过具名函数转账
function sendViaFoo(address payable _to) public payable {
ReceiveEther re = ReceiveEther(_to);
re.deposit{value: msg.value}();
}
}
综合实例:ERC20 Token + ICO
以下是一个简化的 ERC20 代币合约,结合了 ICO(首次代币发行)功能,展示了转账设计的实际应用:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract MyErc20Token {
string public name = "MyDollar";
string public symbol = "$";
uint public decimals = 4;
address public owner;
mapping(address => uint) public balanceOf;
constructor() {
owner = msg.sender;
}
// 铸币:用户发送 ETH 购买代币
function mint() external payable {
balanceOf[msg.sender] += msg.value;
}
event TransferEvent(uint oldv, uint newv);
modifier isOwner() {
require(msg.sender == owner, "not owner!");
_;
}
// 转账:从调用者账户转给目标地址
function transfer(address to, uint amount) public {
address from = msg.sender;
uint current = balanceOf[from];
require(current >= amount, "not enough balance!");
uint toc = balanceOf[to];
current -= amount;
toc += amount;
balanceOf[from] = current;
balanceOf[to] = toc;
}
// 提现:仅 owner 可以将合约中的 ETH 提取到自己的地址
function withdraw() external isOwner {
(bool suc, ) = owner.call{value: address(this).balance}("");
require(suc, "failed!");
}
}
合约功能解析:
- mint():用户发送 ETH 调用此函数,合约记录对应的代币余额(payable 允许接收 ETH)
- transfer():代币持有者之间的转账(注意这是代币层面的转账,不是 ETH 转账)
- withdraw():合约 owner 将合约持有的所有 ETH 提取出来,使用
call{value: ...}方式转账 - isOwner modifier:权限控制,确保只有合约部署者可以提现
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/7496
