用 USDC 零 Gas 兌換 ETH:Gas Broker 合約設計詳解

·

第 2 部分 — 路由交易核心邏輯與防重放機制

核心关键词:USDC、ETH、無Gas、Gas Broker、EIP-712、ERC-2612、重放攻击、Permit簽名、跨鏈優惠、價格預言機

為什麼需要 Gas Broker?

在上一篇「系統設計」中,我們描繪了「無 Gas」願景:用戶手裡只有 USDC,也能直接換到原生 ETH,全程不需支付主網 Gas。今天要實現的 Gas Broker 合約 就是關鍵中間層:它負責替用戶墊付 ETH,同時向流動性提供者收取 USDC 作為回報。整個流程只要 1 個函數就能完成—— swap()

swap 函數的完整一步式接口

為了同時處理:

我們需要一次餵進 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 交換?三分鐘帶你看懂核心原理

一步步拆解交易邏輯

  1. 價值檢查
    require(value > reward) 確保獎勵不會超過訂單總額。
  2. 獎勵簽名校驗
    透過 verifyReward() 確認 reward 簽名是由 signer 本人簽署,且與該筆 permit 綁定,防止重放(後文詳述)。
  3. 一次性授權 USDC
    使用 ERC-2612 的 permit(),讓用戶提前簽名而非自己送交易,省去 Gas。
  4. 取得 ETH 兌換價格
    調用鏈上預言機 _getEthAmount(),根據上述差額 value - reward 計算需要轉給用戶的 ETH 數量。
  5. 檢查 Gas Provider 付款
    require(msg.value >= ethAmount),若不足則直接 revert。
  6. 原子結算

    • tokensigner 轉至本合約
    • ethAmount 轉給 signer
    • 剩餘 ETH(若有找零)原路退回給 msg.sender
    • 最後把收到的 USDC 轉給 Gas Provider

整段邏輯在單一交易中完成,任何一步失敗都會整體回滾,確保「要嘛全部成功,要嘛全部失敗」,沒有資金風險。

設計防重放的 Reward 結構

如果只把 reward 當作一個數字簽名,會遇到以下經典重放攻擊:

  1. 用戶 A 發布訂單 O1,獎勵 10 USDC,並被執行。
  2. 惡意節點把 O1 所有參數保存起來。
  3. 隔天 A 再發布訂單 O2,獎勵降為 5 USDC。
  4. 節點用 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?馬上測試最新測試網

本系列尚未結束,下篇將帶你實戰演練單元測試與主網部署。