dApp Docs/Meta-Transaction与Gas Station实现
Development reference. Not independently verified for production.

Meta-Transaction 与 Gas Station 实现指南

适用链: msg-chain-1 | bech32 前缀: msg | 精度: 18 位小数 | Gas 价格: 1,000,000,000 attoMSG/gas
主网状态: No-Go


1. 概述

1.1 什么是 Meta-Transaction

元交易(Meta-Transaction)是一种允许用户在不持有链上原生代币(即 Gas 代币)的情况下提交交易的技术范式。核心思想是:用户签署一笔交易意图(intent),由第三方中继者(Relayer)将这笔签名提交到链上并支付 Gas 费用。这笔 Gas 费用可以通过链下结算、代币兑换或其他机制得到补偿。

在 MSG Chain 的语境下,用户使用 msg1... 地址格式,以 18 位小数精度表达代币数量,签署一条结构化消息。Relayer 服务读取该签名,将交易包装后提交,用户无需持有 MSG 即可完成合约交互。

1.2 Gas Station 网络模型

Gas Station(加油站)是 Meta-Transaction 的工程化实现,提供一套完整的中继服务架构:

+--------+      EIP-712 Signature       +-------------+
|  User   | --------------------------> |   Relayer   |
+--------+                               |    API      |
    |                                     +------+------+
    |  Sign with private key                     |
    |                                            |  broadcast tx
    v                                            v
+--------+                              +------------------+
| Wallet |                              | Forwarder Contract |
+--------+                              +--------+---------+
                                                |
                                                | delegatecall / execute
                                                v
                                        +------------------+
                                        | Target Contract   |
                                        +------------------+

1.3 使用场景

1.3.1 新用户引导(Onboarding)

新用户首次进入 MSG Chain 生态时,钱包中尚无 MSG 代币。通过 Gas Station,用户可以直接使用 DEX、借贷协议或 NFT 市场零成本入门。Relayer 承担首次交互的 Gas 费用,大幅降低获客摩擦。

1.3.2 Gasless dApp

面向消费者的应用(游戏、社交、预测市场)希望消除用户的 Gas 认知负担。Meta-Transaction 可以让用户像使用 Web2 应用一样无缝操作,后台由 Relayer 处理链上费用。

1.3.3 企业级钱包

企业或托管钱包需要集中管理 Gas 费用,而非将 MSG 分发到每个子钱包。企业部署自己的 Relayer 服务,所有子地址的交易由中心化 Gas 池出资,便于财务审计和成本控制。

1.3.4 批量操作

Relayer 可以将多个用户的签名交易批量提交,分摊固定成本。这在空投领取、批量 NFT 铸造等场景下尤其有用。

1.4 MSG Chain 关键参数

参数 值
Chain ID msg-chain-1
Bech32 地址前缀 msg
小数精度 18
Gas 价格(低) 0.01 MSG
Gas 价格(固定) 1,000,000,000 attoMSG/gas
Gas 价格(高) 0.04 MSG
平均出块时间 ~6 秒
CosmWasm 版本 v1.x

1.5 与常规交易对比

特性 常规交易 Meta-Transaction
Gas 支付者 交易发起者 Relayer
签名者 交易发起者 用户(intent)
提交者 交易发起者 Relayer
用户需持有 MSG 是 否
实现复杂度 低 中-高
用户门槛 高 低

2. EIP-712 / Typed Signatures

2.1 结构化数据签名概述

EIP-712 是以太坊标准,定义了结构化数据的编码和签名方法,使签名对人类可读且不易受到重放攻击。在 MSG Chain 中,CosmWasm 合约通过 cosmwasm_std::HexBinary 或 Binary 类型接收并验证 ECDSA 签名,我们采用与 EIP-712 兼容的编码方式。

EIP-712 签名的核心组件:

2.2 EIP-712 Domain Separator 定义

在 MSG Chain 上,Domain Separator 定义为:

domainSeparator = keccak256(
    abi.encode(
        EIP712Domain({
            name: "MSG Chain Meta-Transactions",
            version: "1",
            chainId: 1,
            verifyingContract: forwarder_address,
            salt: hex"00"
        })
    )
)

2.3 Meta-Transaction 类型定义

用户签名的核心消息结构:

struct MetaTransaction {
    address from;
    address to;
    uint256 value;
    bytes data;
    uint256 nonce;
    uint256 gasLimit;
    uint256 deadline;
}

2.4 CosmWasm 中的签名验证方式

2.4.1 Cosmos SDK SIGN_MODE_DIRECT 对比

Cosmos SDK 默认使用 SIGN_MODE_DIRECT(Protobuf 序列化)和 SIGN_MODE_LEGACY_AMINO_JSON(Amino 编码)来签名交易。这两种方式对整个交易进行签名,不适合 Meta-Transaction 场景,因为 Meta-Transaction 需要让用户只签名"意图"(intent),而不关心交易的 Gas 价格、提交时间等执行细节。

模式 适用场景 支持 Meta-Tx
SIGN_MODE_DIRECT 普通 Cosmos 交易 否
SIGN_MODE_LEGACY_AMINO_JSON 普通 Cosmos 交易、Ledger 否
EIP-712 结构化签名 合约内签名验证 是
ECDSA secp256k1 裸签名 自定义场景 是

2.4.2 MSG Chain 推荐的签名方案

MSG Chain 采用基于 EIP-712 的结构化签名方案,与 CosmWasm 的 cosmwasm_std::verify_secp256k1 或 cosmwasm_std::ecdsa_recover 配合使用。

2.5 前端签名示例

const domain = {
    name: "MSG Chain Meta-Transactions",
    version: "1",
    chainId: 1,
    verifyingContract: forwarderAddress,
};

const types = {
    MetaTransaction: [
        { name: "from", type: "address" },
        { name: "to", type: "address" },
        { name: "value", type: "uint256" },
        { name: "data", type: "bytes" },
        { name: "nonce", type: "uint256" },
        { name: "gasLimit", type: "uint256" },
        { name: "deadline", type: "uint256" },
    ],
};

const message = {
    from: userAddress,
    to: contractAddress,
    value: "0",
    data: encodedData,
    nonce: currentNonce,
    gasLimit: "500000",
    deadline: deadlineUnix,
};

const signature = await signer._signTypedData(domain, types, message);

2.6 完整的前端签名工具类

import { ethers } from "ethers";

export interface MetaTransaction {
    from: string;
    to: string;
    value: string;
    data: string;
    nonce: number;
    gasLimit: number;
    deadline: number;
}

export class MetaTransactionSigner {
    private domain: ethers.TypedDataDomain;
    private types: Record<string, Array<ethers.TypedDataField>>;

    constructor(forwarderAddress: string, chainId: number = 1) {
        this.domain = {
            name: "MSG Chain Meta-Transactions",
            version: "1",
            chainId: chainId,
            verifyingContract: forwarderAddress,
        };
        this.types = {
            MetaTransaction: [
                { name: "from", type: "address" },
                { name: "to", type: "address" },
                { name: "value", type: "uint256" },
                { name: "data", type: "bytes" },
                { name: "nonce", type: "uint256" },
                { name: "gasLimit", type: "uint256" },
                { name: "deadline", type: "uint256" },
            ],
        };
    }

    async sign(signer: ethers.Signer, tx: MetaTransaction): Promise<string> {
        return await signer._signTypedData(this.domain, this.types, tx);
    }

    async signAsync(
        signer: ethers.Signer,
        from: string,
        to: string,
        data: string,
        nonce: number,
        deadline?: number
    ): Promise<{ tx: MetaTransaction; signature: string }> {
        const tx: MetaTransaction = {
            from,
            to,
            value: "0",
            data,
            nonce,
            gasLimit: 500000,
            deadline: deadline ?? Math.floor(Date.now() / 1000) + 3600,
        };
        const signature = await this.sign(signer, tx);
        return { tx, signature };
    }

    getTypedDataHash(tx: MetaTransaction): string {
        return ethers.TypedDataEncoder.hash(this.domain, this.types, tx);
    }

    static recover(
        forwarderAddress: string,
        chainId: number,
        tx: MetaTransaction,
        signature: string
    ): string {
        const domain = {
            name: "MSG Chain Meta-Transactions",
            version: "1",
            chainId: chainId,
            verifyingContract: forwarderAddress,
        };
        const types = {
            MetaTransaction: [
                { name: "from", type: "address" },
                { name: "to", type: "address" },
                { name: "value", type: "uint256" },
                { name: "data", type: "bytes" },
                { name: "nonce", type: "uint256" },
                { name: "gasLimit", type: "uint256" },
                { name: "deadline", type: "uint256" },
            ],
        };
        return ethers.TypedDataEncoder.recover(domain, types, tx, signature);
    }
}

2.7 Nonce 管理

每个用户地址维护一个单调递增的 nonce,防止签名重放:

export async function fetchNonce(
    cosmWasmClient: any,
    forwarderContract: string,
    userAddress: string
): Promise<number> {
    const queryMsg = {
        get_nonce: {
            address: userAddress,
        },
    };
    const result = await cosmWasmClient.queryContractSmart(
        forwarderContract,
        queryMsg
    );
    return Number(result.nonce);
}

2.8 截止时间(Deadline)


3. Forwarder 合约

3.1 合约架构

Forwarder 是部署在 MSG Chain 上的核心智能合约,负责验证用户签名并代表用户调用目标合约。

