基于智能合约实现非托管支付
背景
随着区块链技术的普及,加密货币从单一的投资标的逐步延伸至支付场景,成为跨境贸易、数字服务、NFT 交易等领域的重要结算工具。然而,加密支付的核心矛盾始终聚焦于 “资金控制权” 与 “使用便捷性” 的平衡 —— 这一矛盾直接催生了两大主流模式:托管式加密支付与非托管式加密支付。二者基于不同的信任逻辑与技术路径,分别服务于不同用户群体的需求,共同构成了加密支付生态的核心骨架。
托管式的实现方式
实现流程
流程说明
- 用托管服务维护一个地址池
- 每生成一个订单,分配一个唯一地址,订单完成后回收地址
- 结算时归集地址池中的余额,再打给商家
优点
- 技术门槛稍低,地址可依赖托管平台管理
- 无合约风险,资金安全问题交给托管平台
缺点
- 结算过程存在归集操作,归集又依赖手续支付,流程较长,无法实时结算
- 地址占用,地址回收,支付过期,订单支付完成等状态处理存在复杂的业务逻辑,易出bug
- 需要托管商户资金,可能存在合规上的风险
非托管的实现方式
实现流程
流程说明
- 部署合约工厂
- 商户发起支付请求
- 调用合约工厂预测支付地址,实际是合约地址
- 用户扫码付款,监控链上入金
- 向支付地址上部署转账合约
- 调用转账合约发起归集
优点
- 可灵活的创建地址,不依赖托管平台
- 可直接链上结算,结算速度取决于链上速度
- 支持非稳定币的支付也较容易集成
缺点
- 存在私钥管理上的问题
- 部分业务依赖合约代码,对技术要求较高
- 无法实现无GAS费模式支付
核心代码
工厂合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;import "./Vault.sol";contract Create2Factory {event Deployed(address vault,bytes32 salt,address merchant,address payout);function deployVault(bytes32 salt,address merchant,address payout) external returns (address vaultAddr) {bytes memory bytecode = abi.encodePacked(type(Vault).creationCode,abi.encode(merchant, payout));assembly {vaultAddr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)if iszero(vaultAddr) {revert(0, 0)}}emit Deployed(vaultAddr, salt, merchant, payout);}function predict(address merchant,address payout,bytes32 salt) external view returns (address) {bytes memory bytecode = abi.encodePacked(type(Vault).creationCode,abi.encode(merchant, payout));bytes32 codeHash = keccak256(bytecode);returnaddress(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff),address(this),salt,codeHash)))));}
}
转账合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;interface IERC20 {function balanceOf(address) external view returns (uint256);function transfer(address, uint256) external returns (bool);
}contract Vault {address public immutable merchant;address public immutable payout;address public immutable factory;constructor(address _merchant, address _payout) payable {merchant = _merchant;payout = _payout;factory = msg.sender;}receive() external payable {}modifier onlyMerchant() {require(msg.sender == merchant, "not merchant");_;}function sweepETH(uint256 amount) external onlyMerchant {uint256 bal = address(this).balance;require(amount <= bal, "insufficient");(bool ok, ) = payable(payout).call{value: amount}("");require(ok, "eth transfer failed");}function sweepERC20(address token, uint256 amount) external onlyMerchant {require(IERC20(token).transfer(payout, amount),"erc20 transfer failed");}function sweepAll(address[] calldata tokens) external onlyMerchant {uint256 bal = address(this).balance;if (bal > 0) {(bool ok, ) = payable(payout).call{value: bal}("");require(ok, "eth transfer failed");}for (uint256 i = 0; i < tokens.length; i++) {uint256 tb = IERC20(tokens[i]).balanceOf(address(this));if (tb > 0)require(IERC20(tokens[i]).transfer(payout, tb),"erc20 transfer failed");}}
}
转账方法
package com.jinchi.service;import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.http.HttpService;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.RawTransaction;
import org.web3j.crypto.TransactionEncoder;
import org.web3j.utils.Numeric;
import org.web3j.tx.gas.DefaultGasProvider;
import org.web3j.tx.RawTransactionManager;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.Address;
import org.web3j.abi.datatypes.Type;import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;public class SweepERC20Example {public static void main(String[] args) throws Exception {// 1. 连接 RPC 节点Web3j web3 = Web3j.build(new HttpService("https://sepolia.infura.io/v3/fe34061738ec4907b9e6f75069fe6150"));// 2. 服务钱包String privateKey = "f6539a36c4d2a6a8ac821a9a47047ef68a97f70eab32c4b651919c9325012f88"; // ⚠️ 切勿明文保存在生产环境Credentials credentials = Credentials.create(privateKey);// 3. Vault 合约地址String vaultAddress = "0x399cdd9092f57244E0d5378fFBc42125595Edb0D";// 4. 需要 sweep 的 ERC20 代币地址(例如 USDT)String tokenAddress = "0x419Fe9f14Ff3aA22e46ff1d03a73EdF3b70A62ED";// 5. 要转账的数量:0.1 个代币BigDecimal tokenAmount = new BigDecimal("0.1"); // 用 BigDecimal 避免浮点数精度问题// 6. 转换为最小单位:0.1 × 10^6 = 100000BigInteger amountInWei = tokenAmount.multiply(new BigDecimal("10").pow(6)) // 乘以 10^6,精度为6.toBigInteger();// 7. 构造合约函数调用数据Function function = new Function("sweepERC20",Arrays.asList(new Address(tokenAddress), new Uint256(amountInWei)), // 参数Arrays.asList() // 无返回值);String encodedFunction = FunctionEncoder.encode(function);// 8. 获取链上的 nonce(交易计数)BigInteger nonce = web3.ethGetTransactionCount(credentials.getAddress(),org.web3j.protocol.core.DefaultBlockParameterName.LATEST).send().getTransactionCount();// 9. 预估 Gas(也可以设置固定 GasLimit)BigInteger gasPrice = web3.ethGasPrice().send().getGasPrice();BigInteger gasLimit = DefaultGasProvider.GAS_LIMIT; // 默认 21000,可根据实际情况调整// 10. 构造交易RawTransaction rawTransaction = RawTransaction.createTransaction(nonce,gasPrice,gasLimit,vaultAddress,BigInteger.ZERO, // sweepERC20 不需要转 ETHencodedFunction);// 11. 签名交易byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);String hexValue = Numeric.toHexString(signedMessage);// 12. 发送交易String txHash = web3.ethSendRawTransaction(hexValue).send().getTransactionHash();System.out.println("SweepERC20 tx sent: " + txHash);}
}
核心方法
keccak256
基于 Keccak-256 算法实现,主要用于将任意输入数据转换为一个固定长度(256 位,即 32 字节)的哈希值,在通过 CREATE2
opcode 部署合约时,keccak256
是计算预部署地址的核心。
address = keccak256(abi.encodePacked(bytes1(0xff), // 固定前缀deployerAddress, // 部署者(工厂合约)地址salt, // 盐值(32字节,自定义随机数)keccak256(bytecode) // 被部署合约的字节码哈希)
)
关键特点
- 确定性:相同输入永远产生相同哈希值(如
keccak256("hello")
结果固定); - 不可逆性:无法从哈希值反推原始输入;
- 雪崩效应:输入的微小变化(如多一个空格)会导致哈希值完全不同;
- 高效性:在 EVM 中执行成本低(gas 消耗较少)。
主要用途
- 生成唯一标识(哈希值);
- 构建映射(Mapping)的键;
- Create2 地址预测;
- 伪随机数生成(有限场景)。
create2
CREATE2
是以太坊虚拟机(EVM)提供的一个 opcode,用于确定性部署智能合约,即可以在合约部署前预先计算出它的地址。与传统的 CREATE
opcode 相比,CREATE2
最大的优势是部署地址不依赖部署顺序(nonce),仅由特定参数决定,这使得它在需要提前知道合约地址的场景中非常有用。
// 工厂合约中用 CREATE2 部署子合约
function deployWithCreate2(bytes32 salt, address param1) external returns (address newContract) {// 1. 准备被部署合约的字节码(包含构造函数参数)bytes memory bytecode = abi.encodePacked(type(MyContract).creationCode, // 合约的创建代码abi.encode(param1) // 构造函数参数);// 2. 内联汇编调用 CREATE2assembly {// 参数:// 0: 部署时发送的 ETH 数量(这里为 0)// add(bytecode, 0x20): 字节码的起始位置(跳过长度前缀)// mload(bytecode): 字节码的长度// salt: 盐值newContract := create2(0, add(bytecode, 0x20), mload(bytecode), salt)// 检查部署是否成功if iszero(newContract) {revert(0, 0) // 部署失败则回滚}}
}
关键特点
- 地址可预测:部署地址由 4 个参数唯一确定,提前计算后可在部署前就知道最终地址;
- 与 nonce 无关:传统
CREATE
的地址依赖部署者的 nonce(交易次数),而CREATE2
不依赖,避免因 nonce 变化导致地址改变; - 支持 “预部署” 场景:可以先告知用户地址,让用户向该地址转账,之后再部署合约处理资金。
主要用途
- 支付通道:预先生成收款地址,用户转账后再部署合约处理资金(如你的
Vault
模式)。 - 合约升级:通过固定地址的代理合约,结合
CREATE2
部署新实现合约,确保地址不变。 - 链下交易:提前生成合约地址,在链下协商后再上链部署,避免地址变化导致的问题。
- 批量部署:通过不同
salt
批量生成唯一地址,用于需要大量独立合约的场景(如 NFT 钱包)
实验步骤
部署工厂合约
- 部署地址:0xcc35b9E418b91F83bbFB1c614959E111ca87B008
- 合约地址:0x399cdd9092f57244E0d5378fFBc42125595Edb0D
计算收款地址(合约地址)
- 调用地址:0xcc35b9E418b91F83bbFB1c614959E111ca87B008
- 商户地址:0x8643Ae26DD2Eac2240808824B47368faD8b75F05
- 结算地址:0xb18131733b533Ed553C52AA8E8E5d6875d7D4f9D
- 收款地址(合约地址):0x399cdd9092f57244E0d5378fFBc42125595Edb0D
用户支付
区块链转账ERC20
部署转账合约
- 调用地址:0xcc35b9E418b91F83bbFB1c614959E111ca87B008
- 商户地址:0x8643Ae26DD2Eac2240808824B47368faD8b75F05
- 结算地址:0xb18131733b533Ed553C52AA8E8E5d6875d7D4f9D
- 合约地址:0x399cdd9092f57244E0d5378fFBc42125595Edb0D
发起转账
转账由后端Java代码发起,调用转账合约的sweepERC20方法,需要用到Merchant地址的私钥进行签名,由ERC20代币由转账合约转移至结算地址,GAS费由Merchant地址支付,实际生产中可灵活处理。
链上支付未来
基于稳定币链或者智能合约钱包实现真正的无GAS支付,将和传统支付一样丝滑
稳定币公链的发展
Stable
最近Tether和Bitfinex联合开发Layer1公链Stable,以USDT作为原生gas,支持免费的点对点USDT转账,应用于支付领域,目前内测阶段,Stable新特性:
- 原生USDT作为Gas费
- EVM兼容
- FX引擎
Arc
Circle旗下科技公司最近推出Arc公链,专为稳定币金融打造,目前内测阶段,Arc新特性:
- USDC作为原生Gas
- 内置外汇引擎
- 兼容EVM
ERC-4337基础设施的发展
使用 账户抽象 (ERC-4337) 的智能合约钱包创建了一种通过智能合约管理的钱包,而不是像 EOA 钱包(外部拥有地址)那样由单个私钥管理的钱包。
智能合约钱包的可编程性允许开发范围广泛的新用例。通过降低复杂性而不影响安全性或匿名性,智能合约钱包将帮助促进下一波区块链用户的入场。
智能合约钱包交易的典型流程是:
- 用户希望执行一个 UserOperation
- UserOperations 被发送到一个“替代内存池”
- 一个具有 EOA 钱包的 打包器 将所有 UserOperations 进行打包并发送到 EntryPoint 合约
- EntryPoint 合约验证并执行所有 UserOperations
- 打包 UserOperations 的 EOA 钱包将由用户的钱包或 Paymaster 偿还其代表用户支出的 ETH