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