3.1.1 合约状态

use cosmwasm_std::{
    entry_point, to_binary, Addr, Binary, Deps, DepsMut, Env,
    HexBinary, MessageInfo, Response, StdError, StdResult,
    Storage, Uint128, Uint64, WasmMsg, CosmosMsg, SubMsg,
    ReplyOn, from_binary,
};
use cw_storage_plus::{Item, Map};
use serde::{Deserialize, Serialize};
use sha3::{Digest, Keccak256};

pub const CONTRACT_NAME: &str = "msg-chain-forwarder";
pub const CONTRACT_VERSION: &str = "1.0.0";

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
    pub owner: Addr,
    pub paused: bool,
    pub gas_price: Uint128,
    pub max_gas_limit: Uint128,
    pub min_deadline_delta: Uint64,
}

pub const CONFIG: Item<Config> = Item::new("config");
pub const NONCES: Map<&Addr, Uint128> = Map::new("nonces");
pub const TRUSTED_SIGNERS: Map<&Addr, bool> = Map::new("signers");
pub const GAS_BALANCE: Item<Uint128> = Item::new("gas_balance");

3.1.2 消息定义

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    pub owner: String,
    pub gas_price: Uint128,
    pub max_gas_limit: Uint128,
    pub min_deadline_delta: Uint64,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    Execute {
        tx: MetaTransaction,
        signature: HexBinary,
        recovery_id: u8,
    },
    ExecuteBatch {
        txs: Vec<MetaTransaction>,
        signatures: Vec<HexBinary>,
        recovery_ids: Vec<u8>,
    },
    AddSigner { signer: String },
    RemoveSigner { signer: String },
    SetPaused { paused: bool },
    SetGasPrice { price: Uint128 },
    SetMaxGasLimit { limit: Uint128 },
    Deposit {},
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    GetNonce { address: String },
    GetConfig {},
    GetGasBalance {},
    IsSigner { address: String },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MetaTransaction {
    pub from: String,
    pub to: String,
    pub value: Uint128,
    pub data: HexBinary,
    pub nonce: Uint128,
    pub gas_limit: Uint128,
    pub deadline: Uint64,
}

3.1.3 错误类型

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, thiserror::Error)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),
    #[error("Unauthorized")]
    Unauthorized {},
    #[error("Contract is paused")]
    ContractPaused {},
    #[error("Invalid signature")]
    InvalidSignature {},
    #[error("Nonce mismatch: expected {expected}, got {actual}")]
    NonceMismatch { expected: Uint128, actual: Uint128 },
    #[error("Transaction expired: deadline {deadline}, current time {current}")]
    TransactionExpired { deadline: Uint64, current: Uint64 },
    #[error("Gas limit exceeded: limit {limit}, required {required}")]
    GasLimitExceeded { limit: Uint128, required: Uint128 },
    #[error("Gas balance insufficient: balance {balance}, required {required}")]
    InsufficientGasBalance { balance: Uint128, required: Uint128 },
    #[error("Signer not trusted")]
    SignerNotTrusted {},
    #[error("Replay detected: hash {hash}")]
    ReplayDetected { hash: String },
    #[error("Deadline delta too small: delta {delta}, minimum {minimum}")]
    DeadlineDeltaTooSmall { delta: Uint64, minimum: Uint64 },
    #[error("{0}")]
    Generic(String),
}

3.2 合约实例化

#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let config = Config {
        owner: deps.api.addr_validate(&msg.owner)?,
        paused: false,
        gas_price: msg.gas_price,
        max_gas_limit: msg.max_gas_limit,
        min_deadline_delta: msg.min_deadline_delta,
    };
    CONFIG.save(deps.storage, &config)?;
    GAS_BALANCE.save(deps.storage, &Uint128::zero())?;

    Ok(Response::new()
        .add_attribute("action", "instantiate")
        .add_attribute("owner", msg.owner))
}

3.3 execute 核心入口

#[entry_point]
pub fn execute(
    deps: DepsMut,
    env: Env,
    _info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::Execute { tx, signature, recovery_id } => {
            execute_meta_tx(deps, env, tx, signature, recovery_id)
        }
        ExecuteMsg::ExecuteBatch { txs, signatures, recovery_ids } => {
            execute_batch(deps, env, txs, signatures, recovery_ids)
        }
        ExecuteMsg::AddSigner { signer } => execute_add_signer(deps, env, signer),
        ExecuteMsg::RemoveSigner { signer } => execute_remove_signer(deps, env, signer),
        ExecuteMsg::SetPaused { paused } => execute_set_paused(deps, env, paused),
        ExecuteMsg::SetGasPrice { price } => execute_set_gas_price(deps, env, price),
        ExecuteMsg::SetMaxGasLimit { limit } => execute_set_max_gas_limit(deps, env, limit),
        ExecuteMsg::Deposit {} => execute_deposit(deps, env, _info),
    }
}

3.4 execute_meta_tx 核心逻辑

pub fn execute_meta_tx(
    deps: DepsMut,
    env: Env,
    tx: MetaTransaction,
    signature: HexBinary,
    recovery_id: u8,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    if config.paused {
        return Err(ContractError::ContractPaused {});
    }

    let current_time = env.block.time.seconds();
    if current_time > tx.deadline.u64() {
        return Err(ContractError::TransactionExpired {
            deadline: tx.deadline,
            current: Uint64::new(current_time),
        });
    }

    let from_addr = deps.api.addr_validate(&tx.from)?;
    let expected_nonce = NONCES
        .may_load(deps.storage, &from_addr)?
        .unwrap_or(Uint128::zero());

    if tx.nonce != expected_nonce {
        return Err(ContractError::NonceMismatch {
            expected: expected_nonce,
            actual: tx.nonce,
        });
    }

    let typed_data_hash = build_typed_data_hash(&tx, &env, &deps);
    let recovered = recover_address(
        &typed_data_hash,
        signature.as_slice(),
        recovery_id,
    )?;
    let recovered_addr = addr_from_bytes(&recovered, &deps)?;
    if recovered_addr != from_addr {
        return Err(ContractError::InvalidSignature {});
    }

    let new_nonce = expected_nonce.checked_add(Uint128::one())?;
    NONCES.save(deps.storage, &from_addr, &new_nonce)?;

    let sub_msg: Vec<SubMsg> = vec![SubMsg {
        id: 1,
        msg: CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: tx.to.clone(),
            funds: vec![],
            msg: Binary::from(tx.data.as_slice()),
        }),
        gas_limit: Some(tx.gas_limit.u128()),
        reply_on: ReplyOn::Always,
    }];

    let gas_cost = tx.gas_limit.checked_mul(config.gas_price)?;
    let gas_balance = GAS_BALANCE.load(deps.storage)?;
    if gas_balance < gas_cost {
        return Err(ContractError::InsufficientGasBalance {
            balance: gas_balance,
            required: gas_cost,
        });
    }
    let new_balance = gas_balance.checked_sub(gas_cost)?;
    GAS_BALANCE.save(deps.storage, &new_balance)?;

    Ok(Response::new()
        .add_submessages(sub_msg)
        .add_attribute("action", "execute_meta_tx")
        .add_attribute("from", &tx.from)
        .add_attribute("to", &tx.to)
        .add_attribute("nonce", tx.nonce.to_string())
        .add_attribute("gas_cost", gas_cost.to_string()))
}

3.5 批量执行

pub fn execute_batch(
    deps: DepsMut,
    env: Env,
    txs: Vec<MetaTransaction>,
    signatures: Vec<HexBinary>,
    recovery_ids: Vec<u8>,
) -> Result<Response, ContractError> {
    if txs.len() != signatures.len() || txs.len() != recovery_ids.len() {
        return Err(ContractError::Generic("Mismatched input lengths".to_string()));
    }

    let config = CONFIG.load(deps.storage)?;
    if config.paused {
        return Err(ContractError::ContractPaused {});
    }

    let mut response = Response::new();
    let mut total_gas_cost = Uint128::zero();

    for (i, tx) in txs.iter().enumerate() {
        let current_time = env.block.time.seconds();
        if current_time > tx.deadline.u64() {
            continue;
        }

        let from_addr = deps.api.addr_validate(&tx.from)?;
        let expected_nonce = NONCES
            .may_load(deps.storage, &from_addr)?
            .unwrap_or(Uint128::zero());
        if tx.nonce != expected_nonce {
            continue;
        }

        let typed_data_hash = build_typed_data_hash(tx, &env, &deps);
        let recovered = recover_address(
            &typed_data_hash,
            signatures[i].as_slice(),
            recovery_ids[i],
        )?;
        let recovered_addr = addr_from_bytes(&recovered, &deps)?;
        if recovered_addr != from_addr {
            continue;
        }

        let new_nonce = expected_nonce.checked_add(Uint128::one())?;
        NONCES.save(deps.storage, &from_addr, &new_nonce)?;

        let sub_msg = SubMsg {
            id: (i + 1) as u64,
            msg: CosmosMsg::Wasm(WasmMsg::Execute {
                contract_addr: tx.to.clone(),
                funds: vec![],
                msg: Binary::from(tx.data.as_slice()),
            }),
            gas_limit: Some(tx.gas_limit.u128()),
            reply_on: ReplyOn::Never,
        };
        response = response.add_submessage(sub_msg);

        let gas_cost = tx.gas_limit.checked_mul(config.gas_price)?;
        total_gas_cost = total_gas_cost.checked_add(gas_cost)?;
    }

    let gas_balance = GAS_BALANCE.load(deps.storage)?;
    if gas_balance < total_gas_cost {
        return Err(ContractError::InsufficientGasBalance {
            balance: gas_balance,
            required: total_gas_cost,
        });
    }
    let new_balance = gas_balance.checked_sub(total_gas_cost)?;
    GAS_BALANCE.save(deps.storage, &new_balance)?;

    Ok(response
        .add_attribute("action", "execute_batch")
        .add_attribute("batch_size", txs.len().to_string())
        .add_attribute("total_gas_cost", total_gas_cost.to_string()))
}

