交易所账号充值功能需求
- 交易所为每个用户提供充值地址,方便用户充值
- 代币进入充值地址后,再汇总到交易所热钱包
实现方案
为所有用户生成以太坊钱包地址
方案描述
为交易所每个用户生成一个以太坊的钱包地址,交易所统一管理他们的私钥,用户充值直接充值到交易所维护的地址上,然后统一汇总到交易所的热钱包地址。
方案优点
- 简单粗暴
方案缺点
- 私钥的保管问题,容易泄漏用户私钥
- 泄漏后,无法归集用户的token
结论
该方案存在极大的安全隐患,不利于交易所的长期发展。
为用户创建独立的智能合约
方案描述
用户的充值地址为部署智能合约的地址,避免在服务器上存储地址的私钥,代币的归集,通过调用智能合约进行归集。
方案优点
- 避免了在服务器存储私钥
缺点
- 部署合约前,用户没办法显示充值地址
- 部署合约会浪费资金,而且不确定用户是否会用地址进行充值。
结论
该方案虽然可以避免私钥存储在服务器,但是对于交易所来说,成本太高,不是最优的方案。
预计算合约地址
方案描述
通过evm的内部指令 CREATE2 操作码提前计算出要部署的合约地址,CREATE2 是以太坊在2019年2月28号的君士坦丁堡(Constantinople)硬分叉**[1]中引入 的一个新操作码。根据EIP1014[2]**CREATE2操作码引入,主要是用于状态通道,这里我们用于解决交易所钱包的问题。
地址计算公式如下:
1 | keccak256 (0xff ++ address ++ salt ++ keccak256 (init_code)) [12:] |
参数说明:
- address:调用CREATE2的智能合约地址
- salt:随机数
- init_code:要部署合约的字节码(可以保证提供给用户的合约地址中包含了期望的合约字节码)
这样,我们合约可以在需要的时候才部署。例如,当用户决定使用钱包时。我们可以随时计算合约地址,因为上述参数对于每个用户来说是固定的,address是常量,salt是user_id的哈希值,init_code也是常量。
1 | 疑问1:这里合约地址可以预生成,如果知道规律是不是每个人都可以生成?如何保证生成合约地址所属权是交易所的? |
方案改进
上面的解决方案仍然有一个缺陷:交易所需要付费部署智能合约。但是,这是可以避免的。可以在合约构造函数中调用transfer()
函数,然后调用selfdestruct()
。这将退还部署智能合约部分的gas。与常见错误认识相反,其实你可以使用CREATE2操作码在同一地址多次部署智能合约。这是因为CREATE2检查目标地址的 nonce 是否为零(它会在构造函数的开头将其设置为1)。在这种情况下,selfdestruct()
函数每次都会重置地址的 nonce。因此,如果再次使用相同的参数调用CREATE2创建合约,对nonce的检查是可以通过的。
这个解决方案类似于使用以太坊地址的方案,但是无需存储私钥。因为我们不支付智能合约部署费用,所以将钱从充值地址到热钱包的成本大约等于调用transfer()
函数的成本。
最终方案
● 通过user_id获取随机值(salt)的函数
● 调用CREATE2操作码(使用适当的随机数)的智能合约
● 具有如下构造函数的充值钱包合约的字节码:
1 | constructor () { |
对于每个新用户,我们通过下面的公式计算其充值钱包地址:
1 | keccak256 (0xff ++ fabric_addr ++ hash (user_id) ++ keccak256 (wallet_init_code)) [12:] |
当用户将代币转入其充值钱包地址时,后台系统会监控到 Transfer事件,并且目标参数( _to )是充值地址。此时,在实际部署充值钱包合约前,已经可以增加用户在交易所的余额了。
1 | 疑问:预先生成的地址,用户如果先转账,而合约没创建或者监听过程中程序异常,这种情况用户的token去哪里了? |
当用户充值钱包中累积了足够的代币时,我们就可以将所有币一次性转入平台热钱包。为此,后台调用工厂合约的如下方法:
1 | function deployWallet (uint256 salt) { |
此时充值钱包智能合约的构造函数被调用,这会将所有代币转入热钱包然后自动销毁。
以下是完整代码:
1 | // Note that this is not the production code |
注意,这不是我们的生产环境代码,因为我们还要优化钱包合约的字节码,并且使用操作码编写了。