这是一篇对实现一个加密货币支付平台的技术方案的基础的思考和调研,思考之所以排在调研前是因为先想到了加密货币实现在线支付的场景,然后通过调研验证了自己的想法。
写这篇文章的起源是,是想给自己规划的个人项目加在线支付的场景,比如微信支付、支付宝、Paypal等,但个人非公司主体在如何简单快速实现支付的对接时让我有了一些思考,既然链上的账本交易是公开的,是不是可以实现加密货币的支付系统呢?细想来链路是通的实现并不复杂。然后又扩展想到如何实现一个加密货币聚合支付的平台服务商,带着这个疑问梳理出了下文的内容。
又是偶然的机会在 TradingView 的付费流程里看到了加密支付的选项,顺着又发现了 Coinbase 提供的支付商户服务 Coinbase Commerce,通过简单的体验了流程基本上验证了我的想法,当然类似的服务还有 BitPay。
关键词:#加密货币 #聚合支付 #支付商户服务平台 #支付服务商
名词解释:用户、商户、支付服务商(下文会围绕这三者展开,即用户在商户电商系统通过支付服务商发起加密货币支付)
如何实现商户的收款与确认收款
开始这个话题前我们先看看如何往交易所进行充值的(以往币安里充USDT为例进行说明)
- 币安给用户现货账户的币种对应网络创建对应收款账户(如
USDT
对应TRX
转账网络)- 目的在于确认链上的一笔交易是指向到特定用户的(即确认收款方身份)
- 这个收款账户的控制方是币安,但该地址与特定用户一对一关联
- 当然这个账户并非是外部账户, 而是一个支持收款的合约账户(通过
CREATE2
预生成)
- 从币安复制
USDT
充值地址 - 从外部(其他交易所或钱包)将
USDT
对应TRX
网络发起转账 - 币安在USDT钱包地址(充值地址)中检测到成功的交易
- 币安在收款账户对应用户的现货账户的
USDT
币种增加余额 - 币安定期将分散的临时地址中的余额汇总到热钱包中(创建合约然后再
selfdestruct
销毁合约)
我们实现商户的收款与确认收款与交易所的流程类似,但会有一个重要的差异
- 交易所是给用户维度(用户对应币种和网络)创建的钱包地址,来保障一个钱包地址的一笔/多笔充值均可以关联到该用户
- 如果按交易所充值的做法来实现商户的收款,在商户维度创建钱包地址,但同一个商户地址会同时接受不同用户的付款交易,那商户就无法确认交易与用户的关系
- 中心化支付服务商(如
Alipay/Paypal
)的做法是在交易信息中增加「商户订单号」的形式来确权的 - 所以需要在支付单维度为每笔订单生成唯一收款账户,这样就保障了链上交易记录与订单挂钩,订单又与用户挂钩
所以此时商户收款与确认收款的流程应该是这样的
- 用户在商户侧购买商品并发起结算生成加密支付的交易单,跳转至聚合支付服务商收银台
- 用户选择需要支付的加密货币类型(如
ERC20
网络的USDT
) - 支付服务商为商户的该笔交易单创建唯一的收款地址(合约地址)
- 支付服务商开始监测该收款地址的变动
- 用户向交易单对应的钱包地址进行转账
- 支付服务商监测到该地址产生了已确认的交易后,向商户进行支付通知
- 商户侧收到支付通知后标记订单支付
如何实现资金的入账与分佣
涉及到两个话题,一个是资金安全,另一个是商户的提现流程如何设计,这两个话题之所以一起描述是因为有很紧密的关系
- 资金安全,或者理解为如何做到商户对支付平台的单向信任,说的直白一点就是钱你平台收了,那会不会跑路或者你的钱包私钥泄露了怎么办
- 资金入账,即支付服务商收的款项以什么样的流程转移到商家自己控制的钱包里
关于收款账户:数字货币的收款流程和收款账户
收款账户的地址可以选择外部账户(Externally-owned account
) 和合约账户(Contract account
), 我们对比一下选出最优的
- 外部账户:为每一笔交易创建一个外部账户,支付服务商需要托管账户的私钥
- 合约账户:为每一笔交易创建一个智能合约并用合约地址作为收款地址,避免保存地址的私钥信息,通过调用智能合约进行资金转移
显然,方案2从资金安全考虑是最优的,但为每一笔交易都部署合约显然是昂贵的,所以最优的方式是通过CREATE2
操作码预计算出合约地址,通过此地址先进行收款,等需要转移资金时再部署合约
关于入账与分佣:收入的数字货币如何转移到商户的钱包里
前文提到为每一笔交易预创建一个合约地址收的资金,会有两个流向:商户的钱包(入账)、服务商的钱包(分佣),下面讲解如何将合约钱包中的数字货币进行转移的
- 为预生成的地址实际创建合约,合约中标明资金的转移规则
- 按平台佣金规则计算出的佣金,转账到平台账户
- 剩余的资金转账到商户账户
- 销毁合约(通过自毁函数
selfdestruct
),这将退还部署智能合约部分的Gas
费
此时资金已经成功入账和分佣,但是这种方式安全吗?我们会在合约的构造方法里加上入账方的地址参数,这样确保了接受者始终是我们预期的,这样无论如何攻击者都无法使用任何收款来获取资金的控制权。
关于账户安全:商户对资金的担忧或信任基础建立在对资金账户的绝对控制权
一笔交易完成后,在很短的时间里会通过智能合约进行资金转移,即资金会入账到商户自己的钱包里,如何保障钱包的安全至关重要
- 在商户在支付服务商平台首次注册时,在客户端生成钱包(含地址、助记词、私钥),私钥信息绝对不会发送到服务商的服务端
- 当然此时秘钥等信息的安全备份会是商家的重点,否则损失无法挽回
- 也可以有加密备份私钥的方式可选,服务商只存储加密密钥,做双重保险(如
Coinbase Commerce
提供的将加密版本的私钥存储到Google Drive
的方式)
- 服务商仅将钱包地址存储以进行资金的转入
- 商户提现,当然此时钱包就是自己也不存在提现一说了,如果就是想将服务商帮创建的钱包余额转移到自己的钱包中
- 商户可以将助记词导入到钱包工具中自行转账
- 如果需要服务商平台执行,那就需要商户通过自行上传的秘钥在客户端进行对交易进行签名,由服务商把签名后的交易广播出去
如何通过代码验证收款与入账流程
1. 首先创建 Wallet 合约,用于调用此合约创建收款账户来引导用户支付
这里部署在以太坊测试网上的合约地址为 :0x4d3e2C755aCCeDb752878b2E025Bdfa2cFE7116C
CREATE2 定义了新的账户生成方式:keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]
(其中 Account a = new Account{salt: _salt}(_to);
是 Solidity 0.6.2
加入的支持 CREATE2
的语法糖)。
2. 接下来单独编译 Account 合约得到 ByteCode 和 ABI数据, 用于获得创建收款账户的必要参数
3. 预生成收款合约地址
4. 发起往合约地址的付款
- Transaction Details:https://rinkeby.etherscan.io/tx/0x3a7027b6c6336f50d71a02eda18e1319f00ccc150912ce52f6ff92c97b4fbf0a
- Contract Address:https://rinkeby.etherscan.io/address/0xdebedc5e19376c4c2c8b365d47cf7dc031cd7a7a
5. 创建Account智能合约实现资金转移
- 通过调用
Wallet
合约的create
方法来创建Account
合约:https://rinkeby.etherscan.io/address/0x4d3e2C755aCCeDb752878b2E025Bdfa2cFE7116C#writeContract Account
合约创建后在构造方法中直接实现了资金的转移,无需显示调用,验证合约的资金流向:https://rinkeby.etherscan.io/address/0xdebedc5e19376c4c2c8b365d47cf7dc031cd7a7a#internaltx
https://gist.github.com/JingwenTian/f9ce513157bd540511e2577914092304
最后,其实还有很多细节问题没有涉及到,比如小额支付时的佣金无法覆盖创建合约和转账的Gas成本、用户多支付的退款流程、用户商户侧主动发起退款流程、在支付服务中币价波动风险及应对策略等的思考,以后有机会再和大家分享。
感谢阅读!如果有问题交流,可以关注并私信我:微信(jingwentian)、Twitter(@0xDaotian)、微信公众号(北极之野)、Substack邮件订阅(文叔白话WEB3)。