3.6 签名验证辅助函数

pub fn build_typed_data_hash(
    tx: &MetaTransaction,
    env: &Env,
    deps: &DepsMut,
) -> [u8; 32] {
    let domain_type_hash = Keccak256::digest(
        b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
    );
    let tx_type_hash = Keccak256::digest(
        b"MetaTransaction(address from,address to,uint256 value,bytes data,uint256 nonce,uint256 gasLimit,uint256 deadline)"
    );

    // Domain Separator
    let mut domain_bytes = Vec::new();
    domain_bytes.extend_from_slice(&domain_type_hash);
    domain_bytes.extend_from_slice(&Keccak256::digest(b"MSG Chain Meta-Transactions"));
    domain_bytes.extend_from_slice(&Keccak256::digest(b"1"));
    // chainId: uint256
    let mut chain_id_bytes = [0u8; 32];
    let chain_id_val = env.block.chain_id.parse::<u128>().unwrap_or(1);
    chain_id_bytes[16..].copy_from_slice(&chain_id_val.to_be_bytes());
    domain_bytes.extend_from_slice(&chain_id_bytes);
    // verifyingContract
    let canonical = deps.api.addr_canonicalize(env.contract.address.as_str()).unwrap_or_default();
    let mut addr_padded = [0u8; 32];
    let raw = canonical.as_slice();
    if raw.len() <= 20 {
        addr_padded[32 - raw.len()..].copy_from_slice(raw);
    }
    domain_bytes.extend_from_slice(&addr_padded);
    let domain_separator = Keccak256::digest(&domain_bytes);

    // Struct Hash
    let mut struct_bytes = Vec::new();
    struct_bytes.extend_from_slice(&tx_type_hash);
    // from
    let from_canonical = deps.api.addr_canonicalize(&tx.from).unwrap_or_default();
    let mut from_padded = [0u8; 32];
    let from_raw = from_canonical.as_slice();
    if from_raw.len() <= 20 {
        from_padded[32 - from_raw.len()..].copy_from_slice(from_raw);
    }
    struct_bytes.extend_from_slice(&from_padded);
    // to
    let to_canonical = deps.api.addr_canonicalize(&tx.to).unwrap_or_default();
    let mut to_padded = [0u8; 32];
    let to_raw = to_canonical.as_slice();
    if to_raw.len() <= 20 {
        to_padded[32 - to_raw.len()..].copy_from_slice(to_raw);
    }
    struct_bytes.extend_from_slice(&to_padded);
    // value
    let mut value_bytes = [0u8; 32];
    tx.value.to_be_bytes(&mut value_bytes);
    struct_bytes.extend_from_slice(&value_bytes);
    // data
    let data_hash = Keccak256::digest(tx.data.as_slice());
    struct_bytes.extend_from_slice(&data_hash);
    // nonce
    let mut nonce_bytes = [0u8; 32];
    tx.nonce.to_be_bytes(&mut nonce_bytes);
    struct_bytes.extend_from_slice(&nonce_bytes);
    // gasLimit
    let mut gas_limit_bytes = [0u8; 32];
    tx.gas_limit.to_be_bytes(&mut gas_limit_bytes);
    struct_bytes.extend_from_slice(&gas_limit_bytes);
    // deadline
    let mut deadline_bytes = [0u8; 32];
    tx.deadline.to_be_bytes(&mut deadline_bytes);
    struct_bytes.extend_from_slice(&deadline_bytes);

    let struct_hash = Keccak256::digest(&struct_bytes);

    // 最终哈希: 0x19 0x01 + domainSeparator + structHash
    let mut final_bytes = Vec::new();
    final_bytes.push(0x19);
    final_bytes.push(0x01);
    final_bytes.extend_from_slice(&domain_separator);
    final_bytes.extend_from_slice(&struct_hash);
    let mut result = [0u8; 32];
    result.copy_from_slice(&Keccak256::digest(&final_bytes));
    result
}

3.7 管理功能

pub fn execute_add_signer(
    deps: DepsMut,
    _env: Env,
    signer: String,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let sender = deps.api.addr_validate(&signer)?;
    if sender != config.owner {
        return Err(ContractError::Unauthorized {});
    }
    TRUSTED_SIGNERS.save(deps.storage, &sender, &true)?;
    Ok(Response::new()
        .add_attribute("action", "add_signer")
        .add_attribute("signer", signer))
}

pub fn execute_set_paused(
    deps: DepsMut,
    _env: Env,
    paused: bool,
) -> Result<Response, ContractError> {
    let mut config = CONFIG.load(deps.storage)?;
    // In real impl, check sender == config.owner
    config.paused = paused;
    CONFIG.save(deps.storage, &config)?;
    Ok(Response::new()
        .add_attribute("action", "set_paused")
        .add_attribute("paused", paused.to_string()))
}

pub fn execute_set_gas_price(
    deps: DepsMut,
    _env: Env,
    price: Uint128,
) -> Result<Response, ContractError> {
    let mut config = CONFIG.load(deps.storage)?;
    config.gas_price = price;
    CONFIG.save(deps.storage, &config)?;
    Ok(Response::new()
        .add_attribute("action", "set_gas_price")
        .add_attribute("price", price.to_string()))
}

pub fn execute_deposit(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
) -> Result<Response, ContractError> {
    let mut balance = GAS_BALANCE.load(deps.storage)?;
    let amount = info
        .funds
        .iter()
        .find(|c| c.denom == "umsg")
        .map(|c| c.amount)
        .unwrap_or(Uint128::zero());
    balance = balance.checked_add(amount)?;
    GAS_BALANCE.save(deps.storage, &balance)?;
    Ok(Response::new()
        .add_attribute("action", "deposit")
        .add_attribute("amount", amount.to_string()))
}

pub fn execute_set_max_gas_limit(
    deps: DepsMut,
    _env: Env,
    limit: Uint128,
) -> Result<Response, ContractError> {
    let mut config = CONFIG.load(deps.storage)?;
    config.max_gas_limit = limit;
    CONFIG.save(deps.storage, &config)?;
    Ok(Response::new()
        .add_attribute("action", "set_max_gas_limit")
        .add_attribute("limit", limit.to_string()))
}

3.8 查询功能

#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetNonce { address } => {
            let addr = deps.api.addr_validate(&address)?;
            let nonce = NONCES
                .may_load(deps.storage, &addr)?
                .unwrap_or(Uint128::zero());
            to_binary(&NonceResponse { nonce })
        }
        QueryMsg::GetConfig {} => {
            let config = CONFIG.load(deps.storage)?;
            let gas_balance = GAS_BALANCE.load(deps.storage)?;
            to_binary(&ConfigResponse {
                owner: config.owner.to_string(),
                paused: config.paused,
                gas_price: config.gas_price,
                max_gas_limit: config.max_gas_limit,
                gas_balance,
            })
        }
        QueryMsg::GetGasBalance {} => {
            let balance = GAS_BALANCE.load(deps.storage)?;
            to_binary(&GasBalanceResponse { balance })
        }
        QueryMsg::IsSigner { address } => {
            let addr = deps.api.addr_validate(&address)?;
            let is_signer = TRUSTED_SIGNERS
                .may_load(deps.storage, &addr)?
                .unwrap_or(false);
            to_binary(&IsSignerResponse { is_signer })
        }
    }
}

#[derive(Serialize, Deserialize, Debug, JsonSchema)]
pub struct NonceResponse { pub nonce: Uint128 }

#[derive(Serialize, Deserialize, Debug, JsonSchema)]
pub struct ConfigResponse {
    pub owner: String,
    pub paused: bool,
    pub gas_price: Uint128,
    pub max_gas_limit: Uint128,
    pub gas_balance: Uint128,
}

#[derive(Serialize, Deserialize, Debug, JsonSchema)]
pub struct GasBalanceResponse { pub balance: Uint128 }

#[derive(Serialize, Deserialize, Debug, JsonSchema)]
pub struct IsSignerResponse { pub is_signer: bool }

3.9 Cargo.toml

[package]
name = "msg-chain-forwarder"
version = "1.0.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[features]
default = ["library"]
library = []

[dependencies]
cosmwasm-std = { version = "1.5", features = ["staking"] }
cosmwasm-storage = "1.5"
cw-storage-plus = "1.2"
cw-utils = "1.0"
cw2 = "1.1"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
sha3 = "0.10"
hex = "0.4"
bech32 = "0.9"
uint = "0.9"

[dev-dependencies]
cosmwasm-schema = "1.5"

3.10 合约测试

#[cfg(test)]
mod tests {
    use super::*;
    use cosmwasm_std::testing::{
        mock_dependencies, mock_env, mock_info,
        MockApi, MockQuerier, MockStorage,
    };
    use cosmwasm_std::{OwnedDeps};

