The CREATE2 opcode was added to the hard fork of Constantinople on February 28 this year. As indicated in the EIP, this opcode was introduced primarily for state channels. However, we used it to solve another problem.
There are users on the exchange with balances. We must provide each user with an Ethereum address to which anyone can send tokens, thereby replenishing their account. Let's call these addresses "wallets." When tokens come to wallets, we must send them to a single wallet (hotwallet).
In the following sections, I analyze solutions to this problem without CREATE2 and explain why we abandoned them. If you are only interested in the end result, you can find it in the “Final Solution” section.
Ethereum Addresses
The easiest solution is to generate new ethereum addresses for new users. These addresses will be wallets. To transfer tokens from a wallet to a hotwallet, you need to sign the transaction by calling the transfer () function with the wallet’s private key from the backend.
This approach has the following advantages:
- it's simple
- the cost of transferring tokens from the wallet to hotwallet is equal to the price of calling the transfer () function
However, we abandoned this approach because it has one significant drawback: you need to store private keys somewhere. And the point is not only that they can be lost, but also that you need to carefully control access to these keys. If at least one of them is compromised, then the tokens of a certain user will not reach a hot wallet.
Create a separate smart contract for each user
Deploying a separate smart contract for each user eliminates the need to store private wallet keys on the server. The exchange will call this smart contract to transfer tokens to the hotwallet.
We also refused this decision, since the user cannot be shown the address of his wallet without deploying a smart contract (this is actually possible, but in a rather complicated way with other shortcomings that we will not discuss here). On the exchange, a user can create as many accounts as he needs, and everyone needs their own wallet. This means that we need to spend money on the deployment of the contract, not even being sure that the user will use this account.
Opcode CREATE2
To fix the problem of the previous method, we decided to use the CREATE2 opcode. CREATE2 allows you to pre-determine the address at which the smart contract will be deployed. The address is calculated using the following formula:
keccak256 (0xff ++ address ++ salt ++ keccak256 (init_code)) [12:]
where:
- address - the address of the smart contract that will call CREATE2
- salt - random value
- init_code - smart contract bytecode for deployment
This ensures that the address that we provide to the user will indeed contain the desired bytecode. In addition, this smart contract can be deployed when we need. For example, when a user decides to use his wallet for the first time.
Moreover, you can calculate the address of a smart contract every time instead of storing it, since:
- The address in the formula is constant, as this is the address of our wallet factory
- salt - hash of user_id
- init_code is persistent since we use the same wallet
More improvements
The previous solution still has one drawback: you have to pay to deploy a smart contract. However, you can get rid of this. To do this, you can call the transfer () function, and then selfdestruct () in the wallet constructor. And then the gas for the deployment of the smart contract will be returned.
Contrary to popular belief, you can deploy a smart contract at the same address several times with the CREATE2 opcode. This is because CREATE2 checks that the nonce of the target address is zero (it is assigned the value “1” at the beginning of the constructor). In this case, the selfdestruct () function resets nonce addresses each time. That way, if you call CREATE2 again with the same arguments, the check for nonce will pass.
Note that this solution is similar to the ethereum address option, but without the need to store private keys. The cost of transferring money from a wallet to a hotwallet is approximately equal to the cost of calling the transfer () function, since we do not pay for the deployment of a smart contract.
Final decision
Initially prepared by:
- function for getting salt by user_id
- a smart contract that will call the CREATE2 opcode with the appropriate salt (i.e. wallet factory)
- wallet byte code corresponding to the contract with the following constructor:
constructor () { address hotWallet = 0x…; address token = 0x…; token.transfer (hotWallet, token.balanceOf (address (this))); selfdestruct (address (0)); }
For each new user, we show his / her wallet address by calculating
keccak256 (0xff ++ address ++ salt ++ keccak256 (init_code)) [12:]
When a user transfers tokens to the corresponding wallet address, our backend sees the Transfer event with the _to parameter equal to the wallet address. At this point, it is already possible to increase the user's balance on the exchange before deploying the wallet.
When a sufficient number of tokens accumulate at the wallet address, we can transfer them all at once to a hotwallet. To do this, the backend calls the function of the smart contract factory, which performs the following actions:
function deployWallet ( uint256) { bytes memory walletBytecode =…; // invoke CREATE2 with wallet bytecode and salt }
Thus, the constructor of the smart wallet contract is called, which transfers all its tokens to the hotwallet address and then self-destructs.
Full code can be found here . Please note that this is not our production code, since we decided to optimize the wallet bytecode and wrote it in the opcodes.
Posted by Pavel Kondratenkov, Ethereum Specialist