通过CREATE2获得合约地址:解决交易所充值账号问题

Seven 2022-01-25 17:44:18
Categories: > > Tags:

交易所账号充值功能需求

实现方案

为所有用户生成以太坊钱包地址

方案描述

为交易所每个用户生成一个以太坊的钱包地址,交易所统一管理他们的私钥,用户充值直接充值到交易所维护的地址上,然后统一汇总到交易所的热钱包地址。

方案优点
方案缺点
结论

该方案存在极大的安全隐患,不利于交易所的长期发展。

为用户创建独立的智能合约

方案描述

用户的充值地址为部署智能合约的地址,避免在服务器上存储地址的私钥,代币的归集,通过调用智能合约进行归集。

方案优点
缺点
结论

该方案虽然可以避免私钥存储在服务器,但是对于交易所来说,成本太高,不是最优的方案。

预计算合约地址

方案描述

通过evm的内部指令 CREATE2 操作码提前计算出要部署的合约地址,CREATE2 是以太坊在2019年2月28号的君士坦丁堡(Constantinople)硬分叉**[1]中引入 的一个新操作码。根据EIP1014[2]**CREATE2操作码引入,主要是用于状态通道,这里我们用于解决交易所钱包的问题。

地址计算公式如下:

1
keccak256 (0xff ++ address ++ salt ++ keccak256 (init_code)) [12:]

参数说明:

这样,我们合约可以在需要的时候才部署。例如,当用户决定使用钱包时。我们可以随时计算合约地址,因为上述参数对于每个用户来说是固定的,address是常量,salt是user_id的哈希值,init_code也是常量。

1
2
疑问1:这里合约地址可以预生成,如果知道规律是不是每个人都可以生成?如何保证生成合约地址所属权是交易所的?
答案:
方案改进

上面的解决方案仍然有一个缺陷:交易所需要付费部署智能合约。但是,这是可以避免的。可以在合约构造函数中调用transfer()函数,然后调用selfdestruct()。这将退还部署智能合约部分的gas。与常见错误认识相反,其实你可以使用CREATE2操作码在同一地址多次部署智能合约。这是因为CREATE2检查目标地址的 nonce 是否为零(它会在构造函数的开头将其设置为1)。在这种情况下,selfdestruct()函数每次都会重置地址的 nonce。因此,如果再次使用相同的参数调用CREATE2创建合约,对nonce的检查是可以通过的。

这个解决方案类似于使用以太坊地址的方案,但是无需存储私钥。因为我们不支付智能合约部署费用,所以将钱从充值地址到热钱包的成本大约等于调用transfer()函数的成本。

最终方案

● 通过user_id获取随机值(salt)的函数

● 调用CREATE2操作码(使用适当的随机数)的智能合约

● 具有如下构造函数的充值钱包合约的字节码:

1
2
3
4
5
6
constructor () {
address hotWallet = 0x …;
address token = 0x …;
token.transfer (hotWallet, token.balanceOf (address(this)));
selfdestruct (address (0));
}

对于每个新用户,我们通过下面的公式计算其充值钱包地址:

1
keccak256 (0xff ++ fabric_addr ++ hash (user_id) ++ keccak256 (wallet_init_code)) [12:]

当用户将代币转入其充值钱包地址时,后台系统会监控到 Transfer事件,并且目标参数( _to )是充值地址。此时,在实际部署充值钱包合约前,已经可以增加用户在交易所的余额了。

1
2
疑问:预先生成的地址,用户如果先转账,而合约没创建或者监听过程中程序异常,这种情况用户的token去哪里了?
答案:

当用户充值钱包中累积了足够的代币时,我们就可以将所有币一次性转入平台热钱包。为此,后台调用工厂合约的如下方法:

1
2
3
4
function deployWallet (uint256 salt) {
bytes memory walletBytecode = …;
// 用充值钱包合约的字节码及 salt 调用 CREATE2
}

此时充值钱包智能合约的构造函数被调用,这会将所有代币转入热钱包然后自动销毁。

以下是完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Note that this is not the production code
pragma solidity 0.5.6;

import "./IERC20.sol";

contract Wallet {
address internal token = 0x123...<hot_wallet_addr>;
address internal hotWallet = 0x321...<hot_wallet_addr>;

constructor() public {
// send all tokens from this contract to hotwallet
IERC20(token).transfer(
hotWallet,
IERC20(token).balanceOf(address(this))
);
// selfdestruct to receive gas refund and reset nonce to 0
selfdestruct(address(0x0));
}
}

contract Fabric {
function createContract(uint256 salt) public {
// get wallet init_code
bytes memory bytecode = type(Wallet).creationCode;
assembly {
let codeSize := mload(bytecode) // get size of init_bytecode
let newAddr := create2(
0, // 0 wei
add(bytecode, 32), // the bytecode itself starts at the second slot. The first slot contains array length
codeSize, // size of init_code
salt // salt from function arguments
)
}
}
}

注意,这不是我们的生产环境代码,因为我们还要优化钱包合约的字节码,并且使用操作码编写了。

附言

参考资料