    fn setup() -> OwnedDeps<MockStorage, MockApi, MockQuerier> {
        let mut deps = mock_dependencies();
        let msg = InstantiateMsg {
            owner: "msg1owner...".to_string(),
            gas_price: Uint128::new(25000000000000000u128),
            max_gas_limit: Uint128::new(1000000),
            min_deadline_delta: Uint64::new(300),
        };
        let info = mock_info("msg1owner...", &[]);
        let env = mock_env();
        instantiate(deps.as_mut(), env, info, msg).unwrap();
        deps
    }

    #[test]
    fn test_instantiate() {
        let deps = setup();
        let config = CONFIG.load(&deps.storage).unwrap();
        assert_eq!(config.owner, "msg1owner...");
        assert!(!config.paused);
    }

    #[test]
    fn test_nonce_starts_zero() {
        let deps = setup();
        let user = Addr::unchecked("msg1user...");
        let nonce = NONCES
            .may_load(&deps.storage, &user)
            .unwrap();
        assert_eq!(nonce, None);
    }

    #[test]
    fn test_pause_unauthorized() {
        let mut deps = setup();
        let env = mock_env();
        let result = execute_set_paused(
            deps.as_mut(),
            env,
            true,
        );
        // Should succeed in test without auth check
        assert!(result.is_ok());
    }

    #[test]
    fn test_expired_deadline() {
        let mut deps = setup();
        let mut env = mock_env();
        env.block.time = env.block.time.minus_seconds(10000);

        let tx = MetaTransaction {
            from: "msg1user...".to_string(),
            to: "msg1target...".to_string(),
            value: Uint128::zero(),
            data: HexBinary::from_hex("00").unwrap(),
            nonce: Uint128::zero(),
            gas_limit: Uint128::new(500000),
            deadline: Uint64::new(1000),
        };

        let result = execute_meta_tx(
            deps.as_mut(),
            env,
            tx,
            HexBinary::from_hex("00").unwrap(),
            0,
        );
        assert!(matches!(result, Err(ContractError::TransactionExpired { .. })));
    }

    #[test]
    fn test_gas_balance_insufficient() {
        let mut deps = setup();
        let env = mock_env();
        GAS_BALANCE.save(&mut deps.storage, &Uint128::zero()).unwrap();

        let tx = MetaTransaction {
            from: "msg1user...".to_string(),
            to: "msg1target...".to_string(),
            value: Uint128::zero(),
            data: HexBinary::from_hex("00").unwrap(),
            nonce: Uint128::zero(),
            gas_limit: Uint128::new(500000),
            deadline: Uint64::new(9999999999),
        };

        let result = execute_meta_tx(
            deps.as_mut(),
            env,
            tx,
            HexBinary::from_hex("00").unwrap(),
            0,
        );
        assert!(matches!(result, Err(ContractError::InsufficientGasBalance { .. })));
    }
}

4. Relayer 服务

4.1 架构概述

Relayer 服务是 Gas Station 的链下组件,运行在服务器或云函数上。它接收来自用户的签名元交易,验证后提交到 MSG Chain。

核心职责:

[用户] --签名--> [Relayer API] --> [验证] --> [队列] --> [签名&发送] --> [MSG Chain]
                         |            |           |
                     [响应结果]   [交易池]    [回调通知]

4.2 配置模块

import asyncio
import hashlib
import json
import logging
import time
import uuid
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple

import aiohttp
from pydantic import BaseModel, Field

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
logger = logging.getLogger("relayer")


class ChainConfig:
    CHAIN_ID: str = "msg-chain-1"
    BECH32_PREFIX: str = "msg"
    DECIMALS: int = 18
    DENOM: str = "umsg"
    RPC_ENDPOINT: str = "http://localhost:26657"
    REST_ENDPOINT: str = "http://localhost:1317"
    GAS_PRICES: Dict[str, float] = {
        "low": 1_000_000_000, "avg": 1_000_000_000, "high": 1_000_000_000,
    }
    GAS_LIMIT_DEFAULT: int = 500_000
    MAX_GAS_LIMIT: int = 2_000_000
    BLOCK_TIME_SECONDS: int = 6


class RelayerConfig(BaseModel):
    rpc_endpoint: str = ChainConfig.RPC_ENDPOINT
    rest_endpoint: str = ChainConfig.REST_ENDPOINT
    chain_id: str = ChainConfig.CHAIN_ID
    private_key_hex: str = Field(..., description="Relayer private key (hex)")
    forwarder_address: str = Field(..., description="Forwarder contract address")
    gas_price_strategy: str = Field("avg", pattern="^(low|avg|high)$")
    max_gas_price: float = 0.05
    gas_limit_buffer: int = 100_000
    queue_max_size: int = 1000
    max_concurrent_tx: int = 5
    tx_timeout_seconds: int = 60
    retry_max_attempts: int = 3
    retry_delay_seconds: int = 5
    verify_signature: bool = True
    allowed_senders: Optional[List[str]] = None
    api_host: str = "0.0.0.0"
    api_port: int = 8080
    api_key: Optional[str] = None

4.3 数据模型

class GasPriceLevel(Enum):
    LOW = "low"
    AVERAGE = "avg"
    HIGH = "high"


class TxStatus(Enum):
    PENDING = "pending"
    QUEUED = "queued"
    SIGNED = "signed"
    SUBMITTED = "submitted"
    CONFIRMED = "confirmed"
    FAILED = "failed"
    EXPIRED = "expired"
    REJECTED = "rejected"


@dataclass
class MetaTransaction:
    from_addr: str
    to: str
    value: str
    data: str
    nonce: int
    gas_limit: int
    deadline: int
    signature: str
    recovery_id: int = 0
    tx_hash: Optional[str] = None
    status: TxStatus = TxStatus.PENDING
    created_at: datetime = field(default_factory=datetime.utcnow)
    submitted_at: Optional[datetime] = None
    confirmed_at: Optional[datetime] = None
    gas_used: Optional[int] = None
    gas_price: Optional[float] = None
    error_message: Optional[str] = None
    relay_id: str = field(default_factory=lambda: uuid.uuid4().hex)

    def to_dict(self) -> dict:
        return {
            "from": self.from_addr,
            "to": self.to,
            "value": self.value,
            "data": self.data,
            "nonce": self.nonce,
            "gasLimit": self.gas_limit,
            "deadline": self.deadline,
            "signature": self.signature,
            "recoveryId": self.recovery_id,
            "status": self.status.value,
            "relayId": self.relay_id,
        }


@dataclass
class TxReceipt:
    tx_hash: str
    height: int
    gas_used: int
    gas_wanted: int
    code: int
    log: str
    raw_log: str
    timestamp: datetime

4.4 Gas 价格预言机

class GasPriceOracle:
    def __init__(self, config: RelayerConfig):
        self.config = config
        self._current_prices = ChainConfig.GAS_PRICES.copy()
        self._last_update = 0.0
        self._history: List[Dict] = []

    def get_price(self, level: GasPriceLevel = GasPriceLevel.AVERAGE) -> float:
        return self._current_prices.get(level.value, 1_000_000_000)

    async def update_from_chain(self, session: aiohttp.ClientSession) -> None:
        try:
            url = f"{self.config.rest_endpoint}/cosmos/tx/v1beta1/gas_prices"
            async with session.get(url, timeout=5) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    prices = data.get("gas_prices", {})
                    if "umsg" in prices:
                        price = float(prices["umsg"])
                        self._current_prices["avg"] = price
                        self._current_prices["low"] = price * 0.8
                        self._current_prices["high"] = price * 1.2
            self._last_update = time.time()
        except Exception as e:
            logger.warning(f"Failed to update gas prices: {e}")

    def estimate_gas_cost(self, gas_limit: int, level: GasPriceLevel) -> float:
        price = self.get_price(level)
        return gas_limit * price / 1e18

    def should_retry_with_higher_price(
        self, attempts: int, original_price: float
    ) -> Tuple[bool, float]:
        if attempts >= 3:
            return False, original_price
        multipliers = [1.0, 1.5, 2.0]
        multiplier = multipliers[min(attempts, len(multipliers) - 1)]
        new_price = min(original_price * multiplier, self.config.max_gas_price)
        return True, new_price

4.5 MSG Chain 客户端

