第 2 部分 — 路由交易核心邏輯與防重放機制
核心关键词:USDC、ETH、無Gas、Gas Broker、EIP-712、ERC-2612、重放攻击、Permit簽名、跨鏈優惠、價格預言機
為什麼需要 Gas Broker?
在上一篇「系統設計」中,我們描繪了「無 Gas」願景:用戶手裡只有 USDC,也能直接換到原生 ETH,全程不需支付主網 Gas。今天要實現的 Gas Broker 合約 就是關鍵中間層:它負責替用戶墊付 ETH,同時向流動性提供者收取 USDC 作為回報。整個流程只要 1 個函數就能完成—— swap()。
swap 函數的完整一步式接口
為了同時處理:
- 用戶授權 USDC(
permit簽名) - 獎勵確認(
reward簽名) - 正確結算 ETH 與 USDC
我們需要一次餵進 11 個參數:
function swap(
address signer, // 客戶地址
address token, // 代幣地址(USDC)
uint256 value, // 客戶願意出的 USDC 總額
uint256 deadline, // permit 到期時間
uint256 reward, // 給 Gas Provider 的獎勵
uint8 permitV, bytes32 permitR, bytes32 permitS, // permit 簽名 3 段
uint8 rewardV, bytes32 rewardR, bytes32 rewardS // reward 簽名 3 段
) external payable👉 還不熟悉 DeFi 無 Gas 交換?三分鐘帶你看懂核心原理
一步步拆解交易邏輯
- 價值檢查
require(value > reward)確保獎勵不會超過訂單總額。 - 獎勵簽名校驗
透過verifyReward()確認 reward 簽名是由signer本人簽署,且與該筆 permit 綁定,防止重放(後文詳述)。 - 一次性授權 USDC
使用 ERC-2612 的permit(),讓用戶提前簽名而非自己送交易,省去 Gas。 - 取得 ETH 兌換價格
調用鏈上預言機_getEthAmount(),根據上述差額value - reward計算需要轉給用戶的 ETH 數量。 - 檢查 Gas Provider 付款
require(msg.value >= ethAmount),若不足則直接 revert。 原子結算
- 把
token從signer轉至本合約 - 將
ethAmount轉給signer - 剩餘 ETH(若有找零)原路退回給
msg.sender - 最後把收到的 USDC 轉給 Gas Provider
- 把
整段邏輯在單一交易中完成,任何一步失敗都會整體回滾,確保「要嘛全部成功,要嘛全部失敗」,沒有資金風險。
設計防重放的 Reward 結構
如果只把 reward 當作一個數字簽名,會遇到以下經典重放攻擊:
- 用戶 A 發布訂單 O1,獎勵 10 USDC,並被執行。
- 惡意節點把 O1 所有參數保存起來。
- 隔天 A 再發布訂單 O2,獎勵降為 5 USDC。
- 節點用 O1 的高獎勵簽名來執行 O2,結果 A 多付 2 倍獎勵!
解法:讓「獎勵簽名」只對應特定的「permit 簽名」。
新增字段:permitHash
struct Reward {
uint256 value; // 獎勵金額
bytes32 permitHash; // keccak256(permit 簽名) 綁定
}在每次計算 hashReward() 時納入 permitHash,就像區塊鏈的「前一區塊哈希」一樣,把獎勵與授權「捆綁」,攻擊者就無法在其他訂單重放歷史簽名。
為什麼 permit 本身不需再串 nonce?因為 IERC2612.permit() 內建 nonce 機制,同一簽名不會被重複使用。鏈上驗證流程
bytes32 DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256("EIP712Domain(...)"),
keccak256("Gas broker"),
keccak256("1"),
chainId,
address(this)
)
);用戶在 MetaMask 內看到的不再是難懂的 bytes32,而是結構化字段(遵循 EIP-712),降低簽名恐懼感,進一步提高採用率。
完整代碼一覽
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";
interface IPriceOracle {
function getPriceInEth(address token,uint amount) external view returns (uint256);
}
interface IERC2612 {
function permit(address,address,uint,uint,uint8,bytes32,bytes32) external;
}
struct Reward {
uint256 value;
bytes32 permitHash;
}
contract GasBroker {
using SafeERC20 for IERC20;
bytes32 public immutable DOMAIN_SEPARATOR;
IPriceOracle immutable priceOracle;
constructor(uint256 chainId, address _oracle) {
DOMAIN_SEPARATOR = keccak256(/* 同上 */);
priceOracle = IPriceOracle(_oracle);
}
/* swap(), hashReward(), verifyReward(), _getEthAmount(), getEthAmount() 如上文實作 */
}將以上合約部署後,流動性提供者可先透過 getEthAmount() 查詢報價,再呼叫 swap() 幫用戶支付 Gas 換幣;而使用者端只需要簽兩次名—— permit & reward,即可享受「無 Gas」體驗。
FAQ — 常見疑問解答
**Q1:沒有 ETH 也能簽名嗎?
A** 可以。簽名是鏈下動作,用戶的錢包只要有私鑰即可產生 permit & reward 簽名,不需要先擁有 ETH。
**Q2:萬一預言機被操控怎麼辦?
A** 建議使用經過時間加權平均價格(TWAP)的預言機,且項目方可設定最大滑點容忍值或價格更新閾值,保護雙方。
**Q3:Gas Provider 如何賺錢?
A** 在設計的 reward 機制裡,Gas Provider 實際收到的 USDC 會大於他墊付的 ETH(按即時兌換率計算),利潤取決於 reward 參數與市場價差。
**Q4:同一筆 permit 簽名能否用在兩個不同區塊鏈?
A** 不能。DOMAIN_SEPARATOR 已把鏈 ID 寫死,不同鏈或不同合約地址都會導致簽名檢驗失敗。
**Q5:如果用戶在簽名後反悔,能取消訂單嗎?
A** 由於 permit 的 「deadline」 字段,用戶可把時間設得極短,或在 deadline 前主動呼叫 transferFrom 轉回 USDC 來「作廢」該許可。此外,ERC-2612 nonce 進一步保證每筆訂單獨立。
**Q6:合約升級會影響已簽名的 permit 嗎?
A** 不會。Permit 簽名中已固定 verifyingContract 地址,若日後升級需部署新合約,必須產生新的 DOMAIN_SEPARATOR,舊簽名自然失效。
👉 想親自體驗零 Gas 買 ETH?馬上測試最新測試網
本系列尚未結束,下篇將帶你實戰演練單元測試與主網部署。