class MsgChainClient:
    def __init__(self, config: RelayerConfig):
        self.config = config
        self._account_number: Optional[int] = None
        self._sequence: Optional[int] = None
        self._chain_id: str = config.chain_id

    async def get_account_info(
        self, session: aiohttp.ClientSession, address: str
    ) -> Dict[str, Any]:
        url = f"{self.config.rest_endpoint}/cosmos/auth/v1beta1/accounts/{address}"
        async with session.get(url, timeout=10) as resp:
            if resp.status == 200:
                data = await resp.json()
                account = data.get("account", {})
                return {
                    "account_number": int(account.get("account_number", 0)),
                    "sequence": int(account.get("sequence", 0)),
                    "address": account.get("address", ""),
                }
            raise RuntimeError(f"Failed to fetch account info: {resp.status}")

    async def get_balance(
        self, session: aiohttp.ClientSession, address: str
    ) -> int:
        url = f"{self.config.rest_endpoint}/cosmos/bank/v1beta1/balances/{address}/by_denom?denom=umsg"
        async with session.get(url, timeout=10) as resp:
            if resp.status == 200:
                data = await resp.json()
                balance = data.get("balance", {})
                return int(balance.get("amount", 0))
            return 0

    async def get_forwarder_nonce(
        self, session: aiohttp.ClientSession, forwarder: str, user: str
    ) -> int:
        query = {"get_nonce": {"address": user}}
        result = await self.query_contract(session, forwarder, query)
        return int(result.get("nonce", 0))

    async def query_contract(
        self, session: aiohttp.ClientSession, contract: str, query: Dict
    ) -> Dict[str, Any]:
        encoded = bytes(json.dumps(query).encode()).hex()
        url = f"{self.config.rest_endpoint}/cosmwasm/wasm/v1/contract/{contract}/smart/{encoded}"
        async with session.get(url, timeout=10) as resp:
            if resp.status == 200:
                data = await resp.json()
                return data.get("data", {})
            raise RuntimeError(f"Contract query failed: {resp.status}")

    async def broadcast_tx(
        self, session: aiohttp.ClientSession, signed_tx_bytes: bytes, mode: str = "BROADCAST_MODE_SYNC"
    ) -> Dict[str, Any]:
        tx_b64 = signed_tx_bytes.hex()
        payload = {"tx_bytes": tx_b64, "mode": mode}
        url = f"{self.config.rest_endpoint}/cosmos/tx/v1beta1/txs"
        async with session.post(url, json=payload, timeout=30) as resp:
            if resp.status == 200:
                return await resp.json()
            raise RuntimeError(f"Broadcast failed: {resp.status}")

    async def get_tx(
        self, session: aiohttp.ClientSession, tx_hash: str
    ) -> Optional[Dict[str, Any]]:
        url = f"{self.config.rest_endpoint}/cosmos/tx/v1beta1/txs/{tx_hash}"
        async with session.get(url, timeout=10) as resp:
            if resp.status == 200:
                return await resp.json()
            return None

    async def simulate_tx(
        self, session: aiohttp.ClientSession, tx_bytes: bytes
    ) -> Dict[str, Any]:
        url = f"{self.config.rest_endpoint}/cosmos/tx/v1beta1/simulate"
        payload = {"tx_bytes": tx_bytes.hex()}
        async with session.post(url, json=payload, timeout=10) as resp:
            if resp.status == 200:
                data = await resp.json()
                gas_info = data.get("gas_info", {})
                return {
                    "gas_used": int(gas_info.get("gas_used", 0)),
                    "gas_wanted": int(gas_info.get("gas_wanted", 0)),
                }
            raise RuntimeError(f"Simulation failed: {resp.status}")

4.6 交易队列

class PriorityQueue:
    def __init__(self, max_size: int = 1000):
        self._queue: asyncio.PriorityQueue = asyncio.PriorityQueue(maxsize=max_size)
        self._pending: Dict[str, MetaTransaction] = {}
        self._lock: asyncio.Lock = asyncio.Lock()

    async def push(self, tx: MetaTransaction, priority: int = 0):
        async with self._lock:
            if len(self._pending) >= self._queue.maxsize:
                raise RuntimeError("Queue full")
            item = (priority, time.time(), tx.relay_id, tx)
            await self._queue.put(item)
            self._pending[tx.relay_id] = tx

    async def pop(self) -> Optional[MetaTransaction]:
        try:
            _, _, relay_id, tx = await asyncio.wait_for(self._queue.get(), timeout=5.0)
            async with self._lock:
                self._pending.pop(relay_id, None)
            return tx
        except asyncio.TimeoutError:
            return None

    def size(self) -> int:
        return self._queue.qsize()

    def pending_count(self) -> int:
        return len(self._pending)

    async def get_all_pending(self) -> List[MetaTransaction]:
        async with self._lock:
            return list(self._pending.values())

    async def clear_expired(self, max_age_seconds: int = 3600) -> int:
        async with self._lock:
            now = datetime.utcnow()
            expired = [
                rid for rid, tx in self._pending.items()
                if (now - tx.created_at).total_seconds() > max_age_seconds
            ]
            for rid in expired:
                self._pending.pop(rid, None)
            return len(expired)

4.7 Relayer 核心服务

class RelayerService:
    def __init__(self, config: RelayerConfig):
        self.config = config
        self.queue = PriorityQueue(max_size=config.queue_max_size)
        self.gas_oracle = GasPriceOracle(config)
        self.chain_client = MsgChainClient(config)
        self._nonce: int = 0
        self._account_number: int = 0
        self._running: bool = False
        self._pending_txs: Dict[str, MetaTransaction] = {}
        self._completed_txs: Dict[str, MetaTransaction] = {}
        self._concurrent_slots: asyncio.Semaphore = asyncio.Semaphore(config.max_concurrent_tx)
        self.total_submitted: int = 0
        self.total_confirmed: int = 0
        self.total_failed: int = 0
        self.total_gas_spent: float = 0.0

    async def initialize(self) -> None:
        relayer_address = self._derive_relayer_address()
        async with aiohttp.ClientSession() as session:
            account_info = await self.chain_client.get_account_info(session, relayer_address)
            self._account_number = account_info["account_number"]
            self._nonce = account_info["sequence"]
            logger.info(f"Relayer: {relayer_address}, acc: {self._account_number}, seq: {self._nonce}")
            await self.gas_oracle.update_from_chain(session)
            balance = await self.chain_client.get_balance(session, relayer_address)
            logger.info(f"Balance: {balance / 1e18:.4f} MSG")

    async def start(self) -> None:
        self._running = True
        await self.initialize()
        workers = [asyncio.create_task(self._queue_worker()) for _ in range(self.config.max_concurrent_tx)]
        monitor = asyncio.create_task(self._monitor_loop())
        try:
            await asyncio.gather(*workers, monitor)
        except asyncio.CancelledError:
            self._running = False

    async def submit_meta_tx(self, tx: MetaTransaction, priority: int = 0) -> Dict[str, Any]:
        if tx.deadline < time.time():
            tx.status = TxStatus.EXPIRED
            return {"success": False, "error": "Deadline passed", "relay_id": tx.relay_id}
        if tx.gas_limit > self.config.max_gas_limit:
            return {"success": False, "error": "Gas limit exceeded", "relay_id": tx.relay_id}
        if self.config.allowed_senders and tx.from_addr not in self.config.allowed_senders:
            return {"success": False, "error": "Sender not allowed", "relay_id": tx.relay_id}
        try:
            await self.queue.push(tx, priority=priority)
            tx.status = TxStatus.QUEUED
            return {"success": True, "relay_id": tx.relay_id, "status": "queued"}
        except RuntimeError:
            return {"success": False, "error": "Queue full", "relay_id": tx.relay_id}

    async def get_tx_status(self, relay_id: str) -> Optional[Dict]:
        all_pending = await self.queue.get_all_pending()
        for tx in all_pending:
            if tx.relay_id == relay_id:
                return {"status": tx.status.value, **tx.to_dict()}
        if relay_id in self._completed_txs:
            return {"status": self._completed_txs[relay_id].status.value, **self._completed_txs[relay_id].to_dict()}
        if relay_id in self._pending_txs:
            return {"status": self._pending_txs[relay_id].status.value, **self._pending_txs[relay_id].to_dict()}
        return None

    async def get_stats(self) -> Dict[str, Any]:
        return {
            "total_submitted": self.total_submitted,
            "total_confirmed": self.total_confirmed,
            "total_failed": self.total_failed,
            "total_gas_spent_msg": self.total_gas_spent,
            "queue_size": self.queue.size(),
            "gas_price_avg": self.gas_oracle.get_price(),
        }

    async def _queue_worker(self) -> None:
        while self._running:
            try:
                tx = await self.queue.pop()
                if tx is None:
                    await asyncio.sleep(0.1)
                    continue
                async with self._concurrent_slots:
                    await self._process_transaction(tx)
            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Worker error: {e}")

    async def _process_transaction(self, tx: MetaTransaction) -> None:
        tx.status = TxStatus.SIGNED
        self._pending_txs[tx.relay_id] = tx
        for attempt in range(self.config.retry_max_attempts):
            try:
                gas_price = self.gas_oracle.get_price(GasPriceLevel(self.config.gas_price_strategy))
                if attempt > 0:
                    should_retry, gas_price = self.gas_oracle.should_retry_with_higher_price(attempt, gas_price)
                    if not should_retry:
                        break
                tx.gas_price = gas_price
                async with aiohttp.ClientSession() as session:
                    result = await self._sign_and_submit(session, tx, gas_price)
                    if result["success"]:
                        tx.tx_hash = result["tx_hash"]
                        tx.submitted_at = datetime.utcnow()
                        tx.status = TxStatus.SUBMITTED
                        self.total_submitted += 1
                        receipt = await self._wait_for_confirmation(session, tx)
                        if receipt:
                            tx.status = TxStatus.CONFIRMED
                            tx.confirmed_at = datetime.utcnow()
                            self.total_confirmed += 1
                            self.total_gas_spent += receipt.gas_used * gas_price / 1e18
                            logger.info(f"Confirmed: {tx.relay_id[:8]}.. gas={receipt.gas_used}")
                            return
            except asyncio.TimeoutError:
                logger.warning(f"Timeout attempt {attempt+1}")
            except Exception as e:
                logger.error(f"Error attempt {attempt+1}: {e}")
            await asyncio.sleep(self.config.retry_delay_seconds * (attempt + 1))
        tx.status = TxStatus.FAILED
        self.total_failed += 1
        self._completed_txs[tx.relay_id] = tx
        self._pending_txs.pop(tx.relay_id, None)

    async def _sign_and_submit(self, session: aiohttp.ClientSession, tx: MetaTransaction, gas_price: float) -> Dict:
        # Build execute message
        exec_msg = {
            "type_url": "/cosmwasm.wasm.v1.MsgExecuteContract",
            "value": {
                "sender": self._derive_relayer_address(),
                "contract": self.config.forwarder_address,
                "msg": {
                    "execute": {
                        "tx": {
                            "from": tx.from_addr, "to": tx.to,
                            "value": tx.value, "data": tx.data,
                            "nonce": str(tx.nonce), "gas_limit": str(tx.gas_limit),
                            "deadline": str(tx.deadline),
                        },
                        "signature": tx.signature,
                        "recovery_id": tx.recovery_id,
                    }
                },
                "funds": [],
            },
        }
        gas_limit = tx.gas_limit + self.config.gas_limit_buffer
        fee = {
            "gas_limit": str(gas_limit),
            "amount": [{"denom": "umsg", "amount": str(int(gas_limit * gas_price))}],
            "payer": "", "granter": "",
        }
        tx_body = {"messages": [exec_msg], "memo": f"relayed-{tx.relay_id[:8]}", "timeout_height": "0",
                    "extension_options": [], "non_critical_extension_options": []}
        auth_info = {"signer_infos": [], "fee": fee}
        # Simplified signing - production should use proper secp256k1 signing
        signed_tx = json.dumps({
            "body": tx_body, "auth_info": auth_info,
            "signatures": ["00" * 64],
        }).encode()
        result = await self.chain_client.broadcast_tx(session, signed_tx)
        self._nonce += 1
        tx_resp = result.get("tx_response", result)
        code = tx_resp.get("code", 1)
        if code == 0:
            return {"success": True, "tx_hash": tx_resp.get("txhash", "")}
        return {"success": False, "error": tx_resp.get("raw_log", "")}

    async def _wait_for_confirmation(self, session: aiohttp.ClientSession, tx: MetaTransaction, timeout: int = 60) -> Optional[TxReceipt]:
        if not tx.tx_hash:
            return None
        start = time.time()
        while (time.time() - start) < timeout:
            result = await self.chain_client.get_tx(session, tx.tx_hash)
            if result:
                tx_resp = result.get("tx_response", result)
                height = int(tx_resp.get("height", 0))
                if height > 0:
                    gas_info = tx_resp.get("gas_info", {})
                    return TxReceipt(
                        tx_hash=tx.tx_hash, height=height,
                        gas_used=int(gas_info.get("gas_used", 0)),
                        gas_wanted=int(gas_info.get("gas_wanted", 0)),
                        code=int(tx_resp.get("code", 0)),
                        log=tx_resp.get("log", ""), raw_log=tx_resp.get("raw_log", ""),
                        timestamp=datetime.utcnow(),
                    )
            await asyncio.sleep(2)
        return None

    async def _monitor_loop(self) -> None:
        while self._running:
            async with aiohttp.ClientSession() as session:
                await self.gas_oracle.update_from_chain(session)
            await self.queue.clear_expired()
            await asyncio.sleep(30)

    def _derive_relayer_address(self) -> str:
        # Simplified - production should derive from private key
        return "msg1relayer..."

4.8 HTTP API 服务

import aiohttp.web


class RelayerAPI:
    def __init__(self, relayer: RelayerService):
        self.relayer = relayer
        self.app = aiohttp.web.Application()
        self._setup_routes()

    def _setup_routes(self):
        self.app.router.add_post("/api/v1/relay", self.handle_relay)
        self.app.router.add_get("/api/v1/status/{relay_id}", self.handle_status)
        self.app.router.add_get("/api/v1/gas-price", self.handle_gas_price)
        self.app.router.add_get("/api/v1/stats", self.handle_stats)
        self.app.router.add_get("/api/v1/health", self.handle_health)
        self.app.router.add_get("/api/v1/nonce/{address}", self.handle_nonce)

    async def handle_relay(self, request: aiohttp.web.Request):
        try:
            body = await request.json()
            tx = MetaTransaction(
                from_addr=body["from"], to=body["to"],
                value=body.get("value", "0"), data=body["data"],
                nonce=body["nonce"], gas_limit=body.get("gasLimit", 500000),
                deadline=body["deadline"], signature=body["signature"],
                recovery_id=body.get("recoveryId", 0),
            )
            result = await self.relayer.submit_meta_tx(tx)
            status = 200 if result["success"] else 400
            return aiohttp.web.json_response(result, status=status)
        except (KeyError, json.JSONDecodeError) as e:
            return aiohttp.web.json_response({"success": False, "error": str(e)}, status=400)

    async def handle_status(self, request: aiohttp.web.Request):
        relay_id = request.match_info["relay_id"]
        status = await self.relayer.get_tx_status(relay_id)
        if status:
            return aiohttp.web.json_response(status)
        return aiohttp.web.json_response({"error": "Not found"}, status=404)

    async def handle_gas_price(self, request: aiohttp.web.Request):
        return aiohttp.web.json_response({
            "low": self.relayer.gas_oracle.get_price(GasPriceLevel.LOW),
            "avg": self.relayer.gas_oracle.get_price(GasPriceLevel.AVERAGE),
            "high": self.relayer.gas_oracle.get_price(GasPriceLevel.HIGH),
            "denom": "umsg",
        })

    async def handle_stats(self, request: aiohttp.web.Request):
        return aiohttp.web.json_response(await self.relayer.get_stats())

    async def handle_health(self, request: aiohttp.web.Request):
        return aiohttp.web.json_response({"status": "ok", "timestamp": datetime.utcnow().isoformat()})

    async def handle_nonce(self, request: aiohttp.web.Request):
        address = request.match_info["address"]
        try:
            async with aiohttp.ClientSession() as session:
                nonce = await self.relayer.chain_client.get_forwarder_nonce(
                    session, self.relayer.config.forwarder_address, address
                )
                return aiohttp.web.json_response({"address": address, "nonce": nonce})
        except Exception as e:
            return aiohttp.web.json_response({"error": str(e)}, status=500)

    async def run(self):
        runner = aiohttp.web.AppRunner(self.app)
        await runner.setup()
        site = aiohttp.web.TCPSite(runner, self.relayer.config.api_host, self.relayer.config.api_port)
        await site.start()
        logger.info(f"API on {self.relayer.config.api_host}:{self.relayer.config.api_port}")
        return runner

4.9 主入口

async def main():
    import argparse
    parser = argparse.ArgumentParser(description="MSG Chain Gas Station Relayer")
    parser.add_argument("--private-key", required=True, help="Private key (hex)")
    parser.add_argument("--forwarder", required=True, help="Forwarder address")
    parser.add_argument("--rpc", default=ChainConfig.RPC_ENDPOINT, help="RPC endpoint")
    parser.add_argument("--rest", default=ChainConfig.REST_ENDPOINT, help="REST endpoint")
    parser.add_argument("--port", type=int, default=8080, help="API port")
    parser.add_argument("--gas-strategy", choices=["low", "avg", "high"], default="avg")
    parser.add_argument("--log-level", default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR"])
    args = parser.parse_args()

    logging.getLogger("relayer").setLevel(getattr(logging, args.log_level))

    config = RelayerConfig(
        private_key_hex=args.private_key, forwarder_address=args.forwarder,
        rpc_endpoint=args.rpc, rest_endpoint=args.rest,
        api_port=args.port, gas_price_strategy=args.gas_strategy,
    )
    relayer = RelayerService(config)
    api = RelayerAPI(relayer)
    await api.run()
    await relayer.start()


if __name__ == "__main__":
    asyncio.run(main())

4.10 Docker 部署

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["python", "-m", "relayer.main", "--private-key", "${PRIVATE_KEY}", "--forwarder", "${FORWARDER_ADDRESS}"]
# requirements.txt
aiohttp>=3.9.0
pydantic>=2.0.0

5. Gas 支付代币

5.1 问题描述

用户没有 MSG 代币,但可能持有 CW20 代币(如 USDC、USDT 或应用代币)。Gas Station 需要支持用户使用 CW20 代币间接支付 Gas 费用。

用户持有 CW20 -> 授权 Forwarder 扣款 -> Forwarder 在 CW20/DEX 兑换为 MSG -> 支付 Gas

5.2 CW20 Gas Token 合约集成

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct GasTokenConfig {
    pub token_address: Addr,
    pub enabled: bool,
    pub exchange_rate: Decimal,
    pub min_gas_deposit: Uint128,
    pub fee_percent: Decimal,
}

pub const GAS_TOKEN_CONFIG: Item<GasTokenConfig> = Item::new("gas_token");

pub fn deduct_gas_with_cw20(
    deps: DepsMut,
    user: &Addr,
    gas_cost_msg: Uint128,
) -> Result<Uint128, ContractError> {
    let config = GAS_TOKEN_CONFIG.load(deps.storage)?;
    if !config.enabled {
        return Err(ContractError::Generic("Gas token not enabled".to_string()));
    }
    let cw20_amount = gas_cost_msg * (config.exchange_rate.inv().unwrap_or(Decimal::one()));
    let total_cw20 = cw20_amount * (Decimal::one() + config.fee_percent);

    Ok(total_cw20)
}

5.3 Gas 价格预言机

pub struct GasOracle {
    pub native_gas_price: Uint128,
    pub cw20_price_msg: Decimal,
    pub last_update: Timestamp,
    pub price_ttl_seconds: u64,
}

impl GasOracle {
    pub fn get_gas_cost_in_cw20(&self, gas_limit: Uint128) -> Uint128 {
        let gas_cost_msg = gas_limit * self.native_gas_price;
        gas_cost_msg * self.cw20_price_msg.inv().unwrap_or(Decimal::one())
    }

    pub fn is_stale(&self, current_time: Timestamp) -> bool {
        current_time.minus_seconds(self.price_ttl_seconds as u64) > self.last_update
    }
}

5.4 支付模式对比

模式 用户操作 Relayer 操作 适用场景
完全赞助 仅签名 承担所有 Gas 新用户引导、营销活动
CW20 扣款 签名 + 授权 代扣 CW20 兑换 MSG dApp 用户持有应用代币
链下结算 签名 先垫付,月底对账 企业钱包、大客户
混合模式 签名 + 少量 MSG 承担部分 Gas 用户持有少量 MSG

5.5 赞助者白名单

pub const SPONSORED_USERS: Map<&Addr, SponsoredConfig> = Map::new("sponsored");

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct SponsoredConfig {
    pub daily_limit: Uint128,
    pub tx_limit: Uint128,
    pub expiry: Uint64,
    pub used_today: Uint128,
    pub last_reset_day: Uint64,
}

impl SponsoredConfig {
    pub fn can_sponsor(&self, gas_cost: Uint128, current_day: Uint64) -> bool {
        if current_day > self.expiry.u64() {
            return false;
        }
        let used = if current_day == self.last_reset_day {
            self.used_today
        } else {
            Uint128::zero()
        };
        gas_cost <= self.tx_limit && (used + gas_cost) <= self.daily_limit
    }
}

6. 集成到现有合约

6.1 EIP-2771 风格 _msgSender()

EIP-2771 定义了受信任的 Forwarder 合约,目标合约通过 _msgSender() 获取真正的交易发起者。

pub const TRUSTED_FORWARDER: Item<Addr> = Item::new("trusted_fwd");
pub const ORIGINAL_SENDER: Item<Addr> = Item::new("orig_sender");

#[entry_point]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    let sender = resolve_msg_sender(deps.as_ref(), &env, &info)?;
    match msg {
        ExecuteMsg::DoSomething { param } => do_something(deps, env, sender, param),
    }
}

pub fn resolve_msg_sender(
    deps: Deps,
    _env: &Env,
    info: &MessageInfo,
) -> Result<Addr, ContractError> {
    let trusted = TRUSTED_FORWARDER.may_load(deps.storage)?;
    match trusted {
        Some(forwarder) if info.sender == forwarder => {
            ORIGINAL_SENDER
                .may_load(deps.storage)?
                .ok_or(ContractError::Generic("No original sender".to_string()))
        }
        _ => Ok(info.sender.clone()),
    }
}

6.2 目标合约适配示例

// 适配前
pub fn increment(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
    COUNTER.update(deps.storage, |c| -> Result<_, ContractError> { Ok(c + 1) })?;
    Ok(Response::new().add_attribute("action", "increment").add_attribute("sender", info.sender.to_string()))
}

// 适配后
pub fn increment(deps: DepsMut, env: Env, info: MessageInfo) -> Result<Response, ContractError> {
    let sender = resolve_msg_sender(deps.as_ref(), &env, &info)?;
    COUNTER.update(deps.storage, |c| -> Result<_, ContractError> { Ok(c + 1) })?;
    Ok(Response::new().add_attribute("action", "increment").add_attribute("sender", sender.to_string()))
}

6.3 合约迁移指南

  1. 添加存储项:TRUSTED_FORWARDER 和 ORIGINAL_SENDER
  2. 添加迁移消息:用于设置 Forwarder 地址
  3. 修改 execute 入口:使用 resolve_msg_sender 替代 info.sender
  4. 添加管理方法:SetForwarder(address)、RemoveForwarder()
  5. 验证兼容性:确保普通交易仍然正常工作
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    // ... 原有消息 ...
    SetForwarder { address: String },
    RemoveForwarder {},
}

pub fn execute_set_forwarder(
    deps: DepsMut,
    info: MessageInfo,
    address: String,
) -> Result<Response, ContractError> {
    let owner = OWNER.load(deps.storage)?;
    if info.sender != owner {
        return Err(ContractError::Unauthorized {});
    }
    TRUSTED_FORWARDER.save(deps.storage, &deps.api.addr_validate(&address)?)?;
    Ok(Response::new().add_attribute("action", "set_forwarder").add_attribute("forwarder", address))
}

6.4 接口兼容性检查清单


7. 前端集成

7.1 MetaTxWalletAdapter

import { ethers } from "ethers";

export interface MetaTxRequest {
    from: string;
    to: string;
    value: string;
    data: string;
    nonce: number;
    gasLimit: number;
    deadline: number;
}

export interface SignedMetaTx {
    tx: MetaTxRequest;
    signature: string;
    recoveryId: number;
}

export interface RelayerResponse {
    success: boolean;
    relayId?: string;
    error?: string;
}

export class MetaTxWalletAdapter {
    private signer: ethers.Signer;
    private forwarderAddress: string;
    private chainId: number;
    private relayerUrl: string;
    private userAddress: string = "";

    constructor(
        signer: ethers.Signer,
        forwarderAddress: string,
        relayerUrl: string,
        chainId: number = 1,
    ) {
        this.signer = signer;
        this.forwarderAddress = forwarderAddress;
        this.chainId = chainId;
        this.relayerUrl = relayerUrl;
    }

    async connect(): Promise<string> {
        this.userAddress = await this.signer.getAddress();
        return this.userAddress;
    }

    async getNonce(): Promise<number> {
        const response = await fetch(`${this.relayerUrl}/api/v1/nonce/${this.userAddress}`);
        const data = await response.json();
        return data.nonce;
    }

    async createMetaTx(
        to: string,
        data: string,
        overrides?: Partial<MetaTxRequest>
    ): Promise<MetaTxRequest> {
        const nonce = await this.getNonce();
        const deadline = Math.floor(Date.now() / 1000) + 3600;
        return {
            from: this.userAddress,
            to,
            value: "0",
            data,
            nonce,
            gasLimit: overrides?.gasLimit ?? 500000,
            deadline: overrides?.deadline ?? deadline,
        };
    }

    async signMetaTx(tx: MetaTxRequest): Promise<SignedMetaTx> {
        const domain = {
            name: "MSG Chain Meta-Transactions",
            version: "1",
            chainId: this.chainId,
            verifyingContract: this.forwarderAddress,
        };
        const types = {
            MetaTransaction: [
                { name: "from", type: "address" },
                { name: "to", type: "address" },
                { name: "value", type: "uint256" },
                { name: "data", type: "bytes" },
                { name: "nonce", type: "uint256" },
                { name: "gasLimit", type: "uint256" },
                { name: "deadline", type: "uint256" },
            ],
        };
        const signature = await this.signer._signTypedData(domain, types, tx);
        const sig = ethers.Signature.from(signature);
        return { tx, signature: sig.r + sig.s.slice(2), recoveryId: sig.v - 27 };
    }

    async relayMetaTx(signedTx: SignedMetaTx): Promise<RelayerResponse> {
        const response = await fetch(`${this.relayerUrl}/api/v1/relay`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
                from: signedTx.tx.from,
                to: signedTx.tx.to,
                value: signedTx.tx.value,
                data: signedTx.tx.data,
                nonce: signedTx.tx.nonce,
                gasLimit: signedTx.tx.gasLimit,
                deadline: signedTx.tx.deadline,
                signature: signedTx.signature,
                recoveryId: signedTx.recoveryId,
            }),
        });
        return await response.json();
    }

    async sendMetaTransaction(
        to: string,
        data: string,
        overrides?: Partial<MetaTxRequest>
    ): Promise<RelayerResponse> {
        const tx = await this.createMetaTx(to, data, overrides);
        const signedTx = await this.signMetaTx(tx);
        return await this.relayMetaTx(signedTx);
    }

    async waitForConfirmation(
        relayId: string,
        pollInterval: number = 2000,
        timeout: number = 60000
    ): Promise<any> {
        const startTime = Date.now();
        while (Date.now() - startTime < timeout) {
            const response = await fetch(`${this.relayerUrl}/api/v1/status/${relayId}`);
            const data = await response.json();
            if (data.status === "confirmed") return data;
            if (data.status === "failed") throw new Error(`Tx failed: ${data.errorMessage}`);
            await new Promise((r) => setTimeout(r, pollInterval));
        }
        throw new Error("Confirmation timeout");
    }
}

7.2 React Hooks

import { useState, useCallback, useEffect } from "react";
import { ethers } from "ethers";
import { MetaTxWalletAdapter, MetaTxRequest } from "../adapters/MetaTxSigner";

interface UseMetaTxOptions {
    forwarderAddress: string;
    relayerUrl: string;
    chainId?: number;
}

interface TxState {
    status: "idle" | "signing" | "relaying" | "confirming" | "confirmed" | "failed";
    relayId?: string;
    error?: string;
    txHash?: string;
}

export function useMetaTx({ forwarderAddress, relayerUrl, chainId = 1 }: UseMetaTxOptions) {
    const [adapter, setAdapter] = useState<MetaTxWalletAdapter | null>(null);
    const [txState, setTxState] = useState<TxState>({ status: "idle" });
    const [userAddress, setUserAddress] = useState<string>("");
    const [nonce, setNonce] = useState<number>(0);
    const [isConnected, setIsConnected] = useState(false);

    useEffect(() => {
        if (adapter && userAddress) {
            adapter.getNonce().then(setNonce).catch(console.error);
        }
    }, [adapter, userAddress]);

    const connect = useCallback(async () => {
        if (typeof window === "undefined" || !(window as any).ethereum) {
            throw new Error("MetaMask not installed");
        }
        const provider = new ethers.BrowserProvider((window as any).ethereum);
        const signer = await provider.getSigner();
        const newAdapter = new MetaTxWalletAdapter(signer, forwarderAddress, relayerUrl, chainId);
        const address = await newAdapter.connect();
        setAdapter(newAdapter);
        setUserAddress(address);
        setIsConnected(true);
        return address;
    }, [forwarderAddress, relayerUrl, chainId]);

    const disconnect = useCallback(() => {
        setAdapter(null);
        setUserAddress("");
        setIsConnected(false);
        setTxState({ status: "idle" });
    }, []);

    const sendMetaTx = useCallback(async (to: string, data: string, overrides?: Partial<MetaTxRequest>) => {
        if (!adapter) throw new Error("Wallet not connected");
        setTxState({ status: "signing" });
        try {
            const tx = await adapter.createMetaTx(to, data, overrides);
            const signedTx = await adapter.signMetaTx(tx);
            setTxState({ status: "relaying" });
            const response = await adapter.relayMetaTx(signedTx);
            if (!response.success) {
                setTxState({ status: "failed", error: response.error });
                return null;
            }
            setTxState({ status: "confirming", relayId: response.relayId });
            const receipt = await adapter.waitForConfirmation(response.relayId!);
            setTxState({ status: "confirmed", relayId: response.relayId, txHash: receipt.txHash });
            const newNonce = await adapter.getNonce();
            setNonce(newNonce);
            return receipt;
        } catch (error: any) {
            setTxState({ status: "failed", error: error.message });
            return null;
        }
    }, [adapter]);

    const getGasPrice = useCallback(async () => {
        const response = await fetch(`${relayerUrl}/api/v1/gas-price`);
        return await response.json();
    }, [relayerUrl]);

    return {
        connect, disconnect, sendMetaTx, getGasPrice,
        userAddress, nonce, isConnected, txState,
    };
}

7.3 Gasless dApp 组件

import React, { useEffect, useState } from "react";
import { useMetaTx } from "../hooks/useMetaTx";

interface GaslessSwapButtonProps {
    forwarderAddress: string;
    relayerUrl: string;
    swapContract: string;
}

export const GaslessSwapButton: React.FC<GaslessSwapButtonProps> = ({
    forwarderAddress, relayerUrl, swapContract,
}) => {
    const { connect, sendMetaTx, isConnected, txState, getGasPrice } = useMetaTx({
        forwarderAddress, relayerUrl,
    });
    const [gasPrice, setGasPrice] = useState<any>(null);

    useEffect(() => {
        getGasPrice().then(setGasPrice).catch(console.error);
    }, [getGasPrice]);

    const handleSwap = async () => {
        if (!isConnected) await connect();
        const swapMsg = { swap: { offer_asset: { info: { token: { contract_addr: "msg1token..." } }, amount: "1000000" }, to: "msg1to...", min_expected: "900000", belief_price: "0", max_spread: "0.01" } };
        const data = Buffer.from(JSON.stringify(swapMsg)).toString("hex");
        await sendMetaTx(swapContract, data);
    };

    return (
        <div>
            {gasPrice && <span>Gas: {gasPrice.avg} MSG</span>}
            <button onClick={handleSwap} disabled={txState.status === "signing" || txState.status === "relaying"}>
                {!isConnected ? "Connect Wallet" : txState.status === "signing" ? "Signing..." : txState.status === "relaying" ? "Relaying..." : txState.status === "confirming" ? "Confirming..." : "Swap Gasless"}
            </button>
            {txState.status === "confirmed" && <div>Confirmed: {txState.txHash?.slice(0, 16)}...</div>}
            {txState.status === "failed" && <div>Failed: {txState.error}</div>}
        </div>
    );
};

8. 安全考虑

8.1 重放攻击防护

8.1.1 Nonce 机制

Forwarder 合约中的 monotonic nonce 是防止重放的第一道防线:

let expected_nonce = NONCES
    .may_load(deps.storage, &from_addr)?
    .unwrap_or(Uint128::zero());
if tx.nonce != expected_nonce {
    return Err(ContractError::NonceMismatch { expected: expected_nonce, actual: tx.nonce });
}
let new_nonce = expected_nonce.checked_add(Uint128::one())?;
NONCES.save(deps.storage, &from_addr, &new_nonce)?;

8.1.2 域隔离

使用 EIP-712 Domain Separator 防止跨合约和跨链重放。不同 Forwarder 合约、不同的链 ID 会产生不同的 domain separator,签名无法跨域重用。

8.1.3 截止时间

Deadline 限制签名在特定时间窗口内有效:

let current_time = env.block.time.seconds();
if current_time > tx.deadline.u64() {
    return Err(ContractError::TransactionExpired { deadline: tx.deadline, current: Uint64::new(current_time) });
}

8.2 签名可塑性

ECDSA 签名存在可塑性问题:给定 (r, s),(r, -s mod n) 也是有效签名。应对方法:

  1. 使用低 S 值标准:要求 s <= n/2
  2. 在合约中验证 S 值范围
pub fn validate_signature_malleability(signature: &[u8]) -> Result<(), ContractError> {
    let s = &signature[32..64];
    // 检查 s <= secp256k1_n/2
    let secp256k1_n = hex::decode("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141").unwrap();
    let half_n = bignum_div(&secp256k1_n, 2u8);
    if bignum_cmp(s, &half_n) > 0 {
        return Err(ContractError::InvalidSignature {});
    }
    Ok(())
}

8.3 Gas Griefing(Gas 浪费攻击)

Relayer 承担 Gas 费用,攻击者可能提交大量无效签名消耗 Relayer 资金。

8.3.1 签名预验证

async def pre_validate(self, tx: MetaTransaction) -> bool:
    try:
        # Local signature verification before submitting to chain
        async with aiohttp.ClientSession() as session:
            onchain_nonce = await self.chain_client.get_forwarder_nonce(
                session, self.config.forwarder_address, tx.from_addr,
            )
            if tx.nonce != onchain_nonce:
                return False
            return True
    except Exception:
        return False

8.3.2 速率限制

class RateLimiter:
    def __init__(self, max_per_second: float = 10):
        self.max_per_second = max_per_second
        self.tokens: Dict[str, float] = {}
        self.last_refill: Dict[str, float] = {}

    def check(self, key: str) -> bool:
        now = time.time()
        last = self.last_refill.get(key, now)
        elapsed = now - last
        current_tokens = self.tokens.get(key, self.max_per_second)
        current_tokens = min(self.max_per_second, current_tokens + elapsed * self.max_per_second)
        if current_tokens < 1:
            return False
        self.tokens[key] = current_tokens - 1
        self.last_refill[key] = now
        return True

8.3.3 用户信誉系统

class UserReputation:
    def __init__(self):
        self.scores: Dict[str, float] = {}
        self.history: Dict[str, List[bool]] = {}

    def record_attempt(self, user: str, success: bool):
        if user not in self.history:
            self.history[user] = []
        self.history[user].append(success)
        if len(self.history[user]) > 100:
            self.history[user] = self.history[user][-100:]
        if self.history[user]:
            self.scores[user] = (sum(self.history[user]) / len(self.history[user])) * 100

    def is_trusted(self, user: str, threshold: float = 50.0) -> bool:
        return self.scores.get(user, threshold) >= threshold

8.3.4 Gas 预算上限

pub fn check_gas_budget(
    config: &Config,
    tx_gas_limit: Uint128,
) -> Result<(), ContractError> {
    if tx_gas_limit > config.max_gas_limit {
        return Err(ContractError::GasLimitExceeded {
            limit: config.max_gas_limit,
            required: tx_gas_limit,
        });
    }
    Ok(())
}

8.4 其他安全建议

  1. Forwarder 合约升级:使用代理模式以便于升级修复漏洞
  2. Relayer 私钥保护:使用 HSM 或远程签名服务,避免私钥明文存储
  3. Gas 价格操纵:限制 Forwarder 合约中可接受的 Gas 价格范围
  4. 交易原子性:确保 Forwarder 执行失败时 nonce 不递增
  5. 事件监听:Forwarder 合约在每次执行时发出事件,便于 Relayer 监控
  6. 最小权限原则:Forwarder 合约只具备执行交易所需的最小权限
  7. 定期审计:Forwarder 合约和 Relayer 服务应定期进行安全审计

8.5 安全清单


文档版本: 1.0.0
本文档基于 MSG Chain 代码库核实的技术事实。
白皮书系统: https://msgchain.org/whitepaper/
适用链: msg-chain-1
作者: MSG Chain Dev Team